Publishing
This guide covers everything needed to publish scaldys-project to PyPI
using GitHub Actions with OIDC Trusted Publishing and deploy project documentation
to GitHub Pages — no API tokens or secrets to manage.
Overview
Releases are triggered by pushing a version tag (e.g. v1.0.0) to GitHub.
The release.yml workflow then builds the distributions with uv build
and publishes them to PyPI via uv publish --trusted-publishing always.
In parallel, the docs.yml workflow builds the Sphinx manual and deploys
the updated documentation site to GitHub Pages (https://scaldys.github.io/scaldys-project/).
CI (ci.yml) runs on every push and pull request: it lints, type-checks,
tests across Ubuntu / macOS / Windows, and verifies the build. A release
should only be triggered once CI is green.
Why automated publishing?
scaldys-project is a pure-Python, open-source build tool. There are no
compiled extensions and no source-code protection concerns: the package
is distributed intentionally as a source-transparent wheel
(py3-none-any).
This makes fully automated CI publishing the right choice:
uv buildon a Linux runner produces the correct and complete artifact.OIDC Trusted Publishing eliminates the need to store any API token as a CI secret: PyPI verifies the identity of the GitHub Actions workflow directly.
Every release goes through the same reproducible pipeline, independent of any developer’s local machine state.
This design is intentionally different from projects derived from the
scaldys-template template, which use Cython-compiled extensions. Those
projects build their binary wheels locally (scaldys-project build all)
and publish manually (scaldys-project publish) to avoid uploading a
pure-Python source distribution that would expose protected implementation
code. scaldys-project itself has no such constraint.
Prerequisites
A PyPI account with 2FA enabled (now required by PyPI for publishing)
The GitHub repository must exist and have GitHub Actions enabled
Step 1 — PyPI Trusted Publishing
Trusted Publishing lets PyPI verify the identity of a GitHub Actions workflow using OIDC tokens. No API token or secret is stored in the repository.
Log in to pypi.org and go to Your account → Publishing (or visit
https://pypi.org/manage/account/publishing/directly).Under Add a new pending publisher, fill in:
Field
Value
PyPI project name
scaldys-projectOwner
scaldysRepository name
scaldys-projectWorkflow name
release.ymlEnvironment name
releaseClick Add. PyPI will accept tokens from that exact workflow/environment combination and no other source.
Note
Use testpypi (test.pypi.org) to do a dry run first. Set up a
separate pending publisher on Test PyPI with the same fields, and
temporarily point the workflow at --index testpypi. See
TestPyPI Dry Run below.
Step 2 — GitHub Environment
The workflow declares environment: release, which must exist in the
repository settings before the workflow can publish.
In the GitHub repository, go to Settings → Environments → New environment.
Name it
release.Optionally add a required reviewer (deployment protection rule) so that a human must approve each publish job before it runs.
Save.
The id-token: write permission in the workflow is what allows GitHub to
mint the OIDC token that PyPI verifies.
Step 3 — GitHub Pages Configuration
GitHub Pages hosts the public Sphinx documentation.
In the GitHub repository, go to Settings → Pages.
Under Build and deployment → Source, select GitHub Actions.
Save.
The docs.yml workflow automatically deploys the built documentation to
GitHub Pages (https://scaldys.github.io/scaldys-project/) on release tags
using native GitHub Pages deployment actions.
Step 4 — Workflow Files
The workflow files are located in the repository under .github/workflows/.
ci.yml
Runs on every push and pull request:
.github/workflows/ci.yml
Jobs:
code_quality —
ruff check,ruff format --diff,mdformat,pyright,sphinx-linttest —
pytest --covon Ubuntu, macOS, and Windowsbuild —
uv build(verifies the package can be built cleanly)
release.yml
Runs when a v* tag is pushed:
.github/workflows/release.yml
on:
push:
tags:
- v*
jobs:
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
environment:
name: release
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Build the project
run: uv build --index-strategy unsafe-best-match
- name: Publish
run: uv publish --trusted-publishing always
docs.yml
Runs when a v* tag is pushed (or triggered manually via workflow_dispatch):
.github/workflows/docs.yml
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
deploy-docs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Install documentation dependencies
run: uv sync --only-group dev
- name: Build Sphinx HTML documentation
run: uv run sphinx-build -b html docs/manual/source build/manual/html
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: build/manual/html
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Step 5 — Version Bump
Version is declared once in pyproject.toml:
[project]
version = "1.0.0"
It is read at runtime via importlib.metadata in
src/scaldys_project/__about__.py.
Before tagging a release:
Edit
pyproject.tomland bumpversion.Update
CHANGELOGwith the release notes.Commit both changes:
git commit -am "Release v1.0.0"
Step 6 — Trigger a Release
Push a version tag to kick off both the release.yml and docs.yml workflows:
git tag v1.0.0
git push origin v1.0.0
GitHub Actions will:
Check out the repository at the tagged commit.
Build and publish package distributions to PyPI via
release.yml.Compile the Sphinx manual and deploy the updated static site to GitHub Pages (
https://scaldys.github.io/scaldys-project/) viadocs.yml.
The package will appear on https://pypi.org/project/scaldys-project/
and the updated documentation will be live on GitHub Pages within a minute or two.
TestPyPI Dry Run
Before the first real release it is a good idea to verify the full pipeline against Test PyPI.
Create an account on
test.pypi.organd add a pending publisher there with the same fields as in Step 1 but pointing attest.pypi.org.Temporarily change the publish step in
release.yml:run: uv publish --index testpypi --trusted-publishing always
Push a pre-release tag:
git tag v1.0.0rc1 git push origin v1.0.0rc1
Confirm the package appears on
https://test.pypi.org/project/scaldys-project/.Revert the workflow change before the real release.