refactor(ci): split release.yml so each workflow carries one design - #119
refactor(ci): split release.yml so each workflow carries one design#119thecodedrift wants to merge 5 commits into
Conversation
`release.yml` held two jobs with opposite trust properties behind a single
header. Split it, with no change to what any job does:
- `release-cli-changeset.yml` — the `version` job, keeping
`concurrency: release-${{ github.ref }}`. It consumes untrusted changeset
text and holds no credential and no OIDC identity.
- `release-cli.yml` — the `check` and `publish` jobs, deliberately in one
file: the credential-free gate is what keeps an OIDC-capable job from being
instantiated on an ordinary merge, and it must not be separated from the job
it protects. Deliberately no concurrency group; the gate makes a duplicate
run a no-op and the residual TOCTOU is treated as possibly-already-published.
- `vale-binaries.yml` → `release-vale.yml`, rename only; its workflow name and
concurrency group are unchanged, so its check name still reports.
Also corrects a header claim that had been false since the environment was
created: `npm-production` has a required reviewer and a protected-branches
deployment policy, so a release has always waited on a human approval the
comment said was not there.
Verified while implementing, and corrected in the change's design/tasks:
`changeset status --output=` writes an object whose per-package entries live
under `releases` (not a bare array), and an absolute `--output` path is
resolved against the working directory rather than ignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
There was a problem hiding this comment.
Pull request overview
Refactors the release CI configuration by splitting the former monolithic release.yml into dedicated workflows per trust model (CLI version PR, CLI publish, Vale publish), and updates docs/spec material to reflect the new workflow filenames and corrected changeset status --output output shape.
Changes:
- Split
release.ymlintorelease-cli-changeset.yml(Version Packages PR) andrelease-cli.yml(check + gated publish), and delete the originalrelease.yml. - Rename the Vale workflow file to
release-vale.ymland update references in Vale package READMEs and the Vale manifest comment. - Update OpenSpec
design.md/tasks.mdand add a changeset entry describing the workflow split.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
packages/vale-win32-x64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
packages/vale-win32-arm64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
packages/vale-linux-x64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
packages/vale-linux-arm64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
packages/vale-darwin-x64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
packages/vale-darwin-arm64/README.md |
Update workflow reference from vale-binaries.yml to release-vale.yml. |
openspec/changes/nightly-cli-builds/tasks.md |
Mark PR1 tasks complete and update measured facts for later PRs. |
openspec/changes/nightly-cli-builds/design.md |
Correct the documented changeset status --output JSON shape and related guidance. |
.github/workflows/release.yml |
Removed legacy combined release workflow. |
.github/workflows/release-vale.yml |
Rename/reference updates to reflect the split CLI workflows. |
.github/workflows/release-cli.yml |
New: CLI check + gated publish workflow. |
.github/workflows/release-cli-changeset.yml |
New: Version Packages PR workflow (credential-free). |
.github/scripts/vale-manifest.json |
Update comment reference to release-vale.yml. |
.changeset/nightly-cli-builds.md |
New changeset documenting the workflow split and header correction. |
Suppressed comments (1)
.github/workflows/release-vale.yml:36
- The comment refers to the “STAMPED version” as what
release-cli.ymluses, butrelease-cli.ymlchecks the manifest (exact) version frompackages/cli/package.json; it doesn’t stamp versions the way Vale does. Rewording avoids implying the CLI workflow stamps versions too.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The rename left `name: Vale Binaries` and `concurrency: vale-binaries` inside `release-vale.yml`, on the reasoning that task 1.5 asked for no behavior change. That preserved exactly the mismatch this split exists to remove: a file named for one thing announcing itself as another, in a change whose whole subject is making each workflow say what it is. Safe because branch protection requires only `Validate` (confirmed in task 0.1), so no required check is keyed to the old display name. The concurrency group is renamed with it; the only effect is that a run already in flight under the old group would not serialize against a new one, which is a single transition, not a standing property. Task 1.5 now says to do this, resolving its tension with 1.7 rather than leaving the next reader to rediscover it.
|
@claude /review |
|
Claude finished @thecodedrift's task in 4m 25s —— View job Review completeTodo list
SummaryThis is a clean, well-reasoned split of One real discrepancy found (posted inline on Second finding (posted inline on Everything else checked out:
Nothing here blocks merging; both findings are about the accuracy of the PR narrative/changeset text rather than the shipped workflow behavior, which looks correct and appropriately cautious. |
The release note claimed nothing about how the CLI is built, versioned, or published changes. Two things do: check/publish no longer share the release-* concurrency group, and the release runs as two workflow runs, so its check contexts are renamed. The publish mechanics are what is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Both findings were real and are addressed: the PR body now describes the Vale — AI Coding Agent |
Both inline comments are answered on their threads (the changeset overclaim is fixed in 78cf458; the — AI Coding Agent |
|
@claude /review |
|
Claude finished @thecodedrift's task in 5m 54s —— View job Review completeTodo list
SummaryI re-diffed the extracted One new finding, posted inline on Everything else checked out:
Nothing here blocks merging; the one new finding is a documentation/behavior-mismatch issue rather than a functional risk to the release itself. |
Removing the concurrency group was justified by a mitigation that lives in a different file. `release-vale.yml` runs `npm view` immediately before each `npm publish` and skips when the version is already there; `release-cli.yml` had a bare `npm publish` with no pre-check and no error handling. The header, design D6, and task 1.3 all claimed otherwise. The `check` job does query npm, but it is a separate job — which is exactly where the window is. A gate in another job cannot close the gap between its own answer and the publish. So the guard moves to where it can do the work, immediately before the publish. That is better than serializing rather than merely equivalent: it is idempotent instead of ordered, so it also absorbs a re-run against a version an earlier attempt already shipped, which a concurrency group does nothing about. Without it the losing run of a race fails with npm's "cannot publish over the previously published version" — a duplicate that reads as a broken release. The three places that asserted the old story now describe this one, and say that the omitted concurrency group is safe *because of* the guard rather than on its own.
…o openspec/nightly-1-workflow-split
Stack (root → tip):
PR 1 of 3 for the
nightly-cli-buildschange. Behavior-preserving apart from one correction, and it exists so PRs 2 and 3 add one flow each to a file that already has one job.The split
release-cli-changeset.ymlversion— opens the Version Packages PRconcurrency: release-${{ github.ref }}release-cli.ymlcheck+publish, in one filenpm-production, approvalrelease-vale.ymlvale-binaries.ymlrelease.ymlis deleted. Each new file carries a header written for its own trust story rather than a copy of the original —release.yml's header is the reason its security properties survived this long, and splitting it without splitting the reasoning would have thrown that away.checkandpublishstay together deliberately: the credential-free gate is what keeps an OIDC-capable job from existing on an ordinary push, and separating it from the job it protects is the arrangement most likely to be broken by a later partial edit.release-cli.ymlhas no concurrency group, also deliberately, and the header says why — the gate makes a duplicate publish a no-op, and the residual TOCTOU is handled the way the Vale workflow already handles it, treating a publish failure as possibly-already-published.Verified equivalence
A parsed-config diff of old versus new shows
version,check, andpublishbyte-identical, with the same triggers and the samepermissions: {}. All 9 workflow files parse underjs-yaml.Two intentional behavior differences:
check/publishno longer share therelease-*concurrency group (D6).Release CLI Version PR / …andRelease CLI / …instead ofRelease / ….Vale Binaries→Release Vale,vale-binaries→release-vale), so its check context changes too and anything keyed on the old name (badges, notification rules,gh run list --workflow) stops matching.None of these is a required check. Branch protection on
mainrequires onlyValidate, fromci.yml— confirmed via the API, so no repo-settings change is needed.pr-check-openspec.yml,require-changeset.yml,stack-breadcrumb.yml, and.github/scripts/*.cjskey on nothing workflow-name-specific.The renamed Vale workflow is renamed through:
name:becomesRelease Valeandconcurrency:becomesrelease-vale. A file calledrelease-vale.ymlthat still announced itself asVale Binarieswould reproduce the naming mismatch this split exists to remove, so task 1.5 calls for both. Its whole header comment is preserved verbatim apart from therelease.yml→release-cli.ymlreference.The one behavior-adjacent correction
release.yml's header claimed "No required reviewers (fully automatic once the Version Packages PR merges), by design." That is false:npm-productionhasrequired_reviewers(reviewerthecodedrift) plus a protected-branches policy. A release has always waited on an approval the file denied existed. The new header states what is configured, and why.Three things measurement corrected in the proposal
Task group 0 is verification, and it contradicted the design in three places. All three are fixed in
design.mdandtasks.mdon this branch, since PR 2 codes against them:changeset status --outputreturns an object, not an array. It is{changesets: [...], releases: [{name, type, oldVersion, changesets, newVersion}]}. The bump isdata.releases.find(r => r.name === "@taskless/cli").newVersion. The proposal described a bare top-level array — so the review finding about not indexing with[0]was right about the hazard and wrong about the shape.--output=/tmp/x.jsonresolves against cwd to<cwd>/tmp/x.jsonand fails loudly withENOENTand exit 1. It does not silently write nothing. It would write to the wrong place silently only if that directory happened to exist.npm-autopublishalready exists, created 2026-08-18 — but with no protection rules and no deployment branch policy, meaning any branch may deploy to it. Group 2 is half-done and the missing half is the branch restriction. Noted inline on task 2.1 so nobody assumes it is complete. Inert today since no workflow references it; load-bearing the moment PR 3 does.@taskless/cli-nightlyis confirmed unclaimed on npm.Verification
pnpm lintclean ·pnpm typecheck1/1 ·pnpm test602 passing ·openspec validate --all --strict24/24 ·.github/scriptstests 113 passing across 4 files (turbo does not cover these, so they were run directly)Refs #111