diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml new file mode 100644 index 0000000..886a977 --- /dev/null +++ b/.github/workflows/cherry-pick.yml @@ -0,0 +1,257 @@ +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: '' + 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' + 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 }} + # 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 + # 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 --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}; } + 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 }} + 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="${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) + 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 + + # 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; capture stderr to reliably distinguish empty vs conflict + CONFLICT=false + EMPTY=false + for COMMIT in $COMMITS; do + 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 || git reset --hard HEAD 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 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 + + # 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 + 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 + + 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 + done <<< "$TARGET_BRANCHES" diff --git a/.github/workflows/gatekeeper.yml b/.github/workflows/gatekeeper.yml new file mode 100644 index 0000000..70cc27e --- /dev/null +++ b/.github/workflows/gatekeeper.yml @@ -0,0 +1,215 @@ +name: Shared Cross-Repo Gatekeeper + +on: + workflow_call: + secrets: + CROSS_REPO_TOKEN: + description: 'PAT with org-wide search permissions for cross-repo PR lookup' + required: false + +jobs: + check-reviews: + runs-on: ubuntu-latest + steps: + - name: Verify All Sister PRs Are Approved + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_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-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) + + # 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" --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 --limit 100 --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" --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 --limit 100 --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."