Skip to content

chore(openfeature): pin mixpanel >=5.3.0 for FallbackReason (SDK-126)… #3

chore(openfeature): pin mixpanel >=5.3.0 for FallbackReason (SDK-126)…

chore(openfeature): pin mixpanel >=5.3.0 for FallbackReason (SDK-126)… #3

Workflow file for this run

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"