Skip to content

ci(release): the human act is the environment approval, not a typed version (ADR-0125) - #10150

Merged
os-elon merged 2 commits into
mainfrom
claude/release-failure-83i4qc
Aug 20, 2026
Merged

ci(release): the human act is the environment approval, not a typed version (ADR-0125)#10150
os-elon merged 2 commits into
mainfrom
claude/release-failure-83i4qc

Conversation

@os-elon

@os-elonos-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Governed surface — docs/adr/** + AGENTS.md. Do not queue, do not arm auto-merge. This is yours to merge by hand (Prime Directive #14). No auto-merge is armed and none will be.

Maintainer ruling, 2026-08-20:

release workflow 我觉得,合并 changeset 后,发版本前只需要有人工批准就可以,没必要现在这样一定需要我去手工输入版本号。

What changes

The 2026-08-07 ruling 「版本发布必须是人工的」 is untouched and still binding. What changes is which human act carries it.

BeforeAfter
What starts the releaseyou open Actions → Run workflowmerging the Version Packages PR
The human confirmationyou type the exact versionyou approve the release environment
What runs unattendednothingqueue a deployment and wait
Where the barrier livesthe workflow_dispatchevent — no push can synthesise itthe environment's required reviewers — a repo setting

That last row is the whole risk, and it is why D3 below exists.

Why the typed version was not the thing protecting you

Typing 17.1.0 did two jobs:

  1. Proved a human was there. Its point — but the workflow_dispatchevent is what did that, not the typing.
  2. Cross-checked the intent. Nearly vacuous: changeset publish ships what package.json says, so a wrong string never publishes the wrong version, it aborts the run. It catches exactly one thing — you believing main carries a version it doesn't — and showing you the version covers that strictly better than asking you to recite it.

#10146 is the cost of asking for the string at all: that run's log said 17.5.0, which is @object-ui/console@17.5.0, the vendored objectui build, while this repo was publishing 17.1.0. A maintainer retyping a number read off a release log had a live chance of stopping their own release.

The decisions (ADR-0125)

  • D1publish runs on push to main, gated on one predicate computed by the existing hardened audit: main's @objectstack/cli version is absent from npm. True only just after a version PR merges; false on the other ~18 daily landings, so no ordinary merge queues a deployment. The version is read from the object database at github.sha with release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170's tripwire, never off disk.
  • D2environment: release holds the whole job — no checkout, no build, no publish — and the job's name: is computed from the audited version, so the approval screen reads “Publish 17.2.0 to npm (awaiting approval)”. You confirm a version you are shown.
  • D3 — ⛔ Void if the release environment loses its required reviewers. An unprotected environment passes automatically and silently and is indistinguishable in the log from an approved one. Under a push trigger that isn't a degraded gate, it's no gate — rc.3/rc.4 by design. You confirmed the reviewers are configured; the ADR, the workflow header and Prime Directive Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15 all now say the decision is conditional on that, and that no file here can check it.
  • D4workflow_dispatch survives as the repair lane: no version input, plus a dispatch-only force for finishing a partial publish (the one case D1's predicate can't see — canary on npm, stragglers not). Same environment gate.
  • D5 — Workflow-level concurrency → per-job groups. The old group was keyed on github.event_name and its comment said the point was that the lanes can never displace each other; that key stops separating anything once both lanes are pushes. Worse: a job waiting on approval holds its run in progress, so under one shared group an hour spent deciding would evict the intervening main pushes and stop chore: version packages (rc) #4935 regenerating.

The guard keeps R2's tripwire and adds one: refuse if the tree has moved off the version the approval was given against.

AGENTS.md #15

Gains the new reserved act — ⛔ never approve a pending release deployment — and records that merging the Version Packages PR is no longer adjacent to the release but is its trigger, which makes it the single most load-bearing prohibition in that directive.

Verification

Repo gates run locally, all green: check:adr-links, check:adr-anchors, check:doc-authoring, check:doc-anchors, check:required-contexts, check:workflow-status-functions, scripts/pm/check-governed-prose.mjs.

check:workflow-status-functions caught a real defect on the first pass: the publish job's if: read needs.*.outputs.* without a status function. Fixed as success() && ( … ) — the parentheses are load-bearing, since && binds tighter than || and success() && A || B would have let the force branch publish on top of a failed audit, which is the job that computes the version the name, guard and tag all read.

Job matrix after the change:

jobifenvironmentconcurrency
version-prevent == pushrelease-version-pr-<ref>
release-integrity(both events)release-integrity-<ref>
publishsuccess() && (pending || forced dispatch)releaserelease-publish-<ref>
docker!cancelled() && (…published)

One residual race is documented in the file rather than hidden: if the version-PR landing's audit job is itself pending behind an earlier one and a third push arrives, GitHub cancels it and the release quietly doesn't queue. Narrow, visible (no approval request arrives), and recoverable with no special handling — the version is still absent from npm, so the dispatch repair lane re-audits and queues the same deployment.

Landing this is a no-op, and that is the point

17.1.0 already shipped — run 32362556463 succeeded at 11:26 on 2d3860d, and npm now carries latest: 17.1.0 with @objectstack/cli, @objectstack/spec, @objectstack/core, @objectstack/account and @objectstack/console all present, plus the full tag set on the remote. (That run is also the production proof of #10148's fix: same lane, same pnpm pin, the publish it previously died before reaching.)

So merging this PR queues nothing: main carries 17.1.0, npm has it, D1's predicate is false, and publish is skipped on the landing push exactly as it will be on every ordinary merge. The new path first does anything real on the next Version Packages PR — which is the gentlest possible way to find out whether it behaves, since a wrong predicate shows up as no deployment queued rather than as an unwanted publish.

What is verified here is the YAML's shape, every gate the repo runs against it, and the predicate's arithmetic. What cannot be verified from here is the end-to-end push→approve path; that is observed on the next release.

…ersion (ADR-0125)
Maintainer ruling, 2026-08-20: 「合并 changeset 后,发版本前只需要有人工批准就可以,
没必要现在这样一定需要我去手工输入版本号。」
The 2026-08-07 ruling 「版本发布必须是人工的」 is untouched and still binding.
What changes is which human act carries it:
before maintainer opens Actions -> Run workflow and TYPES the exact version
after merging the Version Packages PR queues a deployment; the maintainer
APPROVES the `release` environment, and only then does anything run
Typing the version was doing two jobs and only one of them well. Proving a human
was there was its point -- but the `workflow_dispatch` EVENT is what did that,
not the typing. The cross-check against packages/cli/package.json is nearly
vacuous: changeset publish ships what package.json says, so a wrong string never
publishes the wrong version, it aborts the run. #10146 showed the cost of asking
for the string at all -- that run's log said 17.5.0, which is
@object-ui/console@17.5.0, the vendored objectui build, while the repo was
publishing 17.1.0.
D1 `publish` now runs on push to main, gated on ONE predicate computed by the
existing hardened audit: main's @objectstack/cli version is absent from
npm. True only just after a version PR merges, false on the other ~18
daily landings, so no ordinary merge queues a deployment.
D2 `environment: release` holds the WHOLE job -- no checkout, no build, no
publish -- and the job's name is computed from the audited version, so the
approval screen reads "Publish 17.1.0 to npm (awaiting approval)". The
maintainer confirms a version they are SHOWN, read from the object
database at github.sha, instead of one they recall.
D4 workflow_dispatch survives as the repair lane with no version input, plus
a dispatch-only `force` for finishing a partial publish -- the one case
D1's predicate cannot see. Same environment gate.
D5 Workflow-level concurrency is replaced by per-job groups. The old group
was keyed on github.event_name and its comment said the point was that the
lanes can never displace each other; that key stops separating anything
once both lanes are pushes. Worse, a job waiting on approval holds its run
in progress, so under one group an hour spent deciding would evict the
intervening pushes and stop #4935 regenerating.
The guard keeps R2's tripwire -- version read from the object database at
github.sha, refuse if the workspace disagrees -- and adds one: refuse if the tree
has moved off the version the approval was given against.
WHAT THIS COSTS, IN ONE LINE: the barrier moves from a property of the event
(a push cannot become a dispatch) to a repo SETTING (required reviewers on the
`release` environment) that no file here can assert. The maintainer confirmed it
is configured on 2026-08-20; ADR-0125 D3 is conditional on that staying true,
and both the workflow header and Prime Directive #15 now say so in the places a
reader will actually be standing.
AGENTS.md #15 gains the new reserved act -- never approve a pending `release`
deployment -- and records that merging the Version Packages PR is no longer
adjacent to the release but IS its trigger.
Governed surface (docs/adr/**, AGENTS.md): pushed for the maintainer to merge by
hand. No auto-merge armed.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRPcZzc7rfBQsNxJw3BSRh
@os-elon
os-elon requested a review from hotlong as a code ownerAugust 20, 2026 11:30
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation ci/cd labels Aug 20, 2026
…ratchet
check-skill-line-ratchet went red in Lint & Repo Gates: AGENTS.md was 965 lines
against a ceiling of 961, which main already sits on with zero headroom. The
amendment had split one paragraph into three and spent four lines on the split
alone.
Raising the ceiling is not the remedy — the gate says that takes a maintainer
ruling quoted in the PR, and there is none for this; the prose was simply too
long. Refolded into one paragraph, net zero lines, with every load-bearing point
kept: the barrier has moved from the workflow_dispatch EVENT to a repo setting
no file here can assert, an unprotected environment mimics an approved one
perfectly in the run log, and merging `chore: version packages` now queues a
deployment in the maintainer's name rather than moving a version number.
The new reserved act stays where it belongs — inline in the list of things a
seat may not do, costing no line at all: ⛔ approving a pending `release`
environment deployment, with the note that a deployment waiting for hours is the
system working, not a state to clear.
Gates re-run locally, all green: adr-links, adr-anchors, doc-authoring,
doc-anchors, role-word, quick-reference-counts, skill-frame-sync,
skill-frame-freshness, workflow-status-functions, required-contexts,
pm/governed-prose, pm/skill-line-ratchet, pm/skill-id-lint.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRPcZzc7rfBQsNxJw3BSRh
@os-elon
os-elon added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 81d1fa1Aug 20, 2026
20 checks passed
@os-elon
os-elon deleted the claude/release-failure-83i4qc branch August 20, 2026 13:27
os-steve pushed a commit that referenced this pull request Aug 24, 2026
…er in PD #15
Two sentences, two governed files, nothing else.
1. `docs/adr/0125-...md` line 3 declared itself "Proposed (2026-08-20) —
awaiting the maintainer's hand-merge" while being the authority the publish
lane cites eleven times. The merge it says it awaits is, by its own text,
the acceptance act; that merge landed as 81d1fa1 (#10150) on 2026-08-20.
The date is read from that merge, not from today.
2. `AGENTS.md` Prime Directive #15 said the Version Packages PR "is
regenerated on every push to `main`". The `version-pr` job left the push
trigger; it now runs on a six-hourly `schedule` or an on-demand
`refresh_version_pr` dispatch. `release.yml`'s `push: branches: [main]`
trigger drives the publish lane instead, so the sentence now says which
trigger it means.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-elon