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
justinmerrell added a commit that referenced this pull request Aug 25, 2026
The main-branch ruleset required one approving review on every pull
request, and CODEOWNERS opened with a `*` catch-all naming the only
maintainer. Since GitHub cannot request a review from a pull request's
author, that combination blocked every pull request the maintainer
opened, and every one was merged by spending the OrganizationAdmin
bypass — #42, #45, #47, #48, #52, #53, #55 and #57 all carry
`reviewDecision: REVIEW_REQUIRED`. A control satisfied on no pull
request and stepped over on all of them enforced nothing, while still
charging every contributor the wait.

Adopt the mechanism musher-dev/platform already uses, so that moving
between the two repositories does not mean relearning when review is
required: pair `required_approving_review_count: 0` with
`require_code_owner_review: true`, and reduce CODEOWNERS to the two
paths that define the gate itself. A pull request touching no owned
path now merges on green CI; one touching `.github/CODEOWNERS` or
`.github/rulesets/` still needs its owner.

Both halves break silently — a catch-all restores the blanket gate from
one side, a count of 1 from the other — so `tools/src/rulesets.ts`
(RUL-01..RUL-09) keeps them in step from inside the existing Lint job.
RUL-09 has no counterpart upstream: it rejects a required status check
that no workflow publishes, or that a `paths:`-filtered workflow
publishes, which are the two ways a required context hangs a pull
request forever. It is also why the check runs inside Lint rather than
as a workflow of its own.

Reconcile two drifts the committed JSON had accumulated since
2026-08-08, both in the file's favour: `Signed off` is now genuinely a
required check rather than only a declared one, and release-tags no
longer grants the OrganizationAdmin bypass its own documentation says
it does not have.

GitHub waives the code-owner requirement for a pull request's author,
which with a single owner is the whole gate on the paths that matter
most. codeowners-notice.yml posts a sticky comment on self-owned edits
so the waiver is visible; it is a notifier and must never become a
required check.

GOVERNANCE.md advertised approval as mechanical in three places and
CONTRIBUTING.md in one; they now say plainly which obligations are
enforced and which are asked for. ADR 0015 records the trade, including
that owning specifications/ becomes worth revisiting the moment there
is a second maintainer.

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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