256 lines
10 KiB
YAML
256 lines
10 KiB
YAML
# Build, test, sign and publish Tally.
|
|
#
|
|
# Every push, any branch: the engine and both terminal frontends are built and tested,
|
|
# the JNI layer is proved against a real JVM, the Android libraries are audited, and the
|
|
# app is built and unit tested. Artifacts are attached to the run.
|
|
#
|
|
# Pushes to master also sign and publish, signing with action-hsm-sign:
|
|
# - the APK, signed (schemes v2 and v3) by the HSM
|
|
# - every release file, with a detached signature logged to sigstore's public
|
|
# transparency log
|
|
# - all of it to the Forgejo generic package registry as `tally`, under the short SHA
|
|
# and under `latest`
|
|
#
|
|
# Secrets: HSM_USER_PIN (signing), PACKAGE_PUSH (registry), NTFY_* (notifications).
|
|
# Runners: ubuntu-latest, and ubuntu-latest-with-hsm, which has the HSM on a smart USB hub
|
|
# (per-port power switching) and runs one job at a time.
|
|
name: Build
|
|
on:
|
|
workflow_dispatch:
|
|
push:
|
|
branches:
|
|
- '*'
|
|
env:
|
|
RELEASE_OPTIMIZATION: ReleaseSafe
|
|
# Pinned and checked, because CI should not take whatever was published today. The
|
|
# same versions as .mise.toml, which is what a developer machine uses.
|
|
ZLINT_VERSION: v0.9.0
|
|
ZLINT_SHA256: 2485f4f744345e4b7c23a6023e96f0a22306d832f64ba3aeba08d35c90f81a71
|
|
GRADLE_VERSION: 8.14.5
|
|
GRADLE_SHA256: 6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
|
|
jobs:
|
|
engine:
|
|
name: Engine, CLI and TUI
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
- name: Setup Zig
|
|
# No version given on purpose: setup-zig resolves it from minimum_zig_version
|
|
# in build.zig.zon, so the toolchain cannot drift from what the package declares.
|
|
uses: https://github.com/mlugg/setup-zig@v2.2.1
|
|
- name: Check formatting
|
|
# Before the build, which fetches dependencies into zig-pkg/ - not ours to format.
|
|
run: zig fmt --check build.zig build.zig.zon build engine src
|
|
- name: Lint
|
|
run: |
|
|
curl -fsSLo "$RUNNER_TEMP/zlint" \
|
|
"https://github.com/DonIsaac/zlint/releases/download/${ZLINT_VERSION}/zlint-linux-x86_64"
|
|
echo "${ZLINT_SHA256} $RUNNER_TEMP/zlint" | sha256sum -c -
|
|
chmod 755 "$RUNNER_TEMP/zlint"
|
|
"$RUNNER_TEMP/zlint" --deny-warnings
|
|
- name: Build project
|
|
run: zig build --summary all
|
|
- name: Run tests
|
|
# Engine, CLI, TUI, the C ABI from Zig, the JNI layer against a synthetic
|
|
# function table, and the C ABI from C through the installed header.
|
|
run: zig build test --summary all
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '21'
|
|
- name: Prove the JNI table against a real JVM
|
|
run: zig build jvm-test --summary all
|
|
- name: Package the CLI and TUI
|
|
# One binary holds both. Static musl on Linux, so it runs on any distribution.
|
|
run: |
|
|
mkdir -p dist
|
|
for target in x86_64-linux-musl aarch64-linux-musl aarch64-macos; do
|
|
zig build -Dtarget="$target" -Doptimize="$RELEASE_OPTIMIZATION" --prefix "out/$target"
|
|
name="tally-${target%-musl}"
|
|
cp "out/$target/bin/tally" "dist/$name"
|
|
done
|
|
ls -la dist
|
|
- name: Upload
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: tally-cli
|
|
path: dist/
|
|
- name: Notify
|
|
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
|
if: always() && env.GITEA_ACTIONS == 'true'
|
|
with:
|
|
host: ${{ secrets.NTFY_HOST }}
|
|
topic: ${{ secrets.NTFY_TOPIC }}
|
|
status: ${{ job.status }}
|
|
user: ${{ secrets.NTFY_USER }}
|
|
password: ${{ secrets.NTFY_PASSWORD }}
|
|
|
|
android:
|
|
name: Android
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v4
|
|
- name: Setup Zig
|
|
uses: https://github.com/mlugg/setup-zig@v2.2.1
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '21'
|
|
- name: Setup Android SDK
|
|
# The packages `mise run android-sdk` installs. No NDK: Zig builds the native
|
|
# library without one.
|
|
uses: https://github.com/android-actions/setup-android@v3
|
|
with:
|
|
packages: 'platform-tools platforms;android-35 build-tools;35.0.0'
|
|
log-accepted-android-sdk-licenses: 'false'
|
|
- name: Setup Gradle
|
|
# No wrapper is committed (android/README.md), so the version comes from here,
|
|
# as it comes from .mise.toml locally.
|
|
run: |
|
|
curl -fsSLo "$RUNNER_TEMP/gradle.zip" \
|
|
"https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip"
|
|
echo "${GRADLE_SHA256} $RUNNER_TEMP/gradle.zip" | sha256sum -c -
|
|
unzip -q "$RUNNER_TEMP/gradle.zip" -d "$RUNNER_TEMP"
|
|
echo "$RUNNER_TEMP/gradle-${GRADLE_VERSION}/bin" >> "$GITHUB_PATH"
|
|
- name: Build the native libraries
|
|
# Gradle never invokes Zig, so this comes first or the APK packages nothing.
|
|
run: zig build android --summary all
|
|
- name: Audit the native libraries
|
|
# No undefined symbols, no TLS, no DT_NEEDED: each one loads on a desktop and
|
|
# fails in dlopen on a phone.
|
|
run: |
|
|
command -v readelf >/dev/null || { sudo apt-get update && sudo apt-get install -y binutils; }
|
|
android/audit-libs.sh zig-out/android
|
|
- name: Unit test and build the app
|
|
working-directory: android
|
|
# The release APK is unsigned here; signing happens on the HSM runner. Lint's
|
|
# release checks run as part of assembleRelease.
|
|
run: gradle --no-daemon testDebugUnitTest assembleDebug assembleRelease
|
|
- name: Upload the unsigned release APK
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: tally-apk-unsigned
|
|
path: android/app/build/outputs/apk/release/app-release-unsigned.apk
|
|
- name: Upload the debug APK
|
|
# Signed with this runner's throwaway debug key, so it installs only where no
|
|
# other build of the app is installed. For trying a branch, not for keeping.
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: tally-apk-debug
|
|
path: android/app/build/outputs/apk/debug/app-debug.apk
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: android-test-results
|
|
path: android/app/build/test-results/
|
|
- name: Notify
|
|
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
|
if: always() && env.GITEA_ACTIONS == 'true'
|
|
with:
|
|
host: ${{ secrets.NTFY_HOST }}
|
|
topic: ${{ secrets.NTFY_TOPIC }}
|
|
status: ${{ job.status }}
|
|
user: ${{ secrets.NTFY_USER }}
|
|
password: ${{ secrets.NTFY_PASSWORD }}
|
|
|
|
sign:
|
|
name: Sign
|
|
# Only what is published is signed: the HSM is hardware on a hub, not something to
|
|
# cycle for every branch push.
|
|
if: github.ref == 'refs/heads/master'
|
|
runs-on: ubuntu-latest-with-hsm
|
|
needs: [engine, android]
|
|
steps:
|
|
- name: Download the binaries
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: tally-cli
|
|
path: dist
|
|
- name: Download the unsigned APK
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: tally-apk-unsigned
|
|
path: unsigned
|
|
- name: Sign the APK and every release file
|
|
# The APK first, then a detached signature of every file in dist/, the signed APK
|
|
# included, each logged to sigstore. One power cycle of the HSM for all of it.
|
|
# The APK's signature is the app's identity on every device that installs it, for
|
|
# good: the key is the HSM's, and its certificate is the one `make-cert` wrote to
|
|
# the token (README, "Signing").
|
|
id: sign
|
|
uses: https://git.lerch.org/lobo/action-hsm-sign@v3
|
|
with:
|
|
pin: ${{ secrets.HSM_USER_PIN }}
|
|
apk: unsigned/app-release-unsigned.apk
|
|
apk_output: dist/tally.apk
|
|
files: dist/*
|
|
public_key: 'https://emil.lerch.org/serverpublic.pem'
|
|
uhub_control: 'true'
|
|
- name: Transparency log entries
|
|
run: |
|
|
echo "${{ steps.sign.outputs.SOURCE_0 }}: ${{ steps.sign.outputs.URL_0 }}"
|
|
echo "${{ steps.sign.outputs.SOURCE_1 }}: ${{ steps.sign.outputs.URL_1 }}"
|
|
echo "${{ steps.sign.outputs.SOURCE_2 }}: ${{ steps.sign.outputs.URL_2 }}"
|
|
echo "${{ steps.sign.outputs.SOURCE_3 }}: ${{ steps.sign.outputs.URL_3 }}"
|
|
- name: Upload
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: tally-release
|
|
path: dist/
|
|
- name: Notify
|
|
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
|
if: always()
|
|
with:
|
|
host: ${{ secrets.NTFY_HOST }}
|
|
topic: ${{ secrets.NTFY_TOPIC }}
|
|
status: ${{ job.status }}
|
|
user: ${{ secrets.NTFY_USER }}
|
|
password: ${{ secrets.NTFY_PASSWORD }}
|
|
|
|
publish:
|
|
name: Publish
|
|
if: github.ref == 'refs/heads/master'
|
|
runs-on: ubuntu-latest
|
|
needs: sign
|
|
steps:
|
|
- name: Download the signed release
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: tally-release
|
|
path: dist
|
|
- name: Publish to the Forgejo generic package registry
|
|
env:
|
|
PACKAGE_PUSH: ${{ secrets.PACKAGE_PUSH }}
|
|
ACTOR: ${{ github.actor }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
set -eu
|
|
shortsha="$(printf '%s' "$SHA" | cut -c1-7)"
|
|
base="https://git.lerch.org/api/packages/${OWNER}/generic/tally"
|
|
cd dist
|
|
for version in "$shortsha" latest; do
|
|
for file in *; do
|
|
url="${base}/${version}/${file}"
|
|
# The registry refuses a second PUT of the same file (409), so `latest`
|
|
# is replaced by delete-then-put. A 404 for a new short SHA is harmless.
|
|
curl -sS -o /dev/null -w "DELETE %{http_code} ${url}\n" \
|
|
-u "${ACTOR}:${PACKAGE_PUSH}" -X DELETE "${url}" || true
|
|
curl -sSf -u "${ACTOR}:${PACKAGE_PUSH}" --upload-file "${file}" "${url}"
|
|
echo "uploaded ${url}"
|
|
done
|
|
done
|
|
- name: Notify
|
|
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
|
if: always()
|
|
with:
|
|
host: ${{ secrets.NTFY_HOST }}
|
|
topic: ${{ secrets.NTFY_TOPIC }}
|
|
status: ${{ job.status }}
|
|
user: ${{ secrets.NTFY_USER }}
|
|
password: ${{ secrets.NTFY_PASSWORD }}
|