No description
  • Dockerfile 71.2%
  • Shell 28.8%
Find a file
fiatcode bf05ee6cac
refactor: make build.sh the sole source of toolchain versions
Containerfile version ARGs had defaults too — a second source that drifted (build.sh 3.44.1 vs Containerfile 3.44.0). Made them bare ARGs (no defaults) so the value must come from build.sh's --build-arg, and pulled ANDROID_CMDLINE_TOOLS into build.sh as well. IMAGE_REVISION/CREATED keep defaults (provenance fallbacks).
2026-06-10 14:08:41 +07:00
flutter-android refactor: make build.sh the sole source of toolchain versions 2026-06-10 14:08:41 +07:00
go refactor: make build.sh the sole source of toolchain versions 2026-06-10 14:08:41 +07:00
build.sh refactor: make build.sh the sole source of toolchain versions 2026-06-10 14:08:41 +07:00
README.md feat: version-tag images and stamp OCI provenance labels 2026-06-10 13:45:41 +07:00

ci-images

Custom container images for the self-hosted Forgejo Actions runner on the workstation. The runner is a single laptop, so instead of downloading Flutter / the Android SDK / Go on every job, the toolchains are baked into local images and the runner references them by label.

Runner setup itself lives in the Logseq runbook Forgejo CI with Rootless Podman.

Images

Image Label Toolchain Used by
localhost/fiatcode-ci-go go Go 1.26.3, ripgrep, Node.js peekseq
localhost/fiatcode-ci-flutter-android flutter-android Flutter 3.44.0, Android SDK 36 + NDK 28.2, Java 17, Node.js gbikudus-app, kuwot-app

Both are based on ghcr.io/catthehacker/ubuntu:act-22.04 — that base already carries Node.js and git, which the JS actions (checkout, setup-java, upload-artifact) require inside the job container. Building up from it avoids the node: not found trap.

Build

./build.sh                  # both
./build.sh go               # just the Go image
./build.sh flutter-android  # just the Flutter image (slow)

Images go into the local Podman store (localhost/...). No registry, nothing pushed — the runner runs on the same machine that builds them.

Versioning & provenance

Each build is tagged twice — a version tag and :latest:

localhost/fiatcode-ci-go:1.26.3              localhost/fiatcode-ci-go:latest
localhost/fiatcode-ci-flutter-android:3.44.0 localhost/fiatcode-ci-flutter-android:latest

The runner config rides :latest (rebuild → restart picks it up). To know exactly what a given image holds, read its OCI labels — they're stamped from the same versions, so they can't drift from what's installed:

podman image inspect localhost/fiatcode-ci-flutter-android:latest \
  --format '{{range $k,$v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}'
# dev.fiatcode.flutter.version=3.44.0
# dev.fiatcode.android.platform=android-36
# dev.fiatcode.android.buildtools=36.0.0
# dev.fiatcode.android.ndk=28.2.13676358
# org.opencontainers.image.revision=<ci-images git short SHA>
# org.opencontainers.image.created=<build timestamp>

org.opencontainers.image.revision ties the running image back to the exact ci-images commit that built it — so "what's in :latest" is always answerable from the image alone.

Versions are defined once, in build.sh (passed as --build-arg, used for both the tag and the labels). The Containerfile ARG defaults are just fallbacks.

Wire into the runner

  1. Add the labels in ~/.local/share/forgejo-runner/config.yml, keeping docker as the generic fallback, and set force_pull: false so Podman uses the local images instead of trying to pull localhost/... from a registry:

    runner:
      labels:
        - docker:docker://ghcr.io/catthehacker/ubuntu:act-22.04
        - go:docker://localhost/fiatcode-ci-go:latest
        - flutter-android:docker://localhost/fiatcode-ci-flutter-android:latest
    container:
      docker_host: "unix:///run/user/1000/podman/podman.sock"
      force_pull: false
    
  2. Restart the runner so it re-advertises labels:

    systemctl --user restart forgejo-runner.service
    

    Build the images before restarting. A label that points at a missing image means jobs routed to it fail.

Use in a workflow

Switch runs-on to the baked label and delete the now-redundant toolchain-setup steps.

peekseq .forgejo/workflows/ci.yml:

# before
jobs:
  test:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - uses: https://github.com/actions/setup-go@v5   # <- drop
      - run: sudo apt-get update && sudo apt-get install -y ripgrep   # <- drop
      - run: go test ./...

# after
jobs:
  test:
    runs-on: go            # Go + ripgrep already in the image
    steps:
      - uses: actions/checkout@v4
      - run: go vet ./...
      - run: go test ./...
      - run: go build ./...

gbikudus-app / kuwot-app Flutter workflows:

# before
runs-on: docker
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-java@v4                          # <- drop
  - uses: https://github.com/android-actions/setup-android@v3   # <- drop
  - uses: https://github.com/subosito/flutter-action@v2  # <- drop
  - run: flutter pub get
  ...

# after
runs-on: flutter-android   # Flutter + Android SDK + Java already in the image
steps:
  - uses: actions/checkout@v4
  - run: flutter pub get
  ...

Everything else (the keystore decode, google-services.json decode in the release job, the tag-driven build) stays the same.

Maintenance

Baked images go stale when a toolchain bumps. All versions live in build.sh — bump there, rebuild, restart the runner:

  • Flutter → bump FLUTTER_VERSION (match the workstation's flutter --version).
  • Go → bump GO_VERSION (match the repo's go.mod).
  • Android → if a build re-downloads an SDK platform/NDK every run, the baked ANDROID_PLATFORM / ANDROID_BUILD_TOOLS / ANDROID_NDK no longer match the app's Flutter version (check Flutter's gradle defaults: compileSdkVersion / ndkVersion) — bump and rebuild.

The version tag + labels update automatically since build.sh drives both.

The docker label stays as a fallback for any repo that doesn't want a baked toolchain.