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

11 2 10

Clone this repository

https://git.vm.fail/danielroe.dev/uppt https://git.vm.fail/did:plc:alsvq6fatm4fox4s7fjw7xvi
ssh://git@knot1.tangled.sh:2222/danielroe.dev/uppt ssh://git@knot1.tangled.sh:2222/did:plc:alsvq6fatm4fox4s7fjw7xvi

For self-hosted knots, clone URLs may differ based on your setup.


README.md

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.

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.

Usage#

name: release

on:
  push:
    branches: [main]
  pull_request_target:
    types: [closed]
    branches: [main]
  # this is required to trigger releases when the release PR is merged, or to rerun a release if needed
  workflow_dispatch:

permissions: {}

jobs:
  # `pr` mode: parse commits since the last tag, push a `release/vX.Y.Z`
  # branch, open or update a draft release PR, and close any superseded
  # release PRs (e.g. `release/v1.0.1` when the bump is now `release/v1.1.0`).
  pr:
    if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
    runs-on: ubuntu-latest
    permissions:
      contents: write       # push the `release/vX.Y.Z` branch and delete superseded ones
      pull-requests: write  # create a release PR, update its body, close superseded PRs
    steps:
      - uses: danielroe/uppt@main
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

  # `release` mode: the release PR was merged. Tag the squash commit, cut a
  # GitHub release from the PR body, dispatch the publish workflow. The
  # `release/v` head-ref guard is what keeps regular feature-PR merges from
  # triggering a tag attempt.
  release:
    if: |
      github.event_name == 'pull_request_target'
      && github.event.pull_request.merged == true
      && startsWith(github.event.pull_request.head.ref, 'release/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write       # push the `vX.Y.Z` tag and create the GitHub release
      actions: write        # `gh workflow run release.yml --ref vX.Y.Z` chained dispatch
    steps:
      - uses: danielroe/uppt@main
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

  # `publish` mode: the chained dispatch from `release` lands here as a
  # `workflow_dispatch` event on a `vX.Y.Z` tag ref. Manual recovery uses
  # the same path (Run workflow -> pick a `v*` tag).
  publish:
    if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: read        # checkout the tag
      id-token: write       # OIDC claim for npm trusted publisher
    environment: npm        # must match the trusted-publisher entry on npmjs.com
    steps:
      - uses: danielroe/uppt@main
IMPORTANT

Once you add this workflow, it is strongly recommended to run npx pin-github-action .github/workflows/release.yml to pin the action's version to a SHA.

Is pull_request_target safe here?#

pull_request_target is a well-known footgun, but is used safely in this action:

  • It checks out the squash commit on the default branch, not the PR.
  • It does not install dependencies or run anything from the codebase being released.
  • It reads a single value from the codebase - package.json#version - which is validated against a strict semver regex.
  • 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.

What it does#

Creates a PR (pr)#

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.

TIP

You can edit this PR to add your own release notes. Anything above ## 👉 Changelog is preserved when the changelog is updated.

Creates a release (release)#

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.

Stages a publish (publish)#

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.

Inputs#

Input Default Description
mode auto auto, pr, release, or publish.
token ${{ github.token }} Required for release; recommended for pr. Not used by publish.
base-branch default branch Base branch for the release PR.
node-version 24 Node version used for the scripts and for publish. Needs to support --experimental-strip-types (Node 22.6+, 24+ recommended).
npm-access public npm access level (public or restricted).
publish-workflow release.yml Workflow filename to dispatch after tagging. Must declare workflow_dispatch.
checkout true Set to false if the caller has already checked out the right ref.

Prerequisites#

For publish to work end to end you need:

  • 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.
  • A GitHub environment named npm (or whichever name you put on the publish job).
  • The package must already exist on npmjs.com; npm stage publish cannot stage a brand-new package.

Credits#

Inspired by unjs/changelogen and antfu/changelogithub.

There are also a number of other actions and workflows you might want to check out, including:

License#

Made with ❤️

Published under MIT License.