rastrillo / fiesta Public

Check the site's links against the published releases alone before it ships

The suite's link check lets a /1/ link reach the release the source
would build while that release is uncut. That is right for the
stretch before a release, and wrong at ship time: today releases/1.0.0/
exists and is /1/'s target, it has no copy.js, and the fallback passes
the link anyway. A site shipped before its release would 404 on it,
and the same can happen after tagging, when the fresh-dist test skips.

FIESTA_LINKS_PUBLISHED=1 turns the fallback off: /1/ resolves against
the newest release committed under releases/ and nothing else, as the
edge serves the tree being shipped. Any other non-empty value fails
the test rather than being read as off. make site-links-published runs
that mode on a git archive export of SITE_REF (HEAD unless told), so
nothing uncommitted counts, and refuses a ref whose link check
predates the mode instead of passing it through the fallback. It is
not in ci: it fails from a change to js/, css/ or i18n/ until the
release carrying it is committed, as it should, and today it fails on
/1/copy.js.

Regression control, in the suite: the scratch release 1.0.0 has
fiesta.css and no copy.js, the source build has both. With the
fallback the page's /1/copy.js resolves; published only, it fails,
beside /1/missing.js. Red first, with linkTree ignoring the mode as
the old check did: "with FIESTA_LINKS_PUBLISHED=1 the check still fell
back to the source build" and "published only, reported [/1/missing.js
...]; want only [/1/copy.js /1/missing.js] broken".

Runs: the suite's mode passes (the fallback taken); FIESTA_LINKS_PUBLISHED=1
fails on the real site with 36 lines, one per Getting started page,
each "/1/copy.js ... which the edge answers with a 404";
make site-links-published SITE_REF=95bd8cc refuses, its check having
no published-only mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a7ded2ffcbfef8afaf8e9223a2fc277a9bceaa41 parent 79e722b
2 files changed, +92 −11
  • Makefile +24 −1
  • internal/www/links_test.go +68 −10

CI cancelled — superseded by a newer push — run details

diff --git a/Makefile b/Makefile
index f9f6c00..3219b4a 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
+.PHONY: ci suite suite-roles gofmt no-rastrillo size history root staticcheck govulncheck gitleaks browser heights-sweep site site-links-published
# CGO_ENABLED=0 across the tree is an acceptance criterion, not a
# preference: Fiesta is CSS, scripts and the Go that embeds and checks
@@ -267,6 +267,29 @@ heights-sweep: | $(BIN)/tmp
site: | $(BIN)/tmp
go run ./cmd/fiesta site -releases releases -site site -out .build/site
+# site-links-published is the ship check for links: the commit
+# SITE_REF names (HEAD unless told), exported with git archive, so
+# nothing uncommitted or untracked in the checkout counts, and its
+# site's every link resolved as the edge will serve that tree, with
+# /1/ reaching the newest release committed under releases/ and
+# nothing else (FIESTA_LINKS_PUBLISHED=1). The suite's run of the same
+# test lets a /1/ link reach the release the source would build while
+# that release is uncut, so it passes a link that 404s the day the
+# site ships ahead of its release; this does not. Not in ci, for that
+# reason: it fails from a change to js/, css/ or i18n/ until the
+# release carrying it is committed, as it should. Run it, and see it
+# pass, before shipping the site.
+SITE_REF ?= HEAD
+site-links-published: | $(BIN)/tmp
+ @d=$$(mktemp -d) && status=0 && \
+ git archive --format=tar '$(SITE_REF)' | tar -x -C "$$d" && \
+ if ! grep -q FIESTA_LINKS_PUBLISHED "$$d/internal/www/links_test.go"; then \
+ echo "site-links-published: $(SITE_REF)'s link check has no published-only mode, so it would pass through the fallback; check a commit that has it"; status=1; \
+ else \
+ (cd "$$d" && FIESTA_LINKS_PUBLISHED=1 go test ./internal/www -run '^TestEveryLinkInTheSiteResolves$$' -count=1 -v) || status=1; \
+ fi; \
+ rm -rf "$$d"; exit $$status
+
root no-rastrillo size staticcheck govulncheck gitleaks: | $(BIN)/tmp
$(BIN)/tmp:
diff --git a/internal/www/links_test.go b/internal/www/links_test.go
index e82d648..b37635e 100644
--- a/internal/www/links_test.go
+++ b/internal/www/links_test.go
@@ -267,6 +267,33 @@ func servingSource(t *testing.T, tree map[string][]byte, version string, dist ma
return out
}
+// publishedOnlyEnv turns off the unpublished fallback below: the
+// ship check, make site-links-published, sets it on the exported tree
+// about to go out, where a /1/ link the newest published release
+// cannot answer is a 404 the day the site ships, whatever the source
+// would build. The fallback alone would pass such a link: today
+// releases/1.0.0/ exists and is /1/'s target, and it has no copy.js.
+const publishedOnlyEnv = "FIESTA_LINKS_PUBLISHED"
+
+// linkTree is the tree the link check reads: tree as site.Build made
+// it, or, while the source is ahead of releases/ and publishedOnly is
+// false, tree with dist served as the release to come (servingSource).
+// It reports whether it fell back to dist.
+func linkTree(t *testing.T, tree map[string][]byte, releases fs.FS, version string, dist map[string][]byte, publishedOnly bool) (map[string][]byte, bool) {
+ t.Helper()
+ if publishedOnly {
+ return tree, false
+ }
+ ahead, err := unpublished(releases, version, dist)
+ if err != nil {
+ t.Fatalf("reading releases/%s: %v", version, err)
+ }
+ if !ahead {
+ return tree, false
+ }
+ return servingSource(t, tree, version, dist), true
+}
+
// TestEveryLinkInTheSiteResolves: on every page of the built site,
// the landing page in each language, the design system's index, every
// example page and every frame, every href, src and srcset reaches a
@@ -283,15 +310,30 @@ func servingSource(t *testing.T, tree map[string][]byte, version string, dist ma
// /1/, and the site goes out with the release that carries them. Read
// against 1.0.0 alone, every new file is a 404 from the day it is
// added to the day the release is cut. Once releases/ holds what the
-// source builds, /1/ resolves against it as the edge will.
+// source builds, /1/ resolves against it as the edge will. With
+// FIESTA_LINKS_PUBLISHED=1 there is no fallback: /1/ resolves against
+// the newest published release only, as the edge serves the tree being
+// shipped. That mode fails from a change to the source until the
+// release that carries it is cut, by design, so it runs before a ship
+// (make site-links-published) and not in the suite.
//
// The control comes first: a scratch site whose one page links a
// missing page, a missing fragment, a wrong path under the site's own
// origin and an outside URL that is not on the list. The check has to
// name all four, or a clean run of the real site proves nothing. A
// second control holds the unpublished stretch: a file the source
-// builds resolves through /1/, and one it does not build still fails.
+// builds resolves through /1/, and one it does not build still fails;
+// and with the fallback off, the file only the source builds fails
+// too.
func TestEveryLinkInTheSiteResolves(t *testing.T) {
+ var publishedOnly bool
+ switch v := os.Getenv(publishedOnlyEnv); v {
+ case "":
+ case "1":
+ publishedOnly = true
+ default:
+ t.Fatalf("%s=%q; set it to 1 or leave it unset", publishedOnlyEnv, v)
+ }
control := map[string][]byte{
"_redirects": []byte("/1/* /1.0.0/:splat 200\n"),
"1.0.0/fiesta.css": []byte("body{}"),
@@ -321,18 +363,16 @@ func TestEveryLinkInTheSiteResolves(t *testing.T) {
return
}
- tree := builtSite(t)
dist, err := build.Dist(fiesta.FS(), fiesta.Version)
if err != nil {
t.Fatalf("building the release the source would publish: %v", err)
}
- ahead, err := unpublished(os.DirFS("../../releases"), fiesta.Version, dist)
- if err != nil {
- t.Fatalf("reading releases/%s: %v", fiesta.Version, err)
- }
- if ahead {
+ tree, fellBack := linkTree(t, builtSite(t), os.DirFS("../../releases"), fiesta.Version, dist, publishedOnly)
+ switch {
+ case publishedOnly:
+ t.Logf("%s=1: /1/ resolves against the newest published release only", publishedOnlyEnv)
+ case fellBack:
t.Logf("the source is ahead of releases/%s, so /1/ resolves against the release it builds", fiesta.Version)
- tree = servingSource(t, tree, fiesta.Version, dist)
}
problems, pages := linkProblems(t, tree)
for _, p := range problems {
@@ -379,14 +419,32 @@ func unpublishedControl(t *testing.T) {
"index.html": []byte(`<main id="main"><link rel="stylesheet" href="/1/fiesta.css">` +
`<script src="/1/copy.js"></script><script src="/1/missing.js"></script></main>`),
}
+ // releases/1.0.0/ as it is published: fiesta.css, and no copy.js.
+ published := map[string][]byte{"fiesta.css": []byte("body{}")}
+ published["SHA256SUMS"] = sumsOf(published)
+ releases := fstest.MapFS{}
+ for name, data := range published {
+ releases["1.0.0/"+name] = &fstest.MapFile{Data: data}
+ }
+ fallback, fellBack := linkTree(t, tree, releases, "1.0.0", dist, false)
+ if !fellBack {
+ t.Error("the unpublished control: a source build with copy.js, over a 1.0.0 without it, did not fall back to the release to come")
+ }
+ shipped, fellBack := linkTree(t, tree, releases, "1.0.0", dist, true)
+ if fellBack {
+ t.Errorf("the unpublished control: with %s=1 the check still fell back to the source build", publishedOnlyEnv)
+ }
for _, c := range []struct {
name string
tree map[string][]byte
want []string
}{
{"against 1.0.0", tree, []string{"/1/copy.js", "/1/missing.js"}},
- {"as the release to come", servingSource(t, tree, "1.0.0", dist), []string{"/1/missing.js"}},
+ {"as the release to come", fallback, []string{"/1/missing.js"}},
{"as a new version's release", servingSource(t, tree, "1.1.0", dist), []string{"/1/missing.js"}},
+ // The ship check: a file only the source builds is a 404 on the
+ // edge until it is released, and must fail here.
+ {"published only", shipped, []string{"/1/copy.js", "/1/missing.js"}},
} {
problems, _ := linkProblems(t, c.tree)
var got []string