Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
91 changes: 91 additions & 0 deletions scripts/deploy/publish-npm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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
}
)
})

it('should not mask npm token lookup failures as publish failures', () => {
const tokenError = new Error('failed to fetch npm token')
getNpmTokenMock.mock.mockImplementation(() => {
throw tokenError
})

assert.throws(
() => main([]),
(error: Error) => {
assert.equal(error, tokenError)
assert.doesNotMatch(error.message, /scripts\/release\/renew-token\.ts/)
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')
}
29 changes: 21 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,19 @@ 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',
const npmToken = dryRun ? '' : getNpmToken()

try {
command`yarn workspaces foreach --verbose --all --topological --no-private npm publish --tolerate-republish --access public ${dryRun ? ['--dry-run'] : []}`
.withEnvironment({
YARN_NPM_AUTH_TOKEN: npmToken,
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