Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/DEFAULT_GITHUB_AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ This repository uses a default GitHub automation baseline for quality, safety, a
- Workflow: `.github/workflows/ruleset-json-validation.yml`
- Verifies `.github/rulesets/*.json` structure and required status-check context.

8. **Auto-merge label automation**
- Workflow: `.github/workflows/auto-merge.yml`
- Labels `auto-merge` or `autofix` on a PR arm GitHub native auto-merge (squash).
- When `Merge Gate / All Gates Passed` check succeeds, the `merge-on-gate-pass` job
validates eligibility and calls the merge API directly.
- Bot-authored PRs can be automatically approved when `AUTO_MERGE_BOT_APPROVE=true`
(repository variable) and `AUTO_MERGE_APPROVE_TOKEN` (PAT secret) are configured.
- Required labels (create manually or via the CLI):
```bash
gh label create auto-merge --color 0075ca --description "Squash-merge when all CI gates pass"
gh label create autofix --color e4e669 --description "Auto-merge for automated fix PRs"
```
- Human-authored PRs always require at least one human approval regardless of labels.

9. **Dependabot auto-merge**
- Workflow: `.github/workflows/dependabot-automerge.yml`
- Auto-approves and enables squash-merge for Dependabot patch and minor-dev bumps.
- Major version bumps always require manual review.

## One-time GitHub settings (manual)

In repository settings, enable branch protection (or rulesets) on `main` with these minimum requirements:
Expand Down
118 changes: 118 additions & 0 deletions .github/actions/check-auto-merge-eligibility/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Check Auto-Merge Eligibility

Check warning on line 1 in .github/actions/check-auto-merge-eligibility/action.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

1:1 [document-start] missing document start "---"
description: >
Validates all eligibility criteria for automatic PR merging and outputs
whether the PR is eligible together with a human-readable reason.
Checks: not draft, targets main, not a fork, has auto-merge/autofix label,
not blocked/conflicted, no CHANGES_REQUESTED reviews, has an APPROVED review.
Comment on lines +1 to +6

inputs:
pr-number:
description: Pull request number to evaluate
required: true
github-token:
description: >
GitHub token with pull-requests:read and checks:read scope.
Defaults to the built-in GITHUB_TOKEN.
required: false
default: ${{ github.token }}

outputs:
eligible:
description: "'true' if the PR meets all auto-merge criteria, 'false' otherwise"
value: ${{ steps.check.outputs.eligible }}
reason:
description: >
Human-readable explanation of the eligibility decision.
When eligible=true this is a success message; otherwise it lists failures.
value: ${{ steps.check.outputs.reason }}

runs:
using: composite
steps:
- name: Evaluate PR eligibility
id: check
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
PR_NUMBER: ${{ inputs.pr-number }}
run: |
set -euo pipefail

# ---------------------------------------------------------------------------
# Fetch PR metadata via the GitHub CLI.
# ---------------------------------------------------------------------------
pr_json="$(gh pr view "$PR_NUMBER" \
--json number,isDraft,baseRefName,headRepository,labels,mergeStateStatus,reviewDecision,state \
2>&1)" || {
# Sanitize raw gh output: strip ANSI escape codes, keep first line,
# truncate using bash substring (character-aware, avoids splitting
# multi-byte UTF-8 sequences that cut -c can mishandle on some systems).
first_line="$(printf '%s' "${pr_json}" | head -n1 | sed 's/\x1b\[[0-9;]*m//g')"
sanitized="${first_line:0:120}"
echo "eligible=false" >> "$GITHUB_OUTPUT"
printf 'reason=Failed to fetch PR data: %s\n' "${sanitized}" >> "$GITHUB_OUTPUT"
exit 0
}

# ---------------------------------------------------------------------------
# Extract fields via jq.
# ---------------------------------------------------------------------------
is_draft="$(jq -r '.isDraft' <<< "$pr_json")"
base_ref="$(jq -r '.baseRefName' <<< "$pr_json")"
head_repo="$(jq -r '.headRepository.nameWithOwner // empty' <<< "$pr_json")"
pr_state="$(jq -r '.state' <<< "$pr_json")"
merge_state="$(jq -r '.mergeStateStatus // empty' <<< "$pr_json")"
review_decision="$(jq -r '.reviewDecision // empty' <<< "$pr_json")"
has_label="$(jq -r '
[.labels[].name] | any(. == "auto-merge" or . == "autofix")
' <<< "$pr_json")"

current_repo="${GITHUB_REPOSITORY:-}"

# ---------------------------------------------------------------------------
# Accumulate failures.
# ---------------------------------------------------------------------------
FAILURES=()

[[ "$pr_state" != "OPEN" ]] && \
FAILURES+=("PR state is '${pr_state}' (requires OPEN)")

[[ "$is_draft" == "true" ]] && \
FAILURES+=("PR is a draft")

[[ "$base_ref" != "main" ]] && \
FAILURES+=("base branch is '${base_ref}' (requires 'main')")

if [[ -n "$current_repo" && -n "$head_repo" && "$head_repo" != "$current_repo" ]]; then
FAILURES+=("PR is from a fork (${head_repo}); fork PRs are not eligible")
fi

[[ "$has_label" != "true" ]] && \
FAILURES+=("missing 'auto-merge' or 'autofix' label")

case "$merge_state" in
DIRTY) FAILURES+=("merge conflicts detected (mergeStateStatus=DIRTY)") ;;
BEHIND) FAILURES+=("branch is behind base branch (mergeStateStatus=BEHIND)") ;;
BLOCKED) FAILURES+=("merge is blocked by required status checks (mergeStateStatus=BLOCKED)") ;;
esac

if [[ "$review_decision" == "CHANGES_REQUESTED" ]]; then
FAILURES+=("has CHANGES_REQUESTED review")
elif [[ "$review_decision" != "APPROVED" ]]; then
FAILURES+=("awaiting required approval (reviewDecision='${review_decision}')")
fi

# ---------------------------------------------------------------------------
# Emit outputs.
# ---------------------------------------------------------------------------
if (( ${#FAILURES[@]} == 0 )); then
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "reason=All eligibility checks passed." >> "$GITHUB_OUTPUT"
echo "✅ PR #${PR_NUMBER} is eligible for auto-merge."
else
echo "eligible=false" >> "$GITHUB_OUTPUT"
joined="$(printf '%s; ' "${FAILURES[@]}")"
joined="${joined%; }" # trim trailing '; '
printf 'reason=%s\n' "${joined}" >> "$GITHUB_OUTPUT"
echo "❌ PR #${PR_NUMBER} is NOT eligible: ${joined}"
fi
16 changes: 16 additions & 0 deletions .github/rulesets/main-default-automation.ruleset.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
}
]
}
},
{
// Merge queue: prevents the thundering-herd problem when many
// auto-merge labeled PRs all become mergeable simultaneously.
// Requires enabling the merge queue in repository Settings → General
// → Pull Requests → "Allow merge queue" (UI-only setting).
Comment on lines +37 to +40
"type": "merge_queue",
"parameters": {
"check_response_timeout_minutes": 60,
"grouping_strategy": "ALLGREEN",
"max_entries_to_build": 5,
"max_entries_to_merge": 5,
"merge_method": "SQUASH",
"min_entries_to_merge": 1,
"min_entries_to_merge_wait_minutes": 5
}
}

// Optional hardening rules (enable when policy-ready):
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ This directory contains all GitHub Actions workflows for the **Aria** repository
- **Support-only lanes (not required for merge):** `ci.yml`, `pr-tests.yml`, and `ci-pipeline.yml`
- **Workflow hygiene checks remain active:** `workflow-validation.yml` and `actionlint.yml` for workflow/config changes

### Auto-Merge Policy

Auto-merge is controlled by `.github/workflows/auto-merge.yml` (consolidated from the old `auto-merge.yml` + `auto-merge-on-ci.yml`).

| Job | Trigger | Behaviour |
|-----|---------|-----------|
| `enable` | `pull_request` labeled `auto-merge` or `autofix` | Arms GitHub native auto-merge (squash) and posts an informational comment |
| `disable` | `pull_request` last matching label removed | Disables native auto-merge |
| `merge-on-gate-pass` | `check_run` completed for `All Gates Passed` | Evaluates all associated PRs: checks label, draft, fork, conflicts, and review state; squash-merges eligible PRs and posts results |
| `bot-approve` | `pull_request` with auto-merge label (gated by `AUTO_MERGE_BOT_APPROVE == 'true'`) | Approves PRs authored by bot actors listed in `BOT_APPROVE_ALLOWLIST` using `AUTO_MERGE_APPROVE_TOKEN` |

**Eligibility requirements** (enforced by `merge-on-gate-pass` and codified in `.github/actions/check-auto-merge-eligibility/action.yml`):
- PR is not a draft
- PR targets `main`
- PR is from the same repo (forks are always skipped)
- PR has `auto-merge` or `autofix` label
- Merge state is not `DIRTY` / `BEHIND` / `BLOCKED`
- No `CHANGES_REQUESTED` reviews
- At least one `APPROVED` review

**Bot-approve actor allowlist** (`BOT_APPROVE_ALLOWLIST` env var in `auto-merge.yml`):
- `github-actions[bot]`
- `copilot-swe-agent[bot]`

Human-authored PRs **always** require a real human approval regardless of the `AUTO_MERGE_BOT_APPROVE` setting.

**Deprecated:** `auto-merge-on-ci.yml` is now a manual-only stub. All auto-merge logic is in `auto-merge.yml`.

---

## Required Secrets & Variables
Expand All @@ -161,6 +189,8 @@ Configure these under **Settings → Secrets and variables → Actions**.
| `OPENAI_API_KEY` | Secret | `ci-pipeline.yml`, `pr-test-summary-comment.yml` (optional) | OpenAI API access for provider integration tests and AI-generated PR workflow summaries (falls back to deterministic summary when unset) |
| `AZURE_OPENAI_ENDPOINT` | Secret | `ci-pipeline.yml` (optional) | Azure OpenAI endpoint URL |
| `AZURE_OPENAI_API_KEY` | Secret | `ci-pipeline.yml` (optional) | Azure OpenAI API key |
| `AUTO_MERGE_APPROVE_TOKEN` | Secret | `auto-merge.yml` (bot-approve job) | PAT with `pull-requests:write` scope issued by a separate identity; required for automated approval of bot-authored PRs. Never use `GITHUB_TOKEN` — it cannot approve its own PRs. |
| `AUTO_MERGE_BOT_APPROVE` | Variable | `auto-merge.yml` (bot-approve job) | Set to `'true'` to enable automated approval of PRs authored by accounts in `BOT_APPROVE_ALLOWLIST`. Defaults to off. Human-authored PRs always require a human approval. |

> 🔐 Never log secret values. Always pass secrets via `env:` blocks scoped to the smallest possible step.

Expand Down
107 changes: 37 additions & 70 deletions .github/workflows/auto-merge-on-ci.yml
Original file line number Diff line number Diff line change
@@ -1,78 +1,45 @@
name: Auto-merge on CI success
# =============================================================================
# NOTICE: This workflow has been consolidated into auto-merge.yml
# =============================================================================
# The CI-triggered merge path previously in this file (watching the "AGI smoke"
# workflow_run event) has been replaced by the `merge-on-gate-pass` job in
# `.github/workflows/auto-merge.yml`, which watches the canonical
# `Merge Gate / All Gates Passed` check_run instead.
#
# Key improvements in the new path:
# • Triggers on `check_run` (reliable PR association) instead of
# `workflow_run` (pull_requests field is often empty for human PRs).
# • Watches the real merge gate, not a single smoke workflow.
# • Full eligibility checks: label, draft, fork, conflicts, review state.
# • Writes step summary and posts PR comments on result.
#
# This file is intentionally left as a manual-only no-op so it does not
# consume CI minutes. Delete or repurpose it as needed.
# =============================================================================

name: Auto-merge on CI success (deprecated — see auto-merge.yml)

Check warning on line 20 in .github/workflows/auto-merge-on-ci.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

20:1 [document-start] missing document start "---"

on:

Check warning on line 22 in .github/workflows/auto-merge-on-ci.yml

View workflow job for this annotation

GitHub Actions / Validate Copilot setup files & linters

22:1 [truthy] truthy value should be one of [false, true]
workflow_run:
workflows: ["AGI smoke"]
types: [completed]
workflow_dispatch:
inputs:
notice:
description: >
This workflow is a stub. Auto-merge logic has moved to auto-merge.yml
(merge-on-gate-pass job). Trigger that workflow instead.
required: false
default: 'See auto-merge.yml'

permissions:
contents: write
pull-requests: write
issues: write

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
HARDEN_RUNNER_SHA: 0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.2
GITHUB_SCRIPT_SHA: 60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
contents: read

jobs:
auto-merge:
name: Auto-merge PRs labeled 'auto-merge'
notice:
name: Deprecated — logic moved to auto-merge.yml
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Harden runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0
with:
egress-policy: audit
disable-sudo: true

- name: Auto-merge PRs labeled 'auto-merge'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const run = context.payload.workflow_run;
if (!run) return;
if (run.conclusion !== 'success') {
core.info(`Workflow run conclusion: ${run.conclusion}; skipping`);
return;
}
const prs = run.pull_requests || [];
if (!prs.length) {
core.info('No pull requests associated; skipping.');
return;
}
for (const pr of prs) {
const prNumber = pr.number;
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const labelNames = labels.map(l => l.name);
if (!labelNames.includes('auto-merge')) {
core.info(`PR #${prNumber} not labeled auto-merge; skipping`);
continue;
}
const { data: prData } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
// If mergeable is 'unknown', the API may still be calculating; guard defensively
if (prData.mergeable === false) {
core.info(`PR #${prNumber} not mergeable; skipping`);
continue;
}
try {
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
merge_method: 'squash'
});
core.info(`Merged PR #${prNumber}`);
} catch (err) {
core.info(`Failed to merge PR #${prNumber}: ${err}`);
}
}
- name: Print notice
run: |
echo "This workflow is a stub."
echo "Auto-merge on CI success logic is now in .github/workflows/auto-merge.yml"
echo "(merge-on-gate-pass job, triggered by check_run: All Gates Passed)."
Loading
Loading