fix(ci): serialize CI per ref + tag images at build time (fd race #1093)
The publish lane raced on the runner's shared docker daemon when two dev
pushes landed seconds apart: a concurrent run evicted the freshly-built
:<sha> image mid-push, so the post-push `docker tag :<sha> :dev` failed
with "No such image" (a flake — a lone re-run went green).
Two guards, both matching CI-runner's canonical build workflows:
- Workflow-level `concurrency: ci-${{ github.ref }}` with
cancel-in-progress:false serializes runs per ref, so no two publishes
ever share the daemon. false (not true) because rule 46 requires every
push to emit its own immutable :<sha> image — a superseded run must
still finish.
- Build both the :<sha> and moving (:dev/:latest) tags in one
`docker build -t ... -t ...`, then push each. Removes the fragile
post-push `docker tag` of an image a concurrent run could have evicted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAGR73DUowdVFVvYzLXC5C
This commit is contained in:
@@ -14,6 +14,17 @@ on:
|
||||
# pull_request intentionally absent — push on [dev, main] already fires CI for
|
||||
# every dev commit and dev→main PRs. Single-operator repo, no fork PRs.
|
||||
|
||||
# Serialize CI runs per ref so the publish lane never shares the runner's docker
|
||||
# daemon with another run. Two publishes racing on one daemon evict each other's
|
||||
# freshly-built image mid-push, breaking the post-build tag/push (issue #1093).
|
||||
# cancel-in-progress:false is deliberate — rule 46 requires EVERY dev/main push to
|
||||
# publish its own immutable :<sha> image, so a superseded run must still run to
|
||||
# completion (never cancelled) to emit its SHA. (Forgejo honors `concurrency:` —
|
||||
# CI-runner's build workflows use it.)
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Fast-fail lint lane. ruff is pre-installed in the ci-python image, so this
|
||||
# runs with NO dependency install and surfaces lint bounces in seconds.
|
||||
@@ -115,14 +126,25 @@ jobs:
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
IMAGE=git.fabledsword.com/bvandeusen/steward
|
||||
docker build -t "$IMAGE:${{ github.sha }}" .
|
||||
docker push "$IMAGE:${{ github.sha }}"
|
||||
# The moving tag for this ref: dev→:dev, main→:latest (rule 46). main IS
|
||||
# the production line, so :latest tracks main's tip; there is no :main.
|
||||
MOVING=""
|
||||
if [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:dev"
|
||||
docker push "$IMAGE:dev"
|
||||
MOVING="dev"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:latest"
|
||||
docker push "$IMAGE:latest"
|
||||
MOVING="latest"
|
||||
fi
|
||||
# Tag BOTH targets at build time (mirrors CI-runner's build workflows):
|
||||
# one `docker build -t :<sha> -t :<moving>` points both tags at the image
|
||||
# atomically, so we never run a post-push `docker tag` of an image a
|
||||
# concurrent run could have evicted from the shared daemon (issue #1093).
|
||||
if [ -n "$MOVING" ]; then
|
||||
docker build -t "$IMAGE:${{ github.sha }}" -t "$IMAGE:$MOVING" .
|
||||
docker push "$IMAGE:${{ github.sha }}"
|
||||
docker push "$IMAGE:$MOVING"
|
||||
else
|
||||
docker build -t "$IMAGE:${{ github.sha }}" .
|
||||
docker push "$IMAGE:${{ github.sha }}"
|
||||
fi
|
||||
- name: Prune dangling layers
|
||||
if: always()
|
||||
|
||||
Reference in New Issue
Block a user