Skip to content

Orchestrator Auto-Merge #1226

Orchestrator Auto-Merge

Orchestrator Auto-Merge #1226

name: Orchestrator Auto-Merge
permissions:
contents: write
pull-requests: write
on:
workflow_run:
workflows: ["Orchestrator CI"]
types:
- completed
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
enable-auto-merge:
name: 🤖 Enable Auto-Merge
runs-on: ubuntu-latest
# Job runs on both success and failure to provide feedback
# Only merges if CI succeeded (checked in steps)
steps:
- name: ⤵️ Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: 🔍 Find PR for this commit
id: find-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
HEAD_SHA="${{ github.event.workflow_run.head_sha }}"
echo "Looking for PR with head SHA: $HEAD_SHA"
# Try workflow_run.pull_requests first (fastest if available)
PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}"
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then
echo "workflow_run.pull_requests is empty, querying API..."
# Fetch PR via GitHub API using commit SHA
PR_DATA=$(gh api "repos/${{ github.repository }}/commits/$HEAD_SHA/pulls" \
--jq '.[0] | {number: .number, author: .user.login}' 2>/dev/null || echo "")
if [ -z "$PR_DATA" ] || [ "$PR_DATA" = "null" ]; then
echo "DEBUG: No PR found via SHA ($HEAD_SHA). Trying branch fallback..."
echo "DEBUG: Head Branch: '$HEAD_BRANCH'"
if [ -n "$HEAD_BRANCH" ]; then
echo "DEBUG: Searching for open PRs with head branch: $HEAD_BRANCH"
# Capture raw output first for debugging
RAW_SEARCH=$(gh pr list --head "$HEAD_BRANCH" --state open --json number,author)
echo "DEBUG: Raw search result: $RAW_SEARCH"
PR_DATA=$(echo "$RAW_SEARCH" | jq '.[0] | {number: .number, author: .author.login}' 2>/dev/null || echo "")
else
echo "DEBUG: Head branch is empty."
fi
fi
if [ -z "$PR_DATA" ] || [ "$PR_DATA" = "null" ]; then
echo "DEBUG: Final PR_DATA check failed."
echo "No PR found for commit $HEAD_SHA or branch $HEAD_BRANCH"
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author')
else
# Even if workflow_run.pull_requests is available, fetch author from PR to ensure accuracy
# The actor might be a bot, but we need the actual PR author
echo "workflow_run.pull_requests found PR #$PR_NUMBER, fetching author from PR..."
PR_AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login' 2>/dev/null || echo "")
if [ -z "$PR_AUTHOR" ] || [ "$PR_AUTHOR" = "null" ]; then
echo "⚠️ Could not fetch PR author, falling back to actor"
PR_AUTHOR="${{ github.event.workflow_run.actor.login }}"
fi
fi
# Final validation: ensure we have a valid author
if [ -z "$PR_AUTHOR" ] || [ "$PR_AUTHOR" = "null" ]; then
echo "⚠️ Unable to determine PR author. Skipping auto-merge."
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Found PR #$PR_NUMBER by $PR_AUTHOR"
{
echo "pr_number=$PR_NUMBER"
echo "pr_author=$PR_AUTHOR"
echo "found=true"
} >> "$GITHUB_OUTPUT"
- name: ❌ Report CI Failure
if: steps.find-pr.outputs.found == 'true' && github.event.workflow_run.conclusion != 'success'
uses: actions/github-script@v8
env:
RUN_NUMBER: "${{ github.event.workflow_run.run_number }}"
RUN_URL: "${{ github.event.workflow_run.html_url }}"
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = parseInt(process.env.PR_NUMBER);
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const signature = "❌ **Auto-merge skipped:**";
const existing = comments.find(c => c.user.type === "Bot" && c.body.includes(signature));
const body = "❌ **Auto-merge skipped:** The associated CI run ([#" + process.env.RUN_NUMBER + "](" + process.env.RUN_URL + ")) did not complete successfully. Please resolve all CI issues to trigger auto-merge.";
if (existing) {
console.log("Updating existing auto-merge failure comment...");
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body
});
} else {
console.log("Posting new auto-merge failure comment...");
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
}
- name: 🚫 Skip if no PR found
if: steps.find-pr.outputs.found != 'true'
run: |
echo "No PR associated with this workflow run. Skipping auto-merge."
exit 0
- name: 👤 Check if author is allowed
id: check-author
if: steps.find-pr.outputs.found == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
AUTHOR="${{ steps.find-pr.outputs.pr_author }}"
ALLOWED_AUTHORS="dependabot[bot] app/dependabot renovate[bot] app/renovate github-actions[bot] github-actions FaserF"
echo "Checking if '$AUTHOR' is in allowed list..."
if echo "$ALLOWED_AUTHORS" | grep -Fqw "$AUTHOR"; then
echo "✅ Author '$AUTHOR' is allowed for auto-merge"
echo "allowed=true" >> "$GITHUB_OUTPUT"
else
echo "⏭️ Author '$AUTHOR' is not in auto-merge list. Skipping."
echo "allowed=false" >> "$GITHUB_OUTPUT"
bash .github/scripts/minimize-comments.sh "${{ steps.find-pr.outputs.pr_number }}" "Auto-merge" || true
gh issue comment "${{ steps.find-pr.outputs.pr_number }}" --body "⏭️ **Auto-merge skipped:** Author '$AUTHOR' is not in the allowed list for automated merging."
fi
- name: 🔬 Check if CI jobs actually ran and succeeded
id: check-ci-ran
if: steps.find-pr.outputs.found == 'true' && steps.check-author.outputs.allowed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
run: |
echo "Checking if actual CI jobs ran in workflow run $RUN_ID..."
# Get all jobs from the workflow run
JOBS=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN_ID/jobs" --paginate --jq '.jobs[] | {name: .name, conclusion: .conclusion}' | jq -c .)
# Count jobs by status
SUCCESS_COUNT=0
FAILED_COUNT=0
SKIPPED_COUNT=0
TOTAL_JOBS=0
# Jobs to exclude (notification/summary only, not verification)
EXCLUDE_PATTERNS="CI Start|CI Success|Report|Notification|Summary|Generate Config|Detect Changes"
echo "Analyzing jobs..."
while IFS= read -r job; do
name=$(echo "$job" | jq -r '.name')
conclusion=$(echo "$job" | jq -r '.conclusion')
# Skip excluded jobs (notification/summary only)
if echo "$name" | grep -qiE "$EXCLUDE_PATTERNS"; then
echo "⏭️ Excluded (notification/summary): $name"
continue
fi
# Check for actual CI/verification jobs (case-insensitive)
# Look for: Lint, Build, Compliance, Test, Validate, Check, Scan, Verify
name_upper=$(echo "$name" | tr '[:lower:]' '[:upper:]')
if echo "$name_upper" | grep -qE "(LINT|BUILD|COMPLIANCE|TEST|VALIDATE|CHECK|SCAN|VERIFY|TRIVY|HADOLINT|SHELLCHECK)"; then
((TOTAL_JOBS++)) || true
case "$conclusion" in
"skipped")
((SKIPPED_COUNT++)) || true
echo "⏭️ Skipped: $name"
;;
"success")
((SUCCESS_COUNT++)) || true
echo "✅ Success: $name"
;;
"failure"|"cancelled"|"timed_out")
((FAILED_COUNT++)) || true
echo "❌ Failed: $name ($conclusion)"
;;
*)
echo "❓ Unknown: $name ($conclusion)"
;;
esac
else
echo "ℹ️ Not a verification job: $name"
fi
done <<< "$JOBS"
echo ""
echo "Summary: total=$TOTAL_JOBS, success=$SUCCESS_COUNT, failed=$FAILED_COUNT, skipped=$SKIPPED_COUNT"
# Determine if auto-merge is allowed
if [ "$FAILED_COUNT" -gt 0 ]; then
echo "❌ CI jobs failed - auto-merge blocked"
{
echo "ci_ran=false"
echo "reason=failed"
} >> "$GITHUB_OUTPUT"
# Post comment and fail
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "❌ **Auto-merge aborted:** CI jobs failed."
exit 1
elif [ "$TOTAL_JOBS" -eq 0 ]; then
echo "⚠️ No verification jobs detected (docs-only or workflow-only changes)"
{
echo "ci_ran=false"
echo "reason=no_jobs"
} >> "$GITHUB_OUTPUT"
# Post info comment and exit success (don't fail workflow for docs)
gh issue comment "$PR_NUMBER" --body "ℹ️ **Auto-merge skipped:** No verification jobs required (Docs/Workflow only)."
exit 0
elif [ "$SUCCESS_COUNT" -eq 0 ]; then
echo "⚠️ All verification jobs were skipped - auto-merge blocked"
{
echo "ci_ran=false"
echo "reason=all_skipped"
} >> "$GITHUB_OUTPUT"
# Post comment and fail
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "❌ **Auto-merge aborted:** All verification jobs were skipped."
exit 1
else
echo "✅ All CI verification jobs completed successfully"
{
echo "ci_ran=true"
echo "reason=success"
} >> "$GITHUB_OUTPUT"
fi
- name: 🔍 Verify PR is still up-to-date and CI is passing
id: verify-pr-status
if: steps.find-pr.outputs.found == 'true' && steps.check-author.outputs.allowed == 'true' && steps.check-ci-ran.outputs.ci_ran == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
ORIGINAL_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
echo "Verifying PR #$PR_NUMBER is still up-to-date and all CI checks are passing..."
# Get current HEAD SHA of the PR
CURRENT_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
echo "Original SHA: $ORIGINAL_SHA"
echo "Current SHA: $CURRENT_SHA"
# Check if PR HEAD has changed (new commits pushed)
if [ "$CURRENT_SHA" != "$ORIGINAL_SHA" ]; then
echo "❌ PR HEAD has changed! New commits detected."
echo "Original SHA: $ORIGINAL_SHA"
echo "Current SHA: $CURRENT_SHA"
echo "Auto-merge blocked - new CI runs must complete first."
{
echo "still_valid=false"
echo "reason=new_commits"
} >> "$GITHUB_OUTPUT"
# Inform user
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "ℹ️ **Auto-merge skipped:** New commits were detected on the PR."
exit 0 # Exit 0 here might be okay as new CI will trigger another run, but let's be consistent. Keeping 0 is safer to avoid noise.
fi
echo "✅ PR HEAD is still the same"
# Check for running or failed CI workflows for this SHA
echo "Checking for running or failed CI workflows..."
# Get all workflow runs for this SHA (only Orchestrator CI)
WORKFLOW_RUNS_JSON=$(gh api "repos/${{ github.repository }}/actions/runs" \
--jq ".workflow_runs[] | select(.head_sha == \"$CURRENT_SHA\" and .name == \"Orchestrator CI\")" \
--paginate)
# Count running workflows
RUNNING_COUNT=$(echo "$WORKFLOW_RUNS_JSON" | jq -r 'select(.status == "in_progress" or .status == "queued") | .id' | grep -c . || echo "0")
# Count failed workflows
FAILED_COUNT=$(echo "$WORKFLOW_RUNS_JSON" | jq -r 'select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out") | .id' | grep -c . || echo "0")
# Count successful workflows (for logging)
SUCCESS_COUNT=$(echo "$WORKFLOW_RUNS_JSON" | jq -r 'select(.conclusion == "success") | .id' | grep -c . || echo "0")
# Log details of each workflow run
echo "$WORKFLOW_RUNS_JSON" | jq -r '. | "Found workflow run: \(.name) (id: \(.id), status: \(.status), conclusion: \(.conclusion // "none"))"' || true
echo ""
echo "CI Status Summary:"
echo " Running: $RUNNING_COUNT"
echo " Failed: $FAILED_COUNT"
echo " Success: $SUCCESS_COUNT"
# Block auto-merge if there are running or failed workflows
if [ "$RUNNING_COUNT" -gt 0 ]; then
echo "❌ CI workflows are still running - auto-merge blocked"
{
echo "still_valid=false"
echo "reason=ci_running"
} >> "$GITHUB_OUTPUT"
# Post comment and fail workflow for visibility
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "❌ **Auto-merge aborted:** CI workflows are still running."
exit 1
fi
if [ "$FAILED_COUNT" -gt 0 ]; then
echo "❌ CI workflows have failed - auto-merge blocked"
{
echo "still_valid=false"
echo "reason=ci_failed"
} >> "$GITHUB_OUTPUT"
# Post comment and fail workflow
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "❌ **Auto-merge aborted:** CI validation failed."
exit 1
fi
echo "✅ PR is still up-to-date and all CI checks are passing"
{
echo "still_valid=true"
echo "reason=all_checks_passed"
} >> "$GITHUB_OUTPUT"
- name: 🔍 Check for no-merge label
id: check-label
if: >-
steps.find-pr.outputs.found == 'true' &&
steps.check-author.outputs.allowed == 'true' &&
steps.check-ci-ran.outputs.ci_ran == 'true' &&
steps.verify-pr-status.outputs.still_valid == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
run: |
echo "Fetching labels for PR #$PR_NUMBER..."
# Default to skip=true, only set to false after successful check
echo "skip=true" >> "$GITHUB_OUTPUT"
if ! labels=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name'); then
echo "Error: Failed to fetch labels for PR #$PR_NUMBER"
exit 1
fi
if echo "$labels" | grep -q "no-merge"; then
echo "🛑 'no-merge' label found. Skipping auto-merge."
echo "skip=true" >> "$GITHUB_OUTPUT"
bash .github/scripts/minimize-comments.sh "$PR_NUMBER" "Auto-merge" || true
gh issue comment "$PR_NUMBER" --body "🛑 **Auto-merge cancelled:** 'no-merge' label detected on the PR."
exit 0
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: 🤖 Enable Native Auto-Merge
if: >-
steps.find-pr.outputs.found == 'true' &&
steps.check-author.outputs.allowed == 'true' &&
steps.check-ci-ran.outputs.ci_ran == 'true' &&
steps.verify-pr-status.outputs.still_valid == 'true' &&
steps.check-label.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
run: |
echo "🚀 Enabling auto-merge for PR #$PR_NUMBER"
# Enable strict auto-merge.
# This delegates the 'when to merge' decision to GitHub's Branch Protection rules.
# The PR will only merge once ALL required status checks (CI) have passed.
# Using [skip-tests] since PR was already fully tested - no need to re-run on master.
PR_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title')
gh pr merge --auto --squash "$PR_NUMBER" \
--subject "${PR_TITLE} (#${PR_NUMBER}) [skip-tests]" \
--body "Auto-merged after successful CI verification.
[skip-tests] - This PR passed all CI checks before merge."
- name: 📢 Post Merge Success Comment
if: >-
steps.find-pr.outputs.found == 'true' &&
steps.check-author.outputs.allowed == 'true' &&
steps.check-ci-ran.outputs.ci_ran == 'true' &&
steps.verify-pr-status.outputs.still_valid == 'true' &&
steps.check-label.outputs.skip != 'true'
uses: actions/github-script@v8
with:
script: |
const prNumber = parseInt('${{ steps.find-pr.outputs.pr_number }}');
if (isNaN(prNumber)) return;
try {
// Minimize previous comments via script is hard in JS action context without exec
// But we can rely on the fact that if we got here, previous steps didn't fail/exit
// Actually, we should minimize here too to clean up previous "skipped" messages
const exec = require('@actions/exec');
await exec.exec('bash', ['.github/scripts/minimize-comments.sh', String(prNumber), 'Auto-merge']);
} catch (error) {
console.log('Failed to minimize comments: ' + error.message);
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: "🚀 **Auto-merge enabled.**\n\nThe PR will be merged automatically once all branch protection rules are satisfied."
});