diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c54af4d6..3fcac0be 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -28,6 +28,34 @@ on: required: true type: string +# Only one publish of this image at a time. +# +# Three triggers feed this workflow and nothing stopped them overlapping. On +# 2026-09-12 two dispatches for 1.3.6 ran simultaneously (17:02:37 and +# 17:04:52); one was cancelled by hand. Both would have pushed :X.Y.Z, :X.Y, +# :X and :latest, and the last writer of :latest wins — so a slower run of an +# OLDER version can land on top of a newer one and quietly become what every +# `docker pull nself/nself-admin` gets. +# +# The group is deliberately a constant rather than the version. Serialising +# per-version would still let 1.3.6 and 1.3.7 race, and it is precisely the +# shared floating tags they contend for. One image, one publisher. +# +# cancel-in-progress is false on purpose. Cancelling a publish mid-push is not +# a safe stop: it is what happened to release.yml on v1.3.6, which was killed +# partway through pushing and left the manifest unfinished. A queued run costs +# a few minutes; a half-pushed manifest is a broken :latest. +# +# Known trade-off: GitHub keeps only ONE pending run per group, so a third +# request cancels the second while it waits. That is the right outcome for the +# duplicate-dispatch case this fixes (same version twice — the redundant one +# should go). For distinct versions queued three deep it would drop the middle +# one; the Hygiene release-tag gate catches a version whose image never +# published, and releases here are sequential and rare. +concurrency: + group: docker-publish-nself-admin + cancel-in-progress: false + env: DOCKER_IMAGE: nself/nself-admin