Publishing
This page covers how to set up a GitHub Actions workflow that automatically
publishes your package to a registry whenever a tag-sync tag is pushed.
- Python packages — publish to PyPI with
uv publish - npm packages — publish to npmjs.com with
npm publish
How the workflow fits together
tag-sync publish creates and pushes the git tag. GitHub Actions detects that
push and runs the workflow, which:
- Verifies the pushed tag matches the current version in the manifest using
tag-sync check— a defence-in-depth guard against stale checkouts or accidental tag pushes. - Builds the package.
- Publishes to the registry.
Tag pattern matching
All examples below use v[0-9]+.[0-9]+.[0-9]+* as the trigger pattern. This
matches the default tag-sync pattern for stable releases (v1.2.3) as well
as pre-release suffixes (v1.2.3a1, v1.2.3b2, v1.2.3rc1), while
rejecting arbitrary strings that merely start with v.
If you use a custom tag pattern, adjust the filter accordingly:
Publishing a Python package to PyPI
Authentication: OIDC trusted publishing
The recommended approach is OIDC trusted publishing. PyPI issues a short-lived token directly to the workflow at runtime — no API tokens or secrets need to be stored in GitHub.
One-time PyPI setup
- Go to your PyPI project → Manage → Publishing.
-
Under Add a new publisher, choose GitHub Actions and fill in:
Field Value Owner your GitHub username or org Repository your repo name Workflow filename publish.ymlEnvironment name pypi(optional but recommended) -
Click Add.
One-time GitHub setup
In your repository go to Settings → Environments and create an
environment named pypi. You can add protection rules here — for example,
requiring manual approval from a trusted reviewer before each publish run.
Workflow file
Create .github/workflows/publish.yml:
name: Publish
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/<your-package>/
permissions:
id-token: write # required for OIDC trusted publishing
contents: read
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Verify tag matches package version
run: uvx tag-sync check ${{ github.ref_name }}
- name: Build
run: uv build
- name: Publish
run: uv publish
Running tests before publishing
Gate the publish on a passing test suite with a needs: dependency:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- run: uv sync --locked
- run: uv run pytest
publish:
needs: test
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/<your-package>/
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Verify tag matches package version
run: uvx tag-sync check ${{ github.ref_name }}
- run: uv build
- run: uv publish
End-to-end flow
# 1. Bump the version in pyproject.toml
uv version 1.3.0
# 2. Commit the version bump
git add pyproject.toml uv.lock
git commit -m "bump version to 1.3.0"
# 3. Push the tag — triggers the workflow
tag-sync publish
Publishing an npm package to npmjs.com
Authentication: npm access token
npm does not yet support keyless OIDC publishing, so you need to store an access token as a GitHub Actions secret.
One-time npm setup
- Log in to npmjs.com and go to Access Tokens.
- Generate a new Automation token (scoped to publish).
- Copy the token.
One-time GitHub setup
In your repository go to Settings → Secrets and variables →
Actions and add a secret named NPM_TOKEN with the token value.
Workflow file
Create .github/workflows/publish.yml:
name: Publish
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "lts/*"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: npm ci
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Verify tag matches package version
run: uvx tag-sync check ${{ github.ref_name }}
- name: Publish
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
actions/setup-node writes an .npmrc that injects NODE_AUTH_TOKEN as the
registry auth credential, so no manual .npmrc configuration is needed.
Running tests before publishing
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- run: npm ci
- run: npm test
publish:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
registry-url: "https://registry.npmjs.org"
- uses: astral-sh/setup-uv@v7
- run: npm ci
- name: Verify tag matches package version
run: uvx tag-sync check ${{ github.ref_name }}
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}