Skip to content

feat(repo): publish from Cloudflare Pages, and generate the cache contract - #55

Merged
justinmerrell merged 1 commit into
mainfrom
feat/cloudflare-pages-publication
Aug 19, 2026
Merged

feat(repo): publish from Cloudflare Pages, and generate the cache contract#55
justinmerrell merged 1 commit into
mainfrom
feat/cloudflare-pages-publication

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

Nothing about the contract. The origin the contract is served from moves from
GitHub Pages to Cloudflare Pages, and the cache policy for every published path
moves into this repository as a generated _headers. The published tree also gains a
human entry point and loses two GitHub-Pages-only files. No schema, no prose, no
fixture changes — check:drift, check:compat and the conformance corpus are
untouched.

Why

Closes #49 and #50. Reasoning in
ADR 0012, which supersedes ADR 0001 §5
and closes ADR 0006 follow-up 1.

Every $id this repository publishes is a dead URL today, and standing the hostname up
established that ADR 0001 §5 cannot hold as written. It specified "a GitHub Pages
artifact fronted by Cloudflare, which supplies the immutable cache headers Pages cannot
set itself" — but GitHub cannot renew a custom domain's certificate behind an orange
cloud (pages-health-check#153,
a hard 525 at ~90 days), and proxying is the only reason Cloudflare was in the path.
The workarounds are Enterprise- and Business-gated; the musher.dev zone is Free.

The part worth reviewing carefully

Cloudflare Pages applies every matching rule and comma-joins duplicate header names.
A more specific rule does not win. So the obvious shape — one broad pinned-path rule plus
per-alias overrides — emits Cache-Control: public, max-age=31536000, immutable, public, max-age=300, must-revalidate and caches the alias for a year.

The rules therefore partition, and assertNoOverlap enforces that over the paths
actually written on every build, so a new artifact cannot quietly acquire a second
opinion about how long it may be cached. I verified the guard fires by introducing the
overlap deliberately; the build fails.

/*
  Access-Control-Allow-Origin: *
  X-Content-Type-Options: nosniff

/*.schema.json
  Content-Type: application/schema+json; charset=utf-8

/component/v1/component.schema.json
  Cache-Control: public, max-age=300, must-revalidate

/component/v1.0.0/*
  Cache-Control: public, max-age=31536000, immutable

One rule per release rather than two — the directory rule covers the schema and its
.sha256 sidecar, which is exactly as immutable as the bytes it attests. The build
fails at 90 rules against Cloudflare's cap of 100, because a file over the ceiling is
rejected wholesale: the failure mode is no policy at all, not a truncated one.

Two corrections to the issues as filed

Compatibility

  • No schema change (docs, tooling, or CI only)

Notes for the reviewer

Two things that would have failed silently.

release.yml's publish job calls pages.yml as a reusable workflow, and a reusable
workflow inherits no secrets — so it needs secrets: inherit, or the deploy breaks on
exactly the run that first publishes a version at its immutable URL.

task site:deploy declares requires: [CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID],
so a missing secret fails the job rather than letting wrangler deploy anonymously.

wrangler is exact-pinned, not bunx. #49 wrote bunx wrangler pages deploy, which
resolves whatever is latest at deploy time and hands it a Cloudflare credential — in a
repo that SHA-pins every action and SHA-256-verifies the actionlint tarball. It is now a
lockfile dependency under dependabot's bun/tools ecosystem, and GOVERNANCE.md names
it as the one dependency here that holds a secret. It does add ~180 lines of lockfile.

The credential exposure is real and stated rather than mitigated away (ADR 0012 §6):
a push to main in a public repo now reaches a Cloudflare token. check:drift and
check:published still run before it is used.

This will be red on main until the secrets land (foundation-bootstrap#129). Pages
is not one of the four required checks, so it blocks nothing.

Verification

task check — all 18 gates pass, 65 tests. _headers and the pages are covered by the
determinism test, which now compares the whole tree rather than one file.

Pinned-path behaviour cannot be observed from main (zero tags; PRs #1#3 are the
unmerged release PRs), so I assembled a multi-release fixture to confirm the emitted
shape — one immutable rule per release, the alias's spec.md link resolving at the tag
it actually serves, versions newest-first with checksums.

Post-merge, once infra applies the token:

curl -sI https://schemas.musher.dev/component/v1/component.schema.json
curl -sI https://schemas.musher.dev/ | head -1        # 200, not 404

Checklist

  • task check passes locally
  • schemas/dist/ untouched — no schema change
  • No behavioural change, so no new conformance fixtures
  • No spec.md change; the decision is recorded in ADR 0012
  • Conventional and scoped feat(repo):, so release-please cuts nothing
  • DCO signed off

🤖 Generated with Claude Code

…tract

ADR 0001 §5 specified a GitHub Pages artifact fronted by Cloudflare for the
immutable cache headers Pages cannot set itself. Both halves cannot hold:
GitHub cannot renew a custom domain's certificate behind an orange cloud, and
proxying is the only reason Cloudflare was in the path. Every $id this
repository publishes is a dead URL today.

The origin becomes Cloudflare Pages, and the cache policy moves into this
repository as a generated _headers. site.ts already enumerates every path it
writes and knows which are aliases and which are pinned, so there is no second
source of truth. Pages merges every matching rule and comma-joins duplicate
header names rather than letting the more specific win, so the rules must
partition; assertNoOverlap holds that over the paths actually written.

The origin also gains a human entry point — / and /<family>/ were 404s — and
drops CNAME and .nojekyll, neither of which ever did anything.

Closes #49, closes #50. Records in ADR 0012 §5 that musher-dev/specs is
archived rather than deleted as ADR 0001 §1 said; #51 stays open for the
organisation ruleset that still names it, which no change here can make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit c93a802 into main Aug 19, 2026
6 checks passed
@justinmerrell
justinmerrell deleted the feat/cloudflare-pages-publication branch August 19, 2026 10:17
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.

Publish to Cloudflare Pages, and generate _headers — schemas.musher.dev is being stood up

1 participant