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>
This commit is contained in:
@@ -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
|
||||||
+38
@@ -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
|
||||||
@@ -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 <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.
|
||||||
Reference in New Issue
Block a user