From 7f078970e87f09129a5e72814784903759dc0740 Mon Sep 17 00:00:00 2001 From: David Seveloff Date: Sun, 30 Aug 2026 15:46:07 +0300 Subject: [PATCH 1/4] CI: Add cherry-pick release branches composite action [ED-25426] Add reusable composite action that cherry-picks merged PRs across release/stable, release/beta, and main using isolated git worktrees. Ref: ED-25426 Co-authored-by: Cursor --- .../cherry-pick-release-branches/README.md | 64 +++++ .../cherry-pick-release-branches/action.yml | 71 +++++ .../cherry-pick-release-branches.sh | 250 ++++++++++++++++++ 3 files changed, 385 insertions(+) create mode 100644 actions/cherry-pick-release-branches/README.md create mode 100644 actions/cherry-pick-release-branches/action.yml create mode 100755 actions/cherry-pick-release-branches/cherry-pick-release-branches.sh diff --git a/actions/cherry-pick-release-branches/README.md b/actions/cherry-pick-release-branches/README.md new file mode 100644 index 000000000..72b6fec77 --- /dev/null +++ b/actions/cherry-pick-release-branches/README.md @@ -0,0 +1,64 @@ +# Cherry Pick Release Branches + +Automatically cherry-picks merged PRs across the release branch cascade using isolated git worktrees. + +## Cascade rules + +| Merged into | Cherry-pick targets | +| --- | --- | +| `release/stable` | `release/beta`, `main` | +| `release/beta` | `main` | + +Branch names are configurable via inputs. + +## Usage + +The calling workflow must check out the target repository with `fetch-depth: 0` before invoking this action. + +```yaml +permissions: + contents: write + pull-requests: write + +steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + + - uses: elementor/elementor-editor-github-actions/actions/cherry-pick-release-branches@main + with: + token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} + using-pat: ${{ secrets.GH_PAT != '' }} + base-ref: ${{ github.event.pull_request.base.ref }} + pr-number: ${{ github.event.pull_request.number }} + merge-sha: ${{ github.event.pull_request.merge_commit_sha }} + pr-title: ${{ github.event.pull_request.title }} + pr-user-login: ${{ github.event.pull_request.user.login }} + pr-url: ${{ github.event.pull_request.html_url }} + source-repo: ${{ github.event.pull_request.head.repo.full_name }} +``` + +## Inputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `token` | yes | — | GitHub token with `contents: write` and `pull-requests: write` | +| `base-ref` | yes | — | Branch the source PR was merged into | +| `pr-number` | yes | — | Source PR number | +| `merge-sha` | yes | — | Merge commit SHA | +| `pr-title` | yes | — | Source PR title | +| `pr-user-login` | yes | — | Source PR author | +| `pr-url` | yes | — | Source PR URL | +| `source-repo` | yes | — | Head repo (`owner/repo`) | +| `using-pat` | no | `false` | Set to `true` when using `GH_PAT` | +| `stable-branch` | no | `release/stable` | Stable branch name | +| `beta-branch` | no | `release/beta` | Beta branch name | +| `main-branch` | no | `main` | Main branch name | + +## Behavior + +- Creates one git worktree per target branch under `$RUNNER_TEMP/cherry-pick-worktrees/` +- Opens a PR on success, or a draft PR with conflict markers on failure +- Skips targets whose remote branch does not exist +- Uses `-m 1` only for merge commits; squash merges use plain `git cherry-pick` diff --git a/actions/cherry-pick-release-branches/action.yml b/actions/cherry-pick-release-branches/action.yml new file mode 100644 index 000000000..88684093c --- /dev/null +++ b/actions/cherry-pick-release-branches/action.yml @@ -0,0 +1,71 @@ +name: Cherry Pick Release Branches +description: >- + Cherry-pick a merged PR to downstream release branches using isolated git worktrees. + release/stable merges cascade to release/beta and main; release/beta merges cascade to main. + +inputs: + token: + description: GitHub token with contents and pull-requests write access + required: true + base-ref: + description: Base branch the PR was merged into + required: true + pr-number: + description: Merged pull request number + required: true + merge-sha: + description: Merge commit SHA to cherry-pick + required: true + pr-title: + description: Original PR title + required: true + pr-user-login: + description: Original PR author login + required: true + pr-url: + description: Original PR URL + required: true + source-repo: + description: Head repository full name (owner/repo) + required: true + using-pat: + description: Whether a PAT is used so downstream PR workflows trigger automatically + required: false + default: 'false' + stable-branch: + description: Stable release branch name + required: false + default: release/stable + beta-branch: + description: Beta release branch name + required: false + default: release/beta + main-branch: + description: Main branch name + required: false + default: main + +runs: + using: composite + steps: + - name: Fetch all branches + shell: bash + run: git fetch --all + + - name: Cherry-pick to downstream branches + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + USING_PAT: ${{ inputs.using-pat }} + BASE_REF: ${{ inputs.base-ref }} + PR_NUMBER: ${{ inputs.pr-number }} + MERGE_SHA: ${{ inputs.merge-sha }} + PR_TITLE: ${{ inputs.pr-title }} + PR_USER_LOGIN: ${{ inputs.pr-user-login }} + ORIG_URL: ${{ inputs.pr-url }} + SOURCE_REPO: ${{ inputs.source-repo }} + STABLE_BRANCH: ${{ inputs.stable-branch }} + BETA_BRANCH: ${{ inputs.beta-branch }} + MAIN_BRANCH: ${{ inputs.main-branch }} + ACTION_PATH: ${{ github.action_path }} + run: bash "$ACTION_PATH/cherry-pick-release-branches.sh" diff --git a/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh b/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh new file mode 100755 index 000000000..f1db1db06 --- /dev/null +++ b/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Cherry-pick a merged PR to downstream release branches using isolated git worktrees. +# +# Required environment variables: +# BASE_REF - branch the PR was merged into (e.g. release/stable) +# PR_NUMBER - merged PR number +# MERGE_SHA - merge commit SHA to cherry-pick +# PR_TITLE - original PR title +# PR_USER_LOGIN - original PR author login +# ORIG_URL - original PR URL +# SOURCE_REPO - head repo full name +# +# Optional: +# USING_PAT - "true" when a PAT is used (workflows trigger automatically) +# STABLE_BRANCH - default: release/stable +# BETA_BRANCH - default: release/beta +# MAIN_BRANCH - default: main +# JIRA_TICKET_PATTERN - regex for Jira ticket extraction (default: [A-Z]{2,10}-[0-9]+) + +JIRA_TICKET_PATTERN="${JIRA_TICKET_PATTERN:-[A-Z]{2,10}-[0-9]+}" +WORKTREE_ROOT="${RUNNER_TEMP:-/tmp}/cherry-pick-worktrees" +STABLE_BRANCH="${STABLE_BRANCH:-release/stable}" +BETA_BRANCH="${BETA_BRANCH:-release/beta}" +MAIN_BRANCH="${MAIN_BRANCH:-main}" + +require_env() { + local name="$1" + if [ -z "${!name:-}" ]; then + echo "::error::Missing required environment variable: $name" + exit 1 + fi +} + +require_env BASE_REF +require_env PR_NUMBER +require_env MERGE_SHA +require_env PR_TITLE +require_env PR_USER_LOGIN +require_env ORIG_URL +require_env SOURCE_REPO + +sanitize_title() { + echo "$1" | sed 's/[;&|<>`$()]//g' +} + +target_to_safe() { + echo "$1" | sed 's/[\/\.]/_/g' +} + +resolve_targets_csv() { + case "$1" in + "$STABLE_BRANCH") echo "${BETA_BRANCH},${MAIN_BRANCH}" ;; + "$BETA_BRANCH") echo "${MAIN_BRANCH}" ;; + *) echo "" ;; + esac +} + +is_merge_commit() { + local parent_count + parent_count="$(git rev-list --parents -n 1 "$1" | awk '{print NF - 1}')" + [ "$parent_count" -ge 2 ] +} + +run_cherry_pick() { + local merge_sha="$1" + + if is_merge_commit "$merge_sha"; then + git cherry-pick -m 1 "$merge_sha" + else + git cherry-pick "$merge_sha" + fi +} + +extract_pr_title_parts() { + local sanitized_title type ticket ticket_suffix title_body + + sanitized_title="$(sanitize_title "$PR_TITLE")" + + type="$(echo "$sanitized_title" | grep -oE '^[A-Za-z]+:' | head -1)" + if [ -z "$type" ]; then + type="Internal:" + fi + + ticket="$(echo "$sanitized_title" | grep -oE "$JIRA_TICKET_PATTERN" | head -1)" + if [ -n "$ticket" ]; then + ticket_suffix=" [${ticket}]" + else + ticket_suffix=" [NO-TICKET]" + fi + + title_body="$(echo "$sanitized_title" | sed 's/^[A-Za-z]*: *//')" + + PR_TITLE_TYPE="$type" + PR_TITLE_BODY="$title_body" + PR_TICKET_SUFFIX="$ticket_suffix" +} + +create_conflict_pr() { + local target="$1" + local cp_branch="$2" + local conflict_branch="$3" + + git add . + git commit -m "Cherry-pick PR #${PR_NUMBER} with conflicts - manual resolution needed" + + if ! git push --force-with-lease origin "${cp_branch}:${conflict_branch}"; then + echo "::warning:: Failed to push conflict branch ${conflict_branch}" + git cherry-pick --abort + return 1 + fi + + if gh pr list --head "$conflict_branch" --base "$target" --state open | grep -q .; then + echo "::notice:: Draft PR already exists for conflict resolution: ${conflict_branch}" + return 0 + fi + + gh pr create \ + --base "$target" \ + --head "$conflict_branch" \ + --assignee "$PR_USER_LOGIN" \ + --label "auto-reviewed" \ + --title "${PR_TITLE_TYPE} Cherry-pick PR ${PR_NUMBER} to ${target} with conflicts: ${PR_TITLE_BODY}" \ + --body "⚠️ **Manual Resolution Required** + +This cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${target}\` branch has conflicts that need manual resolution. + +**Conflict Files:** +The conflicted files are included in this branch with conflict markers. + +**Resolution Steps:** +1. Check out this branch: \`git checkout ${conflict_branch}\` +2. Resolve conflicts in the marked files +3. Stage resolved files: \`git add \` +4. Amend the commit: \`git commit --amend\` +5. Push changes: \`git push --force-with-lease\` +6. Mark this PR as ready for review + +**Original PR:** [#${PR_NUMBER}](${ORIG_URL}) +**Trigger:** Automatic cascade from merge to \`${BASE_REF}\`" \ + --draft + + echo "::notice:: Created draft PR for manual conflict resolution: ${conflict_branch}" +} + +create_success_pr() { + local target="$1" + local cp_branch="$2" + + if gh pr list --head "$cp_branch" --base "$target" --state open | grep -q .; then + echo "PR already exists for branch ${cp_branch} -> ${target}, skipping creation" + return 0 + fi + + local pr_url new_pr + pr_url="$(gh pr create \ + --base "$target" \ + --head "$cp_branch" \ + --assignee "$PR_USER_LOGIN" \ + --label "auto-reviewed" \ + --title "${PR_TITLE_TYPE} Cherry-pick PR ${PR_NUMBER} to ${target}: ${PR_TITLE_BODY}" \ + --body "Automatic cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${target}\` branch. + +**Source:** ${SOURCE_REPO} +**Original Author:** @${PR_USER_LOGIN} +**Trigger:** Automatic cascade from merge to \`${BASE_REF}\`")" + + new_pr="$(echo "$pr_url" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+' || echo "")" + + if [ -z "$new_pr" ]; then + return 0 + fi + + if [ "${USING_PAT:-false}" = "true" ]; then + echo "::notice:: PR #${new_pr} created with PAT - workflows will trigger automatically" + return 0 + fi + + echo "Created PR #${new_pr} with GITHUB_TOKEN, triggering workflows via empty commit..." + git commit --allow-empty -m "Trigger workflows for PR #${new_pr}" + git push origin "$cp_branch" + echo "::notice:: Triggered workflows for PR #${new_pr}" +} + +cherry_pick_to_target() { + local target="$1" + local target_safe cp_branch conflict_branch worktree_path + + target_safe="$(target_to_safe "$target")" + cp_branch="cherry-pick-pr${PR_NUMBER}_to_${target_safe}" + conflict_branch="${cp_branch}_conflicts" + worktree_path="${WORKTREE_ROOT}/${target_safe}" + + if ! git show-ref --verify --quiet "refs/remotes/origin/${target}"; then + echo "::warning:: Branch ${target} does not exist - skipping" + return 0 + fi + + git worktree remove --force "$worktree_path" 2>/dev/null || true + rm -rf "$worktree_path" + mkdir -p "$WORKTREE_ROOT" + + if ! git worktree add -B "$cp_branch" "$worktree_path" "origin/${target}"; then + echo "::warning:: Failed to create worktree for ${target} - skipping" + return 0 + fi + + pushd "$worktree_path" >/dev/null + + if ! run_cherry_pick "$MERGE_SHA"; then + echo "::error:: Cherry-pick conflicts detected for PR #${PR_NUMBER} on branch ${target}" + create_conflict_pr "$target" "$cp_branch" "$conflict_branch" || true + popd >/dev/null + git worktree remove --force "$worktree_path" 2>/dev/null || true + return 0 + fi + + echo "Cherry-pick successful for ${target}" + + if ! git push --force-with-lease origin "$cp_branch"; then + echo "::warning:: Failed to push branch ${cp_branch}" + popd >/dev/null + git worktree remove --force "$worktree_path" 2>/dev/null || true + return 0 + fi + + create_success_pr "$target" "$cp_branch" + + popd >/dev/null + git worktree remove --force "$worktree_path" 2>/dev/null || true +} + +TARGETS_CSV="$(resolve_targets_csv "$BASE_REF")" +if [ -z "$TARGETS_CSV" ]; then + echo "No cherry-pick targets configured for base branch: ${BASE_REF}" + exit 0 +fi + +git config user.name "github-actions[bot]" +git config user.email "github-actions[bot]@users.noreply.github.com" + +extract_pr_title_parts + +IFS=',' read -ra TARGETS <<< "$TARGETS_CSV" +for target in "${TARGETS[@]}"; do + cherry_pick_to_target "$target" +done + +rm -rf "$WORKTREE_ROOT" From 6563a4a411c74089cbb1b027b3b3eb92ce1bac65 Mon Sep 17 00:00:00 2001 From: David Seveloff Date: Sun, 30 Aug 2026 15:55:28 +0300 Subject: [PATCH 2/4] CI: Fix Prettier formatting in cherry-pick action docs [ED-25426] Ref: ED-25426 Co-authored-by: Cursor --- .../cherry-pick-release-branches/README.md | 34 ++--- .../cherry-pick-release-branches/action.yml | 128 +++++++++--------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/actions/cherry-pick-release-branches/README.md b/actions/cherry-pick-release-branches/README.md index 72b6fec77..671a79ddd 100644 --- a/actions/cherry-pick-release-branches/README.md +++ b/actions/cherry-pick-release-branches/README.md @@ -4,10 +4,10 @@ Automatically cherry-picks merged PRs across the release branch cascade using is ## Cascade rules -| Merged into | Cherry-pick targets | -| --- | --- | +| Merged into | Cherry-pick targets | +| ---------------- | ---------------------- | | `release/stable` | `release/beta`, `main` | -| `release/beta` | `main` | +| `release/beta` | `main` | Branch names are configurable via inputs. @@ -41,20 +41,20 @@ steps: ## Inputs -| Input | Required | Default | Description | -| --- | --- | --- | --- | -| `token` | yes | — | GitHub token with `contents: write` and `pull-requests: write` | -| `base-ref` | yes | — | Branch the source PR was merged into | -| `pr-number` | yes | — | Source PR number | -| `merge-sha` | yes | — | Merge commit SHA | -| `pr-title` | yes | — | Source PR title | -| `pr-user-login` | yes | — | Source PR author | -| `pr-url` | yes | — | Source PR URL | -| `source-repo` | yes | — | Head repo (`owner/repo`) | -| `using-pat` | no | `false` | Set to `true` when using `GH_PAT` | -| `stable-branch` | no | `release/stable` | Stable branch name | -| `beta-branch` | no | `release/beta` | Beta branch name | -| `main-branch` | no | `main` | Main branch name | +| Input | Required | Default | Description | +| --------------- | -------- | ---------------- | -------------------------------------------------------------- | +| `token` | yes | — | GitHub token with `contents: write` and `pull-requests: write` | +| `base-ref` | yes | — | Branch the source PR was merged into | +| `pr-number` | yes | — | Source PR number | +| `merge-sha` | yes | — | Merge commit SHA | +| `pr-title` | yes | — | Source PR title | +| `pr-user-login` | yes | — | Source PR author | +| `pr-url` | yes | — | Source PR URL | +| `source-repo` | yes | — | Head repo (`owner/repo`) | +| `using-pat` | no | `false` | Set to `true` when using `GH_PAT` | +| `stable-branch` | no | `release/stable` | Stable branch name | +| `beta-branch` | no | `release/beta` | Beta branch name | +| `main-branch` | no | `main` | Main branch name | ## Behavior diff --git a/actions/cherry-pick-release-branches/action.yml b/actions/cherry-pick-release-branches/action.yml index 88684093c..b3e0fbf62 100644 --- a/actions/cherry-pick-release-branches/action.yml +++ b/actions/cherry-pick-release-branches/action.yml @@ -1,71 +1,71 @@ name: Cherry Pick Release Branches description: >- - Cherry-pick a merged PR to downstream release branches using isolated git worktrees. - release/stable merges cascade to release/beta and main; release/beta merges cascade to main. + Cherry-pick a merged PR to downstream release branches using isolated git worktrees. + release/stable merges cascade to release/beta and main; release/beta merges cascade to main. inputs: - token: - description: GitHub token with contents and pull-requests write access - required: true - base-ref: - description: Base branch the PR was merged into - required: true - pr-number: - description: Merged pull request number - required: true - merge-sha: - description: Merge commit SHA to cherry-pick - required: true - pr-title: - description: Original PR title - required: true - pr-user-login: - description: Original PR author login - required: true - pr-url: - description: Original PR URL - required: true - source-repo: - description: Head repository full name (owner/repo) - required: true - using-pat: - description: Whether a PAT is used so downstream PR workflows trigger automatically - required: false - default: 'false' - stable-branch: - description: Stable release branch name - required: false - default: release/stable - beta-branch: - description: Beta release branch name - required: false - default: release/beta - main-branch: - description: Main branch name - required: false - default: main + token: + description: GitHub token with contents and pull-requests write access + required: true + base-ref: + description: Base branch the PR was merged into + required: true + pr-number: + description: Merged pull request number + required: true + merge-sha: + description: Merge commit SHA to cherry-pick + required: true + pr-title: + description: Original PR title + required: true + pr-user-login: + description: Original PR author login + required: true + pr-url: + description: Original PR URL + required: true + source-repo: + description: Head repository full name (owner/repo) + required: true + using-pat: + description: Whether a PAT is used so downstream PR workflows trigger automatically + required: false + default: 'false' + stable-branch: + description: Stable release branch name + required: false + default: release/stable + beta-branch: + description: Beta release branch name + required: false + default: release/beta + main-branch: + description: Main branch name + required: false + default: main runs: - using: composite - steps: - - name: Fetch all branches - shell: bash - run: git fetch --all + using: composite + steps: + - name: Fetch all branches + shell: bash + run: git fetch --all - - name: Cherry-pick to downstream branches - shell: bash - env: - GH_TOKEN: ${{ inputs.token }} - USING_PAT: ${{ inputs.using-pat }} - BASE_REF: ${{ inputs.base-ref }} - PR_NUMBER: ${{ inputs.pr-number }} - MERGE_SHA: ${{ inputs.merge-sha }} - PR_TITLE: ${{ inputs.pr-title }} - PR_USER_LOGIN: ${{ inputs.pr-user-login }} - ORIG_URL: ${{ inputs.pr-url }} - SOURCE_REPO: ${{ inputs.source-repo }} - STABLE_BRANCH: ${{ inputs.stable-branch }} - BETA_BRANCH: ${{ inputs.beta-branch }} - MAIN_BRANCH: ${{ inputs.main-branch }} - ACTION_PATH: ${{ github.action_path }} - run: bash "$ACTION_PATH/cherry-pick-release-branches.sh" + - name: Cherry-pick to downstream branches + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + USING_PAT: ${{ inputs.using-pat }} + BASE_REF: ${{ inputs.base-ref }} + PR_NUMBER: ${{ inputs.pr-number }} + MERGE_SHA: ${{ inputs.merge-sha }} + PR_TITLE: ${{ inputs.pr-title }} + PR_USER_LOGIN: ${{ inputs.pr-user-login }} + ORIG_URL: ${{ inputs.pr-url }} + SOURCE_REPO: ${{ inputs.source-repo }} + STABLE_BRANCH: ${{ inputs.stable-branch }} + BETA_BRANCH: ${{ inputs.beta-branch }} + MAIN_BRANCH: ${{ inputs.main-branch }} + ACTION_PATH: ${{ github.action_path }} + run: bash "$ACTION_PATH/cherry-pick-release-branches.sh" From 3439eaca001e4fe0f77f83e36fd39ee9ce6c5b25 Mon Sep 17 00:00:00 2001 From: David Seveloff Date: Sun, 30 Aug 2026 16:01:57 +0300 Subject: [PATCH 3/4] CI: Create downstream PRs instead of cherry-picks [ED-25426] Merge the merged commit into sync branches and open normal PRs with the original title instead of cherry-pick PRs. Ref: ED-25426 Co-authored-by: Cursor --- .../cherry-pick-release-branches/README.md | 19 +-- .../cherry-pick-release-branches/action.yml | 15 ++- .../cherry-pick-release-branches.sh | 113 +++++++----------- 3 files changed, 64 insertions(+), 83 deletions(-) diff --git a/actions/cherry-pick-release-branches/README.md b/actions/cherry-pick-release-branches/README.md index 671a79ddd..b9ce086a9 100644 --- a/actions/cherry-pick-release-branches/README.md +++ b/actions/cherry-pick-release-branches/README.md @@ -1,10 +1,10 @@ -# Cherry Pick Release Branches +# Sync Release Branches -Automatically cherry-picks merged PRs across the release branch cascade using isolated git worktrees. +Creates downstream PRs after a release-branch merge using isolated git worktrees. This action does **not** cherry-pick — it merges the merged commit into a branch based on each target and opens a normal PR. ## Cascade rules -| Merged into | Cherry-pick targets | +| Merged into | PR targets | | ---------------- | ---------------------- | | `release/stable` | `release/beta`, `main` | | `release/beta` | `main` | @@ -13,7 +13,7 @@ Branch names are configurable via inputs. ## Usage -The calling workflow must check out the target repository with `fetch-depth: 0` before invoking this action. +The calling workflow must check out the target repository with `fetch-depth: 0` before invoking this action. Skip PRs whose head branch starts with `sync-pr` to avoid cascade loops. ```yaml permissions: @@ -37,6 +37,7 @@ steps: pr-user-login: ${{ github.event.pull_request.user.login }} pr-url: ${{ github.event.pull_request.html_url }} source-repo: ${{ github.event.pull_request.head.repo.full_name }} + head-ref: ${{ github.event.pull_request.head.ref }} ``` ## Inputs @@ -47,10 +48,11 @@ steps: | `base-ref` | yes | — | Branch the source PR was merged into | | `pr-number` | yes | — | Source PR number | | `merge-sha` | yes | — | Merge commit SHA | -| `pr-title` | yes | — | Source PR title | +| `pr-title` | yes | — | Source PR title (reused for downstream PR title) | | `pr-user-login` | yes | — | Source PR author | | `pr-url` | yes | — | Source PR URL | | `source-repo` | yes | — | Head repo (`owner/repo`) | +| `head-ref` | no | `''` | Source PR head branch | | `using-pat` | no | `false` | Set to `true` when using `GH_PAT` | | `stable-branch` | no | `release/stable` | Stable branch name | | `beta-branch` | no | `release/beta` | Beta branch name | @@ -58,7 +60,8 @@ steps: ## Behavior -- Creates one git worktree per target branch under `$RUNNER_TEMP/cherry-pick-worktrees/` -- Opens a PR on success, or a draft PR with conflict markers on failure +- Creates one git worktree per target branch under `$RUNNER_TEMP/release-sync-worktrees/` +- Merges the source merge commit into `sync-pr_to_` branches +- Opens a PR with the **same title** as the merged PR +- Opens a draft PR with conflict markers when merge fails - Skips targets whose remote branch does not exist -- Uses `-m 1` only for merge commits; squash merges use plain `git cherry-pick` diff --git a/actions/cherry-pick-release-branches/action.yml b/actions/cherry-pick-release-branches/action.yml index b3e0fbf62..af51ada0f 100644 --- a/actions/cherry-pick-release-branches/action.yml +++ b/actions/cherry-pick-release-branches/action.yml @@ -1,7 +1,7 @@ -name: Cherry Pick Release Branches +name: Sync Release Branches description: >- - Cherry-pick a merged PR to downstream release branches using isolated git worktrees. - release/stable merges cascade to release/beta and main; release/beta merges cascade to main. + Create PRs on downstream release branches after a merge using isolated git worktrees. + release/stable merges open PRs to release/beta and main; release/beta merges open a PR to main. inputs: token: @@ -14,7 +14,7 @@ inputs: description: Merged pull request number required: true merge-sha: - description: Merge commit SHA to cherry-pick + description: Merge commit SHA to sync into downstream branches required: true pr-title: description: Original PR title @@ -28,6 +28,10 @@ inputs: source-repo: description: Head repository full name (owner/repo) required: true + head-ref: + description: Original PR head branch name + required: false + default: '' using-pat: description: Whether a PAT is used so downstream PR workflows trigger automatically required: false @@ -52,7 +56,7 @@ runs: shell: bash run: git fetch --all - - name: Cherry-pick to downstream branches + - name: Create downstream PRs shell: bash env: GH_TOKEN: ${{ inputs.token }} @@ -64,6 +68,7 @@ runs: PR_USER_LOGIN: ${{ inputs.pr-user-login }} ORIG_URL: ${{ inputs.pr-url }} SOURCE_REPO: ${{ inputs.source-repo }} + HEAD_REF: ${{ inputs.head-ref }} STABLE_BRANCH: ${{ inputs.stable-branch }} BETA_BRANCH: ${{ inputs.beta-branch }} MAIN_BRANCH: ${{ inputs.main-branch }} diff --git a/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh b/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh index f1db1db06..d2f34687c 100755 --- a/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh +++ b/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh @@ -1,29 +1,30 @@ #!/usr/bin/env bash set -euo pipefail -# Cherry-pick a merged PR to downstream release branches using isolated git worktrees. +# Create downstream PRs after a release-branch merge using isolated git worktrees. +# Applies the merged commit with git merge (not cherry-pick) and opens normal PRs. # # Required environment variables: # BASE_REF - branch the PR was merged into (e.g. release/stable) # PR_NUMBER - merged PR number -# MERGE_SHA - merge commit SHA to cherry-pick +# MERGE_SHA - merge commit SHA to sync # PR_TITLE - original PR title # PR_USER_LOGIN - original PR author login # ORIG_URL - original PR URL # SOURCE_REPO - head repo full name # # Optional: +# HEAD_REF - original PR head branch (used for loop prevention messaging) # USING_PAT - "true" when a PAT is used (workflows trigger automatically) # STABLE_BRANCH - default: release/stable # BETA_BRANCH - default: release/beta # MAIN_BRANCH - default: main -# JIRA_TICKET_PATTERN - regex for Jira ticket extraction (default: [A-Z]{2,10}-[0-9]+) -JIRA_TICKET_PATTERN="${JIRA_TICKET_PATTERN:-[A-Z]{2,10}-[0-9]+}" -WORKTREE_ROOT="${RUNNER_TEMP:-/tmp}/cherry-pick-worktrees" +WORKTREE_ROOT="${RUNNER_TEMP:-/tmp}/release-sync-worktrees" STABLE_BRANCH="${STABLE_BRANCH:-release/stable}" BETA_BRANCH="${BETA_BRANCH:-release/beta}" MAIN_BRANCH="${MAIN_BRANCH:-main}" +SYNC_BRANCH_PREFIX="sync-pr" require_env() { local name="$1" @@ -57,57 +58,27 @@ resolve_targets_csv() { esac } -is_merge_commit() { - local parent_count - parent_count="$(git rev-list --parents -n 1 "$1" | awk '{print NF - 1}')" - [ "$parent_count" -ge 2 ] -} - -run_cherry_pick() { +merge_merged_commit() { local merge_sha="$1" - if is_merge_commit "$merge_sha"; then - git cherry-pick -m 1 "$merge_sha" - else - git cherry-pick "$merge_sha" - fi + git merge --no-ff "$merge_sha" -m "Sync PR #${PR_NUMBER} from ${BASE_REF}" } -extract_pr_title_parts() { - local sanitized_title type ticket ticket_suffix title_body - - sanitized_title="$(sanitize_title "$PR_TITLE")" - - type="$(echo "$sanitized_title" | grep -oE '^[A-Za-z]+:' | head -1)" - if [ -z "$type" ]; then - type="Internal:" - fi - - ticket="$(echo "$sanitized_title" | grep -oE "$JIRA_TICKET_PATTERN" | head -1)" - if [ -n "$ticket" ]; then - ticket_suffix=" [${ticket}]" - else - ticket_suffix=" [NO-TICKET]" - fi - - title_body="$(echo "$sanitized_title" | sed 's/^[A-Za-z]*: *//')" - - PR_TITLE_TYPE="$type" - PR_TITLE_BODY="$title_body" - PR_TICKET_SUFFIX="$ticket_suffix" +sanitize_pr_title() { + SANITIZED_PR_TITLE="$(sanitize_title "$PR_TITLE")" } create_conflict_pr() { local target="$1" - local cp_branch="$2" + local sync_branch="$2" local conflict_branch="$3" git add . - git commit -m "Cherry-pick PR #${PR_NUMBER} with conflicts - manual resolution needed" + git commit -m "Sync PR #${PR_NUMBER} to ${target} with conflicts - manual resolution needed" - if ! git push --force-with-lease origin "${cp_branch}:${conflict_branch}"; then + if ! git push --force-with-lease origin "${sync_branch}:${conflict_branch}"; then echo "::warning:: Failed to push conflict branch ${conflict_branch}" - git cherry-pick --abort + git merge --abort 2>/dev/null || true return 1 fi @@ -121,10 +92,10 @@ create_conflict_pr() { --head "$conflict_branch" \ --assignee "$PR_USER_LOGIN" \ --label "auto-reviewed" \ - --title "${PR_TITLE_TYPE} Cherry-pick PR ${PR_NUMBER} to ${target} with conflicts: ${PR_TITLE_BODY}" \ + --title "${SANITIZED_PR_TITLE} (sync to ${target} — conflicts)" \ --body "⚠️ **Manual Resolution Required** -This cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${target}\` branch has conflicts that need manual resolution. +This sync PR for [#${PR_NUMBER}](${ORIG_URL}) into \`${target}\` has conflicts that need manual resolution. **Conflict Files:** The conflicted files are included in this branch with conflict markers. @@ -138,7 +109,8 @@ The conflicted files are included in this branch with conflict markers. 6. Mark this PR as ready for review **Original PR:** [#${PR_NUMBER}](${ORIG_URL}) -**Trigger:** Automatic cascade from merge to \`${BASE_REF}\`" \ +**Merged to:** \`${BASE_REF}\` +**Source branch:** \`${HEAD_REF:-unknown}\`" \ --draft echo "::notice:: Created draft PR for manual conflict resolution: ${conflict_branch}" @@ -146,25 +118,26 @@ The conflicted files are included in this branch with conflict markers. create_success_pr() { local target="$1" - local cp_branch="$2" + local sync_branch="$2" - if gh pr list --head "$cp_branch" --base "$target" --state open | grep -q .; then - echo "PR already exists for branch ${cp_branch} -> ${target}, skipping creation" + if gh pr list --head "$sync_branch" --base "$target" --state open | grep -q .; then + echo "PR already exists for branch ${sync_branch} -> ${target}, skipping creation" return 0 fi local pr_url new_pr pr_url="$(gh pr create \ --base "$target" \ - --head "$cp_branch" \ + --head "$sync_branch" \ --assignee "$PR_USER_LOGIN" \ --label "auto-reviewed" \ - --title "${PR_TITLE_TYPE} Cherry-pick PR ${PR_NUMBER} to ${target}: ${PR_TITLE_BODY}" \ - --body "Automatic cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${target}\` branch. + --title "${SANITIZED_PR_TITLE}" \ + --body "Automatic sync PR created after [#${PR_NUMBER}](${ORIG_URL}) was merged into \`${BASE_REF}\`. +**Target branch:** \`${target}\` **Source:** ${SOURCE_REPO} -**Original Author:** @${PR_USER_LOGIN} -**Trigger:** Automatic cascade from merge to \`${BASE_REF}\`")" +**Source branch:** \`${HEAD_REF:-unknown}\` +**Original author:** @${PR_USER_LOGIN}")" new_pr="$(echo "$pr_url" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+' || echo "")" @@ -179,17 +152,17 @@ create_success_pr() { echo "Created PR #${new_pr} with GITHUB_TOKEN, triggering workflows via empty commit..." git commit --allow-empty -m "Trigger workflows for PR #${new_pr}" - git push origin "$cp_branch" + git push origin "$sync_branch" echo "::notice:: Triggered workflows for PR #${new_pr}" } -cherry_pick_to_target() { +sync_to_target() { local target="$1" - local target_safe cp_branch conflict_branch worktree_path + local target_safe sync_branch conflict_branch worktree_path target_safe="$(target_to_safe "$target")" - cp_branch="cherry-pick-pr${PR_NUMBER}_to_${target_safe}" - conflict_branch="${cp_branch}_conflicts" + sync_branch="${SYNC_BRANCH_PREFIX}${PR_NUMBER}_to_${target_safe}" + conflict_branch="${sync_branch}_conflicts" worktree_path="${WORKTREE_ROOT}/${target_safe}" if ! git show-ref --verify --quiet "refs/remotes/origin/${target}"; then @@ -201,31 +174,31 @@ cherry_pick_to_target() { rm -rf "$worktree_path" mkdir -p "$WORKTREE_ROOT" - if ! git worktree add -B "$cp_branch" "$worktree_path" "origin/${target}"; then + if ! git worktree add -B "$sync_branch" "$worktree_path" "origin/${target}"; then echo "::warning:: Failed to create worktree for ${target} - skipping" return 0 fi pushd "$worktree_path" >/dev/null - if ! run_cherry_pick "$MERGE_SHA"; then - echo "::error:: Cherry-pick conflicts detected for PR #${PR_NUMBER} on branch ${target}" - create_conflict_pr "$target" "$cp_branch" "$conflict_branch" || true + if ! merge_merged_commit "$MERGE_SHA"; then + echo "::error:: Merge conflicts detected for PR #${PR_NUMBER} on branch ${target}" + create_conflict_pr "$target" "$sync_branch" "$conflict_branch" || true popd >/dev/null git worktree remove --force "$worktree_path" 2>/dev/null || true return 0 fi - echo "Cherry-pick successful for ${target}" + echo "Merge successful for ${target}" - if ! git push --force-with-lease origin "$cp_branch"; then - echo "::warning:: Failed to push branch ${cp_branch}" + if ! git push --force-with-lease origin "$sync_branch"; then + echo "::warning:: Failed to push branch ${sync_branch}" popd >/dev/null git worktree remove --force "$worktree_path" 2>/dev/null || true return 0 fi - create_success_pr "$target" "$cp_branch" + create_success_pr "$target" "$sync_branch" popd >/dev/null git worktree remove --force "$worktree_path" 2>/dev/null || true @@ -233,18 +206,18 @@ cherry_pick_to_target() { TARGETS_CSV="$(resolve_targets_csv "$BASE_REF")" if [ -z "$TARGETS_CSV" ]; then - echo "No cherry-pick targets configured for base branch: ${BASE_REF}" + echo "No sync targets configured for base branch: ${BASE_REF}" exit 0 fi git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" -extract_pr_title_parts +sanitize_pr_title IFS=',' read -ra TARGETS <<< "$TARGETS_CSV" for target in "${TARGETS[@]}"; do - cherry_pick_to_target "$target" + sync_to_target "$target" done rm -rf "$WORKTREE_ROOT" From 37cc2c204991b7fab5d9c0d380142d0f4c621d4b Mon Sep 17 00:00:00 2001 From: David Seveloff Date: Sun, 30 Aug 2026 16:05:24 +0300 Subject: [PATCH 4/4] CI: Rename action path to sync-release-branches [ED-25426] Ref: ED-25426 Co-authored-by: Cursor --- .../README.md | 2 +- .../action.yml | 2 +- .../sync-release-branches.sh} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename actions/{cherry-pick-release-branches => sync-release-branches}/README.md (97%) rename actions/{cherry-pick-release-branches => sync-release-branches}/action.yml (97%) rename actions/{cherry-pick-release-branches/cherry-pick-release-branches.sh => sync-release-branches/sync-release-branches.sh} (100%) diff --git a/actions/cherry-pick-release-branches/README.md b/actions/sync-release-branches/README.md similarity index 97% rename from actions/cherry-pick-release-branches/README.md rename to actions/sync-release-branches/README.md index b9ce086a9..e07d0a860 100644 --- a/actions/cherry-pick-release-branches/README.md +++ b/actions/sync-release-branches/README.md @@ -26,7 +26,7 @@ steps: fetch-depth: 0 token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} - - uses: elementor/elementor-editor-github-actions/actions/cherry-pick-release-branches@main + - uses: elementor/elementor-editor-github-actions/actions/sync-release-branches@main with: token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} using-pat: ${{ secrets.GH_PAT != '' }} diff --git a/actions/cherry-pick-release-branches/action.yml b/actions/sync-release-branches/action.yml similarity index 97% rename from actions/cherry-pick-release-branches/action.yml rename to actions/sync-release-branches/action.yml index af51ada0f..c863558ce 100644 --- a/actions/cherry-pick-release-branches/action.yml +++ b/actions/sync-release-branches/action.yml @@ -73,4 +73,4 @@ runs: BETA_BRANCH: ${{ inputs.beta-branch }} MAIN_BRANCH: ${{ inputs.main-branch }} ACTION_PATH: ${{ github.action_path }} - run: bash "$ACTION_PATH/cherry-pick-release-branches.sh" + run: bash "$ACTION_PATH/sync-release-branches.sh" diff --git a/actions/cherry-pick-release-branches/cherry-pick-release-branches.sh b/actions/sync-release-branches/sync-release-branches.sh similarity index 100% rename from actions/cherry-pick-release-branches/cherry-pick-release-branches.sh rename to actions/sync-release-branches/sync-release-branches.sh