chore(openfeature): pin mixpanel >=5.3.0 for FallbackReason (SDK-126)… #3
Workflow file for this run
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: Publish Release to PyPI | |
| # Tag-triggered: PyPI Trusted Publishing requires the publish to come from a | |
| # workflow run triggered by a tag-push matching the patterns configured in the | |
| # project's PyPI Trusted Publisher entry. Add new tag prefixes here as | |
| # additional modules are added to .github/modules.json (e.g. `- 'foo-v*'`). | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| - 'openfeature/v*' | |
| concurrency: | |
| group: pypi-publish | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # create the draft GitHub release | |
| id-token: write # OIDC for PyPI trusted publishing | |
| jobs: | |
| publish: | |
| name: "Publish ${{ github.ref_name }} to PyPI" | |
| runs-on: ubuntu-latest | |
| environment: release | |
| timeout-minutes: 30 | |
| # One-time PyPI / GitHub Environment / tag ruleset setup is required. | |
| # The `environment:` name above must match what the PyPI project's | |
| # Trusted Publisher entry is configured to require. | |
| steps: | |
| - name: Checkout tagged commit | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve module from tag | |
| id: module | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| MODULE=$(jq -r --arg t "$TAG" ' | |
| to_entries | |
| | map(select(.value.tag_prefix as $p | $t | startswith($p))) | |
| | max_by(.value.tag_prefix | length) | |
| | .key // empty | |
| ' .github/modules.json) | |
| if [ -z "$MODULE" ]; then | |
| echo "::error::Tag '$TAG' does not match any module tag_prefix in .github/modules.json" | |
| exit 1 | |
| fi | |
| MODULE_CONFIG=$(jq -e --arg m "$MODULE" '.[$m]' .github/modules.json) | |
| TAG_PREFIX=$(echo "$MODULE_CONFIG" | jq -r '.tag_prefix') | |
| VERSION="${TAG#${TAG_PREFIX}}" | |
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | |
| echo "::error::Version '$VERSION' (from tag '$TAG') is not a valid semver" | |
| exit 1 | |
| fi | |
| PYPROJECT_TOML=$(echo "$MODULE_CONFIG" | jq -r '.pyproject_toml') | |
| PROJECT_DIR=$(dirname "$PYPROJECT_TOML") | |
| { | |
| echo "module=$MODULE" | |
| echo "version=$VERSION" | |
| echo "tag_prefix=$TAG_PREFIX" | |
| echo "pyproject_toml=$PYPROJECT_TOML" | |
| echo "project_dir=$PROJECT_DIR" | |
| echo "version_files<<EOF" | |
| echo "$MODULE_CONFIG" | jq -r '.version_files[]?' | |
| echo "EOF" | |
| echo "changelog=$(echo "$MODULE_CONFIG" | jq -r '.changelog')" | |
| echo "package_name=$(echo "$MODULE_CONFIG" | jq -r '.package_name')" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Resolved tag '$TAG' -> module '$MODULE', version '$VERSION', project dir '$PROJECT_DIR'" | |
| - name: Verify tag commit is on master | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| git fetch origin master | |
| TAG_SHA=$(git rev-parse HEAD) | |
| if ! git merge-base --is-ancestor "$TAG_SHA" origin/master; then | |
| echo "::error::Tag '$TAG' ($TAG_SHA) is not an ancestor of origin/master." | |
| echo "Tags must be pushed from a commit on the master branch." | |
| exit 1 | |
| fi | |
| - name: Validate package version matches tag | |
| env: | |
| VERSION: ${{ steps.module.outputs.version }} | |
| PYPROJECT_TOML: ${{ steps.module.outputs.pyproject_toml }} | |
| VERSION_FILES: ${{ steps.module.outputs.version_files }} | |
| run: | | |
| set -euo pipefail | |
| PKG_VERSION=$(python3 - <<PY | |
| import os | |
| try: | |
| import tomllib | |
| except ModuleNotFoundError: | |
| import tomli as tomllib | |
| with open(os.environ["PYPROJECT_TOML"], "rb") as f: | |
| data = tomllib.load(f) | |
| project = data.get("project", {}) | |
| dynamic = project.get("dynamic", []) or [] | |
| if "version" in dynamic: | |
| # Read the version literal from the first version_files entry. | |
| files = [f for f in os.environ.get("VERSION_FILES", "").splitlines() if f.strip()] | |
| if not files: | |
| raise SystemExit("dynamic version but no version_files configured") | |
| import re | |
| with open(files[0]) as vf: | |
| text = vf.read() | |
| m = re.search(r"^__version__\s*=\s*['\"]([^'\"]+)['\"]", text, re.M) | |
| if not m: | |
| raise SystemExit(f"no __version__ literal in {files[0]}") | |
| print(m.group(1)) | |
| else: | |
| print(project.get("version", "")) | |
| PY | |
| ) | |
| if [ "$VERSION" != "$PKG_VERSION" ]; then | |
| echo "::error::Tag version '$VERSION' does not match package version '$PKG_VERSION'" | |
| exit 1 | |
| fi | |
| echo "Version confirmed: $PKG_VERSION" | |
| - name: Setup Python | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install build tooling | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install build twine | |
| - name: Install package and run tests | |
| env: | |
| MODULE: ${{ steps.module.outputs.module }} | |
| run: | | |
| python -m pip install -e .[test] | |
| if [ "$MODULE" = "openfeature" ]; then | |
| python -m pip install -e ./openfeature-provider[test] | |
| pytest openfeature-provider/tests/ | |
| else | |
| pytest | |
| fi | |
| - name: Build distribution | |
| env: | |
| PROJECT_DIR: ${{ steps.module.outputs.project_dir }} | |
| run: | | |
| set -euo pipefail | |
| rm -rf dist | |
| # `python -m build` writes to ./dist relative to its working directory. | |
| # For sub-projects (openfeature) we cd into the project dir, build, | |
| # then move artifacts to a top-level ./dist for the upload step. | |
| if [ "$PROJECT_DIR" = "." ]; then | |
| python -m build | |
| else | |
| (cd "$PROJECT_DIR" && python -m build) | |
| mkdir -p dist | |
| mv "$PROJECT_DIR"/dist/* dist/ | |
| fi | |
| ls -la dist | |
| - name: Validate distribution | |
| run: python -m twine check dist/* | |
| - name: Extract changelog section for tag | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| CHANGELOG: ${{ steps.module.outputs.changelog }} | |
| run: | | |
| # Extract this tag's section from CHANGELOG.md via a sed range. | |
| # The address `\@^## \[TAG\]@,\@^## \[@` selects from the version | |
| # header through the next `## [` line; the inner block deletes | |
| # both markers and prints the body. sed range patterns don't test | |
| # the end pattern against the start line, so the start doesn't | |
| # self-terminate. `\@...@` switches the address delimiter from | |
| # `/` to `@` so tags containing `/` (e.g. `openfeature/v0.1.0`) | |
| # don't conflict with the default `/`. Mirrors the deployed | |
| # mixpanel-android extraction logic. Falls back to a placeholder | |
| # if the section is missing or empty. | |
| sed -n '\@^## \['"${TAG}"'\]@,\@^## \[@{\@^## \['"${TAG}"'\]@d;\@^## \[@d;p;}' "$CHANGELOG" 2>/dev/null > release_notes.md || true | |
| if [ ! -s release_notes.md ]; then | |
| echo "Release $TAG" > release_notes.md | |
| fi | |
| echo "--- release_notes.md ---" | |
| cat release_notes.md | |
| - name: Create draft GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| # Idempotent: if a draft release for this tag already exists, leave it alone. | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "GitHub release for $TAG already exists; skipping creation" | |
| else | |
| gh release create "$TAG" \ | |
| --draft \ | |
| --title "$TAG" \ | |
| --notes-file release_notes.md | |
| fi | |
| # Publish last - PyPI uploads are irreversible (releases can be yanked | |
| # but not re-uploaded under the same version). The `release` GitHub | |
| # Environment's required reviewer acts as the human gate. | |
| # | |
| # Trusted Publishing: twine >= 5 picks up the GitHub OIDC token | |
| # automatically when `id-token: write` is set and no | |
| # username/password/token is provided. | |
| - name: Publish to PyPI | |
| run: python -m twine upload --skip-existing --non-interactive dist/* | |
| - name: Summary | |
| env: | |
| MODULE: ${{ steps.module.outputs.module }} | |
| VERSION: ${{ steps.module.outputs.version }} | |
| PACKAGE_NAME: ${{ steps.module.outputs.package_name }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| { | |
| echo "## ${MODULE} ${VERSION} published" | |
| echo "" | |
| echo "- [PyPI](https://pypi.org/project/${PACKAGE_NAME}/${VERSION}/)" | |
| echo "- [Draft GitHub Release](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG})" | |
| echo "" | |
| echo "### Next step" | |
| echo "Review the draft GitHub release and click **Publish release** to make it live." | |
| } >> "$GITHUB_STEP_SUMMARY" |