straw/.forgejo/workflows/build.yml
Cobb 737eb50cf4 diag: remove sending logs to a server
The owner retired the log service the app sent to, so the "Send logs"
button in Settings and the send from the crash handler go, with
everything that existed only for them: LogShipper, the crash-handler
install in StrawApp, the build-time token (its read in
strawApp/build.gradle.kts, the BuildConfig field and the CI env line),
LogDump.captureScrubbedLinesBlocking and the second, lighter scrub
profile that only the shipper used.

Kept, because other code uses it: LogDump.capture and scrubLine (the
"Export logs" share sheet and the on-screen error strings), strawLogI,
and BuildConfig itself. LogDumpScrubTest now covers the one profile
left (still 10 tests).

Measured by the builder with this same patch applied to e7ba6bcc3 (the
parent of the test-only commit b0a5e7213), not yet on this commit's
parent: 46 of 46 unit tests pass; an old-versus-new
comparison of the scrubber over 48,212 log lines found 0 differences;
assembleDebug passes with the token unset and with a placeholder set;
a search of the built APK finds no trace of the log host, the ingest
URL, the token or the button text, where a control build of the parent
commit has them.
2026-10-22 17:29:44 -07:00

179 lines
9.1 KiB
YAML

# 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 is ROOTLESS (#444): the signed APK is handed to a forced-command
# script on the Lucy host (key STRAW_FDROID_LUCY_KEY, pinned to that one script
# via restrict,command=). The host does the fdroid index re-sign + the Rackham
# rsync — the signing keystore never enters this CI container, and this job no
# longer needs the host docker socket. The host script re-verifies the APK
# signer before publishing.
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
# Run every step with bash, not the runner's default dash — the steps use
# `set -euo pipefail` (pipefail is a bashism; dash errors "Illegal option").
# The straw-build image ships /usr/bin/bash.
defaults:
run:
shell: bash
container:
image: git.sulkta.com/sulkta-infra/straw-build:latest
steps:
# We clone with plain git instead of actions/checkout@v4: that action is
# a Node action, and the straw-build job container ships the Android +
# Rust toolchain but NOT node — so checkout@v4 dies with
# `exec: "node": not found`. git is in the image, both repos are public,
# and a shell clone also sidesteps the runner's flaky data.forgejo.org
# action fetch. strawcore must be a SIBLING of straw because
# rust/strawcore depends on it via `path = "../../../strawcore"`.
- name: Checkout straw + strawcore (sibling, no JS actions)
run: |
set -euo pipefail
git clone https://git.sulkta.com/Sulkta-OSS/straw.git straw
git -C straw checkout --detach "${{ github.sha }}"
git clone --depth 1 https://git.sulkta.com/Sulkta-OSS/strawcore.git strawcore
echo "straw: $(git -C straw rev-parse --short HEAD)"
echo "strawcore: $(git -C strawcore rev-parse --short HEAD)"
- name: Decode signing keystore
env:
KS_B64: ${{ secrets.STRAW_SIGNING_KEYSTORE_B64 }}
run: echo "$KS_B64" | base64 -d > "$GITHUB_WORKSPACE/straw.keystore"
# Auto-derive versionCode from the main commit count so a release can
# NEVER silently reuse a code — the 2026-07-29 trap where a commit that
# didn't hand-bump ProjectConfig rebuilt the same debug_<vc>.apk, the
# publish receiver's anti-downgrade guard refused the duplicate (exit 3 =
# "nothing to do"), and the CI went green having shipped nothing. The
# OFFSET keeps the number continuous with the manual era (last hand-set
# code was 91). Full clone above ⇒ rev-list --count is the true count.
- name: Auto versionCode from commit count
working-directory: straw
run: |
set -euo pipefail
COUNT=$(git rev-list --count HEAD)
VC=$(( COUNT - 12208 ))
sed -i "s/^const val STRAW_VERSION_CODE = .*/const val STRAW_VERSION_CODE = $VC/" \
buildSrc/src/main/kotlin/ProjectConfig.kt
grep -q "STRAW_VERSION_CODE = $VC\$" buildSrc/src/main/kotlin/ProjectConfig.kt \
|| { echo "::error::versionCode auto-patch failed (sed matched nothing)"; exit 1; }
echo "auto versionCode = $VC (commit count $COUNT)"
# The unit tests run before the APK is built, so that a failing test
# fails the job before anything is built, signed or published. Neither
# step has a secret in its environment. Both use the build step's
# target dir (off the container rootfs) and its low CPU and I/O priority.
- name: Rust unit tests (strawcore)
working-directory: straw/rust
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/cargo-target
run: ionice -c3 nice -n 19 cargo test -p strawcore --lib
- name: JVM unit tests
working-directory: straw
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/cargo-target
run: ionice -c3 nice -n 19 ./gradlew :strawApp:testDebugUnitTest --no-daemon --stacktrace
- 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).
# apksigner lives under build-tools/<ver>/. AGP (compileSdk 36) needs
# build-tools 36; older straw-build images pre-installed only 34.0.0 and
# AGP auto-fetched 36 at build time, so this sort -V | tail -1 resolves to
# 36.0.0's apksigner. (ci/Dockerfile now pre-installs 36 too — see there.)
# apksigner is a shell wrapper that needs `java` on PATH; the image
# sets JAVA_HOME but doesn't put its bin on PATH for run steps (gradle
# uses JAVA_HOME directly, so the build itself is fine).
export PATH="$JAVA_HOME/bin:$PATH"
APKSIGNER=$(ls "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
FP=$("$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 (rootless, no docker socket): hand the signed APK to the
# Lucy host forced-command. The host re-verifies the signer, re-signs
# the fdroid index (keystore stays on Lucy), and rsyncs to Rackham. ----
- name: Publish to fdroid via Lucy host forced-command
# Publish on a push to main OR a manual dispatch. A workflow_dispatch is
# how we ship a strawcore-only change (strawcore is cloned fresh at build
# time) into an APK without any straw code change — without the dispatch
# arm a dispatch builds + verifies the APK but silently never publishes.
if: >-
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
env:
LUCY_KEY: ${{ secrets.STRAW_FDROID_LUCY_KEY }}
# Publish target + its host-key are Forgejo secrets, NOT literals, so
# this public workflow carries no infra topology — only references.
PUBLISH_TARGET: ${{ secrets.STRAW_PUBLISH_TARGET }}
LUCY_HOSTKEY: ${{ secrets.STRAW_LUCY_HOSTKEY }}
run: |
set -euo pipefail
install -d -m700 "$HOME/.ssh"
printf '%s\n' "$LUCY_KEY" > "$HOME/.ssh/id_lucy"
chmod 600 "$HOME/.ssh/id_lucy"
# Pin the publish host key (no TOFU) — from a secret, not a literal.
printf '%s\n' "$LUCY_HOSTKEY" > "$HOME/.ssh/known_hosts_lucy"
# Hand off the signed APK on stdin. Host exit codes: 0 = published,
# 3 = already published (benign no-op on a same-commit re-run),
# anything else = real failure.
set +e
ssh -i "$HOME/.ssh/id_lucy" -o IdentitiesOnly=yes -o BatchMode=yes \
-o StrictHostKeyChecking=yes -o UserKnownHostsFile="$HOME/.ssh/known_hosts_lucy" \
"$PUBLISH_TARGET" "publish ${STRAW_APK}" < "$GITHUB_WORKSPACE/${STRAW_APK}"
rc=$?
set -e
case "$rc" in
0) echo "Published vc=${STRAW_VC} to fdroid.sulkta.com (rootless — no docker socket)";;
3) echo "vc=${STRAW_VC} already published — nothing to do";;
*) echo "::error::fdroid publish failed (rc=$rc)"; exit "$rc";;
esac