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