Skip to content

Commit 6be08dc

Browse files
committed
chore(ui-scripts): implement trusted publishing for npm
1 parent 9b2121f commit 6be08dc

File tree

6 files changed

+96
-92
lines changed

6 files changed

+96
-92
lines changed

.github/workflows/manual-release-to-npm.yml renamed to .github/workflows/_manual-release-reusable.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
name: Manual Npm release
2-
on: workflow_dispatch
1+
name: Manual release to npm (Reusable)
2+
on:
3+
workflow_call:
4+
secrets:
5+
inherit
6+
7+
permissions:
8+
id-token: write
9+
contents: write
10+
311
jobs:
412
release:
513
runs-on: ubuntu-latest
@@ -19,10 +27,6 @@ jobs:
1927
- name: Set up project
2028
run: pnpm run bootstrap
2129
- name: Release to NPM
22-
env:
23-
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
24-
NPM_EMAIL: ${{secrets.NPM_EMAIL}}
25-
NPM_USERNAME: ${{secrets.NPM_USERNAME}}
2630
run: pnpm run release
2731
- name: Get commit message
2832
run: | # puts the first line of the last commit message to the commmit_message env var

.github/workflows/manual-release-from-pr.yml renamed to .github/workflows/_pr-release-reusable.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
name: npm release from PR
2-
on: workflow_dispatch
1+
name: PR release to npm (Reusable)
2+
on:
3+
workflow_call:
4+
secrets:
5+
inherit
6+
7+
permissions:
8+
id-token: write
9+
contents: write
10+
311
jobs:
412
release:
513
runs-on: ubuntu-latest
@@ -18,10 +26,6 @@ jobs:
1826
- name: Set up project
1927
run: pnpm run bootstrap
2028
- name: Release to NPM
21-
env:
22-
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
23-
NPM_EMAIL: ${{secrets.NPM_EMAIL}}
24-
NPM_USERNAME: ${{secrets.NPM_USERNAME}}
2529
run: pnpm run release -- --prRelease
2630
- name: Get commit message
2731
run: | # puts the first line of the last commit message to the commmit_message env var
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
name: AUTO npm release, docs deploy and ssr regression - on master
1+
name: Release to npm (Reusable)
22
on:
3-
push:
4-
branches:
5-
- master
3+
workflow_call:
4+
secrets:
5+
inherit
6+
7+
permissions:
8+
id-token: write # Required for OIDC
9+
contents: write # Required for git tags
10+
611
jobs:
712
release:
813
runs-on: ubuntu-latest
@@ -23,11 +28,8 @@ jobs:
2328
- name: Run tests.
2429
run: USE_REACT_STRICT_MODE=0 pnpm run test:vitest
2530
- name: Release to NPM
26-
env:
27-
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
28-
NPM_EMAIL: ${{secrets.NPM_EMAIL}}
29-
NPM_USERNAME: ${{secrets.NPM_USERNAME}}
3031
run: pnpm run release
32+
# Note: No env block needed - OIDC auth is automatic
3133
tag:
3234
needs: release
3335
if: "startsWith(github.event.head_commit.message, 'chore(release)')"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Release to npm (OIDC Entry Point)
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
workflow_dispatch:
8+
inputs:
9+
release_type:
10+
description: 'Type of release to perform'
11+
required: true
12+
type: choice
13+
options:
14+
- manual
15+
- pr-snapshot
16+
default: 'manual'
17+
18+
permissions:
19+
id-token: write # Required for OIDC token generation
20+
contents: write # Required for git operations (tagging)
21+
22+
jobs:
23+
# Automatic production release on push to master
24+
auto-release:
25+
if: github.event_name == 'push'
26+
uses: ./.github/workflows/_release-reusable.yml
27+
permissions:
28+
id-token: write
29+
contents: write
30+
secrets: inherit
31+
32+
# Manual release (on-demand)
33+
manual-release:
34+
if: github.event_name == 'workflow_dispatch' && inputs.release_type == 'manual'
35+
uses: ./.github/workflows/_manual-release-reusable.yml
36+
permissions:
37+
id-token: write
38+
contents: write
39+
secrets: inherit
40+
41+
# PR snapshot release
42+
pr-release:
43+
if: github.event_name == 'workflow_dispatch' && inputs.release_type == 'pr-snapshot'
44+
uses: ./.github/workflows/_pr-release-reusable.yml
45+
permissions:
46+
id-token: write
47+
contents: write
48+
secrets: inherit

packages/ui-scripts/lib/commands/publish.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,14 @@ async function publishPackage(pkg, tag) {
215215
setTimeout(resolve, delay)
216216
})
217217

218-
const publishArgs = ['publish', pkg.location, '--tag', tag, '--no-git-checks']
218+
const publishArgs = [
219+
'publish',
220+
pkg.location,
221+
'--tag',
222+
tag,
223+
'--no-git-checks',
224+
'--provenance'
225+
]
219226
await runCommandAsync('pnpm', publishArgs)
220227

221228
return wait(500)

packages/ui-scripts/lib/utils/npm.js

Lines changed: 10 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
* SOFTWARE.
2323
*/
24-
import fs from 'fs'
25-
import path from 'path'
26-
import os from 'os'
2724
import semver from 'semver'
2825
import pkgUtils from '@instructure/pkg-utils'
2926
import {
@@ -34,11 +31,6 @@ import {
3431
} from '@instructure/command-utils'
3532

3633
import { Project } from '@lerna/project'
37-
38-
const NPM_SCOPE = '@instructure:registry=https://registry.npmjs.org/'
39-
40-
// Track user .npmrc backup for cleanup
41-
let userNpmrcBackup = null
4234
const syncRootPackageVersion = async (useProjectVersion) => {
4335
const project = new Project(process.cwd())
4436
const rootPkg = pkgUtils.getPackage()
@@ -111,76 +103,23 @@ export async function bumpPackages(packageName, requestedVersion) {
111103
}
112104

113105
export function createNPMRCFile() {
114-
const { NPM_TOKEN, NPM_EMAIL, NPM_USERNAME } = process.env
115-
116-
// Only write an npmrc file if these are defined, otherwise assume the system is properly configured
117-
if (NPM_TOKEN) {
118-
const userHome = os.homedir()
119-
const userNpmrcPath = path.join(userHome, '.npmrc')
120-
121-
// Backup existing user .npmrc if it exists
122-
if (fs.existsSync(userNpmrcPath)) {
123-
const existingContent = fs.readFileSync(userNpmrcPath, 'utf8')
124-
userNpmrcBackup = {
125-
path: userNpmrcPath,
126-
content: existingContent,
127-
existed: true
128-
}
129-
info(`📦 Backing up existing ${userNpmrcPath}`)
130-
} else {
131-
userNpmrcBackup = {
132-
path: userNpmrcPath,
133-
content: null,
134-
existed: false
135-
}
136-
}
137-
138-
// Write auth credentials to user .npmrc
139-
const authConfig = `//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n${NPM_SCOPE}\nemail=${NPM_EMAIL}\nname=${NPM_USERNAME}\n`
140-
141-
if (userNpmrcBackup.existed) {
142-
// Append to existing content
143-
fs.writeFileSync(userNpmrcPath, userNpmrcBackup.content + '\n' + authConfig)
144-
} else {
145-
// Create new file
146-
fs.writeFileSync(userNpmrcPath, authConfig)
147-
}
148-
149-
info(`📦 Written auth config to ${userNpmrcPath}`)
150-
}
106+
info('📦 Using OIDC authentication (npm trusted publishing)')
151107

108+
// Verify OIDC authentication works
152109
try {
153-
info('running pnpm whoami:')
110+
info('📦 Running pnpm whoami to verify OIDC auth:')
154111
runCommandSync('pnpm', ['whoami'])
155112
} catch (e) {
156-
error(`Could not determine if NPM auth was successful: ${e}`)
113+
error(`Could not verify OIDC authentication: ${e}`)
114+
error('Make sure:')
115+
error(' 1. Workflow has id-token: write permissions')
116+
error(' 2. npm packages are configured for trusted publishing')
117+
error(' 3. Workflow is running in GitHub Actions')
157118
process.exit(1)
158119
}
159120
}
160121

161122
export function cleanupNPMRCFile() {
162-
if (!userNpmrcBackup) {
163-
// Nothing to cleanup
164-
return
165-
}
166-
167-
try {
168-
if (userNpmrcBackup.existed) {
169-
// Restore original content
170-
fs.writeFileSync(userNpmrcBackup.path, userNpmrcBackup.content)
171-
info(`📦 Restored original ${userNpmrcBackup.path}`)
172-
} else {
173-
// Remove the file we created
174-
if (fs.existsSync(userNpmrcBackup.path)) {
175-
fs.unlinkSync(userNpmrcBackup.path)
176-
info(`📦 Removed ${userNpmrcBackup.path}`)
177-
}
178-
}
179-
} catch (e) {
180-
error(`Failed to cleanup .npmrc: ${e}`)
181-
// Don't exit - cleanup failure shouldn't break the release
182-
} finally {
183-
// Reset backup state
184-
userNpmrcBackup = null
185-
}
123+
// No cleanup needed with OIDC authentication
124+
// This function is kept for backward compatibility
186125
}

0 commit comments

Comments
 (0)