From e22d3b9168edd6f3f4f048da1b675c7a1b866d42 Mon Sep 17 00:00:00 2001 From: mrambossek Date: Sun, 24 May 2026 18:03:47 +0200 Subject: [PATCH] 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) --- .gitea/workflows/build.yaml | 71 ++++++++++++++++++ Dockerfile | 38 ++++++++++ SPEC.md | 146 ++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 Dockerfile create mode 100644 SPEC.md diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..d510080 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,71 @@ +name: Build flutter-node image + +# Triggers: +# - push to main → immediate rebuild (any change in the repo) +# - weekly cron → picks up upstream patch updates in +# cirruslabs/flutter:stable and node:20 +# without a source change +# - manual dispatch → on-demand +on: + push: + branches: [main] + schedule: + # Sun 03:00 UTC. cirruslabs typically ships :stable bumps a few + # hours after the upstream Flutter stable channel — adjust freely. + - cron: "0 3 * * 0" + workflow_dispatch: + +jobs: + build: + # Skip the job entirely (no runner provisioning) when the registry + # host isn't configured — shows as "skipped" in the UI rather than + # burning minutes on a build that can't be pushed. Secrets can't be + # referenced in job-level if:, so REGISTRY_TOKEN is still checked + # in the first step below. + if: vars.REGISTRY_HOST != '' + runs-on: ubuntu-docker + permissions: + packages: write + steps: + - name: Validate registry token + run: | + if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then + echo "::error::Missing required config: secrets.REGISTRY_TOKEN" + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Compute date tag + id: date + run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" + + # Same registry + PAT pattern other projects on this Gitea + # instance use. The auto-injected gitea token is rejected by the + # registry path, hence the manually-issued PAT in REGISTRY_TOKEN. + - name: Log in to Gitea registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.REGISTRY_HOST }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + # Two tags: + # :stable — moving, what runners pull by default + # :YYYYMMDD — immutable, for pinning / rollback + tags: | + ${{ vars.REGISTRY_HOST }}/ci-images/flutter-node:stable + ${{ vars.REGISTRY_HOST }}/ci-images/flutter-node:${{ steps.date.outputs.date }} + build-args: | + IMAGE_SOURCE=https://${{ vars.REGISTRY_HOST }}/ci-images/flutter-node + cache-from: type=registry,ref=${{ vars.REGISTRY_HOST }}/ci-images/flutter-node:stable + cache-to: type=inline diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3590843 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Generic CI runner image: cirruslabs/flutter base + Node from the +# official node image, so Gitea Actions / GitHub Actions runners can +# execute JS-based actions (actions/checkout, actions/upload-artifact, +# actions/setup-*) on Flutter projects. Vanilla cirruslabs/flutter +# ships no Node, which is why pipelines using those actions fail with +# `exec: "node": executable file not found in $PATH`. +# +# Bases are pinned via build-args. Bump deliberately by editing this +# file; the weekly cron in .gitea/workflows/build.yaml picks up +# Cirrus's :stable refresh (and Node 20.x patch updates) automatically +# without a source change. + +ARG FLUTTER_IMAGE=ghcr.io/cirruslabs/flutter:stable +ARG NODE_IMAGE=node:20-bookworm-slim +ARG IMAGE_SOURCE= + +FROM ${NODE_IMAGE} AS node + +FROM ${FLUTTER_IMAGE} +ARG IMAGE_SOURCE + +# Links the published OCI package to its source repo so Gitea inherits +# visibility (public repo → publicly pullable image, no auth needed). +# IMAGE_SOURCE is supplied at build time from vars.REGISTRY_HOST. +LABEL org.opencontainers.image.source="${IMAGE_SOURCE}" +LABEL org.opencontainers.image.description="Flutter (cirruslabs/flutter:stable) + Node 20, for CI runners that execute JS-based actions." +LABEL org.opencontainers.image.licenses="MIT" + +# Bring node + npm + npx in from the official node image. Symlinks +# mirror what `npm install -g` does on a normal install. +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ + && ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx + +# Sanity gate: a missing binary fails the build rather than silently +# shipping a broken image. +RUN node --version && npm --version && npx --version && flutter --version diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..0f67094 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,146 @@ +# 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 && 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 @`. + 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=` 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.