diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 000000000..1fe3c804e --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,138 @@ +# 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" + + - 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//. 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 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + 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 diff --git a/.forgejo/workflows/gitleaks.yml b/.forgejo/workflows/gitleaks.yml new file mode 100644 index 000000000..10d7847f3 --- /dev/null +++ b/.forgejo/workflows/gitleaks.yml @@ -0,0 +1,40 @@ +# .forgejo/workflows/gitleaks.yml +# +# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at +# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered +# (task #295). +# +# Pairs with the pre-receive hook installed on every bare repo — that one is +# the strict enforcement layer (rejects the push); this one provides the +# per-PR red ✗ that branch-protection rules can require before merge. +# +# Layer 1 (this workflow): visible per-PR status, can be a required check. +# Layer 2 (pre-receive hook): strict enforcement at the server. +# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos. + +name: gitleaks + +on: + push: + pull_request: + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history — gitleaks needs depth to scan a commit range. + fetch-depth: 0 + + - name: install gitleaks + run: | + curl -sSL -o gl.tar.gz \ + https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz + tar xzf gl.tar.gz gitleaks + chmod +x gitleaks + ./gitleaks version + + - name: scan + run: | + ./gitleaks detect --source . --no-banner --redact --verbose