diff --git a/.gitignore b/.gitignore index cf5bd37..356629e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Build output /foodster +# The tag `make image` last built, handed to `make push`. +/.release-tag + # Generated by `templ generate` during the container build. *_templ.go diff --git a/Makefile b/Makefile index 47af48c..6a5b8da 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,11 @@ BIN := foodster PKG := ./cmd/foodster STATIC := cmd/foodster/static +# What `make image` last built. push reads it rather than re-deriving the tag: +# sorting tags by date is ambiguous when two point at the same commit, and +# re-deriving is what let a parallel make push the wrong one. +TAGFILE := .release-tag + # Vendored Datastar client. Bump, run `make vendor`, commit the result. DATASTAR_VERSION ?= v1.0.3 SEED ?= seeds/testi.json @@ -94,16 +99,41 @@ image: ## Build and tag an image as vYYYYMMDD-N. Creates a git tag. echo "==> $$tag"; \ git tag "$$tag"; \ podman build --platform linux/amd64 --build-arg VERSION="$$tag" \ - -t "$(FOODSTER_REPO):$$tag" -t "$(FOODSTER_REPO):latest" . + -t "$(FOODSTER_REPO):$$tag" -t "$(FOODSTER_REPO):latest" . ; \ + echo "$$tag" > $(TAGFILE) -push: ## Push the newest tag and :latest +# Pushing reported success while uploading the previous release once, because +# nothing compared what was built against what arrived. So afterwards, ask the +# registry what it actually serves for each tag and fail if it is not the +# image we just built. +push: ## Push the newest tag and :latest, then verify the registry @test -n "$(FOODSTER_REPO)" || { echo "set FOODSTER_REPO in .env"; exit 1; } - @tag=$$(git tag -l 'v*' --sort=-creatordate | head -n1); \ - test -n "$$tag" || { echo "no tags yet - run make image"; exit 1; }; \ + @test -f $(TAGFILE) || { echo "nothing built - run make image"; exit 1; }; \ + tag=$$(cat $(TAGFILE)); \ + built=$$(podman image inspect "$(FOODSTER_REPO):$$tag" --format '{{.Id}}' 2>/dev/null) || \ + { echo "no local image tagged $$tag - run make image"; exit 1; }; \ podman push "$(FOODSTER_REPO):$$tag"; \ - podman push "$(FOODSTER_REPO):latest" + podman push "$(FOODSTER_REPO):latest"; \ + echo "==> verifying $$tag"; \ + for ref in "$$tag" latest; do \ + podman pull -q "$(FOODSTER_REPO):$$ref" >/dev/null 2>&1 || \ + { echo " FAIL $$ref is not in the registry"; exit 1; }; \ + served=$$(podman image inspect "$(FOODSTER_REPO):$$ref" --format '{{.Id}}'); \ + if [ "$$served" != "$$built" ]; then \ + echo " FAIL $$ref serves $$served"; \ + echo " expected $$built"; \ + exit 1; \ + fi; \ + echo " ok $$ref"; \ + done -release: image push ## Build, tag and push in one go +# Sub-makes, not prerequisites. Under `make -j` — and -j16 is the default on +# at least one machine here — these run concurrently, so push resolves the +# newest tag and uploads :latest before image has finished building and +# tagging. That silently ships the previous release a second time. +release: ## Build, tag and push in one go + @$(MAKE) --no-print-directory image + @$(MAKE) --no-print-directory push up: ## Start the stack @mkdir -p data # or the engine creates it root-owned and the app cannot write