From 3f286d6639c5d8af10e7b2dc9b63f2036c9efefa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 11:45:23 +0900 Subject: [PATCH 1/4] ci: schedule hourly approved-PR steward --- .github/workflows/hourly-pr-steward.yml | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/hourly-pr-steward.yml diff --git a/.github/workflows/hourly-pr-steward.yml b/.github/workflows/hourly-pr-steward.yml new file mode 100644 index 0000000..21238f6 --- /dev/null +++ b/.github/workflows/hourly-pr-steward.yml @@ -0,0 +1,89 @@ +name: Hourly PR steward + +on: + schedule: + # Avoid the top-of-hour congestion window. Scheduled runs use UTC and the + # latest commit on the default branch. + - cron: "17 * * * *" + workflow_dispatch: + +# The steward can update trusted same-repository branches and merge only after +# GitHub reports an approved review and every required check has passed. +permissions: + contents: write + pull-requests: write + checks: read + +concurrency: + group: hourly-pr-steward + cancel-in-progress: false + +jobs: + advance-approved-pull-requests: + name: Advance approved pull requests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Update, verify, and merge trusted pull requests + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + shell: bash + run: | + set -euo pipefail + + gh pr list \ + --repo "$REPOSITORY" \ + --state open \ + --limit 100 \ + --json number,isDraft,author,headRepositoryOwner,headRefOid,mergeStateStatus,reviewDecision \ + > "$RUNNER_TEMP/open-pull-requests.json" + + jq -c '.[]' "$RUNNER_TEMP/open-pull-requests.json" | while IFS= read -r pull_request; do + number="$(jq -r '.number' <<<"$pull_request")" + is_draft="$(jq -r '.isDraft' <<<"$pull_request")" + author="$(jq -r '.author.login // ""' <<<"$pull_request")" + head_owner="$(jq -r '.headRepositoryOwner.login // ""' <<<"$pull_request")" + head_sha="$(jq -r '.headRefOid' <<<"$pull_request")" + merge_state="$(jq -r '.mergeStateStatus // "UNKNOWN"' <<<"$pull_request")" + review_decision="$(jq -r '.reviewDecision // ""' <<<"$pull_request")" + + if [[ "$is_draft" != "false" || "$head_owner" != "ContextualWisdomLab" ]]; then + continue + fi + + case "$author" in + seonghobae|dependabot|dependabot[bot]|github-actions|github-actions[bot]|opencode-agent) + ;; + *) + continue + ;; + esac + + # Keep trusted branches current. A successful update invalidates the + # old check evidence, so the steward waits for the next hourly pass. + if [[ "$merge_state" == "BEHIND" ]]; then + gh pr update-branch "$number" --repo "$REPOSITORY" || true + continue + fi + + if [[ "$review_decision" != "APPROVED" ]]; then + continue + fi + + # Never infer safety from optional checks. The repository's required + # check set remains the source of truth; pending or failed checks make + # this command non-zero and the PR is left untouched. + if ! gh pr checks "$number" --repo "$REPOSITORY" --required --fail-fast; then + continue + fi + + # Bind the merge to the reviewed and checked head SHA. GitHub still + # enforces rulesets, required checks, and review requirements. + gh api \ + --method PUT \ + "repos/$REPOSITORY/pulls/$number/merge" \ + -f merge_method=squash \ + -f sha="$head_sha" \ + >/dev/null + done From 1e8dfcae88e9e89fc2defbe390b7a43125991bbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 11:45:57 +0900 Subject: [PATCH 2/4] ci: make trusted bot author matching literal --- .github/workflows/hourly-pr-steward.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-pr-steward.yml b/.github/workflows/hourly-pr-steward.yml index 21238f6..3c72a93 100644 --- a/.github/workflows/hourly-pr-steward.yml +++ b/.github/workflows/hourly-pr-steward.yml @@ -53,7 +53,7 @@ jobs: fi case "$author" in - seonghobae|dependabot|dependabot[bot]|github-actions|github-actions[bot]|opencode-agent) + seonghobae|dependabot|app/dependabot|github-actions|app/github-actions|opencode-agent) ;; *) continue From dd614cc29f9fc9aa3ed689a9f8528c7e77bc9ab9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:00:59 +0900 Subject: [PATCH 3/4] ci: arm GitHub auto-merge instead of merging with workflow token --- .github/workflows/hourly-pr-steward.yml | 48 +++++++++++++++---------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/hourly-pr-steward.yml b/.github/workflows/hourly-pr-steward.yml index 3c72a93..e8e897a 100644 --- a/.github/workflows/hourly-pr-steward.yml +++ b/.github/workflows/hourly-pr-steward.yml @@ -7,8 +7,8 @@ on: - cron: "17 * * * *" workflow_dispatch: -# The steward can update trusted same-repository branches and merge only after -# GitHub reports an approved review and every required check has passed. +# The steward can update trusted same-repository branches and arm auto-merge +# only after GitHub reports an approved review and clean required checks. permissions: contents: write pull-requests: write @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Update, verify, and merge trusted pull requests + - name: Update, verify, and arm trusted pull requests env: GH_TOKEN: ${{ github.token }} REPOSITORY: ${{ github.repository }} @@ -52,13 +52,25 @@ jobs: continue fi - case "$author" in - seonghobae|dependabot|app/dependabot|github-actions|app/github-actions|opencode-agent) - ;; - *) - continue - ;; - esac + trusted_author=false + for allowed_author in \ + seonghobae \ + dependabot \ + 'dependabot[bot]' \ + app/dependabot \ + github-actions \ + 'github-actions[bot]' \ + app/github-actions \ + opencode-agent + do + if [[ "$author" == "$allowed_author" ]]; then + trusted_author=true + break + fi + done + if [[ "$trusted_author" != "true" ]]; then + continue + fi # Keep trusted branches current. A successful update invalidates the # old check evidence, so the steward waits for the next hourly pass. @@ -78,12 +90,12 @@ jobs: continue fi - # Bind the merge to the reviewed and checked head SHA. GitHub still - # enforces rulesets, required checks, and review requirements. - gh api \ - --method PUT \ - "repos/$REPOSITORY/pulls/$number/merge" \ - -f merge_method=squash \ - -f sha="$head_sha" \ - >/dev/null + # Arm GitHub's native auto-merge service rather than creating the + # merge commit directly with GITHUB_TOKEN. Rulesets remain final, + # and the exact reviewed/check head must still match. + gh pr merge "$number" \ + --repo "$REPOSITORY" \ + --auto \ + --squash \ + --match-head-commit "$head_sha" done From 4893880cd3154feb2133e251270281d26ad506e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 13:05:09 +0900 Subject: [PATCH 4/4] ci: make hourly required-check gate explicitly fail closed --- .github/workflows/hourly-pr-steward.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hourly-pr-steward.yml b/.github/workflows/hourly-pr-steward.yml index e8e897a..81b4ff6 100644 --- a/.github/workflows/hourly-pr-steward.yml +++ b/.github/workflows/hourly-pr-steward.yml @@ -84,9 +84,10 @@ jobs: fi # Never infer safety from optional checks. The repository's required - # check set remains the source of truth; pending or failed checks make - # this command non-zero and the PR is left untouched. - if ! gh pr checks "$number" --repo "$REPOSITORY" --required --fail-fast; then + # check set remains the source of truth. `gh pr checks` exits nonzero + # for failed checks and uses exit code 8 for pending checks, so either + # condition leaves the PR untouched. + if ! gh pr checks "$number" --repo "$REPOSITORY" --required; then continue fi