Skip to content

Guard npm latest, detect skipped releases, check the CHANGELOG heading (#33) - #56

Merged
planadecu merged 2 commits into
release/1.0from
feat/1.0-publish-guards
Sep 29, 2026
Merged

planadecu merged 2 commits into
release/1.0from
feat/1.0-publish-guards

Conversation

@planadecu

Copy link
Copy Markdown
Collaborator

Closes #33. The issue closes when release/1.0 reaches main, since GitHub only auto-closes on merges into the default branch.

Stacked on #55. This branch was cut from feat/1.0-toolchain-node22 because #55 rewrites the same workflow files (SHA-pinned actions, publish.yml comments, cache removal). Until #55 merges, this diff also shows its two commits (ea068c4, 1b4b16f). Only 5dd0cb2 belongs to this PR. I'll rebase onto release/1.0 once #55 is in.

Why

On 2026-09-29 the 0.29.5 and 0.30.0 publish runs were cancelled. The npm-publish concurrency group never cancels a publish that is already running. But GitHub keeps only one pending run per group, so each later merge's publish job replaced the older one while it waited for approval. This PR implements the "Decision (2026-09-29)" comment: options 3 (guard latest), 2 (detect) and 1 (document), plus the CHANGELOG heading check. The concurrency behaviour itself stays as it is. There is no version bump, per the release-branch rules.

What changed

Guard latest (scripts/release-plan.mjs, called by scripts/release-state.sh)

  • release-state.sh now also runs npm view glassnode-api versions dist-tags --json, with the same failure policy as before: an E404 means the package was never published, and any other failure fails the job instead of publishing. It outputs dist_tag and latest.
  • publish.yml always passes the tag explicitly: npm publish --tag "$DIST_TAG".
    • latest: for a higher or equal version, or when there is no latest yet.
    • backport-<major>.<minor>: for a version lower than the current latest, e.g. re-running the cancelled 0.29.5 after 0.30.1 → backport-0.29. The name identifies the release line. It is also not a valid semver range, which npm requires of tag names: v0.29 would parse as >=0.29.0 <0.30.0-0. I checked this with semver.validRange.
    • next: for a prerelease that is not lower. Otherwise npm would move latest onto a prerelease.
  • The publish job's re-check runs right before npm publish, so the tag reflects latest as it is at that moment, even after a long wait for approval.
  • The GitHub Release gets --latest=false when the dist-tag is not latest. It gets --prerelease for a prerelease version.
  • Semver comparison: full SemVer 2.0.0 precedence (prerelease identifiers, numeric vs alphanumeric, build metadata ignored), using BigInt. It adds no dependency.
  • Unchanged: the E404 → publish path, the gitHead → tag-only re-run path, skip, tagging and the GitHub Release.

Detect skipped versions. The job summary shows a warning and the log gets a ::warning:: annotation. Together they list every ## x.y.z CHANGELOG version between the last published version and the current one that is missing on npm. "Last published" is the highest npm version below the current one, so a re-run after the current version is out still reports the gap.

CHANGELOG heading check in ci.yml (scripts/check-changelog-heading.mjs, the first step of the test job, before install)

  • The target is github.base_ref, or github.ref_name (main) when publish.yml calls ci.yml.
  • Into main: the top ## heading must be exactly ## <package.json version>.
  • Into release/**: ## <x.y.z> (unreleased) is also accepted. It is not compared with package.json, so release/1.0 (## 1.0.0 (unreleased), package.json 0.30.1) passes.
  • The job name is unchanged, so no ruleset change is needed.

Docs

  • The publish.yml header, plus a new CONTRIBUTING.md "Releases (for maintainers)" section, explain:
    • the concurrency behaviour;
    • "merge release PRs one at a time";
    • recovery: "Re-run all jobs" on the skipped version's run; a lower version gets backport-x.y.
  • CLAUDE.md CI/Publishing/Release branches are updated.
  • Caveat, documented: a re-run uses its own commit's workflow, so re-running a run from before this change (e.g. 0.29.5 or 0.30.0 today) does not have the guard. After such a re-run, check npm dist-tag ls glassnode-api and restore latest by hand if needed.
  • CHANGELOG entry under ## 1.0.0 (unreleased) → ### Tooling. The published package does not change.

How it was verified

  • test/release-scripts.spec.ts (38 tests, part of the Vitest suite, so CI runs it):
    • compareSemver: the semver.org precedence list, checked pairwise, plus build metadata and invalid input.
    • distTagFor: older, older patch, older major, newer, equal, missing latest, prerelease above / below / with no latest.
    • skippedVersions (the 2026-09-29 scenario) and parseNpmView (E404, other errors, unparseable output, a single version returned as a string, no latest tag).
    • release-state.sh end to end, with a fake npm on PATH: new above latest → latest; lower → backport-0.30 plus the skipped warning; tag-only; skip; package not on npm → latest; prerelease → next; npm ETIMEDOUT on either query → non-zero exit with no outputs.
    • The heading check: passes for main exact, release exact and release unreleased. Fails for an unreleased heading into main, a mismatched version (main and release), an unreleased non-semver heading, another suffix, trailing whitespace and a missing heading.
  • A mutation check: making distTagFor always return latest fails 5 tests.
  • A differential check of compareSemver against the semver package: 529 version pairs, 0 mismatches. This was a one-off check; semver is not added as a dependency.
  • actionlint and shellcheck scripts/release-state.sh are clean.
  • pnpm run lint, tsc -p tsconfig.test.json, prettier --check . and pnpm run test:coverage pass (689 passed, 1 skipped). The pre-commit hook (tests, lint-staged, build) also passed.
  • A read-only run of release-state.sh against the real registry works: it reports action=publish, dist_tag=latest and the skipped-version warning.

Impact

  • There is no change to the published package or its API, and no version bump.
  • Releases: latest can no longer move backwards through this workflow, and skipped versions show up in the release summary.
  • PRs into main now fail on a CHANGELOG heading mismatch. That includes the final release/1.0 → main PR until it bumps package.json to 1.0.0 and renames the heading to ## 1.0.0.

planadecu and others added 2 commits September 29, 2026 23:21
On 2026-09-29 the 0.29.5 and 0.30.0 publish runs were cancelled: GitHub keeps
one pending run per concurrency group, so a later merge's publish replaced them
while they waited for approval.

- publish.yml passes an explicit dist-tag to npm publish (scripts/release-plan.mjs,
  SemVer 2.0.0 comparison incl. prereleases, no dependency): latest, or
  backport-<major>.<minor> for a version lower than the current latest, or next
  for a prerelease, so latest never moves backwards. Such a GitHub Release is
  not marked Latest.
- The release job summary warns about CHANGELOG versions between the last
  published version and this one that are missing on npm.
- ci.yml checks the top CHANGELOG heading (scripts/check-changelog-heading.mjs):
  exactly "## <package.json version>" into main; "## <x.y.z> (unreleased)" is
  also accepted into release/**.
- CONTRIBUTING.md, CLAUDE.md and the publish.yml header document the
  concurrency behaviour, the recovery and "merge release PRs one at a time".
- test/release-scripts.spec.ts covers the scripts, release-state.sh end to end
  with a fake npm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…op publishConfig.tag (#33)

- release-state.sh fails, writing no outputs, unless the dist-tag from
  release-plan.mjs is latest, next or backport-<n>.<n>.
- The fake-npm end-to-end tests record and assert the exact npm arguments, so
  dropping `versions dist-tags` or `--prefer-online` fails a test.
- package.json publishConfig no longer sets tag: "latest". The guard relied on
  --tag overriding it, and a manual or older-npm publish would still force
  latest. npm publish --dry-run still defaults to latest and honours
  --tag backport-0.29.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@planadecu
planadecu force-pushed the feat/1.0-publish-guards branch from 12d33a2 to ed2552a Compare September 29, 2026 21:21
@planadecu
planadecu merged commit 5bcbabd into release/1.0 Sep 29, 2026
2 checks passed
@planadecu planadecu mentioned this pull request Sep 29, 2026
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.

1 participant