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

docs: update readme

+34 -17
+34 -17
README.md
··· 1 1 # uppt 2 2 3 - 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. 3 + > 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. 4 + 5 + The aim of **uppt** is to make a very simple, secure release workflow for maintainers which adheres to best security practices and doesn't require tokens or trusting a third-party GitHub App. It was extracted from scripts used in [nuxt/nuxt](https://github.com/nuxt/nuxt). 4 6 5 7 ## Usage 6 8 ··· 13 15 pull_request_target: 14 16 types: [closed] 15 17 branches: [main] 16 - # Required: `release` mode chains into `publish` via `gh workflow run`, which 17 - # fires `workflow_dispatch`. Also serves as the manual publish-recovery entry point. 18 + # this is required to trigger releases when the release PR is merged, or to rerun a release if needed 18 19 workflow_dispatch: 19 20 20 21 permissions: {} ··· 61 62 permissions: 62 63 contents: read # checkout the tag 63 64 id-token: write # OIDC claim for npm trusted publisher 64 - environment: npm # matches the trusted-publisher entry on npmjs.com 65 + environment: npm # must match the trusted-publisher entry on npmjs.com 65 66 steps: 66 67 - uses: danielroe/uppt@v1 67 68 with: ··· 70 71 71 72 ### Is `pull_request_target` safe here? 72 73 73 - `pull_request_target` is the well-known footgun: it runs in the target branch's context, with write permissions and access to secrets, and the classic exploit is checking out the PR head and running build or test scripts from attacker-controlled code. 74 + `pull_request_target` is a well-known footgun, but is used safely in this action: 75 + 76 + - It checks out the squash commit on the default branch, not the PR. 77 + - It does not install dependencies or run anything from the codebase being released. 78 + - It reads a single value from the codebase - `package.json#version` - which is validated against a strict semver regex. 79 + - All subprocess calls use `execFileSync` with argv arrays, and the generated PR body is passed as an env var and forwarded to `gh release create --notes` as a single arg. 80 + 81 + ## What it does 82 + 83 + ### Creates a PR (`pr`) 74 84 75 - The `release` job avoids that pattern. Concretely: 85 + Whenever you push to the default branch, this action parses conventional commits since the latest semver tag, decides the next bump (major, minor or patch) and creates a `release/vX.Y.Z` branch with the version bump, and opens or updates a draft PR against the base branch. 76 86 77 - - It checks out `github.event.pull_request.merge_commit_sha` (the squash commit on the default branch, created after the maintainer approved and clicked merge). It never checks out `head.sha`. 78 - - It does not run `npm install`, `pnpm install`, `postinstall`, or any build / test scripts from the merged code. The only thing it executes is `node scripts/tag-and-release.ts` from `${{ github.action_path }}`, which is this action's pinned checkout, not the consumer repo's. 79 - - The single value it reads from the merged code is `package.json#version`, and it is validated against a strict semver regex before flowing into `git tag` / `gh` argv. Flag-injection (`--upload-pack=...`) and ref-confusion attacks are blocked at that gate. 80 - - All subprocess calls use `execFileSync` with argv arrays, never `execSync` or shell interpolation. `PR_BODY` is passed as an env var and forwarded to `gh release create --notes` as a single argv, so backtick / `$()` content in a PR body is inert. 87 + > [!TIP] 88 + > You can edit this PR to add your own release notes. Anything above `## 👉 Changelog` is preserved when the changelog is updated. 81 89 82 - In short: the only attacker-controlled input that reaches a subprocess is the semver-validated package version, passed argv-not-shell. 90 + ### Creates a release (`release`) 83 91 84 - ## What it does 92 + When you merge a release PR, the action tags that commit, creates a GitHub Release using the PR body as notes, then dispatches the publish workflow on the new tag. 85 93 86 - - **`pr`** (push to default branch): parses conventional commits since the latest semver tag, decides the next bump (`major` / `minor` / `patch`), pushes a `release/vX.Y.Z` branch with the version bump as `github-actions[bot]`, and opens or updates a draft PR against the base branch. The PR body uses `## 👉 Changelog` as a marker; text above the marker is preserved across updates. If a subsequent commit shifts the target version (e.g. a `feat:` lands after a patch PR was opened), the stale PR is closed, its branch deleted, and its preamble carried into the new PR. Superseded-PR cleanup is scoped to the same base branch, so a repo with maintenance branches (e.g. `main`, `4.x`, `3.x`) can have a release PR open against each one without them clobbering each other. 87 - - **`release`** (`pull_request_target: closed` from a merged PR): reads the version from `package.json` at the merge commit, tags that commit, creates a GitHub Release using the PR body as notes, then dispatches the publish workflow on the new tag. 88 - - **`publish`** (`push: tags: ['v*']`, `workflow_dispatch` on a tag): if `pnpm-lock.yaml` is present, runs `pnpm pack` (so `catalog:` specifiers resolve) then `npm stage publish ./<tarball>.tgz --provenance --access <access>`. Otherwise runs `npm stage publish --provenance --access <access>` from source. Always `npm stage publish`, never `npm publish`. OIDC trusted publishing, no `NPM_TOKEN`. The maintainer approves the staged version with 2FA on npmjs.com afterwards. 94 + ### Stages a publish (`publish`) 89 95 90 - Mode is auto-detected from `github.event_name` by default; set `mode:` explicitly to override. 96 + This runs `pnpm pack` (if you have a `pnpm-lock.yaml`) and then runs `npm stage publish` with OIDC authentication. The staged version then needs to be approved by a maintainer with 2FA on npmjs.com before it goes live. 91 97 92 98 ## Inputs 93 99 ··· 109 115 - A GitHub environment named `npm` (or whichever name you put on the publish job). 110 116 - The package must already exist on npmjs.com; `npm stage publish` cannot stage a brand-new package. 111 117 118 + ## Credits 119 + 120 + Inspired by [unjs/changelogen](https://github.com/unjs/changelogen) and [antfu/changelogithub](https://github.com/antfu/changelogithub/). 121 + 122 + There are also a number of other actions and workflows you might want to check out, including: 123 + 124 + - [changesets](https://github.com/changesets/changesets) 125 + - [release-please](https://github.com/googleapis/release-please) 126 + 112 127 ## License 113 128 114 - [MIT](./LICENSE) 129 + Made with ❤️ 130 + 131 + Published under [MIT License](./LICENCE).