1- # Publishes the CLI to npm, with no token anywhere .
1+ # Ships the CLI when a release is cut .
22#
3- # npm revoked every classic token on 9 December 2025 and they cannot be
4- # recreated. Granular tokens still exist but enforce 2FA and expire after 90
5- # days at most, which for an unattended pipeline means a secret that silently
6- # stops working four times a year.
3+ # Two destinations, one of which is not switched on yet.
74#
8- # So this uses trusted publishing: GitHub mints a short-lived OIDC token for
9- # this workflow, npm checks it against a trust relationship configured on the
10- # npm side, and nothing long-lived is stored. Provenance is generated
11- # automatically as a result — the package page can then show which commit and
12- # which workflow built it.
5+ # A GitHub release always gets the packed tarball attached. That is enough to
6+ # install and run the CLI without npm knowing it exists, which is why it is the
7+ # path that works today.
138#
14- # Deliberately not on every push to main. npm will not let a version be
15- # replaced or a name reused, so publishing is the one step here that cannot be
16- # taken back: it should happen because somebody decided to release.
17- #
18- # Cut a GitHub release whose tag is the version ("v0.2.0") and this runs.
9+ # npm needs a trust relationship configured on npmjs.com before it will accept
10+ # anything — see the repository issue about it. Until that exists there is no
11+ # way to publish: npm revoked every classic token on 9 December 2025 and they
12+ # cannot be recreated, so there is no token to fall back on. Rather than a
13+ # workflow that fails every release until somebody remembers, the npm step is
14+ # switched on by a repository variable. Set NPM_TRUSTED_PUBLISHING to "true"
15+ # once the npm side is configured and the next release publishes there too; no
16+ # change to this file is needed.
1917name : Publish
2018
2119on :
2422 workflow_dispatch :
2523 inputs :
2624 dryRun :
27- description : ' Pack and check without publishing.'
25+ description : ' Build and pack without attaching or publishing anything .'
2826 type : boolean
2927 default : true
3028
3129permissions :
32- contents : read
33- # The whole mechanism. Without it there is no OIDC token to present, and npm
34- # falls back to looking for credentials that deliberately do not exist here.
30+ # Uploading the tarball to the release.
31+ contents : write
32+ # Trusted publishing mints its OIDC token from this. Harmless while the npm
33+ # step is off, and one less thing to remember when it is turned on.
3534 id-token : write
3635
3736jobs :
3837 publish :
39- # Must be a GitHub-hosted runner : trusted publishing does not accept OIDC
40- # tokens from self-hosted ones. This is why publishing lives on GitHub and
41- # not beside the rest of the pipeline on the Forgejo runners.
38+ # Must be GitHub-hosted: trusted publishing does not accept OIDC tokens from
39+ # self-hosted runners. It is why publishing lives here rather than beside
40+ # the rest of the pipeline on the Forgejo runners.
4241 runs-on : ubuntu-latest
4342 steps :
4443 - uses : actions/checkout@v4
4544
4645 - uses : actions/setup-node@v4
4746 with :
48- # Trusted publishing needs Node 22.14 or newer.
47+ # 22.14 is the floor for trusted publishing, so the newest LTS keeps
48+ # both paths available.
4949 node-version : ' 24'
5050 registry-url : ' https://registry.npmjs.org'
5151 cache : npm
5252
53- # The runner's bundled npm is usually new enough, but "usually" is not a
54- # thing to discover during a release. OIDC publishing landed in 11.5.1.
55- - name : Ensure npm understands trusted publishing
56- run : |
57- npm install -g npm@latest
58- npm --version
59-
6053 - run : npm ci
6154
62- # Never publish something that was not tested, even though CI already ran
63- # on this commit: a release can be cut from any commit, including one CI
55+ # Never ship something that was not tested, even though CI already ran on
56+ # this commit: a release can be cut from any commit, including one CI
6457 # never saw.
6558 - name : Build and test
6659 run : npm test
6760
68- # A tag that disagrees with package.json publishes a version nobody asked
69- # for, under a name the release notes do not match.
61+ # A tag that disagrees with package.json ships a version nobody asked for,
62+ # under a name the release notes do not match.
7063 - name : The tag and package.json agree
7164 if : github.event_name == 'release'
7265 run : |
@@ -76,15 +69,47 @@ jobs:
7669 echo "Release tag is $tag but package.json says $packaged."
7770 exit 1
7871 fi
79- echo "Publishing $packaged."
72+ echo "Shipping $packaged."
73+
74+ - name : Pack
75+ id : pack
76+ run : |
77+ file="$(npm pack --silent)"
78+ echo "file=$file" >> "$GITHUB_OUTPUT"
79+ echo "Packed $file ($(du -h "$file" | cut -f1))"
80+ npm pack --dry-run
81+
82+ # The tarball is a complete, installable package — the same bytes npm
83+ # would have received — so a release is usable whether or not npm ever
84+ # hears about it.
85+ - name : Attach the tarball to the release
86+ if : github.event_name == 'release'
87+ env :
88+ GH_TOKEN : ${{ github.token }}
89+ run : |
90+ gh release upload "$GITHUB_REF_NAME" "${{ steps.pack.outputs.file }}" --clobber
91+ echo "Install it with:"
92+ echo " npm install https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/${{ steps.pack.outputs.file }}"
8093
81- - name : What would be published
82- run : npm pack --dry-run
94+ # Off until the npm organisation has a trusted publisher. No token is
95+ # involved either way: access and provenance come from publishConfig, so
96+ # this either authenticates by OIDC or does not run at all.
97+ - name : Publish to npm
98+ if : |
99+ vars.NPM_TRUSTED_PUBLISHING == 'true' &&
100+ (github.event_name == 'release' || inputs.dryRun == false)
101+ run : |
102+ npm install -g npm@latest
103+ npm --version
104+ npm publish
83105
84- # No token, no NODE_AUTH_TOKEN, no secret of any kind. Access and
85- # provenance come from publishConfig in package.json, so publishing by
86- # hand from a laptop fails rather than quietly producing an unattested
87- # package.
88- - name : Publish
89- if : github.event_name == 'release' || inputs.dryRun == false
90- run : npm publish
106+ - name : Say where it went
107+ if : github.event_name == 'release'
108+ run : |
109+ echo "Attached to the GitHub release: ${{ steps.pack.outputs.file }}"
110+ if [ "${{ vars.NPM_TRUSTED_PUBLISHING }}" = "true" ]; then
111+ echo "Published to npm."
112+ else
113+ echo "Not published to npm: NPM_TRUSTED_PUBLISHING is not set to true."
114+ echo "That is expected until the npm organisation has a trusted publisher."
115+ fi
0 commit comments