rastrillo / fiesta Public

Build the shipped site from its release tag with make site-publish

fiesta site -published is the one build that promises a release a
year, so the command that ships the site has to name what is
published, and name it from what is really published, not from
whatever a person types at ship time. make site-publish
SITE_REF=vX.Y.Z does that, then leaves the tree in
.build/publish/public for carlos ship.

Before it builds anything, it refuses unless SITE_REF has the shape
vX.Y.Z, is an annotated tag here, is the same tag on origin, holds
releases/X.Y.Z/, and is the newest X.y.z there, so /X/ serves it;
and unless the tag's own fiesta site has -published. A tag from
before -published existed would otherwise build a site with no
release promised and no sign that anything was missed. It then runs
site-links-published on the same tag, so the links are checked
against what ships, and builds from git archive of the tag with the
tag's own fiesta site, naming every release directory in the tag's
tree as published. fiesta site compares each with its tag and
writes nothing if one differs. A run that fails after building
removes .build/publish/public, so a refused site is never left there
to ship.

Like site-links-published, each step fails with its own message and
the export goes in an EXIT trap that keeps the step's status. go run
takes -buildvcs=false: the export sits in TMPDIR, which may be inside
another repository, and VCS stamping there fails the build.

The site target's comment no longer says it caches releases for a
year, and points to this target for the site that ships.

Proved by a scratch run in a scratch clone with its own bare origin:
see the branch's discussion for each refusal and the passing build.
Human

Prompt

Guard the opt-in. Add a Makefile target used for shipping, for example make site-publish SITE_REF=v1.1.0. It refuses unless SITE_REF is an annotated tag v<x.y.z> and every release directory it promises as immutable matches a tag (reuse what TestPublishedReleasesMatchTheirTags checks). It then builds from git archive of that tag and passes the published list. Build the export the way site-links-published does now: steps that fail loudly, cleanup in a trap. Order: the published-links check (site-links-published) still runs before shipping. Say how the two targets sit together. Prove the Makefile guard with a scratch run.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

make site-publish SITE_REF=vX.Y.Z checks the tag, runs the link check, and builds the tag's site with every release in it published.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 03bee3bf7561909496a34cecd9021b8434327c78 parent db55c30
1 file changed, +76 −4
  • Makefile +76 −4
diff --git a/Makefile b/Makefile
index 8974d27..5ec1bde 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: ci suite suite-roles gofmt no-rastrillo size history root staticcheck govulncheck gitleaks browser heights-sweep site site-links-published
+.PHONY: ci suite suite-roles gofmt no-rastrillo size history root staticcheck govulncheck gitleaks browser heights-sweep site site-links-published site-publish
# CGO_ENABLED=0 across the tree is an acceptance criterion, not a
# preference: Fiesta is CSS, scripts and the Go that embeds and checks
@@ -257,14 +257,15 @@ heights-sweep: | $(BIN)/tmp
# site assembles fiesta.rastrillo.org into .build/site/public from the
# committed releases and the site source: each release under its exact
# version, _redirects sending /M/* to the newest M.y.z and the design
-# system's former addresses to their pages, _headers caching each
-# release for a year and /M/ for five minutes, and at the root the pages
+# system's former addresses to their pages, _headers caching /M/ and
+# each release for five minutes, and at the root the pages
# internal/www assembles from site/, the landing page, the design
# system under /design-system/ and site/static/. The tree is
# .build/site/public; the command's own .fiesta stamp sits above it in
# .build/site, so nothing hidden is in the shipped tree. Nothing in ci
# depends on this target, since the releases are committed and checked
-# there.
+# there. It is for looking at: the site that ships is site-publish's,
+# the one build that lets browsers keep a release for a year.
site: | $(BIN)/tmp
go run ./cmd/fiesta site -releases releases -site site -out .build/site
@@ -304,6 +305,77 @@ site-links-published: | $(BIN)/tmp
fi; \
cd "$$d/tree" && FIESTA_LINKS_PUBLISHED=1 go test ./internal/www -run '^TestEveryLinkInTheSiteResolves$$' -count=1 -v
+# site-publish builds the site that ships, from a release tag:
+# make site-publish SITE_REF=vX.Y.Z, then carlos ship
+# .build/publish/public. It is the one build that tells browsers to
+# keep a release for a year as immutable. Every other build keeps each
+# release five minutes, because until its tag exists a release may be
+# rebuilt as a candidate under the same name, and a browser told it
+# never changes would hold the old bytes for a year.
+#
+# It refuses, before building anything, unless SITE_REF is an
+# annotated tag vX.Y.Z here, origin holds the same tag, the tag's tree
+# has releases/X.Y.Z/ and /X/ would serve it, and the tag's fiesta site
+# can be told what is published; then it runs site-links-published on
+# the same tag, so the links are checked against what ships. It builds
+# from git archive of the tag, as site-links-published does, with the
+# tag's own fiesta site, and names every release directory in the
+# tag's tree as published. fiesta site checks each against its tag
+# vX.Y.Z here, file for file (site.TagDifferences, which
+# TestPublishedReleasesMatchTheirTags runs on every push), and writes
+# nothing if one differs. A tag from before -published existed is
+# refused, rather than shipped with no _headers. The two targets sit
+# together this way: site-links-published stays the check to run on
+# any ref before tagging, and site-publish runs it again on the tag as
+# its own first build step, so nothing ships unchecked.
+#
+# Each step that can fail ends the recipe with its own failure, and
+# the export goes in an EXIT trap, as in site-links-published. The
+# built site goes to .build/publish, outside the export, so it
+# outlives the trap for carlos ship to read; a run that fails after
+# building removes .build/publish/public, so a refused site is never
+# there to ship.
+PUBLISH_OUT := $(BIN)/publish
+site-publish: | $(BIN)/tmp
+ @ref='$(SITE_REF)'; \
+ printf '%s\n' "$$ref" | grep -Eqx 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' || { \
+ echo "site-publish: SITE_REF=$$ref is not a release tag vX.Y.Z; name the tag the site ships from"; exit 1; }; \
+ version=$${ref#v}; major=$${version%%.*}; \
+ [ "$$(git cat-file -t "refs/tags/$$ref" 2>/dev/null)" = tag ] || { \
+ echo "site-publish: $$ref is not an annotated tag here; a release is tagged with git tag -a and pushed"; exit 1; }; \
+ here=$$(git rev-parse "refs/tags/$$ref") || exit 1; \
+ there=$$(GIT_TERMINAL_PROMPT=0 git ls-remote --tags origin "refs/tags/$$ref") || { \
+ echo "site-publish: could not ask origin for $$ref"; exit 1; }; \
+ [ "$${there%%[[:space:]]*}" = "$$here" ] || { \
+ echo "site-publish: origin does not hold this $$ref ($$here); push the tag, or fetch origin's"; exit 1; }; \
+ d=$$(mktemp -d) || exit 1; \
+ trap 's=$$?; rm -rf "$$d" || { echo "site-publish: could not remove $$d"; \
+ [ $$s -ne 0 ] || s=1; }; exit $$s' EXIT; \
+ trap 'exit 130' INT TERM; \
+ git archive --format=tar -o "$$d/site.tar" "refs/tags/$$ref" || { \
+ echo "site-publish: git archive could not export $$ref"; exit 1; }; \
+ mkdir "$$d/tree" && tar -x -C "$$d/tree" -f "$$d/site.tar" || { \
+ echo "site-publish: could not extract $$ref's tree"; exit 1; }; \
+ [ -d "$$d/tree/releases/$$version" ] || { \
+ echo "site-publish: $$ref holds no releases/$$version, so the site would not serve the release it is shipped for"; exit 1; }; \
+ newest=$$(ls "$$d/tree/releases" | grep -E "^$$major\.[0-9]+\.[0-9]+$$" | sort -t. -k2,2n -k3,3n | tail -n 1); \
+ [ "$$newest" = "$$version" ] || { \
+ echo "site-publish: /$$major/ would serve $$newest, not $$version; ship the tag of the newest $$major.x"; exit 1; }; \
+ grep -q '"published"' "$$d/tree/cmd/fiesta/main.go" || { \
+ echo "site-publish: $$ref's fiesta site has no -published, so it cannot tell browsers a release never changes; ship a tag that has it"; exit 1; }; \
+ published=$$(ls "$$d/tree/releases" | paste -sd, -); \
+ repo=$$(git rev-parse --show-toplevel) || exit 1; \
+ $(MAKE) --no-print-directory site-links-published SITE_REF="refs/tags/$$ref" || { \
+ echo "site-publish: the links of $$ref's site do not all resolve against its published releases"; exit 1; }; \
+ echo "site-publish: building $$ref with $$published published"; \
+ (cd "$$d/tree" && go run -buildvcs=false ./cmd/fiesta site -releases releases -site site \
+ -out '$(PUBLISH_OUT)' -published "$$published" -git "$$repo") || { \
+ echo "site-publish: fiesta site refused $$ref"; exit 1; }; \
+ grep -qxF "/$$major/* /$$version/:splat 200" '$(PUBLISH_OUT)/public/_redirects' || { \
+ rm -rf '$(PUBLISH_OUT)/public'; \
+ echo "site-publish: the built _redirects does not send /$$major/ to $$version; removed $(PUBLISH_OUT)/public"; exit 1; }; \
+ echo "site-publish: $(PUBLISH_OUT)/public is $$ref's site, $$published kept for a year; ship that tree"
+
root no-rastrillo size staticcheck govulncheck gitleaks: | $(BIN)/tmp
$(BIN)/tmp: