---
# System prepended metadata

title: Automating Python Package Releases to PyPI with GitHub Actions

---

# Automating Python Package Releases to PyPI with GitHub Actions

Automating the release process of your Python packages to the Python Package Index (PyPI) can save you time and help ensure a consistent and reliable release process. In this guide, we'll walk you through the steps to automate the release of a Python package using GitHub Actions. We'll cover everything from setting up GitHub Actions and PyPI to configuring tokens and creating release workflows.

## Prerequisites

Before you begin, make sure you have the following prerequisites in place:

- A Python package that you want to release.
- A GitHub repository for your package.
- A PyPI account (https://pypi.org/account/register/).

## Step 1: Configure GitHub Secrets

To securely store and use sensitive information like API tokens, we'll configure GitHub Secrets. Secrets are encrypted environment variables that can be accessed in your GitHub Actions workflows.

1. Navigate to your GitHub repository.

2. Click on the "Settings" tab.

3. In the left sidebar, click on "Secrets."

4. Click the "New repository secret" button.

5. Create two secrets:
   - `GITHUB_TOKEN`: Your GitHub Personal Access Token with the necessary permissions to create releases.
   - `PYPI_TOKEN`: Your PyPI API token for uploading packages.

## Step 2: Create a Release Workflow

In your repository, create a new file named `release.yaml` in the `.github/workflows` directory. This YAML file will define your GitHub Actions workflow for automated releases.

```yaml
name: Release

on:
  workflow_dispatch:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash -l {0}

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Miniconda
      uses: conda-incubator/setup-miniconda@v2
      with:
        miniconda-version: "latest"
        mamba-version: "*"
        environment-file: conda/dev.yaml
        channels: conda-forge,nodefaults
        activate-environment: yourpackage
        use-mamba: true
        miniforge-variant: Mambaforge

    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 18

    - name: Test release
      if: ${{ github.event_name != 'workflow_dispatch' }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: make release-dry

    - name: Release
      if: ${{ github.event_name == 'workflow_dispatch' }}
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
      run: |
        poetry config pypi-token.pypi ${PYPI_TOKEN}
        make release
```

This workflow defines the following steps:

- It runs on every push to the `main` branch, pull requests to the `main` branch, and manually triggered workflow dispatches.
- It sets up the environment, including Miniconda and Node.js, as required by your package.
- It tests the release (dry run) when triggered by a push.
- It performs the actual release when triggered manually.

## Step 3: Configure Semantic Release

Semantic Release is a tool that automates versioning and package publishing based on commit messages. You'll need to configure Semantic Release in your project for automated versioning.

### `.release.json`

Create a `.release.json` file in your project root to configure Semantic Release. This file defines the release process, including how versions are determined and what actions are taken during the release.

```json
{
  "branches": ["main"],
  "tagFormat": "${version}",
  "plugins": [
    [
      "@semantic-release/commit-analyzer",
      {
        "preset": "conventionalcommits"
      }
    ],
    [
      "semantic-release-replace-plugin",
      {
        "replacements": [
          {
            "files": ["yourpackage/__init__.py"],
            "from": "return \".*\"  # changed by semantic-release",
            "to": "return \"${nextRelease.version}\"  # changed by semantic-release",
            "results": [
              {
                "file": "yourpackage/__init__.py",
                "hasChanged": true,
                "numMatches": 1,
                "numReplacements": 1
              }
            ],
            "countMatches": true
          },
          {
            "files": ["pyproject.toml"],
            "from": "version = \".*\"  # changed by semantic-release",
            "to": "version = \"${nextRelease.version}\"  # changed by semantic-release",
            "results": [
              {
                "file": "pyproject.toml",
                "hasChanged": true,
                "numMatches": 1,
                "numReplacements": 1
              }
            ],
            "countMatches": true
          }
        ]
      }
    ],
    [
      "@semantic-release/release-notes-generator",
      {
        "preset": "conventionalcommits"
      }
    ],
    [
      "@semantic-release/changelog",
      {
        "changelogTitle": "Release Notes\n---",
        "changelogFile": "CHANGELOG.md"
      }
    ],
    [
      "@semantic-release/exec",
      {
        "prepareCmd": "poetry build",
        "publishCmd": "poetry publish"
      }
    ],
    [
      "@semantic-release/github",
      {
        "assets": ["dist/*.whl", "dist/*.tar.gz"]
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["pyproject.toml", "CHANGELOG.md", "yourpackage/__init__.py"],
        "message": "chore(release): ${nextRelease.version}"
      }
    ]
  ]
}
```

This configuration file defines the release process, including how version numbers are determined, how the changelog is generated, and what commands are executed during the release.

## Step 4: Makefile and Versioning

Your `Makefile` plays a crucial role in managing the release process. Here's an example of a `Makefile` with key release-related targets:

```make
PYTHON = poetry run python
SEMANTIC_RELEASE = npx --yes \
  -p semantic-release \
  -p conventional-changelog-conventionalcommits \
  -p "@semantic-release/commit-analyzer" \
  -p "@semantic-release/release-notes-generator" \
  -p "@semantic-release/changelog" \
  -p "@semantic-release/exec" \
  -p "@semantic-release/github" \
  -p "semantic-release-replace-plugin" \
  semantic-release

...
# ... (other targets)

.PHONY: release
release: ## Make release
	$(SEMANTIC_RELEASE) --ci

.PHONY: release-dry
release-dry: ## Test make release
	$(SEMANTIC_RELEASE) --dry-run
```

The `release`

 and `release-dry` targets run Semantic Release with the `--ci` and `--dry-run` flags, respectively.

## Step 5: Create a PyPI Package

Before you can automate package releases, ensure that you've created a Python package, possibly using a tool like Poetry or setuptools. You should have a `pyproject.toml` or `setup.py` file that defines your package's metadata.

## Step 6: Create a Manual Release

To trigger the initial release and tag your repository with the first version, you need to create a manual release in your GitHub repository. Follow these steps:

1. In your GitHub repository, go to the "Releases" section.

2. Click the "Draft a new release" button.

3. Fill in the tag version (e.g., `v1.0.0`).

4. Provide a title and description for the release.

5. Attach any release assets if needed (e.g., wheels, source distributions).

6. Click the "Publish release" button.

This manual release will establish the initial version and tag in your repository.

## Conclusion

Automating the release process of your Python packages to PyPI using GitHub Actions and Semantic Release can save you time and ensure consistency in your release workflow. By configuring GitHub Secrets, creating a release workflow, setting up Semantic Release, and creating a PyPI package, you can streamline the process of publishing new versions of your package to PyPI with confidence.