Skip to content

Publish to PyPI via GitHub CI #893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -51,6 +51,9 @@ jobs:
- name: Build
run: pipx run build

- name: Build
run: pipx run twine check --strict

- name: Archive files
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
52 changes: 52 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Publish

on:
release:
types: [created]

env:
FORCE_COLOR: 1

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# pipx is pre-installed in 'ubuntu' VMs

- name: Provision nox environment
run: pipx run nox --install-only

- name: Build distribution via nox
run: pipx run nox --no-install --error-on-missing-interpreters -s release_build

- name: Upload distribution
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
with:
name: packages
path: dist/
if-no-files-found: error
compression-level: 0

publish:
environment:
name: pypi
url: https://pypi.org/project/packaging/${{ github.ref_name }}
permissions:
id-token: write

runs-on: ubuntu-latest

steps:
- name: Download distribution
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 4.3.0
with:
name: packages
path: dist/

- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
print-hash: true
7 changes: 6 additions & 1 deletion docs/development/release-process.rst
Original file line number Diff line number Diff line change
@@ -11,10 +11,13 @@ Release Process

$ nox -s release -- YY.N

You will need the password for your GPG key as well as an API token for PyPI.
This creates and pushes a new tag for the release.

#. Add a `release on GitHub <https://github.com/pypa/packaging/releases>`__.

This schedules a CI workflow which will build and publish the package to
PyPI. Publishing will wait for any `required approvals`_.

#. Notify the other project owners of the release.

.. note::
@@ -24,3 +27,5 @@ Release Process
- PyPI maintainer (or owner) access to ``packaging``
- push directly to the ``main`` branch on the source repository
- push tags directly to the source repository

.. _required approvals: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/reviewing-deployments#approving-or-rejecting-a-job
83 changes: 63 additions & 20 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,6 @@
import tempfile
import textwrap
import time
import webbrowser
from pathlib import Path

import nox
@@ -123,6 +122,9 @@ def release(session):
session.run("git", "add", str(changelog_file), external=True)
_bump(session, version=release_version, file=version_file, kind="release")

# Check the built distribution.
_build_and_check(session, release_version, remove=True)

# Tag the release commit.
# fmt: off
session.run(
@@ -141,11 +143,63 @@ def release(session):
next_version = f"{major}.{minor + 1}.dev0"
_bump(session, version=next_version, file=version_file, kind="development")

# Checkout the git tag.
session.run("git", "checkout", "-q", release_version, external=True)
# Push the commits and tag.
# NOTE: The following fails if pushing to the branch is not allowed. This can
# happen on GitHub, if the main branch is protected, there are required
# CI checks and "Include administrators" is enabled on the protection.
session.run("git", "push", "upstream", "main", release_version, external=True)


@nox.session
def release_build(session):
# Parse version from command-line arguments, if provided, otherwise get
# from Git tag.
try:
release_version = _get_version_from_arguments(session.posargs)
except ValueError as e:
if session.posargs:
session.error(f"Invalid arguments: {e}")

release_version = session.run(
"git", "describe", "--exact-match", silent=True, external=True
)
release_version = "" if release_version is None else release_version.strip()
session.debug(f"version: {release_version}")
checkout = False
else:
checkout = True

# Check state of working directory.
_check_working_directory_state(session)

# Ensure there are no uncommitted changes.
result = subprocess.run(
["git", "status", "--porcelain"], capture_output=True, encoding="utf-8"
)
if result.stdout:
print(result.stdout, end="", file=sys.stderr)
session.error("The working tree has uncommitted changes")

# Check out the Git tag, if provided.
if checkout:
session.run("git", "checkout", "-q", release_version, external=True)

# Build the distribution.
_build_and_check(session, release_version)

# Get back out into main, if we checked out before.
if checkout:
session.run("git", "checkout", "-q", "main", external=True)


def _build_and_check(session, release_version, remove=False):
package_name = "packaging"

session.install("build", "twine")

# Determine if we're in install-only mode.
install_only = session.run("python", "--version", silent=True) is None

# Build the distribution.
session.run("python", "-m", "build")

@@ -155,30 +209,19 @@ def release(session):
f"dist/{package_name}-{release_version}-py3-none-any.whl",
f"dist/{package_name}-{release_version}.tar.gz",
]
if files != expected:
if files != expected and not install_only:
diff_generator = difflib.context_diff(
expected, files, fromfile="expected", tofile="got", lineterm=""
)
diff = "\n".join(diff_generator)
session.error(f"Got the wrong files:\n{diff}")

# Get back out into main.
session.run("git", "checkout", "-q", "main", external=True)

# Check and upload distribution files.
session.run("twine", "check", *files)

# Push the commits and tag.
# NOTE: The following fails if pushing to the branch is not allowed. This can
# happen on GitHub, if the main branch is protected, there are required
# CI checks and "Include administrators" is enabled on the protection.
session.run("git", "push", "upstream", "main", release_version, external=True)

# Upload the distribution.
session.run("twine", "upload", *files)
# Check distribution files.
session.run("twine", "check", "--strict", *files)

# Open up the GitHub release page.
webbrowser.open("https://github.com/pypa/packaging/releases")
# Remove distribution files, if requested.
if remove and not install_only:
shutil.rmtree("dist", ignore_errors=True)


@nox.session