Skip to content

fix(release-kit): registry-driven versioning, explicit release commits, v7 action pins - #2990

Merged
cabljac merged 4 commits into
kitsfrom
fix/release-kit-versioning
Aug 27, 2026
Merged

fix(release-kit): registry-driven versioning, explicit release commits, v7 action pins#2990
cabljac merged 4 commits into
kitsfrom
fix/release-kit-versioning

Conversation

@cabljac

@cabljac cabljac commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #2978.

  • npm version silently skipped git commit/tag (.git not next to kit's package.json): bump now uses --no-git-tag-version, with explicit commit + annotated per-kit tag (<pkg>@<version>) from repo root. Commit fails loudly if nothing staged.
  • Re-runs recomputed the same version from package.json and 403'd: next version now derived from the registry (npm view <pkg> time, includes unpublished versions) unioned with package.json.
  • Bump level silently ignored when an rc existed: level now applied to the highest stable version; an rc line continues only if the requested level doesn't open a higher one.
  • npm-shrinkwrap.json bumped but not committed: now staged in the release commit.
  • Concurrent runs raced the branch push: workflow-level concurrency group; push is atomic and happens only after a successful publish.

Action pin bumps for #2977 split out into #2991; the two merge independently (non-overlapping hunks).

Verified: dry-run dispatch green, computes 0.0.2-rc.1 for rtdb (the version #2978 requires); version logic passed 10 synthetic registry scenarios locally. The non-dry-run commit/tag/push path has not been exercised live.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Note

Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported.

…umps explicitly

npm version only performs git commit/tag when .git sits next to
package.json; kits live in kits/<name>/ with .git at the repo root, so
the release commit and tag were silently skipped and the branch kept the
old version. Every run then recomputed the same version from
package.json and re-runs collided with npm's 403 on previously published
versions.

Determine the next version from the union of package.json and all
versions the registry has ever seen (npm view <pkg> time includes
unpublished ones), bump with --no-git-tag-version, then commit and tag
explicitly from the repo root using per-kit tag names (<pkg>@<version>).
The commit fails loudly if the bump staged nothing, and the push happens
only after a successful publish.

Fixes #2978
Findings from adversarial review of the previous commit:

- Stage npm-shrinkwrap.json in the release commit: npm version bumps it
  and all kits ship one, so the tag would not have reproduced the
  published tarball.
- Add a workflow-level concurrency group: all releases push the same
  branch, and a concurrent run would fail its push after npm publish
  already succeeded.
- Apply the bump level to the highest stable version instead of the
  overall highest. Previously any rc on the registry silently forced a
  prerelease bump (major+RC on a 1.3.0-rc line yielded 1.3.0-rc.N, not
  2.0.0-rc.0), and a stable minor on a 2.0.0-rc line finalized 2.0.0.
  An rc line now continues only when the requested level does not open
  a higher line.
- git push --atomic so the tag cannot land without the branch update.
- set -o pipefail in the version step; run: shells default to bash -e
  without pipefail, so a failed npx semver was masked by tail.
- Pin semver to 7.8.5.
@cabljac
cabljac force-pushed the fix/release-kit-versioning branch from c9353b1 to dff59cf Compare August 27, 2026 10:51
@cabljac
cabljac requested review from CorieW and IzaakGough August 27, 2026 11:07
@cabljac
cabljac marked this pull request as ready for review August 27, 2026 11:07
Second adversarial review round:

- Fail the version step when npm view exits non-zero with no output;
  previously || true mapped that to an empty taken-set and the run
  proceeded on package.json alone, 403ing late at publish.
- Include time.unpublished.versions in the taken set and drop the
  literal unpublished key: a fully unpublished package keeps its
  reserved versions there, not as top-level time keys.
- Skip gh release create when the release already exists, so a re-run
  after a transient gh failure does not mint a new npm version just to
  get a release page.
- Refuse dispatch from a non-branch ref: the final push would create a
  stray branch named after the tag.
- Document the depth-1 pending queue of the concurrency group: a third
  dispatch cancels the queued run, so dispatch kits one at a time.
- Make the push step a literal block so a future second line cannot
  fold into the same command.

@IzaakGough IzaakGough left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lgtm - Two things AI flagged (mostly just things to be aware of than fixes required afaik)

The time.unpublished.versions branch cannot be reached

npm view <pkg> <field> resolves the spec before reading the field, and a fully unpublished package has no versions to match, so it errors first and the parser exits at the E404 branch. Verified against left-pad-test, a real tombstone:

registry packument:  time.unpublished.versions = ["0.0.1"]
npm view … time:     {"error":{"code":"E404","summary":"Unpublished on 2017-01-13…"}}
current parser:      (prints nothing, exits 0)

E404 makes "never published" and "fully unpublished" indistinguishable, so the reserved version the code was added to capture is dropped. The scenario needs a 72 hour window or npm support, so the risk is negligible either way; the reason to touch it is that three lines currently read as an active safeguard. Deleting them resolves it as well as fixing them does.

If fixing, reading the packument directly makes the branch reachable and drops the dependency on CLI output shape:

curl -sf "https://registry.npmjs.org/$PKG_NAME"

Push after publish, for the record

Moving the push after publish is the better trade. The old ordering could leave a pushed commit and tag for a version that never reached npm, and since versions now come from the registry, npm running ahead of git is the recoverable direction.

The residual: if kits moves between the release job's checkout and its push, the push is rejected after publish succeeded, and --atomic takes the tag with the branch.

! [rejected]  HEAD -> main (fetch first)
! [rejected]  pkg@1.0.0 -> pkg@1.0.0 (atomic push failed)

The GitHub Release step is skipped too, so the version sits on npm with no commit, tag or release. The run goes red, and cleanup is a version bump, commit, tag and gh release create. The window is checkout to push, under a minute in the run logs, against roughly two pushes a day on kits. Optional insurance, if the manual cleanup is worth avoiding:

git fetch origin "$REF_NAME"
git rebase "origin/$REF_NAME"

That does mean the tag points at a tree including kits/ changes that landed after the build, so it no longer matches the published tarball. Pushing the tag first on its own avoids that, since its name cannot race.

Optional: pin npm

npm 12 returns npm view … time --json as an array rather than an object, which the parser would read as one key "0", falling back to package.json alone. Not live today, since node-version: "24" gives npm 11.19.0, though Node does move npm majors mid-line (v18.14.0, v18.19.0, v20.7.0). npm i -g npm@11 settles it, and the curl above makes it moot.

npm view resolves the package spec before reading a field, so a fully
unpublished package returns E404 and its reserved versions under
time.unpublished never reach the parser; the unpublished handling added
earlier was unreachable. npm view --json output shape also changes
across npm majors (npm 12 returns an array), which would silently empty
the taken-set. Fetching the packument from the registry avoids both:
the registry 404s only for never-published names, and the document
shape is the registry's, not the CLI's.
@cabljac

cabljac commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Nice catches, both reproduced exactly as you described: npm view left-pad-test time --json E404s while the raw packument still carries time.unpublished.versions, so the unpublished branch really was unreachable, and npm 12's --json array shape would have silently emptied the taken-set.

Fixed both in 28813dc by fetching the packument straight from the registry:

HTTP_STATUS=$(curl -sS --retry 3 -o "$PACKUMENT" -w '%{http_code}' "$NPM_CONFIG_REGISTRY/$PKG_NAME")

404 now means never-published only (tombstones return 200 with time.unpublished, verified against left-pad-test), and the parse depends on the registry document shape rather than npm CLI output, so the npm pin becomes unnecessary too.

On the push race: agreed it's the better trade and we're leaving it as is. The fetch+rebase would make the tag point at a tree that no longer matches the published tarball, which is quieter but worse than a loud red run with a short manual cleanup.

@cabljac
cabljac merged commit 26d9182 into kits Aug 27, 2026
11 checks passed
cabljac added a commit that referenced this pull request Aug 27, 2026
Syncs release-kit.yaml byte-for-byte with the version that landed on
kits in #2990. This copy never executes (Release Kit is always
dispatched from kits; next has no kits/ directories); it only lists the
workflow in the Actions UI, and it had drifted to show the old broken
versioning logic. No behavior change. Will need re-syncing if #2991
lands on kits.
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