💚 Bake Node into the CI job image
setup-node never consults `node` on PATH; its only fast path is a probe of /opt/hostedtoolcache, which the ephemeral act_runner job containers always miss, so every job paid a ~50 MB Node download. docker/Dockerfile extends the runner's default catthehacker/act image with the Node distribution overlaid at the exact tool-cache layout, so setup-node finds 26.8.2 and skips the fetch while node-version-file, cache: npm and registry-url keep working unchanged. Image layers dedupe against the base the host already pulled and prune via normal docker hygiene — the cleanup story a host bind of /opt/hostedtoolcache lacks. To make the bake deterministic, .node-version is pinned to the exact 26.8.2 the image carries; scripts/runner-image.sh guards that coupling and builds/pushes the tag the three node jobs now reference via container.image. Ops follow-up (outside the repo): build once with `npm run build:runner-image -- --push` on a machine with registry creds. If the package is private, the runner needs container registry credentials in its config.
This commit is contained in:
1 parent
b9fe21175f
commit
245dfaf198
6 files changed
+88
-5
No files matched your search
+17
-4
@@ -49,11 +49,16 @@ jobs:
|
|||||||
needs: release-gate
|
needs: release-gate
|
||||||
if: needs.release-gate.outputs.skip != 'true'
|
if: needs.release-gate.outputs.skip != 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bind-mount the shared pages tree so the coverage step below can write
|
# `image` extends the runner's default job image (catthehacker/act)
|
||||||
# into it. The runner whitelists this path via `container.valid_volumes`
|
# with Node 26 pre-planted in the tool cache layout, so setup-node's
|
||||||
# (docker-space `setup/gitea.sh`); `image` is omitted on purpose so the
|
# version probe hits and never downloads (see docker/Dockerfile). The
|
||||||
# runner keeps using its default job image.
|
# tag MUST equal the exact version pinned in `.node-version`; rebuild
|
||||||
|
# via `npm run build:runner-image -- --push` and repoint here on every
|
||||||
|
# Node bump. The volume bind-mounts the shared pages tree so the
|
||||||
|
# coverage step below can write into it; the runner whitelists this
|
||||||
|
# path via `container.valid_volumes` (docker-space `setup/gitea.sh`).
|
||||||
container:
|
container:
|
||||||
|
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
|
||||||
volumes:
|
volumes:
|
||||||
- /data/gitea-pages:/data/gitea-pages
|
- /data/gitea-pages:/data/gitea-pages
|
||||||
steps:
|
steps:
|
||||||
@@ -108,6 +113,10 @@ jobs:
|
|||||||
if: needs.release-gate.outputs.skip != 'true'
|
if: needs.release-gate.outputs.skip != 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
# Same baked image as `build` — without it this job re-downloads Node
|
||||||
|
# per run (see docker/Dockerfile).
|
||||||
|
container:
|
||||||
|
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -121,6 +130,10 @@ jobs:
|
|||||||
if: startsWith(gitea.ref, 'refs/tags/')
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Same baked image as `build` — setup-node still owns the registry-url
|
||||||
|
# `.npmrc` rewrite here; only the Node download is skipped.
|
||||||
|
container:
|
||||||
|
image: gitea.e1nsnull.de/tmu/act-ci:26.8.2
|
||||||
# The release page is created with the run's automatic Gitea token
|
# The release page is created with the run's automatic Gitea token
|
||||||
# (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`.
|
# (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`.
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
26
|
26.8.2
|
||||||
@@ -27,6 +27,12 @@
|
|||||||
"knope",
|
"knope",
|
||||||
"runwisp",
|
"runwisp",
|
||||||
"glab",
|
"glab",
|
||||||
|
"hostedtoolcache",
|
||||||
|
"catthehacker",
|
||||||
|
"nsnull",
|
||||||
|
"dedup",
|
||||||
|
"dedupe",
|
||||||
|
"repoint",
|
||||||
"postversion",
|
"postversion",
|
||||||
"prebuild",
|
"prebuild",
|
||||||
"Zilla",
|
"Zilla",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# CI job image for the Gitea act_runner: the runner's default job image with
|
||||||
|
# Node pre-planted where actions/setup-node looks first.
|
||||||
|
#
|
||||||
|
# Why this layout: setup-node ignores `node` on PATH; its only fast path is a
|
||||||
|
# probe of /opt/hostedtoolcache/node/<version>/<arch>. Without an entry there
|
||||||
|
# it downloads the ~50 MB distribution on EVERY job (the runner's job
|
||||||
|
# containers are ephemeral, so its tool cache never survives a job). The
|
||||||
|
# official node images keep exactly the layout setup-node expects under
|
||||||
|
# /usr/local, so this layer is a pure file overlay — no scripts, no env.
|
||||||
|
#
|
||||||
|
# Why not a host bind of /opt/hostedtoolcache: binds never self-prune. Docker
|
||||||
|
# images are content-addressed: the base layers dedupe against the act image
|
||||||
|
# the host already has, and `docker image prune` / re-pulls are the cleanup
|
||||||
|
# story.
|
||||||
|
#
|
||||||
|
# NODE_VERSION must match `.node-version` exactly. setup-node resolves a float
|
||||||
|
# like `26` to the latest known patch at runtime, so a bump silently busts the
|
||||||
|
# baked entry; `.node-version` is pinned to x.y.z and scripts/runner-image.sh
|
||||||
|
# guards the coupling. Rebuild + repoint `container.image` in
|
||||||
|
# .gitea/workflows/ci.yml on every bump.
|
||||||
|
FROM catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
ARG NODE_VERSION=26.8.2
|
||||||
|
|
||||||
|
# node image: bin/ + lib/ under /usr/local → tool cache: bin/ + lib/ under <ver>/x64.
|
||||||
|
COPY --from=node:${NODE_VERSION} /usr/local /opt/hostedtoolcache/node/${NODE_VERSION}/x64
|
||||||
|
|
||||||
|
# Fail the build (not CI) if the overlay or the version arg were wrong.
|
||||||
|
# Shell form on purpose: exec form (`RUN [...]`) does not expand ARG values.
|
||||||
|
RUN "/opt/hostedtoolcache/node/${NODE_VERSION}/x64/bin/node" --version
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"build:runner-image": "./scripts/runner-image.sh",
|
||||||
"prebuild": "rm -rf dist",
|
"prebuild": "rm -rf dist",
|
||||||
"check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell",
|
"check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell",
|
||||||
"check:cspell": "cspell lint ${LEFTHOOK_FILES:-.}",
|
"check:cspell": "cspell lint ${LEFTHOOK_FILES:-.}",
|
||||||
|
|||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build (and optionally push) the CI job image from docker/Dockerfile.
|
||||||
|
# Run wherever docker + registry credentials live (the runner host, or any
|
||||||
|
# machine that can reach the registry). The registry/repo below MUST match
|
||||||
|
# the `container.image` references in .gitea/workflows/ci.yml — the runner
|
||||||
|
# pulls the image by name.
|
||||||
|
#
|
||||||
|
# Usage: scripts/runner-image.sh [--push]
|
||||||
|
|
||||||
|
IMAGE_REPO="gitea.e1nsnull.de/tmu/act-ci"
|
||||||
|
|
||||||
|
NODE_VERSION="$(tr -d '[:space:]' < .node-version)"
|
||||||
|
if [[ ! "${NODE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "error: .node-version must be pinned to an exact x.y.z, got '${NODE_VERSION}'." >&2
|
||||||
|
echo " setup-node resolves floats like '26' to the latest patch at runtime," >&2
|
||||||
|
echo " which silently busts the tool-cache entry baked into the image." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE="${IMAGE_REPO}:${NODE_VERSION}"
|
||||||
|
|
||||||
|
# --pull: refresh the act base layer so the derivative does not float on an
|
||||||
|
# aging default image forever (layer dedup keeps this cheap).
|
||||||
|
docker build --pull --build-arg "NODE_VERSION=${NODE_VERSION}" -t "${IMAGE}" -f docker/Dockerfile .
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--push" ]]; then
|
||||||
|
docker push "${IMAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "built ${IMAGE}"
|
||||||
|
echo "reminder: bump container.image in .gitea/workflows/ci.yml to this tag"
|
||||||
Reference in new issue
Block a user