feat(ci): publish unreleased main as @taskless/cli-nightly - #122
feat(ci): publish unreleased main as @taskless/cli-nightly#122thecodedrift wants to merge 3 commits into
Conversation
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
…y-2-publish Reconciles two independent fixes of the same defect. Gate 1 was written as a bare directory listing, which is never true because `.changeset/` permanently holds `README.md` and `config.json`. That was found twice: once by review on the proposal branch, and again by measurement while building the gate here, because this branch was cut before the first fix landed. Both descriptions were right and each said something the other did not. The resolution keeps this branch's wording — it is checked off and folds in what building the thing actually taught — and grafts on the two clauses only the other had: that `changesets init` writes both files and nothing removes them, which is why they are permanent, and that a bare emptiness test would fire on the Version Packages merge, the one case the gate exists to handle. Task 4.3's rationale also survives from this side, since it corrects the proposal's stated mechanism: the leading-zero rule bites only if the separator is a `.`, while a bare concatenation stays valid semver and fails differently — a 21-digit numeric identifier past exact double precision, which breaks ordering rather than validity.
There was a problem hiding this comment.
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 leavingpackages/cli/package.jsonmodified. - Introduces
release-cli-nightly.ymlwith 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.
| versions=$(npm view @taskless/cli-nightly versions --json 2>/dev/null) || versions='[]' | ||
| if SHORT_SHA="$short_sha" node -e ' | ||
| const { hasNightlyForSha } = require("./.github/scripts/nightly-pack.cjs"); | ||
| let raw = ""; | ||
| process.stdin | ||
| .on("data", (chunk) => { raw += chunk; }) | ||
| .on("end", () => { | ||
| const versions = JSON.parse(raw.trim() || "[]"); | ||
| process.exit(hasNightlyForSha(versions, process.env.SHORT_SHA) ? 0 : 1); | ||
| }); | ||
| ' <<< "$versions"; then | ||
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | ||
| echo "A nightly ending in x${short_sha} is already published — nothing to do." | ||
| else | ||
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | ||
| echo "Will build a nightly for ${short_sha}." | ||
| fi |
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.
|
@claude /review |
|
Claude finished @thecodedrift's task in 2m 37s —— View job Claude finished @thecodedrift's taskReviewed the diff against
Overall assessmentSolid piece of work — the design doc's five corrections (leading-zero vs. precision-loss, Findings1. (Correctness, confirmed) The pre-publish 2. (Correctness, confirmed) The write/pack/restore isn't fully inside the 3. (Test coverage) The gate-2 stdin/JSON-parsing wrapper is untested 4. (Minor/cosmetic) Misplaced JSDoc 5. (Simplification, non-blocking) Some duplication with the Vale release scripts
Nothing else rose to the level of a concrete bug — argument parsing, regex escaping, the |
Stack (root → tip):
PR 2 of 3 for
nightly-cli-builds. Adds the nightly publish itself: a pack script, its unit tests, andrelease-cli-nightly.yml.Shape
.github/scripts/nightly-pack.cjs— pure exported helpers (selectProposedVersion,buildNightlyVersion,applyNightlyIdentity,hasNightlyForSha,isValidVersion) plus amain()that rewritespackages/cli/package.jsonto@taskless/cli-nightlyand the stamped version, packs, and restores the manifest infinally. 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 tomain; a credential-freegatejob, thenpublishundernpm-autopublish. The gate/publish split is the same propertyrelease-cli.ymlrelies on: the OIDC identity only exists for a run that will actually publish..gitignoreentries.Against this repo's real state it produces
@taskless/cli-nightly@0.11.0-20260820051459x773226a. Tarball verified: name and version rewritten,binstill{"taskless":"./dist/index.js"}, 13optionalDependenciesintact, andgit status packages/cli/package.jsonclean 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.
Gate 1 was specified as a directory listing.
.changeset/is never empty —README.mdandconfig.jsonare permanent — so it would have reported "pending" on every push forever, including the Version Packages merge it is supposed to self-handle. Now therequire-changeset.ymlrule: any.changeset/*.mdother thanREADME.md.The
xseparator'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 never0. What concatenation actually produces is a 21-digit numeric identifier past exact double precision —202608181234560123456reads back as202608181234560140000, andn === 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.npm view --jsonon a 404 prints an error object to stdout and exits non-zero. So the natural$(npm view … || echo '[]')yields object-then-[]and crashesJSON.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.npm view <pkg> versions --jsonreturns 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.git rev-parse --shortauto-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 statusoutput; both gates as shell logic, including thenpm view404 path and the "onlyREADME.mdandconfig.json" case; YAML parse of all 10 workflows.Reasoned only: anything requiring the environment or OIDC — the
npm-autopublishbranch policy, the trusted-publisher binding, the real publish, and--tag latestactually 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:
npm-autopublishneeds its deployment branch policy restricted tomain. Done — verified ascustom_branch_policies: truewith exactly one policy,branch: main.@taskless/cli-nightlymust be published once manually from the packed tarball, not the package directory (publishing a directory burns the name on a placeholder0.0.0— the traprelease-vale.ymldocuments), then a trusted-publisher binding registered againstrelease-cli-nightly.ymland thenpm-autopublishenvironment. 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 lintclean ·pnpm typecheck1/1 ·pnpm test602 passing ·openspec validate --all --strict24/24 ·node --test .github/scripts/*.test.cjs122 passingThe change is deliberately not archived — PR 3 is the tip.
Refs #111
Refs OSS-34