Select the types of activity you want to include in your feed.
[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
···11# up-action
2233-This is a GitHub action to test your project against the latest (nightly) release of Nuxt.
33+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.
44+55+## Usage
66+77+```yaml
88+name: release
99+1010+on:
1111+ push:
1212+ branches: [main]
1313+ pull_request_target:
1414+ types: [closed]
1515+ branches: [main]
1616+ # Required: `release` mode chains into `publish` via `gh workflow run`, which
1717+ # fires `workflow_dispatch`. Also serves as the manual publish-recovery entry point.
1818+ workflow_dispatch:
1919+2020+permissions: {}
2121+2222+jobs:
2323+ # `pr` mode: parse commits since the last tag, push a `release/vX.Y.Z`
2424+ # branch, open or update a draft release PR, and close any superseded
2525+ # release PRs (e.g. `release/v1.0.1` when the bump is now `release/v1.1.0`).
2626+ pr:
2727+ if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
2828+ runs-on: ubuntu-latest
2929+ permissions:
3030+ contents: write # push the `release/vX.Y.Z` branch and delete superseded ones
3131+ pull-requests: write # create a release PR, update its body, close superseded PRs
3232+ steps:
3333+ - uses: danielroe/up-action@v1
3434+ with:
3535+ token: ${{ secrets.GITHUB_TOKEN }}
43655-> [!WARNING]
66-> This action is currently under development and may not behave as expected.
3737+ # `release` mode: the release PR was merged. Tag the squash commit, cut a
3838+ # GitHub release from the PR body, dispatch the publish workflow. The
3939+ # `release/v` head-ref guard is what keeps regular feature-PR merges from
4040+ # triggering a tag attempt.
4141+ release:
4242+ if: |
4343+ github.event_name == 'pull_request_target'
4444+ && github.event.pull_request.merged == true
4545+ && startsWith(github.event.pull_request.head.ref, 'release/v')
4646+ runs-on: ubuntu-latest
4747+ permissions:
4848+ contents: write # push the `vX.Y.Z` tag and create the GitHub release
4949+ actions: write # `gh workflow run release.yml --ref vX.Y.Z` chained dispatch
5050+ steps:
5151+ - uses: danielroe/up-action@v1
5252+ with:
5353+ token: ${{ secrets.GITHUB_TOKEN }}
5454+5555+ # `publish` mode: the chained dispatch from `release` lands here as a
5656+ # `workflow_dispatch` event on a `vX.Y.Z` tag ref. Manual recovery uses
5757+ # the same path (Run workflow -> pick a `v*` tag).
5858+ publish:
5959+ if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
6060+ runs-on: ubuntu-latest
6161+ permissions:
6262+ contents: read # checkout the tag
6363+ id-token: write # OIDC claim for npm trusted publisher
6464+ environment: npm # matches the trusted-publisher entry on npmjs.com
6565+ steps:
6666+ - uses: danielroe/up-action@v1
6767+ with:
6868+ mode: publish
6969+```
7070+7171+### Is `pull_request_target` safe here?
7272+7373+`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.
7474+7575+The `release` job avoids that pattern. Concretely:
7676+7777+- 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`.
7878+- 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.
7979+- 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.
8080+- 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.
8181+8282+In short: the only attacker-controlled input that reaches a subprocess is the semver-validated package version, passed argv-not-shell.
8383+8484+## What it does
8585+8686+- **`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.
8787+- **`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.
8888+- **`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.
8989+9090+Mode is auto-detected from `github.event_name` by default; set `mode:` explicitly to override.
9191+9292+## Inputs
9393+9494+| Input | Default | Description |
9595+| --- | --- | --- |
9696+| `mode` | `auto` | `auto`, `pr`, `release`, or `publish`. |
9797+| `token` | `${{ github.token }}` | Required for `release`; recommended for `pr`. Not used by `publish`. |
9898+| `base-branch` | default branch | Base branch for the release PR. |
9999+| `node-version` | `24` | Node version used for the scripts and for `publish`. Needs to support `--experimental-strip-types` (Node 22.6+, 24+ recommended). |
100100+| `npm-access` | `public` | npm access level (`public` or `restricted`). |
101101+| `publish-workflow` | `release.yml` | Workflow filename to dispatch after tagging. Must declare `workflow_dispatch`. |
102102+| `checkout` | `true` | Set to `false` if the caller has already checked out the right ref. |
103103+104104+## Prerequisites
105105+106106+For `publish` to work end to end you need:
107107+108108+- An npmjs.com trusted-publisher entry per package, pointing at the caller's `release.yml` and the `npm` environment, with the `npm stage publish` permission chip.
109109+- A GitHub environment named `npm` (or whichever name you put on the publish job).
110110+- The package must already exist on npmjs.com; `npm stage publish` cannot stage a brand-new package.
111111+112112+## License
113113+114114+[MIT](./LICENSE)