Skip to content

fix(release): npm needs an explicit dist-tag for a prerelease, chosen from what the registry holds - #93

Merged
krzysztof-smartdataengines merged 1 commit into
mainfrom
fix/npm-dist-tag-for-prereleases
Sep 27, 2026
Merged

krzysztof-smartdataengines merged 1 commit into
mainfrom
fix/npm-dist-tag-for-prereleases

Conversation

@krzysztof-smartdataengines

Copy link
Copy Markdown
Contributor

Summary

The release workflow could not have published a release candidate to npm. npm 11, which the
publishing job installs (npm@11.15.0), refuses a prerelease without an explicit tag:

// lib/commands/publish.js, npm 11.15.0
if (isPreRelease && isDefaultTag) {
  throw new Error('You must specify a tag using --tag when publishing a prerelease version.')
}

The job ran npm publish "$tarball" --access public, so the planned first run, 0.1.0-rc.1, would
have failed at the publish step, after the gate and the reviewer had both said yes. The runbook's
hand-made first publish (0.1.0-dev.0) would have failed the same way. Found by reading the CLI's
source before the first release, because the publishing half of this workflow cannot be rehearsed.

A fixed tag is wrong one way or the other:

  • latest for every publish would put each candidate over the last final;
  • next for every prerelease would leave an unpinned install on the version published by hand
    until the first final.

What changed

  • tools/npm_dist_tag.py chooses the tag from the versions the registry already holds, by
    Semantic Versioning precedence:

    • a final version is latest;
    • a prerelease is latest while no final exists, because an unpinned pip install gets the newest
      prerelease on PyPI too;
    • a prerelease is next once a final exists.

    It refuses, for a person to decide:

    • a version already published;
    • any version lower than one already published, because it would move a tag backwards;
    • a registry answer that lists nothing, or is not a list of versions.

    It reads npm view ... versions --json as npm prints it: a list, or a string when there is one
    version.

  • release.yml:

    • the build job looks the versions up (public metadata, no credential) and runs the script, so
      the publishing job, which must not check out the repository, receives the tag as an output;
    • a failed lookup stops the release instead of guessing an empty list;
    • the publishing job passes --tag explicitly;
    • its last step checks, in a bounded loop, that the registry shows the version under that tag.
  • docs/publishing.md: the hand-made first publish is
    npm publish --access public --tag latest, with the reason and the npm source quoted, and §5.2
    states the rule.

Tests

  • test_release.py:

    • the rule as a table, including rc.10 after rc.9;
    • the refusals;
    • the registry's two answer shapes;
    • the script's output and exit status;
    • no npm publish in the workflow or the runbook without --tag;
    • the publishing job taking the tag the build job computed.

    On main the two workflow tests fail for the stated reason: the publish line has no --tag, and
    the build job has no dist_tag output.

  • check_contexts.py still passes. The publishing job holds id-token: write behind its
    environment and checks nothing out, and every action is pinned to a SHA.

  • Mutations on the committed branch: 10 killed and a control survived. The killed mutations are:

    • candidates always latest, or always next;
    • tags allowed to move backwards;
    • republishing allowed;
    • prerelease identifiers compared as strings;
    • an empty registry accepted;
    • a single-version answer rejected;
    • the workflow publishing without a tag;
    • the workflow not checking the tag;
    • the runbook without a tag.
  • make check on the head 313b972, rebased on feat: Python 3.14 and Node 24 and 26, tested and claimed #92, with both live engines:

    • ruff and mypy clean, and Python 2099 passed and 10 skipped (the orderbook slice).
    • TypeScript: 998 passed, and one live test, frozen-verification.live.test.ts, exceeded vitest's
      default 5 s timeout. Nothing here touches TypeScript. The test ran while this machine was
      shared with another session's integration battery (load average about 4.6 on four threads).
      Eleven isolated reruns of that file followed. Ten passed; one failed another of its tests, and
      that output was not kept. The TypeScript half of make check run again on the same head passed
      999 of 999, at a load average of 5.5. CI runs on unshared runners.
  • The mutation run was repeated on the rebased head, with the same result.

🤖 Generated with Claude Code

…t the registry holds

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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