From 8c35b8c092599d795c2123a4b2b5696124c989ae Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 09:01:15 -0400 Subject: [PATCH 1/4] chore: standardize release process Adds the shared release infrastructure (prepare-release, release-pypi, pr-title-check workflows + modules.json + generate-changelog.sh) so this repo's release flow matches the other Mixpanel SDK repositories. Notes specific to this repo: - The analytics module's version source is dynamic (`pyproject.toml` declares `version = {attr = "mixpanel.__version__"}`), so `version_files` points at `mixpanel/__init__.py` where the literal `__version__` lives. The prepare workflow detects dynamic vs literal automatically. - The legacy `CHANGES.txt` is preserved as-is for historical continuity. New releases will append to a new top-level `CHANGELOG.md` (and to `openfeature-provider/CHANGELOG.md`) so the auto-generated section format matches every other standardized repo. - Publishing uses PyPI Trusted Publishing (OIDC); no long-lived API tokens. See: https://www.notion.so/348e0ba925628029af63c779caa835f9 --- .github/modules.json | 18 ++ .github/scripts/generate-changelog.sh | 80 +++++++++ .github/workflows/pr-title-check.yml | 49 ++++++ .github/workflows/prepare-release.yml | 238 +++++++++++++++++++++++++ .github/workflows/release-pypi.yml | 242 ++++++++++++++++++++++++++ CHANGELOG.md | 6 + README.md | 2 + openfeature-provider/CHANGELOG.md | 4 + openfeature-provider/README.md | 2 + 9 files changed, 641 insertions(+) create mode 100644 .github/modules.json create mode 100755 .github/scripts/generate-changelog.sh create mode 100644 .github/workflows/pr-title-check.yml create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release-pypi.yml create mode 100644 CHANGELOG.md create mode 100644 openfeature-provider/CHANGELOG.md diff --git a/.github/modules.json b/.github/modules.json new file mode 100644 index 0000000..ba1a184 --- /dev/null +++ b/.github/modules.json @@ -0,0 +1,18 @@ +{ + "analytics": { + "tag_prefix": "v", + "pyproject_toml": "pyproject.toml", + "version_files": ["mixpanel/__init__.py"], + "changelog": "CHANGELOG.md", + "readme": "README.md", + "package_name": "mixpanel" + }, + "openfeature": { + "tag_prefix": "openfeature/v", + "pyproject_toml": "openfeature-provider/pyproject.toml", + "version_files": [], + "changelog": "openfeature-provider/CHANGELOG.md", + "readme": "openfeature-provider/README.md", + "package_name": "mixpanel-openfeature" + } +} diff --git a/.github/scripts/generate-changelog.sh b/.github/scripts/generate-changelog.sh new file mode 100755 index 0000000..8f289c7 --- /dev/null +++ b/.github/scripts/generate-changelog.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -euo pipefail + +MODULE="$1" +VERSION_LABEL="$2" +REPO_URL="$3" +END_REF="${4:-HEAD}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MODULES_JSON="$SCRIPT_DIR/../modules.json" + +TAG_PREFIX=$(jq -e -r --arg m "$MODULE" '.[$m].tag_prefix' "$MODULES_JSON") || { + echo "Unknown module: $MODULE. Valid modules: $(jq -r 'keys | join(", ")' "$MODULES_JSON")" >&2 + exit 1 +} +TAG_GLOB="${TAG_PREFIX}*" + +PREVIOUS_TAG=$(git tag --sort=-creatordate --list "$TAG_GLOB" | head -1 || true) + +if [ -z "$PREVIOUS_TAG" ]; then + RANGE="$END_REF" +else + RANGE="${PREVIOUS_TAG}..${END_REF}" +fi + +DATE=$(date +%Y-%m-%d) +SAFE_URL=$(printf '%s' "$REPO_URL" | sed 's|[&/\]|\\&|g') + +declare -a FEATURES=() +declare -a FIXES=() +declare -a CHORES=() + +while IFS= read -r line; do + [ -z "$line" ] && continue + MSG=$(echo "$line" | cut -d' ' -f2-) + + if [[ "$MSG" =~ ^(feat|fix|chore)\((${MODULE}|all)\):\ (.+) ]]; then + TYPE="${BASH_REMATCH[1]}" + DESC="${BASH_REMATCH[3]}" + + DESC=$(echo "$DESC" | sed -E "s|\(#([0-9]+)\)|([#\1](${SAFE_URL}/pull/\1))|g") + + case "$TYPE" in + feat) FEATURES+=("$DESC") ;; + fix) FIXES+=("$DESC") ;; + chore) CHORES+=("$DESC") ;; + esac + fi +done < <(git log --oneline "$RANGE") + +echo "## [${VERSION_LABEL}](${REPO_URL}/tree/${VERSION_LABEL}) (${DATE})" +echo "" + +if [ ${#FEATURES[@]} -gt 0 ]; then + echo "### Features" + for entry in "${FEATURES[@]}"; do + echo "- ${entry}" + done + echo "" +fi + +if [ ${#FIXES[@]} -gt 0 ]; then + echo "### Fixes" + for entry in "${FIXES[@]}"; do + echo "- ${entry}" + done + echo "" +fi + +if [ ${#CHORES[@]} -gt 0 ]; then + echo "### Chores" + for entry in "${CHORES[@]}"; do + echo "- ${entry}" + done + echo "" +fi + +if [ -n "$PREVIOUS_TAG" ]; then + echo "[Full Changelog](${REPO_URL}/compare/${PREVIOUS_TAG}...${VERSION_LABEL})" +fi diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000..efe296d --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,49 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + +jobs: + check-title: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: .github/modules.json + sparse-checkout-cone-mode: false + + - name: Check PR title format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + MODULE_LIST=$(jq -r 'keys | join("|")' .github/modules.json) + # Scope is optional. Bare or scoped to a known module both pass. + MAIN_PATTERN="^(feat|fix|chore)(\((${MODULE_LIST})\))?: .+" + RELEASE_PATTERN="^release: .+" + + if [[ "$PR_TITLE" =~ $MAIN_PATTERN ]] || [[ "$PR_TITLE" =~ $RELEASE_PATTERN ]]; then + echo "PR title is valid: $PR_TITLE" + exit 0 + fi + + echo "PR title does not match the required format." + echo "" + echo " Got: $PR_TITLE" + echo "" + echo "Expected one of:" + echo " feat: description" + echo " fix: description" + echo " chore: description" + echo " feat(): description" + echo " fix(): description" + echo " chore(): description" + echo " release: description" + echo "" + echo "Valid modules: ${MODULE_LIST//|/, }" + exit 1 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..e178045 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,238 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + module: + description: 'Module to release (must match a key in .github/modules.json)' + required: true + type: string + version: + description: 'Release version (e.g., 1.3.0 or 1.3.0-beta.1)' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: prepare-release-${{ inputs.module }} + cancel-in-progress: false + +jobs: + prepare: + name: "Prepare ${{ inputs.module }} ${{ inputs.version }}" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate inputs + env: + MODULE: ${{ inputs.module }} + VERSION: ${{ inputs.version }} + run: | + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Invalid version format: $VERSION" + exit 1 + fi + jq -e --arg m "$MODULE" '.[$m]' .github/modules.json > /dev/null || { + echo "::error::Unknown module '$MODULE'. Valid modules: $(jq -r 'keys | join(", ")' .github/modules.json)" + exit 1 + } + + - name: Resolve module config + id: config + env: + MODULE: ${{ inputs.module }} + VERSION: ${{ inputs.version }} + run: | + MODULE_CONFIG=$(jq -e --arg m "$MODULE" '.[$m]' .github/modules.json) + + TAG_PREFIX=$(echo "$MODULE_CONFIG" | jq -r '.tag_prefix') + { + echo "tag=${TAG_PREFIX}${VERSION}" + echo "tag_prefix=${TAG_PREFIX}" + echo "pyproject_toml=$(echo "$MODULE_CONFIG" | jq -r '.pyproject_toml')" + # Newline-separated list of additional Python source files holding the version literal. + echo "version_files<> "$GITHUB_OUTPUT" + + - name: Validate version not already released + env: + TAG: ${{ steps.config.outputs.tag }} + run: | + if git tag -l "$TAG" | grep -q .; then + echo "::error::Tag $TAG already exists" + exit 1 + fi + + - name: Clean up existing release branch and PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ steps.config.outputs.branch }} + run: | + EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) + if [[ -n "$EXISTING_PR" ]]; then + echo "Closing existing PR #$EXISTING_PR and deleting branch" + gh pr close "$EXISTING_PR" --delete-branch + elif git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "Deleting orphaned branch $BRANCH" + git push origin --delete "$BRANCH" + fi + + - name: Create release branch + env: + BRANCH: ${{ steps.config.outputs.branch }} + run: git checkout -b "$BRANCH" + + - name: Bump version + env: + VERSION: ${{ inputs.version }} + PYPROJECT_TOML: ${{ steps.config.outputs.pyproject_toml }} + VERSION_FILES: ${{ steps.config.outputs.version_files }} + run: | + set -euo pipefail + + # If pyproject.toml uses dynamic version, the canonical source lives + # in the listed Python file(s) (e.g. `__version__ = "X.Y.Z"`). Otherwise + # the literal `version = "X.Y.Z"` line in pyproject.toml is the source. + IS_DYNAMIC=$(python3 - </dev/null; then + echo "::error::No __version__ literal found in $vf" + exit 1 + fi + OLD=$(grep -E "^__version__\s*=\s*['\"][^'\"]+['\"]" "$vf" | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/") + echo "Bumping ${vf}: ${OLD} -> ${VERSION}" + sed -i.bak -E "s|^__version__[[:space:]]*=[[:space:]]*['\"][^'\"]+['\"]|__version__ = \"${VERSION}\"|" "$vf" + rm -f "${vf}.bak" + done <<< "$VERSION_FILES" + else + if ! grep -E "^version[[:space:]]*=[[:space:]]*\"[^\"]+\"" "$PYPROJECT_TOML" >/dev/null; then + echo "::error::No literal version line found in $PYPROJECT_TOML" + exit 1 + fi + OLD=$(grep -E "^version[[:space:]]*=[[:space:]]*\"[^\"]+\"" "$PYPROJECT_TOML" | head -1 | sed -E "s/.*\"([^\"]+)\".*/\1/") + echo "Bumping ${PYPROJECT_TOML}: ${OLD} -> ${VERSION}" + sed -i.bak -E "s|^version[[:space:]]*=[[:space:]]*\"[^\"]+\"|version = \"${VERSION}\"|" "$PYPROJECT_TOML" + rm -f "${PYPROJECT_TOML}.bak" + fi + + - name: Update README version header + env: + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + TAG: ${{ steps.config.outputs.tag }} + README: ${{ steps.config.outputs.readme }} + run: | + DATE=$(date +"%B %d, %Y") + # The `1,/pat/` address range bounds substitution to the first match + # so older changelog entries inside the README aren't trampled. + sed -i -E \ + "1,/^##### _.*_ - \[.*\]\(.*\)\$/ s|^##### _.*_ - \[.*\]\(.*\)\$|##### _${DATE}_ - [${TAG}](${REPO_URL}/releases/tag/${TAG})|" \ + "$README" + + - name: Generate changelog + env: + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + MODULE: ${{ inputs.module }} + TAG: ${{ steps.config.outputs.tag }} + CHANGELOG_FILE: ${{ steps.config.outputs.changelog }} + run: | + CHANGELOG=$(.github/scripts/generate-changelog.sh \ + "$MODULE" "$TAG" "$REPO_URL" HEAD) + + if [ -f "$CHANGELOG_FILE" ]; then + { + printf '# Changelog\n\n%s\n' "$CHANGELOG" + sed '1{/^# Changelog$/d;}' "$CHANGELOG_FILE" + } > CHANGELOG.new.md + mv CHANGELOG.new.md "$CHANGELOG_FILE" + else + mkdir -p "$(dirname "$CHANGELOG_FILE")" + printf '# Changelog\n\n%s\n' "$CHANGELOG" > "$CHANGELOG_FILE" + fi + + - name: Commit and push + env: + MODULE: ${{ inputs.module }} + VERSION: ${{ inputs.version }} + BRANCH: ${{ steps.config.outputs.branch }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "release: prepare ${MODULE} ${VERSION}" + git push origin "$BRANCH" + + - name: Create pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MODULE: ${{ inputs.module }} + VERSION: ${{ inputs.version }} + TAG: ${{ steps.config.outputs.tag }} + TAG_PREFIX: ${{ steps.config.outputs.tag_prefix }} + PYPROJECT_TOML: ${{ steps.config.outputs.pyproject_toml }} + CHANGELOG_FILE: ${{ steps.config.outputs.changelog }} + README: ${{ steps.config.outputs.readme }} + BRANCH: ${{ steps.config.outputs.branch }} + run: | + gh pr create \ + --title "release: prepare ${MODULE} ${VERSION}" \ + --body "$(cat <> "$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 - < release_notes.md + import os, re, sys + tag = os.environ["TAG"] + changelog_path = os.environ["CHANGELOG"] + try: + content = open(changelog_path).read() + except FileNotFoundError: + print(f"Release {tag}") + sys.exit(0) + pattern = r'^## \[' + re.escape(tag) + r'\].*?\n(.*?)(?=^## |\Z)' + match = re.search(pattern, content, re.DOTALL | re.MULTILINE) + if match: + print(match.group(1).strip()) + else: + print(f"Release {tag}") + PY + 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" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..92efc3a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +Release notes for the `mixpanel` Python package will be added here starting +with the first release made under the standardized release process. + +For prior history, see [`CHANGES.txt`](./CHANGES.txt). diff --git a/README.md b/README.md index 35586b5..97b902d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # mixpanel-python +##### _May 13, 2026_ - [v5.1.0](https://github.com/mixpanel/mixpanel-python/releases/tag/v5.1.0) + [![PyPI](https://img.shields.io/pypi/v/mixpanel)](https://pypi.org/project/mixpanel) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mixpanel)](https://pypi.org/project/mixpanel) [![PyPI - Downloads](https://img.shields.io/pypi/dm/mixpanel)](https://pypi.org/project/mixpanel) diff --git a/openfeature-provider/CHANGELOG.md b/openfeature-provider/CHANGELOG.md new file mode 100644 index 0000000..673454c --- /dev/null +++ b/openfeature-provider/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +Release notes for the `mixpanel-openfeature` package will be added here +starting with the first release made under the standardized release process. diff --git a/openfeature-provider/README.md b/openfeature-provider/README.md index 2e45f54..e472f90 100644 --- a/openfeature-provider/README.md +++ b/openfeature-provider/README.md @@ -1,5 +1,7 @@ # Mixpanel Python OpenFeature Provider +##### _May 13, 2026_ - [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-python/releases/tag/openfeature/v0.1.0) + [![PyPI](https://img.shields.io/pypi/v/mixpanel-openfeature.svg)](https://pypi.org/project/mixpanel-openfeature/) [![OpenFeature](https://img.shields.io/badge/OpenFeature-compatible-green)](https://openfeature.dev/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mixpanel/mixpanel-python/blob/master/LICENSE) From 6f62f703809f36b4f01c628ce9ea121ec78574b1 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 11:11:21 -0400 Subject: [PATCH 2/4] fix: correct jq tag-resolution snippet in publish workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous form `select($t | startswith(.value.tag_prefix))` rebinds the context inside `select` so `.value` is no longer the entry — jq errors with "Cannot index string with string \"value\"" and every tag push fails. Bind the prefix first via `.value.tag_prefix as $p` so the comparison runs against the captured variable. --- .github/workflows/release-pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index dd69769..3557a74 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -42,7 +42,7 @@ jobs: run: | MODULE=$(jq -r --arg t "$TAG" ' to_entries - | map(select($t | startswith(.value.tag_prefix))) + | map(select(.value.tag_prefix as $p | $t | startswith($p))) | max_by(.value.tag_prefix | length) | .key // empty ' .github/modules.json) From b7ba2d73261a80005d776ff7d54afdf6e1b685a0 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 15:53:04 -0400 Subject: [PATCH 3/4] chore: switch changelog extraction from Python to sed Aligns the changelog-section extraction with the deployed mixpanel-android release workflow, which uses a sed range. The Python regex implementation was an accident of port-time authorship; sed is the proven approach in the gold-standard Maven Central pipeline. Uses `\@...@` as the sed address delimiter so tags containing `/` (e.g. `openfeature/v0.1.0`) don't conflict with the default `/`. Behavior is otherwise preserved: file-based release_notes.md output, fallback to "Release $TAG" placeholder when the section is missing or empty, and the two-step structure for log visibility in the workflow run. --- .github/workflows/release-pypi.yml | 32 +++++++++++++----------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 3557a74..8f4eb09 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -178,24 +178,20 @@ jobs: TAG: ${{ github.ref_name }} CHANGELOG: ${{ steps.module.outputs.changelog }} run: | - # Match the section header `## [${TAG}](...)` and stop at the next `## ` header. - # Falls back to a placeholder if no matching section is found. - python3 - <<'PY' > release_notes.md - import os, re, sys - tag = os.environ["TAG"] - changelog_path = os.environ["CHANGELOG"] - try: - content = open(changelog_path).read() - except FileNotFoundError: - print(f"Release {tag}") - sys.exit(0) - pattern = r'^## \[' + re.escape(tag) + r'\].*?\n(.*?)(?=^## |\Z)' - match = re.search(pattern, content, re.DOTALL | re.MULTILINE) - if match: - print(match.group(1).strip()) - else: - print(f"Release {tag}") - PY + # 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 From 54cc16cb4ac29a940a47c54d9984890ec35e5a39 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 16:28:58 -0400 Subject: [PATCH 4/4] chore: accept `all` scope in pr-title-check regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with the Android fleet's convention of allowing `feat(all): ...`, `fix(all): ...`, `chore(all): ...` for cross-cutting changes that should appear in every module's changelog. The shared generate-changelog.sh already matches `all` (it was copied verbatim from mixpanel-android), so this regex change is the only piece needed to make the end-to-end flow accept `all`-scoped PR titles. For single-module repos, `feat(all): foo` is functionally equivalent to `feat(): foo` — kept for fleet-wide consistency. --- .github/workflows/pr-title-check.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index efe296d..59867ed 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -23,8 +23,10 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} run: | MODULE_LIST=$(jq -r 'keys | join("|")' .github/modules.json) - # Scope is optional. Bare or scoped to a known module both pass. - MAIN_PATTERN="^(feat|fix|chore)(\((${MODULE_LIST})\))?: .+" + # Scope is optional. Bare, scoped to a known module, or scoped to + # `all` (cross-cutting changes that appear in every module's + # changelog) all pass. + MAIN_PATTERN="^(feat|fix|chore)(\((${MODULE_LIST}|all)\))?: .+" RELEASE_PATTERN="^release: .+" if [[ "$PR_TITLE" =~ $MAIN_PATTERN ]] || [[ "$PR_TITLE" =~ $RELEASE_PATTERN ]]; then @@ -40,10 +42,10 @@ jobs: echo " feat: description" echo " fix: description" echo " chore: description" - echo " feat(): description" - echo " fix(): description" - echo " chore(): description" + echo " feat(|all): description" + echo " fix(|all): description" + echo " chore(|all): description" echo " release: description" echo "" - echo "Valid modules: ${MODULE_LIST//|/, }" + echo "Valid scopes: ${MODULE_LIST//|/, }, all" exit 1