From c164be0484fa2fa3c00ce886d2421065cc47378a Mon Sep 17 00:00:00 2001 From: gerchowl Date: Sat, 18 Apr 2026 18:25:18 +0200 Subject: [PATCH] ci: introduce release-please automated release pipeline (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives version bumps, tagging, and publish triggers from conventional commits for both Python (py-materials → PyPI) and Rust (rs-materials → crates.io, gated) packages. Pipeline on push to main: 1. release-please scans commits since the last tag and opens a per-package Release PR bumping versions + appending CHANGELOG entries. 2. Merging the Release PR pushes a tag (vX.Y.Z or rs-materials/vX.Y.Z), which triggers the existing publish workflows. 3. release-please creates the GitHub Release with auto-generated notes — the duplicate `github-release` jobs in release.yml and release-rs-materials.yml are removed. Also: - PR Hygiene job enforces the contributor PR template checklist via mheap/require-checklist-action. Template restructured into "required" (must be checked) and "if applicable" (delete sections that don't apply) groups. Bot PRs from release-please are exempt (they carry their own release-reviewer checklist for humans). - rs-materials crates.io publishing is gated behind vars.CRATES_IO_PUBLISH_ENABLED until the token is configured and downstream consumer coordination completes (issue #43). - Uses the existing RELEASE_APP GitHub App so the tag release-please pushes triggers downstream workflows (GITHUB_TOKEN pushes are inert under recursion protection). Refs: #43 --- .github/pull_request_template.md | 89 +++++++++++----------- .github/workflows/ci.yml | 31 +++++++- .github/workflows/release-please.yml | 63 +++++++++++++++ .github/workflows/release-rs-materials.yml | 23 +++--- .github/workflows/release.yml | 24 ++---- .release-please-manifest.json | 4 + RELEASE_PROCESS.md | 46 +++++------ release-please-config.json | 38 +++++++++ 8 files changed, 214 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3a5ba1b..b5023dc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,72 +1,69 @@ ## Description - + ## Type of Change - + -- [ ] `feat` -- New feature -- [ ] `fix` -- Bug fix -- [ ] `docs` -- Documentation only -- [ ] `chore` -- Maintenance task (deps, config, etc.) -- [ ] `refactor` -- Code restructuring (no behavior change) -- [ ] `test` -- Adding or updating tests -- [ ] `ci` -- CI/CD pipeline changes -- [ ] `build` -- Build system or dependency changes -- [ ] `revert` -- Reverts a previous commit -- [ ] `style` -- Code style (formatting, whitespace) +- [ ] `feat` — New feature (minor version bump) +- [ ] `fix` — Bug fix (patch version bump) +- [ ] `feat!` / `fix!` / `BREAKING CHANGE:` — Breaking change (major version bump) +- [ ] `docs` — Documentation only (no release) +- [ ] `chore` / `refactor` / `test` / `ci` / `build` / `style` — No release -### Modifiers +## Required -- [ ] Breaking change (`!`) -- This change breaks backward compatibility + -## Changes Made +- [ ] Tests pass locally (`uv run pytest`) +- [ ] Self-reviewed the diff +- [ ] No new warnings or errors in the changed code +- [ ] Linked to an issue in the `Refs:` line at the bottom (or explained why none) - +## If Applicable -## Changelog Entry - - -## Testing +### Documentation + +- [ ] Updated `docs/templates/` and ran `just docs` +- [ ] Updated `CHANGELOG.md` under `## Unreleased` (release-please will move it to a versioned section on release) +- [ ] Updated `README.md` if user-facing API changed + +### Tests + +- [ ] Added new tests covering the change +- [ ] Manual testing performed (steps in **Manual Testing Details** below) + +#### Manual Testing Details - -- [ ] Tests pass locally (`just test`) -- [ ] Manual testing performed (describe below) + -### Manual Testing Details +### Dependencies - +- [ ] Updated `pyproject.toml` and re-locked (`uv lock`) +- [ ] Updated `mat-rs/Cargo.toml` and re-locked (`cargo update`) +- [ ] Verified no breakage with downstream consumers (build123d, ocp_vscode, mat-vis-client) -## Checklist +### Breaking Change - -- [ ] My code follows the project's style guidelines -- [ ] I have performed a self-review of my code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have updated the documentation accordingly (edit `docs/templates/`, then run `just docs`) -- [ ] I have updated `CHANGELOG.md` in the `[Unreleased]` section (and pasted the entry above) -- [ ] My changes generate no new warnings or errors -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published +- [ ] Migration notes added to `docs/migration/` +- [ ] `BREAKING CHANGE:` footer in commit message body +- [ ] Open issue / PR coordination with known downstream consumers ## Additional Notes - + Refs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9a239f..bf995b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,6 +207,29 @@ jobs: with: fail-on-severity: high + pr-hygiene: + # Enforces the PR template checklist. Any unchecked `- [ ]` left in the + # PR body fails the build. The template is designed for the user to + # DELETE sections that don't apply (see .github/pull_request_template.md); + # leaving them unchecked is treated as "you forgot", not "N/A". + # + # Skipped for release-please bot PRs because the bot owns the body and the + # release-reviewer checklist embedded there is for human reviewers, not for + # CI to gate (the gate is the human merging the PR). + name: PR Hygiene + runs-on: ubuntu-22.04 + timeout-minutes: 5 + if: | + github.event_name == 'pull_request' && + github.event.pull_request.user.login != 'github-actions[bot]' && + !startsWith(github.head_ref, 'release-please--') + + steps: + - name: Require checklist items + uses: mheap/require-checklist-action@46d2ca1a0f90144bd081fd13a80b1dc581759365 # v2.5.0 + with: + requireChecklist: true + rust: name: Rust (mat-rs) runs-on: ubuntu-22.04 @@ -243,7 +266,7 @@ jobs: name: CI Summary runs-on: ubuntu-22.04 timeout-minutes: 5 - needs: [lint, test, security, dependency-review, rust] + needs: [lint, test, security, dependency-review, pr-hygiene, rust] if: always() steps: @@ -256,6 +279,7 @@ jobs: echo "Test: ${{ needs.test.result }}" echo "Security: ${{ needs.security.result }}" echo "Dependency Review: ${{ needs.dependency-review.result }}" + echo "PR Hygiene: ${{ needs.pr-hygiene.result }}" echo "Rust (mat-rs): ${{ needs.rust.result }}" echo "" @@ -281,6 +305,11 @@ jobs: FAILED=true fi + if [ "${{ needs.pr-hygiene.result }}" = "failure" ]; then + echo "ERROR: PR Hygiene failed (unchecked checklist items in PR body)" + FAILED=true + fi + if [ "${{ needs.rust.result }}" = "failure" ]; then echo "ERROR: Rust checks failed" FAILED=true diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..28ee9e9 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,63 @@ +# Release Please +# +# Drives the release pipeline from conventional commits. +# +# On every push to main, release-please scans commits since the last tag and +# either: +# 1. Updates / opens a per-package "Release PR" (chore(main): release X.Y.Z) +# that bumps versions + appends to CHANGELOG.md. +# 2. If the just-merged commit IS a Release PR, pushes the tag — which fires +# release.yml (Python → PyPI) or release-rs-materials.yml (Rust → +# crates.io). Release-please also creates the GitHub Release with notes +# generated from the conventional-commit history. +# +# Tag formats (intentional, match existing history): +# - py-materials → vX.Y.Z → triggers release.yml +# - rs-materials → rs-materials/vX.Y.Z → triggers release-rs-materials.yml +# +# Auth: uses the RELEASE_APP GitHub App so the tag push it performs triggers +# downstream workflows. Tags pushed by GITHUB_TOKEN are intentionally inert +# under GitHub's recursion protection — that would silently break PyPI/crates +# publishing. +# +# Triggers: push to main · workflow_dispatch +# +# Docs: https://github.com/googleapis/release-please + +name: release-please + +on: # yamllint disable-line rule:truthy + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-22.04 + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write + + steps: + - name: Generate Release App Token + id: release-app-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Run release-please + uses: googleapis/release-please-action@8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5 # v4.4.1 + with: + token: ${{ steps.release-app-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.github/workflows/release-rs-materials.yml b/.github/workflows/release-rs-materials.yml index dfac3b4..1513ab5 100644 --- a/.github/workflows/release-rs-materials.yml +++ b/.github/workflows/release-rs-materials.yml @@ -1,11 +1,15 @@ name: Release rs-materials +# Triggered by tags pushed by release-please (release-please.yml). +# Tests + publishes the Rust crate to crates.io. The GitHub Release is created +# by release-please — do not duplicate it here. + on: # yamllint disable-line rule:truthy push: tags: ["rs-materials/v*"] permissions: - contents: write + contents: read jobs: test: @@ -19,6 +23,11 @@ jobs: publish-crates-io: needs: test runs-on: ubuntu-latest + # Gated until `CARGO_REGISTRY_TOKEN` is configured and downstream consumers + # are coordinated. Flip on via: + # gh variable set CRATES_IO_PUBLISH_ENABLED --body true + # Tracking issue: MorePET/mat (search label:release-infra) + if: vars.CRATES_IO_PUBLISH_ENABLED == 'true' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -26,15 +35,3 @@ jobs: run: cargo publish --manifest-path mat-rs/Cargo.toml --allow-dirty env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - github-release: - needs: test - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9461f10..da7958f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,16 @@ name: Release +# Triggered by tags pushed by release-please (release-please.yml). +# Builds the wheel + sdist and publishes to PyPI via trusted publishing (OIDC). +# The GitHub Release itself is created by release-please — do not duplicate it +# here. + on: # yamllint disable-line rule:truthy push: tags: ["v*"] permissions: - contents: write + contents: read jobs: build: @@ -38,20 +43,3 @@ jobs: path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - - github-release: - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: dist/* - generate_release_notes: true diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..0eaa7b2 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + ".": "3.0.0", + "mat-rs": "0.2.0" +} diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index b5f6d09..bca9548 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -1,39 +1,33 @@ # Release Process for mat -## Creating a New Release +Releases are driven by [release-please](https://github.com/googleapis/release-please) — see `.github/workflows/release-please.yml`. **Do not bump versions or push tags by hand.** -When releasing a new version of mat: +## How a release happens -1. **Update version numbers:** +1. Land conventional commits on `dev` (`feat:`, `fix:`, `feat!:`, etc. — the commit-msg hook enforces the format). +2. Open a `release/x.y.z → main` PR (manual gate; CI runs the full check matrix). +3. Merge to `main`. On every push to main, release-please re-evaluates the commit history since the last tag and (per package) opens or updates a **Release PR** titled `chore(main): release X.Y.Z`. The PR bumps `pyproject.toml` + `src/pymat/__init__.py` (Python) or `mat-rs/Cargo.toml` (Rust) and prepends a CHANGELOG section generated from commits. +4. Review the Release PR — version computation: + - `feat:` → minor bump + - `fix:` → patch bump + - `feat!:` or `BREAKING CHANGE:` footer → major bump + - `chore:`, `docs:`, `ci:`, `test:`, `style:` → no release +5. Merge the Release PR. Release-please pushes the tag (`vX.Y.Z` for Python, `rs-materials/vX.Y.Z` for Rust). The tag push triggers `release.yml` (PyPI) or `release-rs-materials.yml` (crates.io). Release-please also creates the GitHub Release with auto-generated notes. - ```bash - cd /Users/larsgerchow/Projects/mat +## Two independent packages - # Edit version in: - # - pyproject.toml - # - src/pymat/__init__.py - ``` +Each package has its own Release PR, version, and tag: -2. **Commit and tag:** +| Package | Path | Tag format | Publishes to | +|----------------|----------|------------------------|--------------| +| `py-materials` | `.` | `vX.Y.Z` | PyPI | +| `rs-materials` | `mat-rs` | `rs-materials/vX.Y.Z` | crates.io | - ```bash - git add -A - git commit -m "Release vX.Y.Z - Description" - git tag -a vX.Y.Z -m "Release vX.Y.Z" - ``` +A `feat:` touching only `mat-rs/**` triggers a Rust Release PR; a `feat:` touching `src/pymat/**` triggers a Python Release PR. Commits affecting both produce two Release PRs. -3. **Update the 'latest' tag (force overwrite):** +## Auth - ```bash - git tag -f -a latest -m "Latest release" - ``` - -4. **Push everything:** - - ```bash - git push origin main --tags - git push -f origin latest # Force push to update 'latest' tag - ``` +`release-please.yml` uses the `RELEASE_APP` GitHub App (same App used by `sync-main-to-dev.yml`). This is required — tags pushed by `GITHUB_TOKEN` are inert under GitHub's recursion-protection and would silently skip the publish workflows. ## Why This Works diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..531bc6c --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "separate-pull-requests": true, + "include-v-in-tag": true, + "pull-request-header": ":robot: Automated release. Merging this PR creates a tag, which fires the publish workflow (PyPI for `py-materials`, crates.io for `rs-materials`).\n\n### Release Reviewer Checklist\n\n_Check before merging. PR Hygiene CI is intentionally skipped on release-please PRs — these boxes are for the human merging the release._\n\n- [ ] Version bump matches expected scope (no surprise major from a stray `feat!:`)\n- [ ] CHANGELOG entries read correctly and credit external contributors\n- [ ] No security-sensitive deps changed without prior review\n- [ ] Downstream consumers (build123d, ocp_vscode, mat-vis-client) still compatible\n- [ ] Migration notes (if breaking change) present and linked from CHANGELOG\n- [ ] All required CI checks green", + "changelog-sections": [ + { "type": "feat", "section": "Added" }, + { "type": "fix", "section": "Fixed" }, + { "type": "refactor", "section": "Changed" }, + { "type": "docs", "section": "Documentation" }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "chore", "section": "Chore", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build", "hidden": true }, + { "type": "style", "section": "Style", "hidden": true } + ], + "packages": { + ".": { + "release-type": "python", + "package-name": "py-materials", + "component": "py-materials", + "include-component-in-tag": false, + "extra-files": [ + { + "type": "python", + "path": "src/pymat/__init__.py" + } + ] + }, + "mat-rs": { + "release-type": "rust", + "package-name": "rs-materials", + "component": "rs-materials", + "include-component-in-tag": true, + "tag-separator": "/" + } + } +}