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