diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..7764cda --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,88 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' + +categories: + - type: 'pre-exclude' + when: + label: 'skip-changelog' + + - title: '🚀 Added' + when: + labels: + - 'feature' + - 'enhancement' + + - title: '🔄 Changed' + when: + label: 'changed' + + - title: '🐛 Fixed' + when: + labels: + - 'bug' + - 'fix' + + - title: '📚 Documentation' + when: + label: 'documentation' + + - title: '🧰 Internal' + when: + labels: + - 'internal' + - 'chore' + - 'dependencies' + + - type: 'version-resolver' + semver-increment: 'major' + when: + label: 'major' + + - type: 'version-resolver' + semver-increment: 'minor' + when: + labels: + - 'feature' + - 'enhancement' + + - type: 'version-resolver' + semver-increment: 'patch' + when: + labels: + - 'fix' + - 'bug' + - 'changed' + - 'chore' + - 'internal' + - 'dependencies' + - 'documentation' + +autolabeler: + - label: 'feature' + title: + - '/^feat(\(.+\))?!?:/i' + - label: 'fix' + title: + - '/^fix(\(.+\))?!?:/i' + - label: 'changed' + title: + - '/^refactor(\(.+\))?!?:/i' + - label: 'documentation' + title: + - '/^docs(\(.+\))?!?:/i' + - label: 'dependencies' + title: + - '/^chore\(deps\)(\(.+\))?!?:/i' + - label: 'internal' + title: + - '/^(chore|ci|test|build)(\(.+\))?!?:/i' + +category-template: '### $TITLE' +change-template: '- $TITLE (#$NUMBER) @$AUTHOR' +change-title-escapes: '\<*_&' + +version-resolver: + default: patch + +template: | + $CHANGES diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py new file mode 100644 index 0000000..78068ee --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,47 @@ +"""Prepend a published GitHub release's notes to CHANGELOG.md. + +Run by .github/workflows/changelog.yaml on the `release: published` event. +Drops the leading `## Unreleased` section, if present, since it's +superseded by the version being inserted, then adds a `## [tag] - date` +section built from the release body right after the `# Changelog` title. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +CHANGELOG = Path("CHANGELOG.md") +UNRELEASED_HEADING = "## Unreleased" + + +def main() -> None: + tag = os.environ["RELEASE_TAG"] + date = os.environ["RELEASE_DATE"][:10] # YYYY-MM-DD from an ISO 8601 timestamp + body = os.environ["RELEASE_BODY"].strip() + + lines = CHANGELOG.read_text().splitlines(keepends=True) + if lines[0].strip() != "# Changelog": + msg = f"unexpected first line: {lines[0]!r}" + raise ValueError(msg) + + # Skip blank lines right after the title. + start = 1 + while start < len(lines) and not lines[start].strip(): + start += 1 + + # Drop a leading "## Unreleased" section: its content is superseded by + # the version we're about to insert. + end = start + if start < len(lines) and lines[start].strip() == UNRELEASED_HEADING: + end = start + 1 + while end < len(lines) and not lines[end].startswith("## "): + end += 1 + + section = f"## [{tag}] - {date}\n\n{body}\n" + new_lines = [*lines[:1], "\n", section, "\n", *lines[end:]] + CHANGELOG.write_text("".join(new_lines)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..9ad265c --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,57 @@ +name: Changelog + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: {} + +jobs: + update-changelog: + name: Update CHANGELOG.md + runs-on: ubuntu-24.04 + permissions: + contents: write # for the create-pull-request branch push + pull-requests: write # to open the PR + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Update CHANGELOG.md from the current draft release + id: draft + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + draft="$(gh api "repos/${GH_REPO}/releases" --jq '[.[] | select(.draft == true)] | first')" + if [ -z "$draft" ] || [ "$draft" = "null" ]; then + echo "::error::No draft release found. Push to main first so release-drafter creates one." + exit 1 + fi + + tag="$(jq -r '.tag_name' <<< "$draft")" + url="$(jq -r '.html_url' <<< "$draft")" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "url=$url" >> "$GITHUB_OUTPUT" + + RELEASE_TAG="$tag" \ + RELEASE_DATE="$(date -u +%Y-%m-%d)" \ + RELEASE_BODY="$(jq -r '.body' <<< "$draft")" \ + python3 .github/scripts/update_changelog.py + + - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + # zizmor: ignore[superfluous-actions] + with: + commit-message: "docs: Update changelog for ${{ steps.draft.outputs.tag }}" + title: "docs: Update changelog for ${{ steps.draft.outputs.tag }}" + body: >- + Automated update of `CHANGELOG.md` ahead of publishing + [${{ steps.draft.outputs.tag }}](${{ steps.draft.outputs.url }}). + Merge this before publishing the release so the tag includes + the updated changelog. + branch: changelog/${{ steps.draft.outputs.tag }} + add-paths: CHANGELOG.md diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..629bf9b --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,26 @@ +name: Release Drafter + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + update_release_draft: + name: Update release draft + runs-on: ubuntu-24.04 + permissions: + contents: write # to create/update the draft release + pull-requests: write # to autolabel PRs + steps: + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}