chore: align Python release publishing with Ruby (#716) #155
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Release" | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - '.sampo/changesets/*.md' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # Concurrency control: only one release process can run at a time | |
| # This prevents race conditions if multiple releasable changesets merge simultaneously | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| check-changesets: | |
| name: Check for changesets | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has-changesets: ${{ steps.check.outputs.has-changesets }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| - name: Check for changesets | |
| id: check | |
| run: | | |
| changeset_count=$(find .sampo/changesets -name '*.md' 2>/dev/null | wc -l) | |
| if [ "$changeset_count" -gt 0 ]; then | |
| echo "has-changesets=true" >> "$GITHUB_OUTPUT" | |
| echo "Found $changeset_count changeset(s), ready to release" | |
| else | |
| echo "has-changesets=false" >> "$GITHUB_OUTPUT" | |
| echo "No changesets to release" | |
| fi | |
| notify-approval-needed: | |
| name: Notify Slack - Approval Needed | |
| needs: check-changesets | |
| if: needs.check-changesets.outputs.has-changesets == 'true' | |
| uses: posthog/.github/.github/workflows/notify-approval-needed.yml@5fc4680761e8ac29a61b212756230eba0e276d8c | |
| with: | |
| slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} | |
| slack_user_group_id: ${{ vars.GROUP_CLIENT_LIBRARIES_SLACK_GROUP_ID }} | |
| secrets: | |
| slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} | |
| posthog_project_api_key: ${{ secrets.POSTHOG_PROJECT_API_KEY }} | |
| # Bump versions, commit to main, and regenerate references. This is the single | |
| # approval-gated job (environment "Release"). It does NOT publish — publishing | |
| # happens in the `publish` matrix job below, which has no environment so the | |
| # matrix never multiplies approval prompts. | |
| version-bump: | |
| name: Bump versions and commit to main | |
| needs: [check-changesets, notify-approval-needed] | |
| runs-on: ubuntu-latest | |
| # Use `always()` to ensure the job runs even if notify-approval-needed is skipped, | |
| # but still depend on it to access `needs.notify-approval-needed.outputs.slack_ts` | |
| if: always() && needs.check-changesets.outputs.has-changesets == 'true' | |
| environment: "Release" # This will require an approval from a maintainer, they are notified in Slack above | |
| permissions: | |
| contents: write | |
| outputs: | |
| commit-hash: ${{ steps.commit-release.outputs.commit-hash }} | |
| new_version: ${{ steps.sampo-release.outputs.new_version }} | |
| steps: | |
| - name: Notify Slack - Approved | |
| if: needs.notify-approval-needed.outputs.slack_ts != '' | |
| uses: posthog/.github/.github/actions/slack-thread-reply@5fc4680761e8ac29a61b212756230eba0e276d8c | |
| with: | |
| slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} | |
| slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} | |
| thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} | |
| message: "✅ Release approved! Version bump in progress..." | |
| emoji_reaction: "white_check_mark" | |
| - name: Get GitHub App token | |
| id: releaser | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ secrets.GH_APP_POSTHOG_PYTHON_RELEASER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PYTHON_RELEASER_PRIVATE_KEY }} | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| token: ${{ steps.releaser.outputs.token }} | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: 3.11.11 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 | |
| with: | |
| enable-cache: true | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e | |
| with: | |
| toolchain: 1.91.1 | |
| components: cargo | |
| - name: Cache Sampo CLI | |
| id: cache-sampo | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.cargo/bin/sampo | |
| key: sampo-${{ runner.os }}-${{ runner.arch }} | |
| - name: Install Sampo CLI | |
| if: steps.cache-sampo.outputs.cache-hit != 'true' | |
| run: cargo install sampo | |
| - name: Install dependencies | |
| run: uv sync --extra dev | |
| # `sampo release` bumps every workspace package that has a pending changeset | |
| # (posthog at the root, and openfeature-provider-posthog as a workspace | |
| # member). new_version is the posthog version, used for the version.py sync | |
| # and the posthog tag. | |
| - name: Prepare release with Sampo | |
| id: sampo-release | |
| env: | |
| GITHUB_TOKEN: ${{ steps.releaser.outputs.token }} | |
| run: | | |
| # Sampo writes the root package changelog to CHANGELOG.md because the | |
| # posthog package metadata lives at the repository root. Keep the | |
| # user-facing root changelog as a package index and store posthog's | |
| # generated changelog alongside the package instead. | |
| cp posthog/CHANGELOG.md CHANGELOG.md | |
| sampo release | |
| mv CHANGELOG.md posthog/CHANGELOG.md | |
| printf '%s\n' \ | |
| '# Changelog' \ | |
| '' \ | |
| 'Package-specific changelogs live with each package:' \ | |
| '' \ | |
| '- [`posthog`](posthog/CHANGELOG.md)' \ | |
| '- [`openfeature-provider-posthog`](openfeature-provider/CHANGELOG.md)' \ | |
| > CHANGELOG.md | |
| new_version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") | |
| echo "new_version=$new_version" >> "$GITHUB_OUTPUT" | |
| - name: Sync version to posthog/version.py | |
| env: | |
| NEW_VERSION: ${{ steps.sampo-release.outputs.new_version }} | |
| run: | | |
| echo "VERSION = \"$NEW_VERSION\"" > posthog/version.py | |
| - name: Commit release changes | |
| id: commit-release | |
| uses: planetscale/ghcommit-action@25309d8005ac7c3bcd61d3fe19b69e0fe47dbdde # v0.2.20 | |
| with: | |
| commit_message: "chore: Release v${{ steps.sampo-release.outputs.new_version }} [skip ci]" | |
| repo: ${{ github.repository }} | |
| branch: main | |
| env: | |
| GITHUB_TOKEN: ${{ steps.releaser.outputs.token }} | |
| - name: Sync checkout to release commit | |
| if: steps.commit-release.outputs.commit-hash != '' | |
| env: | |
| COMMIT_HASH: ${{ steps.commit-release.outputs.commit-hash }} | |
| run: | | |
| git fetch origin main | |
| git reset --hard "$COMMIT_HASH" | |
| - name: Generate references | |
| if: steps.commit-release.outputs.commit-hash != '' | |
| run: | | |
| uv run bin/docs generate-references | |
| - name: Check for changes in references | |
| if: steps.commit-release.outputs.commit-hash != '' | |
| id: references-changes | |
| run: | | |
| if [ -n "$(git status --porcelain references/)" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| echo "New references generated in references directory:" | |
| git status --porcelain references/ | |
| else | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "No new references generated in references directory" | |
| fi | |
| - name: Commit generated references | |
| if: steps.commit-release.outputs.commit-hash != '' && steps.references-changes.outputs.changed == 'true' | |
| uses: planetscale/ghcommit-action@25309d8005ac7c3bcd61d3fe19b69e0fe47dbdde # v0.2.20 | |
| with: | |
| commit_message: "Update generated references" | |
| repo: ${{ github.repository }} | |
| branch: main | |
| file_pattern: references/ | |
| env: | |
| GITHUB_TOKEN: ${{ steps.releaser.outputs.token }} | |
| # Notify in case of a failure | |
| - name: Send failure event to PostHog | |
| if: ${{ failure() }} | |
| uses: PostHog/posthog-github-action@58dea254b598fb5d469c0699c98af8288a7f7650 # v1.2.0 | |
| with: | |
| posthog-token: "${{ secrets.POSTHOG_PROJECT_API_KEY }}" | |
| event: "posthog-python-github-release-workflow-failure" | |
| properties: >- | |
| { | |
| "commitSha": "${{ github.sha }}", | |
| "jobStatus": "${{ job.status }}", | |
| "ref": "${{ github.ref }}", | |
| "version": "${{ steps.sampo-release.outputs.new_version }}" | |
| } | |
| - name: Notify Slack - Failed | |
| if: ${{ failure() && needs.notify-approval-needed.outputs.slack_ts != '' }} | |
| uses: posthog/.github/.github/actions/slack-thread-reply@5fc4680761e8ac29a61b212756230eba0e276d8c | |
| with: | |
| slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} | |
| slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} | |
| thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} | |
| message: "❌ Failed to bump versions for `posthog-python@${{ steps.sampo-release.outputs.new_version }}`! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>" | |
| emoji_reaction: "x" | |
| # Build, publish, and tag each package whose version was bumped in this release. | |
| # A matrix over the publishable packages (N packages, like the posthog-ruby and | |
| # JS monorepos). The approval gate lives on `version-bump`, so this job has no | |
| # environment and the matrix never re-triggers approval. `max-parallel: 1` | |
| # publishes sequentially (posthog before its posthoganalytics mirror). | |
| # | |
| # Each package uses PyPI OIDC trusted publishing (no API tokens). A trusted | |
| # publisher must be registered for every package name (this workflow, | |
| # `publish` job) before its first release. | |
| publish: | |
| name: Publish ${{ matrix.package.name }} | |
| needs: [check-changesets, notify-approval-needed, version-bump] | |
| runs-on: ubuntu-latest | |
| if: always() && needs.version-bump.outputs.commit-hash != '' | |
| permissions: | |
| contents: write | |
| id-token: write | |
| strategy: | |
| fail-fast: true | |
| max-parallel: 1 | |
| matrix: | |
| package: | |
| - name: posthog | |
| version_file: pyproject.toml | |
| build: uv run make build_release | |
| packages_dir: dist | |
| tag_prefix: "posthog-v" | |
| changelog: posthog/CHANGELOG.md | |
| github_release: true | |
| # posthoganalytics is a build-time mirror of posthog (same version, no | |
| # separate tag/release); it always ships alongside posthog. | |
| - name: posthoganalytics | |
| version_file: pyproject.toml | |
| build: uv run make build_release_analytics | |
| packages_dir: dist | |
| tag_prefix: "" | |
| changelog: "" | |
| github_release: false | |
| - name: openfeature-provider-posthog | |
| version_file: openfeature-provider/pyproject.toml | |
| build: uv build --package openfeature-provider-posthog --out-dir openfeature-provider/dist | |
| packages_dir: openfeature-provider/dist | |
| tag_prefix: "openfeature-provider-posthog-v" | |
| changelog: openfeature-provider/CHANGELOG.md | |
| github_release: true | |
| steps: | |
| - name: Checkout release commit | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.version-bump.outputs.commit-hash }} | |
| fetch-depth: 0 | |
| - name: Configure Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Fetch tags | |
| run: git fetch --tags --force | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: 3.11.11 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --extra dev | |
| # Publish a package only if this release actually changed its version. The | |
| # release commit (HEAD) is compared to its parent: posthog/posthoganalytics | |
| # gate on the root pyproject, the provider on its own pyproject. | |
| - name: Detect ${{ matrix.package.name }} version change | |
| id: detect | |
| env: | |
| VERSION_FILE: ${{ matrix.package.version_file }} | |
| TAG_PREFIX: ${{ matrix.package.tag_prefix }} | |
| run: | | |
| if git diff --quiet HEAD~1 HEAD -- "$VERSION_FILE"; then | |
| echo "has-new-version=false" >> "$GITHUB_OUTPUT" | |
| echo "${{ matrix.package.name }}: no version change in this release; skipping." | |
| else | |
| version=$(python3 -c "import tomllib; print(tomllib.load(open('$VERSION_FILE','rb'))['project']['version'])") | |
| tag="${TAG_PREFIX}${version}" | |
| if [ -n "$TAG_PREFIX" ] && git rev-parse "$tag" >/dev/null 2>&1; then | |
| echo "has-new-version=false" >> "$GITHUB_OUTPUT" | |
| echo "${{ matrix.package.name }}: tag $tag already exists; skipping." | |
| else | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "has-new-version=true" >> "$GITHUB_OUTPUT" | |
| echo "${{ matrix.package.name }}: releasing $version" | |
| fi | |
| fi | |
| - name: Build ${{ matrix.package.name }} | |
| if: steps.detect.outputs.has-new-version == 'true' | |
| run: ${{ matrix.package.build }} | |
| - name: Publish ${{ matrix.package.name }} to PyPI | |
| if: steps.detect.outputs.has-new-version == 'true' | |
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 | |
| with: | |
| packages-dir: ${{ matrix.package.packages_dir }} | |
| - name: Tag ${{ matrix.package.name }} release | |
| if: steps.detect.outputs.has-new-version == 'true' && matrix.package.tag_prefix != '' | |
| env: | |
| TAG: ${{ steps.detect.outputs.tag }} | |
| COMMIT_HASH: ${{ needs.version-bump.outputs.commit-hash }} | |
| PACKAGE_NAME: ${{ matrix.package.name }} | |
| VERSION: ${{ steps.detect.outputs.version }} | |
| run: | | |
| git tag -a "$TAG" -m "$PACKAGE_NAME $VERSION" "$COMMIT_HASH" | |
| git push origin "$TAG" | |
| - name: Create ${{ matrix.package.name }} GitHub Release | |
| if: steps.detect.outputs.has-new-version == 'true' && matrix.package.github_release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ steps.detect.outputs.tag }} | |
| CHANGELOG_FILE: ${{ matrix.package.changelog }} | |
| run: | | |
| # Use the latest changelog section (lines between the first '## ' and the | |
| # next '## ') as release notes. Fall back to a pointer if the file is | |
| # missing or has no section yet, so a release is never blocked on notes. | |
| fallback="See $CHANGELOG_FILE for details." | |
| if [ -f "$CHANGELOG_FILE" ]; then | |
| awk ' | |
| /^## / { if (found_version) exit; found_version=1; next } | |
| found_version { lines[++n] = $0 } | |
| END { | |
| start = 1 | |
| while (start <= n && lines[start] ~ /^[[:space:]]*$/) start++ | |
| end = n | |
| while (end >= start && lines[end] ~ /^[[:space:]]*$/) end-- | |
| for (i = start; i <= end; i++) print lines[i] | |
| } | |
| ' "$CHANGELOG_FILE" > release-notes.md | |
| if [ -z "$(tr -d '[:space:]' < release-notes.md)" ]; then | |
| echo "$fallback" > release-notes.md | |
| fi | |
| else | |
| echo "$fallback" > release-notes.md | |
| fi | |
| gh release create "$TAG" \ | |
| --target "${{ needs.version-bump.outputs.commit-hash }}" \ | |
| --title "$TAG" \ | |
| --notes-file release-notes.md | |
| # Notify in case of a failure | |
| - name: Send failure event to PostHog | |
| if: ${{ failure() }} | |
| uses: PostHog/posthog-github-action@58dea254b598fb5d469c0699c98af8288a7f7650 # v1.2.0 | |
| with: | |
| posthog-token: "${{ secrets.POSTHOG_PROJECT_API_KEY }}" | |
| event: "posthog-python-github-release-workflow-failure" | |
| properties: >- | |
| { | |
| "commitSha": "${{ github.sha }}", | |
| "jobStatus": "${{ job.status }}", | |
| "ref": "${{ github.ref }}", | |
| "package": "${{ matrix.package.name }}", | |
| "version": "${{ needs.version-bump.outputs.new_version }}" | |
| } | |
| - name: Notify Slack - Failed | |
| if: ${{ failure() && needs.notify-approval-needed.outputs.slack_ts != '' }} | |
| uses: posthog/.github/.github/actions/slack-thread-reply@5fc4680761e8ac29a61b212756230eba0e276d8c | |
| with: | |
| slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} | |
| slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} | |
| thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} | |
| message: "❌ Failed to publish `${{ matrix.package.name }}@${{ needs.version-bump.outputs.new_version }}`! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>" | |
| emoji_reaction: "x" | |
| # Side-effect-free rehearsal: runs ONLY when this workflow is manually | |
| # dispatched on a non-main branch (e.g. this PR branch). It applies the Sampo | |
| # version bump, builds openfeature-provider-posthog, and publishes it to | |
| # TestPyPI via OIDC — exercising the version bump + the trusted-publisher | |
| # handshake without touching main, prod PyPI, git tags, or GitHub releases. | |
| # On real (push-to-main) releases this job is skipped, so the prod flow is | |
| # unaffected. Requires a TestPyPI trusted publisher for | |
| # openfeature-provider-posthog (workflow release.yml, no environment). | |
| dry-run-testpypi: | |
| name: Dry-run publish to TestPyPI (provider) | |
| if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Checkout dispatched branch | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: 3.11.11 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 | |
| with: | |
| enable-cache: true | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e | |
| with: | |
| toolchain: 1.91.1 | |
| components: cargo | |
| - name: Cache Sampo CLI | |
| id: cache-sampo | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ~/.cargo/bin/sampo | |
| key: sampo-${{ runner.os }}-${{ runner.arch }} | |
| - name: Install Sampo CLI | |
| if: steps.cache-sampo.outputs.cache-hit != 'true' | |
| run: cargo install sampo | |
| # Apply pending changesets to bump versions in the working tree (no commit). | |
| # This validates that Sampo discovers the workspace member and bumps it | |
| # from the changeset, exactly as the real version-bump job would. | |
| - name: Apply version bumps with Sampo (no commit) | |
| run: sampo release | |
| - name: Build openfeature-provider-posthog | |
| run: uv build --package openfeature-provider-posthog --out-dir openfeature-provider/dist | |
| - name: Publish to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 | |
| with: | |
| packages-dir: openfeature-provider/dist | |
| repository-url: https://test.pypi.org/legacy/ | |
| notify-released: | |
| name: Notify Slack - Released | |
| needs: [check-changesets, notify-approval-needed, version-bump, publish] | |
| runs-on: ubuntu-latest | |
| if: always() && needs.publish.result == 'success' && needs.notify-approval-needed.outputs.slack_ts != '' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Notify Slack - Released | |
| uses: posthog/.github/.github/actions/slack-thread-reply@5fc4680761e8ac29a61b212756230eba0e276d8c | |
| with: | |
| slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} | |
| slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} | |
| thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} | |
| message: "🚀 posthog-python released successfully!" | |
| emoji_reaction: "rocket" |