From 3638cfbefd0e01455815b3c7015348c392c05019 Mon Sep 17 00:00:00 2001 From: bunnam988 Date: Fri, 7 Aug 2026 12:28:40 +0530 Subject: [PATCH 1/3] Add shared cherry-pick engine and cross-repo gatekeeper workflows --- .github/workflows/cherry-pick.yml | 220 ++++++++++++++++++++++++++++++ .github/workflows/gatekeeper.yml | 211 ++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 .github/workflows/cherry-pick.yml create mode 100644 .github/workflows/gatekeeper.yml diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml new file mode 100644 index 0000000..cd133d1 --- /dev/null +++ b/.github/workflows/cherry-pick.yml @@ -0,0 +1,220 @@ +name: Shared Cherry-Pick Engine + +on: + workflow_call: + inputs: + raw_labels: + description: 'JSON array of labels from the triggering PR event' + required: true + type: string + trigger_label: + description: 'The specific label that triggered this event (from github.event.label.name). Empty for closed events.' + required: false + type: string + default: '' + secrets: + CROSS_REPO_TOKEN: + description: 'PAT with org-wide repo write access for cross-repo label propagation' + required: false + +jobs: + backport: + runs-on: ubuntu-latest + steps: + - name: Extract Metadata & Propagate to Sister PRs + id: parse_meta + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN || secrets.GITHUB_TOKEN }} + CURRENT_PR_URL: ${{ github.event.pull_request.html_url }} + EVENT_LABEL: ${{ github.event.label.name }} + run: | + # 1. Parse the target destination branch(es) and custom topic label + # Prefer the event label (the label that was just added) over grepping the full list + TRIGGER_LABEL="${{ inputs.trigger_label }}" + # If caller didn't pass trigger_label, try from event context directly + if [ -z "$TRIGGER_LABEL" ]; then + TRIGGER_LABEL="$EVENT_LABEL" + fi + + if [[ "$TRIGGER_LABEL" == cherry-pick\ to\ * ]]; then + # Single target from labeled event + TARGET_BRANCHES="${TRIGGER_LABEL#cherry-pick to }" + else + # Fallback for 'closed' events: process ALL cherry-pick labels (handles pre-merge labeling) + TARGET_BRANCHES=$(echo '${{ inputs.raw_labels }}' | jq -r '.[] | .name' | grep "cherry-pick to " | sed 's/cherry-pick to //' || true) + fi + + CUSTOM_TOPIC=$(echo '${{ inputs.raw_labels }}' | jq -r '.[] | .name' | grep "^topic:" | head -n 1) + + if [ -z "$TARGET_BRANCHES" ]; then + echo "No valid cherry-pick destination branch found. Exiting." + exit 0 + fi + + # Output as newline-separated list for multi-target support + echo "target_branches<> $GITHUB_OUTPUT + echo "$TARGET_BRANCHES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "custom_topic=$CUSTOM_TOPIC" >> $GITHUB_OUTPUT + + echo "Target branches: $(echo $TARGET_BRANCHES | tr '\n' ', ')" + + # 2. THE CHAIN REACTION: If a topic label exists, find and tag sister PRs + if [ -n "$CUSTOM_TOPIC" ]; then + echo "Found topic label '$CUSTOM_TOPIC'. Searching for sister PRs to auto-trigger..." + ORG_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f1) + + # Find all merged PRs across the org sharing this exact topic label + SISTER_PRS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state closed --merged --json url,labels 2>/dev/null || echo "[]") + + for row in $(echo "$SISTER_PRS" | jq -r '.[] | @base64'); do + _jq() { echo ${row} | base64 --decode | jq -r ${1}; } + SISTER_URL=$(_jq '.url') + + # Skip the current PR (already running) + if [ "$SISTER_URL" == "$CURRENT_PR_URL" ]; then + echo "â­ī¸ Skipping self: $SISTER_URL" + continue + fi + + # Propagate ALL target labels to sister PRs + while IFS= read -r BRANCH; do + [ -z "$BRANCH" ] && continue + LABEL_TO_ADD="cherry-pick to $BRANCH" + + # Guard: skip if sister PR already has this label (prevents infinite loop) + EXISTING_LABELS=$(echo ${row} | base64 --decode | jq -r '.labels[].name' 2>/dev/null) + if echo "$EXISTING_LABELS" | grep -qF "$LABEL_TO_ADD"; then + echo "â­ī¸ Already labeled ($LABEL_TO_ADD): $SISTER_URL" + continue + fi + + echo "🔗 Propagating '$LABEL_TO_ADD' to sister PR: $SISTER_URL" + SISTER_REPO=$(echo "$SISTER_URL" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/.*|\1|') + gh label create "$LABEL_TO_ADD" --repo "$SISTER_REPO" --color "d93f0b" --force >/dev/null 2>&1 || true + gh pr edit "$SISTER_URL" --add-label "$LABEL_TO_ADD" 2>/dev/null || echo "âš ī¸ Could not label $SISTER_URL (check token permissions)" + done <<< "$TARGET_BRANCHES" + done + fi + + - name: Checkout Code + if: steps.parse_meta.outputs.target_branches != '' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.CROSS_REPO_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Execute Cherry-Pick + if: steps.parse_meta.outputs.target_branches != '' + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN || secrets.GITHUB_TOKEN }} + TARGET_BRANCHES: ${{ steps.parse_meta.outputs.target_branches }} + CUSTOM_TOPIC: ${{ steps.parse_meta.outputs.custom_topic }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + REPO: ${{ github.repository }} + run: | + set -e + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Get the merge commit and find the original commits + MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" + COMMITS=$(git log --pretty=format:"%H" ${MERGE_SHA}^1..${MERGE_SHA}^2 2>/dev/null || echo "$MERGE_SHA") + if [ -z "$COMMITS" ]; then + COMMITS="$MERGE_SHA" + fi + + # Process each target branch + while IFS= read -r TARGET_BRANCH; do + [ -z "$TARGET_BRANCH" ] && continue + echo "" + echo "==========================================" + echo "đŸŽ¯ Cherry-picking to: $TARGET_BRANCH" + echo "==========================================" + + CHERRY_PICK_BRANCH="cherry-pick-${PR_NUMBER}-to-$(echo $TARGET_BRANCH | sed 's|/|-|g')" + + # Build backport label + BACKPORT_LABEL="" + if [ -n "$CUSTOM_TOPIC" ]; then + BRANCH_SUFFIX=$(echo "$TARGET_BRANCH" | sed 's|^support/||; s|^release/||; s|^feature/||') + BACKPORT_LABEL="${CUSTOM_TOPIC}_${BRANCH_SUFFIX}" + fi + + # Verify target branch exists + if ! git fetch origin "$TARGET_BRANCH" 2>/dev/null; then + gh pr comment "$PR_URL" --body "$(printf '❌ **Cherry-pick failed:** Target branch \x60%s\x60 does not exist.\n\nPlease check the label for typos and ensure the branch has been created.' "$TARGET_BRANCH")" + echo "Error: Target branch '$TARGET_BRANCH' does not exist. Skipping." + continue + fi + + # Create cherry-pick branch + git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH" + + # Attempt cherry-pick + CONFLICT=false + EMPTY=false + for COMMIT in $COMMITS; do + if ! git cherry-pick -x "$COMMIT" 2>/dev/null; then + if git diff --cached --quiet 2>/dev/null && [ -z "$(git diff)" ]; then + EMPTY=true + git cherry-pick --abort 2>/dev/null || true + else + CONFLICT=true + git cherry-pick --abort 2>/dev/null || true + fi + break + fi + done + + if [ "$EMPTY" = "true" ]; then + gh pr comment "$PR_URL" --body "$(printf 'â„šī¸ **Cherry-pick to \x60%s\x60 skipped:** The changes already exist on the target branch.\n\nNo action needed — this commit was likely already applied manually.' "$TARGET_BRANCH")" + echo "Cherry-pick skipped: changes already on $TARGET_BRANCH." + git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + continue + fi + + if [ "$CONFLICT" = "true" ]; then + LABEL_HINT="" + if [ -n "$BACKPORT_LABEL" ]; then + LABEL_HINT=$(printf '\n\n> âš ī¸ **Important:** Add the \x60%s\x60 label to your manual PR. This is required for cross-repo cascade — without it, sister repositories will not be triggered for further cherry-picks from this branch.' "$BACKPORT_LABEL") + fi + gh pr comment "$PR_URL" --body "$(printf 'âš ī¸ **Cherry-pick to \x60%s\x60 failed due to merge conflicts.**\n\nPlease resolve the conflicts manually and create a PR targeting \x60%s\x60.%s' "$TARGET_BRANCH" "$TARGET_BRANCH" "$LABEL_HINT")" + echo "Cherry-pick to $TARGET_BRANCH failed due to conflicts." + git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + continue + fi + + # Push the branch (force for idempotency) + git push --force origin "$CHERRY_PICK_BRANCH" + + # Build label args + LABEL_ARGS="" + if [ -n "$BACKPORT_LABEL" ]; then + gh label create "$BACKPORT_LABEL" --repo "$REPO" --color "0e8a16" --force >/dev/null 2>&1 || true + LABEL_ARGS="--label $BACKPORT_LABEL" + fi + + # Create PR only if one doesn't already exist + EXISTING_PR=$(gh pr list --repo "$REPO" --head "$CHERRY_PICK_BRANCH" --state open --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + echo "â„šī¸ Cherry-pick PR #$EXISTING_PR already exists for $CHERRY_PICK_BRANCH. Updated branch." + git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + continue + fi + + gh pr create \ + --repo "$REPO" \ + --head "$CHERRY_PICK_BRANCH" \ + --base "$TARGET_BRANCH" \ + --title "$PR_TITLE" \ + --body "Cherry-pick of #${PR_NUMBER} to \`$TARGET_BRANCH\`." \ + $LABEL_ARGS + + echo "✅ Cherry-pick PR created for $TARGET_BRANCH." + git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + done <<< "$TARGET_BRANCHES" diff --git a/.github/workflows/gatekeeper.yml b/.github/workflows/gatekeeper.yml new file mode 100644 index 0000000..5d5a7b7 --- /dev/null +++ b/.github/workflows/gatekeeper.yml @@ -0,0 +1,211 @@ +name: Shared Cross-Repo Gatekeeper + +on: + workflow_call: # Allows this script to be called by your other 50+ component repos + +jobs: + check-reviews: + runs-on: ubuntu-latest + steps: + - name: Verify All Sister PRs Are Approved + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + # 1. Fetch current PR metadata including labels and target base branch + PR_DATA=$(gh pr view "$PR_NUMBER" -R "$REPO" --json title,body,baseRefName,labels) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + PR_BODY=$(echo "$PR_DATA" | jq -r '.body') + TARGET_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName') + + echo "Analyzing PR metadata for tracking elements on target branch: $TARGET_BRANCH" + + # ========================================== + # 2. EXTRACT TRACKING KEYS & CUSTOM TOPIC LABELS + # ========================================== + # Extract unique uppercase tracking keys from Title/Body (e.g., RDKB-64491) + ALL_IDS=$(echo -e "${PR_TITLE}\n${PR_BODY}" | grep -oE '[A-Z0-9]+-[0-9]+' | sort -u) + + # Extract any user-defined custom topic label (e.g., topic:billing-engine-v2) + CUSTOM_TOPIC=$(echo "$PR_DATA" | jq -r '.labels[].name' 2>/dev/null | grep "^topic:" | head -n 1) + + # If there are no ticket IDs AND no custom topic labels, skip validation safely + if [ -z "$ALL_IDS" ] && [ -z "$CUSTOM_TOPIC" ]; then + echo "No uppercase tracking keys or custom 'topic:*' labels found. Skipping cross-repo validation." + exit 0 + fi + + # Build a clean, dynamic GitHub OR search query string + SEARCH_QUERY="" + if [ -n "$ALL_IDS" ]; then + SEARCH_QUERY=$(echo "$ALL_IDS" | tr '\n' ' ' | sed 's/ $//' | sed 's/ /" OR "/g' | sed 's/^/"/' | sed 's/$/"/') + fi + + if [ -n "$CUSTOM_TOPIC" ]; then + if [ -n "$SEARCH_QUERY" ]; then + SEARCH_QUERY="${SEARCH_QUERY} OR label:\"${CUSTOM_TOPIC}\"" + else + SEARCH_QUERY="label:\"${CUSTOM_TOPIC}\"" + fi + fi + + echo "Active Dynamic Topic Identifiers: $SEARCH_QUERY" + ORG_NAME=$(echo "$REPO" | cut -d'/' -f1) + + # ========================================== + # 3. RUN HISTORICAL AUDIT FOR STABLE TRACKS + # ========================================== + MISSING_CHERRY_PICK_LIST="" + + # Helper: Run ticket key search + topic label search separately, then merge & deduplicate + run_search() { + local state_flag="$1" + local base_flag="$2" + local results="[]" + local merged_flag="" + + # gh search prs --state only accepts open|closed. For merged, use --state closed --merged + if [ "$state_flag" == "merged" ]; then + state_flag="closed" + merged_flag="--merged" + fi + + # Search by ticket keys + if [ -n "$ALL_IDS" ]; then + KEY_QUERY=$(echo "$ALL_IDS" | tr '\n' ' ' | sed 's/ $//' | sed 's/ /" OR "/g' | sed 's/^/"/' | sed 's/$/"/') + if [ -n "$base_flag" ]; then + KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --json url,repository 2>/dev/null || echo "[]") + else + KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --json url,repository 2>/dev/null || echo "[]") + fi + results=$(echo "$results $KEY_RESULTS" | jq -s 'add') + fi + + # Search by topic label (separate query using --label flag) + if [ -n "$CUSTOM_TOPIC" ]; then + if [ -n "$base_flag" ]; then + LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --json url,repository 2>/dev/null || echo "[]") + else + LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --json url,repository 2>/dev/null || echo "[]") + fi + results=$(echo "$results $LABEL_RESULTS" | jq -s 'add') + fi + + # Deduplicate by URL + echo "$results" | jq 'unique_by(.url)' + } + + # Isolate search to the target branch if running on a strict production track + if [[ "$TARGET_BRANCH" =~ ^(release|support)/ ]]; then + echo "đŸ•ĩī¸ Stable/Release track detected. Auditing original development footprint..." + + # Narrow open PR search strictly to this specific target release/support branch + SISTER_PRS=$(run_search "open" "$TARGET_BRANCH") + REPOS_WITH_RELEASE_PRS=$(echo "$SISTER_PRS" | jq -r '.[].repository.name' | sort -u) + + # Find where this cluster originally landed historically on the develop branch + HISTORICAL_PRS=$(run_search "merged" "develop") + ORIGINAL_REPOS=$(echo "$HISTORICAL_PRS" | jq -r '.[].repository.name' | sort -u) + + while read -r repo; do + if [ -n "$repo" ] && ! echo "$REPOS_WITH_RELEASE_PRS" | grep -q "^$repo$"; then + echo "âš ī¸ CRITICAL MISS: Source code changed in '$repo' on develop, but no release PR exists!" + MISSING_CHERRY_PICK_LIST="${MISSING_CHERRY_PICK_LIST}\n| 🛑 | rdkcentral/${repo} | MISSING CHERRY-PICK |" + fi + done <<< "$ORIGINAL_REPOS" + else + # Standard develop branch workflow: Scan all open matches across the organization globally + SISTER_PRS=$(run_search "open" "") + fi + + COUNT=$(echo "$SISTER_PRS" | jq '. | length') + echo "Found $COUNT open PR(s) linked to this active cluster." + + # ========================================== + # 4. EVALUATE PEER APPROVAL STATUS + # ========================================== + CURRENT_PR_URL="https://github.com/${REPO}/pull/${PR_NUMBER}" + UNAPPROVED_PRS=0 + BLOCKED_LIST="" + APPROVED_LIST="" + + for row in $(echo "$SISTER_PRS" | jq -r '.[] | @base64'); do + _jq() { + echo ${row} | base64 --decode | jq -r ${1} + } + PR_URL=$(_jq '.url') + + # Skip evaluating the current PR itself + if [ "$PR_URL" == "$CURRENT_PR_URL" ]; then + echo "â­ī¸ Skipping self: $PR_URL" + continue + fi + + echo "Evaluating: $PR_URL" + + # Fetch the official review state (APPROVED, CHANGES_REQUESTED, or REVIEW_REQUIRED) + REVIEW_STATUS=$(gh pr view "$PR_URL" --json reviewDecision --jq '.reviewDecision' 2>/dev/null || echo "REVIEW_REQUIRED") + + if [ "$REVIEW_STATUS" != "APPROVED" ]; then + echo "❌ Gate Lock: $PR_URL is missing peer approval (Current status: $REVIEW_STATUS)" + UNAPPROVED_PRS=$((UNAPPROVED_PRS + 1)) + BLOCKED_LIST="${BLOCKED_LIST}\n| ❌ | ${PR_URL} | ${REVIEW_STATUS} |" + else + echo "✅ Gate Clear: $PR_URL is approved." + APPROVED_LIST="${APPROVED_LIST}\n| ✅ | ${PR_URL} | APPROVED |" + fi + done + + # ========================================== + # 5. BUILD SUMMARY REPORT + # ========================================== + SUMMARY="## 🔒 Cross-Repo Gatekeeper Report\n\n" + SUMMARY+="**Target Base Branch:** \`${TARGET_BRANCH}\`\n" + SUMMARY+="**Active Identifiers Evaluated:** ${SEARCH_QUERY}\n" + SUMMARY+="**Total Sister PRs Found:** $((COUNT - 1)) (excluding self)\n\n" + SUMMARY+="| Status | Resource / Pull Request | Review State |\n" + SUMMARY+="|--------|-------------|-------------|\n" + + if [ -n "$MISSING_CHERRY_PICK_LIST" ]; then + SUMMARY+=$(echo -e "$MISSING_CHERRY_PICK_LIST") + SUMMARY+="\n" + fi + if [ -n "$BLOCKED_LIST" ]; then + SUMMARY+=$(echo -e "$BLOCKED_LIST") + SUMMARY+="\n" + fi + if [ -n "$APPROVED_LIST" ]; then + SUMMARY+=$(echo -e "$APPROVED_LIST") + SUMMARY+="\n" + fi + + echo -e "$SUMMARY" >> "$GITHUB_STEP_SUMMARY" + + # ========================================== + # 6. ENFORCE MERGE STATUS CONDITIONS + # ========================================== + # RULE A: Missing cherry-picks ALWAYS break production release/support trains + if [ -n "$MISSING_CHERRY_PICK_LIST" ]; then + echo -e "\n> ⛔ **Merge Blocked:** Component repositories were modified on \`develop\` but are completely missing cherry-pick PRs on this stable track." >> "$GITHUB_STEP_SUMMARY" + echo "Error: Cannot merge. Missing component cherry-pick PRs detected." + exit 1 + fi + + # RULE B: Enforce review approvals based on the branch policy + if [ "$UNAPPROVED_PRS" -gt 0 ]; then + if [[ "$TARGET_BRANCH" =~ ^(release|support)/ ]]; then + # STRICT LOCK FOR STABLE TRACKS: Block partial deployments + echo -e "\n> ⛔ **Merge Blocked:** All companion topic component PRs must be approved before ANY can merge into the \`$TARGET_BRANCH\` track." >> "$GITHUB_STEP_SUMMARY" + echo "Error: Cannot merge. One or more cross-repository components in this sequence are still in review." + exit 1 + else + # LOOSE FOR DEVELOP: Informative logging, but zero blocks to maintain developer velocity + echo -e "\n> âš ī¸ **Informational Notice:** Some companion tasks under this topic are still in review across the org. Merging is allowed independently on develop." >> "$GITHUB_STEP_SUMMARY" + echo "Success: Independent branch development allowed on develop track." + exit 0 + fi + fi + + echo -e "\n> **✅ All Clear:** All cross-repo requirements are approved and complete. Safe to merge." >> "$GITHUB_STEP_SUMMARY" + echo "Success: All cross-repository conditions for this cluster are clean! Safe to merge." From 2c430dd14996f753e589bfbb8430076fdad39916 Mon Sep 17 00:00:00 2001 From: bunnam988 Date: Fri, 7 Aug 2026 12:28:40 +0530 Subject: [PATCH 2/3] Fix cherry-pick and gatekeeper workflows for prod - Remove set -e; add error handling around push/PR create so loop continues - Fix commit range for squash/rebase merges via PR API commit count - Fix empty cherry-pick detection: use stderr grep instead of fragile diff check - Fix unquoted LABEL_ARGS: separate gh pr create calls per label presence - Add --limit 100 to all gh search prs calls (was silently capping at 30) - Fix jq -s 'add' -> 'add // []' to prevent null on empty array inputs - Fix ticket regex [A-Z0-9]+ -> [A-Z][A-Z0-9]+ to match rdkcentral format - Add CROSS_REPO_TOKEN secret input to shared gatekeeper for org-wide search --- .github/workflows/cherry-pick.yml | 59 ++++++++++++++++++++----------- .github/workflows/gatekeeper.yml | 22 +++++++----- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index cd133d1..12bccf4 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -65,7 +65,7 @@ jobs: ORG_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f1) # Find all merged PRs across the org sharing this exact topic label - SISTER_PRS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state closed --merged --json url,labels 2>/dev/null || echo "[]") + SISTER_PRS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state closed --merged --limit 100 --json url,labels 2>/dev/null || echo "[]") for row in $(echo "$SISTER_PRS" | jq -r '.[] | @base64'); do _jq() { echo ${row} | base64 --decode | jq -r ${1}; } @@ -115,15 +115,20 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} REPO: ${{ github.repository }} run: | - set -e - # Configure git git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - # Get the merge commit and find the original commits + # Resolve commits that landed on base branch, handling all merge strategies MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" - COMMITS=$(git log --pretty=format:"%H" ${MERGE_SHA}^1..${MERGE_SHA}^2 2>/dev/null || echo "$MERGE_SHA") + if git cat-file -e "${MERGE_SHA}^2" 2>/dev/null; then + # Standard merge commit (2 parents): replay individual feature branch commits in order + COMMITS=$(git log --pretty=format:"%H" --reverse ${MERGE_SHA}^1..${MERGE_SHA}^2 2>/dev/null) + else + # Squash or rebase: derive commit count from PR API to walk back from merge SHA + PR_COMMIT_COUNT=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json commits --jq '.commits | length' 2>/dev/null || echo "1") + COMMITS=$(git log --pretty=format:"%H" --reverse -"${PR_COMMIT_COUNT}" "${MERGE_SHA}" 2>/dev/null) + fi if [ -z "$COMMITS" ]; then COMMITS="$MERGE_SHA" fi @@ -155,14 +160,15 @@ jobs: # Create cherry-pick branch git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH" - # Attempt cherry-pick + # Attempt cherry-pick; capture stderr to reliably distinguish empty vs conflict CONFLICT=false EMPTY=false for COMMIT in $COMMITS; do - if ! git cherry-pick -x "$COMMIT" 2>/dev/null; then - if git diff --cached --quiet 2>/dev/null && [ -z "$(git diff)" ]; then + CHERRY_OUTPUT=$(git cherry-pick -x "$COMMIT" 2>&1) && CHERRY_EXIT=0 || CHERRY_EXIT=$? + if [ $CHERRY_EXIT -ne 0 ]; then + if echo "$CHERRY_OUTPUT" | grep -qiE "empty|nothing to commit|already applied"; then EMPTY=true - git cherry-pick --abort 2>/dev/null || true + git cherry-pick --abort 2>/dev/null || git reset --hard HEAD 2>/dev/null || true else CONFLICT=true git cherry-pick --abort 2>/dev/null || true @@ -189,14 +195,16 @@ jobs: continue fi - # Push the branch (force for idempotency) - git push --force origin "$CHERRY_PICK_BRANCH" + # Push with error handling so remaining branches are still processed on failure + if ! git push --force origin "$CHERRY_PICK_BRANCH" 2>/dev/null; then + echo "âš ī¸ Failed to push $CHERRY_PICK_BRANCH. Skipping PR creation for $TARGET_BRANCH." + git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + continue + fi - # Build label args - LABEL_ARGS="" + # Ensure backport label exists in the repo before attaching it if [ -n "$BACKPORT_LABEL" ]; then gh label create "$BACKPORT_LABEL" --repo "$REPO" --color "0e8a16" --force >/dev/null 2>&1 || true - LABEL_ARGS="--label $BACKPORT_LABEL" fi # Create PR only if one doesn't already exist @@ -207,13 +215,22 @@ jobs: continue fi - gh pr create \ - --repo "$REPO" \ - --head "$CHERRY_PICK_BRANCH" \ - --base "$TARGET_BRANCH" \ - --title "$PR_TITLE" \ - --body "Cherry-pick of #${PR_NUMBER} to \`$TARGET_BRANCH\`." \ - $LABEL_ARGS + if [ -n "$BACKPORT_LABEL" ]; then + gh pr create \ + --repo "$REPO" \ + --head "$CHERRY_PICK_BRANCH" \ + --base "$TARGET_BRANCH" \ + --title "$PR_TITLE" \ + --body "Cherry-pick of #${PR_NUMBER} to \`$TARGET_BRANCH\`." \ + --label "$BACKPORT_LABEL" || echo "âš ī¸ Failed to create PR for $TARGET_BRANCH." + else + gh pr create \ + --repo "$REPO" \ + --head "$CHERRY_PICK_BRANCH" \ + --base "$TARGET_BRANCH" \ + --title "$PR_TITLE" \ + --body "Cherry-pick of #${PR_NUMBER} to \`$TARGET_BRANCH\`." || echo "âš ī¸ Failed to create PR for $TARGET_BRANCH." + fi echo "✅ Cherry-pick PR created for $TARGET_BRANCH." git checkout --detach 2>/dev/null; git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true diff --git a/.github/workflows/gatekeeper.yml b/.github/workflows/gatekeeper.yml index 5d5a7b7..70cc27e 100644 --- a/.github/workflows/gatekeeper.yml +++ b/.github/workflows/gatekeeper.yml @@ -1,7 +1,11 @@ name: Shared Cross-Repo Gatekeeper on: - workflow_call: # Allows this script to be called by your other 50+ component repos + workflow_call: + secrets: + CROSS_REPO_TOKEN: + description: 'PAT with org-wide search permissions for cross-repo PR lookup' + required: false jobs: check-reviews: @@ -9,7 +13,7 @@ jobs: steps: - name: Verify All Sister PRs Are Approved env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN || secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} run: | @@ -25,7 +29,7 @@ jobs: # 2. EXTRACT TRACKING KEYS & CUSTOM TOPIC LABELS # ========================================== # Extract unique uppercase tracking keys from Title/Body (e.g., RDKB-64491) - ALL_IDS=$(echo -e "${PR_TITLE}\n${PR_BODY}" | grep -oE '[A-Z0-9]+-[0-9]+' | sort -u) + ALL_IDS=$(echo -e "${PR_TITLE}\n${PR_BODY}" | grep -oE '[A-Z][A-Z0-9]+-[0-9]+' | sort -u) # Extract any user-defined custom topic label (e.g., topic:billing-engine-v2) CUSTOM_TOPIC=$(echo "$PR_DATA" | jq -r '.labels[].name' 2>/dev/null | grep "^topic:" | head -n 1) @@ -75,21 +79,21 @@ jobs: if [ -n "$ALL_IDS" ]; then KEY_QUERY=$(echo "$ALL_IDS" | tr '\n' ' ' | sed 's/ $//' | sed 's/ /" OR "/g' | sed 's/^/"/' | sed 's/$/"/') if [ -n "$base_flag" ]; then - KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --json url,repository 2>/dev/null || echo "[]") + KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --limit 100 --json url,repository 2>/dev/null || echo "[]") else - KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --json url,repository 2>/dev/null || echo "[]") + KEY_RESULTS=$(gh search prs "${KEY_QUERY}" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --limit 100 --json url,repository 2>/dev/null || echo "[]") fi - results=$(echo "$results $KEY_RESULTS" | jq -s 'add') + results=$(echo "$results $KEY_RESULTS" | jq -s 'add // []') fi # Search by topic label (separate query using --label flag) if [ -n "$CUSTOM_TOPIC" ]; then if [ -n "$base_flag" ]; then - LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --json url,repository 2>/dev/null || echo "[]") + LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --base "$base_flag" --limit 100 --json url,repository 2>/dev/null || echo "[]") else - LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --json url,repository 2>/dev/null || echo "[]") + LABEL_RESULTS=$(gh search prs --label "$CUSTOM_TOPIC" --owner "$ORG_NAME" --state "$state_flag" $merged_flag --limit 100 --json url,repository 2>/dev/null || echo "[]") fi - results=$(echo "$results $LABEL_RESULTS" | jq -s 'add') + results=$(echo "$results $LABEL_RESULTS" | jq -s 'add // []') fi # Deduplicate by URL From 36d4de50303b665465bcfa24c76987d6dc206ef0 Mon Sep 17 00:00:00 2001 From: bunnam988 Date: Fri, 7 Aug 2026 13:44:31 +0530 Subject: [PATCH 3/3] Handle issues:labeled trigger for already-merged PR cherry-picks - Add pr_number_override input to support triggering from issues:labeled event - Fetch PR context (title, URL, merge SHA) via API when pull_request context is empty - Fix CURRENT_PR_URL in parse_meta for issues event using format() expression --- .github/workflows/cherry-pick.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 12bccf4..886a977 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -12,6 +12,11 @@ on: required: false type: string default: '' + pr_number_override: + description: 'PR number when triggered via issues:labeled on an already-merged PR (github.event.pull_request context is unavailable in that case).' + required: false + type: string + default: '' secrets: CROSS_REPO_TOKEN: description: 'PAT with org-wide repo write access for cross-repo label propagation' @@ -25,7 +30,8 @@ jobs: id: parse_meta env: GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN || secrets.GITHUB_TOKEN }} - CURRENT_PR_URL: ${{ github.event.pull_request.html_url }} + # For issues:labeled events on merged PRs, pull_request context is empty — construct URL from override + CURRENT_PR_URL: ${{ inputs.pr_number_override != '' && format('https://github.com/{0}/pull/{1}', github.repository, inputs.pr_number_override) || github.event.pull_request.html_url }} EVENT_LABEL: ${{ github.event.label.name }} run: | # 1. Parse the target destination branch(es) and custom topic label @@ -113,14 +119,28 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} PR_URL: ${{ github.event.pull_request.html_url }} + PR_NUMBER_OVERRIDE: ${{ inputs.pr_number_override }} REPO: ${{ github.repository }} run: | # Configure git git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + # When triggered via issues:labeled on an already-merged PR, pull_request context is empty + if [ -n "$PR_NUMBER_OVERRIDE" ] && [ -z "$PR_NUMBER" ]; then + PR_DATA=$(gh pr view "$PR_NUMBER_OVERRIDE" --repo "$REPO" --json title,url,mergeCommit,state) + if [ "$(echo "$PR_DATA" | jq -r '.state')" != "MERGED" ]; then + echo "PR #$PR_NUMBER_OVERRIDE is not yet merged. Skipping." + exit 0 + fi + PR_NUMBER="$PR_NUMBER_OVERRIDE" + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + PR_URL=$(echo "$PR_DATA" | jq -r '.url') + MERGE_SHA=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty') + fi + # Resolve commits that landed on base branch, handling all merge strategies - MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" + MERGE_SHA="${MERGE_SHA:-${{ github.event.pull_request.merge_commit_sha }}}" if git cat-file -e "${MERGE_SHA}^2" 2>/dev/null; then # Standard merge commit (2 parents): replay individual feature branch commits in order COMMITS=$(git log --pretty=format:"%H" --reverse ${MERGE_SHA}^1..${MERGE_SHA}^2 2>/dev/null)