Skip to content

ci(OMN-14127): run required-context CI aggregators (CI Summary / Security Gate) on GitHub-hosted runners so they never wedge under self-hosted saturation - #1870

Merged
jonahgabriel merged 19 commits into
devfrom
jonah/omn-14127-ci-summary-hosted-aggregator
Jul 9, 2026
Merged

jonahgabriel merged 19 commits into
devfrom
jonah/omn-14127-ci-summary-hosted-aggregator

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the permanent-wedge class in OMN-14127: the required branch-protection contexts CI Summary and Security Gate never post any check-run under self-hosted fleet saturation, leaving PRs BLOCKED forever with 0 failing / 0 pending checks and no auto-recovery.

Closes OMN-14127 (workflow-structure half; runner-fleet capacity half is OMN-13932 / OMN-14080).

Verified root cause (evidence, not assumption)

A needs-gated job gets no GitHub check-run at all until its needs reach a terminal state. Confirmed live:

Note: the ticket title says CI Summary/Tests Gate, but the live required contexts on dev branch protection are CI Summary and Security Gate (Tests Gate is not required). Verified via gh api repos/OmniNode-ai/omniclaude/branches/dev/protection/required_status_checks.

Fix — relocate the result-reading aggregation layer to GitHub-hosted ubuntu-latest

These jobs do only static / result-reading work (echo + needs.*.result) with no LAN/service dependency, so they never need a scarce self-hosted slot. Moving them off the fleet means they get a runner immediately and reach a terminal state as soon as their needs resolve — instead of stalling in the queue and being cancelled.

Job name: (context) Why moved
ci-summary CI Summary (required) Emits the required context; hosted + if: always() → always terminal once its gate needs resolve
security-gate Security Gate (required) Emits the required context; whole subtree now hosted
security-python Python Security Scan Security Gate leaf — pure bandit static scan, hosted-safe → makes the Security Gate subtree fully starvation-immune
detect-secrets Secret Detection Security Gate leaf — pure detect-secrets static scan, hosted-safe
quality-gate Quality Gate Feeds CI Summary
tests-gate Tests Gate Feeds CI Summary
  • No context/job name changed (byte-for-byte). A rename would itself name-mismatch branch protection and wedge every PR.
  • if: always() preserved on all gate aggregators; verdict logic untouched.
  • Heavy jobs are NOT moved — test (matrix), hooks-tests, golden-chain-live, the 35 quality leaves, etc. stay on the self-hosted fleet. Only the lightweight aggregation layer + the two static security leaves move.
  • No branch-protection / required_status_checks change in this PR (operator-gated step 2 — see below).

Residual (honest scope boundary)

CI Summary still needs the self-hosted quality/tests leaf subtree. If those leaves sit queued indefinitely (never even cancel), quality-gate/tests-gate — and therefore CI Summary — still cannot instantiate. That is the runner-fleet capacity problem (OMN-13932 / OMN-14080), not a workflow-structure problem. This PR fully removes the aggregation-layer stall (the observed failure in the ticket trace: gates sitting queued and getting cancelled) and makes Security Gate fully starvation-immune (its entire subtree is now hosted). Full CI Summary starvation-immunity would require either moving its whole leaf subtree hosted or converting it to a no-needs poller — flagged as a possible fast-follow, not done here.

Local verification

  • python3 -c "import yaml; yaml.safe_load(...)" → parses, 60 jobs.
  • actionlint .github/workflows/ci.yml → 5 findings, identical to origin/dev baseline (all pre-existing shellcheck style/info in untouched run: blocks) — zero new findings.
  • pre-commit run --files .github/workflows/ci.yml → exit 0, 0 failures (incl. ban-hardcoded-runner-ip, concurrency-primitive-guard, self-gating-workflows).
  • pre-push hooks (mypy / pyright / ONEX validation / exports) → passed.

Evidence

Summary by CodeRabbit

  • Bug Fixes
    • Updated multiple CI jobs to run on GitHub-hosted runners to improve reliability when self-hosted capacity is constrained.
    • Adjusted CI gate result aggregation to consistently use a hosted environment.
    • Reworked CI summary evaluation to use fail-closed polling with bounded time, producing more accurate success/pending/failure verdicts.
  • Tests
    • Added unit and CLI tests covering the new CI summary verdict logic and run-attempt filtering.
  • Chores
    • Increased HTTP timeout for the no-faked-boundary workflow to improve stability.

Evidence-Ticket: OMN-14127
Evidence-Source: OCC#3722

…s on GitHub-hosted runners

The required branch-protection contexts "CI Summary" and "Security Gate"
are emitted by needs-gated jobs in ci.yml. A needs-gated job gets no
GitHub check-run until its needs reach a terminal state. Under self-hosted
(omnibase-ci) fleet saturation the aggregators either (a) sat queued on the
fleet until the whole run was cancelled — posting no terminal status — or
(b) were never instantiated because an upstream never terminalized. Either
way the required context is ABSENT and the PR wedges BLOCKED forever with
0 failing / 0 pending checks and no auto-recovery (verified: PR #1864,
run 28839961159; recurrence on #1867/#1863/#1868).

Move the result-reading aggregation layer off the self-hosted fleet onto
GitHub-hosted ubuntu-latest so it never queues behind saturation:
  - ci-summary       (required "CI Summary")
  - security-gate    (required "Security Gate")
  - quality-gate, tests-gate  (feed CI Summary)
  - security-python, detect-secrets  (Security Gate leaves; pure static
    scans (bandit / detect-secrets) with no self-hosted/LAN dependency —
    makes the whole Security Gate subtree hosted so that required context
    is starvation-immune)

These jobs do only static/result-reading work (echo + needs.*.result),
so hosting them carries no LAN/service dependency. `if: always()` and every
job name are unchanged byte-for-byte (a rename would itself name-mismatch
branch protection and wedge every PR).

Live required contexts on dev branch protection are "CI Summary" and
"Security Gate" (verified via the protection API) — the ticket title's
"Tests Gate" is not a required context, but it is moved too since it feeds
CI Summary. This is the workflow-structure half of the fix; the
runner-fleet capacity half is tracked under OMN-13932 / OMN-14080.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40b712a9-78d6-4a80-817e-cfd03fbb5f2e

📥 Commits

Reviewing files that changed from the base of the PR and between a63d1c6 and e208567.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • .github/workflows/ci.yml
  • .github/workflows/cr-thread-gate.yml
  • .github/workflows/no-faked-boundary.yml
  • .github/workflows/onex-schema-compat.yml
  • .github/workflows/security-scan.yml
  • pyproject.toml
  • scripts/check-unresolved-threads.sh
  • scripts/ci/ci_summary_gate.py
  • scripts/ci/run_golden_chain_live.py
  • scripts/materialize-dod-evidence-from-occ.sh
  • tests/ci/test_ci_summary_gate.py
  • tests/ci/test_dependency_shape.py
  • tests/ci/test_golden_chain_live_runner.py
  • tests/hooks/test_ci_postgres_workflow.py
📝 Walkthrough

Walkthrough

CI jobs now run on fixed ubuntu-latest runners for security and gate checks, and ci-summary becomes a fail-closed poller backed by a new gate-evaluation script, tests, and a workflow timeout setting.

Changes

CI orchestration updates

Layer / File(s) Summary
Security job runners
.github/workflows/ci.yml
security-python and detect-secrets switch from conditional runner selection to fixed ubuntu-latest runners.
Gate job runners
.github/workflows/ci.yml
quality-gate, tests-gate, and security-gate switch from conditional runner selection to fixed ubuntu-latest runners.
CI Summary poller
.github/workflows/ci.yml
ci-summary runs without needs, polls the current workflow run for job status, and loops on pending verdicts until it reaches a terminal result or deadline.
Gate script and tests
scripts/ci/ci_summary_gate.py, tests/ci/test_ci_summary_gate.py
The gate script adds job normalization, verdict evaluation, reporting, JSON loading, and CLI handling; tests cover verdict rules and CLI exit codes.
Workflow timeout env
.github/workflows/no-faked-boundary.yml
A workflow-level UV_HTTP_TIMEOUT environment variable is added.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: moving required CI aggregation contexts to GitHub-hosted runners to avoid self-hosted saturation wedges.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonah/omn-14127-ci-summary-hosted-aggregator

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

2853-2903: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

ci-summary can still wedge behind self-hosted jobs

compliance and contract-compliance still default to ["self-hosted","omnibase-ci"] on trusted CI, so ci-summary can still fail to reach a terminal state through its needs chain. If this check is required for branch protection, the same saturation wedge remains unless those jobs move off the self-hosted fleet or are removed from the blocking dependency set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 2853 - 2903, The ci-summary aggregator
can still be blocked by self-hosted jobs because it waits on compliance and
contract-compliance, which default to self-hosted runners on trusted CI. Update
the ci-summary job in the workflow so it no longer depends on blocking
self-hosted jobs, either by moving compliance and contract-compliance onto a
GitHub-hosted runner or by removing them from ci-summary’s needs list if they
are not required for the branch-protection context. Keep the required
terminal-status behavior aligned with the other gate jobs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 2853-2903: The ci-summary aggregator can still be blocked by
self-hosted jobs because it waits on compliance and contract-compliance, which
default to self-hosted runners on trusted CI. Update the ci-summary job in the
workflow so it no longer depends on blocking self-hosted jobs, either by moving
compliance and contract-compliance onto a GitHub-hosted runner or by removing
them from ci-summary’s needs list if they are not required for the
branch-protection context. Keep the required terminal-status behavior aligned
with the other gate jobs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 834d0584-f79e-485c-95f2-3831d81d5c84

📥 Commits

Reviewing files that changed from the base of the PR and between a677c8f and 62a55ad.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

…starvation-immune)

Operator-approved follow-up to the hosted-aggregator change. Converts the
`ci-summary` job from a `needs`-gated aggregator into a NO-`needs`, GitHub-hosted,
fail-closed poller so the required `CI Summary` context is immune to self-hosted
fleet saturation.

Why: a `needs`-gated job gets no GitHub check-run until its `needs` reach a
terminal state. Under saturation the gate jobs never terminalized, so the old
needs-based `CI Summary` was ABSENT forever (PR wedged BLOCKED, no auto-recovery).
With NO `needs` the poller's check-run instantiates immediately — the required
context can never be absent — and it always posts a terminal success/failure
within a bounded deadline.

Design (fail-closed, default-deny):
- `ci-summary`: runs-on ubuntu-latest, `if: always()`, NO `needs`, permissions
  actions:read + contents:read, timeout 100m. Polls
  repos/.../runs/<id>/jobs?filter=all every 45s up to a 90m deadline (under the
  observed ~167m run-cancel window) and calls scripts/ci/ci_summary_gate.py.
- scripts/ci/ci_summary_gate.py: pure-stdlib verdict. SUCCESS only when (a) the
  default-deny sweep finds no present+completed non-allowlisted job with a
  conclusion outside {success,skipped}, AND (b) every aggregate gate
  (Quality/Tests/Security Gate + Contract Compliance[ Check]) is present+
  completed+good. Gating on the aggregate gates (themselves if:always fail-closed
  aggregators over their leaves) is what prevents a FALSE GREEN before late-created
  jobs (detect-changes → test → *-gate) are even instantiated. Missing/pending
  gate → PENDING → poll again; deadline → FAILURE (fail-closed). Latest-attempt
  dedup handles `gh run rerun --failed`. Exit 0/1/2 = success/fail/pending.
- tests/ci/test_ci_summary_gate.py: 19 unit tests pinning the fail-closed verdict
  (gate failure/cancel/absent, leaf-failure-before-gates, allowlist ignored,
  self excluded, partial-rerun dedup, empty-run-not-vacuous-green, CLI exit codes).

The name `CI Summary` is unchanged byte-for-byte (it IS the required context).
No branch-protection change. Pure hosted API polling — zero self-hosted/LAN dep.
…2) does not kill the loop

GitHub runs the step as `bash -e {0}`; the gate script's exit 2 (PENDING) was
terminating the step on the first poll instead of continuing to poll. `set +e`
lets the explicit case-statement handle 0/1/2. (Poller already proven to
instantiate immediately on ubuntu-latest and post a terminal status.)
@jonahgabriel

Copy link
Copy Markdown
Contributor Author

Update — fail-closed CI Summary poller added (operator-approved)

Follow-up commits convert the ci-summary job from a needs-gated aggregator into a NO-needs, GitHub-hosted, fail-closed poller, so the required CI Summary context is now fully starvation-immune (not just the aggregation layer).

Why the earlier (hosted-aggregator) change wasn't sufficient for CI Summary

A needs-gated job gets no check-run created until its needs reach a terminal state. Under saturation the self-hosted leaves never terminalized, so the hosted-but-needs-gated ci-summary was still absent. Only removing needs makes the check-run instantiate immediately.

Design (scripts/ci/ci_summary_gate.py + workflow loop)

  • ci-summary: runs-on: ubuntu-latest, if: always(), NO needs, permissions: actions:read, contents:read, timeout-minutes: 100. Polls repos/.../runs/<id>/jobs?filter=all every 45s up to a 90-minute deadline (comfortably under the observed ~167m run-cancel window), calling the gate script each cycle.
  • Fail-closed / default-deny verdict (scripts/ci/ci_summary_gate.py, exit 0/1/2 = success/fail/pending):
    1. Default-deny failure sweep — any present + completed job whose conclusion ∉ {success,skipped} FAILS the gate, except the poller itself and an explicit soft-allowlist.
    2. Completeness anchor — success additionally requires every aggregate gate present+completed+good. Gating on the aggregate gates (themselves if: always() fail-closed aggregators over their leaves) is what prevents a false green before late-created jobs (detect-changes→test→*-gate) are even instantiated.
    • Missing/pending gate → PENDING (poll again); deadline → FAILURE. Latest-attempt dedup handles gh run rerun --failed.
  • Gating set (aggregate gates): Quality Gate, Tests Gate, Security Gate, Contract Compliance Check, Contract Compliance.
  • Soft-allowlist (jobs already non-gating in ci.yml — sweep ignores): Build Docker Image, Deploy to Staging, Deploy to Production, Merge Test Coverage, Markdown Link Check, Cross-Repo Boundary Parity, DoD Evidence Check, AI-Slop Pattern Check (strict, PR diff).
  • Unit-tested: tests/ci/test_ci_summary_gate.py — 19 tests (gate failure/cancel/absent, leaf-failure-before-gates, allowlist ignored, self excluded, partial-rerun dedup, empty-run-not-vacuous-green, CLI exit codes).

Proof on this PR (under live self-hosted saturation — overall CI run queued)

  • CI Summary check-run instantiates immediately (no longer ABSENT); observed in_progress (polling) while the fleet is saturated, and in an earlier revision it posted a terminal status in seconds — it can never be pending-forever.
  • Security Gate posts terminal pass in ~2s (whole subtree hosted).

Name CI Summary unchanged byte-for-byte. No branch-protection change. Pure hosted GitHub-API polling — zero self-hosted/LAN dependency.

@github-actions

github-actions Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Blocking findings (critical/error): 0
Total findings: 0
Models succeeded: none

Note: All reviewer models failed or were unavailable. Degraded results are informational during the pilot phase (OMN-8524) and do not block merge. Error: cli_review exited non-zero (all models failed or binary error)


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded All models unavailable (infra) No (pilot)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

The prior run_id (28899626848) is permanently un-greenable: after the
self-hosted fleet cancelled attempt 1, the no-needs CI Summary poller
(scripts/ci/ci_summary_gate.py over runs/{id}/jobs?filter=all) keeps
seeing prior-attempt cancelled jobs that have not yet re-instantiated in
the new attempt and fails closed at ~15-21s on every rerun. A fresh
run_id (run_attempt=1) has no stale cancelled jobs to poison the
default-deny sweep. No source change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 2901-2907: The job-fetch retry path in the CI workflow skips the
internal deadline check because it `continue`s immediately after a failed `gh
api`/`jq` fetch. Update the retry loop around the `jobs.json` fetch so the
90-minute deadline is evaluated even when the fetch fails, either by checking
the deadline before each retry `continue` or by routing the failure branch
through the same deadline/timeout handling used later in the loop. Keep the
behavior tied to the existing `attempt`, `POLL_INTERVAL_SECONDS`, and
`jobs.json` fetch logic.

In `@scripts/ci/ci_summary_gate.py`:
- Around line 124-157: The default-deny sweep in evaluate() is losing failures
because dedup_latest() collapses same-name rows for the same run_attempt before
the sweep runs. Update the logic so current-attempt duplicate job rows are
preserved for the sweep, or perform the failure scan on the raw jobs list while
still using dedup_latest() only where a collapsed view is actually needed.
Reference dedup_latest(), evaluate(), and sweep_failures when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1d673d8-33a8-4d45-9650-acc33a50fede

📥 Commits

Reviewing files that changed from the base of the PR and between 62a55ad and a63d1c6.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • .github/workflows/no-faked-boundary.yml
  • scripts/ci/ci_summary_gate.py
  • tests/ci/test_ci_summary_gate.py
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/no-faked-boundary.yml

Comment thread .github/workflows/ci.yml
Comment thread scripts/ci/ci_summary_gate.py Outdated
@jonahgabriel
jonahgabriel disabled auto-merge July 8, 2026 17:00
@jonahgabriel
jonahgabriel merged commit 93918aa into dev Jul 9, 2026
100 of 102 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-14127-ci-summary-hosted-aggregator branch July 9, 2026 10:32
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