From 5e89056f629e95cd73e3f896b74d947fc272069b Mon Sep 17 00:00:00 2001 From: Cobb Date: Fri, 19 Jun 2026 20:18:32 -0700 Subject: [PATCH] =?UTF-8?q?ci:=20Forgejo=20build=20workflow=20=E2=80=94=20?= =?UTF-8?q?per-repo=20straw-build=20image,=20gated=20auto-publish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build the Straw APK in CI from a dedicated, ephemeral build container (git.sulkta.com/sulkta-infra/straw-build — Android SDK/NDK + Rust + cargo-ndk, see ci/Dockerfile) instead of the persistent crafting-table. The runner spins the container up per job and tears it down after. On push to main (after the build passes + the signer fingerprint is verified against the canonical key) it publishes to fdroid.sulkta.com: APK into the Lucy repo + index re-sign via the host docker socket, then the signed repo streamed to Rackham web168 over a scoped forced-command deploy key. Keystore + deploy key are Forgejo repo secrets. Build steps run under `ionice -c3 nice` so they can't I/O-starve the live DBs on Lucy. --- .forgejo/workflows/build.yml | 115 +++++++++++++++++++++++++++++++++++ ci/Dockerfile | 63 +++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 .forgejo/workflows/build.yml create mode 100644 ci/Dockerfile diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 000000000..9cd8deadd --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,115 @@ +# Straw APK — build on Forgejo Actions, and (gated on a green build) publish to +# fdroid.sulkta.com so the in-app updater picks it up. +# +# Runs in the dedicated git.sulkta.com/sulkta-infra/straw-build image (Android +# SDK/NDK + Rust + cargo-ndk, see ci/Dockerfile). The signing key comes from +# the STRAW_SIGNING_KEYSTORE_B64 secret (the same androiddebugkey the whole +# series is signed with — vaulted as "Sulkta — Straw fdroid signing keystore"). +# +# Publish path mirrors scripts/publish-fdroid.sh: drop the APK into the Lucy +# fdroid repo + re-sign the index (both via the host docker socket, keystore +# never leaves Lucy), then stream the signed repo to Rackham web168 over a +# scoped, forced-command deploy key (STRAW_FDROID_RACKHAM_KEY). +name: build-apk + +on: + push: + branches: [main] + paths: + - 'strawApp/**' + - 'rust/**' + - 'buildSrc/**' + - 'gradle/**' + - 'gradlew' + - '*.gradle.kts' + - 'gradle.properties' + - 'ci/Dockerfile' + - '.forgejo/workflows/build.yml' + workflow_dispatch: + +jobs: + build-and-publish: + runs-on: lucy-rust + container: + image: git.sulkta.com/sulkta-infra/straw-build:latest + steps: + - name: Checkout straw + uses: actions/checkout@v4 + with: + path: straw + + # strawcore is consumed by rust/strawcore via `path = "../../../strawcore"`, + # i.e. a sibling of the straw checkout — so it MUST live next to it. + - name: Checkout strawcore (sibling) + uses: actions/checkout@v4 + with: + repository: Sulkta-OSS/strawcore + ref: main + path: strawcore + + - name: Decode signing keystore + env: + KS_B64: ${{ secrets.STRAW_SIGNING_KEYSTORE_B64 }} + run: echo "$KS_B64" | base64 -d > "$GITHUB_WORKSPACE/straw.keystore" + + - name: Assemble debug APK + working-directory: straw + env: + STRAW_KEYSTORE_FILE: ${{ github.workspace }}/straw.keystore + STRAW_KEYSTORE_PASS: android + STRAW_KEY_ALIAS: androiddebugkey + STRAW_KEY_PASS: android + # Keep the 4-ABI cross-compile off the container rootfs. + CARGO_TARGET_DIR: ${{ github.workspace }}/cargo-target + # ionice idle class + low nice so the build yields disk/CPU to the + # live DBs on Lucy — a 5GB build I/O-starved the MAS Postgres + # checkpoint and dropped Matrix for ~2min on 2026-06-19. + run: ionice -c3 nice -n 19 ./gradlew :strawApp:assembleDebug --no-daemon --stacktrace + + - name: Verify signer + stage APK + working-directory: straw + run: | + set -euo pipefail + VC=$(grep STRAW_VERSION_CODE buildSrc/src/main/kotlin/ProjectConfig.kt | grep -o '[0-9]\+') + APK=$(ls strawApp/build/outputs/apk/debug/*.apk | head -1) + NAME="com.sulkta.straw.debug_${VC}.apk" + cp "$APK" "$GITHUB_WORKSPACE/$NAME" + echo "Built vc=$VC -> $NAME" + # The whole series is signed with SHA-1 bb9ca96b...; fail loudly if a + # build ever produces a different signer (would break in-place updates). + FP=$("$ANDROID_HOME/build-tools/34.0.0/apksigner" verify --print-certs "$APK" | grep -i 'SHA-1' | grep -o '[0-9a-f]\{40\}') + echo "signer SHA-1: $FP" + if [ "$FP" != "bb9ca96b10ebbc1ac48e037a21f350415d18915f" ]; then + echo "::error::APK signer $FP != canonical key — refusing to publish"; exit 1 + fi + echo "STRAW_VC=$VC" >> "$GITHUB_ENV" + echo "STRAW_APK=$NAME" >> "$GITHUB_ENV" + + # ---- Publish: only on a real push to main, AFTER the build above passed ---- + - name: Publish to fdroid (Lucy repo + index re-sign, via host docker socket) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + set -euo pipefail + FDROID=git.sulkta.com/sulkta-infra/fdroid-sulkta:latest + # Stream the APK into the host-mounted fdroid repo (helper runs as the + # fdroid uid so ownership matches). + docker run --rm -i -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta/repo:/repo \ + --entrypoint sh "$FDROID" -c "cat > /repo/${STRAW_APK}" < "$GITHUB_WORKSPACE/${STRAW_APK}" + # Re-sign the index (the index keystore lives only on Lucy). + docker run --rm -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta:/repo "$FDROID" update + + - name: Publish to Rackham (rsync signed repo to web168) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + RACKHAM_KEY: ${{ secrets.STRAW_FDROID_RACKHAM_KEY }} + run: | + set -euo pipefail + mkdir -p ~/.ssh && echo "$RACKHAM_KEY" > ~/.ssh/id_deploy && chmod 600 ~/.ssh/id_deploy + ssh-keyscan -H 142.44.213.229 >> ~/.ssh/known_hosts 2>/dev/null + FDROID=git.sulkta.com/sulkta-infra/fdroid-sulkta:latest + # tar the signed repo out of the host-mounted volume and stream it to + # the forced-command deploy key on Rackham (which untars + sudo-rsyncs). + docker run --rm -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta:/src \ + --entrypoint sh "$FDROID" -c 'tar c -C /src repo' \ + | ssh -i ~/.ssh/id_deploy kayos@142.44.213.229 + echo "Published vc=${STRAW_VC} to fdroid.sulkta.com" diff --git a/ci/Dockerfile b/ci/Dockerfile new file mode 100644 index 000000000..a0b875f48 --- /dev/null +++ b/ci/Dockerfile @@ -0,0 +1,63 @@ +# Sulkta straw-build — reproducible Android + Rust build image for the Straw APK. +# +# Pushed to git.sulkta.com/sulkta-infra/straw-build:latest and used as the job +# `container:` in .forgejo/workflows/build.yml. It bakes the toolchain that +# otherwise lives only in bind-mounts on the long-running crafting-table, so a +# FRESH Forgejo CI job container is fully self-contained (no host /caches +# dependency, no per-machine signing key). +# +# Toolchain pinned to exactly what builds vc=72 successfully: +# JDK 21 · NDK 27.2.12479018 · build-tools 34.0.0 · platforms android-36 +# Rust stable + 4 Android targets · cargo-ndk · clang/libclang (rquickjs bindgen) +FROM eclipse-temurin:21-jdk-jammy + +ENV DEBIAN_FRONTEND=noninteractive \ + ANDROID_SDK_ROOT=/opt/android-sdk \ + ANDROID_HOME=/opt/android-sdk \ + ANDROID_NDK_HOME=/opt/android-sdk/ndk/27.2.12479018 \ + CARGO_HOME=/opt/cargo \ + RUSTUP_HOME=/opt/rustup \ + PATH=/opt/cargo/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/usr/bin:/bin + +# Base OS deps: clang/libclang for rquickjs bindgen, a C toolchain for the +# QuickJS C sources, unzip for the SDK zips, git+ca-certs for checkout. +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl unzip ca-certificates clang libclang-dev build-essential pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# Android cmdline-tools + the SDK packages the build needs. +ARG CMDLINE_TOOLS_ZIP=commandlinetools-linux-11076708_latest.zip +RUN mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" \ + && curl -fsSL -o /tmp/cmdtools.zip "https://dl.google.com/android/repository/${CMDLINE_TOOLS_ZIP}" \ + && unzip -q /tmp/cmdtools.zip -d "$ANDROID_SDK_ROOT/cmdline-tools" \ + && mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest" \ + && rm /tmp/cmdtools.zip \ + && yes | sdkmanager --licenses >/dev/null 2>&1 \ + && sdkmanager --install \ + "platform-tools" \ + "platforms;android-36" \ + "build-tools;34.0.0" \ + "ndk;27.2.12479018" >/dev/null \ + && rm -rf "$ANDROID_SDK_ROOT/.temp" /tmp/* + +# Rust toolchain + the four Android targets + cargo-ndk. +RUN curl -fsSL https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal \ + && rustup target add \ + aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android \ + && cargo install cargo-ndk --locked \ + && rm -rf /opt/cargo/registry/cache /opt/cargo/registry/src + +# Sanity: fail the image build early if anything's missing. +RUN java -version && cargo --version && cargo ndk --version || true \ + && test -d "$ANDROID_NDK_HOME" && test -d "$ANDROID_SDK_ROOT/build-tools/34.0.0" + +# Publish tooling (appended last so the heavy toolchain layers stay cached): +# docker CLI to talk to the runner's host socket for the fdroid steps, and +# openssh-client to stream the signed repo to Rackham. The build steps don't +# touch the socket; only the gated publish step does. +RUN apt-get update && apt-get install -y --no-install-recommends \ + docker.io openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# The signing keystore is NOT baked — it's injected per-build from the Forgejo +# secret STRAW_SIGNING_KEYSTORE_B64 → STRAW_KEYSTORE_FILE.