Skip to content

fix(release): derive the npm dist-tag from spec/VERSION - #43

Merged
imran-siddique merged 1 commit into
mainfrom
fix/npm-dist-tag
Sep 2, 2026
Merged

fix(release): derive the npm dist-tag from spec/VERSION#43
imran-siddique merged 1 commit into
mainfrom
fix/npm-dist-tag

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

The problem

release.yml published npm with no dist-tag:

run: npm publish npm-dist/*.tgz --access public

npm refuses that outright for a prerelease. Confirmed by hand during today's bootstrap publish:

npm error You must specify a tag using --tag when publishing a prerelease version.

So publish-npm would have failed on any 0.x prerelease. On a stable version it would have succeeded and moved latest without anyone deciding to.

The change

The phase is already declared exactly once, in spec/VERSION. Rather than hardcode a tag in the workflow or in publishConfig, where it needs editing at every phase change, this derives it:

spec/VERSION dist-tag
1.0.0 latest
0.1.0-alpha.1 alpha
0.2.0-beta.3 beta
1.0.0-rc.1 rc
0.1.0-dev dev
  • tools/npm_dist_tag.py prints the tag.
  • check_versions.py now exposes CONTRACT_PATTERN, so both tools read one grammar. A second copy of that regex would be its own drift risk, which is exactly the class of bug fix(contract): align wire spec_version with spec/VERSION before first publish #41 fixed.
  • The build job publishes it as an output, so the distribution is still built once and publish-npm needs no checkout of its own.
  • RELEASING.md documents the derived tag and the by-hand equivalent, npm publish --tag "$(python tools/npm_dist_tag.py)".

One npm behaviour worth recording

npm sets latest on a package's first published version regardless of --tag. Today's bootstrap publish of 0.1.0-alpha.1 went out as --tag alpha and the registry still shows:

dist-tags: { alpha: '0.1.0-alpha.1', latest: '0.1.0-alpha.1' }

That is expected on a first publish, npm protects latest from removal, and it corrects itself when the first stable version ships. Noted in RELEASING.md so it is not mistaken for a bug later.

Verification

Gate Result
check_versions / check_schemas / check_typescript_schemas / check_otel_compatibility pass
check_release_tag v0.1.0-alpha.1 pass
Repository gate tests 16 pass, including 3 new
release.yml parses as valid YAML
python tools/npm_dist_tag.py on current spec/VERSION alpha

The new tests assert the mapping across all five phases, that a bad version raises, and the invariant that matters: a prerelease never resolves to latest.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

The release workflow ran `npm publish npm-dist/*.tgz --access public` with no
`--tag`. npm refuses that outright for a prerelease, so the publish-npm job
would have failed on any 0.x-alpha release, and on a stable one it would have
moved `latest` silently. Confirmed by hand today:

    npm error You must specify a tag using --tag when publishing a prerelease version.

Rather than hardcode a tag in the workflow or in publishConfig, where it would
need editing at every phase change, this derives it from the phase already
declared once in spec/VERSION.

- tools/npm_dist_tag.py prints the tag: latest for a stable version, otherwise
  the prerelease phase (alpha, beta, rc, dev).
- check_versions.py now exposes CONTRACT_PATTERN so both tools read one grammar.
  A second copy of that regex would be its own drift risk, which is the class of
  bug #41 was.
- The build job publishes it as an output, so the artifact is still built once
  and the publish job needs no checkout.
- RELEASING.md documents the derived tag and the by-hand equivalent.

Also records a real npm behaviour worth knowing: npm sets `latest` on a
package's first published version whatever --tag says. That happened on today's
bootstrap publish of 0.1.0-alpha.1 and corrects itself at the first stable
release.

Gates: all four checks, release-tag gate, 16 repository-gate tests including
three new ones asserting a prerelease never resolves to latest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn
@imran-siddique
imran-siddique merged commit 277fe45 into main Sep 2, 2026
8 checks passed
@imran-siddique
imran-siddique deleted the fix/npm-dist-tag branch September 2, 2026 23:25
imran-siddique added a commit that referenced this pull request Sep 2, 2026
Release preparation for `0.1.0-alpha.2`, following step 1 and 2 of
`RELEASING.md`.

## What is in this release

All three already merged to `main`:

| PR | |
|---|---|
| #41 | wire `spec_version` now matches `spec/VERSION` |
| #42 | schema `$id` moved off `agentrust.io`, a domain we do not own |
| #43 | npm dist-tag derived from `spec/VERSION` |

## Why a new version instead of releasing v0.1.0-alpha.1

npm already holds `0.1.0-alpha.1` from the bootstrap publish that
`RELEASING.md` explicitly sanctions:

> If npm does not expose publisher settings until the first version
exists, bootstrap only that first package ownership using npm's
interactive 2FA flow

`release.yml` publishes both registries from one release event, and
`release-assets` has `needs: [publish-pypi, publish-npm]`. Cutting
`v0.1.0-alpha.1` would publish PyPI, fail `publish-npm` on the duplicate
version, and skip attestation entirely. A fresh version lets one build
feed both registries with provenance intact, which is what the pipeline
exists to do.

PyPI has never been published, so `0.1.0-alpha.2` will be its first
version.

## On the moved digests

`spec/VERSION` is the contract version and the source both package
versions derive from, so bumping it changes the wire `spec_version`,
which sits inside the RFC 8785 bytes that get hashed. Both goldens were
regenerated from the code rather than hand-edited, and Python and
TypeScript independently agree on the new tool-transcript hash.

## A dead test case, found while bumping

`tests/test_repository_gates.py` had `"0.1.0-alpha.1"` as a dict key
**twice**:

```python
"0.1.0-alpha.1": ("0.1.0.dev0", "0.1.0-alpha.1.0"),
"0.1.0-alpha.1": ("0.1.0a1", "0.1.0-alpha.1"),
```

Python keeps the last, so the first entry was silently discarded and the
`dev` phase has never been tested. Its npm spelling was wrong as well:
`0.1.0-alpha.1.0` is not something `ecosystem_versions` can produce for
any input. Restored as a real `0.1.0-dev` case, with both alpha
spellings now asserted.

## Verification

| Gate | Result |
|---|---|
| `check_versions` | `contract=0.1.0-alpha.2 python=0.1.0a2
npm=0.1.0-alpha.2` |
| `check_release_tag v0.1.0-alpha.2` | pass |
| `npm_dist_tag` | `alpha` |
| `check_schemas` / `check_typescript_schemas` /
`check_otel_compatibility` | pass |
| Conformance fixtures | 13/13 |
| Python tests | 111 pass |
| TypeScript tests | 41 pass |

## After this merges

1. Configure the npm trusted publisher for `@agentrust-io/telemetry`,
now possible because the package exists.
2. Create the GitHub release tagged `v0.1.0-alpha.2` targeting `main`.
3. Approve the `pypi` and `npm` deployment jobs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

Co-authored-by: Claude Opus 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