Skip to content
Back to skills

Github Auto Merge Setup

BSecurity

Diagnose and fix 'gh pr merge --auto' failures and set up CI-green auto-merge for GitHub repos/workflows. Use when: 'gh pr merge --auto' or a workflow merge step fails with an enablePullRequestAutoMerge error; auto-merge silently not working; enabling auto-merge for scheduled dependency-update workflows (dependabot etc.); adding a ruleset/branch protection with required status checks and choosing safe required checks; choosing merge flags (--rebase/--squash); a release workflow's push to main...

  • 39 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 30, 2026
testingpythongoshellbashexpressdebugginggitapidatabasedocumentation

Works with

  • cli
  • api

Security analysis

B75/100
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned September 30, 2026

npx -y skills add ericmjl/skills --skill github-auto-merge-setup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Github Auto Merge Setup?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Github Auto Merge Setup
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/ericmjl-github-auto-merge-setup/badge)](https://www.skillsdirectory.com/skills/ericmjl-github-auto-merge-setup)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: github-auto-merge-setup
description: >-
  Diagnose and fix 'gh pr merge --auto' failures and set up CI-green auto-merge for GitHub repos/workflows. Use when: 'gh pr merge --auto' or a workflow merge step fails with an enablePullRequestAutoMerge error; auto-merge silently not working; enabling auto-merge for scheduled dependency-update workflows (dependabot etc.); adding a ruleset/branch protection with required status checks and choosing safe required checks; choosing merge flags (--rebase/--squash); a release workflow's push to main rejected with GH013 rule violations; a half-landed release (PyPI/tag published but main never got the 'Bump version' commit, or releases dying at the 'already on PyPI' guard); 'gh pr merge --admin' no-opping while the PR stays BLOCKED under a ruleset (admins are NOT exempt; only bypass_actors); or 'N of M required status checks are expected'. The three-layer gate stack and one-command diagnosis live in the body.
created_by: autolearn
created_at: "2026-08-16"
---

# Github Auto Merge Setup

Diagnose and fix 'gh pr merge --auto' failures and set up CI-green auto-merge for GitHub repos/workflows. Use when: 'gh pr merge <PR> --auto' or a workflow's merge step fails with 'Auto merge is not allowed for this repository (enablePullRequestAutoMerge)', 'Protected branch rules not configured for this branch (enablePullRequestAutoMerge)', or auto-merge silently not working; you need to enable auto-merge ('auto merge if CI is green') for scheduled dependency-update workflows (dependabot, update-ollama-models, pre-commit autoupdate); you are adding a ruleset / branch protection with required status checks and need to know WHICH checks are safe to require; or you are choosing merge flags (--rebase/--squash) for a repo that may disallow some merge methods. Covers the THREE-LAYER gate stack: (1) repo allow_auto_merge setting, (2) merge-method permission (rebase-only repos reject --squash), (3) base-branch protection with required checks — all three must hold or auto-merge refuses; plus required-check selection rules (only checks reporting on every PR; matrix jobs required with parameter suffixes) and the one-command diagnosis: gh api repos/{owner}/{repo} --jq '{allow_auto_merge, allow_squash_merge, allow_rebase_merge, allow_merge_commit}'.  ALSO use when: a release workflow's direct push to main is rejected with GH013 repository rule violations; a green release job published PyPI/tag/release but main never got the 'Bump version' commit (half-landed release); or every subsequent release dies at the 'version already on PyPI' guard after a required-checks ruleset was added to mainALSO use when: 'gh pr merge --admin' appears to no-op (payload fragment ' (mergePullRequest)') and the PR stays BLOCKED under a ruleset — rulesets do NOT exempt repo admins/owners by default (unlike classic branch protection), only explicit bypass_actors can; or a PR shows 'N of M required status checks are expected' / BLOCKED with all-green checks — run the three-surface diff: ruleset required names vs statusCheckRollup vs GET /commits/{sha}/check-runs.  Sibling of github-actions-token-pr-creation-permission (a different repo-level Actions switch).

## Instructions

TODO: Add specific instructions based on observed patterns.

## ## The Three-Layer Auto-Merge Gate

- GitHub enables auto-merge (merge-when-green) ONLY when all THREE independent conditions hold. Diagnose top-down; a failure at any layer produces a DIFFERENT error:

LAYER 1 — Repo setting allow_auto_merge (default OFF).
- Symptom: 'GraphQL: Auto merge is not allowed for this repository (enablePullRequestAutoMerge)'.
- Fix: gh api -X PATCH repos/{owner}/{repo} -f allow_auto_merge=true
- Workflow YAML CANNOT enable this — it is a repo-level switch (same structural class as the PR-creation switch in github-actions-token-pr-creation-permission).

LAYER 2 — Merge-method permission.
- Symptom: merge command fails on a repo that disallows the requested method, e.g. --squash on a rebase-only repo.
- Fix: use the method the repo allows. Check: gh api repos/{owner}/{repo} --jq '{allow_squash_merge, allow_rebase_merge, allow_merge_commit}'.
- llamabot fact: rebase-only — never --squash there.

LAYER 3 — Base-branch protection with blocking requirements.
- Symptom: layers 1-2 fixed, yet 'gh pr merge <n> --auto --rebase' STILL fails: 'Protected branch rules not configured for this branch (enablePullRequestAutoMerge)'.
- Root cause: auto-merge DEFERS the merge until blocking requirements (required status checks / reviews) are satisfied. With NO branch protection on the base branch there is nothing to wait for, so GitHub refuses to enable auto-merge at all. (Counterintuitive: protection must be ADDED to get automatic merging.)
- Fix: create a ruleset (or classic branch protection) on main with required status checks. Verify existing state first: gh api repos/{owner}/{repo}/rulesets --jq '.[].name' (empty = none) and gh api repos/{owner}/{repo}/branches/main/protection.

ONE-COMMAND DIAGNOSIS (run before writing any merge flags):
gh api repos/{owner}/{repo} --jq '{allow_auto_merge, allow_squash_merge, allow_rebase_merge, allow_merge_commit}'

## ## Required-Check Selection Rules (Layer 3)

- A required status check that never reports BLOCKS merging forever. Before requiring a check:

1. ONLY require checks that run on EVERY PR. Read each workflow's 'on: pull_request:' trigger in the YAML — do not guess from a previous PR's check list. Conditional/filtered jobs are traps:
   - WIP bot (marketplace): reports only on WIP/draft PRs — requiring it blocks normal PRs.
   - check-changes / path-filtered jobs: may be skipped on unrelated changes.
   - '(Dry Run) Publish' steps: may be gated behind label/branch conditions.
2. Matrix jobs are required WITH their parameter suffix: 'unit-tests (Python 3.13, pixi)' is a distinct check name from 'unit-tests (Python 3.12, pixi)' — each combination must be listed (or use a check-name that is reported per-matrix and require the parent).
3. Source exact check names from an actual PR: gh pr view <n> --json statusCheckRollup --jq '.statusCheckRollup[].name' or gh pr checks <n>.
4. After creating the ruleset, re-run 'gh pr merge <n> --auto --rebase' on the target PR to confirm it takes (autoMergeRequest becomes non-null in gh pr view <n> --json autoMergeRequest).

llamabot example check set (2026-08-16): linting; bare-package (3.12/3.13 uv); smoke-tests (3.12/3.13 uv); unit-tests (3.12/3.13 pixi); Import time benchmark; Build documentation — EXCLUDING WIP, check-changes, and the dry-run publish.

## ## Workflow-Code Pattern (merge step in CI)

- For workflows that open AND merge their own PRs (scheduled dependency updates):

    if ! gh pr merge "$PR_URL" --auto --rebase; then
      echo "::warning::auto-merge unavailable — PR left open for manual merge"
    fi

- Degrade gracefully: emit ::warning:: and exit 0, leaving the PR open — a permanently-red scheduled workflow hides real failures.
- Keep the PR body honest about whether auto-merge will engage (it silently won't if layers 1-3 aren't all satisfied).
- GH_TOKEN is gh's canonical token env var (GITHUB_TOKEN also works).
- PRs CREATED with GITHUB_TOKEN do not trigger workflows (recursion prevention) — such PRs show only external checks; don't debug missing CI runs on them (see memory #1102 layer 3).

Discovered across llamabot sessions 2026-08-15/16 fixing the update-ollama-models workflow (PR #392, PR #390).

## LAYER 4 — The GITHUB_TOKEN bot-PR deadlock (ruleset × token recursion)

- TRIGGER: Layer 3's ruleset (required checks) exists AND the PR was created/pushed by a workflow using GITHUB_TOKEN (peter-evans/create-pull-request, bot pushes). SYMPTOM (llamabot PR #390, 2026-08-16): auto-merge enabled (autoMergeRequest non-null) but PR status BLOCKED forever; 'gh pr checks <n>' shows ONLY external checks (WIP bot) — zero CI runs on the head SHA.
ROOT CAUSE: 'When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run' (documented anti-recursion). Required status checks NEVER report on such PRs — so Layer 3 and GITHUB_TOKEN bot-PRs are mutually incompatible: the ruleset that ENABLES auto-merge also PERMANENTLY BLOCKS the bot PRs auto-merge was meant to serve. Do not debug why CI didn't fire on a GITHUB_TOKEN PR — it cannot.
FIX (canonical, per create-pull-request docs): have the update workflow create/push its PR with a PAT or GitHub App token (store as a repo secret, pass as the action's 'token:' input) — PAT pushes DO trigger workflow runs, so required checks run and report normally. Requires the user to provision the token; nothing else on the repo side fixes this.
REJECTED workarounds (analyzed 2026-08-16, don't re-derive): (a) workflow_dispatch to run CI manually — path-filter jobs break: check-changes-style jobs read github.base_ref which is EMPTY on dispatch, so 'git diff origin/${{ github.base_ref }}...HEAD' fails -> the required check-changes job FAILS -> blocked; and it means maintaining dispatch triggers on every workflow. (b) Marking check runs completed via API — hacky, unmaintained. (c) Requiring fewer checks does NOT help — NO workflow fires on GITHUB_TOKEN pushes at all, so no check can ever report.
ADJACENT FACTS: (1) SKIPPED checks SATISFY required status checks — GitHub treats a 'skipped' conclusion as passing for branch-protection purposes (so 'Import time benchmark', pull_request-only, is safe to require: on dispatch events it skips rather than blocks). (2) Create the ruleset with strict_required_status_checks_policy: false (non-strict; do NOT require up-to-date branch) — strict mode stalls rebase merges and auto-merge on busy repos. (3) Ruleset creation via REST: POST /repos/{owner}/{repo}/rulesets with target=branch, conditions.ref_name include refs/heads/main, rules[].type=required_status_checks.

## Layer 4 resolutions

- Both GITHUB_TOKEN bot-PR deadlock resolutions VALIDATED end-to-end 2026-08-16 (llamabot PRs #390/#394, both auto-merged):

1. UNSTICK an already-stuck GITHUB_TOKEN bot PR (no PAT needed): update its branch from a USER-authenticated context — 'gh pr update-branch <n>' run locally (or the Update branch button). The update push counts as a regular user push, so workflows fire on the new head SHA, required checks report, and the already-armed auto-merge merges when green. Use this when the bot PR already exists and just needs to merge.

2. PAT-ALTERNATIVE for future scheduled runs — the SELF-DISPATCH pattern. This REVISES 'workaround (a) rejected' above: workflow_dispatch fails ONLY when the path-filter job is left unpatched. Recipe (PR #394, merged):
   - TEST workflow (e.g. pr-tests.yaml): add 'workflow_dispatch:' to 'on:'; patch check-changes/path-filter jobs to treat workflow_dispatch like push — branch the base-ref logic on event type, because github.base_ref is EMPTY on dispatch, which is what broke 'git diff origin/${{ github.base_ref }}...HEAD'.
   - BOT workflow (e.g. update-ollama-models.yaml): add 'permissions: actions: write' (required to dispatch another workflow), and a post-PR-creation step: 'gh workflow run <test-wf.yaml> --ref <bot-branch>' so required checks report on the bot PR head.
   - Validation caveat: SKIPPED checks satisfy required checks (pull_request-only jobs skip on dispatch); confirm the FIRST scheduled run after merging actually merges its PR — the dispatch path is only proven by a real run.

3. RULESET UPDATE GOTCHA: PUT /repos/{owner}/{repo}/rulesets/{id} requires the COMPLETE rules array — sending only a single rule's parameters fragment is silently ignored (the response still shows the old rules). Always GET the ruleset, mutate the full rules list, PUT the whole definition back.

4. WIP-title caveat: a bot PR whose title starts with WIP is blocked by the WIP bot check forever — keep scheduled-bot PR titles WIP-free.

## LAYER 5 — Ruleset side effect: required-checks rulesets break direct-push RELEASE workflows (GH013 half-landed release)

- TRIGGER: a release workflow bumps the version then pushes the bump commit directly to main ('git push --atomic origin HEAD:main refs/tags/vX') on a repo whose main ruleset has required_status_checks (the Layer 3 ruleset). SYMPTOM (llamabot v0.19.13, 2026-08-16): the release job goes GREEN, PyPI + GitHub release + tag all exist — but main never gets the 'Bump version to vX' commit, and EVERY subsequent workflow_run-triggered release recomputes vX and dies at the 'version already on PyPI' guard (looks like a dependabot/trigger problem; it is not — dependabot merges are just the latest trigger). ROOT CAUSE: a direct workflow push has no PR/status checks, so GitHub rejects it with 'GH013: Repository rule violations found for refs/heads/main'. If the release script does not check the push exit code, it prints success anyway ('✓ Pushed HEAD to main and tag vX atomically' right AFTER the rejection); then 'gh release create --target main' AUTO-CREATES the tag at main's OLD tip (not the bump commit) and PyPI publishes — a HALF-LANDED release: artifacts have vX while main's pyproject still says vX-1. DIAGNOSIS: compare main's pyproject version vs latest tag vs PyPI; grep run logs for 'GH013' / 'git push' using distinctive strings rather than step names (partial logs lose step attribution to 'UNKNOWN STEP'); smoking gun = success message printed immediately after a remote rejection in the same log. RECOVERY: land the bump commit on main (PR or authorized direct push) so main catches up to the published version. PREVENTION — when adding a required-checks ruleset to main (Layer 3), AUDIT every workflow that pushes DIRECTLY to main: release scripts must fail hard on push rejection (set -e / explicit exit-code checks, never '|| true' semantics around the push), 'Ensure complete'-style verification steps must actually verify the branch push landed, and never rely on 'gh release create --target' as a tag fallback — it silently re-creates the tag at the wrong commit. Layer 3 fixes auto-merge but quietly bricks direct-push release workflows in the same stroke.

## LAYER 5 mechanism and structural fix (llamabot v0.19.13 fix session, 2026-08-16)

- TWO new load-bearing facts beyond the Layer 5 diagnosis:

1. SILENT-FAILURE MECHANISM — custom shell strings do NOT get '-eo pipefail'. GitHub injects 'bash --noprofile --norc -eo pipefail {0}' ONLY for the bare 'shell: bash' form. A custom string like 'shell: bash -l {0}' (used when a login shell is needed) runs AS-IS with NO '-e' and NO pipefail — so 'git push --atomic' exiting non-zero does NOT fail the step; the script falls through to its success echo and the job goes green after a rejected push. EMPIRICALLY CONFIRMED: the llamabot release step printed its success message immediately after the GH013 rejection. FIX: any step using a custom shell string MUST begin with an explicit 'set -euo pipefail' (and any step whose failure must be loud should avoid custom shells or add explicit exit-code checks around the push).

2. STRUCTURAL FIX — ruleset bypass actor for the release workflow's direct push. GET the full ruleset, add: bypass_actors: [{actor_id: 15368, actor_type: Integration, bypass_mode: always}] — actor_id 15368 is the WELL-KNOWN GitHub Actions app ID (same across all repos; look it up once). PUT /repos/{owner}/{repo}/rulesets/{id} with the COMPLETE definition (per Layer 4 gotcha: PUT requires the full rules array — GET, mutate, PUT the whole thing back, else changes are silently ignored). This lets the release workflow push to main despite required checks while keeping the ruleset enforced for humans/bots.

## LAYER 5 CORRECTION — github-actions[bot] CANNOT be a ruleset bypass actor; release-via-PR is the working fix

- The 'STRUCTURAL FIX' above (bypass_actors actor_id 15368) is DISPROVEN — do NOT retry it: PUT /repos/{owner}/{repo}/rulesets/{id} with that bypass actor FAILS with 'Actor GitHub Actions integration must be part of the ruleset source or owner organization' (llamabot ruleset 20907627, 2026-08-16); the built-in GitHub Actions integration is not eligible as a ruleset bypass actor. THE WORKING STRUCTURAL FIX is routing the release push through a PR (required checks are satisfied naturally by PR merge):
1. TAG PUSHES ARE NOT BLOCKED: a ruleset whose conditions.ref_name includes only refs/heads/main blocks BRANCH pushes only — but 'git push --atomic HEAD:main refs/tags/vX' fails ALL-OR-NOTHING, so push the tag separately AFTER the bump lands on main.
2. SEQUENCE: push bump branch -> gh pr create -> 'gh pr merge "$PR" --rebase --auto --delete-branch=false' -> poll 'gh pr view "$PR" --json state --jq .state' every ~30s until MERGED (bounded loop, then ::error:: + exit 1). CRITICAL: --auto RETURNS IMMEDIATELY — without the poll, tagging/publishing steps race ahead of the actual merge. Auto-merge+poll is more robust than 'gh pr checks --watch' (which races 'no checks reported yet' right after PR creation).
3. REBASE MERGE RECREATES THE SHAs: after MERGED, 'git fetch origin main --tags --force', verify origin/main's HEAD commit SUBJECT matches the expected release-notes commit (guard against something else merging), then 'git tag -a vX -m ... origin/main' and push the tag. BUG CLASS caught in review: a rebase/cleanup step that deletes the local tag but never recreates it leaves the later push step pushing a NONEXISTENT ref.
4. TAG-EXISTS EDGE: pushing an identical already-existing remote tag is REJECTED without force — pre-check 'git ls-remote origin refs/tags/vX'; if the tag exists for a version not yet on PyPI, FAIL LOUDLY (prior partial run; safest is human resolution). Never force-push tags in CI.
5. LAYER 4 APPLIES TO THE RELEASE PR TOO: a GITHUB_TOKEN-created PR gets NO CI runs (event-recursion prevention) so required checks never report — include the SELF-DISPATCH step (bot workflow with 'permissions: actions: write' runs 'gh workflow run <test-wf.yaml> --ref <pr-branch>'), the pattern proven on llamabot's update-ollama-models (PR #394).
PREREQS (one-command check): allow_auto_merge=true AND allow_rebase_merge=true (rebase-only repos: never --squash).

## LAYER 5 ROLLBACK COROLLARY — post-merge force-push rollback of main is impossible under required-checks rulesets

- Discovered reviewing llamabot release-workflow PR #396 (2026-08-16): a release workflow's ROLLBACK step CANNOT force-push main back to PRE_PUSH_SHA after a post-merge failure — the same ruleset that blocks the forward direct push blocks the backward one. WHY (three load-bearing facts): (1) check runs attach to a SHA GLOBALLY, not per-ref; (2) rebase merges RECREATE commits, so the landed main SHA differs from the tested PR-head SHA — landed main SHAs often carry NO check runs at all; (3) PR merges satisfy required checks via PR machinery (checks evaluated on the PR head at merge time), while DIRECT pushes (including rollback force-pushes) are evaluated on the pushed SHA's own check runs — which the previous main tip lacks. DESIGN RULE: in release-workflow rollback matrices, post-merge failure recovery = delete the remote tag + delete the GitHub release + emit a loud ::error:: warning about the stranded version (PyPI publishes are irreversible); NEVER claim 'force-push main' as a rollback action in the header-comment rollback matrix, and drop the now-unused PRE_PUSH_SHA variable from the rollback step (keep the capture step for diagnostics/logging only). ROLLBACK-GUARD SEMANTICS: steps.<id>.outcome is 'skipped' (not empty) for steps that never ran due to an earlier failure, so reverse-order rollback guards comparing outcome != 'success' are SAFE; steps.<id>.outputs.X is empty-string for never-run steps, so [ -n "$VAR" ] guards work. PERMISSIONS NUANCE: an explicit top-level 'permissions:' block REPLACES the default token permissions for ALL jobs in the workflow — a release workflow that creates its own PR AND self-dispatches CI needs BOTH 'pull-requests: write' AND 'actions: write' in that block (missing either silently kills that leg with no other error).

## LAYER 5 VERIFIED LIVE — release-via-PR design works end-to-end (llamabot PR #396)

- The full LAYER 5 CORRECTION design landed in llamabot PR #396 (merged via rebase 2026-08-16) and was verified in production: (1) the release run triggered by the merge of the version-bump PR correctly printed 'Skipping: subject is a version bump/release notes commit' and exited clean — always include a skip-if-already-released subject guard so merge-triggered runs of the release workflow do not double-release; (2) extend the skip-subject list to BOT MERGE subjects too (dependabot 'chore(deps): ...', automated ollama-list commits) so bot merges never trigger releases; (3) all mutation steps carry 'set -euo pipefail' (the custom 'bash -l {0}' shell had silently swallowed the rejected push — see memory #1196). Net effect: main reconciled at 0.19.13, next auto-release computes 0.19.14.

## LAYER 6 — Docs-only/path-filtered PRs: a skipped PARENT matrix job never reports the required check names (all-green-but-BLOCKED)

- REFINES the Layer 4 adjacent fact that skipped checks satisfy required status checks: satisfaction requires a check run with the EXACT required name to REPORT conclusion=skipped. When a path-filter job (check-changes) skips the whole matrix job on a docs-only PR, the matrix NEVER EXPANDS, so the per-combination check names the ruleset requires (e.g. "unit-tests (Python 3.12, pixi)") never come into existence — mergeStateStatus BLOCKED with zero failing checks. SMOKING GUN: the check list shows skipped jobs under a literal UNEXPANDED display name like "${{ matrix.test-type }} (Python ...)" instead of the real matrix names the ruleset expects. DIAGNOSIS CHAIN: (1) gh pr view <n> --json statusCheckRollup — do the required names appear AT ALL (any conclusion)? (2) diff against the ruleset required checks; (3) look for unexpanded matrix expressions in reported names. WORKAROUND (validated on llamabot PR #397, 2026-08-16): gh workflow run pr-tests.yaml --ref <pr-branch> — workflow_dispatch forces has-code-changes=true in the workflow, the matrix runs, and its check runs attach to the head SHA satisfying the ruleset. ADJACENT LESSONS from the same session: (a) never pipe gh pr merge --auto output through head/tail — the arming failure gets swallowed; ALWAYS verify with gh pr view <n> --json autoMergeRequest, and RE-VERIFY at merge time (a prior session arming-verified does not persist — autoMergeRequest was later found null); (b) if checks report SUCCESS yet the PR is STILL BLOCKED, enumerate ALL rulesets (gh api repos/{owner}/{repo}/rulesets — including org-level and any created after your last fetch) and check gh pr view <n> --json reviewDecision for required approvals BEFORE assuming the checks are the problem (llamabot #397 remained BLOCKED even after the dispatched checks went SUCCESS — unresolved at session end; suspect a second ruleset or a required-review rule).

## LAYER 6 UPDATE — admin-bypass asymmetry and dispatch partial-crediting (llamabot PR #397, continued 2026-08-16)

- - Three load-bearing facts beyond the initial Layer 6 entry, from the continued #397 debugging:

1. ADMIN BYPASS ASYMMETRY (rulesets vs classic branch protection): 'gh pr merge <n> --admin' CANNOT override a repository ruleset. Unlike classic branch protection (where admins bypass by default unless 'Include administrators' is checked), RULESETS enforce against EVERYONE including the repo owner/admin unless they are explicitly added as bypass_actors. SYMPTOM: the --admin merge returns a GraphQL payload fragment (' (mergePullRequest)') instead of a clean error, and the PR stays OPEN/BLOCKED — looks like a no-op, not a rejection. FIX PATH (considered, not yet validated): add a bypass actor to the ruleset (Repository admins role via the rulesets UI, or GET-then-PUT the COMPLETE ruleset definition with bypass_actors appended — per the Layer 4 gotcha, a partial PUT is silently ignored). Do not burn turns re-trying --admin or assuming owner status exempts you.

2. DISPATCH PARTIAL-CREDITING: the workflow_dispatch workaround's check runs ARE credited for SOME required names but not all — #397 showed '6 of 8 required status checks are expected': smoke-tests x2, bare-package x2, check-changes, and Import time benchmark (skipped counts) were ALL credited from the dispatched run, while the two unit-tests names were NOT, despite GET /repos/{o}/{r}/commits/{head-sha}/check-runs showing 'unit-tests (Python 3.12, pixi)': success ON THE EXACT PR HEAD SHA. So dispatched checks demonstrably CAN satisfy required checks for a PR (this validates the Layer 6 workaround mechanism) — but per-name crediting can diverge from the commit's check-run ground truth. Working hypothesis at interrupt: per-name LATEST check run wins, so a newer same-name run (queued/in-progress, e.g. from the PR's own pull_request suite) may shadow the dispatch's success. NEXT DIAGNOSTIC STEP (was not reached): query /commits/{sha}/check-runs WITH started_at/completed_at/app fields and look for a newer or in-flight unit-tests run.

3. THREE-SURFACE DIFF DIAGNOSTIC (the technique, generalize it): when a PR is BLOCKED with green checks, diff THREE surfaces — (a) the ruleset's required check names (gh api repos/{o}/{r}/rulesets, enumerate ALL including org-level), (b) gh pr view <n> --json statusCheckRollup (what the PR/ruleset evaluator credits), (c) GET /repos/{o}/{r}/commits/{head-sha}/check-runs (ground truth of what exists on the SHA). (b)-vs-(c) divergence — checks exist on the SHA but are not credited — is distinct from (a)-vs-(b) divergence (name never reported at all, the Layer 6 skipped-matrix case). Also note the rollup can contain a LITERAL UNEXPANDED name entry ('${{ matrix.test-type }} (Python ...)') from the skipped matrix in the pull_request-triggered run, which is NOT a name conflict with the expanded required names.

STATUS at session interrupt: #397 still BLOCKED, unresolved; user interrupted with 'Take action now.' mid-diagnosis.

## Multiple check suites per SHA: workflow_dispatch runs stale the original suite (added 2026-08-16, llamabot)

- A PR showing 'N of M required status checks are expected' / BLOCKED where check runs EXIST on the head SHA with conclusion=success can be caused by MULTIPLE CHECK SUITES for the same SHA from the same app. TRIGGER: you tried to satisfy path-filtered/skipped required checks by running 'gh workflow run <workflow> --ref <pr-branch>' (workflow_dispatch) on the PR's head SHA to force a skipped matrix to run. ROOT CAUSE: when several check suites exist for one SHA+app pair (the original pull_request run + a later dispatch run), GitHub evaluates required checks against the LATEST suite only; check runs from OLDER suites become stale and do NOT count as 'expected' — even at conclusion=success. The dispatch workaround BACKFIRES: it forces the matrix to run but simultaneously stales the original suite's green runs (observed: '6 of 8 expected' with both unit-tests runs green on the SHA, immediately after a 23:12 dispatch superseded the PR's own run). SMOKING GUN: green check runs on the SHA + the ruleset lists them required + the rollup counts them missing + a workflow_dispatch run NEWER than the PR's own run exists on the same SHA. FIX: trigger checks NATIVELY instead of dispatching — push real code changes so path-filter jobs (check-changes -> matrix) expand with their proper names inside the PR's own run; or re-run the PR's ORIGINAL run (re-run keeps that suite latest) rather than creating a new dispatched suite. Confirmed 2026-08-16 on llamabot: once the PR carried actual code, all 12 checks reported expected and the PR merged clean. Related tell: an unexpanded matrix lists literal placeholder display names (e.g. '${{ matrix.test-type }} (Python ...)') that match no required context — see the skipped-checks layer.

## LAYER 7 — action_required bot-run approval gate: pull_request runs from github-actions[bot] PARK awaiting maintainer approval

- REFINES Layer 4's 'GITHUB_TOKEN PRs do not trigger workflows': on a PR created by github-actions[bot] (release workflow or create-pull-request), the pull_request event DOES create a workflow run — but with triggering_actor github-actions[bot] and conclusion 'action_required', PARKED awaiting a maintainer's 'Approve and run' click in the Actions UI. A parked run never reports success/failure, so required checks never complete, auto-merge never fires, and bounded wait-loops (poll 30s x 60min) time out and trigger rollback. SYMPTOM SIGNATURE (llamabot release PR #399, v0.19.14, 2026-08-16): release PR open, dispatched workflow_dispatch checks green, wait loop ends with its empty-state sentinel ('state: unknown'), run list shows the pull_request-event run at action_required. DIAGNOSIS (add to the stalled-bot-PR chain, run EARLY): gh run list --branch <pr-branch> --json databaseId,event,conclusion,triggeringActor — an action_required pull_request run is a HARD blocker the self-dispatch pattern cannot necessarily shadow: it runs the SAME check suite the ruleset requires, and latest-run-wins crediting may prefer the parked run (see Layer 6 dispatch partial-crediting). OPEN QUESTION (unresolved at interrupt): llamabot update-ollama-models pull_request run 31941828924 concluded SUCCESS the SAME morning (after aee4596d 'ci: run tests on bot PRs so auto-merge can complete' and after the same-day can_approve_pull_request_reviews switch flip) while older ollama runs AND release PR #399's run parked action_required — candidates: (a) a maintainer manually approved the ollama run that morning, (b) a repo Actions approval-scope setting changed, (c) something in the aee4596d diff. Diff the two runs field-by-field (gh api repos/{o}/{r}/actions/runs/<id>) BEFORE theorizing. ADJACENT: 'gh pr merge --auto' printing 'Auto-merge enabled' (exit 0) does NOT guarantee autoMergeRequest stays armed — #399 showed auto: null later in the same run; extends the Layer 6 re-verify lesson (verify autoMergeRequest after arming AND again after any wait-loop timeout before diagnosing other layers). WAIT-LOOP READING: a bounded poll loop ending on its empty-state sentinel ('state: unknown') usually means the merge condition genuinely never became true — check the PR state and the blocking check's conclusion BEFORE auditing the loop logic for bugs.

## LAYER 7 RESOLUTION — parked runs report NOTHING; dispatch runs NEVER count; in-workflow approve is the fix (llamabot #399/#400, 2026-08-16)

- RESOLVES the LAYER 7 open question: the update-ollama pull_request run succeeded that morning because a maintainer (Eric) MANUALLY APPROVED the parked run — not the aee4596d diff, not the can_approve_pull_request_reviews switch flip. Human approval let the pull_request suite provide every required check. TWO mechanics nailed on release PR #399 (its rollup showed ONLY the WIP marketplace check): (1) A PARKED (action_required) run creates NO check runs at all — GitHub creates check runs only after the run is approved and starts, so the parked pull_request suite contributes NOTHING (not even pending checks) to the PR rollup. (2) workflow_dispatch check runs attach to the head SHA but do NOT appear in the PR check rollup, and ruleset evaluation follows rollup semantics — DISPATCH CHECK RUNS CAN NEVER SATISFY THE REQUIRED CHECKS ON A PR (observed empirically: dispatch checks green on the #399/#397 head SHA yet absent from the rollup). This supersedes the Layer 6 dispatch workaround and reframes Layer 6 partial-crediting: never design around dispatch-run check satisfaction; the ONLY dependable source of required checks is the pull_request suite of the PR itself running to completion. THE FIX (llamabot PR #400, release-python-package.yaml, step added right after gh pr create): (a) requires permissions: actions: write (already present for the self-dispatch leg); (b) POLL FOR THE RACE — the parked run may not exist immediately after PR creation: loop 10 x 15s on gh api repos/{owner}/{repo}/actions/runs?head_sha=$(git rev-parse HEAD)&status=action_required --jq .workflow_runs[].id until non-empty; (c) approve each parked run: gh api -X POST repos/{owner}/{repo}/actions/runs/{run_id}/approve; (d) self-approval may be blocked (GITHUB_TOKEN approving runs of its own bot PR) — on failure emit ::warning:: (manual approve, or provision a PAT as RELEASE_TOKEN) and CONTINUE; do not fail the release on it. VALIDATION STATUS: PR #400 checks all green (83 pass lines incl. watch repeats); the live end-to-end test (merge #400 -> workflow_run release run -> PR create -> approve step -> auto-merge -> 0.19.14 on PyPI) was IN FLIGHT at session end — UNVERIFIED. Next session: read the release run logs for the approve-success lines vs the warning fallback, then reconcile PyPI/tag/release-notes/main for 0.19.14. Design doc: docs/designs/release-pipeline/LLD.md updated with the parked-run mechanism.

## LAYER 6 TIMELINE — dating a latent skip-pattern landmine (git log -S archaeology)

- When a skip optimization suddenly blocks PRs after months of working fine, the skip pattern did not break — a NEW governance layer (a ruleset listing the skipped check names as required) landed on top of it. Date BOTH sides before proposing any fix:
- When was the skip logic introduced? git log -S 'has-code-changes' --oneline -- .github/workflows/ (pickaxe finds the commit that introduced the string; llamabot: a7fcff30, 2025-07-06 — a pure CI-time optimization, harmless for 13 months). Also git log -S for later jobs that EXTENDED the pattern (llamabot: 5c095862 import-time benchmark).
- When did the ruleset arrive? gh api repos/{owner}/{repo}/rulesets --jq '.[] | {name, created_at}' (llamabot: 'Require CI checks on main', 2026-08-16 — the toxicity is 13 months NEWER than the pattern it poisoned).
User-facing narrative: 'introduced as an optimization on D1, harmless until the ruleset on D2 — required checks must REPORT, skipped jobs never do.' Fix candidates in simplicity order (user constraint 2026-08-18: the SIMPLEST fix, no over-complicating): (1) make the gated matrix jobs report success instead of skipping (restructure so the parent job and check names always come into existence), (2) remove the skip gate entirely, (3) de-list the skip-prone check names from the ruleset. GENERAL RULE: any workflow optimization that makes checks CONDITIONALLY ABSENT is a latent landmine — re-audit skip-if patterns the day a required-checks ruleset is added, and when diagnosing, date both layers rather than assuming the workflow recently regressed.

## LAYER 6 RESOLUTION — gate deletion must preserve required-check-named jobs as always-reporting stubs (llamabot 2026-08-18)

- RESOLVES the LAYER 6 TIMELINE fix-candidate list: Eric chose candidate (2) 'remove the skip gate entirely', and deletion has a NON-OBVIOUS CONSTRAINT the candidate list missed — audit the ruleset's required check names FIRST: if the GATING job's own name is a required check (llamabot: 'check-changes'), the JOB MUST SURVIVE as a gutted always-reporting stub (keep the job id and display name; delete its outputs + diff computation; replace steps with checkout + an unconditional echo, plus a comment stating WHY it survives), because deleting the job outright makes that required name permanently absent and blocks EVERY PR. Only the CONSUMPTION of its output dies: downstream jobs drop 'needs: <gate>' and the gate's if: condition entirely — EXCEPT non-gate clauses in the if: with independent reasons to stay (llamabot import-benchmark keeps 'github.event_name == pull_request' because it posts PR comments, meaningful only on PRs). Template (llamabot commit e4bda55a on ci/ungate-test-matrix): check-changes = checkout + 'Report readiness' echo step; test-matrix runs unconditionally; benchmark if: shrinks to the PR-only clause. Note (2)+stub is SIMPLER than candidate (1) 'make matrix report success instead of skipping' — the stub pattern wins the simplicity contest the user constrained the fix to. VALIDATION STILL PENDING at time of writing: after the PR merges, a docs-only PR must report all required check names and merge unblocked.

## LAYER 6 RESOLUTION — verification procedure for a gate-removal PR

- - Addendum to the gate-deletion stub pattern (llamabot #402 continuation, 2026-08-18): after merging the gate-removal PR, verify with a docs-only PR cut from POST-merge main. LOAD-BEARING FACT: GitHub runs pull_request workflows from the PR's MERGE REF (the merge commit of PR into base) — a verification branch cut from post-merge main gets the NEW ungated workflow; a branch cut BEFORE the merge still runs the OLD gated workflow and proves nothing. Make the docs-only edit genuinely useful (e.g. update the feature LLD's 'Known residual state' section to note docs-only PRs are unblocked) rather than a dummy whitespace touch. Success criteria: the matrix checks report as RUNNING/PASSED with their real expanded names (not skipped, no literal '${{ matrix.* }}' placeholders) and mergeStateStatus reads CLEAN. Also expect the gate-removal PR's own merge to trigger an auto-release if its subject does not match the release workflow's skip-subject list.

## LAYER 6 VALIDATED — gate-deletion stub pattern proven end-to-end (llamabot #403, 2026-08-18)

- Closes the 'VALIDATION STILL PENDING' note above: after PR #402 (9689e5d8, 'ci: run the test matrix unconditionally on every PR') merged, the follow-up docs-only PR #403 — cut from post-merge main per the verification procedure — received the FULL check rollup with real expanded matrix names and merged with ZERO manual intervention: no workflow_dispatch, no admin override, no manual approvals. That was the exact PR class permanently BLOCKED before the fix. As expected by design, #403's merge auto-triggered release 0.19.16 (every merge releases, docs included — see pypi-release-version-desync). Layer 7's in-workflow self-approval also validated a SECOND consecutive time (0.19.15's parked pull_request runs self-approved; release completed clean). Cumulative status: the Layer 6 gate-deletion stub pattern AND the Layer 7 self-approval step are both PROVEN LIVE end-to-end; the llamabot CI/ruleset saga (Layers 1-7) is fully resolved. Post-fix cleanup pattern that worked: remove the session worktree, delete local branches, and record any user corrections (commit-message-intent correction rode in the same PR #402).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…