Skip to content

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

Merged
thecodedrift merged 4 commits into
mainfrom
openspec/nightly-1-workflow-split
Aug 21, 2026
Merged

refactor(ci): split release.yml so each workflow carries one design#119
thecodedrift merged 4 commits into
mainfrom
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

Refs OSS-34

Built on top of #116

Proposal only — no implementation. Artifacts for a nightly build of the CLI published from main as a separate package, plus a split of the release workflows so each carries one design.

What it proposes

  • @taskless/cli-nightly, published from main, version n.m.k-yyyymmddhhmmssx<sha>
  • Two gates: is .changeset/ empty (a directory listing, before any install), and is this SHA already built
  • A new npm-autopublish environment for flows that publish without a human click
  • Four workflow files replacing release.yml and vale-binaries.yml, each with one trust story

The design is argued in #111; design.md records the decisions with their reasoning rather than restating the conclusions.

Built from main, not from PRs

The original framing was "on PR update". That is what the title of #111 said until this proposal was written, and it does not survive contact with release.yml's security model: a PR-triggered publish would route contributor-authored changeset text into a credentialed job, inverting the split that file exists to maintain, and would publish unreviewed code under the @taskless scope.

Tying builds to changeset edits does not work either. Measured on the #71#106 stack, add-vale-rule-engine had 7 commits after its last changeset edit and agent-command-and-vale-authoring had 11 — a nightly stamped at changeset-edit time would omit all of them while looking current.

Delivery: stacked, merging forward, three PRs

  1. Split the release workflows — behavior-preserving, plus a correction to release.yml's header, which claims "No required reviewers … by design" while npm-production demonstrably has a required reviewer.
  2. The nightly — including creating npm-autopublish and registering trusted publishing.
  3. Move Vale to npm-autopublish — tip, archives the change.

The nightly deliberately precedes the Vale move. npm trusted-publisher bindings can be scoped to a GitHub environment. If the six @taskless/vale-* bindings are, moving Vale first invalidates all of them and the next publish fails the OIDC handshake with no stored token to fall back on — trading a working release path for a convenience. The nightly exercises the same environment, handshake, and binding model on a package where failure is free: nothing resolves @taskless/cli-nightly, so a failed first publish blocks nobody. Stated generally in design.md, because it outlives this change: prove a new credential path on something disposable before migrating something that works onto it.

Two prerequisites no implementer can satisfy

Called out in the proposal body rather than buried in tasks, because they gate merges:

  1. The npm-autopublish environment must be created in repo settings — no required reviewers, branch policy on main.
  2. @taskless/cli-nightly must be published once manually and a trusted-publisher binding registered. Trusted publishing is per-package, and the package does not exist yet.

Both sit as maintainer task groups before the nightly unit. A PR waits rather than merging with a dangling environment reference.

Gaps this surfaced in #111

Writing the design down found four things the discussion had left implicit, all captured as tasks:

  • changeset status returns an array; taking [0] breaks the day a second changesets-managed package appears. Filter by name.
  • npm view … versions lists versions regardless of tag, so a version that published but failed to get tagged reads as already-built and is skipped forever. The Vale workflow handles that case explicitly; nightly's was left implicit.
  • Renaming a workflow renames its check. main requires Validate; any branch-protection or breadcrumb reference to an old name silently stops reporting.
  • Whether Vale's trusted-publisher bindings are environment-scoped is unknown and unasked — the most likely way the Vale PR breaks.

openspec validate --all --strict: 24 passed, 0 failed.

No changeset — a proposal ships nothing.

Refs #111

Refs OSS-34

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
@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
@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
@thecodedrift
thecodedrift marked this pull request as ready for review August 20, 2026 05:48
@thecodedrift
thecodedrift force-pushed the openspec/nightly-cli-builds branch from 512110b to d4a3b6a Compare August 20, 2026 22:21
@thecodedrift
thecodedrift force-pushed the openspec/nightly-1-workflow-split branch from cc8e4ca to 2225291 Compare August 20, 2026 22:21
@thecodedrift
thecodedrift force-pushed the openspec/nightly-cli-builds branch from d4a3b6a to 2dd8494 Compare August 20, 2026 23:43
@thecodedrift
thecodedrift force-pushed the openspec/nightly-1-workflow-split branch from 2225291 to e4f95f2 Compare August 20, 2026 23:43
Base automatically changed from openspec/nightly-cli-builds to main August 21, 2026 00:11
thecodedrift and others added 4 commits August 20, 2026 17:13
`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
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.
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
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 force-pushed the openspec/nightly-1-workflow-split branch from e4f95f2 to 137b943 Compare August 21, 2026 00:13
@thecodedrift
thecodedrift merged commit a22a429 into main Aug 21, 2026
2 checks passed
@thecodedrift
thecodedrift deleted the openspec/nightly-1-workflow-split branch August 21, 2026 00:15
thecodedrift added a commit that referenced this pull request Aug 21, 2026
#131

`main` had been failing the `Check for unarchived OpenSpec changes on main`
step in `validate.yml` on every push since 162afa0, because this change
directory was still sitting under `openspec/changes/`. Issue #127 will make a
red `main` suppress nightly publishing outright, so a red `main` had stopped
being cosmetic.

Every requirement in the change's spec delta shipped in #116, #119 and #122 —
the workflow split, the pack script, the build target, both publish gates and
the version stamp. What remained was one environment move and a set of
verification steps, neither of which any delivered requirement depends on. Both
move to #131 rather than holding `main` red.

Removes the `vale-binary-packages` delta from the change. Applying it would
have written into the published spec that the Vale workflow "SHALL publish
without a human approval step, using the reviewer-free publishing environment",
which is false: `release-vale.yml` still names `npm-production`, which has a
required reviewer. A requirement describing deferred work is worse than no
requirement, because a spec is what later readers trust.

Also corrects task 3.2, briefly marked done on the strength of `npm view`
showing a published version. That is evidence of the manual bootstrap in 3.1,
not of a trusted-publisher binding; the two are independent.

Refs #131

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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