chore(ci): custom self-hosted runner image with Android + Flatpak deps preinstalled (#227) #243

Open
james wants to merge 1 commit from 227-runner-image into main
Owner

Summary

Closes #227. Ships a custom self-hosted Forgejo Actions runner image (forge.wynning.tech/james/carol-runner) that bakes in the Android + Flatpak + release-pipeline dep surface those release lanes pay for on every run today (~5-10 min × 2 lanes).

  • .forgejo/runner-image/Dockerfile — Ubuntu 24.04 LTS base; mirrors release-android.yml + release-flatpak.yml's install steps with pinned ARGs.
  • .forgejo/workflows/release-runner-image.yml — builds + publishes on Dockerfile change, monthly cron (0 0 1 * *), or manual dispatch. cosign-signs by digest and emits a SLSA v1.0 attestation byte-for-byte parallel to release.yml.
  • release-android.yml + release-flatpak.ymlcontainer: line points at <TBD-FILLED-BY-FIRST-PUBLISH> with the existing js-24.04 image as the live target until the first runner-image publish lands. Install steps marked <SENTINEL-FALLBACK> so the follow-up digest-swap PR knows what to delete.
  • docs/ci.md — new "Runner image" section covering contents, publish cadence, update flow, and relationship to #237.

Version pins

  • Base: ubuntu:24.04 LTS, linux/amd64 only.
  • Carol toolchain: Node 22 (NodeSource), pnpm 10.18.3 (corepack), gitleaks 8.30.1, actionlint 1.7.12 — read from .tool-versions.
  • Android: OpenJDK 21 (openjdk-21-jdk-headless), cmdline-tools rev 11076708, platforms;android-35, build-tools;35.0.0. SDK licences accepted at image-build time.
  • Flatpak / Tauri: rustup with toolchain 1.83.0 (minimal profile), libwebkit2gtk-4.1-dev + libsoup-3.0-dev + libjavascriptcoregtk-4.1-dev + the rest of the Tauri dep set, flatpak + flatpak-builder, GNOME Platform//48 + Sdk//48 pre-staged (soft-fail).
  • Release-pipeline: cosign 2.5.3, git-cliff 2.6.1 — mirrors release.yml.

Expected compressed image size: ~5-7 GB.

Expected per-release savings

Today the Android lane spends ~3-5 min on JDK + SDK install + license acceptance; the Flatpak lane spends ~7-10 min on rustup + apt deps + GNOME runtime fetch. Both go to zero once the digest swap lands. Wall-clock for the two lanes combined should drop by ~10 min per release.

Before / after diffs

release-android.yml removes:

  • Install JDK ${{ env.JAVA_VERSION }} step (apt-get + update-java-alternatives lookup).
  • Install Android SDK step (cmdline-tools fetch + unzip + license accept + sdkmanager calls).

release-flatpak.yml removes:

  • Install Tauri + Flatpak system deps step (apt-get of 14 packages).
  • Install Rust toolchain step (rustup-init).
  • Install Flatpak runtime + SDK step (flathub remote + system install).

Today the Android + Flatpak workflows still carry collapsed <SENTINEL-FALLBACK> versions of these steps; the digest-swap follow-up PR deletes them.

Test plan

  • actionlint .forgejo/workflows/*.yml — clean.
  • docker buildx build --check .forgejo/runner-image/ — "Check complete, no warnings found."
  • pnpm install --frozen-lockfile — clean.
  • pnpm -F @carol/api typecheck / lint / test — clean (556 passed, 107 skipped).
  • pnpm -F @carol/api-client typecheck / lint / test / check — clean.
  • pnpm -F @carol/client typecheck / lint / test — clean.
  • lefthook hooks (actionlint, gitleaks, conventional commit) pass on the commit.
  • First publish: release-runner-image.yml builds + pushes + signs the runner image, prints the digest in the job summary.
  • Follow-up PR: swap <TBD-FILLED-BY-FIRST-PUBLISH> in both release workflows to the real @sha256:…, delete the <SENTINEL-FALLBACK> install steps.
  • Verify image: cosign verify --key https://forge.wynning.tech/james/carol/raw/branch/main/cosign.pub forge.wynning.tech/james/carol-runner@sha256:<digest>.
  • Cut a release (vX.Y.Z) and confirm the Android + Flatpak lanes save ~10 min combined vs. the previous tag's run.

Relationship to #237

#237 (developer dev-container) and #227 (CI runner image) have an overlapping dep surface but different consumers and release cadences. They stay separate Dockerfiles today; documented under docs/ci.md "Runner image" → "Relationship to #237".

Surprises

  • Flatpak runtime install inside the build container. flatpak install --system needs a session bus that's not present in docker build's default sandbox. The Dockerfile soft-fails that step (|| echo "WARNING: …") so the layer continues without the runtime; the workflow's flatpak-builder invocation pulls it on first run and disk-caches it for subsequent runs. This is documented in both the Dockerfile and docs/ci.md.
  • Android SDK license auto-acceptance. yes | sdkmanager --licenses > /dev/null || true is the same one-liner the workflow used before — runs at image-build time so the runtime workflow never sees the prompt.
  • cosign / SLSA pattern transferred cleanly from release.yml. Same key-pair, same --tlog-upload=true explicitness, same hand-built SLSA v1.0 predicate shape. No deviations.

Links: #227 · ADR-0014

🤖 Generated with Claude Code

## Summary Closes #227. Ships a custom self-hosted Forgejo Actions runner image (`forge.wynning.tech/james/carol-runner`) that bakes in the Android + Flatpak + release-pipeline dep surface those release lanes pay for on every run today (~5-10 min × 2 lanes). - **`.forgejo/runner-image/Dockerfile`** — Ubuntu 24.04 LTS base; mirrors `release-android.yml` + `release-flatpak.yml`'s install steps with pinned `ARG`s. - **`.forgejo/workflows/release-runner-image.yml`** — builds + publishes on Dockerfile change, monthly cron (`0 0 1 * *`), or manual dispatch. cosign-signs by digest and emits a SLSA v1.0 attestation byte-for-byte parallel to `release.yml`. - **`release-android.yml` + `release-flatpak.yml`** — `container:` line points at `<TBD-FILLED-BY-FIRST-PUBLISH>` with the existing `js-24.04` image as the live target until the first runner-image publish lands. Install steps marked `<SENTINEL-FALLBACK>` so the follow-up digest-swap PR knows what to delete. - **`docs/ci.md`** — new "Runner image" section covering contents, publish cadence, update flow, and relationship to #237. ## Version pins - Base: `ubuntu:24.04` LTS, `linux/amd64` only. - Carol toolchain: Node 22 (NodeSource), pnpm 10.18.3 (corepack), gitleaks 8.30.1, actionlint 1.7.12 — read from `.tool-versions`. - Android: OpenJDK 21 (`openjdk-21-jdk-headless`), `cmdline-tools` rev 11076708, `platforms;android-35`, `build-tools;35.0.0`. SDK licences accepted at image-build time. - Flatpak / Tauri: rustup with toolchain `1.83.0` (minimal profile), `libwebkit2gtk-4.1-dev` + `libsoup-3.0-dev` + `libjavascriptcoregtk-4.1-dev` + the rest of the Tauri dep set, `flatpak` + `flatpak-builder`, GNOME `Platform//48` + `Sdk//48` pre-staged (soft-fail). - Release-pipeline: cosign 2.5.3, git-cliff 2.6.1 — mirrors `release.yml`. Expected compressed image size: ~5-7 GB. ## Expected per-release savings Today the Android lane spends ~3-5 min on JDK + SDK install + license acceptance; the Flatpak lane spends ~7-10 min on rustup + apt deps + GNOME runtime fetch. Both go to zero once the digest swap lands. Wall-clock for the two lanes combined should drop by ~10 min per release. ## Before / after diffs `release-android.yml` removes: - `Install JDK ${{ env.JAVA_VERSION }}` step (apt-get + `update-java-alternatives` lookup). - `Install Android SDK` step (cmdline-tools fetch + unzip + license accept + sdkmanager calls). `release-flatpak.yml` removes: - `Install Tauri + Flatpak system deps` step (apt-get of 14 packages). - `Install Rust toolchain` step (rustup-init). - `Install Flatpak runtime + SDK` step (flathub remote + system install). Today the Android + Flatpak workflows still carry collapsed `<SENTINEL-FALLBACK>` versions of these steps; the digest-swap follow-up PR deletes them. ## Test plan - [x] `actionlint .forgejo/workflows/*.yml` — clean. - [x] `docker buildx build --check .forgejo/runner-image/` — "Check complete, no warnings found." - [x] `pnpm install --frozen-lockfile` — clean. - [x] `pnpm -F @carol/api typecheck` / `lint` / `test` — clean (556 passed, 107 skipped). - [x] `pnpm -F @carol/api-client typecheck` / `lint` / `test` / `check` — clean. - [x] `pnpm -F @carol/client typecheck` / `lint` / `test` — clean. - [x] lefthook hooks (actionlint, gitleaks, conventional commit) pass on the commit. - [ ] First publish: `release-runner-image.yml` builds + pushes + signs the runner image, prints the digest in the job summary. - [ ] Follow-up PR: swap `<TBD-FILLED-BY-FIRST-PUBLISH>` in both release workflows to the real `@sha256:…`, delete the `<SENTINEL-FALLBACK>` install steps. - [ ] Verify image: `cosign verify --key https://forge.wynning.tech/james/carol/raw/branch/main/cosign.pub forge.wynning.tech/james/carol-runner@sha256:<digest>`. - [ ] Cut a release (`vX.Y.Z`) and confirm the Android + Flatpak lanes save ~10 min combined vs. the previous tag's run. ## Relationship to #237 #237 (developer dev-container) and #227 (CI runner image) have an overlapping dep surface but different consumers and release cadences. They stay separate Dockerfiles today; documented under `docs/ci.md` "Runner image" → "Relationship to #237". ## Surprises - **Flatpak runtime install inside the build container.** `flatpak install --system` needs a session bus that's not present in `docker build`'s default sandbox. The Dockerfile soft-fails that step (`|| echo "WARNING: …"`) so the layer continues without the runtime; the workflow's `flatpak-builder` invocation pulls it on first run and disk-caches it for subsequent runs. This is documented in both the Dockerfile and `docs/ci.md`. - **Android SDK license auto-acceptance.** `yes | sdkmanager --licenses > /dev/null || true` is the same one-liner the workflow used before — runs at image-build time so the runtime workflow never sees the prompt. - **cosign / SLSA pattern transferred cleanly** from `release.yml`. Same key-pair, same `--tlog-upload=true` explicitness, same hand-built SLSA v1.0 predicate shape. No deviations. Links: [#227](https://forge.wynning.tech/james/carol/issues/227) · [ADR-0014](/james/carol/src/branch/main/docs/adr/0014-release-pipeline.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(ci): custom self-hosted runner image with Android + Flatpak deps preinstalled (#227)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 6s
PR / OSV-Scanner (pull_request) Successful in 1m33s
PR / Lint (pull_request) Successful in 1m58s
PR / Static analysis (pull_request) Failing after 2m0s
PR / pnpm audit (pull_request) Successful in 2m21s
PR / OpenAPI (pull_request) Successful in 2m34s
PR / Trivy (image) (pull_request) Failing after 1m10s
PR / Client (web export smoke) (pull_request) Successful in 2m54s
PR / Package age policy (soft) (pull_request) Successful in 57s
PR / Test (sqlite) (pull_request) Successful in 2m58s
PR / Test (postgres) (pull_request) Successful in 2m59s
PR / Typecheck (pull_request) Successful in 3m3s
PR / Build (pull_request) Successful in 3m9s
Secrets / gitleaks (pull_request) Successful in 47s
PR / Coverage (soft) (pull_request) Successful in 1m29s
e8dcdbc69d
Adds a Carol-owned Forgejo Actions runner image
(.forgejo/runner-image/Dockerfile) that bakes in OpenJDK 21 +
Android cmdline-tools + the platform / build-tools versions Expo
SDK 56 expects, the Rust toolchain + webkit2gtk + Flatpak runtime
the Tauri/Flatpak lane needs, and the release-pipeline tools
(cosign, git-cliff). The .forgejo/workflows/release-runner-image.yml
workflow builds, signs (cosign), and attests (SLSA v1.0) the image
on Dockerfile changes, a monthly cron, or manual dispatch.

release-android.yml and release-flatpak.yml are updated to target
the new runner via a digest-pinned `container:` reference; the
inline install steps survive as `<SENTINEL-FALLBACK>` until the
first runner-image publish lands and the follow-up PR swaps the
TBD digest for the real `@sha256:…`, deleting the fallback steps.

docs/ci.md gains a "Runner image" section covering contents,
publish cadence, update flow, and the relationship to #237.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 83.0% ≥ 50%
Branches 75.9% ≥ 75%
Functions 91.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 83.0% ✅ | ≥ 50% | | Branches | 75.9% ✅ | ≥ 75% | | Functions | 91.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 6s
PR / OSV-Scanner (pull_request) Successful in 1m33s
PR / Lint (pull_request) Successful in 1m58s
PR / Static analysis (pull_request) Failing after 2m0s
PR / pnpm audit (pull_request) Successful in 2m21s
PR / OpenAPI (pull_request) Successful in 2m34s
PR / Trivy (image) (pull_request) Failing after 1m10s
PR / Client (web export smoke) (pull_request) Successful in 2m54s
PR / Package age policy (soft) (pull_request) Successful in 57s
PR / Test (sqlite) (pull_request) Successful in 2m58s
PR / Test (postgres) (pull_request) Successful in 2m59s
PR / Typecheck (pull_request) Successful in 3m3s
PR / Build (pull_request) Successful in 3m9s
Secrets / gitleaks (pull_request) Successful in 47s
PR / Coverage (soft) (pull_request) Successful in 1m29s
This pull request has changes conflicting with the target branch.
  • .forgejo/workflows/release-flatpak.yml
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 227-runner-image:227-runner-image
git switch 227-runner-image

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff 227-runner-image
git switch 227-runner-image
git rebase main
git switch main
git merge --ff-only 227-runner-image
git switch 227-runner-image
git rebase main
git switch main
git merge --no-ff 227-runner-image
git switch main
git merge --squash 227-runner-image
git switch main
git merge --ff-only 227-runner-image
git switch main
git merge 227-runner-image
git push origin main
Sign in to join this conversation.
No description provided.