From 2eea5984e4a841b63f7c5e1eeab197e698919e9d Mon Sep 17 00:00:00 2001 From: micahpw <6476273+micahpw@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:29:52 -0600 Subject: [PATCH] ci: automated releases via release-please (modeled on R2X) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts the same release-please pattern used by NatLabRockies/R2X: merges to main with Conventional Commit messages are what drive releases now, instead of a human running `git tag` by hand. ## Flow 1. Every push to main: release-please parses commits since the last release and keeps a standing "chore: release vX.Y.Z" PR open, auto-updating docs/source/CHANGELOG.md as more commits land. 2. The moment that PR is merged, release-please creates the tag + GitHub Release automatically. 3. release.yml's build/publish-pypi jobs then run exactly as before (uv build, PyPI trusted publishing), gated on release_created instead of a tag-push event. workflow_dispatch remains as a manual escape hatch (publish-pypi's new skip-existing: true makes re-runs safe). ## Adapted from R2X, not copied - release-type "simple", not "python": GAT's pyproject.toml has `dynamic = ["version"]` — setuptools_scm derives the version entirely from git tags at build time, so there's no static `version = "x.y.z"` string anywhere for release-please to edit. "simple" only manages the changelog + tag/release, which is exactly what's needed here. - bump-minor-pre-major: false, bump-patch-for-minor-pre-major: true — R2X is already past 1.0 so this doesn't apply to their config, but it matters for GAT: without it, any feat: commit could auto-advance to v0.2.0, which is meant to stay a deliberate, human-triggered milestone (the H5PLEXOS.jl removal — see the README warning box). With this config, feat/fix land as patch bumps (0.1.0 -> 0.1.1 -> ...) until someone explicitly overrides the next version via release-please's release-as mechanism. - changelog-path points at docs/source/CHANGELOG.md, matching where GAT's changelog actually lives (R2X's is at the repo root). ## Also added - commit-lint.yml — Conventional Commit message + PR title linting (commitizen + amannn/action-semantic-pull-request), matching R2X's commit.yaml. This is what makes release-please's version/changelog computation reliable — an unprefixed commit is silently invisible to it, not an error, so catching the format early matters. Informational only for now (not in the branch-protection required checks), since making it a hard block is a separate decision. - README: one-line Conventional Commits note in Contributing. ## One-time manual note for the first release-please PR The CHANGELOG already has a hand-written "## [v0.1.1] - Unreleased" section (from #29, before this automation existed). release-please's first generated PR will likely add its own auto-generated v0.1.1 section without knowing about that one, since it only tracks state via .release-please-manifest.json (baselined at 0.1.0, the last real tag). Expect to manually reconcile the two sections once, when reviewing that first PR — a one-time cleanup, not an ongoing issue. Co-Authored-By: Claude Opus 5 --- .github/workflows/commit-lint.yml | 49 +++++++++++++++++++ .github/workflows/release.yml | 80 +++++++++++++++++++++++++++---- .release-please-config.json | 25 ++++++++++ .release-please-manifest.json | 3 ++ README.md | 4 ++ 5 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/commit-lint.yml create mode 100644 .release-please-config.json create mode 100644 .release-please-manifest.json diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 0000000..e5991ad --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,49 @@ +name: commit-lint + +# Enforces Conventional Commits, modeled on NatLabRockies/R2X's +# commit.yaml. This is what makes release-please (release.yml) work at +# all — it computes version bumps and changelog sections by parsing +# commit-message prefixes (feat/fix/docs/...), so an unprefixed commit +# is silently invisible to it rather than an error. +# +# Informational only for now: these checks aren't in the branch +# protection ruleset's required-checks list, so they don't block +# merging. Add them there if you want it enforced rather than +# best-effort. + +on: + pull_request: + types: [opened, reopened, synchronize, edited] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint-commit-messages: + name: lint commit messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 2 + - uses: astral-sh/setup-uv@v4 + - name: Commitizen check + run: uvx --from commitizen cz check --rev-range HEAD^! + + lint-pr-title: + # Squash-merges use the PR title as the final commit message, so + # this is what release-please actually sees for a squashed PR — + # lint it the same way as individual commits. + name: lint PR title + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ca9e97..5c597d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,73 @@ name: release +# Automated release flow modeled on NatLabRockies/R2X's release-please +# setup, adapted for a single package with no static version string to +# bump (GAT uses setuptools_scm — the version is derived entirely from +# git tags at build time, hence release-type "simple" below rather than +# "python": there's no `version = "x.y.z"` line for release-please to +# edit). +# +# Flow: +# 1. Every push to main: release-please looks at conventional-commit +# messages since the last release and keeps a standing "chore: +# release vX.Y.Z" PR open, auto-updating docs/source/CHANGELOG.md +# as more commits land. Merging that PR is the only manual step. +# 2. The moment that PR merges, release-please creates the git tag +# and GitHub Release (release_created=true) — no one runs `git tag` +# by hand anymore. +# 3. build + publish-pypi then run exactly as before, gated on +# release_created instead of a tag-push event. +# +# workflow_dispatch is kept as a manual escape hatch to rebuild/republish +# the current tag if a step needs re-running (publish-pypi's +# skip-existing: true makes this safe to re-trigger). + on: push: - tags: - - 'v*' + branches: [main] + workflow_dispatch: + +concurrency: + group: release + cancel-in-progress: false jobs: - build: + release-please: + name: release-please + if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: write + pull-requests: write + outputs: + release_created: ${{ steps.rp.outputs.release_created }} + tag_name: ${{ steps.rp.outputs.tag_name }} + steps: + - name: Run release-please + id: rp + uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: .release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: main + build: + name: Build distribution + needs: [release-please] + if: | + always() && ( + (github.event_name == 'push' && needs.release-please.outputs.release_created == 'true') || + github.event_name == 'workflow_dispatch' + ) + runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ needs.release-please.outputs.tag_name || github.ref }} - name: Install uv uses: astral-sh/setup-uv@v4 @@ -27,9 +80,11 @@ jobs: - name: Build sdist and wheel run: uv build - - name: Upload release assets + - name: Attach dists to the GitHub release + if: needs.release-please.outputs.release_created == 'true' uses: softprops/action-gh-release@v2 with: + tag_name: ${{ needs.release-please.outputs.tag_name }} files: | dist/nlr_gat-*.whl dist/nlr_gat-*.tar.gz @@ -41,16 +96,21 @@ jobs: path: dist/ # Publishes via PyPI Trusted Publishing (OIDC) — no API token is stored - # in the repo. One-time setup on PyPI is required before the first run: - # add a "pending publisher" for the project name `nlr-gat` with - # owner NatLabRockies, repository GridAnalysisToolkit, workflow - # release.yml, and environment pypi + # in the repo. One-time setup on PyPI: a "pending publisher" for + # project name `nlr-gat`, owner NatLabRockies, repository + # GridAnalysisToolkit, workflow release.yml, environment pypi # (https://pypi.org/manage/account/publishing/). # The `pypi` GitHub environment is created automatically on first use; # protection rules (required reviewers) can be added to it in repo # Settings → Environments to gate publishes. publish-pypi: - needs: build + name: Publish to PyPI + needs: [release-please, build] + if: | + always() && needs.build.result == 'success' && ( + (github.event_name == 'push' && needs.release-please.outputs.release_created == 'true') || + github.event_name == 'workflow_dispatch' + ) runs-on: ubuntu-latest environment: name: pypi @@ -67,3 +127,5 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 0000000..1b8ed71 --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "changelog-path": "docs/source/CHANGELOG.md", + "changelog-sections": [ + { "hidden": false, "section": "Features", "type": "feat" }, + { "hidden": false, "section": "Bug Fixes", "type": "fix" }, + { "hidden": false, "section": "Performance", "type": "perf" }, + { "hidden": false, "section": "Refactoring", "type": "refactor" }, + { "hidden": false, "section": "Documentation", "type": "docs" }, + { "hidden": false, "section": "CI/CD", "type": "ci" }, + { "hidden": false, "section": "Build", "type": "build" }, + { "hidden": true, "section": "Chores", "type": "chore" }, + { "hidden": false, "section": "Tests", "type": "test" } + ], + "packages": { + ".": { + "release-type": "simple", + "package-name": "nlr-gat", + "component": "nlr-gat", + "changelog-path": "docs/source/CHANGELOG.md", + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": true + } + } +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/README.md b/README.md index 1311b67..62bb306 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,10 @@ If you plan to contribute to documentation, install the documentation dependenci `pip install -e ".[dev,doc]"` +Commit messages and PR titles should follow [Conventional Commits](https://www.conventionalcommits.org/) +(`feat: ...`, `fix: ...`, `docs: ...`, etc.) — releases are generated +automatically from these (see `.github/workflows/release.yml`). + ## Building the documentation After installing the doc dependencies: