Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/modules.json
Original file line number Diff line number Diff line change
@@ -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/openfeature/version.rb",
"changelog": "openfeature-provider/CHANGELOG.md",
"readme": "openfeature-provider/README.md",
"package_name": "mixpanel-ruby-openfeature"
}
}
80 changes: 80 additions & 0 deletions .github/scripts/generate-changelog.sh
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions .github/workflows/pr-title-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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, 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
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(<module>|all): description"
echo " fix(<module>|all): description"
echo " chore(<module>|all): description"
echo " release: description"
echo ""
echo "Valid scopes: ${MODULE_LIST//|/, }, all"
exit 1
189 changes: 189 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -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 <<EOF
## Release ${MODULE} ${VERSION}

This PR prepares the ${MODULE} module for release.

### Changes
- Bumps \`VERSION\` to \`${VERSION}\` in \`${VERSION_FILE}\`
- Updates \`${CHANGELOG_FILE}\` with a new section since the last \`${TAG%${VERSION}}*\` tag
- Updates \`${README}\` version header

### After merging
1. Push tag \`${TAG}\` from the merge commit on \`master\` to trigger the publish workflow:
\`\`\`
git checkout master && git pull
git tag ${TAG}
git push origin ${TAG}
\`\`\`
2. The publish workflow creates a draft GitHub release and publishes to RubyGems via OIDC trusted publishing. Review and publish the GitHub release after the workflow finishes.
EOF
)" \
--base master \
--head "$BRANCH"
Loading
Loading