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: