Skip to content

feat(ci): announce a published nightly on the Version Packages PR - #133

Open
thecodedrift wants to merge 1 commit into
mainfrom
feat/nightly-region-on-changeset-pr
Open

feat(ci): announce a published nightly on the Version Packages PR#133
thecodedrift wants to merge 1 commit into
mainfrom
feat/nightly-region-on-changeset-pr

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

When a nightly publishes, the open changesets "Version Packages" pull request (head changeset-release/main, currently #74) gains a build-info region at the end of its body:

<!-- nightly -->
### Build Info
`npx @taskless/cli-nightly@0.11.0-20260818123456x05b3c88`

**Built from:** 05b3c88
**Built at:** 2026-08-18 12:34:56
<!-- /nightly -->

Those reviewers are exactly the audience for a build of the work the pending changesets describe.

⚠️ One deviation from the issue text — please overrule me if you disagree

The issue writes the install line as npx @taskless/cli@<version>. Nightlies publish under @taskless/cli-nightly (.github/scripts/nightly-pack.cjs, design D2 — the pack rewrites name before packing), and @taskless/cli@0.11.0-20260818123456x05b3c88 does not exist on npm. That line would 404, or at best send a reviewer to the last release.

So the region names @taskless/cli-nightly — the package that will actually install. Everything else matches the issue's format byte for byte. If you want the literal text instead, it is one constant in nightly-breadcrumb.cjs.

How it is built

A third job, breadcrumb, holding pull-requests: write and no credential. The publish job holds an OIDC identity authorized to publish under the @taskless scope; adding pull-requests: write to it would widen what a compromised step there can reach from "publish a package" to "publish a package and rewrite pull request text" — including the text of the PR that gates the next release. Same boundary the file already draws between gate and publish, drawn once more. No id-token, no environment, no contents write beyond checkout-read.

needs: publish is what "block on a successful publish" means mechanically — a job whose dependency was skipped does not run, so a suppressed nightly (either gate false) never reaches it.

The version arrives as a job output, and every fact is parsed back out of it. The stamp is <n.m.k>-<yyyymmddhhmmss>x<sha>, so it already carries the build time and the commit. A fresh Date.now() would print a time that disagrees with the version on the line above it, and a fresh git rev-parse would print a sha the published tarball does not carry. The "stamped exactly once" rule is already load-bearing here (--print-version); this is its third consumer and it obeys it.

No open Version Packages PR exits 0. changeset-release/main exists only while changesets are pending, and a nightly can publish in the seconds before changesets opens it — a cosmetic breadcrumb must never fail a run that already published to npm. That is deliberately not the same branch as a failed API call: gh api is allowed to exit non-zero and fail the step, with no || echo '[]' fallback collapsing the two (the same fail-open closed in gate 2 by task 4.13).

gh api -X PATCH repos/{owner}/{repo}/pulls/<n>, never gh pr edit — its GraphQL path is broken by the Projects (classic) deprecation (CLAUDE.md).

Upsert semantics, and what the tests cover

The region is removed and re-appended at the end rather than replaced in place, so it converges from any starting state. .github/scripts/nightly-breadcrumb.test.cjs (16 tests) covers:

  • repeated publishes replace, never accumulate (one region after N publishes)
  • appends at the end when absent; idempotent for one version
  • a manually deleted region is re-attached
  • a region a human moved mid-body is moved back to the end, not duplicated
  • coexistence with <!-- stack --> — that region is left byte-for-byte alone across two publishes, and a stack region containing the word "nightly" is not mistaken for one
  • a non-array pulls response throws (empty vs. malformed are different answers)

Not reusing stack-breadcrumb.cjs

Its REGION_PATTERN hardcodes the name stack and upsertCarriedRegion is keyed to PR numbers. Neither generalizes to a singleton region under another name, and widening them would make a working, well-tested file serve two callers with different invariants. Fresh zero-dependency helpers in the same style, with a .test.cjs sibling picked up by validate.yml's node --test .github/scripts/*.test.cjs.

Spec

openspec/changes/nightly-cli-builds is still open, so this adds D11 to design.md, task 4.14, and a requirement (A published nightly is announced on the pending release pull request) to the cli-nightly-builds delta.

Verification (all run locally in the worktree)

Command Result
pnpm lint pass (eslint, exit 0)
pnpm typecheck pass (1/1 task; cache hit — no TS changed)
pnpm test pass — 51 files, 630 tests
node --test .github/scripts/*.test.cjs pass — 161 tests, 0 fail (16 new)
pnpm openspec validate --all --strict 24 passed, 0 failed
node .github/scripts/openspec-visibility.cjs every requirement in 23 specs visible

Also smoke-tested the script end to end against fixture pulls.json payloads: an existing region is replaced, an empty list exits 0 with changed=false, and an error object exits 1.

Conflict heads-up

The PR for #127 is being written in parallel and also touches .github/workflows/release-cli-nightly.yml (it changes how the workflow triggers). Edits here are localized — an outputs: block on publish and one appended job — but whichever merges second will need a rebase.

CI-only, no user-facing release note → skip-changeset. Per .github/workflows/changeset.yml the check is advisory now; the label suppresses the warning rather than unblocking a merge.

Fixes #128

Copilot AI lite review requested due to automatic review settings August 21, 2026 04:42
@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Aug 21, 2026

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

Adds a post-publish nightly build breadcrumb to the pending Version Packages PR.

Changes:

  • Adds a least-privileged breadcrumb workflow job.
  • Implements breadcrumb parsing, rendering, upsert logic, and tests.
  • Updates OpenSpec design, requirements, and tasks.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Summary Review comments
openspec/changes/nightly-cli-builds/tasks.md Adds task 4.14. No final comments.
openspec/changes/nightly-cli-builds/specs/cli-nightly-builds/spec.md Adds announcement requirements and scenarios. No final comments.
openspec/changes/nightly-cli-builds/design.md Documents design D11. No final comments.
.github/workflows/release-cli-nightly.yml Adds the post-publish PR annotation job. Critical: Full-body updates can lose concurrent changes; use re-fetch-and-merge or coordinate writers.
.github/scripts/nightly-breadcrumb.test.cjs Tests parsing, rendering, replacement, and edge cases. No final comments.
.github/scripts/nightly-breadcrumb.cjs Implements version parsing and breadcrumb upsertion. Moderate: Global newline normalization can alter unrelated user-authored content; limit it to removed-region boundaries and add a regression test.
Suppressed comments (3)

.github/scripts/nightly-breadcrumb.cjs:183

  • This helper places the nightly region at the end only for the body it currently receives. If the Version Packages PR also has carried <!-- PR:N --> regions, the existing stack canonicalizer treats <!-- nightly --> as description and appends carried regions after it, so the next stack reconcile/carry moves nightly away from the required final position. Preserve this marker in the shared canonical layout and cover the carried-region case.
function upsertRegion(body, version) {
  const region = renderRegion(version);
  const description = stripRegion(body);
  return description.length === 0 ? region : `${description}\n\n${region}`;

.github/workflows/release-cli-nightly.yml:386

  • The head filter does not constrain the base branch. GitHub permits multiple open PRs from the same head branch to different bases, so a PR from changeset-release/main targeting another branch could be returned and selected for this PATCH instead of the Version Packages PR. Add base=main to the query (or validate pull.base.ref in the selector) before writing.
          gh api "repos/${REPO}/pulls?state=open&head=${OWNER}:changeset-release/main" > nightly-pulls.json

.github/workflows/release-cli-nightly.yml:357

  • This workflow deliberately has no cross-run concurrency, but this job writes the single shared “latest nightly” region. Two pushes can publish concurrently; if the older run reaches this job after the newer run, its PATCH will leave the Version Packages PR pointing at the stale build. needs: publish only orders jobs within one run, so serialize or cancel stale breadcrumb jobs (or reject an older stamped version before writing).
  breadcrumb:
    name: Link the nightly on the Version Packages PR
    needs: publish

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

Comment on lines +161 to +162
.replaceAll(ALL_REGIONS_PATTERN, "")
.replaceAll(/\n{3,}/g, "\n\n")
Comment on lines +403 to +404
gh api -X PATCH "repos/${REPO}/pulls/${PULL_NUMBER}" \
-f body="$(cat nightly-body.md)" > /dev/null
After a nightly publishes, the open `changeset-release/main` pull request
gains a `<!-- nightly -->` region at the end of its body naming the build:
the install line, the commit it was built from, and the time it was built.
Reviewers of that PR are exactly the audience for a build of the work its
changesets describe.

The region is removed and re-appended on every publish, so a hand-deleted
region comes back, repeated publishes replace rather than accumulate, and
a body someone has reordered converges. It never touches the `<!-- stack -->`
region stack-breadcrumb.yml may maintain on the same body.

A THIRD JOB, holding `pull-requests: write` and no credential — never a
step in `publish`, which would then hold both an npm OIDC identity and the
ability to rewrite pull request text. It `needs: publish`, so a suppressed
nightly never reaches it, and the version arrives as a job output: the
stamp already encodes the build time and the sha, so both are parsed back
out rather than read from a second clock.

No open Version Packages PR exits 0 — the branch exists only while
changesets are pending — but a failed `gh api` query still fails the step.

Note: the issue writes the install line as `npx @taskless/cli@<version>`.
Nightlies publish as `@taskless/cli-nightly`, and that version does not
exist under the released name, so the region names the package that will
actually install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift force-pushed the feat/nightly-region-on-changeset-pr branch from 413c039 to 8936111 Compare August 21, 2026 06:04
@thecodedrift

Copy link
Copy Markdown
Member Author

Rebased onto main at 423363a (the archived-change main) — no merge commit, history stays linear. Force-pushed with a lease; the single commit is GPG-signed (G).

The nightly-cli-builds change was archived while this was in flight, so the three files I had edited under openspec/changes/nightly-cli-builds/ no longer exist. Git followed the rename and put my edits inside openspec/changes/archive/2026-08-21-nightly-cli-builds/; those are dropped — an archived change is closed and does not get reopened.

The spec content is re-homed accordingly:

  • The requirement (A published nightly is announced on the pending release pull request, five scenarios) now lives directly in openspec/specs/cli-nightly-builds/spec.md, appended after A nightly publish carries the same attestation as a release.
  • Design decision D11 has nowhere to live, so its reasoning is folded into the header comment of .github/workflows/release-cli-nightly.yml, under ANNOUNCING THE NIGHTLY ON THE VERSION PACKAGES PR — why a third job rather than a step in publish, why the version is parsed rather than re-measured, why not stack-breadcrumb.cjs, and why the region names @taskless/cli-nightly and not the issue's literal @taskless/cli. That file is where someone editing this behavior will actually be looking.
  • openspec/specs/vale-binary-packages/spec.md is untouched — the Vale-environment requirement stays withheld pending Finish nightly-cli-builds: move Vale to npm-autopublish and verify the acceptance criteria #131.

The diff against main is now four files: the two new .github/scripts/nightly-breadcrumb*.cjs, the workflow, and the spec. Nothing under openspec/changes/.

Re-verified after the rebase: pnpm lint exit 0 · pnpm typecheck 1/1 · pnpm test 51 files / 630 tests · node --test .github/scripts/*.test.cjs 161 pass, 0 fail · pnpm openspec validate --all --strict 24 passed, 0 failed · openspec-visibility.cjs all requirements in 24 specs visible.

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

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


Review mode: incremental — read 2 prior review thread(s) before reviewing.

Both prior threads (from copilot-pull-request-reviewer) are still isResolved: false with no addressed-marker reply, so I re-raised both as [Unchanged since last review]:

  • .github/scripts/nightly-breadcrumb.cjs:162 — global newline collapse in stripRegion can alter unrelated user-authored content.
  • .github/workflows/release-cli-nightly.yml:446 — full-body PATCH is a GET-then-write with no re-fetch/merge, vulnerable to lost updates from concurrent writers of the same PR body (changesets bot, stack-breadcrumb.yml).

The prior review also had 3 suppressed (collapsed, non-thread) Copilot comments — lower confidence, never posted as inline threads, so they don't carry isResolved state. I re-examined each against the current code rather than skip them:

  • Missing base check in selectVersionPullRequest — held up, re-raised as [New].
  • No concurrency/staleness protection on the breadcrumb job across separate workflow runs — held up, re-raised as [New].
  • Possible interaction with stack-breadcrumb.cjs's canonical body layout moving the region away from the end — I traced this through `stack-breadcrumb.cjs` (`ownDescription` / `canonicalizeBody`) and confirmed the mechanism concretely: it survives when carried regions are also present. Re-raised as [New] with the trace.

Findings (5 inline comments posted)

  1. [Unchanged] nightly-breadcrumb.cjs:162 — unconditional \n{3,}\n\n collapse whenever any region is/was present, not scoped to the removal seam.
  2. [Unchanged] release-cli-nightly.yml:446 — full-body PATCH from an earlier snapshot; lost-update window against other writers of the same PR body.
  3. [New] nightly-breadcrumb.cjs:205selectVersionPullRequest matches only on head.ref, never base.ref; a second open PR from changeset-release/main to a non-main base could be mis-selected.
  4. [New] release-cli-nightly.yml:399needs: publish only orders jobs within one run; no concurrency group means an older run's breadcrumb job can finish after a newer run's and write stale build info.
  5. [New] nightly-breadcrumb.cjs:183 — if the Version Packages PR carries stack-breadcrumb.cjs `` regions, its next reconcile treats the nightly region as ordinary description and can re-lay it before those carried blocks, breaking the "always at the end" requirement. Not exercised by either test suite.

Everything else — version parsing/rendering, the upsert idempotency, the gh api -X PATCH (not gh pr edit) choice, job permission scoping, the gate/publish/breadcrumb split, and the spec scenarios — reads correctly and matches what the extensive header comments describe. Test coverage for the parts that are tested (16 new tests) is solid.

I did not run lint/typecheck/tests myself — CI (Validate, Changeset) already reports those as green on this PR.
· Branch

return (
text
.replaceAll(ALL_REGIONS_PATTERN, "")
.replaceAll(/\n{3,}/g, "\n\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Unchanged since last review] This .replaceAll(/\n{3,}/g, "\n\n") is unconditional across the whole body whenever a region was present, not scoped to the seam the removal left behind. If the PR description contains an intentional 3+ newline run elsewhere (e.g. inside a fenced code block that isn't indented, or deliberate spacing), every future republish silently collapses it — content unrelated to the managed region gets rewritten. Restricting the collapse to the boundary immediately around the removed region (or tracking the removal offsets) plus a regression test for unrelated blank-line runs would close this.

run: |
set -euo pipefail
gh api -X PATCH "repos/${REPO}/pulls/${PULL_NUMBER}" \
-f body="$(cat nightly-body.md)" > /dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Unchanged since last review] This PATCHes the full body from a snapshot fetched earlier in the job (nightly-pulls.json). Both the changesets bot (pushing new release notes to this same PR) and stack-breadcrumb.yml (if this PR is stacked) can rewrite the body between the gh api GET here and this PATCH, and whichever writes last wins — the nightly region or the concurrently-written content can be silently dropped. A re-fetch-and-merge (or optimistic concurrency check) before the PATCH would close the window; at minimum this is worth an explicit acknowledged-risk note if it's accepted as-is.

(pull) =>
pull &&
pull.head &&
pull.head.ref === branch &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] (Related to a suppressed, non-thread comment on the prior Copilot review, which I independently confirmed by reading the code.) This only matches on pull.head.ref === branch; it never checks pull.base.ref. GitHub allows multiple open PRs from the same head branch to different base branches, so if a second PR is ever opened from changeset-release/main targeting a non-main base (e.g. for testing), the gh api query in the workflow (head=${OWNER}:changeset-release/main, no base= filter) can return both, and .find() here would silently pick whichever comes first — potentially not the real Version Packages PR — and PATCH its body instead. Adding a pull.base.ref === "main" check here (or base=main in the gh api query) would close this.

# answers and only one of them is fine.
breadcrumb:
name: Link the nightly on the Version Packages PR
needs: publish

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] (Related to a suppressed, non-thread comment on the prior Copilot review.) needs: publish only orders jobs within a single workflow run. There's no concurrency group on this workflow (deliberately, per the header comment — but that reasoning covers publish's own idempotent npm view guard, not this job). Two pushes to main in quick succession start two independent runs; if the older run's breadcrumb job happens to execute after the newer run's, its PATCH will leave the Version Packages PR pointing at the stale build even though a newer nightly already published. Worth either a concurrency group scoped to this workflow (cancelling in-progress older runs) or comparing the stamped version's timestamp against whatever is already on the PR before writing.

function upsertRegion(body, version) {
const region = renderRegion(version);
const description = stripRegion(body);
return description.length === 0 ? region : `${description}\n\n${region}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[New] (Related to a suppressed, non-thread comment on the prior Copilot review; I traced it through stack-breadcrumb.cjs to confirm.) This places the region at the true end of the body only as long as nothing else re-lays that body afterward. If the Version Packages PR is also part of a stack that carries regions (via carryForward/carryBackward in stack-breadcrumb.cjs), the next stack reconcile calls canonicalizeBody, which rebuilds the body as breadcrumb → ownDescription(body) → carried regions (sorted). ownDescription only strips and regions — it doesn't know about — so the nightly region survives embedded inside "description" and gets reassembled *before* the carried blocks, no longer at the end of the body. The workflow header (and this file's own doc comment) note that "a Version Packages PR can be in a stack" and that the two region types "never rewrite" each other's markers, but that only covers this script not touching / content — it doesn't cover stack-breadcrumb.cjs's own canonicalizer moving the nightly region once carried regions exist. The requirement text says the region "SHALL be placed at the end of the pull request body" unconditionally; this is the one path where that can be violated, and it isn't covered by either test suite (nightly-breadcrumb.test.cjs only tests the reverse direction: this script not disturbing an existing stack region).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Link to latest Nightly is added to the Changeset PR

2 participants