From 8c006ef8d8dce9ca8ddae41f2f637dbbc3cd1292 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 09:21:49 -0400 Subject: [PATCH 1/4] chore: standardize release process Adds the shared release infrastructure (prepare-release, release-, pr-title-check workflows + modules.json + generate-changelog.sh) so this repo's release flow matches the other Mixpanel SDK repositories. See: https://www.notion.so/348e0ba925628029af63c779caa835f9 --- .github/modules.json | 10 ++ .github/scripts/generate-changelog.sh | 80 ++++++++++ .github/workflows/pr-title-check.yml | 49 ++++++ .github/workflows/prepare-release.yml | 188 ++++++++++++++++++++++++ .github/workflows/release-packagist.yml | 185 +++++++++++++++++++++++ CHANGELOG.md | 74 ++++++++++ README.md | 3 + 7 files changed, 589 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-packagist.yml create mode 100644 CHANGELOG.md diff --git a/.github/modules.json b/.github/modules.json new file mode 100644 index 0000000..023a507 --- /dev/null +++ b/.github/modules.json @@ -0,0 +1,10 @@ +{ + "analytics": { + "tag_prefix": "", + "composer_json": "composer.json", + "version_files": [], + "changelog": "CHANGELOG.md", + "readme": "README.md", + "package_name": "mixpanel/mixpanel-php" + } +} 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..87a6702 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,188 @@ +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., 2.12.0 or 2.12.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 may be empty (e.g. mixpanel-php uses bare semver tags + # like `2.11.0`). The concatenation below still yields the bare + # version string in that case. + TAG_PREFIX=$(echo "$MODULE_CONFIG" | jq -r '.tag_prefix') + { + echo "tag=${TAG_PREFIX}${VERSION}" + echo "composer_json=$(echo "$MODULE_CONFIG" | jq -r '.composer_json')" + echo "changelog=$(echo "$MODULE_CONFIG" | jq -r '.changelog')" + echo "readme=$(echo "$MODULE_CONFIG" | jq -r '.readme')" + echo "package_name=$(echo "$MODULE_CONFIG" | jq -r '.package_name')" + echo "branch=release/${MODULE}/${VERSION}" + } >> "$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" + + # NOTE: composer.json intentionally has NO `version` field. Packagist + # reads the version from the git tag itself, and putting an explicit + # version in composer.json is actively discouraged because it goes + # stale. So the prepare workflow has no "bump composer.json version" + # step. The README header + CHANGELOG prepend below are the only file + # changes. + + - 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") + # Replace the version header line. + # The `1,/pat/` address range bounds the substitution to lines from + # the start of the file through the first match, so a README that + # accidentally contains a second matching line is left untouched. + 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 + 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 }} + CHANGELOG_FILE: ${{ steps.config.outputs.changelog }} + README: ${{ steps.config.outputs.readme }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add "$CHANGELOG_FILE" "$README" + 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 }} + 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'" + + - 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: Setup PHP + uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0 + with: + php-version: '7.4' + tools: composer:v2 + coverage: none + + - name: Validate composer.json + env: + COMPOSER_JSON: ${{ steps.module.outputs.composer_json }} + run: composer validate --strict --working-dir="$(dirname "$COMPOSER_JSON")" + + - name: Install dependencies (with dev for tests) + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Run PHPUnit + run: vendor/bin/phpunit + + - name: Re-install without dev (production smoke test) + run: | + rm -rf vendor + composer install --no-interaction --no-progress --prefer-dist --no-dev + + - name: Extract changelog section for tag + env: + 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 + 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 + + - 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} ready" + echo "" + echo "- Tag: \`${TAG}\`" + echo "- Package: [${PACKAGE_NAME}](https://packagist.org/packages/${PACKAGE_NAME})" + echo "- [Draft GitHub Release](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG})" + echo "" + echo "### Packagist sync" + echo "Packagist auto-syncs from the configured GitHub webhook on tag push." + echo "If the new version does not appear within a few minutes, check:" + echo "1. The Packagist webhook on this repo's GitHub Settings -> Webhooks page (it should target https://packagist.org/api/github)" + echo "2. The package page on Packagist - a manual \"Update\" button is available there" + 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..56af575 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +# Changelog + +## [2.11.0](https://github.com/mixpanel/mixpanel-php/tree/2.11.0) (2026-05-13) + +- Fix identify regex for $anon_id +- Fix PHP 8.2 deprecation warning + +## [2.10.0](https://github.com/mixpanel/mixpanel-php/tree/2.10.0) + +- send millisecond precision timestamps + +## [2.9.0](https://github.com/mixpanel/mixpanel-php/tree/2.9.0) + +- update regex for $anon_id check +- Group Analytics Support +- Fix PHP 8.1 deprecation warning +- PHP 7.4 compatibility + +## [2.8.1](https://github.com/mixpanel/mixpanel-php/tree/2.8.1) + +- Updated `$anon_id` regex in `identify` method to support all Mixpanel distinct IDs + +## [2.8.0](https://github.com/mixpanel/mixpanel-php/tree/2.8.0) + +- Added `$anon_id` parameter to `identify` method, and a track call when parameter exists and is in UUID v4 format +- Change parameter names for `createAlias` method to `$distinct_id` and `$alias` +- Prevent unnecessary call to _encode on non-forked CurlConsumer +- make sure 'Connection' exists before accessing it + +## [2.7.0](https://github.com/mixpanel/mixpanel-php/tree/2.7.0) + +- Dropped test support for EOL PHP version (all < 7.1) +- Added [Parallel cURL implementation](https://github.com/mixpanel/mixpanel-php/commit/6f15000309093b54f7f59f07af297f576fd3a498) +- [Make createAlias adhere to the consumer config](https://github.com/mixpanel/mixpanel-php/commit/1f814c1be704217e4bc8bf570fad844360fa7318) +- Added [option to set $ignore_alias on people updates](https://github.com/mixpanel/mixpanel-php/commit/f2812f4e696ef747b2ab0640f46df97d1bf309c0) +- Fixed [Singleton instance must depend on requested token](https://github.com/mixpanel/mixpanel-php/commit/d50267c48b08eb3c5e1dee2b5dd932cf2b4c3977) +- Fixed license type in composer.json +- Remove testing on HHVM as its no longer support by composer + +## [2.6.2](https://github.com/mixpanel/mixpanel-php/tree/2.6.2) + +- Added support for $ignore_time +- Cleaned up some comments to be more clear + +## [2.6.1](https://github.com/mixpanel/mixpanel-php/tree/2.6.1) + +- Fixed bug in SocketConsumer timeout + +## [2.6](https://github.com/mixpanel/mixpanel-php/tree/2.6) + +- Updated default for `connect_timeout` in SocketConsumer to be 5 + +## [2.5](https://github.com/mixpanel/mixpanel-php/tree/2.5) + +- `timeout` option now refers to `CURLOPT_TIMEOUT` instead of `CURLOPT_CONNECTTIMEOUT` in non-forked cURL calls, it has been removed from the SocketConsumer in favor of a new `connect_timeout` option. +- Added a new `connect_timeout` option for CURLOPT_CONNECTTIMEOUT in non-forked cURL calls (CurlConsumer) and the socket timeout (SocketConsumer) +- Set default timeout (CURLOPT_TIMEOUT) to 30 seconds in non-forked cURL calls +- Set default connection timeoute (CURLOPT_CONNECTTIMEOUT) to 5 seconds in non-forked cURL calls +- We now pass cURL errors from non-forked cURL calls to `_handle_error` with the curl errno and message + +## [2.4](https://github.com/mixpanel/mixpanel-php/tree/2.4) + +- Fixed a bug where passing the integer 0 for the `ip` parameter would be ignored + +## [2.1 - 2.3](https://github.com/mixpanel/mixpanel-php/tree/2.3) + +- Broken releases + +## [2.0](https://github.com/mixpanel/mixpanel-php/tree/2.0) + +- Changed the default consumer to be 'curl' (CurlConsumer) +- Changed the default setting of 'fork' to false in the Curl Consumer. This means that by default, events and profile updates are sent synchronously using the PHP cURL lib when using the Curl Consumer. +- 'createAlias' uses the CurlConsumer with 'fork' explicitly set to false (as we need this to be synchronous) instead of the SocketConsumer. +- Fixed bug where max_queue_size was never read diff --git a/README.md b/README.md index cd4b42c..b6ffa8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ Mixpanel PHP Library [![Build Status](https://travis-ci.org/mixpanel/mixpanel-php.svg)](https://travis-ci.org/mixpanel/mixpanel-php) ============ + +##### _May 13, 2026_ - [2.11.0](https://github.com/mixpanel/mixpanel-php/releases/tag/2.11.0) + This library provides an API to track events and update profiles on Mixpanel. Install with Composer From d73bd73951d334e1c92176332be158d7d4458910 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 15:53:08 -0400 Subject: [PATCH 2/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-packagist.yml | 32 +++++++++++-------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release-packagist.yml b/.github/workflows/release-packagist.yml index 5d39dd9..c1d858c 100644 --- a/.github/workflows/release-packagist.yml +++ b/.github/workflows/release-packagist.yml @@ -124,24 +124,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 f9cf08419ab9cd8fef0509d3d8215086a1868a44 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 16:29:04 -0400 Subject: [PATCH 3/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 From d31968e973f0741895c0928ba0804f8d41c0e5fb Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 20 May 2026 10:05:06 -0400 Subject: [PATCH 4/4] chore: align changelog generator scope policy with pr-title-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the same fix applied to mixpanel-flutter and mixpanel-react-native. Previously the generator required every commit to be scoped to either `` or `all` — meaning a perfectly valid bare-titled PR like `feat: add foo` would silently drop out of the changelog despite passing the pr-title-check workflow (which accepts bare titles). Split the rule so it matches pr-title-check semantics: - feat / fix: bare, scoped to the current module, or scoped to `all` - chore: explicit scope required — bare `chore:` is the convention for changes intentionally hidden from the changelog (release prep, CI tweaks, lockfile bumps, internal docs) Other-module scopes (e.g. `feat(other):`) remain excluded from this module's changelog, as before. Co-Authored-By: Claude Opus 4.7 --- .github/scripts/generate-changelog.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/scripts/generate-changelog.sh b/.github/scripts/generate-changelog.sh index 8f289c7..7ebd946 100755 --- a/.github/scripts/generate-changelog.sh +++ b/.github/scripts/generate-changelog.sh @@ -34,17 +34,24 @@ while IFS= read -r line; do [ -z "$line" ] && continue MSG=$(echo "$line" | cut -d' ' -f2-) - if [[ "$MSG" =~ ^(feat|fix|chore)\((${MODULE}|all)\):\ (.+) ]]; then + # feat / fix: include whether bare, scoped to our module, or scoped to + # `all` (cross-cutting changes that appear in every module's changelog). + # chore: include only when explicitly scoped to our module or `all` — + # bare `chore:` is the convention for changes intentionally hidden from + # the changelog (release prep PRs, CI tweaks, lockfile bumps, internal + # docs). + if [[ "$MSG" =~ ^(feat|fix)(\((${MODULE}|all)\))?:\ (.+) ]]; then TYPE="${BASH_REMATCH[1]}" - DESC="${BASH_REMATCH[3]}" - + DESC="${BASH_REMATCH[4]}" 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 + elif [[ "$MSG" =~ ^chore\((${MODULE}|all)\):\ (.+) ]]; then + DESC="${BASH_REMATCH[2]}" + DESC=$(echo "$DESC" | sed -E "s|\(#([0-9]+)\)|([#\1](${SAFE_URL}/pull/\1))|g") + CHORES+=("$DESC") fi done < <(git log --oneline "$RANGE")