Skip to content
Merged
Show file tree
Hide file tree
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
154 changes: 154 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: Prepare Release

# Cut a release entirely from GitHub: Actions -> Prepare Release -> Run workflow.
#
# This bumps the version in pyproject.toml and the package __init__.py, turns
# the "## Unreleased" CHANGELOG.md section into the new version's entry, commits
# to the default branch, then creates and pushes an annotated v#.#.# tag. The
# tag push starts the Release workflow (build, PyPI upload, GitHub Release,
# docs) as its own run.
#
# The default branch is protected, so the commit and tag are pushed with the
# RELEASE_TOKEN repository secret: a token from an account allowed to bypass
# the branch protection rule. A token other than GITHUB_TOKEN is also what lets
# the tag push trigger release.yml.

on:
workflow_dispatch:
inputs:
bump:
description: Version component to bump (ignored if an explicit version is given)
required: true
default: patch
type: choice
options:
- patch
- minor
- major
version:
description: Explicit version to release, e.g. 1.2.3 (optional, overrides bump)
required: false
type: string
require_changelog:
description: Fail if CHANGELOG.md has neither an Unreleased section with entries nor an entry for the new version
required: false
default: false
type: boolean

# Pushes use RELEASE_TOKEN, so GITHUB_TOKEN only needs to read.
permissions:
contents: read

concurrency:
group: prepare-release
cancel-in-progress: false

jobs:
tag:
runs-on: ubuntu-latest

steps:
- name: Require default branch
if: github.ref_name != github.event.repository.default_branch
run: |
echo "::error::Releases must be cut from ${{ github.event.repository.default_branch }} (got ${{ github.ref_name }})"
exit 1

- name: Require RELEASE_TOKEN
env:
HAS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN != '' }}
run: |
if [ "${HAS_RELEASE_TOKEN}" != "true" ]; then
echo "::error::The RELEASE_TOKEN secret is not set; it is needed to push past branch protection"
exit 1
fi

- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install pipenv
run: pip install pipenv

- name: Set up project environment
run: make setup

- name: Install project package
run: make install-self

- name: Run tests
run: make tests

- name: Determine new version
id: version
env:
BUMP: ${{ inputs.bump }}
EXPLICIT: ${{ inputs.version }}
run: |
new_version="$(python3 - <<'PY'
import os, re, sys, tomllib

current = tomllib.load(open("pyproject.toml", "rb"))["project"]["version"]
explicit = os.environ.get("EXPLICIT", "").strip().lstrip("v")
semver = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")

if explicit:
if not semver.match(explicit):
sys.exit(f"Invalid version {explicit!r}; expected #.#.#")
print(explicit)
sys.exit(0)

m = semver.match(current)
if not m:
sys.exit(f"Current version {current!r} is not #.#.#; pass an explicit version")
major, minor, patch = map(int, m.groups())
bump = os.environ["BUMP"]
if bump == "major":
major, minor, patch = major + 1, 0, 0
elif bump == "minor":
minor, patch = minor + 1, 0
else:
patch += 1
print(f"{major}.{minor}.{patch}")
PY
)"
echo "New version: ${new_version}"
if git rev-parse -q --verify "refs/tags/v${new_version}" >/dev/null; then
echo "::error::Tag v${new_version} already exists"
exit 1
fi
echo "version=${new_version}" >> "$GITHUB_OUTPUT"

- name: Update CHANGELOG
env:
REQUIRE: ${{ inputs.require_changelog }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if ! python3 scripts/changelog.py release "${VERSION}"; then
if [ "${REQUIRE}" = "true" ]; then
echo "::error::CHANGELOG.md has no entries under '## Unreleased' and no '## v${VERSION}' entry"
exit 1
fi
echo "::warning::CHANGELOG.md has no entries for v${VERSION}; the GitHub Release will only have auto-generated notes"
fi

- name: Bump version
run: make change-version VERSION=${{ steps.version.outputs.version }}

- name: Commit, tag, and push
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add pyproject.toml src/pyhatching/__init__.py CHANGELOG.md
git commit -m "build: release v${VERSION}"
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push --atomic origin "HEAD:${GITHUB_REF_NAME}" "refs/tags/v${VERSION}"
38 changes: 36 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Release

# Runs on every v* tag push: from the "Prepare Release" workflow (pushed with
# RELEASE_TOKEN) or from `make release` run locally.
on:
push:
tags:
Expand All @@ -8,13 +10,22 @@ on:
permissions:
contents: write

concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest

env:
RELEASE_TAG: ${{ github.ref_name }}

steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: refs/tags/${{ env.RELEASE_TAG }}

- name: Set up Python
uses: actions/setup-python@v5
Expand All @@ -27,6 +38,14 @@ jobs:
- name: Set up project environment
run: make setup

- name: Verify tag matches package version
run: |
pkg_version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
if [ "v${pkg_version}" != "${RELEASE_TAG}" ]; then
echo "::error::Tag ${RELEASE_TAG} does not match pyproject.toml version ${pkg_version}"
exit 1
fi

- name: Build package
run: make build

Expand All @@ -36,9 +55,24 @@ jobs:
- name: Package documentation
run: tar -czf docs.tar.gz -C docs .

- name: Extract release notes from CHANGELOG
run: python3 scripts/changelog.py notes "${RELEASE_TAG}" > release-notes.md || true

- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_DEPLOY_TOKEN }}
run: |
pipenv run python3 -m twine check dist/*
pipenv run python3 -m twine upload --non-interactive --skip-existing dist/*

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_TAG }}
body_path: release-notes.md
generate_release_notes: true
files: |
dist/*
docs.tar.gz
Expand All @@ -49,6 +83,6 @@ jobs:
run: |
gh api repos/gormaniac/docs.gormo.co/dispatches \
-f event_type=doc-release \
-f 'client_payload[tag]=${{ github.ref_name }}' \
-f "client_payload[tag]=${RELEASE_TAG}" \
-f 'client_payload[repository]=${{ github.repository }}' \
-f 'client_payload[github_sha]=${{ github.sha }}'
-f "client_payload[github_sha]=$(git rev-parse HEAD)"
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Unreleased
### Features
- Support the full Hatching Triage API (public, private, and self-hosted instances).
- Add `pyhatching.testing.FakeTriage`, a fake Triage API server for testing code built on pyhatching.
- Add a live test suite that runs against tria.ge when `HATCHING_TOKEN` is set.
- Publish the Python package to PyPI from the Release GitHub Action.
- Add a manually run Prepare Release GitHub Action that bumps the version, rolls the CHANGELOG, tags, and runs the release.
- Add `make tests` to run the test suite.
### Fixes
- Resolve hashes in the streaming downloads and `kernel_report`.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ read-docs: # Open the package docs locally

.PHONY: release
release: change-version clean setup build docs # Build a new versioned release and push it (requires VERSION=#.#.#)
git add doc/* pyproject.toml $(PKG_DIR)/__init__.py
python3 scripts/changelog.py release $(VERSION) || echo "WARNING: no CHANGELOG entries for v$(VERSION)"
git add doc/* pyproject.toml $(PKG_DIR)/__init__.py CHANGELOG.md
git commit -m "build: release v$(VERSION)"
git push
git tag -a v$(VERSION) -m "Release v$(VERSION)"
Expand All @@ -60,3 +61,7 @@ release: change-version clean setup build docs # Build a new versioned release a
.PHONY: pypi
pypi: # Upload all Python packages in "dist/" to PyPI.
pipenv run python3 -m twine upload dist/*

.PHONY: tests
tests: # Run the pytest suite for this project.
pipenv run pytest tests/
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,20 @@ Supports the whole Triage API (public, private, and self-hosted instances):
`pyhatching.testing.FakeTriage` is a fake Triage API server for testing code built on pyhatching.

Read the full documentation [here](https://gormo.co/docs/pyhatching/).

## Releasing

Releases are cut from GitHub Actions with the **Prepare Release** workflow, and pending changes are tracked under a `## Unreleased` header in `CHANGELOG.md`.

1. Add entries to the `## Unreleased` section at the top of `CHANGELOG.md` (under its `### Features` and `### Fixes` headers) as changes land on `main`.
2. Go to **Actions → Prepare Release → Run workflow** on `main`, pick a `patch`/`minor`/`major` bump (or enter an explicit version).

The workflow runs the tests, bumps the version, renames `## Unreleased` to `## v#.#.# - <date>` (starting a fresh empty `## Unreleased` above it), commits, and pushes a `v#.#.#` tag. The **Release** workflow then builds the package, uploads it to PyPI (using the `PYPI_DEPLOY_TOKEN` secret), creates a GitHub Release with the changelog entry as notes and the package and docs attached, and notifies the docs site. Pushing a `v*` tag manually (e.g. `make release VERSION=#.#.#`) also triggers the Release workflow.

`main` is protected, so Prepare Release pushes the release commit and tag with the `RELEASE_TOKEN` repository secret: a token with **Contents: read and write** from an account allowed to bypass the branch protection rule. The tag push starts the Release workflow as its own run under Actions.

The workflows rely on these repository secrets:

- `PYPI_DEPLOY_TOKEN` - the PyPI API token used to upload the package.
- `DOCS_DISPATCH_TOKEN` - used to notify the docs site repo.
- `RELEASE_TOKEN` (required by Prepare Release) - see above. If it expires or is revoked, the push step fails and the token needs to be replaced.
Loading
Loading