Skip to content

Commit 890fc18

Browse files
committed
ci(npm): publish every npm package through trusted publishing
The four publish workflows authenticate to npm with the runner's OIDC token instead of the long-lived NPM_TOKEN secret, so no npm credential is stored anywhere and every release carries a provenance attestation. - Publish jobs request id-token: write and run Node 24, whose npm 11 is required for trusted publishing. - sim and sim-setup pack once with bun, which rewrites workspace: ranges, smoke-test that tarball, then npm publish the same tarball, so what was tested is what ships. The smoke tests now also assert the bumped version. - sim-setup republishes when its own workflow changes, as sim already does. - simstudio gets the repository field provenance requires. - The setup action drops its registry-url input, which existed only to feed NODE_AUTH_TOKEN.
1 parent 776c33a commit 890fc18

6 files changed

Lines changed: 87 additions & 55 deletions

File tree

‎.github/actions/setup/action.yml‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ inputs:
1414
description: Node version to install. Empty keeps the runner's preinstalled Node, for jobs that only ever ran Bun.
1515
required: false
1616
default: '24'
17-
registry-url:
18-
description: npm registry to write an .npmrc for, so `npm publish` reads NODE_AUTH_TOKEN. Empty writes none.
19-
required: false
20-
default: ''
2117

2218
# Sticky disk keys are scoped by event name, and fork PRs get their own namespace on
2319
# top: untrusted fork runs must never share a cache with push runs (whose caches
@@ -43,7 +39,6 @@ runs:
4339
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
4440
with:
4541
node-version: ${{ inputs.node-version }}
46-
registry-url: ${{ inputs.registry-url }}
4742

4843
- name: Mount Bun cache
4944
if: inputs.provider == '' || inputs.provider == 'blacksmith'

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ 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.
911
permissions:
1012
contents: read
13+
id-token: write
1114

1215
# A newer push waits for the running publish rather than racing its version check.
1316
concurrency:
@@ -25,12 +28,12 @@ jobs:
2528
with:
2629
persist-credentials: false
2730

31+
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
2832
- name: Setup workspace
2933
uses: ./.github/actions/setup
3034
with:
3135
provider: ${{ vars.CI_PROVIDER }}
32-
node-version: '22'
33-
registry-url: https://registry.npmjs.org/
36+
node-version: '24'
3437

3538
- name: Build package
3639
working-directory: packages/cli
@@ -56,8 +59,6 @@ jobs:
5659
if: steps.version_check.outputs.exists == 'false'
5760
working-directory: packages/cli
5861
run: npm publish --access=public
59-
env:
60-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6162

6263
- name: Log skipped publish
6364
if: steps.version_check.outputs.exists == 'true'

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

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ 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.
1416
permissions:
1517
contents: read
18+
id-token: write
1619

1720
concurrency:
1821
group: publish-sim-cli-${{ github.ref }}
@@ -29,18 +32,14 @@ jobs:
2932
with:
3033
persist-credentials: false
3134

32-
# Vitest 5 requires Node 22.12+; the published bundle is still
33-
# smoke-tested on Node 20 below, its minimum supported runtime.
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.
3438
- name: Setup workspace
3539
uses: ./.github/actions/setup
3640
with:
3741
provider: ${{ vars.CI_PROVIDER }}
38-
node-version: '22'
39-
40-
- name: Verify npm authentication
41-
env:
42-
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
43-
run: bun pm whoami
42+
node-version: '24'
4443

4544
- name: Auto-bump package version
4645
run: bun run bump:npm-packages sim-cli
@@ -54,9 +53,10 @@ jobs:
5453
run: bun run type-check
5554

5655
# The usage-reporting destination is baked into the bundle at build time
57-
# (docs/cli/usage-data), so only the two steps that build it get the
58-
# token. Every other step, the tests above in particular, runs without
59-
# it: a test that runs a real command must never report to production.
56+
# (docs/cli/usage-data), so only this step gets the token, and the
57+
# tarball packed from its output is what ships. Every other step, the
58+
# tests above in particular, runs without it: a test that runs a real
59+
# command must never report to production.
6060
- name: Build package
6161
working-directory: packages/sim-cli
6262
env:
@@ -107,23 +107,38 @@ jobs:
107107
echo "tag=$TAG"
108108
} >> "$GITHUB_OUTPUT"
109109
110+
# 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.
113+
- name: Pack package
114+
id: pack
115+
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"
120+
110121
- name: Setup Node 20 for the bundle smoke test
111122
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
112123
with:
113124
node-version: '20'
114125

115126
- name: Smoke-test packed Node bundle
116-
working-directory: packages/sim-cli
127+
env:
128+
TARBALL: ${{ steps.pack.outputs.tarball }}
129+
VERSION: ${{ steps.release.outputs.version }}
117130
run: |
118131
set -euo pipefail
119132
SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-cli-smoke.XXXXXX")"
120-
PACKAGE_PATH="$SMOKE_DIR/sim-cli.tgz"
121-
bun pm pack --ignore-scripts --filename "$PACKAGE_PATH" --quiet
122-
tar -xzf "$PACKAGE_PATH" -C "$SMOKE_DIR"
123-
"$SMOKE_DIR/package/dist/index.js" --version
133+
tar -xzf "$TARBALL" -C "$SMOKE_DIR"
134+
test "$("$SMOKE_DIR/package/dist/index.js" --version)" = "$VERSION"
135+
136+
- name: Setup Node 24 for trusted publishing
137+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
138+
with:
139+
node-version: '24'
124140

125141
- name: Verify version is unpublished
126-
working-directory: packages/sim-cli
127142
env:
128143
VERSION: ${{ steps.release.outputs.version }}
129144
run: |
@@ -132,16 +147,13 @@ jobs:
132147
exit 1
133148
fi
134149
135-
# `bun publish` runs `prepublishOnly`, which rebuilds `dist`, and that
136-
# rebuild is what ships, so it needs the token as well.
150+
# npm publishes a tarball without running lifecycle scripts, so nothing
151+
# rebuilds `dist` here. Provenance is attached automatically.
137152
- name: Publish to npm
138-
working-directory: packages/sim-cli
139153
env:
140-
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
154+
TARBALL: ${{ steps.pack.outputs.tarball }}
141155
NPM_TAG: ${{ steps.release.outputs.tag }}
142-
SIM_CLI_TELEMETRY_KEY: ${{ vars.SIM_CLI_TELEMETRY_KEY }}
143-
SIM_CLI_TELEMETRY_HOST: ${{ vars.SIM_CLI_TELEMETRY_HOST }}
144-
run: bun publish --access public --tag "$NPM_TAG" --no-save
156+
run: npm publish "$TARBALL" --access public --tag "$NPM_TAG"
145157

146158
- name: Summarize release
147159
env:

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

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ on:
1010
- 'packages/utils/**'
1111
- 'docker-compose.prod.yml'
1212
- 'bun.lock'
13+
# A change to how the package is built or published has to republish it.
14+
- '.github/workflows/publish-sim-setup.yml'
1315

16+
# npm trusted publishing: the publish step trades the runner's OIDC token for a
17+
# short-lived npm credential, so no npm token is stored anywhere.
1418
permissions:
1519
contents: read
20+
id-token: write
1621

1722
concurrency:
1823
group: publish-sim-setup-${{ github.ref }}
@@ -29,18 +34,14 @@ jobs:
2934
with:
3035
persist-credentials: false
3136

32-
# Vitest 5 requires Node 22.12+; the published bundle is still
33-
# smoke-tested on Node 20 below, its minimum supported runtime.
37+
# Node 24 bundles npm 11.5.1+, which trusted publishing requires; the
38+
# published bundle is still smoke-tested on Node 20 below, its minimum
39+
# supported runtime.
3440
- name: Setup workspace
3541
uses: ./.github/actions/setup
3642
with:
3743
provider: ${{ vars.CI_PROVIDER }}
38-
node-version: '22'
39-
40-
- name: Verify npm authentication
41-
env:
42-
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
43-
run: bun pm whoami
44+
node-version: '24'
4445

4546
- name: Auto-bump package version
4647
run: bun run bump:npm-packages sim-setup
@@ -104,21 +105,32 @@ jobs:
104105
echo "tag=$TAG"
105106
} >> "$GITHUB_OUTPUT"
106107
108+
# Bun packs because it rewrites `workspace:` ranges to real versions,
109+
# which `npm pack` would publish verbatim. The smoke test and the publish
110+
# below both use this one tarball, so what was tested is what ships.
111+
- name: Pack package
112+
id: pack
113+
working-directory: packages/sim-setup
114+
run: |
115+
TARBALL="$RUNNER_TEMP/sim-setup.tgz"
116+
bun pm pack --ignore-scripts --filename "$TARBALL" --quiet
117+
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
118+
107119
- name: Setup Node 20 for the bundle smoke test
108120
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
109121
with:
110122
node-version: '20'
111123

112124
- name: Smoke-test packed Node bundle
113-
working-directory: packages/sim-setup
125+
env:
126+
TARBALL: ${{ steps.pack.outputs.tarball }}
127+
VERSION: ${{ steps.release.outputs.version }}
114128
run: |
115129
set -euo pipefail
116130
SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-setup-smoke.XXXXXX")"
117-
PACKAGE_PATH="$SMOKE_DIR/sim-setup.tgz"
118-
bun pm pack --ignore-scripts --filename "$PACKAGE_PATH" --quiet
119-
tar -xzf "$PACKAGE_PATH" -C "$SMOKE_DIR"
131+
tar -xzf "$TARBALL" -C "$SMOKE_DIR"
120132
121-
if tar -tzf "$PACKAGE_PATH" | grep -Eq '^package/(apps|docker|helm|packages|scripts)/'; then
133+
if tar -tzf "$TARBALL" | grep -Eq '^package/(apps|docker|helm|packages|scripts)/'; then
122134
echo 'Packed setup package contains repository source directories.' >&2
123135
exit 1
124136
fi
@@ -130,7 +142,7 @@ jobs:
130142
fi
131143
132144
cd "$SMOKE_DIR"
133-
node package/dist/index.js --version
145+
test "$(node package/dist/index.js --version)" = "$VERSION"
134146
node package/dist/index.js --help
135147
for command in config add doctor start stop restart update status logs down reset; do
136148
node package/dist/index.js "$command" --help > /dev/null
@@ -145,8 +157,12 @@ jobs:
145157
exit 1
146158
fi
147159
160+
- name: Setup Node 24 for trusted publishing
161+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
162+
with:
163+
node-version: '24'
164+
148165
- name: Verify version is unpublished
149-
working-directory: packages/sim-setup
150166
env:
151167
VERSION: ${{ steps.release.outputs.version }}
152168
run: |
@@ -155,12 +171,13 @@ jobs:
155171
exit 1
156172
fi
157173
174+
# npm publishes a tarball without running lifecycle scripts, so nothing
175+
# rebuilds `dist` here. Provenance is attached automatically.
158176
- name: Publish to npm
159-
working-directory: packages/sim-setup
160177
env:
161-
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
178+
TARBALL: ${{ steps.pack.outputs.tarball }}
162179
NPM_TAG: ${{ steps.release.outputs.tag }}
163-
run: bun publish --access public --tag "$NPM_TAG" --no-save
180+
run: npm publish "$TARBALL" --access public --tag "$NPM_TAG"
164181

165182
- name: Summarize release
166183
env:

‎.github/workflows/publish-ts-sdk.yml‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ on:
66
paths:
77
- 'packages/ts-sdk/**'
88

9+
# `contents: write` creates the GitHub release. `id-token: write` is npm trusted
10+
# publishing: the publish step trades the runner's OIDC token for a short-lived
11+
# npm credential, so no npm token is stored anywhere.
912
permissions:
1013
contents: write
14+
id-token: write
1115

1216
# A newer push waits for the running publish rather than racing its version check.
1317
concurrency:
@@ -25,12 +29,12 @@ jobs:
2529
with:
2630
persist-credentials: false
2731

32+
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
2833
- name: Setup workspace
2934
uses: ./.github/actions/setup
3035
with:
3136
provider: ${{ vars.CI_PROVIDER }}
32-
node-version: '22'
33-
registry-url: https://registry.npmjs.org/
37+
node-version: '24'
3438

3539
- name: Run tests
3640
working-directory: packages/ts-sdk
@@ -60,8 +64,6 @@ jobs:
6064
if: steps.version_check.outputs.exists == 'false'
6165
working-directory: packages/ts-sdk
6266
run: npm publish --access=public
63-
env:
64-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6567

6668
- name: Log skipped publish
6769
if: steps.version_check.outputs.exists == 'true'

‎packages/cli/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
],
3636
"author": "Sim",
3737
"license": "Apache-2.0",
38+
"repository": {
39+
"type": "git",
40+
"url": "git+https://github.com/simstudioai/sim.git",
41+
"directory": "packages/cli"
42+
},
3843
"engines": {
3944
"node": ">=18"
4045
},

0 commit comments

Comments
 (0)