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
17 changes: 9 additions & 8 deletions .github/workflows/codeql-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
# codeql-scan-dispatch.yml (which runs natively, unrestricted, in
# ContextualWisdomLab/.github). The shard then fails intentionally to release
# its runner; the handler publishes codeql-dispatch/<language> and reruns only
# that exact failed job. On rerun the shard reads the terminal status once.
# that exact failed job. On rerun the shard consumes an authenticated terminal
# status when one exists. If earlier attempts never reached dispatch and no
# authenticated verdict exists, the rerun may dispatch the same exact shard;
# the central target/PR/language concurrency lane bounds duplicate recovery.
# Design:
# docs/adr/0025-codeql-required-workflow-dispatch-architecture.md. The
# merge-preview scan (analyze-merge) is required nowhere (PR #1766) and was
Expand Down Expand Up @@ -158,9 +161,11 @@ jobs:
steps:
- name: Request current-head CodeQL scan dispatch
# Each shard dispatches only its own language and passes its exact
# run/job identity. The shard intentionally fails after dispatch so
# its runner is released; the trusted handler later reruns that one
# failed job after publishing a terminal current-head verdict.
# run, job, language, and head identity. The shard intentionally fails
# after dispatch so its runner is released; the trusted handler later
# reruns that exact failed job after publishing a terminal verdict.
# Rerun attempt count is not a dispatch receipt: earlier attempts can
# be cancelled before runner assignment and execute zero steps.
id: dispatch
if: needs.detect-languages.outputs.code == 'true'
env:
Expand Down Expand Up @@ -215,10 +220,6 @@ jobs:
exit 0
;;
esac
if [ "$RUN_ATTEMPT" != "1" ]; then
echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict."
exit 1
fi
if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$REQUIRED_JOB_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::CodeQL dispatch requires canonical current run and job ids."
Expand Down
67 changes: 58 additions & 9 deletions docs/adr/0025-codeql-required-workflow-dispatch-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,18 @@ codeql-pr.yml (required workflow, runs in target repo context)
does) before dispatching.
analyze-head (matrix) -- SAME REQUIRED-CHECK NAME:
"CodeQL compatibility analysis (${{ matrix.language }})".
No codeql-action reference. On attempt one it
dispatches its exact run id, job id, language,
and head, then fails intentionally to release
the runner. The trusted handler publishes the
terminal status and reruns only that failed
job. On attempt two the shard reads the
authenticated current-head status once and
reflects it as this job's own exit code.
No codeql-action reference. Each invocation
first consumes a trusted terminal
codeql-dispatch/<language> status for the exact
current head when one exists. Otherwise it
validates the exact required run/job/language
identity, dispatches that one shard, records
verdict=pending, and fails intentionally to
release the runner. The trusted handler later
publishes the terminal status and reruns only
that failed job. A later run_attempt is not
treated as proof that any earlier attempt
reached the dispatch step.

.github/workflows/codeql-scan-dispatch.yml (NEW, runs natively in .github,
NOT admitted through the ruleset, so codeql-action is unrestricted here)
Expand Down Expand Up @@ -170,6 +174,36 @@ dispatch was rejected because the handler validates one shard and wakes one
exact required job per run; changing that contract would enlarge the security
and recovery surface without solving another observed need.

### Rerun recovery is evidence-driven, not attempt-driven

`github.run_attempt` is execution metadata. It is not an authenticated receipt
that a previous attempt reached `Request current-head CodeQL scan dispatch`.
The concrete counterexample is
`ContextualWisdomLab/accounting-information-platform#49@065f9ab7038bf35db4ef129827de6ab8ee6a1038`,
required CodeQL run `33890965185`: attempts 1 and 2 were cancelled before
runner assignment (`runner_id=0`, `steps=[]`). Attempt 3 finally ran, found no
trusted terminal `codeql-dispatch/actions` or `codeql-dispatch/python` status,
and the former `RUN_ATTEMPT != 1` guard rejected both shards before dispatch.
The unchanged consumer head was therefore unable to recover after capacity
returned.

The required workflow must instead use authenticated evidence. For the exact
live PR head and language shard, a terminal status created by the expected
central identity is consumed. If no such terminal verdict exists, the shard
re-validates its run/job/head identity and may dispatch again regardless of the
numeric attempt. The central target/repository/PR/language concurrency key
bounds duplicate recovery; the handler independently re-validates live PR and
wake identity before it publishes a verdict or reruns the exact job. Missing
evidence remains fail closed: redispatch produces `verdict=pending`, never a
synthetic success.

A manually requested rerun can arrive while an earlier native dispatch is still
queued but has not published a terminal status. In that case the existing
concurrency lane may replace work for the same exact logical shard. This is a
bounded restart risk, not a reason to restore attempt-number inference. If
observed churn becomes material, the successor design must add an authenticated
pending/dispatch-receipt state keyed to the same exact identity.

## Scope decision: `analyze-merge` is dropped, not migrated

`analyze-merge` ("CodeQL merge preview") is confirmed, per PR #1766's own
Expand Down Expand Up @@ -230,6 +264,10 @@ blocker for this one.
documented, evidently deliberate platform limitation
("CodeQL requires configuration at the repository level"), not a bug
report candidate.
- **Use run-attempt number as a dispatch receipt:** rejected after the AIP #49
reproduction. Earlier attempts can be cancelled before any step executes,
so an attempt number cannot prove that a native scan was requested. Only
authenticated exact-head status/receipt evidence may suppress redispatch.

## Risks and effects

Expand All @@ -250,6 +288,10 @@ blocker for this one.
assert zero matches, as a permanent contract test) — re-adding it with
the bug still present would recreate the exact org-wide 100%-startup_failure
incident this ADR exists to prevent.
- Rerun recovery can replace an already queued same-shard native dispatch when
no terminal status exists yet. The concurrency key keeps that restart within
the exact repository/PR/language identity. If this causes material churn,
add an authenticated pending receipt rather than trusting run-attempt order.

## Follow-up

Expand All @@ -261,7 +303,14 @@ blocker for this one.
ADR is the record).
4. Add a permanent contract test asserting no `codeql-action` reference
exists anywhere in `codeql-pr.yml`.
5. Only then, re-add `.github/workflows/codeql-pr.yml` to ruleset `18156473`'s
5. Keep the rerun-recovery regression that executes the production dispatch
shell with a later `run_attempt`, no trusted terminal verdict, and exact
run/job/head identity; it must reach bounded dispatch with `verdict=pending`.
6. After protected integration, rerun the unchanged AIP #49 head and require
real authenticated terminal `codeql-dispatch/actions` and
`codeql-dispatch/python` verdicts before treating the owner repair as
effective for that consumer.
7. Only then, re-add `.github/workflows/codeql-pr.yml` to ruleset `18156473`'s
required `workflows` list (admin:org PUT, same mechanism used to remove
it) and verify a real PR observes a successful, correctly-named required
check before declaring this ADR's status Accepted.
60 changes: 60 additions & 0 deletions docs/doctoring/codeql-rerun-pre-runner-cancellation-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CodeQL rerun recovery after pre-runner cancellation

## Problem and exact evidence

The required `CodeQL PR` workflow used `github.run_attempt != 1` as if it proved that an earlier attempt had successfully dispatched the native CodeQL scan. That inference is false when an earlier attempt is cancelled before runner assignment.

`ContextualWisdomLab/accounting-information-platform` PR #49 provides the concrete reproduction on exact head `065f9ab7038bf35db4ef129827de6ab8ee6a1038`, workflow run `33890965185`.

- Attempt 1 `Detect CodeQL languages` job `101082241642` ended `cancelled` with `runner_id=0` and `steps=[]`; its downstream compatibility job was also cancelled without execution.
- Attempt 2 `Detect CodeQL languages` job `101128192785` ended the same way: `cancelled`, `runner_id=0`, `steps=[]`; the downstream compatibility job again never executed.
- Attempt 3 finally obtained runners. The `actions` shard job `101220582725` and `python` shard job `101220582747` reached `Request current-head CodeQL scan dispatch`, found no authenticated `codeql-dispatch/<language>` terminal status, then failed solely because `RUN_ATTEMPT=3`.
- The target exact head had no `codeql-dispatch/actions` or `codeql-dispatch/python` commit status. Thus the attempt number did not identify a prior dispatch receipt or a terminal scan verdict.

This leaves an unchanged PR head permanently unable to obtain the required CodeQL result even after runner capacity recovers.

## Chosen repair

Keep the existing trust sequence:

1. re-read the live pull request and reject closed or moved heads;
2. read only `codeql-dispatch/<language>` statuses created by the expected `opencode-agent` identity;
3. if an authenticated terminal status exists, reflect it without dispatching;
4. otherwise validate the exact required run/job identity, obtain the OIDC-bound app token, and dispatch the exact repository/PR/head/language shard.

Remove the `RUN_ATTEMPT != 1` veto. A rerun attempt number is execution metadata, not evidence that the dispatch step ever ran. The native handler already serializes the same target-repository / pull-request / language tuple and re-validates live PR and wake identity before publishing a verdict or rerunning the exact required job.

This does not convert a missing CodeQL verdict to success. The required shard still fails with `verdict=pending` after dispatch and becomes successful only when the trusted handler publishes an authenticated terminal `success` status and reruns the exact job. A forged status, stale head, failed/error verdict, unavailable OIDC/app token, malformed run/job identity, or absent dispatch receipt remains fail closed.

## Executable regression

`tests/test_codeql_pr_rerun_recovery_contract.py` executes the production `Request current-head CodeQL scan dispatch` Bash block with:

- `RUN_ATTEMPT=3`;
- the same live target head;
- no authenticated CodeQL status;
- mocked OIDC and app-token exchange boundaries; and
- an exact run/job/language wake identity matching the accounting-platform reproduction.

The test requires the step to publish `verdict=pending` and to emit a `codeql-scan` repository-dispatch payload bound to `ContextualWisdomLab/accounting-information-platform`, PR #49, the exact head, run `33890965185`, job `101220582747`, and `python`.

Before the production change, the real shell block exits at the attempt-number guard before OIDC or dispatch, so this regression is RED for the observed reason. After the guard is removed, the same shell block reaches the bounded dispatch path.

## Risks, rollback, and acceptance

A manually requested rerun while a prior native dispatch is still queued but has not yet published a terminal status may replace work in the existing central target/PR/language concurrency lane. This is bounded to the same exact logical shard and does not broaden repository, head, language, credential, or merge authority. If live evidence shows harmful restart churn, the successor design should add an authenticated dispatch-receipt/pending state rather than restoring attempt-number inference.

Rollback is not `RUN_ATTEMPT != 1`; that recreates the proven dead end. A valid replacement must distinguish “prior dispatch accepted” from “prior attempt never executed” using authenticated evidence and retain exact-head fail-closed semantics.

GREEN requires all of the following on one unchanged successor head:

- the focused rerun-recovery regression passes;
- the existing `test_codeql_pr_workflow_contract.py` suite remains green;
- the complete central test, 100% coverage, docstring, workflow syntax, security and review gates pass;
- after protected integration, the unchanged accounting-platform PR #49 head is rerun and obtains a real authenticated terminal CodeQL verdict without provider/model or leaf-repository workaround.

## References

GitHub. (2026). *Re-running workflows and jobs*. GitHub Docs. https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs

GitHub. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs
113 changes: 113 additions & 0 deletions tests/test_codeql_pr_rerun_recovery_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Regression for CodeQL reruns whose earlier attempts never reached dispatch."""

from __future__ import annotations

import json
import os
import shutil
import subprocess
from pathlib import Path

from tests.test_opencode_workflow_shell_syntax import _extract_run_block


REPO_ROOT = Path(__file__).resolve().parents[1]
WORKFLOW_PATH = REPO_ROOT / ".github/workflows/codeql-pr.yml"
DISPATCH_STEP_NAME = "Request current-head CodeQL scan dispatch"


def test_rerun_without_authenticated_verdict_can_redispatch(tmp_path: Path) -> None:
"""A later attempt may dispatch when earlier attempts never produced a verdict."""
bash = shutil.which("bash")
jq = shutil.which("jq")
assert bash is not None and jq is not None

workflow = WORKFLOW_PATH.read_text(encoding="utf-8")
script = _extract_run_block(workflow, DISPATCH_STEP_NAME)
head_sha = "b" * 40
base_sha = "a" * 40

fake_bin = tmp_path / "bin"
fake_bin.mkdir()
dispatch_body = tmp_path / "dispatch.json"

fake_gh = fake_bin / "gh"
fake_gh.write_text(
"#!/usr/bin/env bash\n"
"set -euo pipefail\n"
'test "$1" = api\n'
"shift\n"
'if [ "${1:-}" = "-X" ]; then\n'
' test "$2" = POST\n'
' test "$3" = "repos/ContextualWisdomLab/.github/dispatches"\n'
' cat >"$FAKE_DISPATCH_BODY"\n'
" exit 0\n"
"fi\n"
'case "$1" in\n'
" */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n"
" */statuses) printf '%s\\n' '[]' ;;\n"
" *) exit 1 ;;\n"
"esac\n",
encoding="utf-8",
)
fake_gh.chmod(0o755)

fake_curl = fake_bin / "curl"
fake_curl.write_text(
"#!/usr/bin/env bash\n"
"set -euo pipefail\n"
'last="${@: -1}"\n'
'case "$last" in\n'
" *audience=opencode-github-action) printf '%s\\n' '{\"value\":\"oidc-token\"}' ;;\n"
" */exchange_github_app_token) printf '%s\\n' '{\"token\":\"app-token\"}' ;;\n"
" *) exit 1 ;;\n"
"esac\n",
encoding="utf-8",
)
fake_curl.chmod(0o755)

output = tmp_path / "github-output"
env = {
**os.environ,
"PATH": f"{fake_bin}:{os.environ['PATH']}",
"FAKE_PULL_JSON": json.dumps({"head": {"sha": head_sha}, "state": "open"}),
"FAKE_DISPATCH_BODY": str(dispatch_body),
"GH_TOKEN": "leaf-token",
"OIDC_AUDIENCE": "opencode-github-action",
"OPENCODE_API_BASE_URL": "https://api.opencode.ai",
"TARGET_REPOSITORY": "ContextualWisdomLab/accounting-information-platform",
"PR_NUMBER": "49",
"PR_BASE_REF": "develop",
"PR_BASE_SHA": base_sha,
"PR_HEAD_REF": "fix/restore-accounting-doc-ci-evidence",
"PR_HEAD_SHA": head_sha,
"LANGUAGE": "python",
"BUILD_MODE": "none",
"RUN_ATTEMPT": "3",
"REQUIRED_RUN_ID": "33890965185",
"REQUIRED_JOB_ID": "101220582747",
"ACTIONS_ID_TOKEN_REQUEST_TOKEN": "request-token",
"ACTIONS_ID_TOKEN_REQUEST_URL": "https://oidc.example/token",
"GITHUB_OUTPUT": str(output),
}

result = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=env,
timeout=60,
)

assert result.returncode == 0, result.stderr
assert "verdict=pending" in output.read_text(encoding="utf-8")
payload = json.loads(dispatch_body.read_text(encoding="utf-8"))
assert payload["event_type"] == "codeql-scan"
client_payload = payload["client_payload"]
assert client_payload["target_repository"] == "ContextualWisdomLab/accounting-information-platform"
assert client_payload["pr_head_sha"] == head_sha
assert client_payload["required_run_id"] == "33890965185"
assert client_payload["required_job_id"] == "101220582747"
assert client_payload["required_language"] == "python"
Loading