Skip to content

perf(ci): check only the Go packages a PR can affect - #27

Merged
jayleaton merged 2 commits into
mainfrom
perf/ci-affected
Oct 22, 2026
Merged

jayleaton merged 2 commits into
mainfrom
perf/ci-affected

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Problem

Every PR that touched Go ran the whole suite: build, lint and go test -race ./... across all packages, and every generator. With ~200 PRs/day and all full checks now running on Actions, that costs wall time and minutes. For example, a one-line change in apps/web/ui linted and tested packages/auth, packages/database and the API.

How

changed files ──► packages (go list file + embed lists; else nearest package dir)
              ──► + every package whose code or tests import them (go list -test -deps)
              ──► build / golangci-lint / go test -race / coverage floors on just those
              ──► generators only if touched: sqlc (queries, sqlc/, sqlc.yaml), templ (*.templ, *_templ.go),
                  go:generate per package (directive source or its -destination changed)

full run if: go.mod, go.sum, tools/, scripts/, .github/, .golangci.yml, Makefile, mise.toml,
             */migrations/*, sqlc.yaml  — and always for pushes to main, merge queue, manual runs
  • scripts/affected.sh (bash + go list, about 2 s in CI) writes full, packages, dirs, sqlc, templ and generate outputs. make affected prints them locally, and AFFECTED_FILES=… takes an explicit file list.
  • Diff base: the checkout fetches depth 2. A PR's checkout is the merge commit, so HEAD^1 is the base.
  • Coverage floors: check-coverage.sh takes TESTED=<pkgs> and checks only floors of packages that were tested. An affected floor package still has to be in the profile.
  • Probes: the layering and test-boundary probes run on full runs, which is exactly when their inputs (scripts/, .golangci.yml) change. The cheap test-location check always runs.
  • Admin app (feat(admin): optional admin dashboard (users, bans, roles, sessions, invites, audit log) #26): the admin packages, packages/database/adminrepo and its sqlc packages are handled by the same package graph. sqlc query/output paths are read from sqlc.yaml, so adminrepo query changes rerun sqlc. adminrepo migrations force a full run. The admin layering probes and test-boundary probes run on full runs, and the adminrepo/admin coverage floors are checked when those packages are affected. Both UIs are templ-generated when a .templ changes.
  • Unchanged: mobile path filtering, the docs-only skip, ci-ok/draft semantics (fix(ci): a skipped draft run can no longer cancel or mask the real run #9), and the cache (perf(ci): warm cache across dependency changes, Postgres overlap, path-filtered main pushes #13).
  • AGENTS.md (Verifying) describes it, including when to add a new shared input to the full-run list.

Evidence

Scratch PRs, all four of each phase run at the same time. Before: #18–#21 against main's workflow. After: #22–#25 stacked on this branch, so only the scenario change is in their diff. All are closed; branches and caches deleted.

scenario before: wall / go job after: wall / go job after: affected
docs only (docs/BUILD-PLAN.md) 17 s / not run 17 s / not run –
web only (apps/web/ui/helpers.go) 110 s / 87 s 61 s / 44 s 4 packages (ui, ui/mocks, ui/cmd/preview, cmd/server)
auth only (packages/auth/password.go) 102 s / 84 s 80 s / 61 s 8 packages (auth, database + dbtest via auth migrations, server, ui, cmd)
admin only (apps/admin/internal/services/services.go; #29 → #30, after rebase onto #26) 137 s / 119 s 84 s / 63 s 9 admin packages (services, api, ui, server, cmd/admin + their mocks/apitest); coverage floors checked only for admin services, api, ui
go.mod change (new dependency) 105 s / 87 s 80 s / 61 s full run (full run: go.mod); same work as before, the difference is runner variance
  • Web only: lint went 20 → 6 s, tests 26 → 11 s, and the probes (10 s) were skipped.
  • Auth only: tests stay about 23 s, because auth and Postgres-backed tests are most of the suite.
  • Negative proof: on the auth-only PR, renaming an exported packages/auth function used only by apps/web/cmd/server made CI fail in Build with apps/web/cmd/server/main.go:155:68: undefined: auth.MobileTrustedOrigins, and ci-ok went red. Dependents are checked, not just the changed package.
  • Local checks: scenario outputs checked with AFFECTED_FILES (docs, mobile, web/templ, services, hand-edited mocks, SQL query, embedded template, go.mod). actionlint is clean; gofmt, go vet ./... and go build ./... pass. Full checks ran on Actions under the compute rule; this PR's own run is a full run, since it changes the workflow and scripts.

Merge Danger

Door: two-way. Reverting the workflow restores full PR runs.

Blast Radius: CI.

  • The main risk is an input that can change test results without being in the Go package graph or the full-run list. Example: a file read at test time from outside every package dir. Such a change would be checked only on the main push and in the merge queue, which stay full. Known shared inputs are in the full-run list, and AGENTS.md says to add new ones there.
  • Partial runs never save caches; they restore main's cache as before.

🤖 Generated with Claude Code

@jayleaton
jayleaton marked this pull request as ready for review October 22, 2026 09:01
jayleaton and others added 2 commits October 22, 2026 16:05
scripts/affected.sh maps changed files to packages (go list file and
embed lists, nearest package dir otherwise) and adds every package whose
code or tests import them (go list -test -deps). PR runs build, lint,
test and coverage-check only those, and rerun only the generators whose
inputs or outputs changed (sqlc, templ, per-package go:generate).
go.mod/go.sum, tools, scripts, workflows, lint config, Makefile,
migrations and sqlc config force a full run, as do pushes to main, the
merge queue and manual runs. The layering and test-boundary probes run
on full runs, which is when their inputs change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminrepo has its own queries and generated packages in the same
sqlc.yaml; a change there must rerun sqlc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton merged commit af65cf7 into main Oct 22, 2026
5 checks passed
@jayleaton
jayleaton deleted the perf/ci-affected branch October 22, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant