Files
mrambossekandClaude Opus 4.7 e22d3b9168
Build flutter-node image / build (push) Successful in 2m45s
initial: flutter-node runner image
Custom CI runner image combining cirruslabs/flutter:stable with Node 20
from the official node image, so Gitea Actions runners can execute
JS-based actions (actions/checkout, actions/upload-artifact, etc.) on
Flutter projects.

Registry host is configurable via the vars.REGISTRY_HOST Gitea variable;
weekly cron picks up upstream patch updates without source changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:03:47 +02:00

147 lines
5.9 KiB
Markdown

# flutter-node
Custom CI runner image: Flutter SDK from `cirruslabs/flutter` with
Node 20 binaries overlaid from the official `node` image. Two-file
repo (`Dockerfile`, `.gitea/workflows/build.yaml`); the workflow
builds and publishes the image on push, weekly, and on demand.
## 1. Why this exists
Gitea Actions' built-in actions — `actions/checkout`, `actions/setup-*`,
`actions/upload-artifact`, basically every official action — are
JavaScript-based and need a `node` binary on the runner's PATH. The
vanilla `ghcr.io/cirruslabs/flutter:stable` image ships Flutter +
Android SDK + JDK but no Node, so Flutter workflows that use those
actions fail with:
```
exec: "node": executable file not found in $PATH
```
Two ways to fix that: install Node into every runner image that uses
it (apt-get on each), or bake it in once and use that everywhere.
This repo is the latter.
## 2. Published image
| Field | Value |
| --- | --- |
| Registry path | `${REGISTRY_HOST}/ci-images/flutter-node` |
| Tags | `:stable` (moving) + `:YYYYMMDD` (immutable, dated build) |
| Visibility | Public — anonymous `docker pull` works |
| Linked repo | `https://${REGISTRY_HOST}/ci-images/flutter-node` (via OCI `image.source` label, so Gitea links package → repo and inherits visibility) |
`REGISTRY_HOST` is a Gitea Actions variable (`vars.REGISTRY_HOST`) configured at
the org or repo level — the workflow uses it for the push tags, and passes it
into the Docker build as the `IMAGE_SOURCE` build-arg so the OCI source label
matches. Nothing in this repo hardcodes a hostname; pointing the image at a
different Gitea instance is just a variable change.
Bumping is deliberate: edit `Dockerfile` to change a `FROM` tag, push.
The weekly cron picks up upstream patch updates without source
changes (cirruslabs republishes `:stable` and Debian republishes
`bookworm-slim` for security fixes).
## 3. Build inputs
```
FROM ${NODE_IMAGE} AS node # default: node:20-bookworm-slim
FROM ${FLUTTER_IMAGE} # default: ghcr.io/cirruslabs/flutter:stable
```
The runtime stage is `${FLUTTER_IMAGE}` with the following bits copied
in from the node stage:
- `/usr/local/bin/node`
- `/usr/local/lib/node_modules` (npm + the npx shim live in here)
Two symlinks materialize `/usr/local/bin/npm` and `/usr/local/bin/npx`
pointing into the module tree — mirrors what `npm install -g` does on
a normal install.
A `RUN node --version && npm --version && npx --version && flutter
--version` at the end of the Dockerfile fails the build if any of
those isn't on PATH, so a broken layer can't sneak into a published
image.
## 4. Workflow triggers
`.gitea/workflows/build.yaml` runs on:
| Trigger | When | Why |
| --- | --- | --- |
| `push: branches: [main]` | Any commit lands on main | Pick up source changes immediately |
| `schedule: 0 3 * * 0` | Sunday 03:00 UTC | Pick up upstream patch updates with no source change |
| `workflow_dispatch` | Manual button in Gitea UI | On-demand rebuild |
The job runs on `runs-on: ubuntu-docker` (a runner with a Docker
daemon available), **not** `flutter` — that intentionally sidesteps
the chicken-and-egg where building the flutter-node image would need
the flutter-node image.
Auth to the registry uses `vars.REGISTRY_HOST` + `secrets.REGISTRY_TOKEN`
— same pattern the other projects on this Gitea instance use. The
auto-injected `gitea.token` is rejected by the registry path; a
manually-issued PAT with `write:package` scope is required.
## 5. Consumers
Any runner config that points at this image — typically Flutter projects
whose workflows declare `runs-on: flutter`.
Adding a new consumer: nothing changes here. Just point the consuming
project's `runs-on:` at `flutter` and make sure the runner config has
`flutter:docker://${REGISTRY_HOST}/ci-images/flutter-node:stable`
mapped in (substituting the same host you set in `vars.REGISTRY_HOST`).
## 6. Pinning + rollback
The `:stable` tag is **moving** — runners pulling it without an
`image_pull_policy: always` setting may keep using a cached older
layer until Docker prunes. Two ways to deal:
1. **Pin to a dated tag in the runner config** (recommended for prod).
Change `…/flutter-node:stable``…/flutter-node:20260524`. Bumping
is then a deliberate runner-config edit, never a surprise.
2. **Tell the runner to always pull** (`image_pull_policy: always`).
Cheap if the runner host is on a fast link to the registry.
Rolling back a bad image: every successful workflow leaves a dated
tag in the registry. To roll back, point the runner config at the
previous date tag — no need to re-push anything from this repo.
## 7. Testing changes locally
The image builds with plain Docker — no buildx required for a smoke
test:
```bash
docker build -t flutter-node:test .
docker run --rm flutter-node:test node --version
docker run --rm flutter-node:test flutter --version
```
For a fuller smoke test against the actual JS actions, mount it into
a local act-runner setup and run a workflow that does `actions/checkout`
+ `actions/upload-artifact`.
## 8. Extending
The Dockerfile is intentionally small. If you need to add tooling:
- **OS packages** (apt-get) — add a `RUN apt-get update && apt-get install
-y --no-install-recommends <packages> && rm -rf /var/lib/apt/lists/*`
before the sanity-gate step. Keep the apt-get in one RUN to avoid
layer bloat.
- **Global npm packages** — add `RUN npm install -g <pkg>@<version>`.
Pin versions; `latest` defeats the point of having a reproducible
image.
- **A second runtime** (Python, Go, etc.) — for one or two binaries,
same `COPY --from=<official-image>` trick used for Node. For a full
language toolchain, consider a separate image (`ci-images/flutter-python`
etc.) so the matrix stays comprehensible.
Anything that adds noticeable image size warrants a comment in this
SPEC explaining why; the whole point of a generic runner image is
that it doesn't grow into a kitchen sink.