Skip to content

feat(agent-actions): cancel in-flight CI runs when a PR is auto-closed for the contributor cap - #2662

Merged
JSONbored merged 7 commits into
mainfrom
feat/cancel-ci-on-contributor-cap
Jul 3, 2026
Merged

feat(agent-actions): cancel in-flight CI runs when a PR is auto-closed for the contributor cap#2662
JSONbored merged 7 commits into
mainfrom
feat/cancel-ci-on-contributor-cap

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Closing a PR for exceeding the per-contributor open-item cap (feat(agent-actions): configurable per-contributor open PR/issue cap with auto-close over the limit #2270) currently leaves its in-flight GitHub Actions runs burning CI minutes for nothing — nothing in this codebase calls the Actions run-cancel endpoint today.
  • Adds an opt-in contributorCapCancelCi setting (per-repo .gittensory.yml/DB, or an install-wide CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var fallback for repos that haven't configured their own value) that, right after a closeKind: "contributor_cap" close succeeds, lists and cancels that PR's in-progress/queued workflow runs at its head SHA.
  • New cancelInFlightWorkflowRunsForHeadSha GitHub REST wrapper (src/github/app.ts) — greenfield, no existing Actions-API wrapper to extend. Never throws; returns a typed {kind: "cancelled" | "permission_missing" | "error"} result so a missing scope or transient failure can never retroactively turn an already-successful close into a recorded "error".
  • Requires the actions: write App permission, added to the self-host setup-wizard manifest. A pre-existing installation must explicitly re-approve this permission bump before cancellation works for them — GitHub does not silently grant it. Until they do (or if they never do), the feature degrades gracefully: the cancellation attempt is skipped and logged via a dedicated github_app.contributor_cap_ci_cancel_permission_missing audit event, and the close itself is completely unaffected. Added a new "Re-approving a permission bump on an existing App" docs section with the exact re-approval steps.
  • Off by default; zero behavior change for any repo/install until it explicitly opts in.

Closes #2462.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes. (Touches more files than usual because this is one cohesive feature: the config-as-code chain, the REST wrapper, the executor hook, and the App-manifest permission bump + its docs are not independently mergeable — landing them separately would leave the feature half-wired at every intermediate step.)
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked an issue, or this is small enough that the summary explains why an issue is not needed.

Validation

  • git diff --check
  • npm run actionlint (via npm run test:ci)
  • npm run db:migrations:check — migration 0097, contiguous, no new duplicates
  • npm run typecheck
  • npm run test:coverage locally — full branch coverage confirmed on every changed file via a manual lcov diff-range parse (not just the aggregate %). Tests cover: cancellation with in_progress + queued runs, zero-runs, a 409 (already-cancelling) treated as a non-failure, a genuine 403 while listing vs. while cancelling, a rate-limited 403 excluded from permission-missing on both the list AND cancel calls, a non-permission cancel failure silently skipped (not counted, not failed), a network/non-Error throw, a malformed repo name, a 403/list-error body with no message field (exercises the ||/?? fallback sides), the executor hook enabled/disabled/permission-degraded/env-default/repo-override, and the .gittensory.yml settings: parsing (set/clear-via-null/omit/invalid). Also mutation-tested the core "close still succeeds even if the cancel hook fails" isolation property by temporarily making the hook throw and confirming the close-audit outcome degraded from "completed" to "error" as expected — then restored.
  • npm run test:workers (via npm run test:ci)
  • npm run build:mcp (via npm run test:ci)
  • npm run test:mcp-pack (via npm run test:ci)
  • npm run ui:openapi:check — regenerated and committed apps/gittensory-ui/public/openapi.json
  • npm run ui:lint (via npm run test:ci)
  • npm run ui:typecheck (via npm run test:ci)
  • npm run ui:build (via npm run test:ci)
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — This DOES touch the GitHub App's permission manifest, so flagging explicitly: the negative path (permission not granted) is covered by 4 dedicated tests (unit-level 403 handling + an integration test asserting the close still succeeds and only a permission_missing audit is recorded, never a close failure).
  • API/OpenAPI/MCP behavior is updated and tested where needed. — contributorCapCancelCi added to the RepositorySettings OpenAPI schema and regenerated.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, docs-only UI change (no data-driven UI).
  • Visible UI changes include a UI Evidence section below with screenshots. — N/A, docs prose only, no visual/interactive UI change.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs. — .gittensory.yml.example, .env.example, and docs.self-hosting-github-app.tsx (new permission list entry + a new re-approval-steps section) all updated; the manifest↔docs parity test (setup-wizard-docs-parity.test.ts) passes.

Notes

  • Sixth of a batch of independent maintainer-only roadmap items (chore(selfhost): beta-stable release readiness roadmap #1936) being worked one PR at a time.
  • This is the only PR in the batch so far that widens the GitHub App's permission scope — flagging that explicitly since it's operationally more significant than a typical settings-field addition. The feature is inert until BOTH a repo opts in AND the App's actions: write grant is re-approved on the specific installation, so there is no forced-upgrade behavior change for anyone who doesn't take both actions.

@dosubot dosubot Bot added the size:L label Jul 3, 2026
@loopover-orb

loopover-orb Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Warning

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

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-03 05:23:36 UTC

20 files · 1 AI reviewer · 1 blocker · readiness 100/100 · CI green · dirty

⏸️ Suggested Action - Manual Review

  • AI reviewers agree on a likely critical defect: src/github/app.ts listWorkflowRunIdsForStatus only reads the default first page from /actions/runs, so any PR head SHA with more than GitHub's default page of matching queued or in-progress runs will leave page-2+ runs uncancelled while cancelInFlightWorkflowRunsForHeadSha reports totalFound and cancelledCount from only the partial page. — Resolve the flagged defect, or override if the AI reviewers are mistaken, then re-run the gate.

Review summary
The change wires an opt-in contributor-cap CI cancellation path through the GitHub wrapper and executor, and the failure shape is mostly right: cancellation happens after the close succeeds and non-permission failures are surfaced separately. The main correctness gap is that the Actions list call only reads GitHub's first page of workflow runs, so high-workflow repos can still leave queued or running jobs behind while the audit reports a complete cancellation.

Blockers

  • src/github/app.ts listWorkflowRunIdsForStatus only reads the default first page from /actions/runs, so any PR head SHA with more than GitHub's default page of matching queued or in-progress runs will leave page-2+ runs uncancelled while cancelInFlightWorkflowRunsForHeadSha reports totalFound and cancelledCount from only the partial page.
Nits — 7 non-blocking
  • nit: src/github/app.ts isActionsPermissionMissingMessage treats an empty 403 response body as a missing Actions permission, which can misclassify an unknown GitHub-side 403 as a permanent permission gap instead of an operational error.
  • nit: src/services/agent-action-executor.ts recordContributorCapCiCancelOutcome logs permission_missing at error level even though the feature is documented as a graceful degradation for existing installations that have not re-approved the permission bump.
  • nit: apps/gittensory-ui/public/openapi.json contains broad unrelated generated churn for public/orb/focus-manifest routes, which makes the actual contributorCapCancelCi API change harder to audit in this PR.
  • In src/github/app.ts, request per_page=100 and follow Link pagination for each status until exhausted before returning ids, with a unit test covering more than one page of queued or in-progress runs.
  • In src/github/app.ts, classify an empty-message 403 as an error unless GitHub returns a known permission-denied message, or include status/body context in the audit so operators can distinguish permission gaps from transient GitHub failures.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • AI maintainer-assist flagged possible low-effort patterns (elevated) — Advisory only — review the noted patterns; this AI assist never blocks the gate.

Concerns raised — review before merging

  • src/github/app.ts listWorkflowRunIdsForStatus only reads the default first page from /actions/runs, so any PR head SHA with more than GitHub's default page of matching queued or in-progress runs will leave page-2+ runs uncancelled while cancelInFlightWorkflowRunsForHeadSha reports totalFound and cancelledCount from only the partial page.
Signal Result Evidence
Code review ❌ 1 blocker 1 reviewer
Linked issue ✅ Linked #2462
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 (size label size:L; 1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 61 registered-repo PR(s), 52 merged, 500 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 61 PR(s), 500 issue(s).
Gate result ❌ Blocking Repo-configured hard blocker found.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • 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: 61 PR(s), 500 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • No action.
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.

🟩 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 Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added gittensor gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. labels Jul 3, 2026
@JSONbored
JSONbored force-pushed the feat/cancel-ci-on-contributor-cap branch from ec73c9d to 5cdc19f Compare July 3, 2026 01:49
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gittensory-ui 46ab76c Commit Preview URL

Branch Preview URL
Jul 03 2026, 06:05 AM

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.14%. Comparing base (737da5b) to head (46ab76c).
⚠️ Report is 10 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2662   +/-   ##
=======================================
  Coverage   96.13%   96.14%           
=======================================
  Files         239      239           
  Lines       26777    26846   +69     
  Branches     9719     9743   +24     
=======================================
+ Hits        25743    25812   +69     
  Misses        424      424           
  Partials      610      610           
Files with missing lines Coverage Δ
src/db/repositories.ts 96.61% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 69.46% <ø> (ø)
src/github/app.ts 98.47% <100.00%> (+0.31%) ⬆️
src/openapi/schemas.ts 100.00% <ø> (ø)
src/queue/processors.ts 92.73% <100.00%> (+<0.01%) ⬆️
src/selfhost/setup-wizard.ts 100.00% <ø> (ø)
src/services/agent-action-executor.ts 95.29% <100.00%> (+0.52%) ⬆️
src/services/agent-approval-queue.ts 100.00% <100.00%> (ø)
src/signals/focus-manifest.ts 99.32% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JSONbored added 6 commits July 2, 2026 23:01
…d for the contributor cap

Closing a PR for exceeding the per-contributor open-item cap currently
leaves its in-flight Actions runs burning CI minutes for nothing.
Adds an opt-in contributorCapCancelCi setting (per-repo, or an
install-wide CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var fallback) that
lists and cancels a closed PR's in-progress/queued workflow runs at
its head SHA right after a contributor_cap close succeeds.

Requires the actions:write permission (added to the self-host App
manifest); an installation that hasn't re-approved it degrades
gracefully -- the cancellation attempt is skipped and logged via a
dedicated audit event, and never blocks or fails the close itself,
which has already succeeded by the time this hook runs.

Off by default; zero behavior change until a repo (or the install-wide
env var) opts in.

Closes #2462.
main independently merged 0097_command_rate_limit.sql, colliding with
this branch's own 0097_contributor_cap_cancel_ci.sql after rebase.
… audit hook

codecov/patch flagged 2 uncovered lines: the closing .catch(() => undefined)
line of each recordAuditEvent call inside recordContributorCapCiCancelOutcome
never registered a hit, despite both outcome paths being exercised by
existing tests (a known v8-instrumentation quirk with a `.catch()` chained
directly onto a large multi-line object-literal call).

Split each audit write into its own small named helper with the call
assigned to a local before awaiting/catching, matching the shape other
audit-write call sites in this file already use without the same gap.
…ful one

cancelOneWorkflowRun returned an untyped {kind:"not_permission_error"} for
any non-403 cancel failure (a genuine 500/404/422), and the caller's loop
only branched on "cancelled" vs "permission_missing" -- silently falling
through past that third case and letting the loop's final return still claim
kind:"cancelled" with an undercounted cancelledCount. A real cancel failure
was audited as a successful cancellation.

cancelOneWorkflowRun now mirrors listWorkflowRunIdsForStatus's own error
shape ({kind:"error", warning}, carrying the actual status + message), and
the caller returns immediately on ANY non-cancelled result instead of
continuing past it -- matching the pattern already used one function up.

Also: recordContributorCapCiCancelOutcome hardcoded every non-cancelled
outcome under the ...permission_missing audit event type, even a genuine
network/list/cancel error with kind:"error" -- now selects the event type
from outcome.kind, so a real failure is no longer misclassified as a
permission gap on any dashboard querying by eventType.

Also fixes a secret-scanner false positive: new test fixtures reused the
pre-existing "installation-token" literal verbatim; since this is the first
time those specific lines appear as new diff content, the scanner flags it
the same way this exact false positive was already worked around elsewhere
in the codebase -- renamed to the established "fake-installation-token"
convention.
… branch

Codecov would otherwise flag auditContributorCapCiCancelFailed's closing
.catch(() => undefined) as an uncovered branch, symmetric to the
cancel-success audit write's own already-tested failure path.
listWorkflowRunIdsForStatus only read GitHub's default first page of
/actions/runs, so a head SHA with more than one page of matching
queued/in_progress runs left page-2+ runs uncancelled while
cancelInFlightWorkflowRunsForHeadSha reported totalFound/cancelledCount
as if the listing were complete.

per_page=100 + follow Link: rel="next" until exhausted, bounded to 10
pages (mirrors backfill.ts's githubPaginatedList/PR_DETAIL_MAX_PAGES).

Addresses a gate review finding on #2462.
@JSONbored
JSONbored force-pushed the feat/cancel-ci-on-contributor-cap branch from 6098328 to 46ab76c Compare July 3, 2026 06:04
@JSONbored
JSONbored merged commit f69aa48 into main Jul 3, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/cancel-ci-on-contributor-cap branch July 3, 2026 06:23
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.

Development

Successfully merging this pull request may close these issues.

feat(agent-actions): cancel in-flight CI runs when a PR is auto-closed for the contributor cap

1 participant