-
Notifications
You must be signed in to change notification settings - Fork 14
ci: replace the PR-title bump script with release-please #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a5db44a
4f88f41
8d94e3e
e8614a9
3613936
60db296
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| name: Lint PR title | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize] | ||
|
|
||
| permissions: | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| pr_title: | ||
| name: 👮 Conventional PR title | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: amannn/action-semantic-pull-request@v6 | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,223 +1,183 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - '*.x' | ||
| workflow_dispatch: | ||
| inputs: | ||
| version_bump: | ||
| description: 'Version bump type for manual release' | ||
| required: true | ||
| default: 'patch' | ||
| type: choice | ||
| options: | ||
| - patch | ||
| - minor | ||
| - major | ||
| use_current_version: | ||
| description: 'Skip version bump and publish the version already set in pyproject.toml' | ||
| publish_tag: | ||
| description: 'Existing tag to (re)publish to PyPI, e.g. v6.1.1. Leave empty for a normal release run.' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
| pull_request: | ||
| types: [closed] | ||
| branches: | ||
| - main | ||
|
|
||
| concurrency: | ||
| group: release-${{ github.event.pull_request.base.ref || github.ref_name }} | ||
| cancel-in-progress: false | ||
| default: '' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: release-${{ github.ref_name }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| prepare: | ||
| name: Prepare release | ||
| if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true | ||
| # Reversible half: keep the Release PR current. Never gated. | ||
| release-pr: | ||
| name: Release PR | ||
| if: >- | ||
| (github.event_name == 'push' || inputs.publish_tag == '') && | ||
| (github.ref_name == 'main' || endsWith(github.ref_name, '.x')) | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| should_release: ${{ steps.release_meta_final.outputs.should_release }} | ||
| bump: ${{ steps.release_meta_final.outputs.bump }} | ||
| previous_version: ${{ steps.release_meta_final.outputs.previous_version }} | ||
| version: ${{ steps.release_meta_final.outputs.version }} | ||
| tag: ${{ steps.release_meta_final.outputs.tag }} | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: write | ||
| issues: write | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - uses: googleapis/release-please-action@v4 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Must Fix] The tag and the GitHub Release are created before any test runs, and a red suite cannot be undone. This job has no The old workflow gated both on the suite. From the base commit: with State after a failed It is also not re-runnable in the obvious way. After tagging, release-please swaps the label on the merged Release PR ( Fix: split the action in two, as the getstream-go pilot does. Call it once with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8d94e3e. Restructured rather than patched, because the getstream-go two-call shape assumes a cheap gate and this one is a full matrix.
The relabel trap you describe is why the ordering matters and also why |
||
| with: | ||
| fetch-depth: 0 | ||
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }} | ||
|
|
||
| - name: Skip when PR is already released | ||
| id: already_released | ||
| run: | | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| echo "value=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| # Idempotency is tracked on the release tag (annotated with the PR number | ||
| # in "Create release tag"), not on a default-branch commit: the version | ||
| # bump is no longer pushed to the protected default branch. | ||
| if git for-each-ref refs/tags --format='%(contents)' | grep -q "(pr #${{ github.event.pull_request.number }})"; then | ||
| echo "value=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "value=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| fi | ||
|
|
||
| - name: Determine version bump (from PR metadata) | ||
| id: release_meta | ||
| if: github.event_name == 'pull_request' && steps.already_released.outputs.value != 'true' | ||
| config-file: release-please-config.json | ||
| manifest-file: .release-please-manifest.json | ||
| target-branch: ${{ github.ref_name }} | ||
| skip-github-release: true | ||
|
|
||
| # Tagging and the GitHub Release are irreversible, so the suite has to run before | ||
| # them, which means knowing a release is pending before the suite starts. The tag | ||
| # lands on the merged Release PR's merge commit, not on this branch's tip, so that | ||
| # commit is also what gets tested. | ||
| detect: | ||
| name: Detect pending release | ||
| if: >- | ||
| (github.event_name == 'push' || inputs.publish_tag == '') && | ||
| (github.ref_name == 'main' || endsWith(github.ref_name, '.x')) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| outputs: | ||
| pending: ${{ steps.find.outputs.pending }} | ||
| sha: ${{ steps.find.outputs.sha }} | ||
| steps: | ||
| - name: Find a merged Release PR waiting to be tagged | ||
| id: find | ||
| env: | ||
| PR_TITLE: ${{ github.event.pull_request.title }} | ||
| run: | | ||
| python3 scripts/release/bump_version.py \ | ||
| --title "$PR_TITLE" \ | ||
| --output "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Determine version bump (manual) | ||
| id: release_meta_manual | ||
| if: github.event_name == 'workflow_dispatch' | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| python3 scripts/release/bump_version.py \ | ||
| --manual-bump "${{ github.event.inputs.version_bump }}" \ | ||
| --use-current-version "${{ github.event.inputs.use_current_version }}" \ | ||
| --output "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Consolidate release metadata | ||
| id: release_meta_final | ||
| run: | | ||
| if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then | ||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||
| echo "bump=none" >> "$GITHUB_OUTPUT" | ||
| sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls?state=closed&base=${GITHUB_REF_NAME}&sort=updated&direction=desc&per_page=50" \ | ||
| --jq '[.[] | select(.merged_at != null and ([.labels[].name] | index("autorelease: pending")))] | .[0].merge_commit_sha // empty')" | ||
| if [ -z "$sha" ]; then | ||
| echo "No pending release." | ||
| echo "pending=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| echo "should_release=${{ steps.release_meta_manual.outputs.should_release }}" >> "$GITHUB_OUTPUT" | ||
| echo "bump=${{ steps.release_meta_manual.outputs.bump }}" >> "$GITHUB_OUTPUT" | ||
| echo "previous_version=${{ steps.release_meta_manual.outputs.previous_version }}" >> "$GITHUB_OUTPUT" | ||
| echo "version=${{ steps.release_meta_manual.outputs.version }}" >> "$GITHUB_OUTPUT" | ||
| echo "tag=${{ steps.release_meta_manual.outputs.tag }}" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "should_release=${{ steps.release_meta.outputs.should_release }}" >> "$GITHUB_OUTPUT" | ||
| echo "bump=${{ steps.release_meta.outputs.bump }}" >> "$GITHUB_OUTPUT" | ||
| echo "previous_version=${{ steps.release_meta.outputs.previous_version }}" >> "$GITHUB_OUTPUT" | ||
| echo "version=${{ steps.release_meta.outputs.version }}" >> "$GITHUB_OUTPUT" | ||
| echo "tag=${{ steps.release_meta.outputs.tag }}" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| echo "Pending release will be tagged at $sha." | ||
| echo "pending=true" >> "$GITHUB_OUTPUT" | ||
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | ||
|
|
||
| test-unit: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Should Fix] Every release commit now runs the unit matrix twice, at the same time, against the same shared Stream app.
That is 12 jobs duplicated per release (ruff, typecheck, 5 This pairing is new. The old trigger was
Either resolution is closed by the same fix: drop Related, and worth knowing when picking: all nine
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8d94e3e: Took that direction rather than dropping Left your Noted on the integration marker matching nothing outside
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow-up, with evidence. The Three non-video legs failed in the Fixed in 60db296: |
||
| name: Test (unit) | ||
| needs: prepare | ||
| if: needs.prepare.outputs.should_release == 'true' | ||
| needs: detect | ||
| if: needs.detect.outputs.pending == 'true' | ||
| uses: ./.github/workflows/run_tests.yml | ||
| with: | ||
| marker: 'not integration' | ||
| secrets: inherit | ||
|
|
||
| test-integration: | ||
| name: Test (integration) | ||
| needs: prepare | ||
| if: needs.prepare.outputs.should_release == 'true' | ||
| needs: detect | ||
| if: needs.detect.outputs.pending == 'true' | ||
| uses: ./.github/workflows/run_tests.yml | ||
| with: | ||
| marker: 'integration' | ||
| secrets: inherit | ||
|
|
||
| # Irreversible half. | ||
| release: | ||
| name: 🚀 Release | ||
| needs: [prepare, test-unit, test-integration] | ||
| if: needs.prepare.outputs.should_release == 'true' | ||
| name: 🚀 Tag and release | ||
| needs: [detect, test-unit, test-integration] | ||
| if: needs.detect.outputs.pending == 'true' | ||
| runs-on: ubuntu-latest | ||
| environment: pypi | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: write | ||
| id-token: write | ||
| issues: write | ||
| pull-requests: write | ||
| outputs: | ||
| release_created: ${{ steps.release.outputs.release_created }} | ||
| tag_name: ${{ steps.release.outputs.tag_name }} | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| with: | ||
| fetch-depth: 0 | ||
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }} | ||
|
|
||
| - uses: ./.github/actions/python-uv-setup | ||
|
|
||
| - name: Apply version bump | ||
| # The suite ran against this workflow's own commit, while the tag lands on the | ||
| # merged Release PR's merge commit. They are the same commit on the path that | ||
| # gets here, a push of that merge. They diverge on a dispatch after the branch | ||
| # has moved, which would tag a tree nothing tested, so refuse instead. Recovery | ||
| # is "Re-run failed jobs" on the run for the merge itself. | ||
| - name: Refuse to tag a commit the suite did not run on | ||
| env: | ||
| VERSION: ${{ needs.prepare.outputs.version }} | ||
| TESTED: ${{ github.sha }} | ||
| PENDING: ${{ needs.detect.outputs.sha }} | ||
| run: | | ||
| python3 - <<'PY' | ||
| import os, re | ||
| from pathlib import Path | ||
| version = os.environ['VERSION'] | ||
| path = Path('pyproject.toml') | ||
| text = path.read_text(encoding='utf-8') | ||
| pattern = re.compile(r'^version\s*=\s*"\d+\.\d+\.\d+"\s*$', re.MULTILINE) | ||
| new_text, count = pattern.subn(f'version = "{version}"', text, count=1) | ||
| if count == 0: | ||
| raise SystemExit('Could not update version line in pyproject.toml') | ||
| path.write_text(new_text, encoding='utf-8') | ||
| PY | ||
|
|
||
| # Commit the bump locally only, so the release tag points at a tree with the | ||
| # correct version. It is intentionally NOT pushed to the protected default | ||
| # branch (which rejects direct pushes). Versioning is driven by tags, not by | ||
| # pyproject.toml on the default branch (see bump_version.py: find_latest_semver_tag). | ||
| - name: Commit version bump (local, for tagging only) | ||
| env: | ||
| VERSION: ${{ needs.prepare.outputs.version }} | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add pyproject.toml | ||
| if git diff --cached --quiet; then | ||
| echo "No version changes to commit." | ||
| else | ||
| git commit -m "chore(release): v${VERSION}" | ||
| if [ "$TESTED" != "$PENDING" ]; then | ||
| echo "::error::The pending release is tagged at $PENDING but this run tested $TESTED." | ||
| echo "::error::Re-run the workflow run for $PENDING instead of dispatching from the branch tip." | ||
| exit 1 | ||
| fi | ||
| echo "Tagging $PENDING, which is the commit the suite ran on." | ||
|
|
||
| - name: Create release tag | ||
| - uses: googleapis/release-please-action@v4 | ||
| id: release | ||
| with: | ||
| config-file: release-please-config.json | ||
| manifest-file: .release-please-manifest.json | ||
| target-branch: ${{ github.ref_name }} | ||
| skip-github-pull-request: true | ||
|
|
||
| # Chained rather than triggered on the release event, because a GitHub Release | ||
| # created with GITHUB_TOKEN starts no new workflow run. Also reachable on its own | ||
| # through workflow_dispatch with publish_tag, which is the recovery path once | ||
| # GitHub has retired the original run and "Re-run failed jobs" is gone. | ||
| publish: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Should Fix] Both fall back to the 360 minute default; only Fix: Separately on this job: the tests and the publish build different trees. Every checkout in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both fixed in 8d94e3e. The tree divergence is closed rather than documented around:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correction on the second half of this. The Passing a ref into the reusable workflow gave CodeQL four high Instead the tests run on the workflow's own commit and the release job refuses to tag when that is not the commit the pending Release PR would be tagged at. On the path that reaches it, a push of that merge, the two are the same commit. That is a stronger guarantee than the ref input gave: previously a dispatch could still tag the merge commit after testing it, now a dispatch that would tag anything the run did not test fails instead. README reworded to match. The |
||
| name: 📦 Publish to PyPI | ||
| needs: release | ||
| if: >- | ||
| !cancelled() && | ||
| (inputs.publish_tag != '' || needs.release.outputs.release_created == 'true') | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| # PyPI Trusted Publishing is bound to this workflow file name and this environment. | ||
| environment: pypi | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| steps: | ||
| - name: Resolve tag | ||
| id: target | ||
| env: | ||
| TAG: ${{ needs.prepare.outputs.tag }} | ||
| PR: ${{ github.event.pull_request.number }} | ||
| PUBLISH_TAG: ${{ inputs.publish_tag }} | ||
| RELEASE_TAG: ${{ needs.release.outputs.tag_name }} | ||
| run: | | ||
| if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then | ||
| echo "Tag ${TAG} already exists; skipping tag creation." | ||
| exit 0 | ||
| fi | ||
| # Annotate the tag with the trigger; the PR number is used by the | ||
| # "Skip when PR is already released" idempotency check. | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| git tag -a "${TAG}" -m "chore(release): ${TAG} (manual)" | ||
| else | ||
| git tag -a "${TAG}" -m "chore(release): ${TAG} (pr #${PR})" | ||
| fi | ||
| git push origin "${TAG}" | ||
| tag="${PUBLISH_TAG:-$RELEASE_TAG}" | ||
| echo "tag=${tag}" >> "$GITHUB_OUTPUT" | ||
| echo "version=${tag#v}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Clean dist directory | ||
| run: rm -rf dist | ||
| - uses: actions/checkout@v5 | ||
| with: | ||
| ref: ${{ steps.target.outputs.tag }} | ||
| # uv build and uv publish execute project and dependency code, and nothing | ||
| # here writes to the repository, so do not leave GITHUB_TOKEN in .git/config. | ||
| persist-credentials: false | ||
|
|
||
| - uses: ./.github/actions/python-uv-setup | ||
|
|
||
| - name: Build distributions | ||
| run: uv build | ||
|
|
||
| - name: Publish to PyPI (Trusted Publishing) | ||
| run: uv publish | ||
|
|
||
| - name: Create release on GitHub | ||
| uses: ncipollo/release-action@v1 | ||
| with: | ||
| tag: ${{ needs.prepare.outputs.tag }} | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| skipIfReleaseExists: true | ||
| body: | | ||
| Release v${{ needs.prepare.outputs.version }} | ||
|
|
||
| - Bump type: `${{ needs.prepare.outputs.bump }}` | ||
| - Previous: `${{ needs.prepare.outputs.previous_version }}` | ||
| - Next: `${{ needs.prepare.outputs.version }}` | ||
| - Trigger: `${{ github.event_name }}` | ||
|
|
||
| Install with: `pip install getstream==${{ needs.prepare.outputs.version }}` | ||
|
|
||
| - name: Verify pip install | ||
| env: | ||
| UV_NO_SOURCES: "1" | ||
| run: | | ||
| uv pip install "getstream==${{ needs.prepare.outputs.version }}" || \ | ||
| uv pip install "getstream==${{ steps.target.outputs.version }}" || \ | ||
| echo "WARNING: pip install verification failed (PyPI index may need a moment to propagate)" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| ".": "6.1.0" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Should Fix] No manual republish path survives, and the one retry that works expires.
The old workflow's
workflow_dispatchtookversion_bump(patch/minor/major) anduse_current_version, which computed from the latest tag and gave an admin a one-click cut at any time. This bareworkflow_dispatchruns release-please, which finds noautorelease: pendingPR after a tag already exists and so outputsrelease_createdfalse, skippingpublish.So after a failed publish the only recovery is "Re-run failed jobs" on the original run, and GitHub retires re-runs after a finite window. Past it, the tagged and GitHub-Released version can never reach PyPI; the only way forward is landing a new commit and cutting a new version.
README.md:264presents the re-run as the retry path without saying it is the only one or that it expires.In fairness,
use_current_versionwas already broken onmain, sincepyproject.tomlhas been stale at 4.1.0 since #274. What is genuinely lost is the tag-derived manual cut.Minor, same line:
workflow_dispatchis not restricted tomainand*.xthe waypushis, andtarget-branch: ${{ github.ref_name }}follows whatever ref it is dispatched from. Dispatching from a feature branch grooms a Release PR against that branch withcontents: writeandpull-requests: writebehind it. Stray Release PRs rather than a bad tag, but a job-level guard closes it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8d94e3e, both halves.
workflow_dispatchnow takespublish_tag. Set it to an existing tag and the run skips release-please entirely and goes straight to build and publish for that tag, so a tagged version can still reach PyPI after GitHub has retired the original run. Left empty it behaves as a normal release run. The README says this instead of presenting the re-run as the retry path.The branch guard is on
release-pranddetect:github.ref_name == 'main' || endsWith(github.ref_name, '.x'). Dispatching from a feature branch now does nothing.Not restoring the tag-derived manual cut. Under release-please a version is a merged Release PR, and a second path that invents one would put the manifest and the tags out of step, which is the failure this PR exists to remove.
Release-As:covers the real need.