ci(390): bump pin to ersatztv-ci:07048b8; compare full shas in pin guard
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m32s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 4m34s

Pin bump for the image rebuilt from the review-fix commit (the two-step working
as designed — the new ci-image-pin guard demanded it).

Also fixes a real bug in that guard, caught locally before CI: it compared
abbreviated shas, but git auto-scales abbreviation length with object count, so
the tag ci-image.yml builds from a fetch-depth:1 shallow clone is 7 chars while
%h on the guard's full clone is 8 — it would have failed on every run. Now
resolves both sides to full shas via rev-parse, which also catches a pin that
names no commit at all.

Verified non-vacuous locally: PASS on the real pin, FAIL on a stale pin, FAIL on
an unresolvable one.

Refs #390
This commit is contained in:
2026-07-17 09:37:05 +02:00
parent 07048b8c96
commit ed52892d68
+23 -14
View File
@@ -25,7 +25,7 @@ name: Build ErsatzTV Image
# cannot read the workflow `env` context. **Bump all five together**; see docs/ci-cd.md ->
# "CI toolchain image" for the two-step procedure.
#
# CI image pin: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
# CI image pin: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
on:
workflow_dispatch:
@@ -65,7 +65,7 @@ jobs:
name: Build & test (.NET)
runs-on: ubuntu-latest
container:
image: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
image: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
@@ -164,7 +164,7 @@ jobs:
name: EF migration integrity (SQLite + MySql)
runs-on: ubuntu-latest
container:
image: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
image: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
@@ -269,7 +269,7 @@ jobs:
# Runs on PRs and on main (regression net); skipped for v* tag builds.
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
container:
image: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
image: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
@@ -478,22 +478,31 @@ jobs:
- name: Verify the pin matches the last-published image
run: |
set -euo pipefail
# The tag ci-image.yml publishes is `git rev-parse --short HEAD` of the push that built it,
# and it only builds on pushes touching these paths — so the currently-published image is
# named by the last commit to touch them. Same abbreviation rules on both sides (core.abbrev).
expected="$(git log -1 --format=%h -- docker/ci .gitea/workflows/ci-image.yml)"
# ci-image.yml tags the image `git rev-parse --short HEAD` of the push that built it, and it
# only builds on pushes touching these paths — so the published image is named by the last
# commit to touch them.
#
# Compare RESOLVED FULL shas, never the abbreviations: git auto-scales abbreviation length
# with the repo's object count, so the tag built in CI from a `fetch-depth: 1` shallow clone
# is 7 chars while `%h` here (full clone) is 8. Comparing those strings would fail always.
expected="$(git log -1 --format=%H -- docker/ci .gitea/workflows/ci-image.yml)"
mapfile -t pins < <(grep -oE 'ersatztv-ci:[0-9a-f]+' .gitea/workflows/docker-build.yml | cut -d: -f2 | sort -u)
echo "Image sources last changed in: ${expected}"
echo "Pins found in docker-build.yml: ${pins[*]}"
echo "Pins found in docker-build.yml: ${pins[*]} (${#pins[@]} distinct)"
if [ "${#pins[@]}" -ne 1 ]; then
echo "::error::docker-build.yml pins MORE THAN ONE ersatztv-ci tag (${pins[*]}). All jobs must pin the same image — bump them together."
exit 1
fi
if [ "${pins[0]}" != "$expected" ]; then
echo "::error::CI toolchain image pin is stale: docker-build.yml pins ersatztv-ci:${pins[0]}, but docker/ci was last changed in ${expected}. Your jobs are testing an image that is NOT built from this PR's docker/ci. Let ci-image.yml publish :${expected}, then update the pin in ALL jobs to that sha (docs/ci-cd.md -> 'CI toolchain image')."
pin_full="$(git rev-parse --verify --quiet "${pins[0]}^{commit}" || true)"
if [ -z "$pin_full" ]; then
echo "::error::The pinned CI image tag ersatztv-ci:${pins[0]} does not resolve to a commit in this repo, so it cannot correspond to an image ci-image.yml built from these sources. Rebuild the image and pin the sha it prints."
exit 1
fi
echo "Pin is current: ersatztv-ci:${pins[0]} matches docker/ci @ ${expected}."
if [ "$pin_full" != "$expected" ]; then
echo "::error::CI toolchain image pin is stale: docker-build.yml pins ersatztv-ci:${pins[0]} ($pin_full), but docker/ci was last changed in $expected. Your jobs are testing an image that is NOT built from this PR's docker/ci. Let ci-image.yml publish the new :<sha>, then update the pin in ALL jobs to it (docs/ci-cd.md -> 'CI toolchain image')."
exit 1
fi
echo "Pin is current: ersatztv-ci:${pins[0]} resolves to $pin_full = docker/ci's last change."
# Non-blocking nudge: if a PR migrates/adds a route but forgets the parity tracker, warn.
# The rule lives in CLAUDE.md → Conventions; this only surfaces an easy-to-miss omission.
@@ -580,7 +589,7 @@ jobs:
# `dotnet build`, so it is not always a "small" job; capacity 4 absorbs that.
runs-on: small
container:
image: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
image: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
@@ -663,7 +672,7 @@ jobs:
# `ubuntu-latest` lane. See the api-docs job above for the full rationale.
runs-on: small
container:
image: 192.168.1.95:3000/timothy/ersatztv-ci:6a9e27c
image: 192.168.1.95:3000/timothy/ersatztv-ci:07048b8
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}