AGENT SKILL

auto-merge

Automatically merge open PRs that have passing CI, no blocking reviews, and no conflicts

License 许可
MIT
所属仓库
aeonfun/aeon
最近更新
2026-11-15
Stars
★ 793
Forks
308

01 前言 Front Matter

name
auto-merge
metadata
{ "var": "", "tags": [ "dev", "meta" ], "title": "Auto Merge", "category": "core" }
description
Automatically merge open PRs that have passing CI, no blocking reviews, and no conflicts

03 SKILL.md 正文

<!-- autoresearch: variation C — safety-hardened (author allowlist, size cap, UNKNOWN retry, fork block, dry-run mode) so an autonomous agent with merge credentials cannot accidentally ship a hostile or oversized PR -->

${var} — Repo (owner/repo) to target. If empty, uses every repo in memory/watched-repos.md. Env: AUTO_MERGE_DRY_RUN=1 logs intent without merging. MAX_AUTO_MERGE=N caps merges per run (default 3).

Merge open PRs that are fully green and pass an explicit safety policy. The policy exists because this skill runs autonomously with write access — a bug in the gate is a bug that ships to main.

Read memory/MEMORY.md and memory/watched-repos.md for repos to target. Read the last 2 days of memory/logs/ to avoid re-logging PRs already merged.

Safety policy

A PR merges only when every one of the following holds:

  • Author allowlist: author.login is one of dependabot[bot], renovate[bot], github-actions[bot], OR appears under a ## Trusted Authors section in memory/watched-repos.md. No allowlist → only the three bot logins are eligible.
  • Size cap: additions + deletions ≤ 500. Override by applying the label auto-merge-large on the PR.
  • Base branch: baseRefName is main or master. Refuse any other target.
  • Not a fork: isCrossRepository == false (fork CI can be tampered with).
  • Not draft: isDraft == false.
  • Not already queued: autoMergeRequest == null (avoid fighting GitHub's native auto-merge if a human enabled it) and the PR is not already in a merge queue (mergeQueueEntry == null, checked in step 2b).
  • No opt-out label: none of {do-not-merge, wip, hold, needs-review, blocked} present.
  • Mergeable state: mergeStateStatus == "CLEAN" (this is stricter than mergeable == "MERGEABLE" — CLEAN additionally requires branch-protection gates to be satisfied).
  • Reviews: reviewDecision != "CHANGES_REQUESTED".
  • Checks: every entry in statusCheckRollup has conclusion in {SUCCESS, NEUTRAL, SKIPPED}. Any FAILURE, TIMED_OUT, CANCELLED, PENDING, or null conclusion disqualifies the PR.
  • Retry cap: this PR has been attempted fewer than 3 times. A PR that has hit MERGE_FAIL three times across runs is paused — repeated failure on a CLEAN-looking PR usually means something subtle (a required check that didn't surface, branch-protection drift, token scope drift). Surface it and stop looping.

Steps

  1. Bootstrap state — per-PR retry counter lives in memory/topics/auto-merge-state.json:

    mkdir -p memory/topics
    

    Then check for memory/topics/auto-merge-state.json with Glob; if it is missing, create it with the Write tool holding {"prs":{},"last_run":null} (a [ -f ] || test and an echo ... > file redirect are both refused in this run). Schema:

    {
      "last_run": "2026-05-23T08:00:00Z",
      "prs": {
        "owner/repo#123": {
          "first_seen": "2026-05-21T10:00:00Z",
          "last_attempt": "2026-05-23T08:00:00Z",
          "attempts": 2,
          "last_outcome": "merge_failed",
          "last_error": "Pull Request is in unstable state"
        }
      }
    }
    

    PR keys are <owner>/<repo>#<number> so state survives multi-repo runs. Cap to 50 most-recent entries (LRU by last_attempt). Validate with jq empty after write; restore from .bak on failure.

  2. List open PRs for each watched repo with the full field set:

    gh pr list -R owner/repo --state open --json number,title,author,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,autoMergeRequest,isCrossRepository,labels,additions,deletions,baseRefName
    
  3. Handle UNKNOWN state — GitHub computes mergeStateStatus lazily. If a PR returns UNKNOWN, sleep 3 seconds and re-query once:

    sleep 3 && gh pr view NUMBER -R owner/repo --json mergeStateStatus,mergeable,statusCheckRollup
    

    If still UNKNOWN after the retry, skip the PR with reason UNKNOWN-persistent and let the next run retry.

    2b. Skip PRs already in a merge queue. A PR this skill queued on an earlier run is still open, so it would be merged again, bump attempts, and hit the retry cap while it waits. gh pr list does not expose the queue, so ask GraphQL per PR:

    gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){pullRequest(number:$n){mergeQueueEntry{state}}}}' \
      -f o=owner -f r=repo -F n=NUMBER --jq '.data.repository.pullRequest.mergeQueueEntry.state // ""'
    

    A non-empty state (e.g. QUEUED, AWAITING_CHECKS, MERGEABLE) skips the PR as SKIP:already-queued:<state>. A failed lookup skips it as SKIP:merge-queue-unknown. Neither counts as an attempt: do not touch its attempts, and leave its state entry as is.

  4. Apply the safety policy to each PR. Record a verdict for every PR: either MERGE or SKIP:<specific-reason>. Reasons must name the failing gate — e.g. SKIP:author-not-allowlisted:contributor123, SKIP:size-cap:823-lines, SKIP:mergeStateStatus=BEHIND, SKIP:label:do-not-merge, SKIP:check-failed:lint, SKIP:retry-cap:3-attempts. Vague reasons like SKIP:not-ready are not acceptable.

  5. Merge qualifying PRs, up to MAX_AUTO_MERGE (default 3):

    • If AUTO_MERGE_DRY_RUN=1, log DRY_RUN:would-merge #N and continue — do NOT invoke merge.
    • Otherwise:
      gh pr merge NUMBER -R owner/repo --squash --delete-branch
      
      A zero exit is not proof of a merge: on a repo with a merge queue, gh pr merge exits 0 after only adding the PR to the queue. Confirm with GitHub before counting or reporting it:
      gh pr view NUMBER -R owner/repo --json state,mergeCommit --jq '[.state, (.mergeCommit.oid // "")] | @tsv'
      
      Only MERGED with a commit SHA is a merge: report that SHA, never one inferred from the command output. OPEN means it was queued: log QUEUED #N, record last_outcome: queued, and report it as queued, not merged. Any other result, or a failed lookup, is logged as MERGE_UNCONFIRMED #N and reported as such. Both count toward MAX_AUTO_MERGE: a merge was attempted and GitHub accepted the command, so the cap still bounds how much this run can send to main. Increment state.prs["<owner>/<repo>#<N>"].attempts on every attempt regardless of outcome. Set first_seen if absent. Reset to 0 (delete the entry) for PRs that no longer appear in the open list (already merged or closed since the last run). If the merge fails (non-zero exit), capture stderr and log MERGE_FAIL #N: <stderr>. Record last_outcome: merge_failed and last_error: <stderr ≤200 chars> on the state entry. A failed merge does NOT count toward the per-run MAX_AUTO_MERGE cap — continue to the next qualifying PR. A PR whose attempts has reached 3 is filtered out in step 3 with SKIP:retry-cap:3-attempts; surface it in step 5b instead of retrying.
  6. Send a notification only when at least one real (non-dry-run) merge succeeded or at least one PR has hit the retry cap (5b below). No merges and no cap hits → no notification, just a log entry.

    5a. At least one merge succeeded:

    *Auto Merge - ${today}*
    Merged N PR(s) on owner/repo:
    - #123: PR title (+45/-12, by @author) - squash merged abc1234   (the mergeCommit SHA GitHub returned)
    Queue cleared. Self-improve cycle unblocked.
    

    5b. Retry cap reached on ≥1 PR (AUTO_MERGE_RETRY_CAP) — include in the same message if both fire, otherwise stand-alone:

    *Auto Merge - retry cap*
    Hit retry cap (3 attempts) on:
    - owner/repo#40 - last error: "Pull Request is in unstable state"
    Stopping auto-merge attempts on this PR. Investigate manually.
    

    Dedup: suppress re-notify if the exact same set of cap-hit PR keys already notified within the last 24h (grep memory/logs/ for prior AUTO_MERGE_RETRY_CAP entries).

  7. Persist state — write the updated memory/topics/auto-merge-state.json. Update last_run to current timestamp. Validate with jq empty; on failure restore from a .bak written before this run.

  8. Log to memory/logs/${today}.md under an ### auto-merge heading:

    • Mode: live | dry-run
    • Repo(s): list
    • Merged: #N title @author +A-D SHA per line, confirmed MERGED by GitHub
    • Queued: #N per line (merge queue accepted it; not merged yet)
    • Skipped: #N SKIP:<reason> per line
    • Retry-capped: owner/repo#N — <last_error> per line (empty if none)
    • Totals: merged=X queued=Q qualified=Y considered=Z retry_capped=R
    • If zero qualified, include a verdict breakdown: AUTO_MERGE_SKIP: 0/Z qualifying (behind=B blocked=L failing=F draft=D already-queued=U author-blocked=A size-blocked=S retry-capped=R)

Network note

gh authenticates via the workflow's GITHUB_TOKEN — no curl needed. If gh pr merge fails with Resource not accessible by integration, the workflow token lacks merge permission on that repo; log once and notify at most once per 7 days (check memory/logs/ for prior notification) to avoid alert spam.

Constraints

  • Never merge a PR whose author is not allowlisted, even if every other gate is green.
  • Never bypass the size cap without the explicit auto-merge-large label (set by a human, not a bot).
  • Never auto-retry a MERGE_FAIL within the same run — if the first merge attempt fails, log and move on.
  • After 3 failed attempts across runs, stop retrying that PR. Surface it once via the retry-cap notification and let the operator investigate.
  • Do not modify PR state other than merging (no comments, no label edits, no branch updates).

Running this as an agent-shipping loop

To close the loop on PRs the agent itself opens (from feature, external-feature, self-improve, etc.), add the agent's GitHub identity under a ## Trusted Authors section in memory/watched-repos.md:

## Trusted Authors
- aeon-bot
- claude-code[bot]

Once allowlisted, agent PRs flow through the same safety policy as bot PRs and get auto-merged on green CI. The retry cap protects against runaway behavior on a stuck PR.

04 同仓库更多 Skills

aeonfun
aeonfun/aeon

sns-digest

Digest Internet Computer governance - new and open NNS proposals and SNS DAO proposals - so the operator sees what is up for a vote without watching the dashboard. Fully public, no secret; fires only when there is something new to report.

★ 793
aeonfun
aeonfun/aeon

icp-deploy-check

Pre-deploy safety checklist for an Internet Computer Rust canister - read a local/cloned canister repo and verify the deploy-time footguns from the canister-security checklist (controller backup, anonymous-principal rejection, no mainnet fetch_root_key, upgrade safety) before it ships. Static, read-only, no network and no mainnet action.

运维与部署
★ 793
aeonfun
aeonfun/aeon

canister-status

Monitor Internet Computer canisters for control-plane changes - a new controller, a module-hash change (upgrade), or a canister going missing from the public index - and alert the moment one happens. Uses only the public IC dashboard API.

开发工程师后端与接口
★ 793
aeonfun
aeonfun/aeon

you-web-search

Web search using You.com Search API with high-quality, cited results — runs keyless out of the box; YDC_API_KEY unlocks higher limits and real-time web crawling

开发工程师后端与接口
★ 793
aeonfun
aeonfun/aeon

write-tweet

Multi-format tweet studio - standalone drafts (10 across 5 size tiers), a 5-10 tweet thread, or 10 remixes of past tweets, selected via ${var}

★ 793
aeonfun
aeonfun/aeon

weekly-aeoncard

Build a weekly token-consumption recap image from memory/token-usage.csv - this week + all-time totals, top skills, rendered as a shareable SVG card.

图像与多媒体
★ 793