ci(390): address cold review — pin-drift guard, honest cron, doc fixes
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Failing after 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 15s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build CI Toolchain Image / Build & push CI image (push) Successful in 29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 6m6s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Failing after 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 15s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build CI Toolchain Image / Build & push CI image (push) Successful in 29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 6m6s
Cold adversarial review findings: - MEDIUM (Renovate generates pin drift): add a blocking ci-image-pin job. Renovate manages docker/ci/Dockerfile's base pins but cannot bump an opaque :<sha> in container.image, so a base bump would publish a new image, test the OLD one, and merge with the Dockerfile disagreeing with the pin. The guard fails when the pin isn't the last commit to touch docker/ci, or when the 5 jobs pin different tags — making the documented two-step enforced rather than remembered. - MEDIUM (cron was a no-op): the weekly rebuild updated nothing (jobs pin :<sha>) and buildcache would have restored the apt layer verbatim, collecting none of the base updates it existed for. Added no-cache on the schedule path and rewrote the comment to state what it actually is: a build canary + a fresh :latest for the next bump. - LOW: FFMPEG_TAG was referenced in the Dockerfile + docs but never existed (the FROM is hardcoded); reworded both. - LOW: paths: filtered the exact file while the docs claimed docker/ci/** — use **. - NIT: docs oversold ENTRYPOINT reset as a gotcha; act overrides it anyway. Marked defensive. Refs #390
This commit is contained in:
@@ -25,11 +25,19 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'docker/ci/Dockerfile'
|
||||
- 'docker/ci/**'
|
||||
- '.gitea/workflows/ci-image.yml'
|
||||
schedule:
|
||||
# Mondays 05:00 UTC — an hour before the dependency scan, so a fresh toolchain is in place
|
||||
# before the week's runs. Gitea registers `schedule` only from the default branch (main).
|
||||
# Mondays 05:00 UTC. Gitea registers `schedule` only from the default branch (main).
|
||||
#
|
||||
# What this cron does and does NOT do — it does **not** update any running job. The jobs in
|
||||
# docker-build.yml pin an immutable :<sha> (deliberately), so a rebuilt image is consumed only
|
||||
# when a human bumps that pin. Its actual value is twofold:
|
||||
# 1. a weekly CANARY — catches "the toolchain image no longer builds" (a NodeSource/apt/base
|
||||
# change) at a time of our choosing, rather than when you next need to bump the pin;
|
||||
# 2. it leaves a freshly-patched :latest so the next pin bump starts from a current base.
|
||||
# `no-cache` on this path is what makes both real: with the shared :buildcache, the
|
||||
# `apt-get update && apt-get install` layer would restore from cache and re-fetch nothing.
|
||||
- cron: '0 5 * * 1'
|
||||
|
||||
# Serialize per ref: concurrent builds would race on the shared :buildcache tag.
|
||||
@@ -98,6 +106,10 @@ jobs:
|
||||
push: true
|
||||
provenance: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
# The scheduled rebuild must bypass the cache or it is pointless: `mode=max` buildcache
|
||||
# would restore the `apt-get update && apt-get install` layer verbatim and pull in none of
|
||||
# the base updates the cron exists to collect. Push-triggered builds keep the cache.
|
||||
no-cache: ${{ github.event_name == 'schedule' }}
|
||||
cache-from: type=registry,ref=192.168.1.95:3000/timothy/ersatztv-ci:buildcache
|
||||
cache-to: type=registry,ref=192.168.1.95:3000/timothy/ersatztv-ci:buildcache,mode=max,ignore-error=true
|
||||
|
||||
|
||||
@@ -453,6 +453,48 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# BLOCKING (ersatztv#390): the CI toolchain image pin in this file must name the image that
|
||||
# ci-image.yml actually last published — i.e. the short sha of the last commit to touch the image's
|
||||
# sources. Without this detector, a PR that edits docker/ci/** publishes a NEW image but runs its own
|
||||
# jobs against the OLD pin: CI green-lights a toolchain it never executed, and once merged, main's
|
||||
# Dockerfile silently disagrees with what CI runs. **Renovate actively generates exactly that PR** —
|
||||
# it manages docker/ci/Dockerfile's base pins (dockerfile manager) but cannot bump an opaque
|
||||
# `:<sha>` in `container.image`, so it would leave the pin behind every time.
|
||||
#
|
||||
# Failing here forces the documented two-step (docs/ci-cd.md -> "CI toolchain image"): push the
|
||||
# Dockerfile change, let ci-image.yml publish `:<sha>`, then update the pin to that sha. Seconds-long
|
||||
# git+grep -> keep it off the build runners.
|
||||
ci-image-pin:
|
||||
name: CI image pin matches docker/ci
|
||||
runs-on: small
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# need real history: `git log -- <path>` on a shallow clone can't find the last
|
||||
# commit that touched the image sources
|
||||
fetch-depth: 0
|
||||
- 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)"
|
||||
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[*]}"
|
||||
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')."
|
||||
exit 1
|
||||
fi
|
||||
echo "Pin is current: ersatztv-ci:${pins[0]} matches docker/ci @ ${expected}."
|
||||
|
||||
# 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.
|
||||
# Deliberately no setup-dotnet/setup-node (and thus no actions/cache) so it can't hit the
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
# (see renovate.json). The two dotnet tool versions mirror what docker-build.yml used to
|
||||
# `dotnet tool install` per run — bump them here, not in the workflow.
|
||||
|
||||
# Bumping FFMPEG_TAG: keep it equal to the tag docker/Dockerfile pins, so CI's ffmpeg stays
|
||||
# prod-identical. Renovate manages that Dockerfile's pin; mirror it here.
|
||||
# Keep the ffmpeg tag on the FROM below equal to the one docker/Dockerfile pins, so CI's ffmpeg
|
||||
# stays prod-identical. Renovate manages both pins (dockerfile manager); mirror any bump here.
|
||||
FROM --platform=linux/amd64 192.168.1.95:3000/timothy/ersatztv-ffmpeg:8.1.2 AS ci-base
|
||||
|
||||
# The ffmpeg base sets ENTRYPOINT ["ffmpeg"] / CMD ["--help"] because it ships as an ffmpeg CLI.
|
||||
|
||||
+15
-5
@@ -231,9 +231,9 @@ commit and stay on `actions/cache` (`~/.nuget/packages`, `~/.npm`).
|
||||
**How it's layered:** `FROM ersatztv-ffmpeg:8.1.2` + `COPY --from=mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64
|
||||
/usr/share/dotnet` — the same pattern `docker/Dockerfile` uses for the prod image. Our ffmpeg base is
|
||||
`ghcr.io/linuxserver/baseimage-ubuntu:noble`, the same Ubuntu release as the SDK image, so the copied
|
||||
SDK matches the base's glibc/ICU. Keep `FFMPEG_TAG` equal to the tag `docker/Dockerfile` pins, so CI's
|
||||
ffmpeg stays prod-identical — that fidelity is what the ersatztv#299 seeded-media/scanner E2E
|
||||
follow-ups will need.
|
||||
SDK matches the base's glibc/ICU. Keep the ffmpeg tag on that `FROM` equal to the one
|
||||
`docker/Dockerfile` pins, so CI's ffmpeg stays prod-identical — that fidelity is what the
|
||||
ersatztv#299 seeded-media/scanner E2E follow-ups will need.
|
||||
|
||||
**Bumping the toolchain is a deliberate two-step.** The jobs pin an immutable `:<sha>`, never
|
||||
`:latest`, so a bad toolchain push cannot break every job at once:
|
||||
@@ -260,8 +260,18 @@ registry. Renovate tracks the Dockerfile's image pins (`dockerfile` manager, see
|
||||
- `actions/checkout` clones as root into a mounted workspace, which trips git's *"detected dubious
|
||||
ownership"* guard and breaks every `git` call in a step. Fixed in the Dockerfile with
|
||||
`git config --global --add safe.directory '*'`.
|
||||
- The ffmpeg base sets `ENTRYPOINT ["ffmpeg"]` because it ships as an ffmpeg CLI. A job container is
|
||||
started long-lived with steps exec'd into it, so the Dockerfile resets both `ENTRYPOINT` and `CMD`.
|
||||
- *(defensive, not load-bearing)* The ffmpeg base sets `ENTRYPOINT ["ffmpeg"]` because it ships as an
|
||||
ffmpeg CLI, so the Dockerfile resets `ENTRYPOINT`/`CMD`. act overrides the entrypoint anyway
|
||||
(`entrypoint=["/bin/sleep" "10800"]`), so this is belt-and-braces for anyone running the image by
|
||||
hand — unlike the two above, which are real.
|
||||
|
||||
**Bumping the pin is enforced, not remembered.** The `ci-image-pin` job (blocking, PR-only) fails if
|
||||
`docker-build.yml`'s pin isn't the short sha of the last commit to touch `docker/ci/**` or
|
||||
`ci-image.yml`, or if the five jobs ever pin different tags. This exists because **Renovate manages
|
||||
`docker/ci/Dockerfile`'s base pins but cannot bump an opaque `:<sha>`** in `container.image` — so a
|
||||
Renovate base bump would otherwise publish a new image, test the *old* one, and merge with the
|
||||
Dockerfile disagreeing with the pin. A red `ci-image-pin` means: let `ci-image.yml` publish the new
|
||||
`:<sha>`, then update all five pins to it.
|
||||
|
||||
**What it is and isn't worth.** Measured honestly (ersatztv#390): the image saves ~15–40s per job
|
||||
(`setup-dotnet` is 8–19s, `setup-node` 2–5s cached, the two tool installs ~9s) plus the 110s
|
||||
|
||||
Reference in New Issue
Block a user