Skip to content

feat(gate): configurable advisory check-runs so a stuck external status never freezes the gate (#4372) - #5793

Merged
JSONbored merged 2 commits into
JSONbored:mainfrom
nickmopen:feat/advisory-check-runs
Jul 15, 2026
Merged

feat(gate): configurable advisory check-runs so a stuck external status never freezes the gate (#4372)#5793
JSONbored merged 2 commits into
JSONbored:mainfrom
nickmopen:feat/advisory-check-runs

Conversation

@nickmopen

Copy link
Copy Markdown
Contributor

Summary

Adds a per-repo gate.advisoryCheckRuns config field so a self-host operator can declare any third-party check-run — a security scanner, a contributor-trust analyzer, a license/CLA bot — as advisory. Fixes #4372: a durable, non-standard check conclusion (most commonly a terminal action_required that only a human can clear in the app's own UI) currently freezes gittensory's gate — the check never turns green, so the PR is held pending forever with no review, no label, no signal to the maintainer.

A listed check-run (matched by name and trusted producing app slug — a name-only match is spoofable and ignored, exactly like cla.checkRunAppSlug):

  1. Never gates CI and never counts as "still running." A matched, completed run is excluded from the live-CI aggregate entirely, the same way bot-owned checks already are — so it can never block or stall the gate.
  2. Is never silently swallowed. A non-passing conclusion (anything other than success/neutral/skipped) routes the PR to the existing manual-review hold with the triggering check/app/conclusion named in the label reason and public comment, so a maintainer can act on the signal their installed app raised.

Entirely config-driven and generic — no vendor name is hardcoded in any behavior-triggering path; the list comes wholly from .loopover.yml. Empty/omitted ⇒ byte-identical behavior for every repo that doesn't opt in.

Supersedes the closed #5780 (same feature, same approach), which the gate auto-closed purely because its CI was red (codecov/patch + validate-tests). This PR is green: full config-as-code parity, every changed line and branch covered, and the whole test:coverage + test:ci gate passing locally.

What's wired (config-as-code parity — all in this PR)

  • Schema/parse (packages/loopover-engine/src/focus-manifest.ts): FocusManifestGateConfig.advisoryCheckRuns, a normalizeOptionalAdvisoryCheckRuns validator (list of { name, appSlug }, both required — appSlug mandatory because name-only matching is spoofable — capped at 16, invalid entries dropped with warnings), default null, the any-gate-configured check, and the gateConfigToJson round-trip. Type mirror in predicted-gate-types.ts.
  • Resolved settings: RepositorySettings.advisoryCheckRuns (src/types.ts + the engine manifest-deps-types.ts mirror); resolver merge in src/signals/focus-manifest.ts.
  • OpenAPI: src/openapi/schemas.ts zod field + regenerated apps/loopover-ui/public/openapi.json.
  • CI aggregate (src/github/backfill.ts): a spoof-resistant matchAdvisoryCheckRun matcher; the reducer excludes a matched run from ciState/hasPending and, when completed non-passing, records it in a new advisoryHoldDetails field. Threaded through both the REST and GraphQL aggregate paths so they stay byte-identical.
  • Disposition (src/settings/agent-actions.ts): advisoryCheckHold folds into heldForManualReview (never a close), with the check/app surfaced in the manual-review label's reason + comment.
  • Threading: the config flows settings → ci-resolution.ts (4 entry points) → the aggregate, and is folded into both the request-memo and durable cache keys so a config change never serves a stale aggregate. The executor + approval-queue pre-merge re-checks apply the same exclusion, so planning and execution can't disagree.
  • Docs: .loopover.yml.example + the byte-identical config/examples/loopover.full.yml twin.

Scope

  • Conventional Commit title.
  • Focused: one coherent feature (per-repo advisory-check-runs), no unrelated changes.
  • Follows CONTRIBUTING.md; no site//CNAME/lovable.
  • Closes #4372.

Validation

  • git diff --check
  • npm run typecheck (0 errors, engine rebuilt)
  • npm run test:coverage — full suite green; every changed src/** + packages/loopover-engine/src/** line and branch covered (new dedicated tests below).
  • npm run docs:drift-check, config-template parity, focus-manifest exhaustiveness — all green.
  • npm run ui:openapi:check (regenerated artifact committed).
  • npm run test:workers / UI checks — the change touches no Worker binding or apps/loopover-ui code; the OpenAPI artifact is regenerated and committed.

New tests: engine parse (valid/round-trip/resolve/cap/drop-missing-fields), CI-aggregate exclusion (completed non-pass → held; completed pass → no hold; in-progress → excluded; wrong app slug → not advisory; unconfigured → unchanged), the manual-review routing (holds, names the check, never closes, empty-array merges), and the cache-key fingerprint (config change → refetch; order-independent). All use synthetic app/check names, never a real vendor.

Safety

  • No secrets/wallets/trust-scores/reward terms anywhere; the surfaced hold text is only a public check name + app slug + conclusion.
  • Public comment stays sanitized (sanitizePublicComment), no compensation/optimization language.
  • Auth/CORS/session — N/A.
  • API/OpenAPI updated + tested.
  • UI Evidence — N/A (backend config + gate behavior; no apps/loopover-ui change).

Notes — requirement #4 (zero-required-contexts isRequired()) is scoped out with rationale

#4372's requirement #4 asks to also fix the general case where a repo with no branch-protection required contexts treats every check as implicitly required. This PR deliberately does not change that global isRequired() behavior, for two reasons: (a) for the exact repos in the issue's evidence, a configured advisory check is now excluded entirely before isRequired() is ever consulted, so this feature already resolves their symptom; (b) changing the fold-all default for non-advisory checks is a separate, higher-risk behavior change to the CI hot path that deserves its own PR and test matrix rather than being bundled here. Flagging it explicitly here (not dropping it) so it can be tracked as a scoped follow-up: the general isRequired() behavior for repos with zero configured required contexts is unchanged by this PR and remains open for a dedicated change.

Closes #4372

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@682ba5d). Learn more about missing BASE report.
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5793   +/-   ##
=======================================
  Coverage        ?   95.15%           
=======================================
  Files           ?      589           
  Lines           ?    46686           
  Branches        ?    14915           
=======================================
  Hits            ?    44424           
  Misses          ?     1511           
  Partials        ?      751           
Flag Coverage Δ
shard-1 43.80% <18.96%> (?)
shard-2 36.09% <48.27%> (?)
shard-3 32.07% <10.34%> (?)
shard-4 32.86% <24.13%> (?)
shard-5 31.51% <1.72%> (?)
shard-6 44.55% <63.79%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-engine/src/focus-manifest.ts 99.11% <100.00%> (ø)
src/github/backfill.ts 96.39% <100.00%> (ø)
src/openapi/schemas.ts 100.00% <ø> (ø)
src/queue/ci-resolution.ts 98.55% <100.00%> (ø)
src/queue/processors.ts 95.80% <ø> (ø)
src/services/agent-action-executor.ts 96.95% <100.00%> (ø)
src/services/agent-approval-queue.ts 99.17% <100.00%> (ø)
src/settings/agent-actions.ts 97.78% <100.00%> (ø)
src/signals/focus-manifest.ts 99.65% <100.00%> (ø)
src/types.ts 100.00% <ø> (ø)

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 14, 2026
@loopover-orb

loopover-orb Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-15 02:18:56 UTC

28 files · 1 AI reviewer · no blockers · readiness 75/100 · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR adds a per-repo `gate.advisoryCheckRuns` config (name+appSlug matched, mirroring the CLA check-run spoof-resistance pattern) that excludes a matched, completed check-run from the live-CI aggregate entirely and, if its conclusion is non-passing, routes the PR to the existing manual-review hold instead of freezing the gate forever. The wiring is threaded consistently end-to-end — schema/parse, RepositorySettings (both duplicated type locations), openapi.json/zod schema, the cache-key fingerprint in ci-resolution.ts (order-independent, folded into both the durable and request-scoped keys), the disposition planner, and the executor's step-8 re-verify — and is exercised by thorough tests covering spoof-mismatch, missing-appSlug, in-progress, no-conclusion, and no-status edge cases. The one design tradeoff worth a second look is `deserializeCachedCiAggregate` always returning `advisoryHoldDetails: []` since that field isn't persisted — the PR's own comment explains this relies on webhook-driven cache invalidation firing the fresh live read that carries the hold, which is plausible given existing invalidation infra but isn't verifiable from this diff alone.

Nits — 5 non-blocking
  • src/github/backfill.ts: `deserializeCachedCiAggregate` unconditionally returns `advisoryHoldDetails: []`, relying entirely on webhook-driven cache invalidation to ever surface a hold on a cache-hit path — worth an explicit test or confirmation that check_run completion from an advisory app actually invalidates the durable cache entry for that head SHA.
  • packages/loopover-engine/src/focus-manifest.ts: `normalizeOptionalAdvisoryCheckRuns` reads `name`/`appSlug` as raw trimmed strings instead of running them through `parsePublicSafeText` like `claCheckRunName`/`claCheckRunAppSlug` — minor inconsistency with the sibling spoof-resistant field it explicitly mirrors.
  • packages/loopover-engine/src/focus-manifest.ts:1297 `MAX_ADVISORY_CHECK_RUNS = 16` is already named (the brief's 'magic number' flag on the `feat(gate): configurable advisory check-runs so a non-resolving external status never blocks or stalls the gate forever #4372` issue-reference comment is a false positive).
  • The long-file/deep-nesting smells flagged externally (backfill.ts, processors.ts, focus-manifest.ts, etc.) are pre-existing file sizes, not something this diff meaningfully worsens — not actionable here.
  • Add (or point to) a test that exercises a cache HIT scenario for a PR whose advisory check completed non-passing, confirming the manual-review hold still fires via the fresh invalidated read rather than silently no-opping on a stale cache entry.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #4372, #5780
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ❌ 5/25 Preflight is holding this PR: the review lane is unavailable, so it is not ready for automated review.
Contributor workload ✅ 10/10 Author activity: 142 registered-repo PR(s), 87 merged, 9 issue(s).
Contributor context ✅ Confirmed Gittensor contributor nickmopen; Gittensor profile; 142 PR(s), 9 issue(s).
Gate result ✅ Passing No configured blocker found.
Improvement ✅ Minor risk: clean · value: minor — Code changes are accompanied by test evidence.
Review context
  • Author: nickmopen
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 142 PR(s), 9 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Await review-lane availability.
  • Triage stale or unlinked PRs.
  • Refresh registry data or choose a registered active repo.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
[BETA] Chat with Gittensory

Ask Gittensory a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @gittensory ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @gittensory chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @gittensory mention with a real question is routed to the closest matching read-only command automatically -- no exact syntax required.

Full command reference: https://gittensory.aethereal.dev/docs/gittensory-commands

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 14, 2026
@JSONbored
JSONbored marked this pull request as draft July 14, 2026 22:28
…us never freezes the gate (JSONbored#4372)

Adds a per-repo `gate.advisoryCheckRuns` field: a list of `{ name, appSlug }`
third-party check-runs (a security scanner, a contributor-trust analyzer, a
CLA bot) to treat as advisory. A durable non-standard conclusion (most often a
terminal `action_required` only a human can clear in that app's own UI) today
freezes gittensory's gate forever — the check never turns green, so the PR is
held pending with no review, label, or signal.

A listed check-run — matched by name AND the trusted producing app slug, the
same spoof-resistant pattern as `cla.checkRunAppSlug` — is excluded from the CI
aggregate entirely (never gates pass/fail, never counts as "still running"), so
it can never block or stall the gate. It is not silently swallowed either: a
non-passing conclusion routes the PR to the existing manual-review hold with the
triggering check/app named in the label reason and public comment.

Config-driven and generic: no vendor name is hardcoded in any behavior-triggering
path. Empty/omitted list is byte-identical to today for every repo that doesn't
opt in. Wires the field through the engine parser, resolved settings, resolver,
OpenAPI, the CI aggregate (REST + GraphQL paths) and its cache keys, the
disposition planner, and the executor/approval-queue re-checks, with docs in both
example configs and full line+branch test coverage.
…-end (JSONbored#4372)

Address the review's one flagged design tradeoff: advisoryHoldDetails is not a
persisted cache column, so a durable cache HIT reconstructs it as []. Add a test
pinning both halves of why that is safe — deserialize returns [] on a hit, and
invalidateCiStateCache (called app-agnostically by an advisory check_run's own
`completed` webhook, before re-review) clears the entry so the next disposition
read misses and re-fetches live, where the hold is re-derived. Tighten the
deserialize comment to cite the exact invalidation handler and covering tests, so
the guarantee is verifiable from the diff rather than asserted only in prose.
@JSONbored
JSONbored force-pushed the feat/advisory-check-runs branch from cea71bf to 1b33341 Compare July 14, 2026 23:59
@JSONbored
JSONbored marked this pull request as ready for review July 15, 2026 02:03
@JSONbored
JSONbored merged commit 2f0e6d2 into JSONbored:main Jul 15, 2026
20 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gate): configurable advisory check-runs so a non-resolving external status never blocks or stalls the gate forever

2 participants