Development record
The website deploys on release, not on merge
adracceptedadr-48
Context
adr-47 makes abcdev.app a rendered surface of this repository. The remaining question is the trigger. Every hosted platform's default is deploy-on-push, and the record is the most alive part of this repository — 1,188 commits in 45 days against 10 releases at the 2026-08-21 snapshot — so a release-bound site shows a record days stale. The adversarial pass on both options is recorded in the implementation prompt's Part 2 (research/abcdev-site/).
Decision
- Production deploys per release, from the tag. The site workflow
checks out the tag, downloads the released binary, checksum-verifies it
exactly as the install one-liner does, renders the site with that
binary, builds the docs into
site/docs/, runsabcd site check, attestssite.tar.gzas a release asset, and deploys with wrangler from a protected GitHub Environment. The site is produced by the same bytes users run, and abcdev.app becomes one verifiable statement: this is abcd at that tag. - The trigger is the release chain, not a
release:event. Releases here are created byrelease.yml'sgh release createunder the workflow's ownGITHUB_TOKEN, and GitHub Actions never fires event triggers for actions taken with that token —auto-release.yml's header documents exactly this semantic and already works around it withworkflow_call. The site deploy is therefore a reusable workflow invoked from the release chain as a separate, non-gating job after the release job: a site failure can neither block nor taint the release (the isolation is job dependency, not workflow-file separation), running aftergh release createcompletes removes the race against asset uploads, and the job is idempotent (re-running deploys the same tag). Pre-releases never deploy production. - Every push to main deploys to a clearly labelled preview (an "unreleased — main@sha" label rendered from build metadata), replacing the Cloudflare branch builds that currently repeat only the version command. The preview is built by Actions with a source-built binary — main is ahead of any release, so the released-bytes rule cannot apply — and deployed as a non-production version; the "unreleased" label is precisely the disclosure of that difference. The team gets the live record; the public gets the released one.
- Emergencies redeploy from the latest tag by
workflow_dispatch(a tag input), never from main — the invariant survives the emergency. A genuine content emergency is a CHANGELOG line and a patch release under adr-37: the fix gets a version and a date. - Cloudflare's automatic production builds are turned off; deploys come
from Actions with wrangler, and the dashboard's build command becomes a
comment in
wrangler.jsonc, where the repo already documents it.
Alternatives Considered
- Deploy production on every merge to main: the platform default, and the record explorer stays current to the hour — rejected because the site's job is to describe a product someone can install: following main lets the site document a verb no downloadable binary has, and the install command, version badge, changelog and docs can disagree for days. Versioned docs would repair that and are far too heavy for a project this size. The staleness cost is served by the labelled preview instead.
- Emergency redeploys from main: rejected — it silently converts the emergency path into deploy-on-merge; dispatch rebuilds from the latest tag only.
- Rendering production with a source-built binary: rejected — the site must be produced by the released, checksum-verified bytes, or the attestation says nothing about what visitors actually read.
Consequences
- The cadence of the website becomes the cadence of releases, making small, frequent releases slightly more valuable than they already are — and ten releases in 45 days suggests that is the cadence anyway.
- The Cloudflare deploy token is scoped to one Worker and lives in a protected GitHub Environment inside the release workflow's existing trust chain; it never touches release signing.
- The runtime posture matches adr-38: everything the site knows is injected at build time; the published pages make no API calls.
Frontmatter
| id | adr-48 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-08-22 |
.abcd/development/decisions/adrs/0048-website-deploys-on-release-not-on-merge.md
open on GitHub ↗ · commit history ↗
References6
- adr-37Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commitrelated
- adr-38Implicit checks are disk-only — the network answers only an explicit askrelated
- itd-135Iris opens abcdev.app and understands who abcd is for, what they and their facilitator own, and how to install it — from one page rendered from the repositoryrelated
- itd-136Alice opens the record explorer and reads the whole development record — every decision, intent, spec and issue as a page, with contributors and references — without reading YAML on GitHubrelated
- itd-137Maya sees how every decision connects — one relationship chart with a date coil and a links-only layout, and a five-lane genealogy of the record over timerelated
- itd-138Bob installs abcd with one checksum-verified command served from the project's own domainrelated
Referenced by4
- adr-47abcdev.app is rendered from this repository alonerelated
- adr-2610091010387033abcdev.app may render public abcd-managed repositories other than abcdrelated
- adr-2610142147242544abcd's record is built onto its own GitHub Pages, and abcdev.app becomes abcd's documentation siterelated
- itd-2610142145236613abcd's own record is built onto its GitHub Pages like any project'srelated
mentions12
- dcn-26082200000000042026-08-22 — The abcdev.app website enters the record: adr-47 (rendered from
- dcn-26082600000000012026-08-26 — bughunt round 7 (state #368): hardened the new issue-resolution
- dcn-26082600000000052026-08-26 — Bug-hunt round 8 (bughunt-b): baseline green after unshallowing
- iss-2608220150157502Cloudflare non-production branch builds run only the version command, so branch previews repeat the production build instead of building the branch — observed and recorded as comments in wrangler.jsonc. The adr-48 deploy design replaces these with a labelled preview deployed from Actions on push to main and turns Cloudflare's automatic production builds off
- iss-2608220750029986adrs/README.md index omits the accepted adr-47 and adr-48 rows (site-build session) — fourth recurrence of the unindexed-ADR class after adr-45 (iss-38 wants these indexes gated); rows added in the same change that records this
- iss-2608231607594913Cloudflare Git-integration branch builds run for pull request branches even though wrangler.jsonc records them as disabled, so an unmerged commit reaches a third-party build system before review. wrangler.jsonc carries 'automatic production builds: disabled' and 'automatic branch builds: disabled', kept in-tree because the dashboard setting has no other durable record. On 2026-08-23 a Cloudflare build ran for a pull request branch at 10:00 UTC and the integration bot posted a successful-deployment comment naming the build id. That build was not Actions: .github/workflows/site.yml triggers on workflow_call, workflow_dispatch and push to main only, and it did not run on that commit; the workflow header itself states that the main-push preview replaces the Cloudflare branch builds that ran only the version command. Two consequences. Operationally, every pull request commit is cloned by a third party before merge, and Cloudflare exposes no delete endpoint for a build record or its logs, so whatever reaches a build log cannot be withdrawn without a vendor support request. Durably, the in-tree record is wrong in the confident direction: a reader checking whether branch builds run gets a clear no. Suggested direction: reconcile the dashboard setting with wrangler.jsonc and decide which is authoritative. A maintainer decides; this record reports.
- iss-2608231912566984The v0.6.2 release published its binaries, then failed on its final job, and
- iss-2608261041027043The 05-internals index and the 04-surfaces row still call shipped site capabilities design targets, contradicting 10-site.md. 05-internals/README.md row 10 says the check gates, the explorer pages and the deploy workflow are design targets, and 04-surfaces/README.md row 22 says the deploy workflow remains a design target — but 10-site.md marks only the abcdev.app MkDocs root as the design target, abcd site check is registered shipped in surface.json, the explorer shipped (itd-136), and the deploy workflow ships (adr-48 accepted, release.yml calls site.yml, CHANGELOG 0.6.6). Both index cells are stale surviving copies of the earlier all-staged claim; outside the release-gate crosscheck's pinned briefDocs corpus, so no gate catches the record-vs-record contradiction. Fix: align both cells with 10-site.md.
- itd-140The Generic/Specific Boundary of the Site Generator Is a Gate, Not a Hope
- spc-37iris-opens-abcdev-app-and-understands-who-abcd-is-for-what-t
- spc-38alice-opens-the-record-explorer-and-reads-the-whole-developm
- spc-2610142146250176abcd-s-own-development-record-is-built