Skip to content

Commit 817731c

Browse files
committed
feat: a release ships the CLI, with npm switched on later by a variable
npm cannot be published to yet: it needs a trust relationship configured on its side, and there is no token to fall back on since every classic one was revoked in December. Waiting for that to be sorted means shipping nothing, so a release now carries the packed tarball as an asset — the same bytes npm would serve, and enough to install and run the CLI without the registry knowing it exists. The npm step stays in the workflow, switched off by a repository variable rather than deleted or left to fail. Setting NPM_TRUSTED_PUBLISHING to true once the npm side is configured makes the next release publish there too, with no change to any file. A workflow that fails every release until somebody remembers is a workflow people learn to ignore. The install instructions lead with the release tarball and say plainly that npx does not work yet, rather than printing a command that resolves to nothing. #1 has the remaining npm steps, including the one that is easy to miss: the trusted publisher goes on the organisation, not on the package, because a package-level one can only be added to a package that already exists.
1 parent 3b6175a commit 817731c

2 files changed

Lines changed: 95 additions & 48 deletions

File tree

.github/workflows/publish.yml

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
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.
1917
name: Publish
2018

2119
on:
@@ -24,49 +22,44 @@ on:
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

3129
permissions:
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

3736
jobs:
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

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,35 @@
33
Your CoderDojo Locker, from the command line. The eight-character code on your
44
card is how you get in — there is nothing to sign up for.
55

6+
Needs Node 20 or newer. No dependencies, so it starts quickly and there is no
7+
native module to fail on a locked-down laptop.
8+
9+
## Installing
10+
11+
Every [release](https://github.com/coderdojo-linz/locker-cli/releases) has the
12+
packaged tarball attached — the same bytes npm would serve — so it installs
13+
without npm knowing the package exists:
14+
615
```bash
7-
npx @coderdojo-linz/locker auth login
8-
npx @coderdojo-linz/locker upload ./my-game --name "Snake"
16+
npm install -g https://github.com/coderdojo-linz/locker-cli/releases/latest/download/coderdojo-linz-locker-0.1.0.tgz
917
```
1018

11-
Needs Node 20 or newer. No dependencies, so `npx` starts quickly and there is no
12-
native module to fail on a locked-down laptop.
19+
Then:
20+
21+
```bash
22+
locker auth login
23+
locker upload ./my-game --name "Snake"
24+
```
25+
26+
It is not on the npm registry yet, so `npx @coderdojo-linz/locker` does not work
27+
— see [the issue about that](https://github.com/coderdojo-linz/locker-cli/issues/1).
28+
Once it is, that becomes the shorter way in and needs no install at all.
29+
30+
From a clone, it runs straight out of the tree:
31+
32+
```bash
33+
npm ci && npm run build && node dist/index.js --help
34+
```
1335

1436
## The four things you will actually do
1537

0 commit comments

Comments
 (0)