Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@

# Changelog

## Proposed

- Route scheduler Actions inventory and force-cancellation through the credential
scoped to the repository hosting each run. Central required-workflow runs use
the receiving repository runner token; target runs retain the explicit
cross-repository Actions token. This prevents an exhausted mutation App quota
from blocking current-head review admission while preserving fail-closed
cross-repository authority.

- **Consolidate current-head queue coalescing into the merge scheduler.** The standalone `Current Head Run Coalescer` duplicated one runner admission for every central pull-request event. Its exact-head worker now runs inside the already-required merge-scheduler job after immutable trusted-source materialization, preserving fail-closed PR/head/base revalidation while deleting the redundant workflow job.

All notable changes to the organization automation repository are documented in
Expand Down
44 changes: 44 additions & 0 deletions docs/doctoring/host-scoped-actions-inventory-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Host-scoped Actions inventory credentials

Decision date: **2026-09-07**

## Problem

The central scheduler reads and cancels workflow runs in two authority domains.
Runs hosted by `ContextualWisdomLab/.github` are visible to the receiving
workflow's runner token. Runs hosted by a target repository require the explicit
cross-repository Actions credential. Sending both through the mutation App
couples current-head admission to that installation's independent rate-limit
bucket and reproduces the queue blocker recorded in
[ContextualWisdomLab/.github#1231](https://github.com/ContextualWisdomLab/.github/pull/1231).

## Decision

Select the credential from the repository that hosts the run. Repository
identity is compared case-insensitively. Central inventory and cancellation use
the configured dispatch/runner token; all target repositories continue through
the explicit Actions token. Missing credentials continue to fail at the GitHub
API boundary—there is no paid, anonymous, or mutable-head fallback.

## Failure scenes

- If the mutation App quota is exhausted, central current-head discovery still
uses the runner token and can release stale central runs.
- If a target repository is queried, the scheduler never substitutes the
central runner token, whose scope is insufficient.
- If repository casing differs, the same central repository is not
misclassified as a target.

## Evidence and follow-up

The permanent regression first appears at RED commit
`8cc62ce8837e456dfac4f592bcbd0786a77e4b81`. The implementation must receive
fresh exact-head GitHub Checks before the PR can leave Proposed status.

## References

GitHub. (2026). *REST API endpoints for workflow runs*.
https://docs.github.com/en/rest/actions/workflow-runs

GitHub. (2026). *Automatic token authentication*.
https://docs.github.com/actions/security-for-github-actions/security-guides/automatic-token-authentication
17 changes: 17 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -3353,3 +3353,20 @@ queries the check-runs API at its own time, order-independently. The implementin
their change was safe because they had scoped it narrowly, not because they had checked for the name
collision — which is the more useful lesson: **a job name is unique only within one workflow file, and the
same name in another file can carry the opposite safety property.**


### Central Actions inventory credential routing

- **Status:** Proposed
- **Owner:** `ContextualWisdomLab/.github`
- **Problem:** Central required-workflow inventory and cancellation inherited the
cross-repository Actions credential, so an exhausted App rate-limit bucket
could prevent discovery or cleanup of the current-head review run.
- **Action:** Route each Actions read/cancel operation by the repository hosting
the run. Use the central runner token only for
`ContextualWisdomLab/.github`; preserve the explicit target Actions token for
every other repository.
- **Evidence:** Historical owner PR
[#1231](https://github.com/ContextualWisdomLab/.github/pull/1231); RED commit
`8cc62ce8837e456dfac4f592bcbd0786a77e4b81`; fresh exact-head hosted checks
remain required before integration.
22 changes: 19 additions & 3 deletions scripts/ci/pr_review_merge_scheduler_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,21 @@ def run_github_dispatch(args: Sequence[str], *, stdin: str | None = None) -> str
return run_with_env(args, stdin=stdin, env=env)


def run_github_actions_for_repository(
repo: str,
args: Sequence[str],
*,
stdin: str | None = None,
) -> str:
"""Run an Actions command with the credential scoped to its host repository."""
central_repo = (
os.environ.get("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY") or ""
).strip()
if central_repo and repo.casefold() == central_repo.casefold():
return run_github_dispatch(args, stdin=stdin)
return run_github_actions(args, stdin=stdin)


def split_repo(repo: str) -> tuple[str, str]:
"""Split an owner/name repository string into owner and repository name."""
try:
Expand Down Expand Up @@ -3162,7 +3177,7 @@ def active_workflow_runs(
args += ["-f", f"created={created}"]
if head_sha:
args += ["-f", f"head_sha={head_sha}"]
payload = json.loads(run_github_actions(args))
payload = json.loads(run_github_actions_for_repository(repo, args))
pages = payload if isinstance(payload, list) else [payload]
for page in pages:
runs.extend(page.get("workflow_runs") or [])
Expand Down Expand Up @@ -3411,14 +3426,15 @@ def force_cancel_workflow_runs(repo: str, run_ids: Sequence[str]) -> dict[str, s
def cancel_one(run_id: str) -> tuple[str, str | None]:
"""Return one run id and its bounded GitHub cancellation error, if any."""
try:
run_github_actions(
run_github_actions_for_repository(
repo,
[
"gh",
"api",
"-X",
"POST",
f"repos/{repo}/actions/runs/{run_id}/force-cancel",
]
],
)
except RuntimeError as exc:
return run_id, str(exc).replace("\n", "; ")[:600]
Expand Down
40 changes: 37 additions & 3 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1785,7 +1785,11 @@ def map(self, func, items):
),
)
cancelled = []
monkeypatch.setattr(sched, "run_github_actions", cancelled.append)
monkeypatch.setattr(
sched,
"run_github_actions",
lambda args, stdin=None: cancelled.append(args),
)
monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda x: None)

run_ids = sched.cancel_stale_opencode_runs("owner/repo", "workflow", make_pr(), dry_run=False)
Expand All @@ -1796,7 +1800,7 @@ def map(self, func, items):


def test_force_cancel_failure_logs_reason_and_does_not_raise(monkeypatch, capsys):
def fail_cancel(args):
def fail_cancel(args, stdin=None):
raise RuntimeError(
"Command failed (1): gh api -X POST "
"repos/owner/repo/actions/runs/29263154177/force-cancel; "
Expand All @@ -1821,7 +1825,7 @@ def fail_cancel(args):


def test_force_cancel_multiple_runs_reports_only_failures(monkeypatch):
def maybe_fail(args):
def maybe_fail(args, stdin=None):
if "runs/2/force-cancel" in " ".join(args):
raise RuntimeError("GitHub returned HTTP 500")
return ""
Expand Down Expand Up @@ -10790,3 +10794,33 @@ def behind_with(nodes):
assert "checks are still queued or running" not in resumed.reason

assert sched.has_in_flight_check_runs(behind_with([])) is False


def test_central_actions_inventory_uses_host_scoped_credentials(monkeypatch):
"""Central run reads and cancellation cannot spend the cross-repository App quota."""
calls = []

def fake_run_with_env(args, *, stdin=None, env=None):
calls.append((tuple(args), None if env is None else env.get("GH_TOKEN")))
return '{"workflow_runs": []}'

monkeypatch.setattr(sched, "run_with_env", fake_run_with_env)
monkeypatch.setenv("GH_TOKEN", "mutation-app-token")
monkeypatch.setenv("SCHEDULER_ACTIONS_TOKEN", "target-actions-token")
monkeypatch.setenv("SCHEDULER_DISPATCH_TOKEN", "central-runner-token")
monkeypatch.setenv(
"SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY",
"contextualwisdomlab/.GITHUB",
)

sched.active_workflow_runs("ContextualWisdomLab/.github", statuses=("queued",))
sched.force_cancel_workflow_runs("ContextualWisdomLab/.github", ["101"])
sched.active_workflow_runs("owner/repo", statuses=("queued",))
sched.force_cancel_workflow_runs("owner/repo", ["202"])

assert [token for _, token in calls] == [
"central-runner-token",
"central-runner-token",
"target-actions-token",
"target-actions-token",
]
Loading