GitHub Actions CI/CD (9/10): Release Workflow — Tag, Build, Publish
Summary: Create a release workflow that triggers when you push a version tag. It builds your package, creates a GitHub Release, and publishes to PyPI using Trusted Publishing — no API tokens stored as secrets.
| Key | Value |
|---|---|
| Package name | helloci |
| Working directory | ~/projects/helloci |
| Release workflow | .github/workflows/release.yml |
| Trigger | tag push matching v* |
| Build tool | python -m build |
| PyPI publishing | Trusted Publishing (OIDC) |
0. Prerequisites
- The
hellociproject with CI, branch protection, and artifacts from Parts 1–8 - A PyPI account at pypi.org
- The
ghCLI installed (for creating releases from the command line)
1. CI vs CD
Up to now, everything you have built is Continuous Integration (CI) — automated checks that run on every push and pull request. CI answers: “is the code correct?”
Continuous Deployment (CD) goes further — when certain conditions are met (like a version tag), the pipeline automatically builds and ships the software. CD answers: “is the code delivered?”
| Stage | Trigger | What happens |
|---|---|---|
| CI | Every push / PR | Lint, test, report |
| CD | Version tag (v1.0.0) | Build, release, publish |
This tutorial adds the CD half.
2. Add the Build Tool
Add build to your development dependencies in pyproject.toml.
[project.optional-dependencies]
db = [
"psycopg[binary]>=3.1",
]
test = [
"pytest>=8.0",
"ruff>=0.4",
"psycopg[binary]>=3.1",
]
build = [
"build>=1.0",
]Code language: TOML, also INI (ini)
Install it.
pip install -e ".[build]"Code language: Shell Session (shell)
3. Build the Package Locally
python -m buildCode language: Shell Session (shell)
Successfully built helloci-0.1.0.tar.gz and helloci-0.1.0-py3-none-any.whl
This creates two files in the dist/ directory.
| File | What it is |
|---|---|
helloci-0.1.0.tar.gz | Source distribution (sdist) — the raw source code |
helloci-0.1.0-py3-none-any.whl | Wheel — the pre-built, installable package |
PyPI accepts both. The wheel is what pip install downloads when available because it installs faster.
Tip: Add
dist/to your.gitignoreif it is not already there.
4. Configure Trusted Publishing on PyPI
Trusted Publishing uses OpenID Connect (OIDC) to let GitHub Actions publish to PyPI without storing an API token. PyPI trusts GitHub as an identity provider.
Log in to pypi.org and navigate to Your projects > helloci > Settings > Publishing (or Manage > Settings > Publishing if the project exists).
If the project does not exist yet on PyPI, go to Publishing under your account settings and add a pending publisher.
Fill in the form:
| Field | Value |
|---|---|
| PyPI project name | helloci |
| Owner | <YOUR_GITHUB_USERNAME> |
| Repository | helloci |
| Workflow name | release.yml |
| Environment name | pypi |
Click Add.
This tells PyPI: “trust tokens issued by GitHub Actions from this specific repository and workflow.”
Note: Trusted Publishing is the recommended way to publish to PyPI. It replaces the older method of storing a PyPI API token as a GitHub secret. No credentials are stored anywhere.
5. Create the Release Workflow
Create .github/workflows/release.yml.
name: Release
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build tool
run: pip install build
- name: Build package
run: python -m build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
github-release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: >
gh release create ${{ github.ref_name }}
dist/*
--title "${{ github.ref_name }}"
--generate-notes
publish-pypi:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1Code language: YAML (yaml)
This workflow has three jobs that run in sequence.
6. Understand the Release Workflow
Trigger:
on:
push:
tags:
- "v*"Code language: YAML (yaml)
The workflow only runs when you push a tag that starts with v — like v1.0.0, v0.2.0, or v1.0.0-rc1.
Job 1 — build:
Builds the wheel and sdist, then uploads them as an artifact so the next jobs can use them.
Job 2 — github-release:
needs: build
permissions:
contents: writeCode language: YAML (yaml)
needs: build— waits for the build job to finishpermissions: contents: write— grants the workflow token permission to create releases
The gh release create command creates a GitHub Release with auto-generated release notes and attaches the wheel and sdist files.
Job 3 — publish-pypi:
environment: pypi
permissions:
id-token: writeCode language: YAML (yaml)
environment: pypi— must match the environment name you configured on PyPIpermissions: id-token: write— allows the workflow to request an OIDC tokenpypa/gh-action-pypi-publish@release/v1— the official PyPI publish action
Note: The
id-token: writepermission is what makes Trusted Publishing work. GitHub issues a short-lived OIDC token that PyPI verifies against your Trusted Publishing configuration. No API key is needed.
7. Create the GitHub Environment
Before the workflow can use the pypi environment, you need to create it.
Navigate to Settings > Environments in your repository. Click New environment. Name it pypi. Click Configure environment.
Optionally, add a protection rule:
- Required reviewers — require someone to approve before the publish job runs
This adds a manual approval step to your release pipeline. The build and GitHub Release happen automatically, but publishing to PyPI waits for a human to click “approve.”
Tip: Required reviewers are optional but recommended for public packages. They prevent accidental publishes from typo tags.
8. Tag and Release
Make sure all your changes are committed and pushed to main.
git add .
git commit -m "Add release workflow with Trusted Publishing"
git pushCode language: Shell Session (shell)
Create and push a version tag.
git tag v0.1.0
git push origin v0.1.0Code language: Shell Session (shell)
Open the Actions tab. You see the Release workflow running. It progresses through three jobs.
build ✓
github-release ✓
publish-pypi ✓Code language: Shell Session (shell)
Check your GitHub Releases page — you see a v0.1.0 release with the wheel and sdist attached.
Check PyPI — your package is live at https://pypi.org/project/helloci/.
Warning: Once a version is published to PyPI, it cannot be overwritten. If you need to fix something, bump the version number and tag again.
9. Update the Version for Future Releases
When you are ready for the next release, update the version in pyproject.toml.
version = "0.2.0"Code language: TOML, also INI (ini)
Commit, tag, and push.
git add pyproject.toml
git commit -m "Bump version to 0.2.0"
git push
git tag v0.2.0
git push origin v0.2.0Code language: Shell Session (shell)
The release workflow runs again automatically.
Summary
You created a release workflow that automates the entire publish process: build the package, create a GitHub Release, and publish to PyPI.
- Tag trigger —
v*tags start the release pipeline python -m build— produces a wheel and sdistgh release create— creates a GitHub Release with auto-generated notes- Trusted Publishing — PyPI trusts GitHub’s OIDC tokens, no API key needed
permissions: id-token: write— enables OIDC token generationenvironment: pypi— ties the workflow to your PyPI Trusted Publishing config
You now have CI (lint, test on every PR) and CD (build and publish on every tag). In Part 10 you will optimize the full pipeline — split jobs, add caching, and schedule nightly runs.
GitHub Actions CI/CD — All Parts
- 1 GitHub Actions CI/CD (1/10): Make It a Package You Can Test
- 2 GitHub Actions CI/CD (2/10): Unit Tests — Fast Feedback
- 3 GitHub Actions CI/CD (3/10): Quality Gate Before Tests — Lint and Formatting
- 4 GitHub Actions CI/CD (4/10): Your First CI Workflow — Run on Every PR
- 5 GitHub Actions CI/CD (5/10): Matrix Testing — Multiple Python Versions
- 6 GitHub Actions CI/CD (6/10): Integration Tests — Real Dependencies in CI
- 7 GitHub Actions CI/CD (7/10): Branch Protection — Make CI a Merge Gate
- 8 GitHub Actions CI/CD (8/10): Artifacts — Keep Evidence From a Run
- 9 GitHub Actions CI/CD (9/10): Release Workflow — Tag, Build, Publish You are here
- 10 GitHub Actions CI/CD (10/10): Going Professional — Split Jobs, Caching, and Nightly Builds
