From 0cb4daf28e8dcbcc129e7c152790ed5c77c4c76e Mon Sep 17 00:00:00 2001 From: Niels Pardon Date: Mon, 8 Jun 2026 13:24:12 +0200 Subject: [PATCH] ci: automate releases with semantic-release Adopt the semantic-release model used by other Substrait repos (e.g. substrait-java) to drive releases from Conventional Commits. A new scheduled (weekly) + manual `Semantic Release` workflow runs semantic-release under a GitHub App token: it computes the next version, updates CHANGELOG.md, creates the vX.Y.Z tag, and publishes a GitHub Release with generated notes. Because substrait-python derives its version from the git tag via setuptools_scm and publishes to PyPI via Trusted Publishing (OIDC), the publish step stays in the existing release.yml (filename preserved for the PyPI trusted publisher). It is repurposed to trigger on the tag pushed by semantic-release, dropping the now-duplicate GitHub Release job. The App token ensures the tag push triggers this workflow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 24 ++---------- .github/workflows/semantic-release.yml | 45 +++++++++++++++++++++++ .releaserc.json | 35 ++++++++++++++++++ RELEASING.md | 51 ++++++++++++++++++++++---- ci/release/run.sh | 14 +++++++ 5 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/semantic-release.yml create mode 100644 .releaserc.json create mode 100755 ci/release/run.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1864620..0ee53ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,12 +2,11 @@ name: Release and Publish on: push: - branches: [ main ] tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] permissions: id-token: write - contents: write + contents: read jobs: build: @@ -17,6 +16,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: @@ -33,27 +34,10 @@ jobs: name: dist path: dist/ retention-days: 1 - release: - name: Release to GitHub - runs-on: ubuntu-latest - needs: build - if: startsWith(github.ref, 'refs/tags/v') - steps: - - name: Download artifact - uses: actions/download-artifact@v8 - with: - name: dist - path: dist/ - - name: Publish to GitHub release page - uses: softprops/action-gh-release@v3 - with: - files: | - ./dist/*.whl - ./dist/*.tar.gz publish: name: Publish to PyPI runs-on: ubuntu-latest - needs: release + needs: build environment: pypi steps: - name: Download artifact diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml new file mode 100644 index 0000000..306e3dd --- /dev/null +++ b/.github/workflows/semantic-release.yml @@ -0,0 +1,45 @@ +name: Semantic Release + +on: + schedule: + # 2 AM on Sunday + - cron: "0 2 * * 0" + workflow_dispatch: + +concurrency: + group: release + cancel-in-progress: false + +jobs: + semantic-release: + if: github.repository == 'substrait-io/substrait-python' + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ secrets.RELEASER_ID }} + private-key: ${{ secrets.RELEASER_KEY }} + - name: Checkout code + uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Get bot user ID + id: bot-user-id + run: | + echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Run semantic-release + run: ./ci/release/run.sh + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + GIT_AUTHOR_NAME: "${{ steps.app-token.outputs.app-slug }}[bot]" + GIT_COMMITTER_NAME: "${{ steps.app-token.outputs.app-slug }}[bot]" + GIT_AUTHOR_EMAIL: "${{ steps.bot-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" + GIT_COMMITTER_EMAIL: "${{ steps.bot-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..5c042b1 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,35 @@ +{ + "branches": ["main"], + "preset": "conventionalcommits", + "plugins": [ + "@semantic-release/release-notes-generator", + [ + "@semantic-release/commit-analyzer", + { + "releaseRules": [ + { "breaking": true, "release": "minor" } + ] + } + ], + [ + "@semantic-release/changelog", + { + "changelogTitle": "Release Notes\n---", + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/github", + { + "successComment": false + } + ], + [ + "@semantic-release/git", + { + "assets": ["CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version}" + } + ] + ] +} diff --git a/RELEASING.md b/RELEASING.md index 300868c..c7e1af0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,14 +1,49 @@ # Releasing substrait-python -Given that you are a Substrait committer or PMC member and have the appropriate permissions, releasing a new version of substrait-python is done by simply creating new Github Release through the UI: +Releases of substrait-python are **fully automated** using +[semantic-release](https://semantic-release.gitbook.io/) and follow the same model +as the other Substrait projects (e.g. substrait-java). -1. Go to https://github.com/substrait-io/substrait-python/releases -2. Click `Draft a new release` -3. Enter the version to be released in the `Tag` field prefixed with a lower case `v`, e.g. `v0.99.0`. Then click `Create new tag` which tells Github to create the tag on publication of the release. -4. Click `Generate release notes` which will automatically populate the release title and release notes fields. -5. If you are happy with the release notes and you are ready for an immediate release simply click `Publish release` otherwise save the release as a draft for later. -6. Monitor the Github Actions release build for the newly created release and tag. +The [Semantic Release](.github/workflows/semantic-release.yml) workflow runs on a +weekly schedule (2 AM UTC on Sundays). It inspects the [Conventional +Commits](https://www.conventionalcommits.org/en/v1.0.0/) merged since the last +release, computes the next version, updates `CHANGELOG.md`, creates the `vX.Y.Z` +git tag, and publishes a GitHub Release with auto-generated notes. + +Creating the tag triggers the [Release and Publish](.github/workflows/release.yml) +workflow, which builds the package (the version is derived from the tag via +`setuptools_scm`) and publishes it to [PyPI](https://pypi.org/project/substrait/) +using Trusted Publishing. + +As a result, there is nothing to do by hand for a normal release — just make sure +your PR titles/commit messages follow the Conventional Commits specification (this +is enforced by the [PR Title Check](.github/workflows/pr_title.yml) workflow). + +## Triggering an off-cycle release + +If you need a release before the next scheduled run (and you are a Substrait +committer or PMC member with the appropriate permissions): + +1. Go to https://github.com/substrait-io/substrait-python/actions/workflows/semantic-release.yml +2. Click `Run workflow` and select the `main` branch. +3. Monitor the workflow run, then the triggered `Release and Publish` run, to + confirm the new version reaches PyPI. + +If there are no release-worthy commits since the last release (only `chore`, `docs`, +`ci`, etc.), semantic-release will report that no release is necessary and do +nothing. ## Versioning -substrait-python follows semantic versioning as described for the Substrait specification here: https://substrait.io/spec/versioning/. \ No newline at end of file +Version bumps are derived from commit types: + +| Commit type | Version bump | +| -------------------------------------------- | ------------ | +| `fix:` | patch | +| `feat:` | minor | +| breaking change (`feat!:`, `BREAKING CHANGE`)| minor | + +substrait-python follows semantic versioning as described for the Substrait +specification here: https://substrait.io/spec/versioning/. Because the project is +pre-1.0, breaking changes produce a **minor** bump rather than a major one (matching +substrait-java). diff --git a/ci/release/run.sh b/ci/release/run.sh new file mode 100755 index 0000000..2f7e8e0 --- /dev/null +++ b/ci/release/run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# shellcheck shell=bash + +set -euo pipefail + +npx --yes \ + -p semantic-release \ + -p "@semantic-release/commit-analyzer" \ + -p "@semantic-release/release-notes-generator" \ + -p "@semantic-release/changelog" \ + -p "@semantic-release/github" \ + -p "@semantic-release/git" \ + -p "conventional-changelog-conventionalcommits" \ + semantic-release --ci