rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1.PHONY: ci suite suite-roles gofmt no-rastrillo size history root staticcheck govulncheck gitleaks browser heights-sweep site site-links-published site-publish
2
3# CGO_ENABLED=0 across the tree is an acceptance criterion, not a
4# preference: Fiesta is CSS, scripts and the Go that embeds and checks
5# them, and nothing here may quietly pull in a cgo dependency because
6# a C toolchain happened to be present.
7export CGO_ENABLED = 0
8
9# The gate runs on one known Go, not whichever release the machine has.
10# govulncheck reports the standard library's vulnerabilities for the
11# toolchain doing the scan, so a runner one patch release behind would
12# fail the gate for code nobody changed, and a laptop and a runner on
13# different patches would disagree about what passing means. go fetches
14# this release through the module proxy on first use.
15#
16# A plain =, not ?=: Go's own container images set GOTOOLCHAIN=local, and
17# ?= would quietly keep whatever Go they ship. Override on the command
18# line if you must: make ci GOTOOLCHAIN=local. Raise it with each Go
19# security release; govulncheck naming a stdlib package "Found in" this
20# version is the signal.
21#
22# Held on 1.26 deliberately, as rastrillo is. Go 1.27 writes export
23# data that x/tools before v0.50.0 cannot read, and v0.50.0 needs go
24# 1.26 in go.mod: moving to 1.27 raises the Go floor for every app that
25# imports Fiesta, so it is its own change.
26export GOTOOLCHAIN = go1.26.10
27
28BIN := $(CURDIR)/.build
29
30# Keep compiler/linker scratch on the checkout filesystem when shared /tmp is full.
31export GOTMPDIR ?= $(BIN)/tmp
32
33# ci is the one gate: what a runner executes and what you run before
34# pushing are the same definition. .amadan/ci.d/ reports these one by
35# one; it never keeps its own copy of a command.
36#
37# The cheap checks go first and alone, so a stray gofmt still costs
38# seconds, and the size gate next, so a bundle over the line is the
39# first thing a red run says. history then makes a runner's shallow
40# clone whole, once, before the suite's shards share it. Everything
41# else is the suite.
42ci: gofmt no-rastrillo size history suite
43
44# The suite's roles. Under amadan, .amadan/ci.d/50-suite@N runs N
45# shards at once and each runs one role, so the run takes as long as
46# the slowest role rather than the sum of them all; by hand, or as a
47# step with no @N, the roles run one after another. N must equal the
48# number of roles here: hack/suite.sh refuses any other count rather
49# than run some roles twice and others nowhere, and ci_test.go checks
50# the two agree before a runner ever sees them.
51#
52# The slowest role is how long the suite takes, so the slow packages
53# are roles of their own, sliced until every role runs in under a
54# minute on a runner. The browser packages are a role each, running
55# only the tests the browser build adds (a package's plain tests run in
56# root, or in its test.<pkg> roles), and ROOT_SPLIT's packages leave
57# root's go test ./... for test.<pkg> roles. ci_test.go checks that
58# every test runs in exactly one role.
59# A package written pkg@n is n roles, each a slice of its tests, dealt
60# by hack/gotest.sh by the seconds in hack/test-weights.txt. In a
61# role's name a / in the path is a . so internal/gallery@2 becomes
62# browser.internal.gallery~1of2 and ~2of2. A test too long for a slice
63# of its own is dealt into parts by the test itself (the height drives,
64# the two axe sweeps, the HTML tab's renders, the floors drive). The
65# tasks that add a browser package append here and rename 50-suite@N
66# to match.
67BROWSER_PKGS := internal/harness check@14 internal/gallery@18 internal/gallery/history internal/www@4
68ROOT_SPLIT := internal/gallery@3 cmd/fiesta@2 internal/www@2
69go-roles = $(foreach p,$(2),$(if $(findstring @,$(p)),$(call go-slices,$(1),$(subst /,.,$(word 1,$(subst @, ,$(p)))),$(word 2,$(subst @, ,$(p)))),$(1).$(subst /,.,$(p))))
70go-slices = $(foreach k,$(shell seq 1 $(3)),$(1).$(2)~$(k)of$(3))
71BROWSER_ROLES := $(call go-roles,browser,$(BROWSER_PKGS))
72TEST_ROLES := $(call go-roles,test,$(ROOT_SPLIT))
73SUITE := root $(TEST_ROLES) staticcheck govulncheck gitleaks $(BROWSER_ROLES)
74
75suite:
76 @MAKE='$(MAKE)' ./hack/suite.sh $(SUITE)
77
78# What ci_test.go counts against the step's @N.
79suite-roles:
80 @echo $(SUITE)
81
82# The repo's own Go files, not everything under the checkout: GOTMPDIR is
83# .build/tmp, and a go command killed mid-build (a Ctrl-C, or a runner
84# cancelling a job a newer push superseded) leaves its generated
85# _testmain.go and cgo files there. gofmt -l . walked into them and failed
86# the next run on files nobody wrote. Dot-directories are pruned, the same
87# rule ./... applies. It is a walk of the working tree rather than a git
88# listing, so an unstaged deletion is simply absent instead of a missing
89# path. NUL-delimited so a path with a space stays one argument, and a
90# gofmt error (a file that does not parse) fails the target instead of
91# vanishing into stderr.
92gofmt:
93 @out=$$(find . -name '.?*' -type d -prune -o -name '*.go' -type f -print0 | xargs -0 gofmt -l) || exit 1; \
94 if [ -n "$$out" ]; then echo "gofmt needed on:"; echo "$$out"; exit 1; fi
95
96# Fiesta never imports Rastrillo, and the packages a Go program imports
97# from Fiesta never link a browser. Both are tests in norastrillo_test.go;
98# this runs them alone, first, so a broken promise is the first thing a
99# red run says.
100no-rastrillo:
101 go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$$' -count=1 .
102
103# size is the one-link promise: each one-file theme the release build
104# makes from the tree gzips under 25 KiB. One test, run alone and
105# verbosely so the three figures are in the log of every run; the
106# test's own package runs again inside root with the rest.
107size:
108 go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$$' -count=1 -v
109
110# FIESTA_TEST_REQUIRE_NODE turns every Node skip into a failure here: on
111# a laptop without Node a skip is honest, but a gate that lost node
112# would otherwise go green having checked none of the JavaScript.
113# Override with FIESTA_TEST_REQUIRE_NODE= to run without it.
114FIESTA_TEST_REQUIRE_NODE ?= 1
115# FIESTA_TEST_REQUIRE_GIT does the same for git: the tag guard of
116# fiesta release -candidate is tested in a scratch repository, and a
117# runner without git would otherwise pass with the guard untested.
118FIESTA_TEST_REQUIRE_GIT ?= 1
119# FIESTA_TEST_REQUIRE_TAGS does the same for the release tags:
120# releases_test.go decides what to check from them, and a clone with
121# none would pass it having compared nothing. make history fetches them
122# into a runner's shallow clone.
123FIESTA_TEST_REQUIRE_TAGS ?= 1
124TEST_ENV := FIESTA_TEST_REQUIRE_NODE=$(FIESTA_TEST_REQUIRE_NODE) FIESTA_TEST_REQUIRE_GIT=$(FIESTA_TEST_REQUIRE_GIT) FIESTA_TEST_REQUIRE_TAGS=$(FIESTA_TEST_REQUIRE_TAGS)
125# root builds, vets and tests every package but ROOT_SPLIT's, whose
126# tests run in the test.<pkg> roles; `make root $(TEST_ROLES)` is the
127# old whole. The browser drives are behind the browser tag and run as
128# roles of their own.
129root:
130 go build ./...
131 go vet ./...
132 $(TEST_ENV) ./hack/gotest.sh root '$(ROOT_SPLIT)'
133
134# test.<pkg>[~<k>of<n>]: one package of plain tests as a role of its own,
135# for a package too slow to sit inside root.
136test.%: | $(BIN)/tmp
137 $(TEST_ENV) ./hack/gotest.sh test '$*'
138
139# staticcheck catches what vet does not: deprecated APIs, values that are
140# never read, and the &*x that looks like a copy and is not.
141# staticcheck.conf at the root says which checks.
142#
143# go run with a version, not a tool directive: Fiesta is a library, and
144# a tool's requirements in this go.mod would join every consuming app's
145# module graph. Bump it alongside the go directive - a staticcheck older
146# than the Go it reads does not know that release's deprecations, and
147# says nothing - and alongside a new minor GOTOOLCHAIN, whose export
148# data an older one cannot decode at all.
149#
150# -tags browser because no file is excluded by it: it is a strict superset
151# of the plain build, and it is the only way the browser drives get read.
152STATICCHECK := honnef.co/go/tools/cmd/staticcheck@v0.8.2
153# staticcheck keeps its cache in the user cache directory, ~/.cache,
154# which a sandboxed CI runner mounts read-only: "failed to initialize
155# build cache ... read-only file system". Where the runner hands the job
156# an absolute GOCACHE, staticcheck's goes inside it: writable wherever
157# Go's is, and evicted with it. Anywhere else it stays where staticcheck
158# puts it. Not beside it: a writable GOCACHE says nothing about its
159# parent.
160ifneq ($(filter /%,$(firstword $(GOCACHE))),)
161export STATICCHECK_CACHE ?= $(GOCACHE)/staticcheck
162endif
163
164staticcheck:
165 go run $(STATICCHECK) -tags browser ./...
166
167# govulncheck reports only vulnerabilities this code can reach, so a
168# finding is a real call path, not a dependency merely present in go.sum.
169# It reads the live vulnerability database, so it can go red with no
170# change here: that is the point of running it on every push. A module
171# finding is fixed by raising the requirement; a standard-library one by
172# raising GOTOOLCHAIN above.
173GOVULNCHECK := golang.org/x/vuln/cmd/govulncheck@v1.8.0
174govulncheck:
175 go run $(GOVULNCHECK) ./...
176
177# history makes a shallow clone whole: a runner clones one commit deep
178# and with no tags, so gitleaks below would scan one commit while
179# promising the history, and releases_test.go would find no tag to
180# hold releases/ to. The whole history rather than a range of the
181# pushed commits: a runner cannot name where a push began, and
182# Fiesta's history is small (fetching it into a fresh one-commit clone
183# took a third of a second). Its own step before the suite, because
184# the suite's shards share one .git and two fetches at once would race
185# for its locks. On a runner (AMADAN_CI) it also fetches the release
186# tags into a clone that is already whole, since a tag pushed after a
187# commit was fetched is not always followed; elsewhere a whole clone
188# is left alone, so make ci by hand never fetches.
189history:
190 @if [ "$$(git rev-parse --is-shallow-repository)" = true ]; then \
191 echo "history: a shallow clone; fetching the whole history and the release tags"; \
192 git fetch -q --unshallow origin 'refs/tags/v*:refs/tags/v*'; \
193 elif [ -n "$${AMADAN_CI:-}" ]; then \
194 git fetch -q origin 'refs/tags/v*:refs/tags/v*'; \
195 fi
196 @if [ "$$(git rev-parse --is-shallow-repository)" != false ]; then echo "history: the clone is still shallow"; exit 1; fi
197 @echo "history: $$(git rev-list --count HEAD) commits; release tags: $$(git tag -l 'v*' | paste -sd' ' -)"
198
199# The whole git history, not only the tree: a key committed and deleted
200# a year ago is still in every clone, and this repository is public.
201# A shallow clone is refused rather than scanned, since its history is
202# one commit; make history comes first in ci for that.
203# --redact keeps anything it does find out of the CI log.
204#
205# The history of HEAD, not of every ref: gitleaks' default is
206# `git log --all`, and a runner's clone keeps a ref for every branch it
207# has built, so one branch's leak turned every other branch red,
208# main included. A leak on a branch is that branch's own run's to find.
209# The rest of the options are gitleaks' own defaults, which --log-opts
210# replaces wholesale.
211GITLEAKS := github.com/zricethezav/gitleaks/v8@v8.30.1
212gitleaks:
213 @if [ "$$(git rev-parse --is-shallow-repository)" != false ]; then echo "gitleaks: this clone is shallow, so its history is one commit; run make history first"; exit 1; fi
214 go run $(GITLEAKS) git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
215
216# The browser drives, one package or slice per browser.<pkg>[~<k>of<n>]
217# target (a / in the path is a . in the name). `make browser` runs them
218# one after another; the suite runs each as its own shard.
219# FIESTA_BROWSER_OPTIONAL stays unset on purpose: a skip is not a pass,
220# so a machine that loses its browser fails loudly instead of reporting
221# green. Chromium profiles also need room when the shared /tmp tmpfs
222# fills, which hack/gotest.sh handles.
223browser: $(BROWSER_ROLES)
224
225# The browser packages that need full Chromium rather than a headless
226# shell, where both are installed (hack/gotest.sh). check's drives
227# include the busy button, which needs the back/forward cache the
228# shell never restores; the gallery's history package holds the legs
229# that need that restore or a real new tab, and nothing else, so the
230# cost of full Chromium falls on them alone.
231export FULL_CHROMIUM := check internal/gallery/history
232
233browser.%: | $(BIN)/tmp
234 ./hack/gotest.sh browser '$*'
235
236# heights-sweep measures the preview heights table on this machine's
237# Chromium and fonts and prints it as Go source for heights.go: both
238# height drives over all twelve locales (FIESTA_SWEEP=full), at once,
239# each into its own log, printed when both are done. It reports; the
240# drives still fail a short box. Not in ci: it takes a quarter of an
241# hour. To calibrate on a runner, push a throwaway branch whose one
242# commit makes .amadan/ci.d this single step, 10-heights-sweep, being
243# `exec make heights-sweep`; the table is in that run's log
244# (amadan ci status rastrillo/fiesta -branch <it> -json). Then delete
245# the branch.
246HEIGHTS_SWEEP := FitTheirContent FitAtThePhonesNarrowest
247heights-sweep: | $(BIN)/tmp
248 @d=$$(mktemp -d) && status=0 && pids= && \
249 for drive in $(HEIGHTS_SWEEP); do \
250 FIESTA_SWEEP=full ./hack/gotest.sh browser internal.gallery -timeout 30m -v -run "^TestPreviewFrameHeights$${drive}1of" > "$$d/$$drive.log" 2>&1 & \
251 pids="$$pids $$!"; \
252 done; \
253 for pid in $$pids; do wait "$$pid" || status=1; done; \
254 for drive in $(HEIGHTS_SWEEP); do echo "==> $$drive"; cat "$$d/$$drive.log"; done; \
255 rm -rf "$$d"; exit $$status
256
257# site assembles fiesta.rastrillo.org into .build/site/public from the
258# committed releases and the site source: each release under its exact
259# version, _redirects sending /M/* to the newest M.y.z and the design
260# system's former addresses to their pages, _headers caching /M/ and
261# each release for five minutes, and at the root the pages
262# internal/www assembles from site/, the landing page, the design
263# system under /design-system/ and site/static/. The tree is
264# .build/site/public; the command's own .fiesta stamp sits above it in
265# .build/site, so nothing hidden is in the shipped tree. Nothing in ci
266# depends on this target, since the releases are committed and checked
267# there. It is for looking at: the site that ships is site-publish's,
268# the one build that lets browsers keep a release for a year.
269site: | $(BIN)/tmp
270 go run ./cmd/fiesta site -releases releases -site site -out .build/site
271
272# site-links-published is the ship check for links: the commit
273# SITE_REF names (HEAD unless told), exported with git archive, so
274# nothing uncommitted or untracked in the checkout counts, and its
275# site's every link resolved as the edge will serve that tree, with
276# /1/ reaching the newest release committed under releases/ and
277# nothing else (FIESTA_LINKS_PUBLISHED=1). The suite's run of the same
278# test lets a /1/ link reach the release the source would build while
279# that release is uncut, so it passes a link that 404s the day the
280# site ships ahead of its release; this does not. Not in ci, for that
281# reason: it fails from a change to js/, css/ or i18n/ until the
282# release carrying it is committed, as it should. Run it, and see it
283# pass, before shipping the site.
284#
285# Each step that can fail ends the recipe with its own failure, and the
286# scratch tree goes in an EXIT trap. A status kept in a variable through
287# an && chain came out 0 when the export failed, so a bad SITE_REF
288# passed with no link checked. The archive is written to a file rather
289# than piped, so a failing git archive is not hidden behind tar's
290# status. The trap keeps the status it was given, and turns a pass into
291# a failure when the tree cannot be removed: a trap whose own status is
292# dropped reported success with a checked-out site left in TMPDIR.
293SITE_REF ?= HEAD
294site-links-published: | $(BIN)/tmp
295 @d=$$(mktemp -d) || exit 1; \
296 trap 's=$$?; rm -rf "$$d" || { echo "site-links-published: could not remove $$d"; \
297 [ $$s -ne 0 ] || s=1; }; exit $$s' EXIT; \
298 trap 'exit 130' INT TERM; \
299 git archive --format=tar -o "$$d/site.tar" '$(SITE_REF)' || { \
300 echo "site-links-published: git archive could not export $(SITE_REF)"; exit 1; }; \
301 mkdir "$$d/tree" && tar -x -C "$$d/tree" -f "$$d/site.tar" || { \
302 echo "site-links-published: could not extract $(SITE_REF)'s tree"; exit 1; }; \
303 if ! grep -q FIESTA_LINKS_PUBLISHED "$$d/tree/internal/www/links_test.go"; then \
304 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"; exit 1; \
305 fi; \
306 cd "$$d/tree" && FIESTA_LINKS_PUBLISHED=1 go test ./internal/www -run '^TestEveryLinkInTheSiteResolves$$' -count=1 -v
307
308# site-publish builds the site that ships, from a release tag:
309# make site-publish SITE_REF=vX.Y.Z, then carlos ship
310# .build/publish/public. It is the one build that tells browsers to
311# keep a release for a year as immutable. Every other build keeps each
312# release five minutes, because until its tag exists a release may be
313# rebuilt as a candidate under the same name, and a browser told it
314# never changes would hold the old bytes for a year.
315#
316# It refuses, before building anything, unless SITE_REF is an
317# annotated tag vX.Y.Z here, origin holds the same tag, the tag's tree
318# has releases/X.Y.Z/ and /X/ would serve it, and the tag's fiesta site
319# can be told what is published; then it runs site-links-published on
320# the same tag, so the links are checked against what ships. It builds
321# from git archive of the tag, as site-links-published does, with the
322# tag's own fiesta site, and names every release directory in the
323# tag's tree as published. fiesta site checks each against its tag
324# vX.Y.Z here, file for file (site.TagDifferences, which
325# TestPublishedReleasesMatchTheirTags runs on every push), and writes
326# nothing if one differs. A tag from before -published existed is
327# refused, rather than shipped with no _headers. The two targets sit
328# together this way: site-links-published stays the check to run on
329# any ref before tagging, and site-publish runs it again on the tag as
330# its own first build step, so nothing ships unchecked.
331#
332# Each step that can fail ends the recipe with its own failure, and
333# the export goes in an EXIT trap, as in site-links-published. The
334# built site goes to .build/publish, outside the export, so it
335# outlives the trap for carlos ship to read; a run that fails after
336# building removes .build/publish/public, so a refused site is never
337# there to ship.
338PUBLISH_OUT := $(BIN)/publish
339site-publish: | $(BIN)/tmp
340 @ref='$(SITE_REF)'; \
341 printf '%s\n' "$$ref" | grep -Eqx 'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)' || { \
342 echo "site-publish: SITE_REF=$$ref is not a release tag vX.Y.Z; name the tag the site ships from"; exit 1; }; \
343 version=$${ref#v}; major=$${version%%.*}; \
344 [ "$$(git cat-file -t "refs/tags/$$ref" 2>/dev/null)" = tag ] || { \
345 echo "site-publish: $$ref is not an annotated tag here; a release is tagged with git tag -a and pushed"; exit 1; }; \
346 here=$$(git rev-parse "refs/tags/$$ref") || exit 1; \
347 there=$$(GIT_TERMINAL_PROMPT=0 git ls-remote --tags origin "refs/tags/$$ref") || { \
348 echo "site-publish: could not ask origin for $$ref"; exit 1; }; \
349 [ "$${there%%[[:space:]]*}" = "$$here" ] || { \
350 echo "site-publish: origin does not hold this $$ref ($$here); push the tag, or fetch origin's"; exit 1; }; \
351 d=$$(mktemp -d) || exit 1; \
352 trap 's=$$?; rm -rf "$$d" || { echo "site-publish: could not remove $$d"; \
353 [ $$s -ne 0 ] || s=1; }; exit $$s' EXIT; \
354 trap 'exit 130' INT TERM; \
355 git archive --format=tar -o "$$d/site.tar" "refs/tags/$$ref" || { \
356 echo "site-publish: git archive could not export $$ref"; exit 1; }; \
357 mkdir "$$d/tree" && tar -x -C "$$d/tree" -f "$$d/site.tar" || { \
358 echo "site-publish: could not extract $$ref's tree"; exit 1; }; \
359 [ -d "$$d/tree/releases/$$version" ] || { \
360 echo "site-publish: $$ref holds no releases/$$version, so the site would not serve the release it is shipped for"; exit 1; }; \
361 newest=$$(ls "$$d/tree/releases" | grep -E "^$$major\.[0-9]+\.[0-9]+$$" | sort -t. -k2,2n -k3,3n | tail -n 1); \
362 [ "$$newest" = "$$version" ] || { \
363 echo "site-publish: /$$major/ would serve $$newest, not $$version; ship the tag of the newest $$major.x"; exit 1; }; \
364 grep -q '"published"' "$$d/tree/cmd/fiesta/main.go" || { \
365 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; }; \
366 published=$$(ls "$$d/tree/releases" | paste -sd, -); \
367 repo=$$(git rev-parse --show-toplevel) || exit 1; \
368 $(MAKE) --no-print-directory site-links-published SITE_REF="refs/tags/$$ref" || { \
369 echo "site-publish: the links of $$ref's site do not all resolve against its published releases"; exit 1; }; \
370 echo "site-publish: building $$ref with $$published published"; \
371 (cd "$$d/tree" && go run -buildvcs=false ./cmd/fiesta site -releases releases -site site \
372 -out '$(PUBLISH_OUT)' -published "$$published" -git "$$repo") || { \
373 echo "site-publish: fiesta site refused $$ref"; exit 1; }; \
374 grep -qxF "/$$major/* /$$version/:splat 200" '$(PUBLISH_OUT)/public/_redirects' || { \
375 rm -rf '$(PUBLISH_OUT)/public'; \
376 echo "site-publish: the built _redirects does not send /$$major/ to $$version; removed $(PUBLISH_OUT)/public"; exit 1; }; \
377 echo "site-publish: $(PUBLISH_OUT)/public is $$ref's site, $$published kept for a year; ship that tree"
378
379root no-rastrillo size staticcheck govulncheck gitleaks: | $(BIN)/tmp
380
381$(BIN)/tmp:
382 mkdir -p "$@"
383