From e546c0cdbc7ea909609e605e7f5063bfe0c58f81 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 27 Aug 2026 10:52:00 +0100 Subject: [PATCH 1/4] fix(release-kit): compute next version from the registry and commit bumps explicitly npm version only performs git commit/tag when .git sits next to package.json; kits live in kits// 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 time includes unpublished ones), bump with --no-git-tag-version, then commit and tag explicitly from the repo root using per-kit tag names (@). The commit fails loudly if the bump staged nothing, and the push happens only after a successful publish. Fixes #2978 --- .github/workflows/release-kit.yaml | 130 ++++++++++++++++++----------- 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/.github/workflows/release-kit.yaml b/.github/workflows/release-kit.yaml index 2ea8f0388..b06da8625 100644 --- a/.github/workflows/release-kit.yaml +++ b/.github/workflows/release-kit.yaml @@ -114,31 +114,63 @@ jobs: echo "$NOTES" >> $GITHUB_OUTPUT echo "$EOF" >> $GITHUB_OUTPUT - - name: Determine Version Bump Type + - name: Determine Next Version id: config working-directory: ${{ inputs.target_kit }} env: BUMP_LEVEL: ${{ inputs.bump_level }} IS_PRERELEASE: ${{ inputs.is_prerelease }} + # The registry is the source of truth for versions: npm reserves every + # version ever published (even after unpublish), and package.json on the + # branch can lag behind what was actually released. + NPM_CONFIG_REGISTRY: https://registry.npmjs.org run: | - CURRENT_VER=$(node -p "require('./package.json').version") - BUMP="$BUMP_LEVEL" + PKG_NAME=$(node -p "require('./package.json').name") + LOCAL_VER=$(node -p "require('./package.json').version") + + # `npm view time` keys include unpublished versions, unlike `versions`. + VIEW_OUT=$(npm view "$PKG_NAME" time --json || true) + TAKEN=$(echo "$VIEW_OUT" | node -e ' + const raw = require("fs").readFileSync(0, "utf8").trim(); + const data = raw ? JSON.parse(raw) : {}; + if (data.error) { + if (data.error.code === "E404") process.exit(0); + console.error(data.error.summary || data.error.code); + process.exit(1); + } + console.log(Object.keys(data).filter(v => v !== "created" && v !== "modified").join(" ")); + ') + + HIGHEST=$(npx --yes semver@7 $LOCAL_VER $TAKEN | tail -n 1) if [ "$IS_PRERELEASE" = "true" ]; then - if [[ "$CURRENT_VER" =~ -rc\.[0-9]+$ ]]; then + if [[ "$HIGHEST" == *-rc.* ]]; then BUMP_TYPE="prerelease" else - BUMP_TYPE="pre${BUMP}" + BUMP_TYPE="pre${BUMP_LEVEL}" fi TARGET_TAG="next" else - BUMP_TYPE="$BUMP" + BUMP_TYPE="$BUMP_LEVEL" TARGET_TAG="latest (and next)" fi - echo "current_ver=$CURRENT_VER" >> $GITHUB_OUTPUT + NEW_VER=$(npx --yes semver@7 -i "$BUMP_TYPE" --preid rc "$HIGHEST") + + for v in $TAKEN; do + if [ "$v" = "$NEW_VER" ]; then + echo "::error::Computed version $NEW_VER already exists on the registry." + exit 1 + fi + done + + echo "current_ver=$LOCAL_VER" >> $GITHUB_OUTPUT + echo "highest_ver=$HIGHEST" >> $GITHUB_OUTPUT echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT echo "target_tag=$TARGET_TAG" >> $GITHUB_OUTPUT + echo "version=$NEW_VER" >> $GITHUB_OUTPUT + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT + echo "tag_name=${PKG_NAME}@${NEW_VER}" >> $GITHUB_OUTPUT - name: Install Dependencies & Build working-directory: ${{ inputs.target_kit }} @@ -146,52 +178,49 @@ jobs: npm ci --registry=https://registry.npmjs.org npm run build - - name: Bump Version (Clear CHANGELOG on Stable Release Only) - id: versioning + - name: Apply Version Bump (Clear CHANGELOG on Stable Release Only) working-directory: ${{ inputs.target_kit }} env: - BUMP_TYPE: ${{ steps.config.outputs.bump_type }} + NEW_VER: ${{ steps.config.outputs.version }} IS_PRERELEASE: ${{ inputs.is_prerelease }} DRY_RUN: ${{ inputs.dry_run }} run: | - if [ "$DRY_RUN" = "true" ]; then - npm version $BUMP_TYPE --preid=rc --no-git-tag-version - else - # Only truncate CHANGELOG.md if this is a STABLE release (not a prerelease) - if [ "$IS_PRERELEASE" != "true" ]; then - echo "Stable release detected: clearing CHANGELOG.md..." - > CHANGELOG.md - git add CHANGELOG.md - else - echo "Prerelease detected: preserving CHANGELOG.md content." - fi + # npm version only performs git operations when .git sits next to + # package.json; kits live in kits// with .git at the repo root, + # so git commit/tag are done explicitly in the next step. + npm version "$NEW_VER" --no-git-tag-version - # Create version bump commit and git tag - if [[ "$BUMP_TYPE" == pre* ]] || [[ "$BUMP_TYPE" == "prerelease" ]]; then - npm version $BUMP_TYPE --preid=rc -m "chore(release): %s [skip ci]" - else - npm version $BUMP_TYPE -m "chore(release): %s [skip ci]" - fi + if [ "$DRY_RUN" != "true" ] && [ "$IS_PRERELEASE" != "true" ]; then + echo "Stable release detected: clearing CHANGELOG.md..." + > CHANGELOG.md fi - NEW_VER=$(node -p "require('./package.json').version") - PKG_NAME=$(node -p "require('./package.json').name") - - echo "version=$NEW_VER" >> $GITHUB_OUTPUT - echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT - echo "tag_name=${PKG_NAME}@${NEW_VER}" >> $GITHUB_OUTPUT + - name: Commit & Tag Release + if: ${{ !inputs.dry_run }} + env: + TARGET_KIT: ${{ inputs.target_kit }} + TAG_NAME: ${{ steps.config.outputs.tag_name }} + run: | + git add "$TARGET_KIT/package.json" + [ -f "$TARGET_KIT/package-lock.json" ] && git add "$TARGET_KIT/package-lock.json" + [ -f "$TARGET_KIT/CHANGELOG.md" ] && git add "$TARGET_KIT/CHANGELOG.md" + # git commit exits non-zero if the bump staged no changes, so a broken + # bump fails the release instead of publishing without a release commit. + git commit -m "chore(release): $TAG_NAME [skip ci]" + git tag -a "$TAG_NAME" -m "$TAG_NAME" - name: Dry Run Summary if: ${{ inputs.dry_run }} env: - PKG_NAME: ${{ steps.versioning.outputs.pkg_name }} + PKG_NAME: ${{ steps.config.outputs.pkg_name }} TARGET_KIT: ${{ inputs.target_kit }} CURRENT_VER: ${{ steps.config.outputs.current_ver }} - VERSION: ${{ steps.versioning.outputs.version }} + HIGHEST_VER: ${{ steps.config.outputs.highest_ver }} + VERSION: ${{ steps.config.outputs.version }} BUMP_TYPE: ${{ steps.config.outputs.bump_type }} TARGET_TAG: ${{ steps.config.outputs.target_tag }} IS_PRERELEASE: ${{ inputs.is_prerelease }} - TAG_NAME: ${{ steps.versioning.outputs.tag_name }} + TAG_NAME: ${{ steps.config.outputs.tag_name }} NOTES: ${{ steps.changelog.outputs.notes }} run: | echo "==========================================================" @@ -199,7 +228,8 @@ jobs: echo "==========================================================" echo " Package Name: $PKG_NAME" echo " Target Directory: $TARGET_KIT" - echo " Current Version: $CURRENT_VER" + echo " package.json Ver: $CURRENT_VER" + echo " Highest Known: $HIGHEST_VER" echo " Target Version: $VERSION" echo " Version Bump: $BUMP_TYPE" echo " NPM Dist-Tag: $TARGET_TAG" @@ -211,12 +241,6 @@ jobs: echo "$NOTES" echo "==========================================================" - - name: Push Version Commit & Tags to Branch - if: ${{ !inputs.dry_run }} - env: - REF_NAME: ${{ github.ref_name }} - run: git push origin "$REF_NAME" --follow-tags - - name: Publish to NPM # zizmor: ignore[use-trusted-publishing] working-directory: ${{ inputs.target_kit }} @@ -224,8 +248,8 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} IS_PRERELEASE: ${{ inputs.is_prerelease }} DRY_RUN: ${{ inputs.dry_run }} - PKG_NAME: ${{ steps.versioning.outputs.pkg_name }} - VERSION: ${{ steps.versioning.outputs.version }} + PKG_NAME: ${{ steps.config.outputs.pkg_name }} + VERSION: ${{ steps.config.outputs.version }} run: | DRY_RUN_FLAG="" if [ "$DRY_RUN" = "true" ]; then @@ -242,13 +266,23 @@ jobs: fi fi + # Pushed only after a successful publish: a failed publish leaves the + # branch untouched, and the next run recomputes the version from the + # registry either way. + - name: Push Version Commit & Tag to Branch + if: ${{ !inputs.dry_run }} + env: + REF_NAME: ${{ github.ref_name }} + TAG_NAME: ${{ steps.config.outputs.tag_name }} + run: git push origin "HEAD:refs/heads/$REF_NAME" "refs/tags/$TAG_NAME" + - name: Create GitHub Release via GitHub CLI if: ${{ !inputs.dry_run }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ steps.versioning.outputs.tag_name }} - PKG_NAME: ${{ steps.versioning.outputs.pkg_name }} - VERSION: ${{ steps.versioning.outputs.version }} + TAG_NAME: ${{ steps.config.outputs.tag_name }} + PKG_NAME: ${{ steps.config.outputs.pkg_name }} + VERSION: ${{ steps.config.outputs.version }} IS_PRERELEASE: ${{ inputs.is_prerelease }} NOTES: ${{ steps.changelog.outputs.notes }} REF_NAME: ${{ github.ref_name }} From dff59cfb03ce0d609f20f47d342287ee8683791b Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 27 Aug 2026 11:43:37 +0100 Subject: [PATCH 2/4] fix(release-kit): harden versioning and release git operations 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. --- .github/workflows/release-kit.yaml | 36 ++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-kit.yaml b/.github/workflows/release-kit.yaml index b06da8625..f9c962668 100644 --- a/.github/workflows/release-kit.yaml +++ b/.github/workflows/release-kit.yaml @@ -41,6 +41,12 @@ on: default: true type: boolean +# All releases push to the same branch; a concurrent run would fail its push +# after already publishing to npm, leaving a version with no commit or tag. +concurrency: + group: release-kit + cancel-in-progress: false + jobs: # ========================================================= # 1. TEST JOB (Low Permissions: Read-Only) @@ -125,6 +131,8 @@ jobs: # branch can lag behind what was actually released. NPM_CONFIG_REGISTRY: https://registry.npmjs.org run: | + set -o pipefail + PKG_NAME=$(node -p "require('./package.json').name") LOCAL_VER=$(node -p "require('./package.json').version") @@ -141,22 +149,29 @@ jobs: console.log(Object.keys(data).filter(v => v !== "created" && v !== "modified").join(" ")); ') - HIGHEST=$(npx --yes semver@7 $LOCAL_VER $TAKEN | tail -n 1) + HIGHEST=$(npx --yes semver@7.8.5 $LOCAL_VER $TAKEN | tail -n 1) + # 0.0.0 seed keeps grep from emptying the pipe when no stable exists yet. + HIGHEST_STABLE=$(npx --yes semver@7.8.5 0.0.0 $LOCAL_VER $TAKEN | grep -v -- - | tail -n 1) + # The bump level is applied to the highest stable version; an existing + # rc line continues unless the requested level opens a higher line. if [ "$IS_PRERELEASE" = "true" ]; then + NEW_VER=$(npx --yes semver@7.8.5 -i "pre${BUMP_LEVEL}" --preid rc "$HIGHEST_STABLE") + BUMP_TYPE="pre${BUMP_LEVEL}" if [[ "$HIGHEST" == *-rc.* ]]; then - BUMP_TYPE="prerelease" - else - BUMP_TYPE="pre${BUMP_LEVEL}" + CONTINUED=$(npx --yes semver@7.8.5 -i prerelease --preid rc "$HIGHEST") + if [ "$(npx --yes semver@7.8.5 "$NEW_VER" "$CONTINUED" | tail -n 1)" = "$CONTINUED" ]; then + NEW_VER="$CONTINUED" + BUMP_TYPE="prerelease" + fi fi TARGET_TAG="next" else + NEW_VER=$(npx --yes semver@7.8.5 -i "$BUMP_LEVEL" "$HIGHEST_STABLE") BUMP_TYPE="$BUMP_LEVEL" TARGET_TAG="latest (and next)" fi - NEW_VER=$(npx --yes semver@7 -i "$BUMP_TYPE" --preid rc "$HIGHEST") - for v in $TAKEN; do if [ "$v" = "$NEW_VER" ]; then echo "::error::Computed version $NEW_VER already exists on the registry." @@ -166,6 +181,7 @@ jobs: echo "current_ver=$LOCAL_VER" >> $GITHUB_OUTPUT echo "highest_ver=$HIGHEST" >> $GITHUB_OUTPUT + echo "highest_stable=$HIGHEST_STABLE" >> $GITHUB_OUTPUT echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT echo "target_tag=$TARGET_TAG" >> $GITHUB_OUTPUT echo "version=$NEW_VER" >> $GITHUB_OUTPUT @@ -202,6 +218,8 @@ jobs: TAG_NAME: ${{ steps.config.outputs.tag_name }} run: | git add "$TARGET_KIT/package.json" + # npm version also bumps the lockfile; kits currently ship npm-shrinkwrap.json. + [ -f "$TARGET_KIT/npm-shrinkwrap.json" ] && git add "$TARGET_KIT/npm-shrinkwrap.json" [ -f "$TARGET_KIT/package-lock.json" ] && git add "$TARGET_KIT/package-lock.json" [ -f "$TARGET_KIT/CHANGELOG.md" ] && git add "$TARGET_KIT/CHANGELOG.md" # git commit exits non-zero if the bump staged no changes, so a broken @@ -216,6 +234,7 @@ jobs: TARGET_KIT: ${{ inputs.target_kit }} CURRENT_VER: ${{ steps.config.outputs.current_ver }} HIGHEST_VER: ${{ steps.config.outputs.highest_ver }} + HIGHEST_STABLE: ${{ steps.config.outputs.highest_stable }} VERSION: ${{ steps.config.outputs.version }} BUMP_TYPE: ${{ steps.config.outputs.bump_type }} TARGET_TAG: ${{ steps.config.outputs.target_tag }} @@ -230,6 +249,7 @@ jobs: echo " Target Directory: $TARGET_KIT" echo " package.json Ver: $CURRENT_VER" echo " Highest Known: $HIGHEST_VER" + echo " Highest Stable: $HIGHEST_STABLE" echo " Target Version: $VERSION" echo " Version Bump: $BUMP_TYPE" echo " NPM Dist-Tag: $TARGET_TAG" @@ -274,7 +294,9 @@ jobs: env: REF_NAME: ${{ github.ref_name }} TAG_NAME: ${{ steps.config.outputs.tag_name }} - run: git push origin "HEAD:refs/heads/$REF_NAME" "refs/tags/$TAG_NAME" + run: + git push --atomic origin "HEAD:refs/heads/$REF_NAME" + "refs/tags/$TAG_NAME" - name: Create GitHub Release via GitHub CLI if: ${{ !inputs.dry_run }} From 86410afa996f9c614d82b26565e91ad864c8dc0e Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 27 Aug 2026 12:29:09 +0100 Subject: [PATCH 3/4] fix(release-kit): close failure-recovery and dispatch edge cases 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. --- .github/workflows/release-kit.yaml | 39 +++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-kit.yaml b/.github/workflows/release-kit.yaml index f9c962668..ac25adcf4 100644 --- a/.github/workflows/release-kit.yaml +++ b/.github/workflows/release-kit.yaml @@ -43,6 +43,9 @@ on: # All releases push to the same branch; a concurrent run would fail its push # after already publishing to npm, leaving a version with no commit or tag. +# GitHub keeps at most ONE pending run per group: with a run in progress and +# one queued, a third dispatch cancels the queued one. Dispatch kits one at a +# time and check for a "Canceled" run before assuming a release happened. concurrency: group: release-kit cancel-in-progress: false @@ -133,11 +136,25 @@ jobs: run: | set -o pipefail + # The push at the end of the job would create a stray branch named + # after the ref if this were dispatched from a tag. + if [ "$GITHUB_REF_TYPE" != "branch" ]; then + echo "::error::Release Kit must be dispatched from a branch, got $GITHUB_REF_TYPE '$GITHUB_REF_NAME'." + exit 1 + fi + PKG_NAME=$(node -p "require('./package.json').name") LOCAL_VER=$(node -p "require('./package.json').version") - # `npm view time` keys include unpublished versions, unlike `versions`. - VIEW_OUT=$(npm view "$PKG_NAME" time --json || true) + # `npm view time` keys include per-version-unpublished versions + # (unlike `versions`); a fully unpublished package keeps its reserved + # versions under time.unpublished.versions instead. + VIEW_STATUS=0 + VIEW_OUT=$(npm view "$PKG_NAME" time --json) || VIEW_STATUS=$? + if [ "$VIEW_STATUS" -ne 0 ] && [ -z "$VIEW_OUT" ]; then + echo "::error::npm view exited $VIEW_STATUS with no output; cannot trust the version computation." + exit 1 + fi TAKEN=$(echo "$VIEW_OUT" | node -e ' const raw = require("fs").readFileSync(0, "utf8").trim(); const data = raw ? JSON.parse(raw) : {}; @@ -146,7 +163,11 @@ jobs: console.error(data.error.summary || data.error.code); process.exit(1); } - console.log(Object.keys(data).filter(v => v !== "created" && v !== "modified").join(" ")); + const meta = ["created", "modified", "unpublished"]; + const keys = Object.keys(data).filter(v => !meta.includes(v)); + const unpub = (data.unpublished && data.unpublished.versions) || []; + const unpubList = Array.isArray(unpub) ? unpub : Object.keys(unpub); + console.log(keys.concat(unpubList).join(" ")); ') HIGHEST=$(npx --yes semver@7.8.5 $LOCAL_VER $TAKEN | tail -n 1) @@ -294,9 +315,8 @@ jobs: env: REF_NAME: ${{ github.ref_name }} TAG_NAME: ${{ steps.config.outputs.tag_name }} - run: - git push --atomic origin "HEAD:refs/heads/$REF_NAME" - "refs/tags/$TAG_NAME" + run: | + git push --atomic origin "HEAD:refs/heads/$REF_NAME" "refs/tags/$TAG_NAME" - name: Create GitHub Release via GitHub CLI if: ${{ !inputs.dry_run }} @@ -314,6 +334,13 @@ jobs: PRERELEASE_FLAG="--prerelease" fi + # A re-run after a transient gh failure must not mint a new version + # just to get a release page, so creation is skipped if it exists. + if gh release view "$TAG_NAME" > /dev/null 2>&1; then + echo "Release $TAG_NAME already exists; skipping creation." + exit 0 + fi + gh release create "$TAG_NAME" \ --title "$PKG_NAME v$VERSION" \ --notes "$NOTES" \ From 28813dcb264a552f46c0299a5978fcb6cbacbd4a Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 27 Aug 2026 13:19:18 +0100 Subject: [PATCH 4/4] fix(release-kit): fetch the packument directly instead of npm view 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. --- .github/workflows/release-kit.yaml | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release-kit.yaml b/.github/workflows/release-kit.yaml index ac25adcf4..e9f6b13ad 100644 --- a/.github/workflows/release-kit.yaml +++ b/.github/workflows/release-kit.yaml @@ -146,29 +146,29 @@ jobs: PKG_NAME=$(node -p "require('./package.json').name") LOCAL_VER=$(node -p "require('./package.json').version") - # `npm view time` keys include per-version-unpublished versions - # (unlike `versions`); a fully unpublished package keeps its reserved - # versions under time.unpublished.versions instead. - VIEW_STATUS=0 - VIEW_OUT=$(npm view "$PKG_NAME" time --json) || VIEW_STATUS=$? - if [ "$VIEW_STATUS" -ne 0 ] && [ -z "$VIEW_OUT" ]; then - echo "::error::npm view exited $VIEW_STATUS with no output; cannot trust the version computation." + # The packument is fetched directly rather than via `npm view`: npm + # hides a fully unpublished package behind E404 (dropping its reserved + # versions in time.unpublished) and its --json output shape changes + # across npm majors. The registry itself 404s only for never-published + # names. time keys include per-version-unpublished versions. + PACKUMENT="$RUNNER_TEMP/packument.json" + HTTP_STATUS=$(curl -sS --retry 3 -o "$PACKUMENT" -w '%{http_code}' "$NPM_CONFIG_REGISTRY/$PKG_NAME") + if [ "$HTTP_STATUS" = "404" ]; then + TAKEN="" + elif [ "$HTTP_STATUS" != "200" ]; then + echo "::error::Registry returned HTTP $HTTP_STATUS for $PKG_NAME; cannot trust the version computation." exit 1 + else + TAKEN=$(node -e ' + const data = JSON.parse(require("fs").readFileSync(0, "utf8")); + const time = data.time || {}; + const meta = ["created", "modified", "unpublished"]; + const keys = Object.keys(time).filter(v => !meta.includes(v)); + const unpub = (time.unpublished && time.unpublished.versions) || []; + const unpubList = Array.isArray(unpub) ? unpub : Object.keys(unpub); + console.log(keys.concat(unpubList).join(" ")); + ' < "$PACKUMENT") fi - TAKEN=$(echo "$VIEW_OUT" | node -e ' - const raw = require("fs").readFileSync(0, "utf8").trim(); - const data = raw ? JSON.parse(raw) : {}; - if (data.error) { - if (data.error.code === "E404") process.exit(0); - console.error(data.error.summary || data.error.code); - process.exit(1); - } - const meta = ["created", "modified", "unpublished"]; - const keys = Object.keys(data).filter(v => !meta.includes(v)); - const unpub = (data.unpublished && data.unpublished.versions) || []; - const unpubList = Array.isArray(unpub) ? unpub : Object.keys(unpub); - console.log(keys.concat(unpubList).join(" ")); - ') HIGHEST=$(npx --yes semver@7.8.5 $LOCAL_VER $TAKEN | tail -n 1) # 0.0.0 seed keeps grep from emptying the pipe when no stable exists yet.