Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions scripts/deploy/publish-npm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import assert from 'node:assert/strict'
import path from 'node:path'
import { before, beforeEach, describe, it, mock } from 'node:test'
import { mockCommandImplementation, mockModule } from './lib/testHelpers.ts'

interface CommandChain {
withEnvironment: (env: Record<string, string>) => CommandChain
withLogs: () => CommandChain
run: () => string | undefined
}

describe('publish-npm', () => {
const commandMock = mock.fn<(template: TemplateStringsArray, ...values: any[]) => CommandChain>()
const getNpmTokenMock = mock.fn<() => string>()

let main: (args?: string[]) => void
let publishError: Error

before(async () => {
await mockModule(path.resolve(import.meta.dirname, '../lib/command.ts'), { command: commandMock })
await mockModule(path.resolve(import.meta.dirname, '../lib/secrets.ts'), { getNpmToken: getNpmTokenMock })

const publishNpmModule: { main: (args?: string[]) => void } = await import('./publish-npm.ts')
;({ main } = publishNpmModule)
})

beforeEach(() => {
publishError = new Error('publish failed')
getNpmTokenMock.mock.mockImplementation(() => 'fake-token')

const baseCommandMock = mock.fn<(template: TemplateStringsArray, ...values: any[]) => CommandChain>()
mockCommandImplementation(baseCommandMock)

commandMock.mock.mockImplementation((template: TemplateStringsArray, ...values: any[]): CommandChain => {
const chain = baseCommandMock(template, ...values)

if (isNpmPublishCommand(template, ...values)) {
const throwingChain: CommandChain = {
withEnvironment: () => throwingChain,
withLogs: () => throwingChain,
run: () => {
throw publishError
},
}

return {
...chain,
withEnvironment: () => throwingChain,
withLogs: () => throwingChain,
run: throwingChain.run,
}
}
return chain
})
})

it('should suggest renewing npm token when npm publish fails', () => {
assert.throws(
() => main([]),
(error: Error) => {
assert.match(error.message, /scripts\/release\/renew-token\.ts/)
assert.equal(error.cause, publishError)
return true
}
)
})
})

function isNpmPublishCommand(template: TemplateStringsArray, ...values: any[]): boolean {
const command = template
.reduce((acc, part, index) => `${acc}${part}${values[index] || ''}`, '')
.replace(/\s+/g, ' ')
.trim()
return command.includes('npm publish')
}
27 changes: 19 additions & 8 deletions scripts/deploy/publish-npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { printLog, runMain } from '../lib/executionUtils.ts'
import { command } from '../lib/command.ts'
import { getNpmToken } from '../lib/secrets.ts'

runMain(() => {
if (!process.env.NODE_TEST_CONTEXT) {
runMain(() => main())
}

export function main(args = process.argv.slice(2)): void {
const {
values: { 'dry-run': dryRun },
} = parseArgs({
args,
options: {
'dry-run': { type: 'boolean', default: false },
},
Expand All @@ -22,11 +27,17 @@ runMain(() => {
command`yarn build`.withEnvironment({ BUILD_MODE: 'release' }).run()

printLog(dryRun ? 'Publishing (dry run)' : 'Publishing')
command`yarn workspaces foreach --verbose --all --topological --no-private npm publish --tolerate-republish --access public ${dryRun ? ['--dry-run'] : []}`
.withEnvironment({
YARN_NPM_AUTH_TOKEN: dryRun ? '' : getNpmToken(),
BUILD_MODE: 'release',
try {
command`yarn workspaces foreach --verbose --all --topological --no-private npm publish --tolerate-republish --access public ${dryRun ? ['--dry-run'] : []}`
.withEnvironment({
YARN_NPM_AUTH_TOKEN: dryRun ? '' : getNpmToken(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid masking token lookup failures as publish failures

When the AWS SSM lookup in getNpmToken() fails before yarn ... npm publish is even started (for example missing AWS credentials or an SSM outage), this try block now rethrows it as "NPM publish failed" and tells operators to renew the npm token. That is misleading for failures that happen while fetching the token rather than during npm publish; keep the token lookup outside this publish-failure wrapper or only wrap the command execution so the new help message is limited to actual publish failures.

Useful? React with 馃憤聽/ 馃憥.

BUILD_MODE: 'release',
})
.withLogs()
.run()
} catch (error) {
throw new Error('NPM publish failed. Run `node ./scripts/release/renew-token.ts` and retry the job.', {
cause: error as Error,
})
.withLogs()
.run()
})
}
}
Loading