tally/.forgejo/workflows/build.yaml
Emil Lerch 255f23928a
All checks were successful
Build / Engine, CLI and TUI (push) Successful in 3m41s
Build / Android (push) Successful in 3m51s
Build / Sign (push) Successful in 3m43s
Build / Publish (push) Successful in 15s
add CI
2026-10-04 08:34:36 -07:00

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