Skip to content
56 changes: 51 additions & 5 deletions .github/workflows/codeql-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,12 @@ jobs:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
LANGUAGE: ${{ matrix.language }}
RUN_ATTEMPT: ${{ github.run_attempt }}
REQUIRED_RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state before CodeQL dispatch."
Expand All @@ -188,6 +190,14 @@ jobs:
echo "Pull request head moved on the live open PR; a fresh dispatch will fire for the current head."
exit 0
fi
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Could not validate live pull request base SHA before CodeQL verdict read."
exit 1
fi
if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::CodeQL shard requires a canonical current run id."
exit 1
fi

statuses="$(gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses")"
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${LANGUAGE}" '
Expand All @@ -208,6 +218,36 @@ jobs:
exit 0
;;
esac

expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}"
expected_job="CodeQL dispatch scan (${LANGUAGE})"
runs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs")"
run_id="$(printf '%s' "$runs_json" | jq -r --arg title "$expected_title" --arg path ".github/workflows/codeql-scan-dispatch.yml" '
[
.[] | .workflow_runs[]
| select(.path == $path)
| select(.event == "repository_dispatch")
| select(.status == "completed")
| select(.display_title == $title or .name == $title)
]
| first
| .id // empty
')"
if [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then
jobs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/runs/${run_id}/jobs")"
job_conclusion="$(printf '%s' "$jobs_json" | jq -r --arg name "$expected_job" '
[.[] | .jobs[] | select(.name == $name)]
| if length == 1 then .[0].conclusion else empty end
')"
case "$job_conclusion" in
success|failure)
echo "verdict=${job_conclusion}" >>"$GITHUB_OUTPUT"
echo "Found completed CodeQL dispatch scan job for ${LANGUAGE}: ${job_conclusion}."
exit 0
;;
esac
fi

if [ "$RUN_ATTEMPT" != "1" ]; then
echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict."
exit 1
Expand Down Expand Up @@ -251,7 +291,6 @@ jobs:
always()
&& github.event.action != 'closed'
&& github.event.pull_request.state != 'closed'
&& github.run_attempt == 1
&& needs.detect-languages.result == 'success'
&& needs.detect-languages.outputs.code == 'true'
runs-on: ubuntu-24.04
Expand All @@ -277,6 +316,9 @@ jobs:
set -euo pipefail
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')"
live_base_ref="$(printf '%s' "$live_pr" | jq -r '.base.ref // empty')"
live_head_ref="$(printf '%s' "$live_pr" | jq -r '.head.ref // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state before CodeQL dispatch."
Expand All @@ -294,6 +336,10 @@ jobs:
echo "::error::CodeQL dispatch requires a canonical current run id."
exit 1
fi
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then
echo "::error::Could not validate live pull request base identity before CodeQL dispatch."
exit 1
fi

include_json="$(printf '%s' "$MATRIX" | jq -c '.include // empty' 2>/dev/null || true)"
if [ -z "$include_json" ] ||
Expand Down Expand Up @@ -385,10 +431,10 @@ jobs:
jq -cn \
--arg target_repository "$TARGET_REPOSITORY" \
--arg pr_number "$PR_NUMBER" \
--arg pr_base_ref "$PR_BASE_REF" \
--arg pr_base_sha "$PR_BASE_SHA" \
--arg pr_head_ref "$PR_HEAD_REF" \
--arg pr_head_sha "$PR_HEAD_SHA" \
--arg pr_base_ref "$live_base_ref" \
--arg pr_base_sha "$live_base" \
--arg pr_head_ref "$live_head_ref" \
--arg pr_head_sha "$live_head" \
--argjson matrix "$pending_matrix" \
--arg required_run_id "$REQUIRED_RUN_ID" \
--argjson required_jobs "$required_jobs" \
Expand Down
44 changes: 41 additions & 3 deletions .github/workflows/codeql-scan-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ run-name: >-
CodeQL Scan Dispatch ${{ github.event.client_payload.target_repository ||
github.repository }}#${{
github.event.client_payload.pr_number || 'event' }}@${{
github.event.client_payload.pr_head_sha || github.sha }}
github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || github.sha }}/${{
github.event.client_payload.pr_base_sha || 'none' }}/${{
github.event.client_payload.required_run_id || github.run_id }}

on:
repository_dispatch:
Expand Down Expand Up @@ -142,8 +144,12 @@ jobs:
PR_NUMBER: ${{ github.event.client_payload.pr_number }}
SUPPLIED_BASE_REF: ${{ github.event.client_payload.pr_base_ref || '' }}
SUPPLIED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || '' }}
SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }}
SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}
SUPPLIED_HEAD_ENVELOPE: ${{ toJSON(github.event.client_payload.pr_head) }}
SUPPLIED_HEAD_SCHEMA: ${{ github.event.client_payload.pr_head.schema || '' }}
SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }}
SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}
SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head.ref || github.event.client_payload.pr_head_ref || '' }}
SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || '' }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
SUPPLIED_MATRIX: ${{ toJSON(github.event.client_payload.matrix) }}
SUPPLIED_REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id || '' }}
SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }}
Expand Down Expand Up @@ -175,6 +181,33 @@ jobs:
fi
printf 'Authorized repository_dispatch actor=%s sender=%s target=%s.\n' "$DISPATCH_ACTOR" "$DISPATCH_SENDER" "$TARGET_REPOSITORY"

if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ]; then
head_envelope_json="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -c 'select(type == "object")' 2>/dev/null || true)"
head_schema_type="$(printf '%s' "$head_envelope_json" | jq -r '.schema | type' 2>/dev/null || true)"
head_schema="$(printf '%s' "$head_envelope_json" | jq -r '.schema // empty' 2>/dev/null || true)"
if [ "$head_schema_type" = "null" ]; then
printf '::error::repository_dispatch supplied unsupported pr_head schema=<missing>.\n'
exit 1
fi
if [ "$head_schema_type" != "string" ]; then
printf '::error::repository_dispatch supplied malformed pr_head envelope; expected string schema="1" and non-empty ref/sha strings.\n'
exit 1
fi
if [ "$head_schema" != "1" ]; then
printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$head_schema"
exit 1
fi
if [ "$(printf '%s' "$head_envelope_json" | jq '(.ref | type == "string" and length > 0) and (.sha | type == "string" and test("^[0-9a-f]{40}$"))')" != "true" ]; then
printf '::error::repository_dispatch supplied malformed pr_head envelope; expected string schema="1" and non-empty ref/sha strings.\n'
exit 1
fi
SUPPLIED_HEAD_REF="$(printf '%s' "$head_envelope_json" | jq -r '.ref')"
SUPPLIED_HEAD_SHA="$(printf '%s' "$head_envelope_json" | jq -r '.sha')"
else
SUPPLIED_HEAD_REF="$SUPPLIED_LEGACY_HEAD_REF"
SUPPLIED_HEAD_SHA="$SUPPLIED_LEGACY_HEAD_SHA"
fi

if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-<empty>}" "${PR_NUMBER:-<empty>}"
Expand Down Expand Up @@ -503,6 +536,11 @@ jobs:
exit 0
fi

if [ "$GATE_OUTCOME" = "success" ]; then
echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The completed dispatch scan job remains the evidence for this head."
exit 0
fi

echo "::error::Could not publish the CodeQL dispatch status after all configured credentials failed; the exact required job will remain failed and will not be woken with stale or missing evidence."
exit 1

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### CodeQL dispatch validates the original versioned head envelope

- `codeql-scan-dispatch.yml` now parses the original `pr_head` JSON and accepts a present envelope only when it is an object with string schema `"1"`, a non-empty string ref, and a 40-character lowercase hexadecimal SHA. Numeric schemas and incomplete envelopes fail closed instead of borrowing legacy fields. The legacy scalar fallback is used only when `pr_head` is absent, and executable regressions prove the nested tuple wins even when stale legacy values are also present. Refs #2043, #2040.

### Failed-check finding names the Strix sandbox instead of the gateway

- `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935.
Expand Down Expand Up @@ -68,6 +72,13 @@
- Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up.

## [Unreleased]
- Accept a versioned `pr_head` object (`schema`, `ref`, and `sha`) in the
central CodeQL scan-dispatch handler while retaining the legacy
`pr_head_ref`/`pr_head_sha` fallback for already-queued callers. This is the
backward-compatible handler prerequisite for moving the producer below
GitHub's ten-top-level-property `repository_dispatch.client_payload` limit;
missing or unknown envelope versions fail closed before pull-request metadata
is used.
- Include merge-scheduler entrypoint, core, and regression-test changes in
the existing runtime-quality workflow's trigger and suite selector. Scheduler
workflow edits retain queue checks and also select the full review-repair
Expand Down
35 changes: 35 additions & 0 deletions docs/adr/0025-codeql-required-workflow-dispatch-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,41 @@ job id, each scan shard looks up only its own id, and a missing, stale, or
mismatched identity still fails closed. The old scalar
`required_job_id`/`required_language` payload is retired.

#### 2026-09-08 amendment: version the head tuple to stay within GitHub's dispatch limit

**Status: Proposed.** Exact-head CodeQL run
[`34214980549`](https://github.com/ContextualWisdomLab/.github/actions/runs/34214980549),
coordinator job
[`102028015000`](https://github.com/ContextualWisdomLab/.github/actions/runs/34214980549/job/102028015000),
failed before creating a handler run because GitHub rejected the producer's
11-property `client_payload` with HTTP 422: no more than ten top-level
properties are accepted. The extra properties are not disposable: live base,
head, producer revision, required-run, job, and matrix identities are all
security or exact-evidence bindings.

The selected migration groups only the head tuple into one versioned object:
`pr_head: {schema: "1", ref: <ref>, sha: <sha>}`. The handler lands first and
accepts this object while retaining the two legacy scalar fields for in-flight
dispatches. When the nested object is present, the handler parses the original
JSON and requires an object containing string schema `"1"`, a non-empty string
ref, and a 40-character lowercase hexadecimal SHA. It rejects numeric schemas,
missing fields, malformed objects, and unknown versions without consulting the
legacy fields; only an absent object activates the scalar fallback. After that
compatibility foundation is merged and proven, the #1902
producer may replace `pr_head_ref` plus `pr_head_sha` with `pr_head`, reducing
its top-level count to ten without weakening live-PR or exact-head checks.

Alternatives were rejected as follows: deleting an identity field loses a
validation invariant; compacting unrelated fields creates an unnecessarily
large schema transition; and changing the producer before the default-branch
handler understands the envelope makes the repairing PR unable to produce its
own exact-head hosted evidence. The legacy fallback is temporary compatibility,
not authority to accept conflicting shapes: producer tests must emit only one
shape, and a later cleanup may remove the scalars after no live caller remains.
Executable contracts deliberately make the nested tuple match the live PR while
supplying different valid legacy values, so a regression to fallback preference
cannot pass unnoticed.

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

`analyze-merge` ("CodeQL merge preview") is confirmed, per PR #1766's own
Expand Down
32 changes: 32 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -3039,6 +3039,38 @@ No second repository may be changed until the central run reaches an explicit su
the detector reports `VERIFIED` for that exact head. GitHub documents the hard boundary: default setup blocks
CodeQL-generated SARIF uploads from advanced configuration, so rollback must never blindly enable it beside
an active uploader.

### Proposed control-plane repair: bounded CodeQL dispatch head envelope — 2026-09-08

**Observed gap.** `.github` PR #1902 exact head `e0924260c2105b49e8840701ce8509d765125b0f`
reached the coordinator in run
[`34214980549`](https://github.com/ContextualWisdomLab/.github/actions/runs/34214980549),
job
[`102028015000`](https://github.com/ContextualWisdomLab/.github/actions/runs/34214980549/job/102028015000),
but GitHub rejected its `repository_dispatch.client_payload` with HTTP 422
because it supplied 11 top-level properties and the API permits no more than
ten. No scan handler or SARIF evidence was created, so this is a producer/API
contract failure rather than a CodeQL analysis failure.

**Boundary and action.** `.github` remains the owner of both the required
producer and native handler contract. Land the backward-compatible handler
foundation first: accept `pr_head: {schema: "1", ref, sha}`, prefer it over the
legacy scalar fields, reject missing or unknown nested-object versions, and
keep legacy fallback only for already-queued calls. Then repair #1902 to replace the two head scalars
with that one object and regenerate combined exact-head hosted evidence. Do not
drop base/head/run/job/matrix/provenance fields, copy handler source, or treat a
predecessor run as GREEN. After migration, remove the legacy bridge only after
an inventory proves no live caller remains.

**Current-source repair.** Review of #2043 found that validating only the
interpolated schema string allowed JSON number `1` and let an incomplete nested
object borrow legacy ref/SHA values. The handler now validates the original JSON
object and uses legacy scalars only when that object is absent. RED coverage
pins numeric schema rejection, missing ref/SHA rejection, legacy-only success,
and nested precedence over deliberately stale legacy values.

**Status:** Proposed; strict handler RED/GREEN contract prepared from protected main, with hosted exact-head evidence still required.

## 2026-09-04 org-wide open-PR sweep: severe central Actions capacity congestion confirmed, `noema_review_gate.py`/`strix.yml` confirmed as a multi-PR hot-file collision zone

**Status:** Investigated via direct read-only Actions API queries and scratch-clone merge attempts against
Expand Down
Loading
Loading