From 83ae66ad8d37a089bc5b09d5a87cd136d8e8af26 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 09:22:12 -0400 Subject: [PATCH 1/5] 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 | 18 ++ .github/scripts/generate-changelog.sh | 80 +++++++ .github/workflows/pr-title-check.yml | 49 ++++ .github/workflows/prepare-release.yml | 189 +++++++++++++++ .github/workflows/release-rubygems.yml | 221 ++++++++++++++++++ CHANGELOG.md | 5 + Readme.rdoc | 2 + openfeature-provider/CHANGELOG.md | 5 + openfeature-provider/README.md | 2 + .../lib/mixpanel-ruby-openfeature/version.rb | 7 + .../mixpanel-ruby-openfeature.gemspec | 4 +- 11 files changed, 581 insertions(+), 1 deletion(-) 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-rubygems.yml create mode 100644 CHANGELOG.md create mode 100644 openfeature-provider/CHANGELOG.md create mode 100644 openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb diff --git a/.github/modules.json b/.github/modules.json new file mode 100644 index 0000000..028a290 --- /dev/null +++ b/.github/modules.json @@ -0,0 +1,18 @@ +{ + "analytics": { + "tag_prefix": "v", + "gemspec": "mixpanel-ruby.gemspec", + "version_file": "lib/mixpanel-ruby/version.rb", + "changelog": "CHANGELOG.md", + "readme": "Readme.rdoc", + "package_name": "mixpanel-ruby" + }, + "openfeature": { + "tag_prefix": "openfeature/v", + "gemspec": "openfeature-provider/mixpanel-ruby-openfeature.gemspec", + "version_file": "openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb", + "changelog": "openfeature-provider/CHANGELOG.md", + "readme": "openfeature-provider/README.md", + "package_name": "mixpanel-ruby-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..be45ace --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,189 @@ +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., 3.2.0 or 0.2.0)' + 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 "gemspec=$(echo "$MODULE_CONFIG" | jq -r '.gemspec')" + echo "version_file=$(echo "$MODULE_CONFIG" | jq -r '.version_file')" + 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 ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "::error::Tag $TAG already exists on origin" + 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 in version.rb + env: + VERSION: ${{ inputs.version }} + VERSION_FILE: ${{ steps.config.outputs.version_file }} + run: | + sed -i -E "s/VERSION = .*/VERSION = '${VERSION}'/" "$VERSION_FILE" + echo "Updated $VERSION_FILE:" + grep -n "VERSION" "$VERSION_FILE" || true + + - 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 first matching version-header line. The 1,/pat/ address + # range bounds the substitution so multi-version READMEs aren't + # trampled. Works for both Markdown and rdoc files (rdoc passes + # unrecognized lines through as text). + 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 }} + VERSION_FILE: ${{ steps.config.outputs.version_file }} + 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 "$VERSION_FILE" "$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 }} + VERSION_FILE: ${{ steps.config.outputs.version_file }} + 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: Validate version_file matches tag + env: + VERSION: ${{ steps.module.outputs.version }} + VERSION_FILE: ${{ steps.module.outputs.version_file }} + run: | + FILE_VERSION=$(grep -E "^[[:space:]]*VERSION[[:space:]]*=" "$VERSION_FILE" \ + | head -1 \ + | sed -E "s/.*VERSION[[:space:]]*=[[:space:]]*['\"]([^'\"]+)['\"].*/\1/") + if [ -z "$FILE_VERSION" ]; then + echo "::error::Could not parse VERSION from $VERSION_FILE" + exit 1 + fi + if [ "$VERSION" != "$FILE_VERSION" ]; then + echo "::error::Tag version '$VERSION' does not match $VERSION_FILE version '$FILE_VERSION'" + exit 1 + fi + echo "Version confirmed: $FILE_VERSION" + + - name: Set up Ruby + uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 + with: + ruby-version: '3.3' + bundler-cache: false + + - name: Install dependencies + env: + GEMSPEC: ${{ steps.module.outputs.gemspec }} + run: | + GEM_DIR=$(dirname "$GEMSPEC") + if [ -f "$GEM_DIR/Gemfile" ]; then + (cd "$GEM_DIR" && bundle install) + else + bundle install + fi + + - name: Run tests + env: + MODULE: ${{ steps.module.outputs.module }} + GEMSPEC: ${{ steps.module.outputs.gemspec }} + run: | + GEM_DIR=$(dirname "$GEMSPEC") + if [ -f "$GEM_DIR/Gemfile" ] && [ "$GEM_DIR" != "." ]; then + (cd "$GEM_DIR" && bundle exec rspec) + else + bundle exec rake + fi + + - name: Build gem + id: build + env: + GEMSPEC: ${{ steps.module.outputs.gemspec }} + run: | + GEM_DIR=$(dirname "$GEMSPEC") + GEMSPEC_BASENAME=$(basename "$GEMSPEC") + if [ "$GEM_DIR" = "." ]; then + gem build "$GEMSPEC_BASENAME" + GEM_FILE=$(ls -t *.gem | head -1) + echo "gem_path=${GEM_FILE}" >> "$GITHUB_OUTPUT" + else + (cd "$GEM_DIR" && gem build "$GEMSPEC_BASENAME") + GEM_FILE=$(ls -t "$GEM_DIR"/*.gem | head -1) + echo "gem_path=${GEM_FILE}" >> "$GITHUB_OUTPUT" + fi + echo "Built: $(ls -la "${GEM_FILE}")" + + - 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: Configure RubyGems trusted publishing credentials + uses: rubygems/configure-rubygems-credentials@762a4b77c3300434bb57c7ce80b20e36231927aa # v2.0.0 + + # Publish last - RubyGems uploads are effectively irreversible (yank is + # restricted). The draft GitHub release + `release` environment reviewer + # acts as the human gate. + - name: Publish gem to RubyGems + env: + GEM_PATH: ${{ steps.build.outputs.gem_path }} + run: | + gem push "$GEM_PATH" + + - 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 "- [RubyGems](https://rubygems.org/gems/${PACKAGE_NAME}/versions/${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..8727123 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [v3.1.0](https://github.com/mixpanel/mixpanel-ruby/tree/v3.1.0) (2026-05-13) + +Initial entry for the standardized release process. See `Readme.rdoc` for prior version history. diff --git a/Readme.rdoc b/Readme.rdoc index 43f5502..f2b3912 100644 --- a/Readme.rdoc +++ b/Readme.rdoc @@ -1,5 +1,7 @@ = mixpanel-ruby: The official Mixpanel Ruby library +##### _May 13, 2026_ - [v3.1.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/v3.1.0) + mixpanel-ruby is a library for tracking events and sending \Mixpanel profile updates to \Mixpanel from your ruby applications. diff --git a/openfeature-provider/CHANGELOG.md b/openfeature-provider/CHANGELOG.md new file mode 100644 index 0000000..377b745 --- /dev/null +++ b/openfeature-provider/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/tree/openfeature/v0.1.0) (2026-05-13) + +Initial release of the Mixpanel OpenFeature provider for Ruby. diff --git a/openfeature-provider/README.md b/openfeature-provider/README.md index 503c778..8a833c7 100644 --- a/openfeature-provider/README.md +++ b/openfeature-provider/README.md @@ -1,5 +1,7 @@ # mixpanel-ruby-openfeature +##### _May 13, 2026_ - [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/openfeature/v0.1.0) + [![Gem Version](https://img.shields.io/gem/v/mixpanel-ruby-openfeature.svg)](https://rubygems.org/gems/mixpanel-ruby-openfeature) [![OpenFeature](https://img.shields.io/badge/OpenFeature-compatible-green)](https://openfeature.dev/) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/mixpanel/mixpanel-ruby/blob/master/LICENSE) diff --git a/openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb b/openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb new file mode 100644 index 0000000..ab13e2c --- /dev/null +++ b/openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Mixpanel + module OpenFeature + VERSION = '0.1.0' + end +end diff --git a/openfeature-provider/mixpanel-ruby-openfeature.gemspec b/openfeature-provider/mixpanel-ruby-openfeature.gemspec index 9a496f3..9db78e8 100644 --- a/openfeature-provider/mixpanel-ruby-openfeature.gemspec +++ b/openfeature-provider/mixpanel-ruby-openfeature.gemspec @@ -1,8 +1,10 @@ # frozen_string_literal: true +require File.join(File.dirname(__FILE__), 'lib/mixpanel-ruby-openfeature/version.rb') + Gem::Specification.new do |spec| spec.name = 'mixpanel-ruby-openfeature' - spec.version = '0.1.0' + spec.version = Mixpanel::OpenFeature::VERSION spec.authors = ['Mixpanel'] spec.email = 'support@mixpanel.com' spec.summary = 'OpenFeature provider for Mixpanel feature flags' From ec635ba0ef32c7dde28568cfbad1ee8fac4cc332 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 10:44:56 -0400 Subject: [PATCH 2/5] chore: relocate openfeature version.rb to namespace-conventional path Moves openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb to openfeature-provider/lib/mixpanel/openfeature/version.rb so the file path matches the Mixpanel::OpenFeature namespace. Also adds require_relative 'openfeature/version' to openfeature.rb so consumers of the gem get Mixpanel::OpenFeature::VERSION defined when they require 'mixpanel/openfeature'. Updates the gemspec require path and .github/modules.json version_file to match the new location. --- .github/modules.json | 2 +- openfeature-provider/lib/mixpanel/openfeature.rb | 1 + .../openfeature}/version.rb | 0 openfeature-provider/mixpanel-ruby-openfeature.gemspec | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) rename openfeature-provider/lib/{mixpanel-ruby-openfeature => mixpanel/openfeature}/version.rb (100%) diff --git a/.github/modules.json b/.github/modules.json index 028a290..f1386ea 100644 --- a/.github/modules.json +++ b/.github/modules.json @@ -10,7 +10,7 @@ "openfeature": { "tag_prefix": "openfeature/v", "gemspec": "openfeature-provider/mixpanel-ruby-openfeature.gemspec", - "version_file": "openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb", + "version_file": "openfeature-provider/lib/mixpanel/openfeature/version.rb", "changelog": "openfeature-provider/CHANGELOG.md", "readme": "openfeature-provider/README.md", "package_name": "mixpanel-ruby-openfeature" diff --git a/openfeature-provider/lib/mixpanel/openfeature.rb b/openfeature-provider/lib/mixpanel/openfeature.rb index 5fb6ba2..a004705 100644 --- a/openfeature-provider/lib/mixpanel/openfeature.rb +++ b/openfeature-provider/lib/mixpanel/openfeature.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true +require_relative 'openfeature/version' require_relative 'openfeature/provider' diff --git a/openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb b/openfeature-provider/lib/mixpanel/openfeature/version.rb similarity index 100% rename from openfeature-provider/lib/mixpanel-ruby-openfeature/version.rb rename to openfeature-provider/lib/mixpanel/openfeature/version.rb diff --git a/openfeature-provider/mixpanel-ruby-openfeature.gemspec b/openfeature-provider/mixpanel-ruby-openfeature.gemspec index 9db78e8..79cdb10 100644 --- a/openfeature-provider/mixpanel-ruby-openfeature.gemspec +++ b/openfeature-provider/mixpanel-ruby-openfeature.gemspec @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.join(File.dirname(__FILE__), 'lib/mixpanel-ruby-openfeature/version.rb') +require File.join(File.dirname(__FILE__), 'lib/mixpanel/openfeature/version.rb') Gem::Specification.new do |spec| spec.name = 'mixpanel-ruby-openfeature' From 7386a1f9ed8e986c709778ba426322608c49610b Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 11:11:21 -0400 Subject: [PATCH 3/5] 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-rubygems.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-rubygems.yml b/.github/workflows/release-rubygems.yml index 4608bbf..0d274b1 100644 --- a/.github/workflows/release-rubygems.yml +++ b/.github/workflows/release-rubygems.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 3d55234306bfae631d73766f8102a41b0f0c2790 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 15:53:06 -0400 Subject: [PATCH 4/5] 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-rubygems.yml | 32 +++++++++++--------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release-rubygems.yml b/.github/workflows/release-rubygems.yml index 0d274b1..dcc3ac1 100644 --- a/.github/workflows/release-rubygems.yml +++ b/.github/workflows/release-rubygems.yml @@ -155,24 +155,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 52d9a4a1d869f9caddff3124f1c0a26d7b369747 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 13 May 2026 16:29:01 -0400 Subject: [PATCH 5/5] 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