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
137 lines
6.0 KiB
YAML
137 lines
6.0 KiB
YAML
name: Build CI Toolchain Image
|
|
|
|
# Builds the shared CI toolchain image (.NET 10 SDK + Node 22 + prod-identical ffmpeg) and
|
|
# pushes it to the Gitea container registry (ersatztv#390). The toolchain jobs in
|
|
# docker-build.yml consume it via `container:`, pinned to an immutable :<sha>.
|
|
#
|
|
# push touching docker/ci/** -> :<short-sha> (+ :latest only from main)
|
|
# workflow_dispatch -> manual rebuild
|
|
# schedule (weekly) -> picks up base-image security updates
|
|
#
|
|
# Deliberately separate from docker-build.yml: this image changes rarely (a Dockerfile edit or
|
|
# the weekly cron), while docker-build.yml runs on every push/PR. Coupling them would rebuild a
|
|
# ~2GB toolchain image on every commit.
|
|
#
|
|
# ROLLOUT NOTE: the jobs pin an immutable :<sha>, never :latest — a broken toolchain image would
|
|
# otherwise block every converted job the moment it was pushed. Bumping the toolchain is therefore
|
|
# a deliberate two-step: merge a docker/ci/Dockerfile change (this workflow publishes a new :<sha>),
|
|
# then update the pin in docker-build.yml in a follow-up PR whose CI proves the new image works.
|
|
# See docs/ci-cd.md -> "CI toolchain image".
|
|
#
|
|
# Like docker-build.yml: the Gitea registry is HTTP-only, so BuildKit needs the inline
|
|
# `http = true` config (it does not inherit the host daemon's insecure-registries setting).
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
push:
|
|
paths:
|
|
- 'docker/ci/**'
|
|
- '.gitea/workflows/ci-image.yml'
|
|
schedule:
|
|
# 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.
|
|
# No cancel-in-progress — a half-pushed toolchain image is worse than a redundant build.
|
|
concurrency:
|
|
group: ersatztv-ci-image-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
REGISTRY: 192.168.1.95:3000
|
|
CI_IMAGE: 192.168.1.95:3000/timothy/ersatztv-ci
|
|
|
|
jobs:
|
|
build:
|
|
name: Build & push CI image
|
|
# `small` = the small-jobs runner lane. This is a docker-only job (no toolchain needed —
|
|
# it *builds* the toolchain), same as docker-build.yml's `build` job.
|
|
runs-on: small
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
# only docker/ci/Dockerfile is needed; no git describe/log here
|
|
fetch-depth: 1
|
|
|
|
- name: Compute tags
|
|
id: meta
|
|
run: |
|
|
set -euo pipefail
|
|
SHORT=$(git rev-parse --short HEAD)
|
|
# Always publish the immutable :<sha> — that is what docker-build.yml pins.
|
|
TAGS=("${CI_IMAGE}:${SHORT}")
|
|
# :latest is a convenience/floating pointer for humans and the weekly rebuild; jobs must
|
|
# never consume it. Only main may move it.
|
|
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
|
|
TAGS+=("${CI_IMAGE}:latest")
|
|
fi
|
|
echo "short=${SHORT}" >> "$GITHUB_OUTPUT"
|
|
{
|
|
echo "tags<<__EOT__"
|
|
printf '%s\n' "${TAGS[@]}"
|
|
echo "__EOT__"
|
|
} >> "$GITHUB_OUTPUT"
|
|
printf 'tag: %s\n' "${TAGS[@]}"
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
with:
|
|
buildkitd-config-inline: |
|
|
[registry."192.168.1.95:3000"]
|
|
http = true
|
|
|
|
- name: Login to Gitea registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ secrets.REGISTRY_USER }}
|
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: ./docker/ci/Dockerfile
|
|
platforms: linux/amd64
|
|
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
|
|
|
|
# The Dockerfile's own build-time smoke test (dotnet --info, node, ffmpeg, ...) already ran
|
|
# inside the build. This re-checks the *pushed* artifact end-to-end: that the registry copy
|
|
# pulls and its toolchain runs, which is exactly what `container:` will do on every job.
|
|
- name: Verify the pushed image
|
|
run: |
|
|
set -euo pipefail
|
|
IMG="${CI_IMAGE}:${{ steps.meta.outputs.short }}"
|
|
echo "Pulling ${IMG}"
|
|
docker pull "$IMG"
|
|
docker run --rm --entrypoint /bin/bash "$IMG" -euxc '
|
|
dotnet --version
|
|
dotnet ef --version
|
|
node --version
|
|
ffmpeg -version | head -1
|
|
git --version
|
|
python3 --version
|
|
# reportgenerator --version exits 1 ("No report files specified"); probe the shim.
|
|
command -v reportgenerator
|
|
'
|
|
echo "CI image OK. Pin this in .gitea/workflows/docker-build.yml -> CI_IMAGE_REF:"
|
|
echo " ${IMG}"
|