From 7b195093b9a9e361bf6611a054886e61ed3a6054 Mon Sep 17 00:00:00 2001 From: Episkey Date: Fri, 10 Jul 2026 04:19:58 -0700 Subject: [PATCH] ci: support fork PR auto-merge --- .github/workflows/auto-merge.yml | 119 +++++++++++++++++++++++++++++++ .github/workflows/pr-gate.yml | 43 ++--------- 2 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/auto-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000000..f3552986f6 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -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<> "$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" diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index c4c0b22a74..cea2506e3f 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -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. @@ -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). @@ -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.