.. _publishing: *********** 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. .. contents:: On this page :local: :depth: 2 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 build`` on 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. 1. Log in to `pypi.org `_ and go to **Your account → Publishing** (or visit ``https://pypi.org/manage/account/publishing/`` directly). 2. Under **Add a new pending publisher**, fill in: ==================== ======================== Field Value ==================== ======================== PyPI project name ``scaldys-project`` Owner ``scaldys`` Repository name ``scaldys-project`` Workflow name ``release.yml`` Environment name ``release`` ==================== ======================== 3. Click **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 :ref:`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. 1. In the GitHub repository, go to **Settings → Environments → New environment**. 2. Name it ``release``. 3. Optionally add a **required reviewer** (deployment protection rule) so that a human must approve each publish job before it runs. 4. 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. 1. In the GitHub repository, go to **Settings → Pages**. 2. Under **Build and deployment → Source**, select **GitHub Actions**. 3. 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-lint`` - **test** — ``pytest --cov`` on Ubuntu, macOS, and Windows - **build** — ``uv build`` (verifies the package can be built cleanly) release.yml ----------- Runs when a ``v*`` tag is pushed:: .github/workflows/release.yml .. code-block:: yaml 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 .. code-block:: yaml 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: 1. Edit ``pyproject.toml`` and bump ``version``. 2. Update ``CHANGELOG`` with the release notes. 3. 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: 1. Check out the repository at the tagged commit. 2. Build and publish package distributions to PyPI via ``release.yml``. 3. Compile the Sphinx manual and deploy the updated static site to GitHub Pages (``https://scaldys.github.io/scaldys-project/``) via ``docs.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: TestPyPI Dry Run ================ Before the first real release it is a good idea to verify the full pipeline against `Test PyPI `_. 1. Create an account on ``test.pypi.org`` and add a pending publisher there with the same fields as in :ref:`Step 1 ` but pointing at ``test.pypi.org``. 2. Temporarily change the publish step in ``release.yml``:: run: uv publish --index testpypi --trusted-publishing always 3. Push a pre-release tag:: git tag v1.0.0rc1 git push origin v1.0.0rc1 4. Confirm the package appears on ``https://test.pypi.org/project/scaldys-project/``. 5. Revert the workflow change before the real release.