Skip to content

Format-Adapter persist↔download path lint

TL;DR. Every Format-Adapter under api/internal/formats/<fmt>/ uploads artifacts with formatutil.Upsert(... PERSIST_PATH ...) and serves them back with formatutil.LookupDigest(... DOWNLOAD_PATH ...). The two paths must agree, or the artifact lands in the blob store but the download handler returns 404. make lint-format-paths walks each adapter's AST and refuses to compile a PR whose persist/download leading-literal prefixes diverge.

Why this lint exists

The 2026-05-08 testcontainer run (item 103 of the product roadmap) surfaced three adapters — Pub, Puppet, and Swift — whose persist site wrote to a path like tmp/<uuid> while the download site read from packages/<name>/<version>.tar.gz. The integration suite eventually caught it, but only after a four-minute testcontainer bootstrap per format.

A static AST walk catches the same bug class in under a second, so the lint sits as a gate in front of the integration job. A red lint pass fails the workflow before docker even spins up.

What the lint checks

For each adapter directory (api/internal/formats/<fmt>/, excluding formatutil/):

  1. Parse every non-test .go file with go/parser.
  2. Find every call to formatutil.Upsert(...) (the persist site) and every call to formatutil.LookupDigest(...) (the download site).
  3. Extract the path argument — index 4 for Upsert, index 3 for LookupDigest.
  4. Compute a leading literal prefix for each path expression:
    • "foo/" + x"foo/"
    • fmt.Sprintf("/foo/%s", x)"/foo/"
    • helper(args) → recurse into helper's first return EXPR
    • localVar → recurse into its := binding
    • opaque (function parameter, cross-package call) → empty string
  5. For each persist path P:
    • If leadingLiteral(P) is empty: skip (no signal to compare).
    • If any lookup in the same adapter shares the prefix: OK.
    • If any lookup is itself indeterminate (opaque expression): skip — the lint cannot prove a mismatch when the lookup side could route through any path at runtime.
    • Otherwise: report mismatch.

The matching rule is intentionally conservative. A clean exit on the current corpus is the deployment gate — false positives block the merge pipeline far more than false negatives. Phase B can tighten the rule later if a real-world bug slips through.

What you'll see when it fires

text
api/internal/formats/pub/pub.go:234:18: format=pub persist="fmt.Sprintf(\"tmp/%s\", uuid)" (leading-literal "tmp/") has no matching lookup (lookup leading-literals: [packages/])

lint-format-paths: 1 persist/lookup mismatch(es)

The line number points at the formatutil.Upsert call site. The report includes:

  • the rendered expression so you can grep for it,
  • the leading literal the linter could prove statically,
  • the set of lookup-side prefixes in the same adapter, so you can tell whether the lookup needs to be widened or the persist needs to be narrowed.

Running it

bash
make lint-format-paths           # gates `make test-integration` in CI
make lint-all                    # runs every lint-* target locally
go test ./tools/lint/format-paths/...   # unit-tests the linter itself

The linter binary lives at tools/lint/format-paths/ as its own Go module so it builds against the stdlib only — no pgx, S3, OIDC or tracing deps to drag in.

Handling a false positive

If the lint fires on a legitimately-different persist/download pair (very unlikely; the prefix-match rule has been audited against all 30+ adapters in the tree), the right fix is almost always to align the two sites on a shared helper function rather than silence the linter. The whole point of the bug class is that divergent literals creep in when the persist and download implementations are written months apart by different people.

If you're certain the divergence is correct, file an issue rather than adding an inline allowlist — the rule needs to grow with the corpus, not get patched per-call-site.

Implementation pointers

Released under the Apache-2.0 License.