# 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 }}