rastrillo / fiesta Public

Fiesta's module, gate and CI steps

Task 1 of the Fiesta part-1 plan: the module amadan.net/rastrillo/fiesta
and the gate that every later task runs before it commits. The gate is
rastrillo's shape so that a runner can be switched on later with no
change here: `make ci` is the one definition, .amadan/ci.d/ steps are
bare `exec make <target>` files at mode 100755, and 50-suite@4 names
the suite's four roles.

Two tests are new. TestFiestaNeverImportsRastrillo and
TestThePublicPackagesLinkNoBrowser are the "No Rastrillo dependency"
constraint as code: Rastrillo adopts Fiesta, so a path back would be a
cycle, and an app that embeds the CSS must not take on a Chrome
driver. Each was shown red with a one-line mutation in a scratch copy
(a require line and an import of rastrillo; an import of chromedp in
the root package) and green without it. ci_test.go is rastrillo's
amadan_ci_test.go; its exec-bit test was shown red first, on 10-gofmt
staged at mode 100644.

Ported from rastrillo at e14b745, checked against origin/main:
hack/suite.sh, staticcheck.conf and .amadan/ci are byte-identical;
hack/gotest.sh differs only by RASTRILLO_CHROME -> FIESTA_CHROME (its
comments still cite rastrillo's ui package as the example); the
Makefile keeps GOTOOLCHAIN, CGO_ENABLED, GOTMPDIR, the gofmt, suite,
suite-roles and go-roles blocks, the STATICCHECK, GOVULNCHECK and
GITLEAKS pins and the STATICCHECK_CACHE rule, and drops every
Rastrillo-only target (money, the examples, chromedp-graph, gorm-free,
generate-check, scaffold-smoke, race, mirror). GOFLAGS=-mod=mod is not
carried: it existed for rastrillo's scratch modules, which Fiesta has
none of. In ci_test.go the two messages that cited AGENTS.md now cite
the Makefile's ci comment, which is where the sentence lives here.
hack/test-weights.txt is empty of weights and keeps the format comment.
SUITE ends in $(BROWSER_ROLES), empty now, so a later task adds a
browser package by setting BROWSER_PKGS alone.

Summary

Adds go.mod, doc.go, the Makefile with ci/gofmt/no-rastrillo/suite and the four roles root, staticcheck, govulncheck and gitleaks, hack/suite.sh and hack/gotest.sh from rastrillo, .amadan/ci with three ci.d steps, staticcheck.conf, ci_test.go and norastrillo_test.go.
Agent notes
make ci passes locally with 4 suite roles; TestSlicesPartitionTheirPackages skips until a package is sliced (Task 8). No fleet lane: CI is local for now (controller decision 2).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 24c93b35159e2323836f998a932fd6f9e09fa74a parent 84ce367
13 files changed, +880 −0
  • .amadan/ci +6 −0
  • .amadan/ci.d/10-gofmt +2 −0
  • .amadan/ci.d/20-no-rastrillo +2 −0
  • .amadan/ci.d/50-suite@4 +2 −0
  • Makefile +180 −0
  • ci_test.go +336 −0
  • doc.go +6 −0
  • go.mod +3 −0
  • hack/gotest.sh +187 −0
  • hack/suite.sh +46 −0
  • hack/test-weights.txt +8 −0
  • norastrillo_test.go +92 −0
  • staticcheck.conf +10 −0

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
mkdir -p "/home/paulca/.local/state/amadan/runner/repos/rastrillo__fiesta/wt/fiesta-1/.build/tmp"
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.139s
=== suite ===
--- shard 0 of 4 ---
==> shard 0 of 4: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 go test -count=1 ./...
ok  	amadan.net/rastrillo/fiesta	0.051s
--- shard 1 of 4 ---
==> shard 1 of 4: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
go: downloading honnef.co/go/tools v0.8.2
go: downloading golang.org/x/tools v0.51.0
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
--- shard 2 of 4 ---
==> shard 2 of 4: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
go: downloading golang.org/x/vuln v1.8.0
go: downloading golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518
go: downloading golang.org/x/mod v0.41.0
go: downloading golang.org/x/tools v0.50.0
go: downloading golang.org/x/sync v0.23.0
No vulnerabilities found.
--- shard 3 of 4 ---
==> shard 3 of 4: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
go: downloading github.com/zricethezav/gitleaks/v8 v8.30.1
go: downloading github.com/rs/zerolog v1.33.0
go: downloading github.com/spf13/cobra v1.9.1
go: downloading github.com/spf13/viper v1.19.0
go: downloading github.com/Masterminds/sprig/v3 v3.3.0
go: downloading github.com/charmbracelet/lipgloss v0.5.0
go: downloading github.com/BobuSumisu/aho-corasick v1.0.3
go: downloading github.com/fatih/semgroup v1.2.0
go: downloading golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
go: downloading github.com/hashicorp/go-version v1.7.0
go: downloading github.com/gitleaks/go-gitdiff v0.9.1
go: downloading github.com/h2non/filetype v1.1.3
go: downloading github.com/mholt/archives v0.1.2
go: downloading github.com/mattn/go-colorable v0.1.14
go: downloading github.com/spf13/pflag v1.0.6
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
go: downloading github.com/mattn/go-runewidth v0.0.14
go: downloading github.com/muesli/termenv v0.15.1
go: downloading golang.org/x/sync v0.11.0
go: downloading github.com/fsnotify/fsnotify v1.8.0
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/sagikazarmark/slog-shim v0.1.0
go: downloading github.com/spf13/afero v1.12.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading dario.cat/mergo v1.0.1
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading github.com/Masterminds/semver/v3 v3.3.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/huandu/xstrings v1.5.0
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/shopspring/decimal v1.4.0
go: downloading golang.org/x/crypto v0.35.0
go: downloading github.com/STARRY-S/zip v0.2.1
go: downloading github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3
go: downloading github.com/bodgit/sevenzip v1.6.0
go: downloading github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
go: downloading github.com/klauspost/compress v1.17.11
go: downloading github.com/klauspost/pgzip v1.2.6
go: downloading github.com/minio/minlz v1.0.0
go: downloading github.com/nwaples/rardecode/v2 v2.1.0
go: downloading github.com/pierrec/lz4/v4 v4.1.21
go: downloading github.com/sorairolake/lzip-go v0.3.5
go: downloading github.com/therootcompany/xz v1.0.1
go: downloading github.com/ulikunitz/xz v0.5.12
go: downloading golang.org/x/text v0.22.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/subosito/gotenv v1.6.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.67.0
go: downloading github.com/pelletier/go-toml/v2 v2.2.3
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/magiconair/properties v1.8.9
go: downloading golang.org/x/sys v0.30.0
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
go: downloading github.com/bodgit/plumbing v1.3.0
go: downloading github.com/bodgit/windows v1.0.1
go: downloading go4.org v0.0.0-20230225012048-214862532bf5
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7
go: downloading github.com/hashicorp/errwrap v1.1.0
7:18PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
7:18PM INF 1 commits scanned.
7:18PM INF scanned ~50630 bytes (50.63 KB) in 32.9ms
7:18PM INF no leaks found
diff --git a/.amadan/ci b/.amadan/ci
new file mode 100755
index 0000000..8c547ee
--- /dev/null
+++ b/.amadan/ci
@@ -0,0 +1,6 @@
+#!/bin/sh
+# amadan CI entry (single-script fallback for runners without step
+# support). The steps in ci.d/ are the same targets, reported one by
+# one. Must stay executable: a non-executable script resolves "skipped".
+set -e
+exec make ci
diff --git a/.amadan/ci.d/10-gofmt b/.amadan/ci.d/10-gofmt
new file mode 100755
index 0000000..e234af5
--- /dev/null
+++ b/.amadan/ci.d/10-gofmt
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec make gofmt
diff --git a/.amadan/ci.d/20-no-rastrillo b/.amadan/ci.d/20-no-rastrillo
new file mode 100755
index 0000000..58315ed
--- /dev/null
+++ b/.amadan/ci.d/20-no-rastrillo
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec make no-rastrillo
diff --git a/.amadan/ci.d/50-suite@4 b/.amadan/ci.d/50-suite@4
new file mode 100755
index 0000000..da26e40
--- /dev/null
+++ b/.amadan/ci.d/50-suite@4
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec make suite
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b93944c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,180 @@
+.PHONY: ci suite suite-roles gofmt no-rastrillo root staticcheck govulncheck gitleaks browser
+
+# CGO_ENABLED=0 across the tree is an acceptance criterion, not a
+# preference: Fiesta is CSS, scripts and the Go that embeds and checks
+# them, and nothing here may quietly pull in a cgo dependency because
+# a C toolchain happened to be present.
+export CGO_ENABLED = 0
+
+# The gate runs on one known Go, not whichever release the machine has.
+# govulncheck reports the standard library's vulnerabilities for the
+# toolchain doing the scan, so a runner one patch release behind would
+# fail the gate for code nobody changed, and a laptop and a runner on
+# different patches would disagree about what passing means. go fetches
+# this release through the module proxy on first use.
+#
+# A plain =, not ?=: Go's own container images set GOTOOLCHAIN=local, and
+# ?= would quietly keep whatever Go they ship. Override on the command
+# line if you must: make ci GOTOOLCHAIN=local. Raise it with each Go
+# security release; govulncheck naming a stdlib package "Found in" this
+# version is the signal.
+#
+# Held on 1.26 deliberately, as rastrillo is. Go 1.27 writes export
+# data that x/tools before v0.50.0 cannot read, and v0.50.0 needs go
+# 1.26 in go.mod: moving to 1.27 raises the Go floor for every app that
+# imports Fiesta, so it is its own change.
+export GOTOOLCHAIN = go1.26.10
+
+BIN := $(CURDIR)/.build
+
+# Keep compiler/linker scratch on the checkout filesystem when shared /tmp is full.
+export GOTMPDIR ?= $(BIN)/tmp
+
+# ci is the one gate: what a runner executes and what you run before
+# pushing are the same definition. .amadan/ci.d/ reports these one by
+# one; it never keeps its own copy of a command.
+#
+# The cheap checks go first and alone, so a stray gofmt still costs
+# seconds. Everything else is the suite.
+ci: gofmt no-rastrillo suite
+
+# The suite's roles. Under amadan, .amadan/ci.d/50-suite@N runs N
+# shards at once and each runs one role, so the run takes as long as
+# the slowest role rather than the sum of them all; by hand, or as a
+# step with no @N, the roles run one after another. N must equal the
+# number of roles here: hack/suite.sh refuses any other count rather
+# than run some roles twice and others nowhere, and ci_test.go checks
+# the two agree before a runner ever sees them.
+#
+# The slowest role is how long the suite takes, so the slow packages
+# are roles of their own. The browser packages are a role each. A
+# package written pkg@n is n roles, each a slice of its tests, dealt
+# by hack/gotest.sh. In a role's name a / in the path is a . so
+# check/drives@2 would become browser.check.drives~1of2 and ~2of2.
+# BROWSER_PKGS is empty until the browser packages exist; the tasks
+# that add them append here and rename 50-suite@N to match.
+BROWSER_PKGS :=
+go-roles = $(foreach p,$(2),$(if $(findstring @,$(p)),$(call go-slices,$(1),$(subst /,.,$(word 1,$(subst @, ,$(p)))),$(word 2,$(subst @, ,$(p)))),$(1).$(subst /,.,$(p))))
+go-slices = $(foreach k,$(shell seq 1 $(3)),$(1).$(2)~$(k)of$(3))
+BROWSER_ROLES := $(call go-roles,browser,$(BROWSER_PKGS))
+SUITE := root staticcheck govulncheck gitleaks $(BROWSER_ROLES)
+
+suite:
+ @MAKE='$(MAKE)' ./hack/suite.sh $(SUITE)
+
+# What ci_test.go counts against the step's @N.
+suite-roles:
+ @echo $(SUITE)
+
+# The repo's own Go files, not everything under the checkout: GOTMPDIR is
+# .build/tmp, and a go command killed mid-build (a Ctrl-C, or a runner
+# cancelling a job a newer push superseded) leaves its generated
+# _testmain.go and cgo files there. gofmt -l . walked into them and failed
+# the next run on files nobody wrote. Dot-directories are pruned, the same
+# rule ./... applies. It is a walk of the working tree rather than a git
+# listing, so an unstaged deletion is simply absent instead of a missing
+# path. NUL-delimited so a path with a space stays one argument, and a
+# gofmt error (a file that does not parse) fails the target instead of
+# vanishing into stderr.
+gofmt:
+ @out=$$(find . -name '.?*' -type d -prune -o -name '*.go' -type f -print0 | xargs -0 gofmt -l) || exit 1; \
+ if [ -n "$$out" ]; then echo "gofmt needed on:"; echo "$$out"; exit 1; fi
+
+# Fiesta never imports Rastrillo, and the packages a Go program imports
+# from Fiesta never link a browser. Both are tests in norastrillo_test.go;
+# this runs them alone, first, so a broken promise is the first thing a
+# red run says.
+no-rastrillo:
+ go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$$' -count=1 .
+
+# FIESTA_TEST_REQUIRE_NODE turns every Node skip into a failure here: on
+# a laptop without Node a skip is honest, but a gate that lost node
+# would otherwise go green having checked none of the JavaScript.
+# Override with FIESTA_TEST_REQUIRE_NODE= to run without it.
+FIESTA_TEST_REQUIRE_NODE ?= 1
+# root builds, vets and tests every package. The browser drives are
+# behind the browser tag and run as roles of their own.
+root:
+ go build ./...
+ go vet ./...
+ FIESTA_TEST_REQUIRE_NODE=$(FIESTA_TEST_REQUIRE_NODE) go test -count=1 ./...
+
+# test.<pkg>[~<k>of<n>]: one package of plain tests as a role of its own,
+# for a package too slow to sit inside root.
+test.%: | $(BIN)/tmp
+ FIESTA_TEST_REQUIRE_NODE=$(FIESTA_TEST_REQUIRE_NODE) ./hack/gotest.sh test '$*'
+
+# staticcheck catches what vet does not: deprecated APIs, values that are
+# never read, and the &*x that looks like a copy and is not.
+# staticcheck.conf at the root says which checks.
+#
+# go run with a version, not a tool directive: Fiesta is a library, and
+# a tool's requirements in this go.mod would join every consuming app's
+# module graph. Bump it alongside the go directive - a staticcheck older
+# than the Go it reads does not know that release's deprecations, and
+# says nothing - and alongside a new minor GOTOOLCHAIN, whose export
+# data an older one cannot decode at all.
+#
+# -tags browser because no file is excluded by it: it is a strict superset
+# of the plain build, and it is the only way the browser drives get read.
+STATICCHECK := honnef.co/go/tools/cmd/staticcheck@v0.8.2
+# staticcheck keeps its cache in the user cache directory, ~/.cache,
+# which a sandboxed CI runner mounts read-only: "failed to initialize
+# build cache ... read-only file system". Where the runner hands the job
+# an absolute GOCACHE, staticcheck's goes inside it: writable wherever
+# Go's is, and evicted with it. Anywhere else it stays where staticcheck
+# puts it. Not beside it: a writable GOCACHE says nothing about its
+# parent.
+ifneq ($(filter /%,$(firstword $(GOCACHE))),)
+export STATICCHECK_CACHE ?= $(GOCACHE)/staticcheck
+endif
+
+staticcheck:
+ go run $(STATICCHECK) -tags browser ./...
+
+# govulncheck reports only vulnerabilities this code can reach, so a
+# finding is a real call path, not a dependency merely present in go.sum.
+# It reads the live vulnerability database, so it can go red with no
+# change here: that is the point of running it on every push. A module
+# finding is fixed by raising the requirement; a standard-library one by
+# raising GOTOOLCHAIN above.
+GOVULNCHECK := golang.org/x/vuln/cmd/govulncheck@v1.8.0
+govulncheck:
+ go run $(GOVULNCHECK) ./...
+
+# The whole git history, not only the tree: a key committed and deleted
+# a year ago is still in every clone, and this repository is public.
+# --redact keeps anything it does find out of the CI log.
+#
+# The history of HEAD, not of every ref: gitleaks' default is
+# `git log --all`, and a runner's clone keeps a ref for every branch it
+# has built, so one branch's leak turned every other branch red,
+# main included. A leak on a branch is that branch's own run's to find.
+# The rest of the options are gitleaks' own defaults, which --log-opts
+# replaces wholesale.
+GITLEAKS := github.com/zricethezav/gitleaks/v8@v8.30.1
+gitleaks:
+ go run $(GITLEAKS) git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
+
+# The browser drives, one package or slice per browser.<pkg>[~<k>of<n>]
+# target (a / in the path is a . in the name). `make browser` runs them
+# one after another; the suite runs each as its own shard.
+# FIESTA_BROWSER_OPTIONAL stays unset on purpose: a skip is not a pass,
+# so a machine that loses its browser fails loudly instead of reporting
+# green. Chromium profiles also need room when the shared /tmp tmpfs
+# fills, which hack/gotest.sh handles.
+browser: $(BROWSER_ROLES)
+
+# The browser packages that need full Chromium rather than a headless
+# shell, where both are installed (hack/gotest.sh). check's drives
+# include the busy button, which needs the back/forward cache the
+# shell never restores.
+export FULL_CHROMIUM := check
+
+browser.%: | $(BIN)/tmp
+ ./hack/gotest.sh browser '$*'
+
+root no-rastrillo staticcheck govulncheck gitleaks: | $(BIN)/tmp
+
+$(BIN)/tmp:
+ mkdir -p "$@"
diff --git a/ci_test.go b/ci_test.go
new file mode 100644
index 0000000..f9b33eb
--- /dev/null
+++ b/ci_test.go
@@ -0,0 +1,336 @@
+package fiesta
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// This file is rastrillo's amadan_ci_test.go, aimed at this repo's own
+// tree. .amadan/ci and .amadan/ci.d/ are what a step-reporting runner
+// executes, and nothing else gates them: a lost exec bit, or a ci.d
+// step that runs its own commands instead of naming a Makefile target,
+// resolves amadan's runner to "skipped", which renders as a pass. That
+// defect shipped five times in rastrillo before these checks existed;
+// Fiesta carries them from its first commit.
+
+// amadanCIDStepFiles lists .amadan/ci.d's entries, sorted, so every
+// check below walks the same list.
+func amadanCIDStepFiles(t *testing.T) []string {
+ t.Helper()
+ entries, err := os.ReadDir(".amadan/ci.d")
+ if err != nil {
+ t.Fatalf("reading .amadan/ci.d: %v", err)
+ }
+ var names []string
+ for _, e := range entries {
+ if e.IsDir() {
+ continue
+ }
+ names = append(names, e.Name())
+ }
+ sort.Strings(names)
+ if len(names) == 0 {
+ t.Fatal(".amadan/ci.d has no step files")
+ }
+ return names
+}
+
+// TestAmadanFilesAreExecutableInGit checks the executable bit the way
+// amadan's runner sees it: through git's index, not the working tree.
+// A filesystem-only check (os.Stat) would pass on a machine where the
+// bit is set locally but was never committed — the exact gap that lets
+// a non-executable step ship and resolve "skipped" (a pass) on every
+// other checkout.
+func TestAmadanFilesAreExecutableInGit(t *testing.T) {
+ out, err := exec.Command("git", "ls-files", "-s", ".amadan/").Output()
+ if err != nil {
+ t.Fatalf("git ls-files -s .amadan/: %v", err)
+ }
+ lines := strings.Split(strings.TrimRight(string(out), "\n"), "\n")
+ if len(lines) == 0 || lines[0] == "" {
+ t.Fatal("git ls-files -s .amadan/ reported nothing tracked")
+ }
+
+ seen := map[string]bool{}
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) < 4 {
+ t.Fatalf("unexpected `git ls-files -s` line: %q", line)
+ }
+ mode, path := fields[0], fields[3]
+ seen[path] = true
+ if mode != "100755" {
+ t.Errorf("%s is committed with mode %s, not 100755 — a runner reading the index (not the working tree) would resolve this job \"skipped\", which renders as a pass", path, mode)
+ }
+ }
+
+ want := []string{".amadan/ci"}
+ for _, name := range amadanCIDStepFiles(t) {
+ want = append(want, filepath.Join(".amadan/ci.d", name))
+ }
+ for _, w := range want {
+ if !seen[w] {
+ t.Errorf("%s is not tracked in git's index at all", w)
+ }
+ }
+}
+
+// execMakePattern anchors on the exact shape a ci.d step is allowed to
+// take: nothing but a call into the Makefile target of the same name.
+var execMakePattern = regexp.MustCompile(`^exec make ([A-Za-z0-9_-]+)$`)
+
+// amadanCIDTargets reads every .amadan/ci.d step, asserts each is a
+// bare `exec make <target>` (the Makefile's ci comment: "ci.d/ ...
+// never keeps its own copy of a command"), and returns the
+// step-file-name -> target map.
+func amadanCIDTargets(t *testing.T) map[string]string {
+ t.Helper()
+ targets := map[string]string{}
+ for _, name := range amadanCIDStepFiles(t) {
+ path := filepath.Join(".amadan/ci.d", name)
+ b, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading %s: %v", path, err)
+ }
+ var nonBlank []string
+ for _, l := range strings.Split(string(b), "\n") {
+ if strings.TrimSpace(l) != "" {
+ nonBlank = append(nonBlank, l)
+ }
+ }
+ if len(nonBlank) != 2 || nonBlank[0] != "#!/bin/sh" {
+ t.Errorf("%s must be exactly a `#!/bin/sh` shebang followed by one `exec make <target>` line; got:\n%s", path, b)
+ continue
+ }
+ m := execMakePattern.FindStringSubmatch(nonBlank[1])
+ if m == nil {
+ t.Errorf("%s's step is %q, not a bare `exec make <target>` — a step that runs its own command instead of a Makefile target is exactly the copy the Makefile says never to keep, and it can drift from what `make ci` actually runs without anything noticing", path, nonBlank[1])
+ continue
+ }
+ targets[name] = m[1]
+ }
+ return targets
+}
+
+// TestAmadanStepsAreBareExecMake is that assertion on its own, so a
+// broken step file fails here with the specific file and line named,
+// rather than only showing up as a set mismatch in the test below.
+func TestAmadanStepsAreBareExecMake(t *testing.T) {
+ amadanCIDTargets(t)
+}
+
+// ciPrereqPattern pulls `ci:`'s prerequisite list out of the Makefile,
+// following GNU Make's backslash-newline continuation so the
+// multi-line target list is captured whole.
+var ciPrereqPattern = regexp.MustCompile(`(?m)^ci:[ \t]*((?:.*\\\n)*.*)$`)
+
+// TestAmadanCITargetsMatchStepFiles enforces the promise the Makefile
+// makes and nothing else checks: add to `ci:` and to ci.d/ together,
+// or a step-reporting runner silently skips what you added. A target
+// added to `ci:` with no matching ci.d step never runs under amadan's
+// runner even though `make ci` covers it; a ci.d step naming a target
+// dropped from (or never added to) `ci:` reports a job that `make ci`
+// — the one gate — no longer executes. Either drift is silent without
+// this.
+func TestAmadanCITargetsMatchStepFiles(t *testing.T) {
+ mk, err := os.ReadFile("Makefile")
+ if err != nil {
+ t.Fatalf("reading Makefile: %v", err)
+ }
+ m := ciPrereqPattern.FindStringSubmatch(string(mk))
+ if m == nil {
+ t.Fatal("Makefile has no `ci:` target; `make ci` is the one gate every step file execs into")
+ }
+ joined := strings.ReplaceAll(m[1], "\\\n", " ")
+ ciTargets := map[string]bool{}
+ for _, f := range strings.Fields(joined) {
+ ciTargets[f] = true
+ }
+ if len(ciTargets) == 0 {
+ t.Fatal("Makefile's `ci:` target has no prerequisites")
+ }
+
+ stepTargets := map[string]bool{}
+ for step, target := range amadanCIDTargets(t) {
+ stepTargets[target] = true
+ if !ciTargets[target] {
+ t.Errorf(".amadan/ci.d/%s execs `make %s`, but %q is not one of ci:'s prerequisites in the Makefile — this step now reports a job `make ci` does not run", step, target, target)
+ }
+ }
+ for target := range ciTargets {
+ if !stepTargets[target] {
+ t.Errorf("Makefile's `ci:` target depends on %q, but no .amadan/ci.d/* step execs `make %s` — amadan's runner silently skips this half of `make ci`", target, target)
+ }
+ }
+}
+
+// TestAmadanSuiteShardsMatchRoles pins the suite step's @N to the
+// Makefile's SUITE. amadan starts N shards and each runs the role at
+// its index, so the two numbers are one fact written twice: a role
+// added without raising N runs nowhere while every shard reports ok.
+// hack/suite.sh refuses the mismatch at run time; this says so before
+// a runner is involved. It also refuses a role that is a ci
+// prerequisite as well, which would run it twice.
+func TestAmadanSuiteShardsMatchRoles(t *testing.T) {
+ if _, err := exec.LookPath("make"); err != nil {
+ t.Skip("make is not installed; the gate itself runs under make")
+ }
+ var step, shards string
+ for name, target := range amadanCIDTargets(t) {
+ if target != "suite" {
+ continue
+ }
+ if step != "" {
+ t.Fatalf("two .amadan/ci.d steps exec `make suite` (%s, %s); each would run every role", step, name)
+ }
+ step = name
+ if i := strings.LastIndex(name, "@"); i >= 0 {
+ shards = name[i+1:]
+ }
+ }
+ if step == "" {
+ t.Fatal("no .amadan/ci.d step execs `make suite`, so the suite's roles run nowhere under amadan")
+ }
+
+ out, err := exec.Command("make", "-s", "--no-print-directory", "suite-roles").Output()
+ if err != nil {
+ t.Fatalf("make suite-roles: %v", err)
+ }
+ roles := strings.Fields(string(out))
+ if len(roles) == 0 {
+ t.Fatal("make suite-roles printed no roles")
+ }
+ if want := strconv.Itoa(len(roles)); shards != want {
+ t.Errorf(".amadan/ci.d/%s runs as %q shards, but the Makefile's SUITE has %d roles; rename it to end in @%s", step, shards, len(roles), want)
+ }
+
+ mk, err := os.ReadFile("Makefile")
+ if err != nil {
+ t.Fatalf("reading Makefile: %v", err)
+ }
+ m := ciPrereqPattern.FindStringSubmatch(string(mk))
+ if m == nil {
+ t.Fatal("Makefile has no `ci:` target")
+ }
+ ci := map[string]bool{}
+ for _, f := range strings.Fields(strings.ReplaceAll(m[1], "\\\n", " ")) {
+ ci[f] = true
+ }
+ seen := map[string]bool{}
+ for _, r := range roles {
+ if seen[r] {
+ t.Errorf("SUITE lists %q twice; two shards would run it", r)
+ }
+ seen[r] = true
+ if ci[r] {
+ t.Errorf("%q is both a suite role and a prerequisite of ci, so make ci runs it twice", r)
+ }
+ }
+}
+
+// sliceRolePattern reads a sliced role: <mode>.<pkg>~<k>of<n>, mode
+// browser or test.
+var sliceRolePattern = regexp.MustCompile(`^(browser|test)\.(.+)~([0-9]+)of([0-9]+)$`)
+
+// TestSlicesPartitionTheirPackages is what lets a package run under
+// -run at all: a filter that matches nothing exits 0, and a slice is a
+// -run too, so this proves the slices cannot leave a test out. Every
+// slice 1..n of a package is a suite role, and hack/gotest.sh's deal
+// puts each name in exactly one slice and leaves no slice empty. The
+// names are every weighted one plus synthetic ones, so this compiles
+// nothing; the real list is dealt by the same function at run time.
+// It skips while no package is sliced.
+func TestSlicesPartitionTheirPackages(t *testing.T) {
+ if _, err := exec.LookPath("make"); err != nil {
+ t.Skip("make is not installed; the gate itself runs under make")
+ }
+ out, err := exec.Command("make", "-s", "--no-print-directory", "suite-roles").Output()
+ if err != nil {
+ t.Fatalf("make suite-roles: %v", err)
+ }
+ type sliced struct{ mode, pkg string }
+ slices := map[sliced]map[int]bool{}
+ counts := map[sliced]int{}
+ for _, r := range strings.Fields(string(out)) {
+ m := sliceRolePattern.FindStringSubmatch(r)
+ if m == nil {
+ continue
+ }
+ k, _ := strconv.Atoi(m[3])
+ n, _ := strconv.Atoi(m[4])
+ key := sliced{m[1], strings.ReplaceAll(m[2], ".", "/")}
+ if prev, ok := counts[key]; ok && prev != n {
+ t.Fatalf("%s %s is sliced both %d and %d ways", key.mode, key.pkg, prev, n)
+ }
+ counts[key] = n
+ if slices[key] == nil {
+ slices[key] = map[int]bool{}
+ }
+ slices[key][k] = true
+ }
+ if len(counts) == 0 {
+ t.Skip("no package is sliced")
+ }
+
+ weights, err := os.ReadFile("hack/test-weights.txt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for key, n := range counts {
+ name := key.mode + " " + key.pkg
+ for k := 1; k <= n; k++ {
+ if !slices[key][k] {
+ t.Errorf("%s is sliced %d ways but slice %d is no suite role, so its tests run nowhere", name, n, k)
+ }
+ }
+
+ var names []string
+ for _, l := range strings.Split(string(weights), "\n") {
+ if f := strings.Fields(l); len(f) == 4 && f[0] == key.mode && f[1] == key.pkg {
+ names = append(names, f[2])
+ }
+ }
+ for i := 0; i < 5*n; i++ {
+ names = append(names, "TestUnweighted"+strconv.Itoa(i))
+ }
+ deal := exec.Command("sh", "hack/gotest.sh", "--deal", key.mode, key.pkg, strconv.Itoa(n))
+ deal.Stdin = strings.NewReader(strings.Join(names, "\n") + "\n")
+ got, err := deal.Output()
+ if err != nil {
+ t.Fatalf("dealing %s: %v", name, err)
+ }
+ dealt := map[string]int{}
+ used := map[int]bool{}
+ for _, l := range strings.Split(strings.TrimSpace(string(got)), "\n") {
+ f := strings.Fields(l)
+ if len(f) != 2 {
+ t.Fatalf("deal line %q is not `<k> <name>`", l)
+ }
+ k, err := strconv.Atoi(f[0])
+ if err != nil || k < 1 || k > n {
+ t.Errorf("%s: %s was dealt to slice %q, outside 1..%d", name, f[1], f[0], n)
+ }
+ used[k] = true
+ dealt[f[1]]++
+ }
+ for _, nm := range names {
+ if dealt[nm] != 1 {
+ t.Errorf("%s: %s was dealt %d times, not once", name, nm, dealt[nm])
+ }
+ }
+ if len(dealt) != len(names) {
+ t.Errorf("%s: dealt %d names from %d", name, len(dealt), len(names))
+ }
+ for k := 1; k <= n; k++ {
+ if !used[k] {
+ t.Errorf("%s: slice %d of %d was dealt nothing from %d names", name, k, n, len(names))
+ }
+ }
+ }
+}
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..440abae
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,6 @@
+// Package fiesta is Rastrillo's design system as a package of its own:
+// the CSS, the optional scripts, the icons, the interface strings and
+// the examples a page links or copies, together with the checks that
+// keep them as they were. Nothing here imports Rastrillo; Rastrillo
+// adopts Fiesta.
+package fiesta
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c7e54a8
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module amadan.net/rastrillo/fiesta
+
+go 1.25.0
diff --git a/hack/gotest.sh b/hack/gotest.sh
new file mode 100755
index 0000000..1ab0366
--- /dev/null
+++ b/hack/gotest.sh
@@ -0,0 +1,187 @@
+#!/bin/sh
+# One go test role of the suite.
+#
+# hack/gotest.sh browser <name> go test -tags browser, one package
+# hack/gotest.sh test <name> go test, one package
+# hack/gotest.sh root '<split>' go test ./... minus the split packages
+#
+# <name> is a package path with its slashes written as dots
+# (internal.designsystem), optionally ~<k>of<n> to run only the k-th of
+# n slices of its tests.
+#
+# A slice is dealt from the package's own test list: heaviest first,
+# each onto the lightest slice so far, by the seconds in
+# hack/test-weights.txt (a test not listed there weighs 1). Every shard
+# computes the same deal, so the n slices of a package are a partition
+# of it: each test runs in exactly one, and a test added later lands in
+# a slice without anyone listing it. Two refusals keep that honest. A
+# package that lists no tests is a broken build or a broken listing, not
+# an empty package; and a slice dealt nothing means n is larger than the
+# package, which would report green having run nothing.
+#
+# hack/gotest.sh --deal <mode> <pkg> <n> reads test names on stdin and
+# prints "<k> <name>" for each: the deal alone, which amadan_ci_test.go
+# checks is a partition without compiling anything.
+set -eu
+
+weights=$(dirname "$0")/test-weights.txt
+
+# deal <mode> <pkg> <n>: names on stdin, "<k> <name>" out. Ties sort by
+# name, so the deal does not depend on the order the names arrive in.
+deal() {
+ awk -v mode="$1" -v pkg="$2" -v wf="$weights" '
+ BEGIN { while ((getline l < wf) > 0) { split(l, f, " "); if (f[1] == mode && f[2] == pkg) w[f[3]] = f[4] } }
+ NF { print (($1 in w) ? w[$1] : 1), $1 }' |
+ sort -k1,1nr -k2,2 |
+ awk -v n="$3" '
+ { best = 1; for (i = 2; i <= n; i++) if (load[i] < load[best]) best = i
+ load[best] += $1; print best, $2 }'
+}
+
+case "${1:-}" in
+--deal)
+ deal "$2" "$3" "$4"
+ exit 0
+ ;;
+root)
+ # Every package but the split ones, which run as roles of their own.
+ # Each split package must be one go list names: one that was renamed
+ # or deleted would otherwise drop out of both halves silently.
+ all=$(go list ./...)
+ keep=$all
+ for p in $2; do
+ ip=$(go list "./${p%@*}")
+ printf '%s\n' "$all" | grep -qx "$ip" || {
+ echo "gotest: $ip is split out of root but go list ./... does not name it"
+ exit 1
+ }
+ keep=$(printf '%s\n' "$keep" | grep -vx "$ip")
+ done
+ # shellcheck disable=SC2086 # one argument per package, on purpose
+ exec go test -count=1 $keep
+ ;;
+browser)
+ tags="-tags=browser"
+ timeout=20m
+ ;;
+test)
+ tags=
+ timeout=10m
+ ;;
+*)
+ echo "gotest: usage: gotest.sh browser|test <name> | root '<split>' | --deal <mode> <pkg> <n>"
+ exit 2
+ ;;
+esac
+mode=$1
+
+name=$2
+rel=$name
+slice=
+case "$name" in
+*~*)
+ rel=${name%%~*}
+ slice=${name#*~}
+ ;;
+esac
+rel=$(printf '%s' "$rel" | tr . /)
+pkg=./$rel/
+
+# Chromium writes a profile per browser into TMPDIR, and a /tmp shared
+# by a whole machine can fill, so browser roles prefer /var/tmp. Only
+# where it is writable: amadan's bwrap sandbox mounts the root
+# filesystem read-only and gives each job a private tmpfs /tmp, which
+# is then the right place. Not inside the checkout, ever: a scratch
+# module built under it would find this repo's .git walking up.
+if [ "$mode" = browser ] && [ -z "${TMPDIR:-}" ] && [ -w /var/tmp ]; then
+ TMPDIR=/var/tmp
+ export TMPDIR
+fi
+
+# The headless shell never restores a page from the back/forward cache,
+# and ui's TestBusyButtonDrive needs one (no flag changes that; full
+# Chromium of the same version does). Where `chromium` is a headless
+# shell, as amadan's cloud runner links it, the packages the Makefile
+# names in FULL_CHROMIUM use full Chromium for Testing from beside it
+# when it is installed there. Only those: on the fleet full Chromium
+# took the design system's slices from minutes to as long as twenty.
+# Discovery only: a readlink without -f leaves the browser to the
+# harness's own search rather than failing the role.
+case " ${FULL_CHROMIUM:-} " in
+*" $rel "*) full=yes ;;
+*) full= ;;
+esac
+if [ "$mode" = browser ] && [ -n "$full" ] && [ -z "${FIESTA_CHROME:-}" ] &&
+ shell=$(command -v chromium 2>/dev/null) && shell=$(readlink -f "$shell" 2>/dev/null); then
+ case "${shell##*/}" in
+ headless_shell | chrome-headless-shell)
+ # The same build number as the shell: one Playwright installs both.
+ build=${shell#*/chromium_headless_shell-}
+ for c in "${shell%/chromium_headless_shell-*}/chromium-${build%%/*}"/chrome-linux*/chrome; do
+ if [ -x "$c" ]; then
+ FIESTA_CHROME=$c
+ export FIESTA_CHROME
+ fi
+ done
+ ;;
+ esac
+fi
+
+# Full Chromium's crash handler wants a database under its config
+# directory, ~/.config by default, and will not start without one
+# ("chrome_crashpad_handler: --database is required"). The cloud
+# runner's HOME is read-only, so there it gets a scratch one.
+# Which browser a role drove is the first question a red browser role
+# raises, and nothing else in the log answers it.
+if [ "$mode" = browser ]; then
+ echo "gotest: on $(uname -n), driving ${FIESTA_CHROME:-the harness's own choice ($(command -v chromium 2>/dev/null || echo none on PATH))}"
+fi
+
+scratch=
+if [ "$mode" = browser ] && [ -z "${CHROME_CONFIG_HOME:-}" ] && ! [ -w "${HOME:-/}" ]; then
+ scratch=$(mktemp -d)
+ CHROME_CONFIG_HOME=$scratch
+ export CHROME_CONFIG_HOME
+fi
+
+# run is exec, unless there is scratch to remove after go test.
+run() {
+ if [ -z "$scratch" ]; then
+ exec "$@"
+ fi
+ status=0
+ "$@" || status=$?
+ rm -rf "$scratch"
+ exit "$status"
+}
+
+if [ -z "$slice" ]; then
+ # shellcheck disable=SC2086 # an empty $tags is no argument at all
+ run go test $tags -timeout "$timeout" "$pkg" -count=1
+fi
+
+k=${slice%of*}
+n=${slice#*of}
+# Its own command, not the head of a pipeline: a package that fails to
+# build must fail here, not list nothing through a grep that succeeds.
+# shellcheck disable=SC2086
+listing=$(go test $tags -list '.*' "$pkg") || {
+ echo "gotest: could not list the tests in $pkg:"
+ printf '%s\n' "$listing"
+ exit 1
+}
+# Examples and fuzz seeds answer to -run as well, so they are dealt too.
+all=$(printf '%s\n' "$listing" | grep -E '^(Test|Example|Fuzz)' || true)
+total=$(printf '%s\n' "$all" | grep -c . || true)
+if [ "$total" -eq 0 ]; then
+ echo "gotest: $pkg listed no tests; refusing to report a slice of nothing as a pass"
+ exit 1
+fi
+mine=$(printf '%s\n' "$all" | deal "$mode" "$rel" "$n" | awk -v k="$k" '$1 == k { print $2 }')
+if [ -z "$mine" ]; then
+ echo "gotest: slice $k of $n of $pkg was dealt none of its $total tests; lower its slice count"
+ exit 1
+fi
+echo "gotest: slice $k of $n of $pkg runs $(printf '%s\n' "$mine" | wc -l) of $total tests"
+# shellcheck disable=SC2086
+run go test $tags -timeout "$timeout" -run "^($(printf '%s\n' "$mine" | paste -sd'|' -))\$" "$pkg" -count=1
diff --git a/hack/suite.sh b/hack/suite.sh
new file mode 100755
index 0000000..c558607
--- /dev/null
+++ b/hack/suite.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+# Runs the suite's roles (the Makefile's SUITE, passed as arguments).
+#
+# amadan sets AMADAN_SHARD and AMADAN_SHARD_COUNT on every step, 0 and 1
+# when the step is not sharded, so those two numbers are the whole
+# decision:
+#
+# - unset, or a count of 1: every role, in order, stopping at the
+# first failure. That is `make ci` by hand, the single-script
+# .amadan/ci fallback, and a suite step that lost its @N.
+# - a count equal to the number of roles: this shard's role, alone.
+# - any other count: a refusal. A count above the roles would leave
+# shards with nothing to do reporting green; one below would leave
+# roles that run nowhere while every shard still says ok.
+set -eu
+
+make=${MAKE:-make}
+roles=$#
+count=${AMADAN_SHARD_COUNT:-1}
+shard=${AMADAN_SHARD:-0}
+
+if [ "$count" -eq 1 ]; then
+ for role; do
+ echo "==> make $role"
+ $make --no-print-directory "$role"
+ done
+ exit 0
+fi
+
+if [ "$count" -ne "$roles" ]; then
+ echo "suite: started as $count shards, but the Makefile's SUITE has $roles roles."
+ echo "suite: rename .amadan/ci.d's suite step to end in @$roles, or every role"
+ echo "suite: past shard $count runs nowhere while each shard still reports ok"
+ exit 1
+fi
+
+i=0
+for role; do
+ if [ "$i" -eq "$shard" ]; then
+ echo "==> shard $shard of $count: make $role"
+ exec $make --no-print-directory "$role"
+ fi
+ i=$((i + 1))
+done
+echo "suite: shard $shard is outside 0..$((count - 1))"
+exit 1
diff --git a/hack/test-weights.txt b/hack/test-weights.txt
new file mode 100644
index 0000000..b32cf9c
--- /dev/null
+++ b/hack/test-weights.txt
@@ -0,0 +1,8 @@
+# Seconds per test, for hack/gotest.sh's slice deal (<mode> <pkg>
+# <test> <seconds>, mode browser or test): the heaviest test goes to
+# the lightest slice first. Only tests of 2s or more are worth listing;
+# any test not here weighs 1. A stale number costs balance, never
+# coverage: every listed test still runs in exactly one slice. Refresh
+# from a `go test -v` run:
+# grep '^--- ' log | sed -E 's/^--- [A-Z]+: ([^ ]+) \(([0-9.]+)s\)/\1 \2/'
+# No package is sliced yet, so there are no weights.
diff --git a/norastrillo_test.go b/norastrillo_test.go
new file mode 100644
index 0000000..f337916
--- /dev/null
+++ b/norastrillo_test.go
@@ -0,0 +1,92 @@
+package fiesta
+
+import (
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+// Fiesta is the design system on its own. Rastrillo adopts it, so a
+// path from here back to Rastrillo would be a cycle, and a user who
+// links the CSS from a static page has no Go at all. The two tests
+// below are the two halves of that promise, executable: no build of
+// this module reaches amadan.net/rastrillo/rastrillo, and the packages
+// a Go program imports from Fiesta pull in no browser.
+
+// rastrilloModule is the import path that must never appear in this
+// module's dependency graph, and never in its go.mod.
+const rastrilloModule = "amadan.net/rastrillo/rastrillo"
+
+// listDeps runs go list with the given arguments and returns its
+// output, one import path per line. A go list that cannot run fails
+// the test rather than listing nothing: piped straight into a check,
+// an empty listing would pass as clean, which is how a toolchain that
+// could not fetch itself once passed rastrillo's own graph check.
+func listDeps(t *testing.T, args ...string) []string {
+ t.Helper()
+ cmd := exec.Command("go", append([]string{"list"}, args...)...)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("go list %s: %v\n%s", strings.Join(args, " "), err, out)
+ }
+ lines := strings.Split(strings.TrimRight(string(out), "\n"), "\n")
+ if len(lines) == 0 || lines[0] == "" {
+ t.Fatalf("go list %s printed nothing", strings.Join(args, " "))
+ }
+ return lines
+}
+
+// requireListed is the precondition that keeps either check honest: a
+// listing that does not name this module's own package is not a
+// listing of this module, whatever else it says.
+func requireListed(t *testing.T, lines []string, args string) {
+ t.Helper()
+ for _, l := range lines {
+ if l == "amadan.net/rastrillo/fiesta" {
+ return
+ }
+ }
+ t.Fatalf("go list %s did not list amadan.net/rastrillo/fiesta itself; the check below would be looking at nothing:\n%s", args, strings.Join(lines, "\n"))
+}
+
+// TestFiestaNeverImportsRastrillo is the global constraint "No
+// Rastrillo dependency" as a test: every package of this module, its
+// tests included and with the browser tag on (a strict superset of the
+// plain build), imports nothing from amadan.net/rastrillo/rastrillo,
+// and go.mod never requires it. The go.mod half catches a requirement
+// nobody imports yet, which `go list -deps` alone would not see.
+func TestFiestaNeverImportsRastrillo(t *testing.T) {
+ mod, err := os.ReadFile("go.mod")
+ if err != nil {
+ t.Fatalf("reading go.mod: %v", err)
+ }
+ if strings.Contains(string(mod), rastrilloModule) {
+ t.Errorf("go.mod mentions %s; Fiesta never requires Rastrillo", rastrilloModule)
+ }
+
+ args := "-deps -test -tags browser ./..."
+ lines := listDeps(t, "-deps", "-test", "-tags", "browser", "./...")
+ requireListed(t, lines, args)
+ for _, l := range lines {
+ if strings.HasPrefix(l, rastrilloModule) {
+ t.Errorf("go list %s reaches %s; Fiesta never imports Rastrillo in any build, tag or test", args, l)
+ }
+ }
+}
+
+// TestThePublicPackagesLinkNoBrowser is the other half: the packages
+// a Go program imports from Fiesta, the root and (from Task 5) example,
+// never link chromedp. The browser is for check/ and the drives, and
+// an app that embeds the CSS must not take on a Chrome driver to do
+// it. Until example exists the list is the root package alone.
+func TestThePublicPackagesLinkNoBrowser(t *testing.T) {
+ args := "-deps ."
+ lines := listDeps(t, "-deps", ".")
+ requireListed(t, lines, args)
+ for _, l := range lines {
+ if strings.Contains(l, "chromedp") {
+ t.Errorf("go list %s reaches %s; the public packages never link a browser", args, l)
+ }
+ }
+}
diff --git a/staticcheck.conf b/staticcheck.conf
new file mode 100644
index 0000000..2e33b14
--- /dev/null
+++ b/staticcheck.conf
@@ -0,0 +1,10 @@
+# staticcheck's default set, minus ST1018.
+#
+# ST1018 asks for Unicode format characters in string literals to be
+# written as escapes. The design system's right-to-left translations
+# carry U+200E and U+200F marks on purpose - they decide where a Latin
+# brand name sits inside Arabic or Hebrew prose - and as ‏ escapes
+# those strings stop being readable or editable by the translator who
+# owns them. "inherit" keeps every other default, including the ones a
+# future staticcheck release adds.
+checks = ["inherit", "-ST1018"]