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>
2 files changed,
+92
−11
Makefile+24 −1internal/www/links_test.go+68 −10
CI cancelled — superseded by a newer push — run details
Intent
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 |