Skip to content

refactor(ci): split release.yml so each workflow carries one design - #119

Open
thecodedrift wants to merge 5 commits into
openspec/nightly-cli-buildsfrom
openspec/nightly-1-workflow-split
Open

refactor(ci): split release.yml so each workflow carries one design#119
thecodedrift wants to merge 5 commits into
openspec/nightly-cli-buildsfrom
openspec/nightly-1-workflow-split

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 19, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

PR 1 of 3 for the nightly-cli-builds change. 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

File Job(s) Credential
release-cli-changeset.yml version — opens the Version Packages PR none, keeps concurrency: release-${{ github.ref }}
release-cli.yml check + publish, in one file npm-production, approval
release-vale.yml renamed from vale-binaries.yml unchanged

release.yml is 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.

check and publish stay 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.yml has 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, and publish byte-identical, with the same triggers and the same permissions: {}. All 9 workflow files parse under js-yaml.

Two intentional behavior differences:

  1. check/publish no longer share the release-* concurrency group (D6).
  2. The release now runs as two workflow runs rather than one, so check contexts become Release CLI Version PR / … and Release CLI / … instead of Release / ….
  3. The Vale workflow's display name and concurrency group are renamed (Vale BinariesRelease Vale, vale-binariesrelease-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 main requires only Validate, from ci.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/*.cjs key on nothing workflow-name-specific.

The renamed Vale workflow is renamed through: name: becomes Release Vale and concurrency: becomes release-vale. A file called release-vale.yml that still announced itself as Vale Binaries would 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 the release.ymlrelease-cli.yml reference.

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-production has required_reviewers (reviewer thecodedrift) 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.md and tasks.md on this branch, since PR 2 codes against them:

  1. changeset status --output returns an object, not an array. It is {changesets: [...], releases: [{name, type, oldVersion, changesets, newVersion}]}. The bump is data.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.
  2. The absolute-path trap was mis-stated. --output=/tmp/x.json resolves against cwd to <cwd>/tmp/x.json and fails loudly with ENOENT and exit 1. It does not silently write nothing. It would write to the wrong place silently only if that directory happened to exist.
  3. npm-autopublish already 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-nightly is confirmed unclaimed on npm.

Verification

pnpm lint clean · pnpm typecheck 1/1 · pnpm test 602 passing · openspec validate --all --strict 24/24 · .github/scripts tests 113 passing across 4 files (turbo does not cover these, so they were run directly)

Refs #111

`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
Copilot AI lite review requested due to automatic review settings August 19, 2026 19:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml into release-cli-changeset.yml (Version Packages PR) and release-cli.yml (check + gated publish), and delete the original release.yml.
  • Rename the Vale workflow file to release-vale.yml and update references in Vale package READMEs and the Vale manifest comment.
  • Update OpenSpec design.md/tasks.md and 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.yml uses, but release-cli.yml checks the manifest (exact) version from packages/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.

Comment thread openspec/changes/nightly-cli-builds/tasks.md Outdated
Comment thread .changeset/nightly-cli-builds.md Outdated
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.
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 4m 25s —— View job


Review complete

Todo list

  • Gather context (PR body, diff, review comments)
  • Understand the request (code review of PR refactor(ci): split release.yml so each workflow carries one design #119)
  • Review workflow YAML changes (release-cli.yml, release-cli-changeset.yml, release-vale.yml, deleted release.yml)
  • Review OpenSpec design.md/tasks.md updates
  • Review changeset note
  • Post inline comments for concrete issues
  • Post final top-level assessment

Summary

This is a clean, well-reasoned split of release.yml into three single-purpose workflows. I diffed the extracted version/check/publish job bodies against the deleted release.yml byte-for-byte (comments aside) and they match the PR's "verified equivalence" claim: same triggers, same permissions: {}, same action SHA pins, same steps. The one intentional behavior correction (the stale "no required reviewers" claim in the header) is accurately described and fixed.

One real discrepancy found (posted inline on release-vale.yml): the PR description states the renamed Vale workflow "keeps name: Vale Binaries and concurrency: vale-binaries... untouched; renaming them is available later." The actual diff does the opposite — name: becomes Release Vale and concurrency: becomes release-vale. This is confirmed by the diff itself and by tasks.md's own task 1.5, which was rewritten in this PR to say the rename should happen ("a file called release-vale.yml that still announces itself as Vale Binaries reproduces the naming mismatch this split exists to remove"). So the code and the OpenSpec docs agree with each other, but the PR body's prose is stale and describes a different decision than what shipped. Functionally this looks safe (branch protection only requires Validate, confirmed via the API per the PR body), but it's an undisclosed third behavior difference not counted in the "two intentional behavior differences" list, and worth reconciling so the description doesn't mislead future readers about what "verified equivalence" actually covers.

Second finding (posted inline on .changeset/nightly-cli-builds.md, echoing Copilot's suppressed comment): the changeset's closing line "Nothing about how @taskless/cli is built, versioned, or published changes" is broader than the PR's own disclosed facts — the loss of the shared release-* concurrency group and the split into two workflow runs (with renamed check contexts) are real operational differences to the release process, even though the publish steps/credentials are unchanged. Worth tightening the wording so the release note doesn't overclaim.

Everything else checked out:

  • No other file in the repo (outside the OpenSpec change dir, which documents history) still references release.yml or vale-binaries.yml.
  • stack-breadcrumb.yml, require-changeset.yml, and pr-check-openspec.yml key on nothing workflow-name-specific, confirming the PR's claim.
  • No test or script (.github/scripts/*.cjs) asserts on the renamed workflow filename string other than the manifest comment itself, which was updated correctly.
  • Action SHA pins, job permissions, and environment gating (npm-production, required reviewer) are unchanged and correctly carried into the new files.
  • design.md's corrected changeset status --output JSON shape (object with releases array, not a bare array) is now consistent with tasks.md and reads clearly.

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.

@thecodedrift
thecodedrift marked this pull request as draft August 19, 2026 19:19
Comment thread .github/workflows/release-vale.yml
Comment thread .changeset/nightly-cli-builds.md Outdated
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
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 4m 25s … Review complete"
#119 (comment)

Both findings were real and are addressed: the PR body now describes the Vale name:/concurrency: rename accurately and counts it as a third intentional behavior difference (previously it claimed the opposite), and 78cf458 rewrites the changeset so the "unchanged" claim is scoped to the publish mechanics with the concurrency-group and two-run differences named explicitly.

— AI Coding Agent

@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @copilot-pull-request-reviewer — "## Pull request overview — Refactors the release CI configuration by splitting the former monolithic release.yml into dedicated workflows per trust model…"

Both inline comments are answered on their threads (the changeset overclaim is fixed in 78cf458; the tasks.md vale-binaries.yml reference is intentional, since task 1.5 is where the rename happens). On the suppressed comment about release-vale.yml's "STAMPED version — the thing release-cli.yml uses" line: the ambiguity predates this PR — that sentence is carried over verbatim from vale-binaries.yml with only the filename updated, and task 1.5 calls for preserving the header comment, so rewording it is left to a change that owns that file's prose.

— AI Coding Agent

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 54s —— View job


Review complete

Todo list

  • Gather context (PR body, diff, prior review comments)
  • Diff against the PR's own 3 commits (HEAD~3..HEAD; matches the stated +274/-181 across 14 files)
  • Review workflow YAML changes (release-cli.yml, release-cli-changeset.yml, release-vale.yml, deleted release.yml)
  • Review OpenSpec design.md/tasks.md updates
  • Review changeset note and README/manifest updates
  • Post inline comments for concrete issues
  • Post final top-level assessment

Summary

I re-diffed the extracted version/check/publish job bodies against the deleted release.yml line by line: steps, permissions:, action SHA pins, and the npm view/npm publish logic are unchanged apart from comment placement, matching the PR's "verified equivalence" claim. The two issues raised in the earlier review round (the stale "Vale keeps its old name" claim in the PR body, and the changeset's "nothing changes" overclaim) are both fixed in the current tip — the PR body now lists the Vale rename as a third intentional difference, and .changeset/nightly-cli-builds.md now scopes the "unchanged" claim to build/publish mechanics while naming the concurrency-group and two-run differences explicitly. I did not re-flag either.

One new finding, posted inline on .github/workflows/release-cli.yml:36-40: the new header claims the residual publish-race (TOCTOU) left by dropping the concurrency group "is handled the way release-vale.yml handles it... a failed publish is treated as possibly-already-published rather than as a fresh error." release-vale.yml really does implement that — it runs npm view before npm publish and skips already-published packages (lines 335-336) — but release-cli.yml's publish step (line 132) is a bare npm publish --provenance --access public with no pre-check and no failure handling. A losing run in a race will just fail the job with npm's "cannot publish over the previously published version" error; nothing here "treats" that as a no-op the way Vale's loop does. Not dangerous — npm's immutable-version guarantee still prevents an actual double-publish — but the header asserts a mitigation that isn't implemented, which undercuts the split's own stated goal of documenting trust/behavior precisely. Suggest either porting the npm view guard into this step, or rewording the claim to "surfaces as a failed-but-harmless run" rather than "handled."

Everything else checked out:

  • No file outside the OpenSpec change directory (which documents history on purpose) still references release.yml or vale-binaries.yml; .github/scripts has no logic keyed on workflow names.
  • release-cli-changeset.yml keeps concurrency: release-${{ github.ref }} and the credential-free/no-OIDC posture; release-cli.yml keeps check+publish together with the gate reasoning intact; both carry permissions: {} at the workflow level with per-job grants matching the original exactly.
  • The npm-production "no required reviewers" correction is accurate and consistently reflected in the new header, the changeset, and design.md.
  • design.md's corrected changeset status --output shape (an object with a releases array, selected by name, never [0]) is now self-consistent with tasks.md, and the "absolute path" trap description matches this repo's actual layout (no tmp/ dir at repo root, so it fails loudly with ENOENT as stated rather than silently).
  • The six Vale package READMEs and the manifest comment are updated consistently and completely.

Nothing here blocks merging; the one new finding is a documentation/behavior-mismatch issue rather than a functional risk to the release itself.
· Branch

Comment thread .github/workflows/release-cli.yml Outdated
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.
@thecodedrift
thecodedrift marked this pull request as ready for review August 20, 2026 05:48
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.

2 participants