Skip to content

feat(ci): publish unreleased main as @taskless/cli-nightly - #122

Merged
thecodedrift merged 3 commits into
mainfrom
openspec/nightly-2-publish
Aug 21, 2026
Merged

feat(ci): publish unreleased main as @taskless/cli-nightly#122
thecodedrift merged 3 commits into
mainfrom
openspec/nightly-2-publish

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 20, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

PR 2 of 3 for nightly-cli-builds. Adds the nightly publish itself: a pack script, its unit tests, and release-cli-nightly.yml.

Shape

  • .github/scripts/nightly-pack.cjs — pure exported helpers (selectProposedVersion, buildNightlyVersion, applyNightlyIdentity, hasNightlyForSha, isValidVersion) plus a main() that rewrites packages/cli/package.json to @taskless/cli-nightly and the stamped version, packs, and restores the manifest in finally. It asserts the official semver grammar before stamping, so an invalid version is never produced rather than being caught downstream.
  • release-cli-nightly.yml — push to main; a credential-free gate job, then publish under npm-autopublish. The gate/publish split is the same property release-cli.yml relies on: the OIDC identity only exists for a run that will actually publish.
  • README section, extended changeset (same file — the stack merges forward), .gitignore entries.

Against this repo's real state it produces @taskless/cli-nightly@0.11.0-20260820051459x773226a. Tarball verified: name and version rewritten, bin still {"taskless":"./dist/index.js"}, 13 optionalDependencies intact, and git status packages/cli/package.json clean afterwards.

Five things the proposal had wrong, found by building it

The first two were already corrected on the branches below; the last three are new and each would have broken a real run.

  1. Gate 1 was specified as a directory listing. .changeset/ is never empty — README.md and config.json are permanent — so it would have reported "pending" on every push forever, including the Version Packages merge it is supposed to self-handle. Now the require-changeset.yml rule: any .changeset/*.md other than README.md.

  2. The x separator's justification was wrong about the mechanism. The leading-zero rule bites only when the separator is a .; a bare concatenation stays valid semver, because the timestamp's leading digit is never 0. What concatenation actually produces is a 21-digit numeric identifier past exact double precision — 202608181234560123456 reads back as 202608181234560140000, and n === n+1 — so it destroys the chronological ordering the timestamp exists for, silently. The test now covers both alternatives against the semver grammar rather than only the dotted one.

  3. npm view --json on a 404 prints an error object to stdout and exits non-zero. So the natural $(npm view … || echo '[]') yields object-then-[] and crashes JSON.parse. The fallback has to replace the capture, not append to it. This is exactly the bootstrap-day state, when the package does not exist yet.

  4. npm view <pkg> versions --json returns a bare string when exactly one version exists — the state immediately after the manual bootstrap publish. Unhandled, the gate would have skipped every build until a second version landed.

  5. git rev-parse --short auto-scales its length with repo size. Unpinned, it would eventually emit 8 characters and stop matching 7-character suffixes, silently disabling dedupe. Pinned to --short=7, and the same value is passed to the pack script so the gate and the stamp cannot disagree.

What is verified, and what is not

Executed: the pack end-to-end and the resulting tarball's manifest; the version computation against real changeset status output; both gates as shell logic, including the npm view 404 path and the "only README.md and config.json" case; YAML parse of all 10 workflows.

Reasoned only: anything requiring the environment or OIDC — the npm-autopublish branch policy, the trusted-publisher binding, the real publish, and --tag latest actually moving on npm. This workflow cannot run end to end until the two maintainer actions happen, and the first real run is the only proof of them.

Before this merges

Two maintainer actions, in this order:

  1. npm-autopublish needs its deployment branch policy restricted to main. Done — verified as custom_branch_policies: true with exactly one policy, branch: main.
  2. @taskless/cli-nightly must be published once manually from the packed tarball, not the package directory (publishing a directory burns the name on a placeholder 0.0.0 — the trap release-vale.yml documents), then a trusted-publisher binding registered against release-cli-nightly.yml and the npm-autopublish environment. The workflow header carries the exact commands.

Registering that binding is also the first direct evidence of whether npm bindings are environment-scoped — which is what determines whether PR 3's Vale move invalidates its six existing bindings. That is why the nightly goes first.

Verification

pnpm lint clean · pnpm typecheck 1/1 · pnpm test 602 passing · openspec validate --all --strict 24/24 · node --test .github/scripts/*.test.cjs 122 passing

The change is deliberately not archived — PR 3 is the tip.

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

Built on top of #119

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

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

This PR adds the “nightly CLI” publishing path: on pushes to main with pending changesets, it packs the CLI as @taskless/cli-nightly with a stamped prerelease version and publishes it via OIDC trusted publishing in the npm-autopublish environment.

Changes:

  • Adds a reusable pack script (.github/scripts/nightly-pack.cjs) plus unit tests to stamp a nightly semver and pack without leaving packages/cli/package.json modified.
  • Introduces release-cli-nightly.yml with a credential-free gate job (pending changesets + dedupe by SHA) and a credentialed publish job.
  • Documents nightlies and ignores the generated tarball/status artifacts.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
README.md Documents how to install/use @taskless/cli-nightly and collision caveats.
openspec/changes/nightly-cli-builds/tasks.md Marks PR2 nightly tasks as completed and records measured corrections.
openspec/changes/nightly-cli-builds/design.md Refines the “x separator” rationale with measured failure modes.
.gitignore Ignores nightly build artifacts (.nightly-dist/, nightly-status.json).
.github/workflows/release-cli-nightly.yml Adds the nightly gate+publish workflow using OIDC trusted publishing.
.github/scripts/nightly-pack.test.cjs Adds unit tests for version selection/stamping and dedupe logic.
.github/scripts/nightly-pack.cjs Implements version selection/stamping, manifest rewrite/restore, and npm pack.
.changeset/nightly-cli-builds.md Extends the changeset to include the nightly publishing feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/release-cli-nightly.yml Outdated
@thecodedrift
thecodedrift marked this pull request as ready for review August 20, 2026 06:58
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 2m 37s —— View job


Claude finished @thecodedrift's task

Reviewed the diff against origin/openspec/nightly-1-workflow-split across correctness, dropped-behavior, reuse/simplification, and altitude/conventions. All findings below were independently verified against the actual file contents.

  • Gather context (PR diff, workflow YAML, pack script, tests)
  • Correctness/security pass
  • Removed-behavior audit (nothing dropped)
  • Reuse/simplification/efficiency pass
  • Altitude & CLAUDE.md/STYLEGUIDE-CODE.md conventions pass
  • Post findings

Overall assessment

Solid piece of work — the design doc's five corrections (leading-zero vs. precision-loss, npm view 404-to-stdout, single-version bare string, pinned --short=7) are all real fixes and all are exercised by tests. No invariant from the base branch was weakened or dropped. Two issues below are worth fixing before merge; the rest are cleanup/simplification.

Findings

1. (Correctness, confirmed) The pre-publish npm view guard doesn't actually close the gate/publish race, because the nightly version isn't deterministic across runs
.github/scripts/nightly-pack.cjs:279 stamps the version with new Date() inside main(), called fresh every time the publish job's pack step runs. The workflow's safety comment (release-cli-nightly.yml:237-241) claims this guard is "the same way release-cli.yml and release-vale.yml guard" — but those two compute a content-derived version (read verbatim from committed package.json, or stamped once upstream under a concurrency: group), so a retry recomputes the identical string and the npm view check is meaningful. Here, two independent executions of the publish job for the same commit — e.g. a duplicate webhook delivery, or re-running a publish step that actually succeeded but whose HTTP response was lost — each call new Date() at a different instant and mint two different versions both ending in the same x<sha> suffix. Gate 2 (hasNightlyForSha) treats "already published" as any version with that suffix; the final guard at release-cli-nightly.yml:248 treats it as that exact string exists. These two checks don't agree, and the missing concurrency: group (deliberate, per D6) relies entirely on them agreeing. Net effect: the "one nightly per commit" invariant the whole x<sha> scheme exists to guarantee can be silently violated.

2. (Correctness, confirmed) The write/pack/restore isn't fully inside the try/finally the comment claims
.github/scripts/nightly-pack.cjs:298-310: the comment above ("the restore is in finally so a failed pack does not strand a rewritten manifest") only covers the spawnSync call. Both writeFileSync(packageJsonPath, ...) (298) and writeFileSync(readmePath, ...) (299) run before the try block starts (300). If the README write throws after the package.json write succeeds (e.g. ENOSPC/EROFS on that specific write), the exception propagates before finally is ever registered, and packages/cli/package.json is left rewritten to the nightly identity in the working tree — contradicting the "committed manifest is never left rewritten" guarantee the PR description itself asserts. Moving both writeFileSync calls inside the try closes the gap.

3. (Test coverage) The gate-2 stdin/JSON-parsing wrapper is untested
release-cli-nightly.yml:156-165 embeds a ~10-line node -e program (stdin listener, JSON.parse, process.exit) inside a bash heredoc inside YAML, wrapping the tested hasNightlyForSha. Only the pure function is covered by nightly-pack.test.cjs; the wrapper — including the SHORT_SHA env round-trip and the 404→'[]' substitution — is exercised only by pushing to main. Every other release script in the repo (vale-gate.cjs, vale-prepare.cjs, nightly-pack.cjs itself) is a standalone .cjs entry point instead. Worth doing the same here (e.g. a small nightly-gate.cjs with its own test), especially since vale-gate.cjs already solves the identical "treat a 404 as empty, not an error" problem via a single in-process fetch() with no subprocess — that pattern could replace the npm view CLI shell-out + heredoc entirely.

4. (Minor/cosmetic) Misplaced JSDoc
.github/scripts/nightly-pack.cjs:173-181: the doc comment for hasNightlyForSha sits directly above buildNightlyReadme's own doc comment and implementation, not above hasNightlyForSha itself (which is defined later, at line 217). A reader following the comment top-to-bottom lands on the wrong function. Likely leftover from reordering during editing.

5. (Simplification, non-blocking) Some duplication with the Vale release scripts

  • parseArguments/setOutput-style CLI parsing (nightly-pack.cjs:233-272) is now a third near-identical copy of helpers already in vale-prepare.cjs and vale-gate.cjs — plausibly worth extracting to a shared .github/scripts/*-util.cjs now that there are three.
  • main()'s write/pack/restore (see finding 2) duplicates the same stamp-and-pack shape as vale-prepare.cjs's per-platform loop, per design.md:38's stated intent to reuse that shape — but the two independently reimplement it and disagree on restore-on-failure behavior (vale-prepare.cjs has no try/finally around its npm pack calls today). Not a blocker for this PR, but a signal a shared helper would pay off for both.
  • pnpm --filter @taskless/cli build (release-cli-nightly.yml:207) runs before the much cheaper pnpm exec changeset status (line 215); reordering would fail fast on a bad changeset state without spending build minutes first.

Nothing else rose to the level of a concrete bug — argument parsing, regex escaping, the npm view 404 handling, and the shell quoting/env: routing for untrusted values all check out.
· Branch: openspec/nightly-2-publish

thecodedrift added a commit that referenced this pull request Aug 20, 2026
A nightly shipped @taskless/cli's skill and recipe content verbatim, so
an agent following it ran `npx @taskless/cli` — the released CLI. Someone
installs a nightly to exercise unreleased behavior and their agent
silently uses the released binary. Nothing errors; the instructions are
simply for a different package.

Adds a `nightly` build target beside `dev` and `self`, rewriting the
baked invocation to `npx @taskless/cli-nightly@<version>` through the
same `__TASKLESS_CLI__` define the other targets already use. Target
resolution moves to `scripts/build-target.ts` so it can be unit-tested
over an explicit environment rather than this process.

The version is computed ONCE and shared. `nightly-pack.cjs
--print-version` stamps it; the build reads TASKLESS_NIGHTLY_VERSION and
the pack takes `--version`, which is the only version input pack mode
accepts — it rejects `--status`/`--sha` so it cannot derive a second one.
Two `new Date()` calls a build apart would ship instructions naming a
version that was never published, so this is enforced rather than
observed. A missing or malformed version fails the build; falling back
to `npx @taskless/cli` would silently reintroduce the bug being fixed.

`build:nightly` emits to `dist`, unlike `dev`/`self`, because
`files: ["dist"]` is what npm packs — so it overwrites a local prod
build, which the comment says.

Also closes a fail-open in gate 2, found in review on #122. Any non-zero
exit from the version query landed in the "no nightly found" branch, so
unparseable output meant publish. Since the version carries a timestamp,
a re-run after a parse failure mints a different version for the same
commit and publishes it — two nightlies for one SHA, no error anywhere,
by the gate whose only job is suppression. Now three-way: present means
skip, parsed-and-absent means build, unparseable means fail. The 404
carve-out stays, because on bootstrap day the package genuinely does not
exist.

The bootstrap block in the header gains the build step it was missing;
following it literally packed a tarball with no dist/.
@thecodedrift
thecodedrift force-pushed the openspec/nightly-1-workflow-split branch from cc8e4ca to 2225291 Compare August 20, 2026 22:21
thecodedrift added a commit that referenced this pull request Aug 20, 2026
A nightly shipped @taskless/cli's skill and recipe content verbatim, so
an agent following it ran `npx @taskless/cli` — the released CLI. Someone
installs a nightly to exercise unreleased behavior and their agent
silently uses the released binary. Nothing errors; the instructions are
simply for a different package.

Adds a `nightly` build target beside `dev` and `self`, rewriting the
baked invocation to `npx @taskless/cli-nightly@<version>` through the
same `__TASKLESS_CLI__` define the other targets already use. Target
resolution moves to `scripts/build-target.ts` so it can be unit-tested
over an explicit environment rather than this process.

The version is computed ONCE and shared. `nightly-pack.cjs
--print-version` stamps it; the build reads TASKLESS_NIGHTLY_VERSION and
the pack takes `--version`, which is the only version input pack mode
accepts — it rejects `--status`/`--sha` so it cannot derive a second one.
Two `new Date()` calls a build apart would ship instructions naming a
version that was never published, so this is enforced rather than
observed. A missing or malformed version fails the build; falling back
to `npx @taskless/cli` would silently reintroduce the bug being fixed.

`build:nightly` emits to `dist`, unlike `dev`/`self`, because
`files: ["dist"]` is what npm packs — so it overwrites a local prod
build, which the comment says.

Also closes a fail-open in gate 2, found in review on #122. Any non-zero
exit from the version query landed in the "no nightly found" branch, so
unparseable output meant publish. Since the version carries a timestamp,
a re-run after a parse failure mints a different version for the same
commit and publishes it — two nightlies for one SHA, no error anywhere,
by the gate whose only job is suppression. Now three-way: present means
skip, parsed-and-absent means build, unparseable means fail. The 404
carve-out stays, because on bootstrap day the package genuinely does not
exist.

The bootstrap block in the header gains the build step it was missing;
following it literally packed a tarball with no dist/.
@thecodedrift
thecodedrift force-pushed the openspec/nightly-2-publish branch from 0595e1c to 29d9b94 Compare August 20, 2026 23:36
thecodedrift added a commit that referenced this pull request Aug 20, 2026
A nightly shipped @taskless/cli's skill and recipe content verbatim, so
an agent following it ran `npx @taskless/cli` — the released CLI. Someone
installs a nightly to exercise unreleased behavior and their agent
silently uses the released binary. Nothing errors; the instructions are
simply for a different package.

Adds a `nightly` build target beside `dev` and `self`, rewriting the
baked invocation to `npx @taskless/cli-nightly@<version>` through the
same `__TASKLESS_CLI__` define the other targets already use. Target
resolution moves to `scripts/build-target.ts` so it can be unit-tested
over an explicit environment rather than this process.

The version is computed ONCE and shared. `nightly-pack.cjs
--print-version` stamps it; the build reads TASKLESS_NIGHTLY_VERSION and
the pack takes `--version`, which is the only version input pack mode
accepts — it rejects `--status`/`--sha` so it cannot derive a second one.
Two `new Date()` calls a build apart would ship instructions naming a
version that was never published, so this is enforced rather than
observed. A missing or malformed version fails the build; falling back
to `npx @taskless/cli` would silently reintroduce the bug being fixed.

`build:nightly` emits to `dist`, unlike `dev`/`self`, because
`files: ["dist"]` is what npm packs — so it overwrites a local prod
build, which the comment says.

Also closes a fail-open in gate 2, found in review on #122. Any non-zero
exit from the version query landed in the "no nightly found" branch, so
unparseable output meant publish. Since the version carries a timestamp,
a re-run after a parse failure mints a different version for the same
commit and publishes it — two nightlies for one SHA, no error anywhere,
by the gate whose only job is suppression. Now three-way: present means
skip, parsed-and-absent means build, unparseable means fail. The 404
carve-out stays, because on bootstrap day the package genuinely does not
exist.

The bootstrap block in the header gains the build step it was missing;
following it literally packed a tarball with no dist/.
@thecodedrift
thecodedrift force-pushed the openspec/nightly-1-workflow-split branch from 2225291 to e4f95f2 Compare August 20, 2026 23:43
@thecodedrift
thecodedrift force-pushed the openspec/nightly-2-publish branch from 29d9b94 to 217c114 Compare August 20, 2026 23:43
@thecodedrift
thecodedrift disabled auto-merge August 21, 2026 00:11
@thecodedrift
thecodedrift force-pushed the openspec/nightly-1-workflow-split branch from e4f95f2 to 137b943 Compare August 21, 2026 00:13
Base automatically changed from openspec/nightly-1-workflow-split to main August 21, 2026 00:15
thecodedrift and others added 3 commits August 20, 2026 17:16
Add release-cli-nightly.yml and the pack script behind it. Every push to
main with changesets pending publishes the CLI under a second name at
<next-version>-<yyyymmddhhmmss>x<short-sha>, so merged-but-unreleased work
is installable.

Two credential-free gates, in their own job, decide whether the publish job
exists at all: pending changesets (before any install) and whether the
commit already has a nightly. The rename happens at pack time, so the
committed manifest and @taskless/cli's version history are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
npm always includes README.md in a tarball regardless of `files`, so the
nightly was publishing @taskless/cli's documentation under a different
package name — install instructions for a package the reader did not
install, with nothing saying so. Someone arriving from a search would
follow them and never learn this is a prerelease of something else.

The pack swap now covers the README the same way it covers the manifest:
written, packed, restored in `finally`, so the committed file is never
left rewritten.

Deliberately minimal. It names the package, links to the real one for
documentation and support, explains what the version string encodes, and
carries the one warning a reader can act on destructively — that the
release and the nightly collide on the `taskless` executable and
installing both globally is unsupported. It does not restate anything
from the CLI's README, because a copy would have to be kept in sync with
a file it was copied from.
A nightly shipped @taskless/cli's skill and recipe content verbatim, so
an agent following it ran `npx @taskless/cli` — the released CLI. Someone
installs a nightly to exercise unreleased behavior and their agent
silently uses the released binary. Nothing errors; the instructions are
simply for a different package.

Adds a `nightly` build target beside `dev` and `self`, rewriting the
baked invocation to `npx @taskless/cli-nightly@<version>` through the
same `__TASKLESS_CLI__` define the other targets already use. Target
resolution moves to `scripts/build-target.ts` so it can be unit-tested
over an explicit environment rather than this process.

The version is computed ONCE and shared. `nightly-pack.cjs
--print-version` stamps it; the build reads TASKLESS_NIGHTLY_VERSION and
the pack takes `--version`, which is the only version input pack mode
accepts — it rejects `--status`/`--sha` so it cannot derive a second one.
Two `new Date()` calls a build apart would ship instructions naming a
version that was never published, so this is enforced rather than
observed. A missing or malformed version fails the build; falling back
to `npx @taskless/cli` would silently reintroduce the bug being fixed.

`build:nightly` emits to `dist`, unlike `dev`/`self`, because
`files: ["dist"]` is what npm packs — so it overwrites a local prod
build, which the comment says.

Also closes a fail-open in gate 2, found in review on #122. Any non-zero
exit from the version query landed in the "no nightly found" branch, so
unparseable output meant publish. Since the version carries a timestamp,
a re-run after a parse failure mints a different version for the same
commit and publishes it — two nightlies for one SHA, no error anywhere,
by the gate whose only job is suppression. Now three-way: present means
skip, parsed-and-absent means build, unparseable means fail. The 404
carve-out stays, because on bootstrap day the package genuinely does not
exist.

The bootstrap block in the header gains the build step it was missing;
following it literally packed a tarball with no dist/.
@thecodedrift
thecodedrift force-pushed the openspec/nightly-2-publish branch from 217c114 to 21c5b81 Compare August 21, 2026 00:16
@thecodedrift
thecodedrift merged commit bf7f86a into main Aug 21, 2026
2 checks passed
@thecodedrift
thecodedrift deleted the openspec/nightly-2-publish branch August 21, 2026 00:18
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