Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/danielroe/uppt. A composite GitHub Action that turns conventional commits into a draft release PR, tags the PR on merge, and stages publishing to npm via OIDC trusted publishing.
github-action npm npmjs oidc staged-publish trusted-publishing
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251import { mkdtempSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { resolve } from 'node:path'import process from 'node:process'import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const execFileSync = vi.hoisted(() => vi.fn())const spawnSync = vi.hoisted(() => vi.fn())vi.mock('node:child_process', () => ({ execFileSync, spawnSync }))
const { main, distTag, versionFromTarballName } = await import('../scripts/publish.ts')
function fixture (files: string[]): string { const dir = mkdtempSync(resolve(tmpdir(), 'uppt-publish-')) for (const file of files) writeFileSync(resolve(dir, file), 'x') return dir}
function npmArgs () { return [...execFileSync.mock.calls, ...spawnSync.mock.calls].map(([, args]) => args as string[])}
let env: NodeJS.ProcessEnv
beforeEach(() => { env = { ...process.env } for (const key of ['NPM_ACCESS', 'NPM_TAG', 'TARBALL_DIR', 'TARBALL_FILES', 'RELEASES', 'NIGHTLY']) delete process.env[key] vi.spyOn(console, 'log').mockImplementation(() => {}) spawnSync.mockReturnValue({ status: 0, stderr: '' })})
afterEach(() => { process.env = env execFileSync.mockReset() spawnSync.mockReset() vi.restoreAllMocks()})
describe('publish', () => { it('requires TARBALL_DIR', () => { expect(() => main()).toThrow('TARBALL_DIR is required') })
it('rejects a missing TARBALL_DIR', () => { process.env.TARBALL_DIR = resolve(tmpdir(), 'uppt-does-not-exist') expect(() => main()).toThrow(/TARBALL_DIR does not exist/) })
it('stages every tarball found in the directory, sorted', () => { process.env.TARBALL_DIR = fixture(['b-1.0.0.tgz', 'a-1.0.0.tgz', 'notes.txt']) main() expect(npmArgs()).toEqual([ ['stage', 'publish', resolve(process.env.TARBALL_DIR!, 'a-1.0.0.tgz'), '--provenance', '--ignore-scripts', '--access=public', '--tag=latest'], ['stage', 'publish', resolve(process.env.TARBALL_DIR!, 'b-1.0.0.tgz'), '--provenance', '--ignore-scripts', '--access=public', '--tag=latest'], ]) })
it('honours NPM_ACCESS=restricted', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.NPM_ACCESS = 'restricted' main() expect(npmArgs()[0]).toContain('--access=restricted') })
it('falls back to public for an unknown NPM_ACCESS', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.NPM_ACCESS = 'nonsense' main() expect(npmArgs()[0]).toContain('--access=public') })
it('throws when the directory holds no tarballs', () => { process.env.TARBALL_DIR = fixture(['notes.txt']) expect(() => main()).toThrow(/No \*\.tgz found in/) })
it('publishes TARBALL_FILES in the given order', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz', 'b-1.0.0.tgz']) process.env.TARBALL_FILES = '["b-1.0.0.tgz","a-1.0.0.tgz"]' main() expect(npmArgs().map(args => args[2])).toEqual([ resolve(process.env.TARBALL_DIR!, 'b-1.0.0.tgz'), resolve(process.env.TARBALL_DIR!, 'a-1.0.0.tgz'), ]) })
it('rejects malformed TARBALL_FILES', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.TARBALL_FILES = '{' expect(() => main()).toThrow(/TARBALL_FILES is not valid JSON/) })
it('rejects a non-array TARBALL_FILES', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.TARBALL_FILES = '{"a":1}' expect(() => main()).toThrow('TARBALL_FILES must be a JSON array of filenames') })
it('rejects a TARBALL_FILES entry that is not a tarball', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.TARBALL_FILES = '["a-1.0.0.zip"]' expect(() => main()).toThrow(/non-tarball entry/) })
it('rejects an empty TARBALL_FILES array', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.TARBALL_FILES = '[]' expect(() => main()).toThrow('TARBALL_FILES was provided but is empty') })
it('derives tarball names from RELEASES when TARBALL_FILES is absent', () => { process.env.TARBALL_DIR = fixture(['nuxt-kit-5.0.0.tgz', 'stray-9.0.0.tgz']) process.env.RELEASES = JSON.stringify([{ name: '@nuxt/kit', version: '5.0.0', dir: 'packages/kit' }]) main() expect(npmArgs().map(args => args[2])).toEqual([ resolve(process.env.TARBALL_DIR!, 'nuxt-kit-5.0.0.tgz'), ]) })
it('throws when a named tarball is missing', () => { process.env.TARBALL_DIR = fixture([]) process.env.TARBALL_FILES = '["a-1.0.0.tgz"]' expect(() => main()).toThrow(/Tarball 'a-1\.0\.0\.tgz' is not present/) })
it('publishes nightlies directly to latest', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz']) main() expect(npmArgs()[0]).toEqual(['publish', resolve(process.env.TARBALL_DIR, 'a-nightly-1.0.1-2605140905-0123456.tgz'), '--provenance', '--ignore-scripts', '--access=public', '--tag=latest']) })
it('publishes nightlies to an overridden dist-tag', () => { process.env.NIGHTLY = 'true' process.env.NPM_TAG = '5x' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz']) main() expect(npmArgs()[0]).toContain('--tag=5x') })
it('skips a nightly that npm rejects with E403', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz', 'b-nightly-1.0.1-2605140905-0123456.tgz']) spawnSync.mockReturnValueOnce({ status: 1, stderr: 'npm error code E403\n' }) const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) const log = vi.spyOn(console, 'log').mockImplementation(() => {}) main() expect(npmArgs()).toHaveLength(2) expect(stderr).toHaveBeenCalledWith('npm error code E403\n') expect(log).toHaveBeenCalledWith(expect.stringContaining('::warning::npm publish of')) })
it('throws on other nightly publish failures', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz']) spawnSync.mockReturnValueOnce({ status: 1, stderr: 'npm error code E404\n' }) vi.spyOn(process.stderr, 'write').mockImplementation(() => true) expect(() => main()).toThrow(/Command failed: npm publish .* \(exit 1\)/) })
it('throws when a nightly publish is killed', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz']) spawnSync.mockReturnValueOnce({ status: null, signal: 'SIGTERM', stderr: '' }) expect(() => main()).toThrow(/\(exit SIGTERM\)/) })
it('throws when npm cannot be spawned', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture(['a-nightly-1.0.1-2605140905-0123456.tgz']) spawnSync.mockReturnValueOnce({ status: null, error: new Error('spawn npm ENOENT') }) expect(() => main()).toThrow('spawn npm ENOENT') })
it('refuses to combine nightlies with RELEASES', () => { process.env.NIGHTLY = 'true' process.env.TARBALL_DIR = fixture([]) process.env.RELEASES = JSON.stringify([{ name: 'a', version: '1.0.0', dir: '.' }]) expect(() => main()).toThrow(/RELEASES cannot be combined with NIGHTLY/) })
it('stages prereleases under the prerelease identifier', () => { process.env.TARBALL_DIR = fixture(['a-5.0.0-beta.0.tgz']) main() expect(npmArgs()[0]).toContain('--tag=beta') })
it('refuses to guess a tag for a tarball with no version in its name', () => { process.env.TARBALL_DIR = fixture(['bundle.tgz']) expect(() => main()).toThrow(/Could not parse a version out of tarball 'bundle\.tgz'/) })
it('accepts an unversioned tarball when NPM_TAG is set', () => { process.env.TARBALL_DIR = fixture(['bundle.tgz']) process.env.NPM_TAG = 'next' main() expect(npmArgs()[0]).toContain('--tag=next') })
it('derives a tag per tarball in independent mode', () => { process.env.TARBALL_DIR = fixture(['nuxt-kit-5.0.0-rc.1.tgz', 'nuxt-schema-4.1.0.tgz']) process.env.RELEASES = JSON.stringify([ { name: '@nuxt/kit', version: '5.0.0-rc.1', dir: 'packages/kit' }, { name: '@nuxt/schema', version: '4.1.0', dir: 'packages/schema' }, ]) main() expect(npmArgs().map(args => args.at(-1))).toEqual(['--tag=rc', '--tag=latest']) })
it('honours NPM_TAG for every tarball', () => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz', 'b-5.0.0-beta.0.tgz']) process.env.NPM_TAG = 'legacy' main() expect(npmArgs().map(args => args.at(-1))).toEqual(['--tag=legacy', '--tag=legacy']) })
it.each(['-bad', 'has space', '1.2.3'])('rejects an invalid NPM_TAG (%s)', (tag) => { process.env.TARBALL_DIR = fixture(['a-1.0.0.tgz']) process.env.NPM_TAG = tag expect(() => main()).toThrow(/NPM_TAG is not a valid dist-tag/) })})
describe('distTag', () => { it.each([ ['1.2.3', 'latest'], ['5.0.0-beta.0', 'beta'], ['5.0.0-rc.1', 'rc'], ['5.0.0-0', 'next'], ['5.0.0-alpha', 'alpha'], ['5.0.0-alpha.beta.0', 'alpha.beta'], ['5.0.0-pre-view.0', 'pre-view'], ['5.0.0-1.2', 'next'], ['5.0.0-beta.0+build.1', 'beta'], ['1.2.3+build.1', 'latest'], ])('maps %s to %s', (version, tag) => { expect(distTag(version)).toBe(tag) })})
describe('versionFromTarballName', () => { it('extracts the version', () => { expect(versionFromTarballName('nuxt-kit-5.0.0-beta.0.tgz')).toBe('5.0.0-beta.0') expect(versionFromTarballName('a-1.0.0.tgz')).toBe('1.0.0') })
it('returns null for an unversioned filename', () => { expect(versionFromTarballName('bundle.tgz')).toBe(null) })})