Skip to content

Commit a0d3145

Browse files
authored
ci: publish to npm when a release is published (#304)
Publish to npm on a published GitHub release, guarded by tests, a tag/version match check, and a skip when the version is already on the registry. Publishes with provenance. Requires an NPM_TOKEN repository secret.
1 parent a63f7c1 commit a0d3145

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Publish the release to npm.
2+
#
3+
# moshcode's own install path is `curl … install.sh | sh`, and npm is a second
4+
# channel rather than the primary one — which is exactly why it needs to be
5+
# automatic. A channel that only updates when someone remembers is a channel
6+
# that silently serves an old version forever.
7+
#
8+
# Runs on a published GitHub release, so the release itself is the trigger and
9+
# there is no separate step to forget. `workflow_dispatch` is for re-running one
10+
# that failed on something transient; it is safe because a version already on
11+
# the registry is skipped rather than attempted.
12+
#
13+
# Requires a repository secret `NPM_TOKEN` — an npm automation token, which is
14+
# the kind that publishes without a 2FA prompt.
15+
name: publish
16+
17+
on:
18+
release:
19+
types: [published]
20+
workflow_dispatch:
21+
22+
permissions:
23+
contents: read
24+
# For npm provenance: the attestation is signed with a short-lived OIDC token
25+
# rather than anything stored here, and it is what lets npm show which build
26+
# this tarball actually came from.
27+
id-token: write
28+
29+
jobs:
30+
publish:
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 15
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
# pnpm version is read from the "packageManager" field in package.json.
37+
# Do not pin a version here — it conflicts with packageManager and fails
38+
# with ERR_PNPM_BAD_PM_VERSION.
39+
- uses: pnpm/action-setup@v4
40+
41+
- uses: actions/setup-node@v4
42+
with:
43+
node-version: 22
44+
cache: pnpm
45+
registry-url: https://registry.npmjs.org
46+
47+
- run: pnpm install --frozen-lockfile
48+
49+
# Publishing is the one action here that cannot be taken back — npm will
50+
# not let a version be replaced — so the tests run first, on the exact
51+
# tree about to be packed.
52+
- run: pnpm run --if-present test
53+
env:
54+
CI: true
55+
56+
- name: Read the version being published
57+
id: version
58+
run: |
59+
VERSION="$(node -p "require('./package.json').version")"
60+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
61+
echo "moshcode@$VERSION"
62+
63+
# A release tagged v0.25.0 carrying package.json 0.24.0 would publish the
64+
# wrong tree under a version nobody can reuse. Cheap to check, impossible
65+
# to undo.
66+
- name: Check the tag matches package.json
67+
if: github.event_name == 'release'
68+
env:
69+
TAG: ${{ github.event.release.tag_name }}
70+
VERSION: ${{ steps.version.outputs.version }}
71+
run: |
72+
if [ "$TAG" != "v$VERSION" ]; then
73+
echo "::error::release tag $TAG does not match package.json version $VERSION"
74+
exit 1
75+
fi
76+
77+
# Makes a re-run harmless. Without it, dispatching the workflow twice
78+
# fails the second time on an E403 that reads like something broke.
79+
- name: Skip if this version is already on npm
80+
id: published
81+
env:
82+
VERSION: ${{ steps.version.outputs.version }}
83+
run: |
84+
if npm view "moshcode@$VERSION" version >/dev/null 2>&1; then
85+
echo "already=true" >> "$GITHUB_OUTPUT"
86+
echo "moshcode@$VERSION is already published — nothing to do"
87+
else
88+
echo "already=false" >> "$GITHUB_OUTPUT"
89+
fi
90+
91+
# Said plainly here, rather than as the 401 npm would otherwise return
92+
# partway through a release.
93+
- name: Require an npm token
94+
if: steps.published.outputs.already == 'false'
95+
env:
96+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
97+
run: |
98+
if [ -z "$NPM_TOKEN" ]; then
99+
echo "::error::NPM_TOKEN is not set — add an npm automation token as a repository secret named NPM_TOKEN"
100+
exit 1
101+
fi
102+
103+
- name: Publish
104+
if: steps.published.outputs.already == 'false'
105+
run: npm publish --access public --provenance
106+
env:
107+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
108+
109+
- name: Confirm the registry has it
110+
if: steps.published.outputs.already == 'false'
111+
env:
112+
VERSION: ${{ steps.version.outputs.version }}
113+
run: |
114+
# The registry is read-through cached, so a fresh publish can 404 for
115+
# a moment. Retry rather than report a good publish as a failure.
116+
for _ in 1 2 3 4 5; do
117+
if [ "$(npm view "moshcode@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
118+
echo "moshcode@$VERSION is on the registry"
119+
exit 0
120+
fi
121+
sleep 5
122+
done
123+
echo "::error::published, but the registry does not report moshcode@$VERSION yet"
124+
exit 1

0 commit comments

Comments
 (0)