Skip to content

Commit 583e8cb

Browse files
committed
ci(npm): publish from a GitHub-hosted job that alone can mint the OIDC token
npm trusted publishing accepts OIDC tokens only from GitHub-hosted runners, and the publish jobs ran on Blacksmith by default. Each workflow now builds, tests, packs and smoke-tests on its usual runner with read-only permissions, then hands the tarball to a GitHub-hosted publish job, the only one granted id-token: write. That job runs no repository code, so no test or dependency can mint an npm credential. The TypeScript SDK's GitHub release moves to its own job with contents: write.
1 parent 890fc18 commit 583e8cb

4 files changed

Lines changed: 224 additions & 98 deletions

File tree

‎.github/workflows/publish-cli.yml‎

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,33 @@ on:
66
paths:
77
- 'packages/cli/**'
88

9-
# npm trusted publishing: the publish step trades the runner's OIDC token for a
10-
# short-lived npm credential, so no npm token is stored anywhere.
119
permissions:
1210
contents: read
13-
id-token: write
1411

1512
# A newer push waits for the running publish rather than racing its version check.
1613
concurrency:
1714
group: publish-cli-${{ github.ref }}
1815
cancel-in-progress: false
1916

2017
jobs:
21-
publish:
22-
name: publish
18+
build:
19+
name: build
2320
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
2421
timeout-minutes: 15
22+
outputs:
23+
version: ${{ steps.package_version.outputs.version }}
24+
exists: ${{ steps.version_check.outputs.exists }}
2525
steps:
2626
- name: Checkout repository
2727
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
2828
with:
2929
persist-credentials: false
3030

31-
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
3231
- name: Setup workspace
3332
uses: ./.github/actions/setup
3433
with:
3534
provider: ${{ vars.CI_PROVIDER }}
36-
node-version: '24'
35+
node-version: '22'
3736

3837
- name: Build package
3938
working-directory: packages/cli
@@ -42,26 +41,67 @@ jobs:
4241
- name: Get package version
4342
id: package_version
4443
working-directory: packages/cli
45-
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
44+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
4645

4746
- name: Check if version already exists
4847
id: version_check
4948
env:
5049
VERSION: ${{ steps.package_version.outputs.version }}
5150
run: |
5251
if npm view "simstudio@$VERSION" version &> /dev/null; then
53-
echo "exists=true" >> $GITHUB_OUTPUT
52+
echo "exists=true" >> "$GITHUB_OUTPUT"
5453
else
55-
echo "exists=false" >> $GITHUB_OUTPUT
54+
echo "exists=false" >> "$GITHUB_OUTPUT"
5655
fi
5756
58-
- name: Publish to npm
57+
# Bun packs because it rewrites `workspace:` ranges to real versions,
58+
# which `npm pack` would publish verbatim.
59+
- name: Pack package
5960
if: steps.version_check.outputs.exists == 'false'
6061
working-directory: packages/cli
61-
run: npm publish --access=public
62+
run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet
63+
64+
- name: Upload package
65+
if: steps.version_check.outputs.exists == 'false'
66+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
67+
with:
68+
name: package
69+
path: ${{ runner.temp }}/package.tgz
70+
if-no-files-found: error
71+
retention-days: 1
6272

6373
- name: Log skipped publish
6474
if: steps.version_check.outputs.exists == 'true'
6575
env:
6676
VERSION: ${{ steps.package_version.outputs.version }}
67-
run: echo "Skipped publishing because version $VERSION already exists on npm"
77+
run: echo "Skipped publishing because version $VERSION already exists on npm"
78+
79+
# npm trusted publishing trades this job's OIDC token for a short-lived npm
80+
# credential, so no npm token is stored anywhere. npm accepts the token only
81+
# from GitHub-hosted runners, and only this job can mint one: it runs no
82+
# repository code, it publishes the tarball the build job packed. npm runs no
83+
# lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance
84+
# is attached automatically.
85+
publish:
86+
name: publish
87+
needs: build
88+
if: needs.build.outputs.exists == 'false'
89+
runs-on: ubuntu-latest
90+
timeout-minutes: 10
91+
permissions:
92+
id-token: write
93+
steps:
94+
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
95+
- name: Setup Node
96+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
97+
with:
98+
node-version: '24'
99+
100+
- name: Download package
101+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
102+
with:
103+
name: package
104+
path: ${{ runner.temp }}
105+
106+
- name: Publish to npm
107+
run: npm publish "$RUNNER_TEMP/package.tgz" --access public

‎.github/workflows/publish-sim-cli.yml‎

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,34 @@ on:
1111
# A change to how the package is built or published has to republish it.
1212
- '.github/workflows/publish-sim-cli.yml'
1313

14-
# npm trusted publishing: the publish step trades the runner's OIDC token for a
15-
# short-lived npm credential, so no npm token is stored anywhere.
1614
permissions:
1715
contents: read
18-
id-token: write
1916

2017
concurrency:
2118
group: publish-sim-cli-${{ github.ref }}
2219
cancel-in-progress: false
2320

2421
jobs:
25-
publish:
26-
name: publish
22+
build:
23+
name: build
2724
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
2825
timeout-minutes: 15
26+
outputs:
27+
version: ${{ steps.release.outputs.version }}
28+
tag: ${{ steps.release.outputs.tag }}
2929
steps:
3030
- name: Checkout repository
3131
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
3232
with:
3333
persist-credentials: false
3434

35-
# Node 24 bundles npm 11.5.1+, which trusted publishing requires; the
36-
# published bundle is still smoke-tested on Node 20 below, its minimum
37-
# supported runtime.
35+
# Vitest 5 requires Node 22.12+; the published bundle is still
36+
# smoke-tested on Node 20 below, its minimum supported runtime.
3837
- name: Setup workspace
3938
uses: ./.github/actions/setup
4039
with:
4140
provider: ${{ vars.CI_PROVIDER }}
42-
node-version: '24'
41+
node-version: '22'
4342

4443
- name: Auto-bump package version
4544
run: bun run bump:npm-packages sim-cli
@@ -108,15 +107,11 @@ jobs:
108107
} >> "$GITHUB_OUTPUT"
109108
110109
# Bun packs because it rewrites `workspace:` ranges to real versions,
111-
# which `npm pack` would publish verbatim. The smoke test and the publish
112-
# below both use this one tarball, so what was tested is what ships.
110+
# which `npm pack` would publish verbatim. The smoke test below and the
111+
# publish job both use this one tarball, so what was tested is what ships.
113112
- name: Pack package
114-
id: pack
115113
working-directory: packages/sim-cli
116-
run: |
117-
TARBALL="$RUNNER_TEMP/sim-cli.tgz"
118-
bun pm pack --ignore-scripts --filename "$TARBALL" --quiet
119-
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
114+
run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet
120115

121116
- name: Setup Node 20 for the bundle smoke test
122117
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
@@ -125,19 +120,13 @@ jobs:
125120

126121
- name: Smoke-test packed Node bundle
127122
env:
128-
TARBALL: ${{ steps.pack.outputs.tarball }}
129123
VERSION: ${{ steps.release.outputs.version }}
130124
run: |
131125
set -euo pipefail
132126
SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-cli-smoke.XXXXXX")"
133-
tar -xzf "$TARBALL" -C "$SMOKE_DIR"
127+
tar -xzf "$RUNNER_TEMP/package.tgz" -C "$SMOKE_DIR"
134128
test "$("$SMOKE_DIR/package/dist/index.js" --version)" = "$VERSION"
135129
136-
- name: Setup Node 24 for trusted publishing
137-
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
138-
with:
139-
node-version: '24'
140-
141130
- name: Verify version is unpublished
142131
env:
143132
VERSION: ${{ steps.release.outputs.version }}
@@ -147,16 +136,47 @@ jobs:
147136
exit 1
148137
fi
149138
150-
# npm publishes a tarball without running lifecycle scripts, so nothing
151-
# rebuilds `dist` here. Provenance is attached automatically.
139+
- name: Upload package
140+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
141+
with:
142+
name: package
143+
path: ${{ runner.temp }}/package.tgz
144+
if-no-files-found: error
145+
retention-days: 1
146+
147+
# npm trusted publishing trades this job's OIDC token for a short-lived npm
148+
# credential, so no npm token is stored anywhere. npm accepts the token only
149+
# from GitHub-hosted runners, and only this job can mint one: it runs no
150+
# repository code, it publishes the tarball the build job tested. npm runs no
151+
# lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance
152+
# is attached automatically.
153+
publish:
154+
name: publish
155+
needs: build
156+
runs-on: ubuntu-latest
157+
timeout-minutes: 10
158+
permissions:
159+
id-token: write
160+
steps:
161+
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
162+
- name: Setup Node
163+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
164+
with:
165+
node-version: '24'
166+
167+
- name: Download package
168+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
169+
with:
170+
name: package
171+
path: ${{ runner.temp }}
172+
152173
- name: Publish to npm
153174
env:
154-
TARBALL: ${{ steps.pack.outputs.tarball }}
155-
NPM_TAG: ${{ steps.release.outputs.tag }}
156-
run: npm publish "$TARBALL" --access public --tag "$NPM_TAG"
175+
NPM_TAG: ${{ needs.build.outputs.tag }}
176+
run: npm publish "$RUNNER_TEMP/package.tgz" --access public --tag "$NPM_TAG"
157177

158178
- name: Summarize release
159179
env:
160-
VERSION: ${{ steps.release.outputs.version }}
161-
NPM_TAG: ${{ steps.release.outputs.tag }}
180+
VERSION: ${{ needs.build.outputs.version }}
181+
NPM_TAG: ${{ needs.build.outputs.tag }}
162182
run: echo "Published sim@$VERSION with the '$NPM_TAG' tag."

0 commit comments

Comments
 (0)