[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
github-action
npm
npmjs
oidc
staged-publish
trusted-publishing
1// Stage-publish prebuilt tarball(s) to npm using OIDC trusted
2// publishing. The maintainer approves the staged version with 2FA on
3// npmjs.com afterwards.
4//
5// The tarball(s) were produced by `uppt/pack` in an earlier job in the
6// same workflow run and downloaded into `TARBALL_DIR` by
7// `actions/download-artifact`. When `TARBALL_FILES` is set (a JSON
8// array of filenames, emitted by `uppt/pack` as a step output), we
9// publish exactly those, in order. Otherwise we fall back to scanning
10// `TARBALL_DIR` for `*.tgz`.
11//
12// `npm publish <tarball>` doesn't run lifecycle scripts in any case
13// (the tarball is treated as an opaque artifact), but we still pass
14// `--ignore-scripts` for clarity.
15//
16// Env:
17// NPM_ACCESS `public` (default) or `restricted`
18// TARBALL_DIR directory holding the prebuilt `*.tgz` files
19// TARBALL_FILES optional JSON array of filenames within TARBALL_DIR
20
21import process from 'node:process'
22import { execFileSync } from 'node:child_process'
23import { existsSync, readdirSync } from 'node:fs'
24import { resolve } from 'node:path'
25
26function run (cmd: string, args: string[]) {
27 console.log('$', cmd, ...args)
28 execFileSync(cmd, args, { stdio: 'inherit' })
29}
30
31function parseTarballFiles (raw: string): string[] {
32 let parsed: unknown
33 try {
34 parsed = JSON.parse(raw)
35 }
36 catch (err) {
37 throw new Error(`TARBALL_FILES is not valid JSON: ${(err as Error).message}`)
38 }
39 if (!Array.isArray(parsed)) {
40 throw new Error('TARBALL_FILES must be a JSON array of filenames')
41 }
42 for (const entry of parsed) {
43 if (typeof entry !== 'string' || !entry.endsWith('.tgz')) {
44 throw new Error(`TARBALL_FILES contains a non-tarball entry: ${JSON.stringify(entry)}`)
45 }
46 }
47 return parsed as string[]
48}
49
50function main () {
51 const access = process.env.NPM_ACCESS === 'restricted' ? 'restricted' : 'public'
52
53 const dir = process.env.TARBALL_DIR
54 if (!dir) throw new Error('TARBALL_DIR is required')
55 if (!existsSync(dir)) throw new Error(`TARBALL_DIR does not exist: ${dir}`)
56
57 const filesEnv = process.env.TARBALL_FILES?.trim()
58 let tarballs: string[]
59 if (filesEnv) {
60 tarballs = parseTarballFiles(filesEnv)
61 if (!tarballs.length) {
62 throw new Error('TARBALL_FILES was provided but is empty')
63 }
64 }
65 else {
66 tarballs = readdirSync(dir).filter(f => f.endsWith('.tgz')).sort()
67 if (!tarballs.length) {
68 throw new Error(`No *.tgz found in ${dir}. Did the pack job upload the artifact?`)
69 }
70 }
71
72 for (const tarball of tarballs) {
73 const tarballPath = resolve(dir, tarball)
74 if (!existsSync(tarballPath)) {
75 throw new Error(`Tarball '${tarball}' is not present in ${dir}`)
76 }
77 run('npm', ['stage', 'publish', tarballPath, '--provenance', '--ignore-scripts', `--access=${access}`])
78 }
79}
80
81try {
82 main()
83}
84catch (err) {
85 console.error(err)
86 process.exit(1)
87}