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
28 changes: 22 additions & 6 deletions .github/workflows/create-draft-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
types:
- closed
workflow_dispatch:

permissions: {}

Expand All @@ -15,9 +16,14 @@ jobs:
create-draft:
name: Build assets and create draft
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/')
(
github.event_name == 'workflow_dispatch' &&
github.ref_name == github.event.repository.default_branch
) || (
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/')
)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
Expand All @@ -27,13 +33,13 @@ jobs:

env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
RELEASE_TARGET: ${{ github.event.pull_request.merge_commit_sha }}
RELEASE_TARGET: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.pull_request.merge_commit_sha }}

steps:
- name: Checkout merged release commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
ref: ${{ env.RELEASE_TARGET }}
fetch-depth: 0
persist-credentials: false

Expand All @@ -51,6 +57,16 @@ jobs:
run: |
set -euo pipefail

if [[ ! "${RELEASE_TARGET}" =~ ^[0-9a-f]{40}$ ]]; then
echo "Release target must be a full commit SHA"
exit 1
fi

if [[ "$(git rev-parse HEAD)" != "${RELEASE_TARGET}" ]]; then
echo "Checked-out commit does not match the release target"
exit 1
fi

tag="$(uv run python release/prepare.py release-tag)"
uv run python release/prepare.py verify-tag "${tag}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
Expand Down Expand Up @@ -80,7 +96,7 @@ jobs:
run: uv sync --locked --all-extras --all-groups

- name: Build and verify release candidate
run: just release-check
run: uv run --no-sync just release-check

- name: Attest distributions
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dev = [
"pytest",
"pytest-cov",
"ruff",
"rust-just>=1.56.0,<2",
"trio",
"twine>=6.2.0",
"vulture>=2.16",
Expand Down
10 changes: 7 additions & 3 deletions release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ just release-check
Before publication, the workflows require all of the following:

- the release pull request came from this repository's `release/` branch
- the release target is the merged pull-request commit on the default branch
- an automatic release target is the merged release pull-request commit
- a manual recovery target is the exact default-branch commit running the
workflow
- no release tag or draft already existed
- the tag equals `v` plus `[project].version`
- the canonical release notes have a non-empty section for that version
Expand Down Expand Up @@ -214,8 +216,10 @@ and runs the core and FastAPI suites without allowing uv to restore the lock.
- Bad release pull request -> close it without merging, fix labels or merged
changes, and run Prepare release again.
- Bad generated notes -> edit and curate them in the open release pull request.
- Failed candidate build -> fix through a normal pull request, then prepare a
new release.
- Failed candidate build before a tag or draft exists -> fix through a normal
pull request. After it merges, open GitHub Actions -> Create draft release,
select the default branch, and run the workflow manually. The recovery path
releases that exact default-branch commit and refuses any other branch.
- Bad draft -> do not publish it. Delete the draft and associated tag, fix the
problem through a pull request, and prepare again.
- Published release -> never replace its artifacts or tag. Fix the problem in
Expand Down
39 changes: 39 additions & 0 deletions tests/automation/test_workflow_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Tests for repository workflow contracts."""

from pathlib import Path
from typing import Final

_ROOT: Final[Path] = Path(__file__).parents[2]


def test_draft_release_uses_locked_just() -> None:
# Given
project = (_ROOT / "pyproject.toml").read_text()
workflow = (_ROOT / ".github/workflows/create-draft-release.yml").read_text()

# When
has_locked_just = '"rust-just>=1.56.0,<2"' in project
uses_locked_just = "uv run --no-sync just release-check" in workflow

# Then
assert has_locked_just
assert uses_locked_just


def test_draft_release_manual_retry_is_default_branch_only() -> None:
# Given
workflow = (_ROOT / ".github/workflows/create-draft-release.yml").read_text()

# When
has_manual_trigger = " workflow_dispatch:\n" in workflow
restricts_default_branch = (
"github.ref_name == github.event.repository.default_branch" in workflow
)
binds_target_to_workflow_sha = (
"github.event_name == 'workflow_dispatch' && github.sha" in workflow
)

# Then
assert has_manual_trigger
assert restricts_default_branch
assert binds_target_to_workflow_sha
Loading
Loading