[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
0

Configure Feed

Select the types of activity you want to include in your feed.

uppt / scripts / tag-and-release.ts
3.6 kB 89 lines
1// Tag the squash-merge commit, create a GitHub release from the PR body, 2// and dispatch the publish workflow. 3// 4// Runs on `pull_request: closed` after the caller's workflow has checked 5// out the merge commit (`ref: github.event.pull_request.merge_commit_sha`). 6// The version is read from `package.json` at that ref, not from the branch name. 7// 8// Env: 9// GITHUB_TOKEN required (tag push, release create, workflow dispatch) 10// GITHUB_REPOSITORY "owner/repo" (set automatically inside Actions) 11// PR_BODY PR body, used verbatim as release notes 12// PUBLISH_WORKFLOW workflow filename to dispatch (default: release.yml) 13// PACKAGES newline-separated list of publishable workspace 14// dirs/globs (monorepo); omit for single-package repos 15 16import process from 'node:process' 17import { execFileSync } from 'node:child_process' 18import { isSemver, resolveCurrentVersion } from './_workspaces.ts' 19 20function run (cmd: string, args: string[], opts: { env?: NodeJS.ProcessEnv } = {}) { 21 execFileSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ...opts.env } }) 22} 23 24function capture (cmd: string, args: string[], env?: NodeJS.ProcessEnv): string { 25 return execFileSync(cmd, args, { encoding: 'utf8', env: { ...process.env, ...env } }).trim() 26} 27 28function tagExists (repo: string, tag: string, env: NodeJS.ProcessEnv): boolean { 29 // `gh api` exits non-zero on 404; treat that as "does not exist". Any other 30 // failure (auth, network) we want to propagate, so we re-check by asking gh 31 // to ignore HTTP errors and inspect the JSON. 32 try { 33 const out = execFileSync( 34 'gh', 35 ['api', '-H', 'Accept: application/vnd.github+json', `/repos/${repo}/git/ref/tags/${tag}`], 36 { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } }, 37 ) 38 return Boolean(out.trim()) 39 } catch { 40 return false 41 } 42} 43 44function main () { 45 const token = process.env.GITHUB_TOKEN 46 if (!token) throw new Error('GITHUB_TOKEN is required') 47 const repo = process.env.GITHUB_REPOSITORY 48 if (!repo || !repo.includes('/')) throw new Error('GITHUB_REPOSITORY is required') 49 50 const version = resolveCurrentVersion(process.cwd(), process.env.PACKAGES?.trim() ?? '') 51 // `version` flows into ref names and `gh` argv. Pin to strict semver to 52 // rule out flag-injection (`--upload-pack=...`) and ref-confusion attacks. 53 if (!isSemver(version)) { 54 throw new Error(`Refusing to tag: resolved version "${version}" is not strict semver`) 55 } 56 const tag = `v${version}` 57 const ghEnv = { GH_TOKEN: token } 58 59 if (tagExists(repo, tag, ghEnv)) { 60 throw new Error(`Refusing to tag: ${tag} already exists on ${repo}. If this is a rerun, delete the tag and the release first, or bump the version.`) 61 } 62 63 // Create the tag via the GitHub API instead of `git push`, so this step 64 // doesn't need git-level write credentials baked into the runner. 65 const sha = capture('git', ['rev-parse', 'HEAD']) 66 run('gh', [ 67 'api', '-X', 'POST', 68 '-H', 'Accept: application/vnd.github+json', 69 `/repos/${repo}/git/refs`, 70 '-f', `ref=refs/tags/${tag}`, 71 '-f', `sha=${sha}`, 72 ], { env: ghEnv }) 73 74 const body = process.env.PR_BODY ?? '' 75 run('gh', ['release', 'create', tag, '--title', tag, '--notes', body], { env: ghEnv }) 76 77 const workflow = process.env.PUBLISH_WORKFLOW || 'release.yml' 78 run('gh', ['workflow', 'run', workflow, '--ref', tag], { env: ghEnv }) 79 80 console.log(`Tagged ${tag}, created release, dispatched ${workflow}.`) 81} 82 83try { 84 main() 85} 86catch (err) { 87 console.error(err) 88 process.exit(1) 89}