feat(repo): publish from Cloudflare Pages, and generate the cache contract - #55
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ahuman entry point and loses two GitHub-Pages-only files. No schema, no prose, no
fixture changes —
check:drift,check:compatand the conformance corpus areuntouched.
Why
Closes #49 and #50. Reasoning in
ADR 0012, which supersedes ADR 0001 §5
and closes ADR 0006 follow-up 1.
Every
$idthis repository publishes is a dead URL today, and standing the hostname upestablished 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
525at ~90 days), and proxying is the only reason Cloudflare was in the path.The workarounds are Enterprise- and Business-gated; the
musher.devzone 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-revalidateand caches the alias for a year.The rules therefore partition, and
assertNoOverlapenforces that over the pathsactually 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.
One rule per release rather than two — the directory rule covers the schema and its
.sha256sidecar, which is exactly as immutable as the bytes it attests. The buildfails 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
Access-Control-Allow-Origin: *. It does —it is in the documented default set,
along with
nosniffand apublic, max-age=0, must-revalidatefor any cacheableasset, and
_headersoverrides defaults rather than appending to them. Both headersare still pinned explicitly, but the reason is "README makes this guarantee, so it
should not rest on a vendor default", not "the header would otherwise be absent". The
ADR says the true thing. The useful consequence: a path
_headersforgets revalidatesrather than pinning stale bytes.
musher-dev/specsis archived, not deleted.ADR 0012 §5 records why archival is the better end state than the deletion ADR 0001 §1
promised — deletion would
404every inbound reference, including ADR 0001's ownaccount of why this repository exists. Retire musher-dev/specs — ADR 0001 §1 says it is superseded and will be deleted #51 stays open: what remains is dropping
specsfrom org ruleset14207433, which no change here can make.Compatibility
Notes for the reviewer
Two things that would have failed silently.
release.yml'spublishjob callspages.ymlas a reusable workflow, and a reusableworkflow inherits no secrets — so it needs
secrets: inherit, or the deploy breaks onexactly the run that first publishes a version at its immutable URL.
task site:deploydeclaresrequires: [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 wrotebunx wrangler pages deploy, whichresolves 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/toolsecosystem, and GOVERNANCE.md namesit 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
mainin a public repo now reaches a Cloudflare token.check:driftandcheck:publishedstill run before it is used.This will be red on
mainuntil the secrets land (foundation-bootstrap#129).Pagesis not one of the four required checks, so it blocks nothing.
Verification
task check— all 18 gates pass, 65 tests._headersand the pages are covered by thedeterminism 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 theunmerged release PRs), so I assembled a multi-release fixture to confirm the emitted
shape — one immutable rule per release, the alias's
spec.mdlink resolving at the tagit actually serves, versions newest-first with checksums.
Post-merge, once infra applies the token:
Checklist
task checkpasses locallyschemas/dist/untouched — no schema changespec.mdchange; the decision is recorded in ADR 0012feat(repo):, so release-please cuts nothing🤖 Generated with Claude Code