Skip to content

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:

  1. 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.
  2. Builds the package.
  3. 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:

on:
  push:
    tags:
      - "release/[0-9]+.[0-9]+.[0-9]+*"

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

  1. Go to your PyPI project → ManagePublishing.
  2. 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.yml
    Environment name pypi (optional but recommended)
  3. Click Add.

One-time GitHub setup

In your repository go to SettingsEnvironments 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

  1. Log in to npmjs.com and go to Access Tokens.
  2. Generate a new Automation token (scoped to publish).
  3. Copy the token.

One-time GitHub setup

In your repository go to SettingsSecrets and variablesActions 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 }}

End-to-end flow

# 1. Bump the version in package.json
npm version 1.3.0 --no-git-tag-version

# 2. Commit the version bump
git add package.json
git commit -m "bump version to 1.3.0"

# 3. Push the tag — triggers the workflow
tag-sync publish