diff --git a/.github/pr-types.json b/.github/pr-types.json new file mode 100644 index 0000000..2f3d64e --- /dev/null +++ b/.github/pr-types.json @@ -0,0 +1,4 @@ +{ + "all": ["feat!", "fix!", "revert!", "feat", "fix", "revert", "perf", "docs", "refactor", "test", "ci", "chore"], + "release": ["feat!", "fix!", "revert!", "feat", "fix", "revert", "perf", "docs"] +} diff --git a/.github/scripts/write_release_notes.py b/.github/scripts/write_release_notes.py index 987e6bf..86b2b18 100644 --- a/.github/scripts/write_release_notes.py +++ b/.github/scripts/write_release_notes.py @@ -4,8 +4,7 @@ import os import subprocess import sys - -PREFIXES = ["feat:", "fix:", "revert:", "perf:", "docs:"] +from pathlib import Path def gh(*args: str) -> str: @@ -14,14 +13,19 @@ def gh(*args: str) -> str: return result.stdout.strip() -def latest_tag() -> str: - return gh("release", "list", "-L", "1", "--json", "tagName", "-q", ".[0].tagName") +LATEST_TAG = gh("release", "list", "-L", "1", "--json", "tagName", "-q", ".[0].tagName") + + +def release_types() -> list[str]: + """Return the PR types that appear in release notes.""" + pr_types_file = Path(__file__).parent.parent / "pr-types.json" + return json.loads(pr_types_file.read_text())["release"] def merged_prs() -> list[dict]: """Return the number and title of PRs merged since the latest release.""" repo = os.environ["GITHUB_REPOSITORY"] - release_date = gh("api", f"repos/{repo}/releases/tags/{latest_tag()}", "--jq", ".created_at") + release_date = gh("api", f"repos/{repo}/releases/tags/{LATEST_TAG}", "--jq", ".created_at") prs = gh( "pr", "list", @@ -36,27 +40,24 @@ def merged_prs() -> list[dict]: "--limit", "50", ) - return json.loads(prs) + return json.loads(prs)[::-1] # Put oldest merged first. def release_notes(prs: list[dict], new_tag: str) -> str: """Return the release notes body for the given PRs. - Groups PRs by title prefix then breaking/non-breaking. + Groups PRs by title prefix, in the order given by pr-types.json. """ entries = "" - for prefix in PREFIXES: - breaking_prefix = f"{prefix.removesuffix(':')}!:" - group = sorted( - (pr for pr in prs if pr["title"].startswith((prefix, breaking_prefix))), - key=lambda pr: (not pr["title"].startswith(breaking_prefix), pr["number"]), - ) - for pr in group: - entries += f"- {pr['title']} (#{pr['number']})\n" + for pr_type in release_types(): + prefix = f"{pr_type}:" + for pr in prs: + if pr["title"].startswith(prefix): + entries += f"- {pr['title']} (#{pr['number']})\n" if not entries: entries = "This is a maintenance release with no fixes or new features.\n" repo = os.environ["GITHUB_REPOSITORY"] - return f"{entries}\nChangelog: https://github.com/{repo}/compare/{latest_tag()}...{new_tag}" + return f"{entries}\nChangelog: https://github.com/{repo}/compare/{LATEST_TAG}...{new_tag}" if __name__ == "__main__": diff --git a/.github/workflows/deps.yaml b/.github/workflows/deps.yaml new file mode 100644 index 0000000..1d6f84d --- /dev/null +++ b/.github/workflows/deps.yaml @@ -0,0 +1,21 @@ +name: Check Python deps + +on: + pull_request: + push: + branches: + - main + +permissions: {} + +jobs: + deps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + - name: Check uv.lock + run: uv lock --check diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml new file mode 100644 index 0000000..9a2fe5d --- /dev/null +++ b/.github/workflows/pr-title.yaml @@ -0,0 +1,28 @@ +name: Check PR title + +on: + pull_request: + types: + - opened + - edited + +permissions: {} + +jobs: + pr-title: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Check PR title + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + set -euo pipefail + types="$(jq -r '.all | join(", ")' .github/pr-types.json)" + pattern="^(${types//, /|}): " + if [[ ! "$TITLE" =~ $pattern ]]; then + echo "PR title must start with one of: ${types}." >&2 + exit 1 + fi diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index 1a45ac9..3a75f15 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -14,8 +14,7 @@ jobs: permissions: security-events: write steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install uv