Skip to content

ci: CI on bot PRs by dispatch, gate acceptance and alarm surfacing - #253

Open
88plug wants to merge 2 commits into
mainfrom
claude/fresh-bot-pr-ci
Open

88plug wants to merge 2 commits into
mainfrom
claude/fresh-bot-pr-ci

Conversation

@88plug

@88plug 88plug commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Bot PRs get real CI without a manual approval, the merge gate accepts that CI, and open alarms are printed wherever the gate or CI runs. This is the bot-pr-ci part of the freshness CI/CD design. It uses GITHUB_TOKEN only.

Why

GitHub holds pull_request runs on a PR that GITHUB_TOKEN created or updated: they wait for approval with conclusion action_required and run no jobs. That is how #240 sat with CI held while only CodeQL ran. A workflow_dispatch run is never held. ci.yml has no event-specific logic, so a dispatched run on the PR branch tests the same head.

What changes

ci.yml

  • Adds a workflow_dispatch trigger. Its concurrency key falls back to ci-<sha>, and dispatched runs are never cancelled.
  • Adds a last job, alarms. It prints one ::warning:: per open ci-alarm issue, with its URL and whether it carries alarm-ack. It also warns when the Watchdog workflow is missing, disabled, or has had no successful run in 3 h.
  • The alarms job reads issues and actions only. It cannot fail CI: the script exits 0 and the job is continue-on-error.
  • Runs the new tests/bot_ci_dispatch_test.sh in the detection-tests job.

hack/bot-ci-dispatch [--branch B] [--workflow ci.yml]

  • Covers only open PRs into main from same-repo autobump/* branches whose author is github-actions[bot]. It never touches forks or human branches.
  • It dispatches the workflow when the head SHA has no run other than held ones.
  • It re-runs failed jobs once per SHA when a run failed or timed out on its first attempt. Cancelled runs are left alone.
  • At most BOT_CI_MAX_INFLIGHT bot runs (default 1) are queued or running at once. It does nothing while a ci.yml run of a human PR is queued, so bot CI cannot starve the 20-40 job pool.
  • A branch whose ci.yml lacks the trigger is skipped with a note; the next nightly recreates it.
  • A run whose head branch is gone (headBranch null) counts as a human run. An unreadable run pool stops the script with exit 1 rather than dispatching blind.
  • It prints one line per decision and is idempotent.

hack/autobump-branch-reset <branch>

  • Generalises the digest job's stale-branch step.
  • It deletes autobump/<name> on origin when its .github/workflows differ from main's. Otherwise create-pull-request's force-push from GITHUB_TOKEN is refused once this PR changes ci.yml.
  • It refuses any branch outside autobump/.

build-and-push.yml

  • All three PR jobs (bump-components, repin-images, bump-convert-pins) now run autobump-branch-reset before create-pull-request and bot-ci-dispatch --branch after it, under the same if:.
  • Those jobs now have actions: write.
  • The comments and PR bodies that said these PRs start no CI are corrected. They now say the PR's own CI is held and point at the dispatched run (gh run list -w ci.yml -e workflow_dispatch -b <branch>).

.github/workflows/bot-sweep.yml ('Bot sweep')

  • Runs hourly at :13 and on dispatch, in concurrency group bot-sweep.
  • Job ci-fill runs hack/bot-ci-dispatch to fill in what the nightly deferred. Later parts add the known-issues and merge jobs.

hack/gate_lib.sh (new) and hack/gate_merge.sh

  • READ, RUNS, code, base_ok and rules_stale move to gate_lib.sh. rules_stale and code are byte-identical to the old copies.
  • New ci_status <sha> <branch> prints green|pending|failed|none. It looks at ci.yml at the SHA through a pull_request run or a workflow_dispatch run on that branch, and the latest completed run decides. Held runs are not counted as attempts; held runs alone count as failed, as before.
  • Once the dispatched run is green, held ACTION_REQUIRED entries of 'CI' and 'Auto-merge Dependabot PRs' no longer block, in either the rollup or the run list. A held run with no dispatched run still exits 1, so human fork PRs are unchanged.
  • base_ok now ignores release-only bot commits on main. These are commits with a release: ... [skip release] subject, authored by 88plug-bot or github-actions[bot], that touch only the release files (and only the VERSION="..." line of scripts/k3d-gpu). The three bot commits of each release no longer force a sync.
  • The gate prints every open ci-alarm issue at the start. While any alarm lacks alarm-ack, it exits 4 on a PR that changes .github/ or a shipped path, both at the start and again just before the merge. It prints the alarm URLs and gh issue edit N --add-label alarm-ack.
  • Shipped paths are packaging/, scripts/, share/, Dockerfile, console/, images.lock, plus hack/release-paths once it exists.
  • PRs that change only EXPERIMENTS.md, docs/, bench/ or tests/ are never refused.
  • The gate looks for gate_lib.sh next to itself first, then in the repository's hack/. A scratchpad copy of the gate alone therefore still works from a checkout of main.

Deleted: .github/workflows/dependabot-auto-merge.yml

  • It ran gh pr merge --auto on a main with no required checks.
  • It never merged anything in 500 runs.
  • It added a held run to every bot PR.
  • Deleting it also retires security-audit finding M-14, which was about that file.

README: one sentence on where a bump PR's CI comes from.

Owner action needed

Issues are disabled on this repository (has_issues: false), so no ci-alarm issue can exist yet. Until Issues are enabled in Settings > General > Features:

  • the gate prints WARNING: could not list the open ci-alarm issues (the '88plug/k3d-gpu' repository has disabled issues); the alarm check was skipped;
  • every CI run carries the matching warning.

The alarm part (P1) needs Issues on as well. Nothing in this PR changes repository settings.

Tests (all run locally on this branch, rebased on 74ff63e)

Suite Result
tests/gate_merge_test.sh 67 passed, 0 failed (the existing cases unchanged and green)
tests/bot_ci_dispatch_test.sh (new, fake gh) 16 passed, 0 failed
tests/ci_wiring_test.sh 93 passed, 0 failed
tests/claims_check.sh 26 files clean
tests/claims_check_test.sh 81 passed
python3 tests/ledger_drift_check.py 41 files agree with 81 ledger rows
Detection-tests job suites: detect 1185, kube_context 38, image 26, host_setup 269, eat 64, platforms_doc 195, reserved_cpus 23, experimental 122, phase_steer 69, prefill_guard 78, auto_release_cas 17 all passed, 0 failed
tests/image_pins_test.sh, tests/packaging_test.sh, tests/bench_test.sh 43, 126 and 71 passed
actionlint 1.7.12 (ACTIONLINT_IMAGE digest from hack/ci-images.env) clean
shellcheck v0.11.0 at CI's warning severity, on every changed script clean

Tests first. The new gate cases were run against main's hack/gate_merge.sh: 12 of them fail there for the expected reasons (no dispatched-run acceptance, held runs terminal, a release-only base forces a sync, alarms ignored, no library lookup) and 55 pass.

CI on this head (c2ffee0). ci.yml pull_request run 40979116002 is green on attempt 2. Attempt 1 had one failure, in a test this PR does not touch: tests/crescent_hold_test.sh, "TERM during the slotted gate's busy wait stops the hold at once" (hold.sh was still running 10 s after TERM on the runner). It passed on the re-run of that job, locally, and on main. It looks timing-sensitive on a loaded runner.

New gate cases

  • ci_status, called directly from gate_lib.sh: 11 cases.
  • rules_stale: stale, current and no-tree cases.
  • Dispatch-only green merges.
  • Held plus dispatch green merges.
  • A held run alone exits 1, whether it shows in the rollup or only in the run list.
  • A dispatch run on another branch is not accepted, and does not excuse a held run.
  • The latest failed dispatch run exits 1.
  • base_ok, against real git:
    • release-only bot commits on main mean no sync;
    • a release commit that also changes code syncs;
    • a human author on a release subject syncs;
    • a subject without [skip release] syncs.
  • Alarms:
    • un-acked plus shipped path exits 4 and prints the URL and ack command;
    • a workflow-only PR is refused;
    • unreadable PR files are refused;
    • GATE_RELEASE_PATHS patterns are honoured;
    • an alarm opened mid-run is refused before the merge;
    • acked alarms are listed and the PR merges;
    • ledger, docs, bench and test PRs are never refused;
    • an unshipped path merges;
    • issues disabled gives a warning and the PR merges.
  • Gate copied alone, and the missing-library error.

Wiring test

  • Every create-pull-request step in every workflow has the reset step before it and the dispatch step after it, under its if:, with actions: write.
  • The file:// stale-branch test now runs for all three branches, in the absent, same and differ cases.
  • autobump-branch-reset refuses non-autobump names.
  • ci.yml has workflow_dispatch; dependabot-auto-merge.yml is absent; bot-sweep.yml has the right shape.
  • The alarms job's own script runs against a fake gh: two alarms, no Watchdog, a stale Watchdog, a Watchdog that never succeeded, a disabled Watchdog, and gh down.

Live read-only checks

Post-merge checks (eyes-on)

  • After the first nightly following the merge, each autobump branch whose workflows differ from main should be deleted and reopened as a fresh PR. Each fresh PR should show a ci.yml run with event workflow_dispatch at its head SHA: gh run list -w ci.yml -e workflow_dispatch -b <branch>.
  • Bot sweep runs hourly and fills deferred dispatches (one bot ci.yml run in flight at a time).
  • chore(convert-pins): move the QAT conversion toolchain to its newest compatible releases #240 was closed on 2026-10-30 at 00:02 UTC. The next nightly with a convert-pins change opens a fresh PR with dispatched CI.

Known limits and still unverified

  • A held Docs run still blocks the gate. docs.yml runs on pull_request for README.md, docs/, docs-build/ and mkdocs.yml, and has no workflow_dispatch. An autobump/image-digests PR that moves the docs-build/Dockerfile digest therefore gets a held Docs run, which the gate reports as Docs=action_required. Only 'CI' and 'Auto-merge Dependabot PRs' are excused, as specified. Until a later part dispatches Docs too, approving that one held run clears it; the App token upgrade removes the hold.
  • No dispatched run was started from this PR. The CI pool had 10 ci.yml runs queued (other PRs and ledger runs), and this PR's own rule is to defer while human PR CI is queued. So these remain unverified until the first nightly after merge:
    • that a dispatched ci.yml run on a bump branch passes as a pull_request run does (ci.yml has no event-specific logic);
    • whether the check runs of a dispatched run appear in the PR's checks UI. The gate does not rely on it: it reads runs by SHA.
  • The 1-run cap is best-effort when the nightly's matrix jobs call the dispatcher at the same moment. An overshoot costs runner time only, and the hourly sweep honours the cap.
  • When an un-acked alarm is open and the PR's file list cannot be read, the gate refuses, even for a ledger PR. That case needs Issues enabled and a failing pulls/N/files call at the same time.

…pen alarms

A PR that GITHUB_TOKEN opens or pushes gets pull_request runs held for
approval (#240 sat with CI action_required), while a workflow_dispatch run
needs none. ci.yml now takes workflow_dispatch, and hack/bot-ci-dispatch runs
it on each same-repo autobump/* branch opened by github-actions[bot] that has
no run at its head, re-running a first-attempt failure once. It keeps one bot
ci.yml run in flight (BOT_CI_MAX_INFLIGHT) and defers while a human PR's CI is
queued, since one run is about 39 jobs against a 20-40 job pool. The three
nightly PR jobs call it after create-pull-request, and the new hourly 'Bot
sweep' workflow (job ci-fill) fills in what they deferred.

hack/autobump-branch-reset generalises the digest job's stale-branch step to
all three PR jobs: a bump branch whose workflow files differ from main's is
deleted before create-pull-request, whose force-push GitHub would refuse.

The merge gate's checks move to hack/gate_lib.sh. ci_status reads ci.yml at a
SHA through a pull_request run or a dispatch run on the PR branch; once the
dispatched run is green, held entries of 'CI' and 'Auto-merge Dependabot PRs'
no longer block. base_ok ignores release-only bot commits on main, so the
three release commits per launcher release no longer force a sync. The gate
prints every open ci-alarm issue and refuses (exit 4) a PR that changes
.github/ or shipped paths while one is not acked; ledger, docs, bench and test
PRs are never refused.

ci.yml gains a last job, alarms, which warns on every open ci-alarm issue and
on a missing, disabled or quiet Watchdog workflow, and never fails.

dependabot-auto-merge.yml is deleted: it ran gh pr merge --auto on an
unprotected main, merged nothing in 500 runs, and held a run on every bot PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant