Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
53 changes: 53 additions & 0 deletions .github/actions/release-branch-sync/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release Branch Sync
description: 'Syncs the stable branch into all open release branches after a release is merged.'

inputs:
merged-release-branch:
required: true
description: 'The release branch that was just merged into stable (e.g., release/7.35.0)'
repo-type:
required: false
description: 'Type of repository (mobile or extension)'
default: 'mobile'
github-token:
description: 'GitHub token used for authentication and PR creation.'
required: true
github-tools-repository:
description: 'The GitHub repository containing the GitHub tools.'
required: false
default: ${{ github.action_repository }}
github-tools-ref:
description: 'The ref of the GitHub tools repository to use.'
required: false
default: ${{ github.action_ref }}

runs:
using: composite
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ inputs.github-token }}

- name: Checkout GitHub tools repository
uses: actions/checkout@v6
with:
repository: ${{ inputs.github-tools-repository }}
ref: ${{ inputs.github-tools-ref }}
path: ./github-tools

- name: Set Git user and email
shell: bash
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Run release branch sync script
env:
MERGED_RELEASE_BRANCH: ${{ inputs.merged-release-branch }}
REPO_TYPE: ${{ inputs.repo-type }}
GITHUB_TOKEN: ${{ inputs.github-token }}
shell: bash
run: bash ./github-tools/.github/scripts/release-branch-sync.sh

277 changes: 277 additions & 0 deletions .github/scripts/release-branch-sync.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
#!/bin/bash
# =============================================================================
# Release Branch Sync Script
# =============================================================================
# Purpose: After a release branch is merged into stable, create PRs to sync
# stable into all other open release branches.
#
# Flow:
# 1. Find all open release branches (release/X.Y.Z)
# 2. For each one, create a branch from stable (stable-sync-release-X.Y.Z)
# 3. Create a PR from that branch into the release branch
# 4. Conflicts are left for manual resolution by developers
#
# Environment variables:
# MERGED_RELEASE_BRANCH - The release branch that was just merged (e.g., release/7.35.0)
# REPO_TYPE - Repository type: 'mobile' or 'extension'
# GITHUB_TOKEN - GitHub token for authentication and PR creation
# =============================================================================

set -e

# Regex pattern for valid release branch names (release/X.Y.Z)
RELEASE_BRANCH_PATTERN='^release/[0-9]+\.[0-9]+\.[0-9]+$'

# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------

log_info() {
echo "INFO: $1"
}

log_success() {
echo "SUCCESS: $1"
}

log_warning() {
echo "WARNING: $1"
}

log_error() {
echo "ERROR: $1"
}

log_section() {
echo ""
echo "============================================================"
echo "$1"
echo "============================================================"
}

# Validate that a branch name matches the release/X.Y.Z format
is_valid_release_branch() {
local branch=$1
[[ "$branch" =~ $RELEASE_BRANCH_PATTERN ]]
}

# Check if a sync PR already exists for a release branch
pr_exists() {
local release_branch=$1
local sync_branch=$2

local existing_pr
existing_pr=$(gh pr list --base "$release_branch" --head "$sync_branch" --state open --json number --jq 'length')

[[ "$existing_pr" -gt 0 ]]
}

# Parse version from release branch name (release/X.Y.Z -> X.Y.Z)
parse_version() {
local branch=$1
echo "$branch" | sed 's|release/||'
}

# Compare two semantic versions
# Returns: 0 if v1 < v2, 1 if v1 >= v2
is_version_older() {
local v1=$1
local v2=$2

local oldest
oldest=$(printf '%s\n%s\n' "$v1" "$v2" | sort -V | head -n1)

[[ "$v1" == "$oldest" && "$v1" != "$v2" ]]
}

# Check if stable has commits that the release branch doesn't have
stable_has_new_commits() {
local release_branch=$1

# Count commits in stable that are not in the release branch
local ahead_count
ahead_count=$(git rev-list --count "origin/${release_branch}..origin/stable" 2>/dev/null || echo "0")

[[ "$ahead_count" -gt 0 ]]
}

# Create a sync PR for a release branch
create_sync_pr() {
local release_branch=$1
local sync_branch=$2

local body="## Summary

This PR syncs the latest changes from \`stable\` into \`${release_branch}\`.

## Why is this needed?

A release branch (\`${MERGED_RELEASE_BRANCH}\`) was merged into \`stable\`. This PR brings those changes (hotfixes, etc.) into \`${release_branch}\`.

## Action Required

**Please review and resolve any merge conflicts manually.**

If there are conflicts, they will appear in this PR. Resolve them to ensure the release branch has all the latest fixes from stable."

gh pr create \
--base "$release_branch" \
--head "$sync_branch" \
--title "chore: sync stable into ${release_branch}" \
--body "$body"
}

# Process a single release branch
# Returns: 0 = PR created, 1 = failed, 2 = skipped
process_release_branch() {
local release_branch=$1
local merged_version=$2
local release_version
release_version=$(parse_version "$release_branch")

log_section "Processing ${release_branch}"

# Skip branches that don't match the release/X.Y.Z format
if ! is_valid_release_branch "$release_branch"; then
log_info "Skipping ${release_branch} (does not match release/X.Y.Z format)"
return 2
fi

# Skip the branch that was just merged
if [[ "$release_branch" == "$MERGED_RELEASE_BRANCH" ]]; then
log_info "Skipping ${release_branch} (just merged into stable)"
return 2
fi

# Skip branches older than the merged release
if is_version_older "$release_version" "$merged_version"; then
log_info "Skipping ${release_branch} (older than merged release ${MERGED_RELEASE_BRANCH})"
return 2
fi

# Create sync branch name (replace / with -)
local sync_branch="stable-sync-${release_branch//\//-}"

# Check if a sync PR already exists
if pr_exists "$release_branch" "$sync_branch"; then
log_warning "Sync PR already exists for ${release_branch}, skipping"
return 2
fi

# Check if stable has any new commits compared to the release branch
if ! stable_has_new_commits "$release_branch"; then
log_success "${release_branch} is already up-to-date with stable, no sync needed"
return 2
fi

log_info "Creating sync branch: ${sync_branch} (from stable)"

# Ensure we're on a clean state
git checkout -f origin/stable 2>/dev/null || true
git clean -fd

# Delete local sync branch if it exists
git branch -D "$sync_branch" 2>/dev/null || true

# Create sync branch from stable
git checkout -b "$sync_branch" origin/stable

# Push the sync branch (force in case it exists remotely)
log_info "Pushing ${sync_branch}..."
if git push -u origin "$sync_branch" --force; then
log_success "Pushed ${sync_branch}"
else
log_error "Failed to push ${sync_branch}"
return 1
fi

# Create the PR (stable-sync branch → release branch)
log_info "Creating PR: ${sync_branch} → ${release_branch}"
if create_sync_pr "$release_branch" "$sync_branch"; then
log_success "Created PR for ${release_branch}"
else
log_error "Failed to create PR for ${release_branch}"
return 1
fi

return 0
}

# -----------------------------------------------------------------------------
# Main Script
# -----------------------------------------------------------------------------

main() {
log_section "Release Branch Sync"

# Validate environment
if [[ -z "$MERGED_RELEASE_BRANCH" ]]; then
log_error "MERGED_RELEASE_BRANCH environment variable is required"
exit 1
fi

if [[ -z "$GITHUB_TOKEN" ]]; then
log_error "GITHUB_TOKEN environment variable is required"
exit 1
fi

log_info "Merged release branch: ${MERGED_RELEASE_BRANCH}"
log_info "Repository type: ${REPO_TYPE:-not set}"

# Get version of the merged release
local merged_version
merged_version=$(parse_version "$MERGED_RELEASE_BRANCH")
log_info "Merged version: ${merged_version}"

# Fetch all branches
log_info "Fetching all branches..."
git fetch --all --prune

# Find all release branches
log_info "Finding open release branches..."
local release_branches
release_branches=$(git branch -r --list 'origin/release/*' | sed 's|origin/||' | tr -d ' ' | sort -t'/' -k2 -V)

if [[ -z "$release_branches" ]]; then
log_warning "No release branches found"
exit 0
fi

log_info "Found release branches:"
echo "$release_branches" | while read -r branch; do
echo " - $branch"
done

# Process each release branch
local processed=0
local skipped=0
local failed=0

while IFS= read -r branch; do
if [[ -z "$branch" ]]; then
continue
fi

local result
process_release_branch "$branch" "$merged_version" && result=$? || result=$?

case $result in
0) ((processed++)) || true ;; # PR created
1) ((failed++)) || true ;; # Failed
2) ((skipped++)) || true ;; # Skipped
esac
done <<< "$release_branches"

# Summary
log_section "Summary"
log_info "PRs created: ${processed}"
log_info "Skipped: ${skipped}"
if [[ "$failed" -gt 0 ]]; then
log_error "Failed: ${failed}"
exit 1
fi

log_success "Release branch sync completed!"
}

main "$@"
Loading