feat(release): retire the prerelease flow under the @next=@latest alias policy - #367
Conversation
…as policy Now that @next is an alias for @latest (PR #366), a separate prerelease channel is nonsensical: publishing 0.40.2-rc.1 to @latest would silently install the prerelease for every user, and @next == @latest means there is no other surface to ship it on. This PR removes the prerelease flow entirely. Changes: - .github/workflows/publish.yml: * Drop the v*-rc* / v*-next* tag-push trigger (publish only on main). * Drop the prerelease branch in the context job — context now resolves to mode=stable | mode=none only. * Drop the 'Stamp version from tag (prerelease only)' step (used to align package.json to a tag suffix). * Add an explicit prerelease-suffix rejection: if package.json has a '-' in the version, the workflow errors and refuses to publish (better message, before publish completes). * Tidy mode comments accordingly (mode=stable|none, dist_tag=latest). * Drop the obsolete 'next prerelease re-advances @next' comment in the @next alignment step (the alias is now done in the same job). - scripts/release.sh: DELETED — its sole job was the prerelease flow. - scripts/prepare-release.sh: drop the -rc suffix handling (cut -d- -f1) and update the 'prerelease versions are not supported' error to reflect the alias policy (no longer refers to the deleted release.sh). - scripts/publish.sh: refresh the dist-tag-invariant comment to the alias wording (the CI workflow is the preferred path; this is the manual fallback for the legacy token-based publish). - CLAUDE.md: * Drop the rc.N form from the versioning line; document that prerelease versions are not publishable under the alias policy. * Drop the 'next release (prerelease)' section in Release process. * Drop the prerelease-version regex note in 'CI gates to remember' — replaced with a 'No prerelease versions' gate. * Drop the 'release.sh' reference in Release scripts (only prepare-release.sh remains). * Drop the prerelease-rebase carveout in 'Git workflow'. * Tidy the Publishing section (single channel, alias wording). - core/workflows/update.md: drop the select_channel step entirely; both 'CHANNEL is latest' and 'CHANNEL is next' branches collapse to one path; the install command pins to @latest. - CHANGELOG: Unreleased 'Removed' entry documents each of the above. Older rc tags in git history (e.g. v0.44.0-rc.2) remain inert — the workflow no longer matches them.
WalkthroughThis PR removes the prerelease release path and standardizes publishing on ChangesPrerelease removal and single-channel publishing
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR retires the prerelease release/publish pathway now that the project policy is @next == @latest (single-channel publishing), and updates CI + docs to prevent prerelease semver strings from being shipped via @latest.
Changes:
- Simplifies
.github/workflows/publish.ymlto publish only frommain, removes tag-triggered prerelease logic, and hard-rejectspackage.jsonversions containing-. - Removes the prerelease helper script (
scripts/release.sh) and updates release documentation/process to reflect the single-channel policy. - Updates the update workflow docs (
core/workflows/update.md) to always install@latestand removes channel selection.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/publish.yml |
Removes prerelease tag triggers/branching and adds prerelease-version rejection while keeping @next aligned to @latest. |
scripts/release.sh |
Deleted prerelease-oriented release helper script. |
scripts/prepare-release.sh |
Removes prerelease suffix handling and adds clearer “no prerelease” messaging. |
scripts/publish.sh |
Updates commentary around the @next == @latest alias invariant for manual/token publishing. |
core/workflows/update.md |
Removes channel selection and pins update/install flow to @latest. |
CLAUDE.md |
Updates release/publishing docs to reflect single-channel policy and no-prerelease gate. |
CHANGELOG.md |
Documents retirement of prerelease flow under the alias policy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/publish.yml (1)
123-134: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winBest-effort
@nextalignment doesn't guarantee the@next =@latest`` invariant.The coding guideline states
publish.ymlmust keep@nextas an alias of@latestby moving both dist-tags to the same version after each release. The current step is best-effort: ifnpm dist-tag addfails (OIDC token may not authorize dist-tag operations), the step warns but doesn't fail, leaving@nextpointing at the previous version. This violates the "must" requirement.Consider adding a post-alignment verification step that compares
@nextand@latestand surfaces a actionable warning (or creates a tracking issue) when they diverge, so the invariant is at least monitored rather than silently broken.💡 Suggested verification step
- name: Show dist-tags run: npm dist-tag ls `@nforma.ai/nforma` || true + - name: Verify `@next` == `@latest` + run: | + DIST_TAGS=$(npm dist-tag ls `@nforma.ai/nforma` 2>/dev/null || echo "") + LATEST=$(echo "$DIST_TAGS" | awk '/^latest:/ {print $2}') + NEXT=$(echo "$DIST_TAGS" | awk '/^next:/ {print $2}') + if [[ "$LATEST" != "$NEXT" ]]; then + echo "::warning::`@next` ($NEXT) != `@latest` ($LATEST). Align manually: npm dist-tag add `@nforma.ai/nforma`@${{ needs.context.outputs.version }} next" + else + echo "OK: `@next` and `@latest` both point to $LATEST" + fi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/publish.yml around lines 123 - 134, The publish workflow’s Align `@next` with `@latest` step is best-effort and can silently leave the tags diverged. Update the logic in the publish job to verify the final dist-tag state after the npm dist-tag add attempt by checking both `@next` and `@latest` for the released version. If they differ, emit an actionable warning (or fail/report via a tracking mechanism) so the invariant is explicitly monitored rather than assumed.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CLAUDE.md`:
- Around line 98-100: The release documentation is inconsistent with the
available npm scripts: `CLAUDE.md` says `prepare-release.sh` is the only release
path, but `package.json` still exposes `release` pointing to the removed
`scripts/release.sh`. Update the release surface so it is consistent by either
removing the `release` script from `package.json` (and any related references)
or keeping `CLAUDE.md` explicitly marked as deprecated until that script is
removed; use the `release` script entry and the `Release scripts` section in
`CLAUDE.md` to locate the affected spots.
In `@core/workflows/update.md`:
- Around line 149-153: The update workflow currently hides failures from the npm
refresh step by piping the global install through a failure-swallowing fallback.
In the update step in update.md, remove the suppression so that the `npm install
-g `@nforma.ai/nforma`@latest` refresh surfaces errors and can stop the workflow
when it fails; keep the command as the version-refresh mechanism, but do not
append a silent fallback that masks the failure.
---
Outside diff comments:
In @.github/workflows/publish.yml:
- Around line 123-134: The publish workflow’s Align `@next` with `@latest` step is
best-effort and can silently leave the tags diverged. Update the logic in the
publish job to verify the final dist-tag state after the npm dist-tag add
attempt by checking both `@next` and `@latest` for the released version. If they
differ, emit an actionable warning (or fail/report via a tracking mechanism) so
the invariant is explicitly monitored rather than assumed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f450cab8-9133-4db7-9ae4-0b11b3d00d1e
📒 Files selected for processing (7)
.github/workflows/publish.ymlCHANGELOG.mdCLAUDE.mdcore/workflows/update.mdscripts/prepare-release.shscripts/publish.shscripts/release.sh
💤 Files with no reviewable changes (1)
- scripts/release.sh
Five valid concerns from Copilot + CodeRabbit on PR #367: 1. prepare-release.sh: --auto arithmetic can crash if CURRENT_VERSION contains a -rc suffix (PATCH becomes non-numeric, '$((PATCH + 1))' fails before the prerelease refusal below). Add an early guard at the top of the script that errors on a prerelease starting version before --auto touches it. 2. publish.sh: legacy token-based script still allowed publishing a prerelease to @latest. Refuse prereleases up front (under the @next=@latest alias policy, publishing 0.40.2-rc.1 to @latest would silently install the prerelease for every user). 3. package.json: 'release' npm script still pointed at the deleted scripts/release.sh. Remove both that line and the '!scripts/release.sh' entry in the published-files allowlist. 4. core/workflows/update.md: 'npm install -g ... || true' silently swallowed install failures — a stale updater could continue with the old version and no clear signal. Drop '|| true' and surface any install error. 5. package.json published-files: removed '!scripts/release.sh' (the file is gone; an allowlist entry for it is now a stale reference).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
scripts/publish.sh:48
- This
if ! echo "$VERSION" | grep ...guard is now redundant: the script already exits earlier if the version contains-. Keeping the extra check makes the control flow misleading; the alignment block can run unconditionally.
# publishing via this legacy script.
if ! echo "$VERSION" | grep -qE '\-'; then
echo ""
echo "=== Aligning @next dist-tag ==="
npm dist-tag add "@nforma.ai/nforma@${VERSION}" next
Three more valid concerns on PR #367's review-fix commit: 1. publish.sh: 'Publishing @nforma.ai/nf...' was a stale typo — the package is @nforma.ai/nforma (was the original pre-rename name). Update the printed banner to the current package name. 2. core/workflows/update.md: my prior 'remove || true' fix was insufficient — the command also redirected stderr to /dev/null, which would still hide the install-failure reason. Drop the stderr redirect so the error actually surfaces. 3. agents/skills/shipping-and-launch/SKILL.md: still listed scripts/release.sh as a release script. Update to the alias- policy wording (only scripts/prepare-release.sh + the legacy scripts/publish.sh; release.sh was deleted).
…alias Under the @next=@latest alias policy the publish MUST land on @latest. A caller passing '--tag next' (or any non-latest tag) would publish to a tag the alias invariant doesn't reach, and the downstream 'npm dist-tag add ... next' step would silently point @next at the wrong tarball (or, worse, at a tag that points somewhere else entirely). Refuse any --tag arg in $@, except an explicit '--tag=latest' (which is a no-op given the script now pins --tag=latest unconditionally). Bare '--tag latest' (space-separated) is also refused to keep the allowlist explicit.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
scripts/publish.sh:73
- This prerelease check is redundant: the script exits earlier if VERSION contains a "-" (prerelease), so this condition is always true. Consider removing the conditional and running the dist-tag alignment unconditionally to reduce dead code.
if ! echo "$VERSION" | grep -qE '\-'; then
echo ""
echo "=== Aligning @next dist-tag ==="
npm dist-tag add "@nforma.ai/nforma@${VERSION}" next
echo "Aligned @next → ${VERSION}"
fi
The previous comment said 'Only a bare --tag latest (or no --tag at all) is allowed' but the parser only allows the --tag=latest form — bare '--tag latest' (space-separated) is refused. Update the comment to match the actual CLI surface so callers aren't misled.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
scripts/publish.sh:75
- This prerelease check is now redundant because the script exits earlier if
package.jsoncontains a-prerelease suffix. Keeping the extragrepgate adds complexity without changing behavior; the alignment step can be unconditional.
if ! echo "$VERSION" | grep -qE '\-'; then
echo ""
echo "=== Aligning @next dist-tag ==="
npm dist-tag add "@nforma.ai/nforma@${VERSION}" next
echo "Aligned @next → ${VERSION}"
fi
…ate flags The previous version allowed '$@ --tag=latest' through, but the script then unconditionally added its own '--tag=latest' before 'npm publish' — resulting in duplicate flags. Strip the --tag=latest allowlist entirely (now the script refuses any --tag arg), which matches the comment too: 'callers who want to publish to @latest should drop --tag entirely'.
…he prerelease gate) The script never cd'd into ROOT_DIR after computing it. This was a latent issue: the npmrc write goes to $ROOT_DIR/.npmrc (the right place), but 'npm publish' packages the current working directory, and the new prerelease gate reads ./package.json (both fail if the user invoked the script from anywhere other than the repo root). Add the missing 'cd $ROOT_DIR' immediately after computing it. Both the prerelease check and the 'npm publish' call now work regardless of the caller's CWD.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
scripts/publish.sh (1)
68-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant prerelease guard — this branch is now unconditional.
Since the new early check at Lines 33-37 hard-exits on any prerelease
VERSION, execution only reaches Line 73 whenVERSIONhas no-suffix. Theif ! echo "$VERSION" | grep -qE '\-'condition is therefore always true and can be dropped for clarity.♻️ Proposed simplification
-if ! echo "$VERSION" | grep -qE '\-'; then - echo "" - echo "=== Aligning `@next` dist-tag ===" - npm dist-tag add "`@nforma.ai/nforma`@${VERSION}" next - echo "Aligned `@next` → ${VERSION}" -fi +echo "" +echo "=== Aligning `@next` dist-tag ===" +npm dist-tag add "`@nforma.ai/nforma`@${VERSION}" next +echo "Aligned `@next` → ${VERSION}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/publish.sh` around lines 68 - 78, The prerelease check around the `@next` dist-tag alignment in publish.sh is now redundant because the earlier VERSION validation already exits on any prerelease, so this branch only runs for stable versions. Simplify the legacy fallback in the dist-tag alignment block by removing the conditional guard and leaving the npm dist-tag add logic in place unconditionally, while keeping the existing “Aligning `@next` dist-tag” and success messages. Use the VERSION handling logic and the `@next` alignment section as the anchors when updating the script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@scripts/publish.sh`:
- Around line 68-78: The prerelease check around the `@next` dist-tag alignment in
publish.sh is now redundant because the earlier VERSION validation already exits
on any prerelease, so this branch only runs for stable versions. Simplify the
legacy fallback in the dist-tag alignment block by removing the conditional
guard and leaving the npm dist-tag add logic in place unconditionally, while
keeping the existing “Aligning `@next` dist-tag” and success messages. Use the
VERSION handling logic and the `@next` alignment section as the anchors when
updating the script.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6bd880c2-a87f-4512-95de-6d8ae35d1e44
📒 Files selected for processing (5)
agents/skills/shipping-and-launch/SKILL.mdcore/workflows/update.mdpackage.jsonscripts/prepare-release.shscripts/publish.sh
💤 Files with no reviewable changes (1)
- package.json
✅ Files skipped from review due to trivial changes (1)
- agents/skills/shipping-and-launch/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (1)
- core/workflows/update.md
…writer live path (#370) * test(release): red-proven gates for the alias-policy guards and goal-writer live path Closes a P4 violation: the anti-prerelease guards shipped in #366/#367 had zero tests, and /nf:goal-writer had no behavioural gate. By this repo's own recurrence record (config-path drift fixed 6x, null-CLI 4x), an ungated class comes back. bin/release-guards.test.cjs — 12 tests over all five guards: - publish.sh: prerelease package.json version refused - publish.sh: caller-supplied --tag refused (next, =next, latest, =latest) - prepare-release.sh: explicit prerelease target refused - prepare-release.sh: --auto with a prerelease CURRENT version refused cleanly (without the early guard, cut -d. -f3 yields "2-rc" and $((PATCH + 1)) is a bash arithmetic error) - publish.yml: version guard present AND its predicate actually matches a prerelease but not a stable version; no v*-rc*/v*-next* triggers; no dist_tag=next / mode=prerelease modes SAFETY: publish.sh ends in `npm publish`, so a test must never be able to reach it — including in the red case where a guard is broken. Each script runs in a throwaway sandbox (its own ROOT_DIR, package.json and .env) against a stub npm that only records argv and never touches the network. Every guard test asserts npm was never invoked with `publish`, so a broken guard fails an assertion rather than publishing. One positive control asserts a stable version DOES reach the publish step, proving the prerelease rejection is caused by the version and not by sandbox breakage. bin/goal-writer-blocks.test.cjs — 9 tests executing the skill's own blocks: - no `node -e` in an executable shell fence (the nf-node-eval-guard DENIES that form, so a documented `node -e` fails every run — the exact bug that reached main in #368 and that lint:isolation plus all four skill lints passed, because they check for arguments AFTER an eval, never -e itself) - every shell block is valid under `bash -n` - the recurrence-scan grep pattern runs under stock /usr/bin/grep, catching non-POSIX \w - the char-count verifier round-trips a condition containing quotes, backticks, parens and $VAR intact - the four /goal contract facts are pinned (4000-char limit, no @file.md syntax, mandatory turn cap, no per-tool permission grant) — drift in any of them yields an unsatisfiable generated condition All seven guards mutation-proven RED, not merely green: each guard was removed, the corresponding test observed to fail, and the file restored. The two goal-writer tests were proven against the actual bugs that shipped. Both files wired into test:ci — a test outside CI is not a gate. * fix(test): execute the SHIPPED char-count verifier, not a reimplementation CodeRabbit caught a P11 violation inside the very file whose purpose is P11: the char-count test ran a new inline Node program instead of extracting and executing the NF_EVAL verifier from goal-writer.md. A reimplementation stays green when the shipped verifier's input contract or count logic changes — exactly the live-path gap this file was written to close. Now extracts the NF_EVAL body from the skill and runs THAT, asserting: - the reported count equals the true condition length, using a condition containing quotes, backticks, parens and $VAR - a short condition reports OK - a 4001-char condition reports TOO LONG (new negative case) Executing the real block immediately surfaced a behavioural difference the reimplementation hid: console.log colourises numeric args via util.inspect, so the shipped verifier's count can arrive wrapped in ANSI escapes. The JSON.stringify-based copy never showed this. Two robustness fixes on the back of that: 1. ANSI stripping no longer relies on a raw ESC byte embedded in source. A literal ESC is invisible, and any formatter or copy-paste that drops it silently degrades the pattern to /\[[0-9;]*m/ — which strips "[33m" but leaves a bare ESC that then defeats the following \s* match. The test would pass or fail depending on whether the runner colourises. Now built with String.fromCharCode(27). 2. Colour is pinned OFF for the verifier subprocess (NO_COLOR=1, FORCE_COLOR removed), so the result no longer depends on the developer's shell. Both test files verified green under a plain run AND under FORCE_COLOR=3 — the environment that would previously have exposed the flakiness. 22/22 across both suites. --------- Co-authored-by: open-swe[bot] <jonathan@jonathanborduas.com>
Why
PR #366 established
@nextas an alias for@latest. With that policy in place, a separate prerelease channel is nonsensical:0.40.2-rc.1to@latestwould silently install the prerelease for every user doingnpm install @nforma.ai/nforma@latest.@next == @latestmeans there is no other surface to ship it on.v*-rc*tag trigger,scripts/release.sh, the version-stamp job, the prerelease branch incontext, theCHANNEL is nextbranch inupdate.md) is now dead weight.This PR retires the entire prerelease flow.
Changes
publish.yml— single channel:v*-rc*/v*-next*tag-push trigger (publish only on main).contextjob —modeis nowstable | none.Stamp version from tag (prerelease only)step.package.jsonhas a-in the version, the workflow errors and refuses to publish (better message, before publish completes).@nextalignment step — drop the obsolete "the next prerelease will also re-advance @next" comment (the alias is now done in the same job).scripts/release.sh— DELETED (its sole job was the prerelease flow).scripts/prepare-release.sh— drop the-rcsuffix handling (cut -d- -f1) and update the prerelease error message to reflect the alias policy.scripts/publish.sh— refresh the dist-tag-invariant comment to the alias wording (CI is the preferred path; this is the manual fallback).CLAUDE.md:rc.Nform from the versioning line; document that prerelease versions are not publishable.next release (prerelease)section.CI gates to remember— replaced with a**No prerelease versions**gate.release.shreference inRelease scripts(onlyprepare-release.shremains).Git workflow.Publishingsection (single channel, alias wording).core/workflows/update.md:select_channelstep entirely.CHANNEL is latestandCHANNEL is nextbranches to one path.@latest.CHANGELOG.md—[Unreleased] Removed:entry documents each of the above.Verified locally
(Per the
testci-flakes-locallynote, fulltest:ciis gated on GitHub CI, not the local aggregate.)Out of scope (still open after this PR)
Unreleasedblock inCHANGELOG.mdshould move to a dated[VERSION]heading under the next release — gated by the existing CHANGELOG gate.v*-rc*tags in git history (e.g.v0.44.0-rc.2) remain inert — the workflow no longer matches them. Cleaning them up is a separate housekeeping task.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
nextmirroringlatest.latest) dist-tag.Bug Fixes
-).latest.Chores
rcrelease flow and removed the old release script.