# CI Requirements — Minstrel > Copy of [the template](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/requirements-sheet.template.md). > Family-wide policy: `ci-runners.md` in the FabledRulebook. ## Runtime images Minstrel's four workflows consume two CI images: ``` git.fabledsword.com/bvandeusen/ci-go:1.26 git.fabledsword.com/bvandeusen/ci-android:36 ``` - `ci-go:1.26` — Go server tests (`.gitea/workflows/test-go.yml`), web SPA tests (`.gitea/workflows/test-web.yml`), and the release container build (`release.yml`'s `image-release` job). - `ci-android:36` — native Kotlin/Compose client: ktlint + detekt + unit tests + debug APK (`.gitea/workflows/android.yml`), and the signed release APK (`release.yml`'s `android-release` job). **`ci-flutter` is no longer consumed, and `ci-flutter` can now be retired.** The M8 rewrite replaced the Flutter client with the native Android app and `flutter.yml` was removed; `ci-android` took its place. `flutter_client/` itself was deleted on 2026-08-16, which was the condition CI-Runner was waiting on before dropping the image — nothing in this repo needs a Flutter toolchain any more. Note this does **not** mean the `flutter-ci` runner *label* goes: the Android jobs still schedule on it while pulling `ci-android:36`, per the label/image split below. The label is a scheduling handle, not a toolchain assertion. ## Image deps used ### From `ci-go:1.26` - **Go** (1.26 toolchain) — `go vet`, `go test -race`, `go build`, `go mod`. - **Node + npm** — `npm ci` and `npm test` / `npm run check` in `test-web.yml`. - **golangci-lint** — lint pass in `test-go.yml`. - **docker CLI** — bridge-IP discovery of the per-job Postgres service container in `test-go.yml` integration job (via the runner's shared `/var/run/docker.sock`). - **docker buildx** — release container build + push in `release.yml`. - **curl** — release-asset polling / upload in `release.yml`. ### From `ci-android:36` - **JDK 25** — Gradle launcher + Android build. Requires Gradle 9.1.0+ in `android/gradle/wrapper`; older Gradle rejects JDK 25 with an opaque `"25.0.3"` error. The workflows also set `JAVA_TOOL_OPTIONS=--enable-native-access=ALL-UNNAMED` to silence Gradle's launcher-JVM restricted-method warning. - **Android SDK + cmdline-tools + build-tools 36.0.0** — APK assembly + signing. No NDK: the native client has no C/C++ sources (this is why it isn't on `ci-flutter`). - **ktlint + detekt** — `./gradlew ktlintCheck` and `./gradlew detekt` in `android.yml`. Image pins track `android/gradle/libs.versions.toml` so local and CI checks agree. - **git** — `actions/checkout@v4` baseline (and any shell git operations). - **base64 + curl** — keystore decode + release-asset upload in `release.yml`'s `android-release` job. ## Per-job tool installs None. ## Notes - **Label/image split.** Workflows keep `runs-on: go-ci` / `runs-on: flutter-ci` as the scheduling label per the [`ci-runners.md`](https://…/FabledRulebook/ci-runners.md) "label = scheduling handle, image = `container.image`" pattern. The labels are intentional handles, not toolchain assertions — which is why the Android jobs still schedule on `flutter-ci` while pulling `ci-android:36`. Switch them to `android-ci` if that runner label is ever registered; nothing breaks either way. - **Integration-job docker-socket dependency.** `test-go.yml`'s integration job uses the runner's shared docker socket (`/var/run/docker.sock`) to bridge-IP-discover the per-job Postgres service container by name + network intersection — the dev compose's `minstrel-postgres-*` containers are explicitly skipped as belt-and-suspenders. Depends on `act_runner.valid_volumes` whitelisting the socket; if that ever stops auto-mounting, integration tests fail at the `docker inspect` step. - **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows. - **In-app update channel — `needs:`, not polling.** `release.yml`'s `image-release` job declares `needs: [android-release]`, so on tag pushes the signed APK is guaranteed present before the image build starts — no polling window, no race. (The old cross-workflow polling against `flutter.yml` is gone with that workflow.) On non-tag `main` pushes `android-release` is skipped and `image-release` instead pulls the most recent release's APK and reconstructs its exact `versionName`, so `:latest` never ships without an update channel. It degrades to an empty `client/` — never a wrong version — if no release, asset, or tag commit-count can be resolved. - **Cache server reachability.** `test-web.yml` does NOT use `cache: 'npm'` on `actions/setup-node` — the Gitea Actions cache server isn't reachable from this runner's container network and `setup-node` was burning ~4m41s on ETIMEDOUT before failing open. With the migration to `ci-go:1.26`, `setup-node` is removed entirely (Node is in the image). The cache concern reappears if a future change re-introduces a network-dependent action. - **Artifacts — stock `actions/upload-artifact@v7` and `actions/download-artifact@v8`; never `@v3`.** ```yaml uses: actions/upload-artifact@v7 uses: actions/download-artifact@v8 ``` Stock works on this forge since the runner moved to gitea/runner 3.x, which edits the actions' client-side `isGhes()` refusal out of their bundles. Proven on 2026-09-10 for upload v4–v7 and download v4–v8 (Scribe spike #3843). Until then this repo pinned SHA mirrors of the Forgejo project's forks, because upstream threw on the hostname before it opened a connection (Scribe 2255). `@v3` is still broken: it reports success, and Gitea serves artifacts back only through the v4 API (`content_encoding = application/zip`), so a v3 upload is stored but invisible to every retrieval path. That is how 72 unreachable artifacts accumulated on this repo (Scribe 2270). **Pairing no longer needs managing.** This entry used to pin upload v5 against download v6 so both bundled `@actions/artifact` ^4.0.0, warning that a mismatch across `release.yml`'s producer/consumer pair would list empty. Tested, and not true on this instance: every download major v4–v8 read the artifacts of every upload major v4–v7, by name and by pattern (CI-runner run 6312). The only real protocol break is v3 → v4. node24 is no longer a concern either — every CI-runner image carries Node 24 and the runner runs actions with the image's `node`. Upload steps set `if-no-files-found: error` rather than the default `warn`, so an upload that matches nothing fails its own job instead of failing the consumer later. Retrieval: `GET /api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` for the id (global run id, not the repo-scoped run number), then `…/actions/artifacts/{id}/zip`. The workstation has no `unzip` — use `python3 -m zipfile -e`. - **Friction asks.** None pending. The two images cover everything Minstrel needs.