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
119 changes: 119 additions & 0 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Enables GitHub native auto-merge for product-autonomous PRs, including PRs
# opened from forks. This workflow runs in the trusted base-repository context:
# it checks out only the base SHA and treats PR file metadata as data.

name: Auto Merge

on:
pull_request_target:
types: [opened, synchronize, reopened, edited, ready_for_review, converted_to_draft, labeled, unlabeled]

concurrency:
group: auto-merge-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write

jobs:
enable:
if: github.event.pull_request.base.ref == 'master'
runs-on: ubuntu-latest
steps:
- name: Check pause state
id: state
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
DRAFT: ${{ github.event.pull_request.draft }}
HOLD: ${{ contains(github.event.pull_request.labels.*.name, 'do-not-merge/hold') }}
run: |
set -euo pipefail

if [ "$DRAFT" = "true" ] || [ "$HOLD" = "true" ]; then
echo "paused=true" >> "$GITHUB_OUTPUT"
auto_merge="$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')"
if [ "$auto_merge" = "true" ]; then
gh pr merge --disable-auto "$PR" --repo "$GITHUB_REPOSITORY"
fi
echo "Auto-merge paused: draft=$DRAFT hold=$HOLD"
exit 0
fi

echo "paused=false" >> "$GITHUB_OUTPUT"

- name: Checkout trusted base
if: steps.state.outputs.paused != 'true'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 1

- name: Set up go
if: steps.state.outputs.paused != 'true'
uses: actions/setup-go@v5
with:
go-version: '1.25'

- name: Judge product autonomy
if: steps.state.outputs.paused != 'true'
id: judge
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
OWNER_GATE_AUTHOR: ${{ github.event.pull_request.user.login }}
OWNER_GATE_BASE_SHA: ${{ github.event.pull_request.base.sha }}
OWNER_GATE_PLATFORM_CLEARED: "false"
GOPROXY: 'https://goproxy.cn,direct'
run: |
set -euo pipefail

diff_file="$RUNNER_TEMP/pr-files.name-status"
gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate \
--jq '.[] | if .status == "renamed" then "R100\t\(.previous_filename)\t\(.filename)" elif .status == "removed" then "D\t\(.filename)" elif .status == "added" then "A\t\(.filename)" else "M\t\(.filename)" end' \
> "$diff_file"

decision="$(go run ./hack/owner-gate < "$diff_file")"
echo "$decision"

auto_merge="$(jq -r '.autoMergeEligible' <<<"$decision")"
reason="$(jq -r '.reason' <<<"$decision")"
{
echo "eligible=$auto_merge"
echo "reason<<OWNER_GATE_EOF"
echo "$reason"
echo "OWNER_GATE_EOF"
} >> "$GITHUB_OUTPUT"

- name: Disable auto-merge for non-autonomous PRs
if: steps.state.outputs.paused != 'true' && steps.judge.outputs.eligible != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
REASON: ${{ steps.judge.outputs.reason }}
run: |
set -euo pipefail

auto_merge="$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')"
if [ "$auto_merge" = "true" ]; then
gh pr merge --disable-auto "$PR" --repo "$GITHUB_REPOSITORY"
fi
echo "Auto-merge not enabled: $REASON"

- name: Enable native auto-merge
if: steps.state.outputs.paused != 'true' && steps.judge.outputs.eligible == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail

auto_merge="$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')"
if [ "$auto_merge" = "true" ]; then
echo "Auto-merge is already enabled for PR #$PR."
exit 0
fi

gh pr merge --auto --squash "$PR" --repo "$GITHUB_REPOSITORY" --match-head-commit "$HEAD_SHA"
43 changes: 6 additions & 37 deletions .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@
#
# Repo settings that this gate RELIES ON:
# - Settings → General → "Allow auto-merge" ✓ + "Allow squash merging" ✓.
# - Repository secret AUTO_MERGE_TOKEN is a bot/GitHub App token with
# contents:write + pull-requests:write. Do NOT use GITHUB_TOKEN for
# auto-merge: GitHub suppresses workflow runs triggered by GITHUB_TOKEN
# events, which can hide post-merge CI/CD automation. Release is scheduled
# weekly and no longer depends on master push events.
# The separate Auto Merge workflow enables GitHub native auto-merge for
# product-autonomous PRs after re-running the owner-gate decision in the
# trusted base-repository context.
# - Required approvals = 0. Do NOT set a global "require N approvals" — that
# would also block product autonomy. Platform approval is enforced by
# owner-gate (admin review), not by branch protection's approval count.
Expand All @@ -47,8 +45,9 @@ name: PR Gate
on:
pull_request:
# labeled/unlabeled REQUIRED so hold-gate re-runs when do-not-merge/hold is
# added/removed after checks went green.
types: [opened, synchronize, reopened, labeled, unlabeled]
# added/removed after checks went green. edited REQUIRED so title fixes
# re-run scope-gate.
types: [opened, synchronize, reopened, edited, ready_for_review, labeled, unlabeled]
pull_request_review:
# submitted/dismissed REQUIRED so owner-gate re-evaluates platform clearance
# when an admin approves or the approval is dismissed (stale-on-push).
Expand Down Expand Up @@ -192,36 +191,6 @@ jobs:
exit 1
- run: echo "hold-gate - not on hold"

auto-merge:
# Native GitHub auto-merge, only for product-autonomous eligible PRs. GitHub
# enforces required checks + (any) required approvals before the actual merge.
# Use a real automation identity instead of GITHUB_TOKEN so post-merge
# repository workflows/events are not suppressed.
if: github.event_name == 'pull_request' && needs.owner-gate.outputs.auto_merge == 'true'
needs: [owner-gate]
runs-on: ubuntu-latest
# enablePullRequestAutoMerge (GraphQL) requires contents:write in addition to
# pull-requests:write; the workflow default above is contents:read, which made
# this job fail with "Resource not accessible by integration" on the first
# real product-autonomous PRs (P3.6 batch, 2026-07-02).
permissions:
contents: write
pull-requests: write
steps:
- name: Require automation token
env:
AUTO_MERGE_TOKEN: ${{ secrets.AUTO_MERGE_TOKEN }}
run: |
set -euo pipefail
if [ -z "$AUTO_MERGE_TOKEN" ]; then
echo "::error::AUTO_MERGE_TOKEN is required for product auto-merge. Configure a bot/GitHub App token with contents:write and pull-requests:write so post-merge repository workflows/events are not suppressed."
exit 1
fi
- name: Enable auto-merge
env:
GH_TOKEN: ${{ secrets.AUTO_MERGE_TOKEN }}
run: gh pr merge --auto --squash "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY"

check-product:
# Runs on every trigger (incl. merge_group + review events): must produce a
# real conclusion, never an `if`-skip that GitHub would count as success.
Expand Down
Loading