ci: stop disk reclaim from deleting a smoke image mid-test
Lint / skill-floor (push) Successful in 9s
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 22s
Lint / doc-drift (push) Successful in 12s

v1.12.2 run 740: build-variant's `docker system prune -af --volumes`
untagged pi-devbox:smoke-studio 30 ms after smoke-studio loaded it, so
every studio assertion failed and the -studio tags were not published.
All jobs share the runner's Docker daemon, and a smoke image is unused
between smoke-test.sh's one-shot `docker run`s.

- scripts/ci-reclaim-docker.sh: the same reclaim, per object type, but
  never prunes an image labelled se.jordbo.pi-devbox.ci-smoke-image;
  labelled orphans are swept after 6h. `assert-protected IMG` is the
  smoke preflight; `lint WORKFLOW...` rejects raw system/image prunes.
- smoke jobs: label their image, assert it, tag it per run
  (pi-devbox:smoke[-studio]-<run_id>), remove it in an always() step.
- all five reclaim sites call the script; lint.yml runs the guard.
- Job order unchanged: studio jobs still gate only the -studio tags.

Verified on an isolated docker:29-dind (old prune deletes a labelled
image; new reclaim keeps it, removes unlabelled + 1970-created labelled
images, spares an in-use image, no errors); actionlint, shellcheck,
lint-shell, check-workflow-shell, check-doc-drift all clean.
This commit is contained in:
2026-10-23 08:34:39 +02:00
parent fb2cb2305c
commit 12e66aa52e
5 changed files with 193 additions and 14 deletions
+39 -14
View File
@@ -90,6 +90,12 @@ env:
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }}
PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }}
# Label that protects a smoke job's loaded image from the disk reclaim every
# job runs on the shared runner daemon (scripts/ci-reclaim-docker.sh says
# why: v1.12.2 run 740 lost pi-devbox:smoke-studio to build-variant's prune
# 30 ms after loading it). Set once here; the smoke builds apply it, the
# reclaim script honours it, the smoke preflight asserts it.
SMOKE_IMAGE_LABEL: se.jordbo.pi-devbox.ci-smoke-image
jobs:
# ── Phase 1: decide whether base needs rebuilding ──────────────────
@@ -538,8 +544,7 @@ jobs:
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker system prune -af --volumes || true
docker builder prune -af || true
bash scripts/ci-reclaim-docker.sh
df -h / || true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -597,6 +602,10 @@ jobs:
needs.resolve-versions.result == 'success' &&
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
runs-on: ubuntu-latest
env:
# Per run, so two runs on different refs cannot test or delete each
# other's image on the shared daemon. run_id is stable across re-runs.
SMOKE_IMAGE: pi-devbox:smoke-${{ github.run_id }}
container:
image: catthehacker/ubuntu:act-latest
steps:
@@ -610,8 +619,7 @@ jobs:
/usr/local/lib/android /usr/local/share/powershell \
/usr/local/share/chromium /usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
bash scripts/ci-reclaim-docker.sh
- uses: docker/setup-buildx-action@v4
with: {driver-opts: network=host}
- uses: docker/login-action@v3
@@ -626,7 +634,8 @@ jobs:
platforms: linux/amd64
push: false
load: true
tags: pi-devbox:smoke
tags: ${{ env.SMOKE_IMAGE }}
labels: ${{ env.SMOKE_IMAGE_LABEL }}=true
build-args: |
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
@@ -648,7 +657,14 @@ jobs:
# Asserting the BUILT image matches it also catches a stale cached layer.
EXPECTED_NODE_MAJOR=$(sed -n 's/^ARG NODE_VERSION=\([0-9][0-9]*\).*/\1/p' Dockerfile.base)
export EXPECTED_NODE_MAJOR
bash scripts/smoke-test.sh pi-devbox:smoke
bash scripts/ci-reclaim-docker.sh assert-protected "$SMOKE_IMAGE"
bash scripts/smoke-test.sh "$SMOKE_IMAGE"
# The label keeps this image from every job's reclaim, so it is removed
# here instead. A job killed before this step leaves an orphan, which the
# reclaim's age sweep deletes once it is older than SMOKE_IMAGE_MAX_AGE.
- name: Remove smoke image
if: always()
run: docker image rm "$SMOKE_IMAGE" || true
# ── Phase 3b: amd64 smoke for the studio variant ────────────────────
# Additive + independent of the core `smoke` job: gates ONLY
@@ -662,6 +678,10 @@ jobs:
needs.resolve-versions.result == 'success' &&
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
runs-on: ubuntu-latest
env:
# Per run, so two runs on different refs cannot test or delete each
# other's image on the shared daemon. run_id is stable across re-runs.
SMOKE_IMAGE: pi-devbox:smoke-studio-${{ github.run_id }}
container:
image: catthehacker/ubuntu:act-latest
steps:
@@ -675,8 +695,7 @@ jobs:
/usr/local/lib/android /usr/local/share/powershell \
/usr/local/share/chromium /usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
bash scripts/ci-reclaim-docker.sh
- uses: docker/setup-buildx-action@v4
with: {driver-opts: network=host}
- uses: docker/login-action@v3
@@ -691,7 +710,8 @@ jobs:
platforms: linux/amd64
push: false
load: true
tags: pi-devbox:smoke-studio
tags: ${{ env.SMOKE_IMAGE }}
labels: ${{ env.SMOKE_IMAGE_LABEL }}=true
build-args: |
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
@@ -716,7 +736,14 @@ jobs:
# Asserting the BUILT image matches it also catches a stale cached layer.
EXPECTED_NODE_MAJOR=$(sed -n 's/^ARG NODE_VERSION=\([0-9][0-9]*\).*/\1/p' Dockerfile.base)
export EXPECTED_NODE_MAJOR
bash scripts/smoke-test.sh pi-devbox:smoke-studio
bash scripts/ci-reclaim-docker.sh assert-protected "$SMOKE_IMAGE"
bash scripts/smoke-test.sh "$SMOKE_IMAGE"
# The label keeps this image from every job's reclaim, so it is removed
# here instead. A job killed before this step leaves an orphan, which the
# reclaim's age sweep deletes once it is older than SMOKE_IMAGE_MAX_AGE.
- name: Remove smoke image
if: always()
run: docker image rm "$SMOKE_IMAGE" || true
# ── Phase 4: multi-arch publish ─────────────────────────────────────
build-variant:
@@ -741,8 +768,7 @@ jobs:
/usr/local/lib/android /usr/local/share/powershell \
/usr/local/share/chromium /usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
bash scripts/ci-reclaim-docker.sh
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4
@@ -834,8 +860,7 @@ jobs:
/usr/local/lib/android /usr/local/share/powershell \
/usr/local/share/chromium /usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
bash scripts/ci-reclaim-docker.sh
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4