diff --git a/.github/workflows/actions-queue-health.yml b/.github/workflows/actions-queue-health.yml index 2084765946..191144bd08 100644 --- a/.github/workflows/actions-queue-health.yml +++ b/.github/workflows/actions-queue-health.yml @@ -20,6 +20,7 @@ jobs: permissions: contents: read actions: read + id-token: write steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 @@ -31,12 +32,78 @@ jobs: with: persist-credentials: false + - name: Exchange OpenCode app token for cross-repo reads + id: queue_read_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + - name: Collect read-only repository and runner evidence env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.queue_read_app_token.outputs.token }} run: | if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN is required for cross-repository queue reads." + echo "::error::PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the exchanged OpenCode app token is required for cross-repository queue reads." exit 1 fi echo "::add-mask::$GH_TOKEN" diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 2c41358cf1..068bf0f758 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -2,7 +2,6 @@ name: Agent Review Runtime Quality CI on: pull_request: - branches: [main] paths: - ".github/workflows/agent-review-runtime-quality-ci.yml" - ".github/workflows/noema-review.yml" @@ -22,6 +21,7 @@ on: - "scripts/ci/ensure_rust_llvm19.sh" - "tests/test_opencode_rust_coverage_toolchain_contract.py" - "tests/test_rust_coverage_timeout_not_measured.py" + - "tests/test_coverage_incomplete_summary.py" - "scripts/ci/resolve_base_rust_toolchain.py" - "tests/test_resolve_base_rust_toolchain.py" - "scripts/ci/place_maturin_extension.py" @@ -166,10 +166,13 @@ jobs: id: affected_suites shell: bash --noprofile --norc -e -o pipefail {0} env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | test "$(git rev-parse HEAD)" = "$HEAD_SHA" + git fetch --no-tags --prune origin "refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" + change_base_sha="$(git merge-base "refs/remotes/origin/$BASE_REF" "$HEAD_SHA")" + test -n "$change_base_sha" noema_suite=false opencode_suite=false strix_suite=false @@ -215,6 +218,7 @@ jobs: scripts/ci/ensure_rust_llvm19.sh|\ tests/test_opencode_rust_coverage_toolchain_contract.py|\ tests/test_rust_coverage_timeout_not_measured.py|\ + tests/test_coverage_incomplete_summary.py|\ scripts/ci/resolve_base_rust_toolchain.py|\ tests/test_resolve_base_rust_toolchain.py|\ scripts/ci/place_maturin_extension.py|\ @@ -334,7 +338,7 @@ jobs: exact_artifact_suite=true ;; esac - done < <(git diff --name-only "$BASE_SHA...$HEAD_SHA") + done < <(git diff --name-only "$change_base_sha...$HEAD_SHA") { echo "noema=$noema_suite" @@ -399,8 +403,8 @@ jobs: if: steps.affected_suites.outputs.opencode == 'true' run: | set -euo pipefail - python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_rust_coverage_timeout_not_measured.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py - python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py + python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_rust_coverage_timeout_not_measured.py tests/test_coverage_incomplete_summary.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py + python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_coverage_incomplete_summary.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py - name: Verify JavaScript materializer documentation contract if: steps.affected_suites.outputs.opencode == 'true' @@ -582,9 +586,15 @@ jobs: tests/test_exact_artifact_quality_single_runner.py - name: Verify consolidated workflow contract + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail python -m pytest -q tests/test_agent_review_runtime_quality_consolidation.py python -m compileall -q tests/test_agent_review_runtime_quality_consolidation.py - git diff --check "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" + git fetch --no-tags --prune origin "refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" + change_base_sha="$(git merge-base "refs/remotes/origin/$BASE_REF" "$HEAD_SHA")" + test -n "$change_base_sha" + git diff --check "$change_base_sha...$HEAD_SHA" git diff --exit-code diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 356244f7fc..8d7feb6a58 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -18,16 +18,17 @@ name: CodeQL PR on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] # Do not restrict the base ref: the org required-workflow ruleset already # scopes this to each repository's actual default branch via # ref_name: ["~DEFAULT_BRANCH"], whatever it is named. A hardcoded # [main, master, develop] list silently produced zero CodeQL checks for # any repository with a different default branch name (confirmed live: # a repository defaulting to gh-pages received every other required - # check but no CodeQL check at all) and would also block coverage for - # stacked PRs targeting a non-default feature branch, matching - # security-scan.yml's own "do not restrict the base ref" precedent. + # check but no CodeQL check at all). This does not widen ruleset 18156473: + # its default-ref scope must inject the required workflow first. A + # feature-base stacked PR gets this gate only where the workflow is run + # natively, or after retargeting to the repository default branch. concurrency: # NOT scoped by head SHA, unlike opencode-review.yml's group -- and that is @@ -58,8 +59,11 @@ permissions: jobs: detect-languages: name: Detect CodeQL languages - # Draft PRs get no runner; ready_for_review re-runs this on the same head. - if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') + # Ruleset consumers do not receive unchanged-head Ready events, so their + # Draft heads materialize security evidence. The native owner saves its + # runner, while Draft conversion and close events only retire stale work + # through the per-PR concurrency group above. + if: github.event.action != 'closed' && github.event.action != 'converted_to_draft' && (github.event.pull_request.draft != true || github.event.pull_request.base.repo.full_name != 'ContextualWisdomLab/.github') runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} timeout-minutes: 5 permissions: @@ -495,6 +499,54 @@ jobs: exit 1 fi + # A later attempt of the same required run must preserve exact work + # already admitted by the protected central handler. Without this + # check, a missing terminal verdict makes every attempt redispatch; + # handler concurrency then replaces the durable queued run and sends + # the same work to the back of the scarce CodeQL queue. + expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${live_head}/${live_base}/${REQUIRED_RUN_ID}/${live_merge}" + required_created_at="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}" --jq .created_at)" + if ! [[ "$required_created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + echo "::error::Could not validate required run creation time before CodeQL admission lookup." + exit 1 + fi + dispatch_runs="$( + gh api --method GET --paginate \ + -f per_page=100 \ + -f event=repository_dispatch \ + -f created=">=${required_created_at}" \ + "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs" | + jq -s . + )" + active_dispatch_id="$(printf '%s' "$dispatch_runs" | jq -r \ + --arg title "$expected_title" \ + --arg path ".github/workflows/codeql-scan-dispatch.yml" ' + [ + .[] | .workflow_runs[]? + | select(.path == $path) + | select(.event == "repository_dispatch") + | select(.display_title == $title or .name == $title) + | select(.status == "queued" or .status == "in_progress" + or .status == "waiting" or .status == "pending" + or .status == "requested") + | select( + (.actor.login // "" | ascii_downcase) as $actor + | $actor == "opencode-agent" or $actor == "opencode-agent[bot]" + ) + | select( + (.triggering_actor.login // "" | ascii_downcase) as $trigger + | $trigger == "opencode-agent" or $trigger == "opencode-agent[bot]" + ) + ] + | first + | .id // empty + ' + )" + if [[ "$active_dispatch_id" =~ ^[1-9][0-9]*$ ]]; then + echo "CodeQL admission deferred; preserving exact active dispatch ${active_dispatch_id}." + exit 0 + fi + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then echo "::error::CodeQL scan dispatch requires GitHub OIDC." exit 1 diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 04656e5b34..eae4b8c47e 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -31,13 +31,6 @@ on: repository_dispatch: types: [codeql-scan, codeql-scan-v2] -concurrency: - group: >- - codeql-scan-dispatch-${{ - github.event.client_payload.target_repository || github.repository }}-${{ - github.event.client_payload.pr_number || github.run_id }} - cancel-in-progress: true - permissions: contents: read @@ -45,7 +38,7 @@ jobs: validate-dispatch: name: validate-dispatch runs-on: - group: CWL central CodeQL + group: CWL central control labels: [self-hosted, linux, x64] timeout-minutes: 8 permissions: @@ -866,7 +859,7 @@ jobs: && needs.scan.result != 'cancelled' && needs.scan.result != 'skipped' runs-on: - group: CWL central CodeQL + group: CWL central control labels: [self-hosted, linux, x64] timeout-minutes: 8 permissions: diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 09bbf8181a..f416c71e3f 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -11,34 +11,432 @@ on: repository_dispatch: types: [opencode-review] -concurrency: - # Workflow-level admission, for the same reason strix.yml, noema-review.yml and - # opencode-review.yml carry theirs at this level: a job-level group is never - # evaluated while the whole run waits behind the organization job ceiling, so - # superseded dispatches for one pull request coalesce only after each of them - # has already been allocated a runner. Measured on 2026-09-06: of the five - # dispatch runs that passed `validate-pr-metadata`, four were rejected hours - # later by `opencode-review`'s privileged metadata check because the head had - # moved while they queued (runs 34002473295, 34010256951, 34015973300, - # 34016922761) -- each after `coverage-source-tree` and `coverage-evidence` - # had run. Cancelling the superseded run at creation returns that slot instead - # of spending it to discover the review's subject no longer exists. - # - # The key is the target pull request, matching the job-level group below and - # codeql-scan-dispatch.yml's workflow-level group; `github.run_id` keeps runs - # without a payload in their own groups rather than colliding. - group: >- - opencode-review-dispatch-${{ - github.event.client_payload.target_repository || github.repository }}-${{ - github.event.client_payload.pr_number || github.run_id }} - cancel-in-progress: true - permissions: contents: read jobs: + admit-exact-head-dispatch: + name: admit-exact-head-dispatch + if: github.event_name == 'repository_dispatch' + runs-on: + group: CWL central OpenCode + labels: [self-hosted, linux, x64] + timeout-minutes: 10 + permissions: + actions: read + contents: write + pull-requests: read + id-token: write + outputs: + admitted: ${{ steps.single_flight.outputs.admitted }} + steps: + - name: Authorize repository dispatch envelope + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_ACTOR: ${{ github.triggering_actor }} + DISPATCH_SENDER: ${{ github.event.sender.login || '' }} + ALLOWED_DISPATCH_ACTOR: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_ACTOR }} + ALLOWED_DISPATCH_TARGETS: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }} + TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }} + 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 || '' }} + DRAFT_REVIEW_ONLY: ${{ toJSON(github.event.client_payload.draft_review_only) }} + run: | + set -euo pipefail + [ "$EVENT_NAME" = "repository_dispatch" ] || { + echo "::error::Central OpenCode admission accepts repository_dispatch only." + exit 1 + } + + actor_allowed=0 + IFS=',' read -r -a allowed_dispatch_actors <<<"$ALLOWED_DISPATCH_ACTOR" + for allowed_actor in "${allowed_dispatch_actors[@]}"; do + allowed_actor="${allowed_actor//[[:space:]]/}" + if [ -n "$allowed_actor" ] && + [ "$DISPATCH_ACTOR" = "$allowed_actor" ] && + [ "$DISPATCH_SENDER" = "$allowed_actor" ]; then + actor_allowed=1 + break + fi + done + if [ "$actor_allowed" -ne 1 ]; then + printf '::error::repository_dispatch authorization rejected actor=%s sender=%s because both must match one configured scheduler identity.\n' "${DISPATCH_ACTOR:-}" "${DISPATCH_SENDER:-}" + exit 1 + fi + + target_allowed=0 + IFS=',' read -r -a allowed_dispatch_targets <<<"$ALLOWED_DISPATCH_TARGETS" + for allowed_target in "${allowed_dispatch_targets[@]}"; do + allowed_target="${allowed_target//[[:space:]]/}" + if [ -n "$allowed_target" ] && [ "$TARGET_REPOSITORY" = "$allowed_target" ]; then + target_allowed=1 + break + fi + done + if [ "$target_allowed" -ne 1 ]; then + printf '::error::repository_dispatch authorization rejected target=%s because it is absent from the configured exact repository allowlist.\n' "${TARGET_REPOSITORY:-}" + exit 1 + fi + + if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || + ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || + [ -z "$SUPPLIED_BASE_REF" ] || + [ -z "$SUPPLIED_HEAD_REF" ] || + ! [[ "$SUPPLIED_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || + ! [[ "$SUPPLIED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + printf '::error::repository_dispatch admission rejected malformed PR identity metadata. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" + exit 1 + fi + if ! [[ "$DRAFT_REVIEW_ONLY" =~ ^(true|false)$ ]]; then + echo "::error::repository_dispatch admission rejected malformed draft-review authority." + exit 1 + fi + printf 'Authorized exact repository_dispatch envelope for %s#%s.\n' "$TARGET_REPOSITORY" "$PR_NUMBER" + + - name: Exchange OpenCode app token for target repository metadata reads + id: metadata_read_app_token + if: >- + github.event_name == 'repository_dispatch' + && github.event.client_payload.target_repository != '' + && github.event.client_payload.target_repository != github.repository + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || + [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + + + - name: Admit one exact-head central dispatch + id: single_flight + env: + GH_TOKEN: ${{ github.token }} + TARGET_READ_TOKEN: ${{ steps.metadata_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }} + PR_NUMBER: ${{ github.event.client_payload.pr_number }} + HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }} + 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 }} + DRAFT_REVIEW_ONLY: ${{ toJSON(github.event.client_payload.draft_review_only) }} + run: | + set -euo pipefail + exact_title="OpenCode Review Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${HEAD_SHA}" + lease_branch="opencode-dispatch-leases" + lease_key="$(printf '%s#%s' "$TARGET_REPOSITORY" "$PR_NUMBER" | sha256sum | cut -d' ' -f1)" + lease_path="opencode-dispatch-leases/${lease_key}.json" + lease_ref_api="repos/ContextualWisdomLab/.github/git/ref/heads/${lease_branch}" + receipt_helper="$(mktemp)" + trap 'rm -f "$receipt_helper"' EXIT + + authorized_draft_marker_matches() { + local marker_name marker_inventory + [ "${DRAFT_REVIEW_ONLY:-false}" = "true" ] || return 1 + marker_name="cwl-draft-review-request-${TARGET_REPOSITORY//\//-}-${PR_NUMBER}-${HEAD_SHA}" + marker_inventory="$(gh api \ + "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${marker_name}&per_page=100")" || return 1 + jq -e --arg marker_name "$marker_name" ' + (.total_count | type) == "number" + and (.total_count | floor) == .total_count + and .total_count >= 0 + and (.artifacts | type) == "array" + and .total_count == (.artifacts | length) + and all(.artifacts[]; + type == "object" + and (.id | type) == "number" + and (.id | floor) == .id + and .id >= 1 + and (.name | type) == "string" + and .name == $marker_name + and (.expired | type) == "boolean" + ) + and any(.artifacts[]; .name == $marker_name and .expired == false) + ' <<<"$marker_inventory" >/dev/null + } + + live_authority_matches() { + local live_pr live_base_repo live_base_ref live_base_sha + local live_head_repo live_head_ref live_head_sha live_draft live_state + live_pr="$(GH_TOKEN="$TARGET_READ_TOKEN" gh api \ + "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" || return 1 + live_base_repo="$(jq -r '.base.repo.full_name // empty' <<<"$live_pr")" + live_base_ref="$(jq -r '.base.ref // empty' <<<"$live_pr")" + live_base_sha="$(jq -r '.base.sha // empty' <<<"$live_pr")" + live_head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$live_pr")" + live_head_ref="$(jq -r '.head.ref // empty' <<<"$live_pr")" + live_head_sha="$(jq -r '.head.sha // empty' <<<"$live_pr")" + live_draft="$(jq -r \ + 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end' \ + <<<"$live_pr")" + live_state="$(jq -r \ + 'if (.state | type) == "string" then .state else empty end' \ + <<<"$live_pr")" + [ "$live_state" = "open" ] && + [ "$live_base_repo" = "$TARGET_REPOSITORY" ] && + [[ "$live_head_repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] && + [ "$live_base_ref" = "$SUPPLIED_BASE_REF" ] && + [ "$live_base_sha" = "$SUPPLIED_BASE_SHA" ] && + [ "$live_head_ref" = "$SUPPLIED_HEAD_REF" ] && + [ "${live_head_sha,,}" = "${SUPPLIED_HEAD_SHA,,}" ] && + [ "${live_head_sha,,}" = "${HEAD_SHA,,}" ] && + { + { [ "$live_draft" = "false" ] && [ "${DRAFT_REVIEW_ONLY:-false}" = "false" ]; } || + { [ "$live_draft" = "true" ] && authorized_draft_marker_matches; } + } + } + if ! live_authority_matches; then + echo "::error::Pull request authority changed before atomic OpenCode admission." + exit 1 + fi + + definite_receipt_state() { + if [ ! -s "$receipt_helper" ] && ! gh api \ + "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=${GITHUB_SHA}" \ + --jq .content | base64 --decode >"$receipt_helper"; then + echo "::error::Could not load the trusted OpenCode receipt helper." >&2 + return 1 + fi + GH_TOKEN="$TARGET_READ_TOKEN" python3 -c 'import runpy,sys; gate=runpy.run_path(sys.argv[1]); reviews=gate["fetch_reviews"](sys.argv[2],int(sys.argv[3])); receipt,_reason=gate["evaluate_receipts"](reviews,sys.argv[4],is_draft=sys.argv[5] == "true"); print("present" if receipt is not None else "missing")' \ + "$receipt_helper" "$TARGET_REPOSITORY" "$PR_NUMBER" "$HEAD_SHA" "${DRAFT_REVIEW_ONLY:-false}" + } + + complete_receipt_admission() { + if ! live_authority_matches; then + echo "::error::Pull request authority changed before receiver receipt admission." + exit 1 + fi + if ! receiver_receipt_state="$(definite_receipt_state)"; then + echo "::error::Could not revalidate the formal exact-head receipt after lease acquisition." + exit 1 + fi + case "$receiver_receipt_state" in + present) + echo "admitted=false" >>"$GITHUB_OUTPUT" + echo "A formal exact-head OpenCode receipt already exists; duplicate receiver work skipped." + ;; + missing) + echo "admitted=true" >>"$GITHUB_OUTPUT" + ;; + *) + echo "::error::Trusted OpenCode receipt helper returned an invalid state." + exit 1 + ;; + esac + } + + validate_lease_ref() { + jq -e ' + .object.type == "commit" + and (.object.sha | type) == "string" + and (.object.sha | test("^[0-9a-f]{40}$")) + ' >/dev/null + } + if ! lease_ref="$(gh api "$lease_ref_api" 2>/dev/null)"; then + if ! jq -cn --arg ref "refs/heads/${lease_branch}" --arg sha "$GITHUB_SHA" \ + '{ref:$ref,sha:$sha}' | + gh api --method POST \ + repos/ContextualWisdomLab/.github/git/refs --input - >/dev/null; then + lease_ref="$(gh api "$lease_ref_api")" || { + echo "::error::Could not initialize or read the central OpenCode lease branch." + exit 1 + } + printf '%s' "$lease_ref" | validate_lease_ref || { + echo "::error::Central OpenCode lease branch identity was malformed." + exit 1 + } + fi + else + printf '%s' "$lease_ref" | validate_lease_ref || { + echo "::error::Central OpenCode lease branch identity was malformed." + exit 1 + } + fi + + lease_content="$(jq -cn \ + --argjson owner_run_id "$GITHUB_RUN_ID" \ + --arg exact_title "$exact_title" \ + --arg head_sha "$HEAD_SHA" \ + '{owner_run_id:$owner_run_id,exact_title:$exact_title,head_sha:$head_sha}')" + encoded_lease="$(printf '%s' "$lease_content" | base64 | tr -d '\n')" + for lease_attempt in 1 2 3 4 5; do + current_blob_sha="" + if current_file="$(gh api \ + "repos/ContextualWisdomLab/.github/contents/${lease_path}?ref=${lease_branch}" \ + 2>/dev/null)"; then + if ! current_blob_sha="$(jq -er ' + select(.encoding == "base64") + | .sha + | select(type == "string" and test("^[0-9a-f]{40}$")) + ' <<<"$current_file")"; then + echo "::error::Central OpenCode lease metadata was malformed." + exit 1 + fi + if ! current_content="$(jq -er '.content' <<<"$current_file" | base64 --decode)"; then + echo "::error::Central OpenCode lease content was not valid base64." + exit 1 + fi + if ! owner_run_id="$(jq -er ' + .owner_run_id | select(type == "number" and . > 0) + ' <<<"$current_content")" || + ! owner_title="$(jq -er \ + --arg prefix "OpenCode Review Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@" ' + .exact_title + | select( + type == "string" + and ((ascii_downcase) | startswith($prefix | ascii_downcase)) + ) + ' <<<"$current_content")" || + ! owner_head="$(jq -er \ + '.head_sha | select(type == "string" and test("^[0-9a-fA-F]{40}$"))' \ + <<<"$current_content")"; then + echo "::error::Central OpenCode lease identity was malformed." + exit 1 + fi + expected_owner_title="OpenCode Review Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${owner_head}" + if [ "${owner_title,,}" != "${expected_owner_title,,}" ]; then + echo "::error::Central OpenCode lease title and head identity disagreed." + exit 1 + fi + if [ "$owner_run_id" = "$GITHUB_RUN_ID" ] && + [ "${owner_head,,}" = "${HEAD_SHA,,}" ]; then + complete_receipt_admission + [ "$receiver_receipt_state" = "missing" ] || exit 0 + echo "Central exact-head rerun ${GITHUB_RUN_ID} retained its canonical lease." + exit 0 + fi + if ! owner_run="$(gh api \ + "repos/ContextualWisdomLab/.github/actions/runs/${owner_run_id}")"; then + echo "::error::Could not validate the central OpenCode lease owner." + exit 1 + fi + if ! owner_status="$(jq -er \ + --argjson owner "$owner_run_id" --arg title "$owner_title" ' + select( + .id == $owner + and .path == ".github/workflows/opencode-review-dispatch.yml" + and .event == "repository_dispatch" + and (.display_title | ascii_downcase) == ($title | ascii_downcase) + and (.status | type) == "string" + ) + | .status + ' <<<"$owner_run")"; then + echo "::error::Central OpenCode lease owner identity was malformed." + exit 1 + fi + case "$owner_status" in + requested|waiting|pending|queued|in_progress|completed) ;; + *) + echo "::error::Central OpenCode lease owner returned invalid status ${owner_status:-}." + exit 1 + ;; + esac + if [ "${owner_head,,}" = "${HEAD_SHA,,}" ] && + [ "$owner_status" != "completed" ]; then + echo "admitted=false" >>"$GITHUB_OUTPUT" + echo "Central exact-head dispatch ${GITHUB_RUN_ID} deferred to active lease owner ${owner_run_id}." + exit 0 + fi + fi + + lease_request="$(jq -cn \ + --arg message "Acquire OpenCode exact-head lease for run ${GITHUB_RUN_ID}" \ + --arg content "$encoded_lease" \ + --arg branch "$lease_branch" \ + --arg sha "$current_blob_sha" ' + {message:$message,content:$content,branch:$branch} + + if $sha == "" then {} else {sha:$sha} end + ')" + if ! live_authority_matches; then + echo "::error::Pull request authority changed before the atomic OpenCode lease mutation." + exit 1 + fi + if lease_result="$(printf '%s' "$lease_request" | gh api --method PUT \ + "repos/ContextualWisdomLab/.github/contents/${lease_path}" --input - \ + 2>/dev/null)"; then + if ! lease_blob_sha="$(jq -er ' + .content.sha + | select(type == "string" and test("^[0-9a-f]{40}$")) + ' <<<"$lease_result")"; then + echo "::error::Central OpenCode lease acquisition returned malformed metadata." + exit 1 + fi + echo "Central exact-head dispatch ${GITHUB_RUN_ID} acquired the atomic lease." + complete_receipt_admission + [ "$receiver_receipt_state" = "missing" ] || exit 0 + exit 0 + fi + echo "Central exact-head lease changed during attempt ${lease_attempt}; retrying from the authoritative branch." + done + echo "::error::Could not acquire the atomic central OpenCode lease after bounded retries." + exit 1 + + validate-pr-metadata: name: validate-pr-metadata + needs: [admit-exact-head-dispatch] # Folded together with the former coverage-source-tree job (2026-09-17): # both jobs only ever exchanged the OpenCode app token for READ-scoped # data (target-repository metadata, then the PR merge tree) and neither @@ -52,7 +450,10 @@ jobs: # for the measurement (run 34931908846: 21 minutes of job execution # inside a 13h57m run, ~97.5% of which was queue wait between exactly # these job boundaries). - if: github.event_name == 'repository_dispatch' + if: >- + needs.admit-exact-head-dispatch.result == 'success' + && needs.admit-exact-head-dispatch.outputs.admitted == 'true' + && github.event_name == 'repository_dispatch' runs-on: group: CWL central OpenCode labels: [self-hosted, linux, x64] @@ -70,6 +471,7 @@ jobs: base_sha: ${{ steps.validate.outputs.base_sha }} head_ref: ${{ steps.validate.outputs.head_ref }} head_sha: ${{ steps.validate.outputs.head_sha }} + is_draft: ${{ steps.validate.outputs.is_draft }} is_private: ${{ steps.validate.outputs.is_private }} steps: - name: Exchange OpenCode app token for target repository metadata reads @@ -160,6 +562,7 @@ jobs: 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 || '' }} + DRAFT_REVIEW_ONLY: ${{ toJSON(github.event.client_payload.draft_review_only) }} run: | set -euo pipefail if [ "$EVENT_NAME" = "repository_dispatch" ]; then @@ -215,6 +618,9 @@ jobs: live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")" live_head_ref="$(jq -r '.head.ref // empty' <<<"$pull_request_json")" live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")" + live_draft="$(jq -r \ + 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end' \ + <<<"$pull_request_json")" live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" live_visibility="$(jq -r '.base.repo.visibility // empty | ascii_downcase' <<<"$pull_request_json")" case "$live_visibility" in @@ -228,6 +634,7 @@ jobs: ! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || ! [[ "$live_base_sha" =~ ^[0-9a-fA-F]{40}$ ]] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] || + ! [[ "$live_draft" =~ ^(true|false)$ ]] || ! [[ "$live_is_private" =~ ^(true|false)$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then @@ -241,6 +648,7 @@ jobs: [ "$SUPPLIED_BASE_SHA" = "$live_base_sha" ] || mismatches+=("base_sha") [ "$SUPPLIED_HEAD_REF" = "$live_head_ref" ] || mismatches+=("head_ref") [ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ] || mismatches+=("head_sha") + [ "$DRAFT_REVIEW_ONLY" = "$live_draft" ] || mismatches+=("draft_review_only") if [ "${#mismatches[@]}" -gt 0 ]; then printf '::error::repository_dispatch metadata does not match the live pull request: %s. supplied_base=%s/%s live_base=%s/%s supplied_head=%s/%s live_head=%s/%s\n' "$(IFS=,; printf '%s' "${mismatches[*]}")" "${SUPPLIED_BASE_REF:-}" "${SUPPLIED_BASE_SHA:-}" "$live_base_ref" "$live_base_sha" "${SUPPLIED_HEAD_REF:-}" "${SUPPLIED_HEAD_SHA:-}" "$live_head_ref" "$live_head_sha" exit 1 @@ -254,6 +662,7 @@ jobs: printf 'base_sha=%s\n' "$live_base_sha" printf 'head_ref=%s\n' "$live_head_ref" printf 'head_sha=%s\n' "$live_head_sha" + printf 'is_draft=%s\n' "$live_draft" printf 'is_private=%s\n' "$live_is_private" } >>"$GITHUB_OUTPUT" printf 'Validated current live metadata for %s#%s: base=%s/%s head=%s/%s.\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$live_base_ref" "$live_base_sha" "$live_head_ref" "$live_head_sha" @@ -392,8 +801,11 @@ jobs: coverage-evidence: name: coverage-evidence - needs: [validate-pr-metadata] + needs: [admit-exact-head-dispatch, validate-pr-metadata] if: >- + needs.admit-exact-head-dispatch.result == 'success' + && needs.admit-exact-head-dispatch.outputs.admitted == 'true' + && needs.validate-pr-metadata.result == 'success' && github.event_name == 'repository_dispatch' runs-on: @@ -2441,7 +2853,12 @@ jobs: if [ "$not_measured" -ne 0 ]; then append "- Not measured: ${not_measured} Rust coverage command(s) exceeded the 900 s per-command cap; line coverage for that code is unproven, not failed." fi - if [ "$failures" -eq 0 ]; then + if [ "$failures" -eq 0 ] && [ "$not_measured" -ne 0 ]; then + append "- Result: NOT MEASURED" + append "- Test evidence: incomplete; timed-out Rust suites are not proven passing." + append "- Coverage thresholds: not proven satisfied for unmeasured Rust code." + append "- Review handling: advisory only; this result is not approval evidence and does not replace required verification." + elif [ "$failures" -eq 0 ]; then append "- Result: PASS" if [ "$measured_any" -eq 0 ]; then append "- Test coverage: not applicable (no supported changed source files or package manifests)" @@ -2517,18 +2934,14 @@ jobs: opencode-review-target: name: opencode-review - needs: [validate-pr-metadata, coverage-evidence] + needs: [admit-exact-head-dispatch, validate-pr-metadata, coverage-evidence] if: >- always() + && needs.admit-exact-head-dispatch.result == 'success' + && needs.admit-exact-head-dispatch.outputs.admitted == 'true' && needs.validate-pr-metadata.result == 'success' && needs.coverage-evidence.result != 'cancelled' && github.event_name == 'repository_dispatch' - concurrency: - group: >- - opencode-review-${{ - needs.validate-pr-metadata.outputs.target_repository }}-${{ - needs.validate-pr-metadata.outputs.pr_number || github.run_id }} - cancel-in-progress: true runs-on: group: CWL central OpenCode labels: [self-hosted, linux, x64] @@ -4519,6 +4932,7 @@ jobs: && needs.coverage-evidence.result == 'success' && steps.opencode_review_model_pool.outputs.review_status == 'success' && steps.central_review_process_fallback_scope.outputs.eligible == 'true' + && needs.validate-pr-metadata.outputs.is_draft == 'false' continue-on-error: true # Keep the normal peer-check hold short, but leave bounded room for # dynamic image/package-build extensions and review publication overhead. @@ -4858,6 +5272,7 @@ jobs: NO_COLOR: "1" PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + PR_DRAFT: ${{ needs.validate-pr-metadata.outputs.is_draft }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} OPENCODE_MODEL_POOL_OUTCOME: ${{ steps.opencode_review_model_pool.outputs.review_status }} @@ -4886,6 +5301,10 @@ jobs: OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" run: | set -euo pipefail + if ! [[ "$PR_DRAFT" =~ ^(true|false)$ ]]; then + echo "::error::OpenCode publication rejected malformed live Draft metadata." + exit 1 + fi quoted_coverage="${COVERAGE_EVIDENCE_RESULT:-}" COVERAGE_EVIDENCE_RESULT="$(python3 scripts/ci/opencode_coverage_identity.py \ --repo "$GH_REPOSITORY" \ @@ -5176,6 +5595,11 @@ jobs: local gh_error_file local review_payload_file local review_response_file + if [ "$event" = "APPROVE" ] && [ "$PR_DRAFT" = "true" ]; then + event="COMMENT" + body="${body//- Result: APPROVE/- Result: DRAFT_REVIEW_COMPLETE}" + body="${body}"$'\n\n'"Draft review-only request completed without publishing merge approval authority." + fi if [ -z "${review_write_token:-}" ]; then printf '::error::OPENCODE_REVIEW_IDENTITY_UNAVAILABLE: refusing to publish %s with a GitHub Actions or PAT identity for head %s.\n' "$event" "$HEAD_SHA" return 1 @@ -7052,6 +7476,12 @@ jobs: grep -Fq -- "- R test evidence: deferred package-load failures require a successful current-head peer R CMD check" } + coverage_decision_is_pass() { + local coverage_decision + coverage_decision="$(printf '%s\n' "${COVERAGE_EVIDENCE_SUMMARY:-}" | grep '^- Result:' || true)" + [ "$coverage_decision" = '- Result: PASS' ] + } + collect_successful_r_cmd_check_evidence() { local output_file="$1" if ! gh pr checks "$PR_NUMBER" \ @@ -7149,7 +7579,8 @@ jobs: publish_blockers_after_model_unavailable() { local pending_wait_status body - if [ "${COVERAGE_EVIDENCE_RESULT:-skipped}" != "success" ]; then + if [ "${COVERAGE_EVIDENCE_RESULT:-skipped}" != "success" ] || + ! coverage_decision_is_pass; then return 1 fi @@ -7278,6 +7709,7 @@ jobs: printf '%s\n' "$reviews_json" | python3 scripts/ci/opencode_existing_approval_gate.py \ --head "$HEAD_SHA" \ + --coverage-summary "$COVERAGE_EVIDENCE_SUMMARY" \ --require-opencode-app } @@ -7481,6 +7913,10 @@ jobs: echo "::endgroup::" exit 1 fi + if ! coverage_decision_is_pass; then + stop_approval_without_review "COVERAGE_NOT_MEASURED" \ + "Coverage measurement is incomplete or its decision is missing. A successful advisory job is not approval evidence and does not replace required verification." + fi if request_changes_for_merge_conflict_if_present; then echo "::endgroup::" exit 0 @@ -7773,6 +8209,7 @@ jobs: if: >- always() && needs.validate-pr-metadata.result == 'success' + && needs.validate-pr-metadata.outputs.is_draft == 'false' && needs.validate-pr-metadata.outputs.target_repository != '' && needs.validate-pr-metadata.outputs.pr_number != '' && needs.validate-pr-metadata.outputs.head_sha != '' @@ -7783,29 +8220,24 @@ jobs: PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} run: | set -euo pipefail - draft_args=() - if [ "$(gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.draft')" = "true" ]; then - draft_args=(--draft) - fi python3 scripts/ci/opencode_review_receipt_gate.py \ --repo "$GH_REPOSITORY" \ --pr-number "$PR_NUMBER" \ - --head-sha "$PR_HEAD_SHA" \ - "${draft_args[@]}" + --head-sha "$PR_HEAD_SHA" - - name: Wake exact-head required OpenCode workflow + - name: Wake every failed exact-head Required OpenCode workflow if: >- always() && github.event_name == 'repository_dispatch' && steps.formal_review_receipt.outcome == 'success' + && needs.validate-pr-metadata.outputs.is_draft == 'false' && needs.validate-pr-metadata.outputs.target_repository != '' && needs.validate-pr-metadata.outputs.head_sha != '' - && github.event.client_payload.required_run_id != '' env: GH_TOKEN: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} GH_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }} + PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} - REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id }} WAKE_TOKEN_SOURCE: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || 'unavailable' }} run: | set -euo pipefail @@ -7813,45 +8245,159 @@ jobs: echo "::error::Actions-capable wake credential is unavailable. Native runs use github.token; sibling runs require PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN." exit 1 fi - [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]] || { - echo "::error::Required OpenCode run id is missing or non-canonical." + live_authority_matches() { + local live_pr live_state live_draft live_head live_created_at + live_pr="$(gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}")" || return 1 + live_state="$(jq -r '.state // empty' <<<"$live_pr")" + live_draft="$(jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end' <<<"$live_pr")" + live_head="$(jq -r '.head.sha // empty' <<<"$live_pr")" + live_created_at="$(jq -r '.created_at // empty' <<<"$live_pr")" + [ "$live_state" = "open" ] && [ "$live_draft" = "false" ] && + [ "${live_head,,}" = "${PR_HEAD_SHA,,}" ] && + [[ "$live_created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || + return 1 + LIVE_PR_CREATED_AT="$live_created_at" + } + if ! live_authority_matches; then + echo "::error::Pull request authority changed before exact-head Required OpenCode wake inventory." exit 1 + fi + inventory_start="$LIVE_PR_CREATED_AT" + collect_required_run_pages() { + local range_start range_end range_total range_probe + local start_epoch end_epoch midpoint_epoch midpoint_time right_start + local probe_url page_url page_output raw_count expected_total=0 + local range_index=0 + local -a range_starts=("$inventory_start") + local -a range_ends=("$(date -u +%Y-%m-%dT%H:%M:%SZ)") + local inventory_file + inventory_file="$(mktemp)" + while [ "$range_index" -lt "${#range_starts[@]}" ]; do + range_start="${range_starts[$range_index]}" + range_end="${range_ends[$range_index]}" + range_index=$((range_index + 1)) + probe_url="repos/${GH_REPOSITORY}/actions/runs?event=pull_request_target&created=${range_start}..${range_end}&per_page=1" + if ! range_probe="$(gh api "$probe_url")" || + ! range_total="$(jq -er ' + .total_count + | if type == "number" and floor == . and . >= 0 then tostring + elif . == "2,500+" then "overflow" + else error("invalid total_count") + end + ' <<<"$range_probe")"; then + echo "::error::Could not validate a bounded Required OpenCode run interval." >&2 + rm -f "$inventory_file" + return 1 + fi + if [ "$range_total" = "overflow" ] || [ "$range_total" -gt 1000 ]; then + start_epoch="$(date -u -d "$range_start" +%s)" || return 1 + end_epoch="$(date -u -d "$range_end" +%s)" || return 1 + if [ "$start_epoch" -ge "$end_epoch" ]; then + echo "::error::More than 1,000 pull_request_target runs share one second; exact inventory cannot be proven." >&2 + rm -f "$inventory_file" + return 1 + fi + midpoint_epoch=$(((start_epoch + end_epoch) / 2)) + midpoint_time="$(date -u -d "@${midpoint_epoch}" +%Y-%m-%dT%H:%M:%SZ)" + right_start="$(date -u -d "@$((midpoint_epoch + 1))" +%Y-%m-%dT%H:%M:%SZ)" + range_starts+=("$range_start" "$right_start") + range_ends+=("$midpoint_time" "$range_end") + continue + fi + page_url="repos/${GH_REPOSITORY}/actions/runs?event=pull_request_target&created=${range_start}..${range_end}&per_page=100" + if ! page_output="$(gh api --paginate "$page_url")"; then + echo "::error::Could not list a bounded Required OpenCode run interval." >&2 + rm -f "$inventory_file" + return 1 + fi + printf '%s\n' "$page_output" >>"$inventory_file" + expected_total=$((expected_total + range_total)) + done + if ! raw_count="$(jq -e -s '[.[] | .workflow_runs[]] | length' "$inventory_file")" || + [ "$raw_count" -ne "$expected_total" ]; then + printf '::error::Required OpenCode inventory was truncated: expected=%s collected=%s.\n' "$expected_total" "${raw_count:-invalid}" >&2 + rm -f "$inventory_file" + return 1 + fi + cat "$inventory_file" + rm -f "$inventory_file" } - # The immutable run id is scoped to GH_REPOSITORY. Revalidate its - # event, central workflow path, and live PR head before rerunning it; - # rendered titles and workflow_url differ between native and - # organization-required workflow contexts. for attempt in $(seq 1 12); do - run="$(gh api "repos/${GH_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")" - required_run="$(printf '%s\n' "$run" | jq -r --arg head "$PR_HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' - select(.id == $run_id) - | select(.event == "pull_request_target") - | select(.path == ".github/workflows/opencode-review.yml") - | select(.head_sha == $head) - | [(.id // ""), (.status // ""), (.conclusion // "")] - | @tsv - ')" - IFS=$'\t' read -r required_run_id required_status required_conclusion <<<"$required_run" - if [ "$required_status" = "completed" ] && [ "$required_conclusion" = "failure" ]; then - gh api -X POST "repos/${GH_REPOSITORY}/actions/runs/${required_run_id}/rerun-failed-jobs" >/dev/null - echo "Re-ran failed jobs for exact-head Required OpenCode Review run ${required_run_id}." + if ! run_pages="$(collect_required_run_pages)"; then + echo "::error::Could not list exact-head Required OpenCode workflow runs." + exit 1 + fi + if ! run_inventory="$(jq -c -s --arg head "$PR_HEAD_SHA" --argjson pr "$PR_NUMBER" ' + if all(.[]; (.workflow_runs | type) == "array") + and all( + .[] | .workflow_runs[]; + (.id | type) == "number" + and (.event | type) == "string" + and (.path | type) == "string" + and (.head_sha | type) == "string" + and (.status | type) == "string" + and ((.conclusion == null) or (.conclusion | type) == "string") + and (.pull_requests | type) == "array" + and all( + .pull_requests[]; + (.number | type) == "number" + and (.head.sha | type) == "string" + ) + ) + then + [ + .[] | .workflow_runs[] + | select( + .event == "pull_request_target" + and .path == ".github/workflows/opencode-review.yml" + and any( + .pull_requests[]; + .number == $pr + and (.head.sha | ascii_downcase) == ($head | ascii_downcase) + ) + ) + ] | unique_by(.id) + else + error("invalid required-run inventory") + end + ' <<<"$run_pages")"; then + echo "::error::Exact-head Required OpenCode workflow inventory was malformed." + exit 1 + fi + failed_ids="$(jq -r ' + .[] + | select(.status == "completed" and .conclusion == "failure") + | .id + ' <<<"$run_inventory")" + if [ -n "$failed_ids" ]; then + while IFS= read -r failed_id; do + [ -n "$failed_id" ] || continue + if ! live_authority_matches; then + echo "::error::Pull request authority changed before Required OpenCode rerun ${failed_id}." + exit 1 + fi + gh api -X POST \ + "repos/${GH_REPOSITORY}/actions/runs/${failed_id}/rerun-failed-jobs" >/dev/null + echo "Re-ran failed jobs for exact-head Required OpenCode Review run ${failed_id}." + done <<<"$failed_ids" exit 0 fi - if [ "$required_status" = "completed" ] && [ "$required_conclusion" = "success" ]; then - echo "Exact-head Required OpenCode Review run ${required_run_id} already succeeded." + if jq -e 'any(.[]; .status == "completed" and .conclusion == "success")' <<<"$run_inventory" >/dev/null; then + echo "An exact-head Required OpenCode Review run already succeeded." exit 0 fi if [ "$attempt" -lt 12 ]; then sleep 5 fi done - echo "::error::Formal OpenCode receipt exists, but the exact-head required workflow did not reach a rerunnable failed state." + echo "::error::Formal OpenCode receipt exists, but no exact-head required workflow reached a rerunnable failed or successful state." exit 1 - name: Publish repository_dispatch OpenCode status if: >- always() && github.event_name == 'repository_dispatch' + && needs.validate-pr-metadata.outputs.is_draft == 'false' && needs.validate-pr-metadata.outputs.target_repository != '' && needs.validate-pr-metadata.outputs.head_sha != '' env: @@ -7862,6 +8408,7 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} OPENCODE_MODEL_POOL_OUTCOME: ${{ steps.opencode_review_model_pool.outputs.review_status }} COVERAGE_EVIDENCE_RESULT: ${{ needs.coverage-evidence.result }} + COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_STATUS_TOKEN_SOURCE: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }} @@ -7895,6 +8442,7 @@ jobs: python3 scripts/ci/opencode_dispatch_status.py \ --model-outcome "${OPENCODE_MODEL_POOL_OUTCOME:-missing}" \ --coverage-result "${COVERAGE_EVIDENCE_RESULT:-missing}" \ + --coverage-summary "${COVERAGE_EVIDENCE_SUMMARY:-}" \ --expected-head "$PR_HEAD_SHA" \ --pull-request-file "$pull_request_file" \ --reviews-file "$reviews_file" @@ -7916,6 +8464,7 @@ jobs: if: >- always() && github.event_name == 'repository_dispatch' + && needs.validate-pr-metadata.outputs.is_draft == 'false' && needs.validate-pr-metadata.outputs.target_repository != '' && needs.validate-pr-metadata.outputs.pr_number != '' && needs.validate-pr-metadata.outputs.head_sha != '' @@ -7944,6 +8493,9 @@ jobs: --interval-seconds 10 - name: Run merge scheduler after approval + if: >- + always() + && needs.validate-pr-metadata.outputs.is_draft == 'false' continue-on-error: true env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }} @@ -7954,6 +8506,7 @@ jobs: PR_BASE_REF: ${{ needs.validate-pr-metadata.outputs.base_ref }} PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }} OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head @@ -7985,6 +8538,7 @@ jobs: if printf '%s\n' "$reviews_json" | python3 scripts/ci/opencode_existing_approval_gate.py \ --head "$PR_HEAD_SHA" \ + --coverage-summary "$COVERAGE_EVIDENCE_SUMMARY" \ --require-opencode-app \ 2>"$gate_error_file"; then approval_visible=1 diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index bbe692b417..4918b5c641 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -501,6 +501,132 @@ jobs: exit 1 fi echo "::add-mask::$app_token" + + # GitHub native concurrency replaces an older pending group member + # even when cancel-in-progress is false, so the receiver deliberately + # has no lossy concurrency group. Avoid duplicate work here instead: + # inventory every active admission state twice so a transition cannot + # disappear between status queries, and collect older-head central + # runs for exact-identity retirement before posting the current head. + active_dispatch_title="OpenCode Review Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${HEAD_SHA}" + active_dispatch_prefix="OpenCode Review Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@" + stale_dispatches_file="$(mktemp)" + same_head_found=false + trap 'rm -f "$helper" "$stale_dispatches_file"' EXIT + for inventory_pass in 1 2; do + for active_status in requested waiting pending queued in_progress; do + runs_url="repos/ContextualWisdomLab/.github/actions/workflows/opencode-review-dispatch.yml/runs?event=repository_dispatch&status=${active_status}&per_page=100" + if ! active_runs="$(GH_TOKEN="$app_token" gh api --paginate "$runs_url")"; then + echo "::error::Could not inspect active OpenCode dispatches; refusing a duplicate scheduler wake." + exit 1 + fi + if ! same_head_active="$(jq -r -s --arg title "$active_dispatch_title" --arg status "$active_status" ' + if all(.[]; (.workflow_runs | type) == "array") + and all( + .[] | .workflow_runs[]; + (.id | type) == "number" + and .path == ".github/workflows/opencode-review-dispatch.yml" + and .event == "repository_dispatch" + and .status == $status + and (.display_title | type) == "string" + ) + then + any( + .[] | .workflow_runs[]; + (.display_title | ascii_downcase) == ($title | ascii_downcase) + ) + else + error("invalid workflow-runs response") + end + ' <<<"$active_runs")"; then + echo "::error::Active OpenCode dispatch data was invalid; refusing a duplicate scheduler wake." + exit 1 + fi + case "$same_head_active" in + true) same_head_found=true ;; + false) ;; + *) + echo "::error::Active OpenCode dispatch guard returned an invalid state." + exit 1 + ;; + esac + jq -r -s --arg prefix "$active_dispatch_prefix" --arg title "$active_dispatch_title" ' + .[] | .workflow_runs[] + | select((.display_title | ascii_downcase) | startswith($prefix | ascii_downcase)) + | select((.display_title | ascii_downcase) != ($title | ascii_downcase)) + | .id + ' <<<"$active_runs" >>"$stale_dispatches_file" + done + done + + live_authority_matches() { + local live_pr live_head live_draft live_state + live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" || return 1 + live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" + live_draft="$(printf '%s' "$live_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')" + live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" + [ "$live_state" = "open" ] && [ "$live_draft" = "false" ] && + [ "${live_head,,}" = "${HEAD_SHA,,}" ] + } + if ! live_authority_matches; then + echo "Pull request authority changed before OpenCode dispatch; scheduler wake skipped." + exit 0 + fi + + while IFS= read -r stale_run_id; do + [ -n "$stale_run_id" ] || continue + if ! live_authority_matches; then + echo "Pull request authority changed before stale OpenCode retirement; scheduler wake skipped." + exit 0 + fi + if ! GH_TOKEN="$app_token" gh api --method POST \ + "repos/ContextualWisdomLab/.github/actions/runs/${stale_run_id}/cancel" >/dev/null; then + echo "::error::Could not cancel superseded central OpenCode dispatch ${stale_run_id}; refusing to add current-head work behind it." + exit 1 + fi + cancellation_verified=false + for cancellation_attempt in 1 2 3 4 5 6; do + if ! stale_state="$(GH_TOKEN="$app_token" gh api \ + "repos/ContextualWisdomLab/.github/actions/runs/${stale_run_id}" \ + --jq '[.status // "", .conclusion // ""] | @tsv')"; then + echo "::error::Could not verify superseded central OpenCode dispatch ${stale_run_id} cancellation." + exit 1 + fi + IFS=$'\t' read -r stale_status stale_conclusion <<<"$stale_state" + if [ "$stale_status" = "completed" ] && + [ "$stale_conclusion" = "cancelled" ]; then + cancellation_verified=true + break + fi + case "$stale_status" in + requested|waiting|pending|queued|in_progress) + ;; + completed) + echo "::error::Superseded central OpenCode dispatch ${stale_run_id} completed with non-cancelled conclusion ${stale_conclusion:-}." + exit 1 + ;; + *) + echo "::error::Superseded central OpenCode dispatch ${stale_run_id} returned invalid status ${stale_status:-}." + exit 1 + ;; + esac + done + if [ "$cancellation_verified" != "true" ]; then + echo "::error::Superseded central OpenCode dispatch ${stale_run_id} did not reach completed/cancelled after accepted cancellation." + exit 1 + fi + echo "Verified cancelled superseded central OpenCode dispatch ${stale_run_id} after live exact-head revalidation." + done < <(sort -nu "$stale_dispatches_file") + + if [ "$same_head_found" = "true" ]; then + echo "An exact-head OpenCode dispatch is already active; scheduler wake skipped after older-head retirement." + exit 0 + fi + + if ! live_authority_matches; then + echo "Pull request authority changed before OpenCode dispatch; scheduler wake skipped." + exit 0 + fi jq -cn \ --arg target_repository "$TARGET_REPOSITORY" \ --arg pr_number "$PR_NUMBER" \ @@ -509,7 +635,8 @@ jobs: --arg pr_head_ref "$HEAD_REF" \ --arg pr_head_sha "$HEAD_SHA" \ --arg required_run_id "$GITHUB_RUN_ID" \ - '{event_type:"opencode-review",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,required_run_id:$required_run_id}}' | + --argjson draft_review_only false \ + '{event_type:"opencode-review",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,draft_review_only:$draft_review_only,required_run_id:$required_run_id}}' | GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input - - name: Fail closed without a current-head OpenCode verdict diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 7777f02bed..30c6d659e2 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -87,8 +87,18 @@ concurrency: github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != '' && format('target-{0}-pr-{1}', github.event.client_payload.target_repository, github.event.client_payload.pr_number) || github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) || github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository) || - github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }} + github.ref }}-${{ + github.event_name == 'pull_request_target' && github.event.action == 'closed' && format('head-{0}-closed', github.event.pull_request.head.sha) || + github.event_name == 'pull_request_target' && format('head-{0}', github.event.pull_request.head.sha) || + github.event_name == 'pull_request_review' && format('head-{0}', github.event.pull_request.head.sha) || + github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' && format('head-{0}', github.event.client_payload.pr_head_sha) || + 'no-head' }} + # Preserve same-head admissions up to GitHub's documented pending limit + # without single-pending replacement; dispatch ordering remains + # platform-controlled. A new head receives a distinct group; the metadata-only + # cleanup job below retires only revalidated predecessor work and proves + # terminal cancellation. + queue: max # Scorecard Token-Permissions (alert #9): declare a least-privilege default at # the workflow level. The scan-pr-queue job that actually needs write access @@ -98,21 +108,183 @@ permissions: contents: read jobs: + cancel-superseded-pr-runs: + if: >- + github.event_name == 'pull_request_target' && + ( + github.event.action == 'synchronize' || + github.event.action == 'closed' + ) + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + actions: write + contents: read + pull-requests: read + env: + GH_TOKEN: ${{ github.token }} + TARGET_REPOSITORY: ${{ github.repository }} + TARGET_PR_NUMBER: ${{ github.event.pull_request.number }} + TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + TARGET_ACTION: ${{ github.event.action }} + steps: + - name: Cancel revalidated predecessor scheduler runs + shell: bash + run: | + set -euo pipefail + + if ! [[ "$TARGET_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || + ! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || + ! [[ "$TARGET_PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Superseded-run cleanup rejected malformed target identity." + exit 1 + fi + + live_target_matches() { + local live_pull live_repository live_number live_state live_head_sha + live_pull="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")" + live_repository="$(jq -r '.base.repo.full_name // empty' <<<"$live_pull")" + live_number="$(jq -r '.number // 0' <<<"$live_pull")" + live_state="$(jq -r '.state // empty' <<<"$live_pull")" + live_head_sha="$(jq -r '.head.sha // empty' <<<"$live_pull")" + [ "$live_repository" = "$TARGET_REPOSITORY" ] && + [ "$live_number" = "$TARGET_PR_NUMBER" ] && + [ "${live_head_sha,,}" = "${TARGET_PR_HEAD_SHA,,}" ] && { + [ "$TARGET_ACTION:$live_state" = "synchronize:open" ] || + [ "$TARGET_ACTION:$live_state" = "closed:closed" ] + } + } + + if ! live_target_matches; then + echo "Superseded-run cleanup skipped because the event no longer matches the live pull request." + exit 0 + fi + + inventory_dir="$(mktemp -d)" + trap 'rm -rf "$inventory_dir"' EXIT + : >"$inventory_dir/active-arrays.jsonl" + for inventory_pass in 1 2; do + for active_status in requested waiting pending queued in_progress; do + inventory_path="$inventory_dir/${inventory_pass}-${active_status}.json" + gh api --paginate --slurp \ + "repos/${TARGET_REPOSITORY}/actions/workflows/pr-review-merge-scheduler.yml/runs?status=${active_status}&per_page=100" \ + >"$inventory_path" + if ! inventory_counts="$(jq -er ' + if type == "array" and length > 0 + and all(.[]; (.total_count | type) == "number") + and all(.[]; (.workflow_runs | type) == "array") + then + [([.[] | .workflow_runs[]] | length), (map(.total_count) | max)] + | @tsv + else + error("invalid workflow-run inventory") + end + ' "$inventory_path")"; then + echo "::error::Scheduler workflow-run inventory was invalid." + exit 1 + fi + IFS=$'\t' read -r collected_count expected_count <<<"$inventory_counts" + if [ "$collected_count" -ne "$expected_count" ]; then + echo "::error::Scheduler workflow-run inventory was incomplete for ${active_status}: expected=${expected_count} collected=${collected_count}." + exit 1 + fi + jq -c '[.[] | .workflow_runs[]]' \ + "$inventory_path" \ + >>"$inventory_dir/active-arrays.jsonl" + done + done + workflow_runs="$(jq -sc 'add // [] | unique_by(.id)' "$inventory_dir/active-arrays.jsonl")" + mapfile -t superseded_run_ids < <( + jq -r \ + --argjson pr_number "$TARGET_PR_NUMBER" \ + --argjson current_run_id "$GITHUB_RUN_ID" \ + --arg target_head "${TARGET_PR_HEAD_SHA,,}" \ + --arg target_action "$TARGET_ACTION" \ + ' + .[] + | select(.id != $current_run_id) + | select( + .status == "requested" or + .status == "waiting" or + .status == "pending" or + .status == "queued" or + .status == "in_progress" + ) + | select( + any( + .pull_requests[]?; + (.head.sha // "" | ascii_downcase) as $run_pr_head + | .number == $pr_number + | select( + $target_action == "closed" or + ( + ($run_pr_head | test("^[0-9a-f]{40}$")) and + $run_pr_head != $target_head + ) + ) + ) + ) + | .id + ' <<<"$workflow_runs" + ) + + for run_id in "${superseded_run_ids[@]}"; do + if ! live_target_matches; then + echo "Superseded-run cleanup stopped because the target changed before cancellation." + exit 0 + fi + if gh api --method POST \ + "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" \ + >/dev/null 2>&1; then + cancellation_verified=false + for attempt in 1 2 3 4 5 6; do + IFS=$'\t' read -r run_status run_conclusion < <( + gh api "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}" \ + --jq '[.status // "", .conclusion // ""] | @tsv' + ) + if [ "$run_status" = "completed" ] && + [ "$run_conclusion" = "cancelled" ]; then + cancellation_verified=true + break + fi + if [ "$attempt" -lt 6 ]; then + sleep 1 + fi + done + if [ "$cancellation_verified" != "true" ]; then + echo "::error::Scheduler run $run_id did not reach completed/cancelled after accepted force-cancel." + exit 1 + fi + echo "Verified cancelled scheduler run $run_id." + continue + fi + IFS=$'\t' read -r run_status run_conclusion < <( + gh api "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}" \ + --jq '[.status // "", .conclusion // ""] | @tsv' + ) + if [ "$run_status" = "completed" ]; then + continue + fi + echo "::error::Could not force-cancel nonterminal scheduler run $run_id." + exit 1 + done + scan-pr-queue: # repository_dispatch review runs do not reliably carry pull_requests metadata. # Without this guard, one completed central review can wake a repo-wide scan. - # Draft PRs get no runner; ready_for_review re-runs this on the same head. + # Ordinary Draft events get no runner. converted_to_draft and closed use the + # control runner only to retire stale central dispatches; inspect_pr returns + # before review, branch, auto-merge, or merge admission. if: >- ( - ( - github.event_name != 'pull_request_target' || - github.event.action != 'closed' - ) && - ( - github.event_name != 'repository_dispatch' || - github.event.client_payload.org_sweep != true - ) - ) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') + github.event_name != 'repository_dispatch' || + github.event.client_payload.org_sweep != true + ) && ( + github.event_name != 'pull_request_target' || + github.event.action == 'closed' || + github.event.action == 'converted_to_draft' || + github.event.pull_request.draft != true + ) # The group admits only trusted central main workflows, including reusable # callers. Keep admission/dispatch off pools occupied by model execution. runs-on: @@ -219,6 +391,7 @@ jobs: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }} TARGET_REPOSITORY_INPUT: ${{ github.event.client_payload.target_repository || '' }} TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number || '' }} + TARGET_HEAD_SHA_INPUT: ${{ github.event.client_payload.pr_head_sha || '' }} TARGET_BASE_BRANCH_INPUT: ${{ github.event.client_payload.base_branch || '' }} ALLOWED_TARGET_REPOSITORIES: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }} run: | @@ -238,8 +411,9 @@ jobs: exit 1 fi if ! [[ "$TARGET_REPOSITORY_INPUT" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || - ! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then - printf '::error::Targeted scheduler dispatch rejected an invalid repository or pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY_INPUT:-}" "${TARGET_PR_NUMBER:-}" + ! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || + ! [[ "$TARGET_HEAD_SHA_INPUT" =~ ^[0-9a-fA-F]{40}$ ]]; then + printf '::error::Targeted scheduler dispatch rejected invalid repository, pull request, or head identity. target=%s pr=%s head=%s\n' "${TARGET_REPOSITORY_INPUT:-}" "${TARGET_PR_NUMBER:-}" "${TARGET_HEAD_SHA_INPUT:-}" exit 1 fi @@ -281,6 +455,10 @@ jobs: printf '::error::Targeted scheduler dispatch base branch does not match the live PR. supplied=%s live=%s\n' "$TARGET_BASE_BRANCH_INPUT" "$live_base_branch" exit 1 fi + if [ "${TARGET_HEAD_SHA_INPUT,,}" != "${live_head_sha,,}" ]; then + printf '::error::Targeted scheduler dispatch head does not match the live PR. supplied=%s live=%s\n' "$TARGET_HEAD_SHA_INPUT" "$live_head_sha" + exit 1 + fi { printf 'repository=%s\n' "$TARGET_REPOSITORY_INPUT" @@ -499,7 +677,10 @@ jobs: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }} TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }} TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }} - SCHEDULER_ACTIONS_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.target_repository != github.repository && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} + # Same-repository github.token has Actions authority only in the + # central receiver. A consumer event needs an explicit organization + # token; blank means central cancellation fails closed. + SCHEDULER_ACTIONS_TOKEN: ${{ github.repository == 'ContextualWisdomLab/.github' && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || '' }} # Same-repository dispatch credential: when this scheduler runs inside # ContextualWisdomLab/.github (the repository the required workflows are # dispatched on), the runner token can dispatch them without any diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml index 1788cfd40b..2930e69e61 100644 --- a/.github/workflows/python-security.yml +++ b/.github/workflows/python-security.yml @@ -24,7 +24,7 @@ name: Python Security on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main, master, develop] push: branches: [main, master, develop] @@ -45,7 +45,7 @@ permissions: jobs: detect-python: name: Detect Python - if: github.event.action != 'closed' + if: github.event.action != 'closed' && github.event.pull_request.draft != true runs-on: ubuntu-24.04 outputs: has_python: ${{ steps.detect.outputs.has_python }} diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..f82673acfe 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -21,8 +21,11 @@ name: SAST Semgrep on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] - # Scan every PR base ref, including feature branches used by stacked PRs. + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] + # Keep the native trigger base-ref agnostic. This does not widen ruleset + # 18156473, which injects this required workflow only for pull requests + # targeting the repository default branch. A feature-base stacked PR gets + # this gate only where the workflow is run natively, or after retargeting. push: branches: [main, master, develop] schedule: @@ -49,7 +52,7 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). - if: github.event.action != 'closed' + if: github.event.action != 'closed' && github.event.pull_request.draft != true runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index 8810c702fd..5ff7adfa84 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -180,7 +180,20 @@ jobs: GH_TOKEN: ${{ secrets.SBOM_INVENTORY_TOKEN || steps.aggregator_app_token.outputs.token }} run: | set -euo pipefail - if git diff --quiet -- docs/sbom; then + unexpected_workspace_status="$( + git status --porcelain=v1 --untracked-files=all -- \ + . \ + ':(exclude)docs/sbom/inventory.json' \ + ':(exclude)docs/sbom/inventory.md' + )" + if [ -n "$unexpected_workspace_status" ]; then + echo "SBOM generator changed a non-inventory owner path:" >&2 + echo "$unexpected_workspace_status" >&2 + exit 1 + fi + if git diff --quiet -- \ + docs/sbom/inventory.json \ + docs/sbom/inventory.md; then echo "No SBOM inventory changes; nothing to publish." exit 0 fi @@ -188,26 +201,29 @@ jobs: branch="automation/sbom-inventory" git config user.name "cwl-sbom-inventory[bot]" git config user.email "cwl-sbom-inventory@users.noreply.github.com" - git add docs/sbom + git add -- \ + docs/sbom/inventory.json \ + docs/sbom/inventory.md git commit -m "chore: refresh org SBOM inventory" + generated_inventory_head="$(git rev-parse HEAD)" + scripts/ci/reconcile_sbom_publication_lineage.sh \ + "$generated_inventory_head" \ + "$generated_inventory_head" # persist-credentials remains false; configure Git's credential helper # from the already masked GH_TOKEN without putting the token in a URL. gh auth setup-git - # Preserve the existing publication head as ancestry without trusting - # its generated tree. A concurrent writer makes the final normal push - # fail closed instead of rewriting remote history. + # Refuse to overwrite any prior non-inventory change. Only a + # generated-only predecessor may merge, with this run's two + # inventory files authoritative. A concurrent writer still makes the + # final normal push fail instead of rewriting remote history. if git ls-remote --exit-code --heads origin "refs/heads/$branch" >/dev/null 2>&1; then git fetch --no-tags origin "refs/heads/$branch" previous_head="$(git rev-parse FETCH_HEAD)" - if ! git merge-base --is-ancestor "$previous_head" HEAD; then - git merge \ - --strategy=ours \ - --no-edit \ - -m "chore: preserve SBOM inventory publication lineage" \ - "$previous_head" - fi + scripts/ci/reconcile_sbom_publication_lineage.sh \ + "$previous_head" \ + "$generated_inventory_head" fi git push origin "HEAD:refs/heads/$branch" diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..393adc783d 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -41,9 +41,11 @@ name: Security Scan on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] - # Do not restrict the base ref: stacked PRs must receive the same - # diff-scoped OSV/dependency and repo-wide Trivy gate as default-branch PRs. + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] + # Keep the native trigger base-ref agnostic. This does not widen ruleset + # 18156473, which injects this required workflow only for pull requests + # targeting the repository default branch. A feature-base stacked PR gets + # this gate only where the workflow is run natively, or after retargeting. concurrency: group: >- @@ -70,7 +72,7 @@ jobs: # here and consumed through `needs`. See # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. - if: github.event.action != 'closed' + if: github.event.action != 'closed' && github.event.pull_request.draft != true runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: @@ -435,7 +437,7 @@ jobs: # push, schedule, and manual backstops remain in secret-scan.yml. gitleaks: name: gitleaks (secret scan) - if: github.event.action != 'closed' && github.repository == 'ContextualWisdomLab/.github' + if: github.event.action != 'closed' && github.event.pull_request.draft != true && github.repository == 'ContextualWisdomLab/.github' runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index ea78c30cc7..d38dcd855f 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -155,7 +155,7 @@ jobs: python -m coverage report - name: Enforce complete production docstrings - run: python -m interrogate --fail-under 100 scripts/ci/materialize_base_python_requirements.py + run: python -m interrogate --fail-under 100 scripts/ci - name: Compile production and quality contracts run: | diff --git a/AGENTS.md b/AGENTS.md index 0972af51c5..5087855f7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,18 +46,23 @@ The materialization contract is also covered by [`docs/doctoring/exact-artifact- and use `github-robot-review-gate` plus `babysit-pr` when diagnosing or monitoring a protected PR. If a named skill is unavailable, preserve its fail-closed trust boundary and exact-current-head evidence rules manually. -- PR-triggered workflow concurrency must be trigger-aware. Group by workflow, - target repository, and pull request number with `cancel-in-progress: true`; - do not include the head SHA, because that prevents a new head from cancelling - its predecessor. Non-PR triggers need an explicit collision-safe fallback. +- PR-triggered workflow concurrency must be trigger-aware. For replaceable + current-state checks, group by workflow, target repository, and pull request + number with `cancel-in-progress: true`. When every same-head admission carries + distinct work that must survive, include the exact head SHA and use bounded + `queue: max`; retire predecessor heads only through a metadata-only cleanup + that inventories every PR-associated trigger, revalidates live authority, + and proves terminal cancellation. Non-PR triggers need an explicit + collision-safe fallback. - Put concurrency at workflow scope when queued jobs must be coalesced before a runner is admitted. Job-level concurrency cannot relieve a saturated runner queue because it is evaluated only after job admission. - Keep cleanup repository-local and event-driven. Do not restore an - organization-wide queue sweep, polling `sleep`, or another scheduled scan to - compensate for incorrect concurrency. Cancel only runs proven to belong to a - superseded head of the same PR, then verify each accepted cancellation - reaches `completed/cancelled`. + organization-wide queue sweep, long-lived polling wait, or another scheduled + scan to compensate for incorrect concurrency. A metadata-only cleanup may use + a few bounded status reads to prove an accepted cancellation reached + `completed/cancelled`; it must fail closed rather than hold a model or review + runner. Cancel only runs proven to belong to a superseded head of the same PR. - Classify a run's PR head by event-specific evidence before cancellation. `pull_request` may use the run's top-level `head_sha`, but `pull_request_target` records the trusted base there; use its PR association diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e12f33542d..539fc304fe 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -163,6 +163,32 @@ workflow does not claim SLSA Build L3. ## Control-plane data flow +### CodeQL required-workflow admission + +```mermaid +flowchart TD + Required["Required CodeQL coordinator"] + Active{"Exact trusted handler active?"} + Validate["Control pool: validate identity"] + Scan["CodeQL pool: matrix scan"] + Settle["Control pool: settle exact run"] + + Required --> Active + Active -->|"yes: preserve"| Required + Active -->|"no: dispatch"| Validate + Validate --> Scan + Scan --> Settle +``` + +The coordinator binds active-run reuse to the protected handler path and +event, the full repository/PR/head/base/required-run/merge-source title, a +trusted app actor, and an active GitHub state. The check precedes OIDC exchange +and never treats activity as success. The handler deliberately has no +workflow-level cancellation: GitHub evaluates that key before protected actor +and live-PR validation, so arrival order cannot be trusted to retire work. +Event-driven scheduler cleanup separately revalidates both the run and PR at +the destructive boundary and cancels only closed or stale-head work. + ```mermaid sequenceDiagram participant PR as Pull request diff --git a/CHANGELOG.d/20260930-coverage-incomplete-approval.md b/CHANGELOG.d/20260930-coverage-incomplete-approval.md new file mode 100644 index 0000000000..4052abf75f --- /dev/null +++ b/CHANGELOG.d/20260930-coverage-incomplete-approval.md @@ -0,0 +1,9 @@ +### Incomplete Rust coverage cannot supply formal approval evidence + +- A Rust coverage timeout now reports `NOT MEASURED`, never `PASS` or a + claim that the timed-out suite passed. The advisory coverage job may finish + successfully, but formal approval requires one unambiguous `PASS` decision + from that same dispatch. Missing, unknown, malformed, and contradictory + decisions withhold approval without manufacturing a source finding. Existing + exact-head review and required-check gates remain in force; unmeasured + coverage does not satisfy or replace required verification. diff --git a/CHANGELOG.d/20260930-draft-pr-runner-guard.md b/CHANGELOG.d/20260930-draft-pr-runner-guard.md index c2dfeff294..5b7b6461c3 100644 --- a/CHANGELOG.d/20260930-draft-pr-runner-guard.md +++ b/CHANGELOG.d/20260930-draft-pr-runner-guard.md @@ -5,3 +5,8 @@ conclude that a draft needs no verdict; marking the pull request ready runs them again on the same head. Noema is unchanged because it reads the live draft state. +- A `converted_to_draft` event now enters the existing per-PR concurrency + group for CodeQL PR, SAST Semgrep, Security Scan, and Python Security. The + event cancels an older queued same-PR run, while every entry job skips before + runner admission. This closes the queue-retention gap without weakening any + Ready-head security check. diff --git a/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md b/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md new file mode 100644 index 0000000000..6859e10f2e --- /dev/null +++ b/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md @@ -0,0 +1,7 @@ +# Security + +- Pin the shared Strix CI runtime to PyJWT 2.15.1. This retains the 2.15.0 + security correction that converts malicious deeply nested JWT payload + recursion into a bounded `DecodeError`, while also carrying the signed + 2.15.1 Base64URL-padding correction. The source input and generated hash + lock now carry the same exact release. diff --git a/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md b/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md new file mode 100644 index 0000000000..3d68a778dc --- /dev/null +++ b/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md @@ -0,0 +1,6 @@ +### Repository metadata validation receives published evidence ancestry + +- The Repository Metadata Reconcile validation job now fetches complete Git + history before the repository-wide contract suite checks whether documented + evidence commits belong to the current exact-head ancestry. Exact revision + verification and credential isolation remain unchanged. diff --git a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md index 9a950691b9..5b64363842 100644 --- a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md +++ b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md @@ -2,8 +2,6 @@ - `scripts/ci/verify_release_maturin_tool_assets.py` fetches from a fixed `https://github.com/PyO3/maturin/releases/download/v1.15.0/` origin, and `verify_assets` admits only - five literal asset names. The downloader now uses a standard-library opener - that admits one credential-free HTTPS redirect only from the exact GitHub - release path to `release-assets.githubusercontent.com`, bounds the response, - and closes it on every path. It uses neither `urlopen` nor - `HTTPSConnection`, and carries no `nosemgrep` or `nosec` suppression. + five literal asset names, but `p/default`'s `dynamic-urllib-use-detected` flagged the call on + main and failed Semgrep on every PR. The call now carries the repository's standard reasoned + `nosemgrep`/`nosec B310` suppression. diff --git a/CHANGELOG.d/20260930-urllib3-security-lock.md b/CHANGELOG.d/20260930-urllib3-security-lock.md new file mode 100644 index 0000000000..4e22487120 --- /dev/null +++ b/CHANGELOG.d/20260930-urllib3-security-lock.md @@ -0,0 +1,6 @@ +# Security + +- Pin the shared pip-audit and Strix CI runtimes to urllib3 2.8.0, which fixes + the HTTPS-proxy TLS-policy crossover and two streaming denial-of-service + vulnerabilities present in 2.7.0. Both source inputs and generated hash locks + now carry the same exact version. diff --git a/CHANGELOG.d/20261001-codeql-draft-materialization.md b/CHANGELOG.d/20261001-codeql-draft-materialization.md new file mode 100644 index 0000000000..551de8a6c5 --- /dev/null +++ b/CHANGELOG.d/20261001-codeql-draft-materialization.md @@ -0,0 +1,3 @@ +- Materialize central CodeQL evidence for Draft consumer heads while keeping native-owner Drafts runner-free and using `converted_to_draft` only to retire stale same-PR work. +- Retire stale or closed-PR central CodeQL, OpenCode, and Strix dispatches from the canonical scheduler after fresh run/PR revalidation, including before Draft skip and outside the open-PR queue. +- Correct required-workflow scope guidance: unfiltered workflow triggers do not widen the organization ruleset beyond default-base pull requests, so stacked feature-base pull requests wait for prerequisite merge and retargeting before central Security, SAST, and CodeQL evidence materializes. diff --git a/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md b/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md new file mode 100644 index 0000000000..d874f043af --- /dev/null +++ b/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md @@ -0,0 +1,24 @@ +### Approval reuse requires a unique passing coverage decision + +- OpenCode's existing exact-head approval path, merge-scheduler reuse gate, and + repository-dispatch status publisher now inspect the coverage evidence + summary as well as the coverage job conclusion. They accept exactly one + `- Result: PASS` line and fail closed for missing, `NOT MEASURED`, non-passing, + or duplicate decisions. A successful advisory coverage job therefore cannot + reuse a predecessor approval when the current dispatch did not measure and + pass coverage. +- The existing-approval CLI regression executes the non-passing decision branch + directly, preserving the repository's 100% statement/branch coverage gate + instead of excluding or suppressing the new fail-closed path. +- Existing approval reuse now evaluates the latest exact-head OpenCode + publication decision instead of skipping a newer `CHANGES_REQUESTED` or + otherwise invalid decision and resurrecting an older `APPROVED` review. +- The canonical Noema document-reader lock now selects `fast-uri` 3.1.8 and + `ip-address` 10.7.2, removing CVE-2026-86472, CVE-2026-101911, and + CVE-2026-101912 from the exact runtime installed by the hosted review lane. + The bundle contract pins both transitive security versions so a later lock + regeneration cannot silently restore the vulnerable releases. +- The central Strix input and generated hash lock now select LiteLLM 1.94.3, + the patched 1.94 release for CVE-2026-84377. This closes the authenticated + provider-credential forwarding and SSRF boundary exposed by 1.94.1 while + preserving the existing Strix package and override set. diff --git a/CHANGELOG.d/20261001-noema-document-reader-dependencies.md b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md index 486643700c..4841cfb3ff 100644 --- a/CHANGELOG.d/20261001-noema-document-reader-dependencies.md +++ b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md @@ -1,5 +1,5 @@ ### Noema document reader closes new transitive advisories -- The generated npm lock now selects `fast-uri` 3.1.8 and `ip-address` 10.7.1, +- The generated npm lock now selects `fast-uri` 3.1.8 and `ip-address` 10.7.2, removing the three medium-severity findings published against the previous transitive versions while leaving the direct runtime manifest unchanged. diff --git a/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md deleted file mode 100644 index 4a19c4bb5e..0000000000 --- a/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md +++ /dev/null @@ -1,8 +0,0 @@ -### Noema document reader transitive security updates - -- Updated the locked `fast-uri` dependency from 3.1.7 to 3.1.8 and - `ip-address` from 10.7.0 to 10.7.1. These are the first releases outside - the affected ranges for GHSA-hrr3-gc8f-f4qj, GHSA-j6r3-76f7-8jcv, and - GHSA-h3mg-xc3c-68pw. The direct dependency ranges are unchanged. -- Added a deterministic regression contract that rejects reintroduction of - vulnerable hoisted or nested copies of either transitive package. diff --git a/CHANGELOG.d/20261001-opencode-same-head-dispatch-idempotency.md b/CHANGELOG.d/20261001-opencode-same-head-dispatch-idempotency.md new file mode 100644 index 0000000000..8609c73085 --- /dev/null +++ b/CHANGELOG.d/20261001-opencode-same-head-dispatch-idempotency.md @@ -0,0 +1,16 @@ +### Fixed + +- Preserve any active exact-head OpenCode dispatch across all GitHub admission + states when the required-check wake path is retried, fail closed on ambiguous + inventory, avoid GitHub's lossy pending concurrency replacement, retire only + live-head-verified older central runs with completion evidence, and revalidate + pull-request authority immediately before a new dispatch. After a receiver + acquires the exact-PR lease, revalidate live authority and the formal + exact-head review receipt again so a completed prior receiver cannot trigger + duplicate coverage or model execution. + Preserve exact-head merge-scheduler admissions within GitHub's documented + `queue: max` bound; a dedicated metadata-only cleanup inventories every + active state twice across all PR-associated scheduler triggers, + rejects incomplete GitHub search results, retires only + live-head-revalidated predecessor runs, and proves each accepted + cancellation reaches `completed/cancelled`. diff --git a/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md b/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md deleted file mode 100644 index e4d748764c..0000000000 --- a/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md +++ /dev/null @@ -1,6 +0,0 @@ -### Shared Strix LiteLLM security floor - -- Pinned LiteLLM 1.94.3 in the Strix source input and regenerated the complete - hash lock after exact-head `pip-audit` found CVE-2026-84377 in 1.94.1. -- Added a source/lock parity regression contract and preserved the stacked Noema - document-reader transitive security repair without copying its implementation. diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e8dfc577..29dc58ca01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,119 @@ +### SBOM inventory publication preserves owner repairs + +- Replaced the inventory publisher's ancestry-only `ours` merge with an + executable lineage reconciler. A refresh now admits a prior publication + branch only when its branch-owned delta is limited to the generated JSON and + Markdown inventories; every non-inventory delta or conflict stops before + publication. Real Git fixture contracts prove repair-loss refusal and the + safe generated-only two-parent path. This repairs the control-plane cause of + `.github#1678@7598436e`, where the scheduled publisher silently restored + vulnerable lock pins despite keeping their repaired commits as ancestors. +- Restrict the generator's own staging and commit lineage to those same two + inventory paths, and reject any other tracked or untracked worktree side + effect before publication. Real Git regressions prove that committed and + uncommitted owner-path changes fail before ancestry reconciliation, + including the first publication path where no remote automation branch + exists yet. +- Inspect every commit reachable only from the prior publication head against + its first parent. A change-then-revert pair can no longer hide an owner-file + mutation from the final-tree allowlist and smuggle both commits into the next + publication's ancestry. + +### Central coverage owner preserves concurrent repairs and restores the 100% gate + +- The `.github#2521` coverage owner now carries the complete valid deltas from + `.github#2530` (hash-pinned full-suite parser dependencies) and `.github#2532` + (explicit GitHub `HTTPError` response closure) through ordinary two-parent + merges. Behavior-level contracts replace dummy/live-CLI coverage for the + OpenCode queue and Strix manifest scanners, close release dependency trust + boundaries, and prove the runtime-archive prescreener's reachable state space; + one redundant unreachable postcondition was removed without weakening any + earlier fail-closed validation. Python 3.10 TOML fallback selection is now a + directly testable compatibility boundary. On the integrated tree, + warnings-as-errors produced 5,228 passed, 5 skipped, and 40 subtests passed; + all 18,232 owned production statements and 7,488 branches are covered. + Hosted exact-head Checks and qualifying independent review remain required. + +### Agent runtime quality compares the live base graph + +- `Agent Review Runtime Quality CI` now derives changed paths and whitespace + checks from the merge-base of the fetched live base ref and the exact PR + head. Long-lived PR events can no longer make already-protected-main files + look like new PR whitespace, while exact-head checkout and fail-closed diff + checks remain unchanged. Exact failure evidence is `.github#1678` run + `36804488453`, job `110185716853`. +- Refetch the base ref immediately before both merge-base decisions. A base + advance during the quality job can no longer revive the same stale-diff + failure at the terminal whitespace gate. +- Preserve canonical parser/security/coverage owner `.github#2530@dc54310c` as + an ordinary second parent. The integrated PR exposed that this quality + workflow still admitted only pull requests targeting `main`, so its own + stacked exact head produced no quality run. A RED contract now requires + stacked-base admission; the minimal repair removes only that base filter. + CodeQL and review evidence remain mandatory; release admission stays HOLD. +### Authorized Draft reviews reach the exact-head receiver + +- Carry an explicit `draft_review_only` boolean from the merge scheduler to the + central OpenCode receiver. The receiver accepts a live Draft only while an + exact repository/PR/head durable request artifact is present, non-expired, + structurally complete, and independently re-fetched with `actions: read`; + malformed payloads, missing markers, and every base/head/state mismatch fail + before the Contents lease can be mutated. Ready pull requests reject Draft + authority. A clean Draft publishes an exact-head formal comment with + `DRAFT_REVIEW_COMPLETE`, never an approval or a merge-authorizing receipt; + the scheduler recognizes only that source-backed comment as completion. + The Ready-only Required producer now sends an explicit boolean `false`, and + a Draft completion lease is retired when the same head becomes Ready so the + mode-independent admission identity cannot suppress the required approval + review. + Review-only runs never publish Ready status, dispatch Noema, invoke the merge + scheduler, or wake merge-required OpenCode jobs. This reconnects the + already-authorized agent-mention/scheduler path without weakening ordinary + ready-for-review admission. + +### OpenCode preserves exact-head queue position + +- Remove the central receiver's lossy native concurrency group after live + evidence showed a later same-head wake cancelling the queued authoritative + run. The required wake now inventories all five active states twice, retires + only identity-validated older-head runs after live authority checks, and + fails closed until every accepted cancellation is proven + `completed/cancelled`. The merge scheduler now preserves exact-head + admissions within GitHub's documented `queue: max` bound and retires + predecessor heads from every PR-associated scheduler trigger only through a + metadata-only, + live-head-revalidated cleanup job. The receiver atomically compare-and-swaps one + repository/PR lease file on a dedicated central branch before source + materialization, coverage, or model execution, closing the cross-producer + check-then-POST race without lossy native concurrency. A dedicated minimal + admission job owns the central `contents: write` grant, rejects unauthorized + or malformed envelopes before OIDC exchange, and revalidates the complete + live state/draft/base/head identity immediately before each compare-and-swap; + metadata and source jobs remain read-only. Self-reruns retain their lease and + different-head takeovers validate the recorded owner. After formal receipt, + the publisher inventories repository-wide runs, binds the intended PR head + through `pull_requests[]` rather than the trusted-base run-level SHA, + recursively partitions the PR-lifetime `created` range below GitHub's + 1,000-result filtered-search ceiling, rejects `total_count`/collection + mismatches, revalidates live authority before each POST, and reruns every + matching failed Required OpenCode job. A losing duplicate therefore needs + neither a callback payload nor a polling runner. ### Maturin download failures close every transport response - Refactor the bounded Maturin asset downloader so successful and rejected responses share one unconditional close path while `HTTPError` keeps its own explicit close path. A new regression exercises a non-200 response and an - opener-raised HTTP error. This removes an impossible optional-response branch + opener-raised HTTP error through a close-observing body without replacing the + error's real `close()` method. This removes an impossible optional-response branch without changing hosts, redirects, byte limits, hashes, or fail-closed error mapping; the focused suite is 17 passed with 100% statement and branch coverage. The trusted full-suite workflow now also tracks the verifier source and its focused test, so a future lifecycle change cannot omit the repository coverage gate that detected this regression. The pull-request trigger admits stacked canonical-owner bases as well as `main`; the protected-branch push - trigger remains restricted to `main`. + trigger remains restricted to `main`. Document the archive extractor and CLI + entry point, and expand the trusted docstring gate from one materializer file + to all `scripts/ci` production modules. ### Shared Strix lock advances beyond the PyJWT recursion DoS @@ -75,7 +178,6 @@ from silently returning to the vulnerable versions. Protected integration, immutable consumer-pin advancement, and fresh exact-head hosted security Checks remain required before release admission. - ### Intel macOS native archives are bound to x86_64 bytes - The release prescreener now requires every native member in an Intel macOS @@ -337,6 +439,24 @@ # Changelog +### CodeQL preserves exact active dispatches and isolates scarce scan capacity + +- The required-workflow coordinator now recognizes a trusted, exact active + central handler run before OIDC exchange and does not enqueue an identical + replacement on later attempts. The match binds protected path/event, + repo/PR/head/base/required-run/source title, trusted actor, and every active + GitHub run state; inexact, untrusted, and terminal runs cannot suppress + recovery. The protected handler no longer uses repository/PR concurrency + before validating payload authority. Its validation and settlement jobs use + the central control pool, while only the CodeQL matrix scan consumes the + scarce CodeQL pool. This fixes the reproduced `.github#2531` chain in which + run `36815888197` replaced queued run `36804251663` for identical work. + The owner-union repair tree passes 249 focused contracts and the complete + warnings-fatal suite: 5,437 passed, 5 optional skips, and 40 subtests, with + all 18,882 production statements and 7,712 branches covered and production + Docstring coverage at 100%. Fresh hosted exact-head evidence and independent + approval remain required. + - **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 diff --git a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md index b9a156417b..1176145c84 100644 --- a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md +++ b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md @@ -330,13 +330,22 @@ The protected bootstrap accepts exactly two event types: base/head parent binding, base-bound status context, and exact handler gate/SARIF/artifact evidence. -Both modes share one repository-and-PR concurrency group and one post-matrix -`settle-required-run` job. The matrix scan has `actions:read`; only settlement -has `actions:write`. Settlement revalidates the open PR, repository, base ref +Both modes share one post-matrix `settle-required-run` job. The handler does +not use workflow-level payload concurrency: GitHub evaluates it before actor +and live-PR validation, so a delayed stale payload could evict valid current +work and then reject itself. Instead, the required-workflow coordinator +preserves a trusted exact active handler identified by protected path/event, +full repo/PR/head/base/required-run/source title, trusted app actor, and active +run state. Stale Draft/closed work is retired by the scheduler only after a +fresh run-and-PR revalidation. The matrix scan has `actions:read`; only +settlement has `actions:write`. Validation and settlement use the central +control pool; only the matrix scan uses the dedicated CodeQL pool. Settlement +revalidates the open PR, repository, base ref and SHA, head ref and SHA, required run, complete required-job map, terminal handler jobs, gate steps, and non-expired SARIF artifacts before issuing one -run-wide rerun request. The common concurrency identity prevents v1 and v2 -from becoming simultaneous writers during cutover. +run-wide rerun request. Exact active-run identity prevents v1 and v2 from +manufacturing duplicate writers during cutover without trusting unvalidated +arrival order. Live evidence for the amendment is recorded in `docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md`. In short, @@ -346,6 +355,15 @@ legacy wakes raced: Actions started the required run and Python received HTTP including `34684575249`, leaving a clean scan without a converged terminal receipt. This is a settlement-timing defect, not a CodeQL finding. +Live evidence on 2026-10-01 added a second amendment. `.github#2531` required +run `36804208074` created handler run `36804251663`; a later attempt with no +terminal receipt posted the identical title as `36815888197`, and the shared +repository/PR concurrency cancelled the durable queued run two seconds later. +The coordinator's exact-active lookup and removal of pre-validation handler +cancellation repair that churn while retaining the existing terminal-proof, +GHAS, SARIF, and bounded settlement requirements. This amendment remains +Proposed until protected rollout and an unchanged-head consumer canary pass. + Landing sequence is normative: 1. Land this dual-event, legacy-compatible handler from fresh protected main. diff --git a/docs/adr/0027-code-scanning-required-workflow-audit.md b/docs/adr/0027-code-scanning-required-workflow-audit.md index a26266b9bc..184e7d05c5 100644 --- a/docs/adr/0027-code-scanning-required-workflow-audit.md +++ b/docs/adr/0027-code-scanning-required-workflow-audit.md @@ -75,6 +75,21 @@ fixed. `tests/test_code_scanning_required_workflow_contract.py::test_ruleset_aud is the permanent regression guard for this. Left as an "Update" rather than rewriting the sections above, so the historical record of what this PR's own RED/GREEN commits contained at each point stays intact. +## Update — 2026-09-05: lightweight dispatch producer re-admitted + +The 2026-09-03 exclusion applied to the former workflow that embedded +`github/codeql-action`. ADR-0025 subsequently split that incompatible design: +the ruleset-facing `codeql-pr.yml` became a lightweight producer/verdict +consumer, while `codeql-scan-dispatch.yml` became the protected native handler +that alone runs `github/codeql-action`. The canonical audit tuple and protected +branch required contexts therefore include `codeql-pr.yml` again without +reintroducing the platform restriction. The native handler remains excluded +from the required-workflow ruleset. This amendment supersedes only the earlier +"must stay excluded" sentence; the historical incident and prohibition on +embedding CodeQL actions in a required workflow remain valid. Live organization +ruleset authority still must be re-read before acceptance; open PR source is +not production authority. + ## References GitHub. (n.d.). *REST API endpoints for rules*. GitHub Docs. https://docs.github.com/rest/repos/rules diff --git a/docs/doctoring/agent-review-live-base-diff-20261001.md b/docs/doctoring/agent-review-live-base-diff-20261001.md new file mode 100644 index 0000000000..fac96b5f72 --- /dev/null +++ b/docs/doctoring/agent-review-live-base-diff-20261001.md @@ -0,0 +1,64 @@ +# Agent review live-base diff RCA + +Status: Proposed shared-workflow repair; hosted exact-head evidence and +independent review remain mandatory. + +## Failure evidence + +Draft `.github#1678` exact head +`b9651115be28f9dd46f8b93a2a753b2652c57970` failed Agent Review Runtime +Quality CI run `36804488453`, job `110185716853`, step `Verify consolidated +workflow contract`. All preceding contract suites succeeded. The terminal +`git diff --check` used event base `f250638827f8252b0d9e5cb2601f4d333f96162f` +and reported 407 whitespace violations across four files. + +## Root cause and boundary + +The PR head already contains protected `main@37b10243cec3d160ecc9c1be75c71428b160a703` +as an ancestor through ordinary owner integration. The pull-request event still +carried its older base SHA, so the workflow treated intervening protected-main +history as the PR delta. The four files are clean relative to the live base; +rewriting their historical contents in the consumer would hide the workflow +identity defect and duplicate the `.github` control-plane responsibility. + +## Repair and verification + +The changed-path selector and terminal whitespace gate refetch the exact base +ref immediately before computing its merge-base with the exact head. This +closes the second stale window where the base could advance during a long +quality job after checkout but before the terminal whitespace gate. The head +checkout assertion remains exact, and a failed fetch or missing merge-base +fails closed. + +The original regression contract was changed first and failed in two cases +against the event-SHA implementation. A follow-up RED contract then failed +because neither merge-base decision refetched the base; the minimal repair adds +one fail-closed fetch at each decision. After the workflow repair, 36 focused +consolidation, single-runner, autofix-context, and runtime-budget tests pass. +Applying the same command to the real #1678 graph resolves the live change base to +`37b10243cec3d160ecc9c1be75c71428b160a703`; `git diff --check` succeeds. + +Completion requires a dedicated owner PR, exact-current-head hosted Checks, +qualifying independent review, ordinary protected-main integration, ordinary +merge of the owner into #1678, and a fresh successful #1678 run. Pending, +queued, skipped, or predecessor results are not passing evidence. + +## Canonical owner integration and stacked admission + +Before dependent exact-head revalidation, the repair ordinary-merges canonical +parser/security/coverage owner +`.github#2530@dc54310c8c5ee6637274e7e82f5ea53d64ad91e6`. The resulting owner PR head +`b66036e3702b95d47fc7eac5eb6097e198d49997` is an ordinary two-parent merge, +but it produced no Agent Review Runtime Quality run: the workflow's +`pull_request` trigger still admitted only base `main`, while this PR now +targets the canonical owner branch. + +The regression contract failed against that filter. The minimal repair removes +only the pull-request base restriction and retains path selection, read-only +permissions, exact-head checkout, and PR-stable concurrency. This permits the +owner workflow to produce exact-head evidence on canonical stacked bases; it +does not make a pending or skipped result passing evidence. + +The merge preserves both lineages without force or rebase and keeps dependency +repair in the canonical owner. The resulting head must rerun all Checks; +predecessor success is causal evidence only, not admission evidence. diff --git a/docs/doctoring/codeql-draft-ready-materialization.md b/docs/doctoring/codeql-draft-ready-materialization.md new file mode 100644 index 0000000000..e1eeecc3ba --- /dev/null +++ b/docs/doctoring/codeql-draft-ready-materialization.md @@ -0,0 +1,66 @@ +# CodeQL Draft event materialization + +Date: 2026-10-01 + +## Failure scene + +Organization ruleset consumers launch the central required workflows for +`opened`, `synchronize`, and `reopened`, but do not launch another run when an +unchanged pull-request head moves from Draft to Ready. The central CodeQL +entry job used the event's Draft snapshot as a blanket job-level runner guard +and assumed `ready_for_review` would re-run the same head. Live unchanged-head +canaries in `ContextualWisdomLab/Orgmetra` disproved that assumption: CodeQL +remained skipped after Ready. + +A second live failure mode existed at the owner: a `converted_to_draft` event +must enter the same per-PR concurrency group to retire stale Ready work, but it +must not start a replacement scan. Treating every Draft event identically +cannot satisfy both obligations. + +## Decision + +CodeQL uses an explicit event and repository matrix: + +- consumer `opened`, `synchronize`, and `reopened` Draft events materialize + exact-head security evidence; +- the native `ContextualWisdomLab/.github` owner skips Draft entry jobs to + preserve runner capacity; +- `converted_to_draft` and `closed` enter workflow concurrency but skip the + entry job, retiring stale same-PR work without a new scan; +- Ready heads continue through the existing scan path. + +This does not admit a pull request for review, publish a review verdict, +weaken a CodeQL finding, or promote predecessor evidence. The existing live +repository, pull-request number, head, base, merge source, required run, +authenticated status, GHAS identity, and SARIF checks remain unchanged. + +Model-backed review workflows keep their Draft exclusion because Ready is the +review-admission boundary. Their missing Ready materialization remains tracked +separately; running a model review while a pull request is Draft would hide the +actual event-delivery defect rather than repair it. + +## Alternatives rejected + +- Rely on `ready_for_review`: ruleset consumers do not receive that event. +- Scan every Draft event: native-owner Drafts consume scarce control runners, + and `converted_to_draft` would replace rather than merely retire work. +- Skip every Draft event: consumer heads can remain permanently without + CodeQL evidence. +- Re-run a skipped Draft job manually: the event snapshot remains Draft and + the policy defect survives. +- Manufacture a success status: discards authenticated CodeQL/SARIF proof. + +## Evidence and follow-up + +The RED contract produced two failures against the blanket Draft guard: it +rejected missing consumer materialization and missing conversion-event +retirement. GREEN requires the complete repository/event matrix and keeps the +per-PR concurrency key unchanged. The focused Draft-control and queue suite +passes 86 tests; the warnings-fatal repository suite passes 5,261 tests, five +optional-platform skips, and 40 subtests. Hosted exact-head Checks remain +mandatory before protected integration. + +Post-merge evidence is a new or synchronized Draft consumer head that reaches +terminal CodeQL dispatch evidence without a Ready transition, plus a native +owner Draft conversion that retires the prior run without assigning a runner. +Review materialization remains an open central-owner Gap. diff --git a/docs/doctoring/draft-transition-queue-retirement.md b/docs/doctoring/draft-transition-queue-retirement.md new file mode 100644 index 0000000000..43d10d48b3 --- /dev/null +++ b/docs/doctoring/draft-transition-queue-retirement.md @@ -0,0 +1,100 @@ +# Draft-transition queue retirement + +## Incident + +A live organization sample on 2026-09-30 found 100 of the 100 most recently +updated Open Ready pull requests with required workflows still queued. Twenty-five +of those pull requests had a terminal workflow failure, a current +`CHANGES_REQUESTED` review, an unresolved review thread, or an explicit +predecessor/partial-implementation boundary and were moved back to Draft at +their unchanged exact heads. + +The central CodeQL PR, SAST Semgrep, Security Scan, and Python Security +workflows already used per-PR `cancel-in-progress: true` concurrency. They +subscribed to `ready_for_review` but not `converted_to_draft`. Consequently, +a Draft transition could stop future product admission but could not create the +same-concurrency replacement run that retires the already queued Ready event. + +A second live sample on 2026-10-01 exposed a distinct central-dispatch gap. +The `.github` receiver held 457 queued and two in-progress +`repository_dispatch` runs. Of the newest 100 queued runs, 38 targeted a +superseded PR head and five targeted an already closed PR; 20 of those stale +runs were CodeQL and 18 were OpenCode. Workflow concurrency only coalesces a +new run in the same group. A Draft transition or PR closure that creates no new +central dispatch therefore leaves the old run queued. The scheduler made this +worse by returning `draft PR` before stale-run cleanup, and closed PRs never +entered its open-PR loop at all. + +## Decision + +Each affected workflow subscribes to `converted_to_draft`. Ordinary entry jobs +require a non-Draft pull request, while CodeQL uses the narrower explicit +repository/event matrix in +[`codeql-draft-ready-materialization.md`](codeql-draft-ready-materialization.md): +consumer Draft heads scan, native-owner Draft heads do not, and +`converted_to_draft` never starts a replacement scan. GitHub therefore applies +workflow-level concurrency and cancels the older same-PR run, then skips the +replacement before assigning a runner. + +The concurrency key, permissions, checkout identity, scanner configuration, +failure threshold, and Ready-head behavior remain unchanged. No workflow run is +rerun manually, no required result is synthesized, and no failed result is +converted to success. + +The scheduler now performs a bounded central-dispatch retirement sweep for the +known protected CodeQL, OpenCode, and Strix workflow paths. It accepts only the +exact repository/PR/head identity encoded in those workflows' `run-name`, then +re-fetches the active run and target PR immediately before cancellation. It +covers all five GitHub active states (`queued`, `in_progress`, `waiting`, +`pending`, and `requested`) so a state transition cannot escape retirement. It +cancels only a run whose target PR is closed or whose encoded head differs from +the freshly fetched live head. Current-head and malformed runs are preserved; +authority-read failures fail closed. A consumer event performs central +retirement only when an explicit organization Actions token is configured; +its repository-scoped token is never treated as central authority. Draft PRs +run this cleanup before the +ordinary Draft skip. The scheduler's existing `pull_request_target` receiver +admits only `converted_to_draft` and `closed` transition events to its bounded +control job; a closed PR is cleaned and returned before any review, branch, +auto-merge, or merge path can run. Ordinary Draft events still assign no +runner. + +## Alternatives + +- Leaving the queue intact was rejected because Draft is an explicit admission + withdrawal and stale queued work consumes the organization job ceiling. +- An unbounded external cancellation client was rejected. The selected repair + remains in the canonical `.github` scheduler owner, recognizes only protected + central workflow identities, and revalidates exact run and PR authority at + the destructive boundary. +- Adding a new cancellation job was rejected because workflow-level concurrency + already performs the exact same-head retirement before runner admission. + +## Verification + +`test_converted_to_draft_retires_queued_run_without_runner` first failed for +all affected workflows because the event was absent. The later single-writer +reconciliation first produced two focused failures against CodeQL's blanket +Draft guard, then passed the combined Draft materialization and retirement +matrix. An affected-workflow audit also found and corrected one stale SAST +test oracle that still required the old closed-only job guard. The resulting +workflow-consumer suite reports 672 passes, and the warnings-fatal repository +suite reports 5,259 passes, five optional-platform skips, and 40 subtests. +Hosted exact-head checks remain required before protected merge. + +The later central-dispatch RED suite reproduced three failures: Draft returned +before cleanup, no CodeQL central cleanup API existed, and closed-PR runs had no +fresh-authority path. The implementation passes all 458 focused scheduler and +admission tests with warnings treated as errors, including stale/current/closed, +malformed identity, authority outage, five-state transitions, Draft ordering, and transition-event +cleanup-only coverage. This is local evidence only; hosted exact-head checks and +independent review remain required. + +## Follow-up + +After ordinary protected merge, observe the next Draft transition and PR +closure. Confirm that older direct runs retire through workflow concurrency and +that stale/closed CodeQL, OpenCode, and Strix central dispatches are cancelled +without touching current-head runs or assigning a CodeQL, OpenCode, or Strix +worker merely to reject stale identity. The bounded scheduler control job is +expected only for the Draft/close transition that performs the retirement. diff --git a/docs/doctoring/hourly-commercial-license-sbom-remediation.md b/docs/doctoring/hourly-commercial-license-sbom-remediation.md index d4ed9b0128..850bf1e825 100644 --- a/docs/doctoring/hourly-commercial-license-sbom-remediation.md +++ b/docs/doctoring/hourly-commercial-license-sbom-remediation.md @@ -20,6 +20,13 @@ The existing license classifier is intentionally high-recall but is not a legal 7. A replacement implementation must not copy protected source, tests, comments, data, expressive structure, or other copyrightable material from the incompatible implementation. Product contracts, published standards, independent interoperability documentation, and lawful black-box behavior are the acceptable specification sources. 8. Update manifests and lockfiles, SBOMs, NOTICE/THIRD_PARTY_NOTICES, tests, architecture/ADR evidence, CHANGELOG when release-relevant, and `docs/product-technical-gap-baseline.md`; then rerun exact-head Checks/reviews and merge only through ordinary branch protection. 9. Preserve concurrent writers. The recurring inventory publication branch must advance without history rewriting; a race fails closed and is retried on a later run. Because checkout deliberately keeps `persist-credentials: false`, publication establishes Git authentication through the masked organization-wide `GH_TOKEN` with `gh auth setup-git` before the first remote Git operation. +10. Preserve reviewed owner work by refusing to publish over it. The publisher may reconcile a prior head only when that branch's merge-base-relative delta is limited to `docs/sbom/inventory.json` and `docs/sbom/inventory.md`; any non-inventory delta or conflict fails closed for explicit owner reconciliation. + +## 2026-10-01 lineage-preservation incident + +Scheduled run `36836856075`, job `110286232014`, created `.github#1678@7598436eb9126db7bdff00fcf31f6fda15d0f58f` by merging prior publication head `44d6ee04f85028c5d723db0b66a70adbf5edc391` with `--strategy=ours`. Git ancestry therefore retained earlier security-owner commits while the resulting tree discarded 23 non-inventory paths. Exact-head OSV, dependency-review, Trivy, and pip-audit jobs then exposed restored vulnerable lock pins. Ancestry was true, but delta succession was false. + +`scripts/ci/reconcile_sbom_publication_lineage.sh` now compares the prior publication branch to its merge base before mutation. Any branch-owned path outside the two generated inventory files stops the run before merge or push so reviewed owner repairs cannot be silently reclassified as generated data. Independent review found that final-tree comparison alone still admitted a history-smuggling sequence: change an owner file, revert it, then leave only an inventory delta at the branch head. The reconciler therefore also inspects every commit reachable only from the prior publication head against its first parent. That catches hidden change/revert pairs and merge-resolution deltas before their commits become ancestors of the next publication. A generated-only predecessor is reconciled by an ordinary no-force two-parent merge, the current run's two inventory files are restored as authoritative, every other conflict fails closed, and commit occurs only after the index is conflict-free. Real temporary-repository tests prove repair-loss refusal, generated-only reconciliation, non-inventory conflict rejection, and reverted-owner-history rejection. ## Standards and interpretation baseline @@ -32,7 +39,7 @@ This is an engineering governance policy and evidence record, not legal advice. ## Verification contract -The scheduler contract is executable in `tests/test_sbom_inventory_scheduler_contract.py`: it binds assertions to the named executable discovery, aggregation, credential, and publication steps; requires an hourly cron; requires live `isFork == false` filtering; passes only the verified repositories explicitly to the aggregator; rejects `github.token` fallback; configures authenticated Git before remote publication; and prohibits force-push behavior. The first inventory run after merge is not considered complete merely because it reports zero findings; unavailable SBOMs and incomplete dependency materialization remain explicit defects to repair. +The scheduler contract is executable in `tests/test_sbom_inventory_scheduler_contract.py`: it binds assertions to the named executable discovery, aggregation, credential, and publication steps; requires an hourly cron; requires live `isFork == false` filtering; passes only the verified repositories explicitly to the aggregator; rejects `github.token` fallback; configures authenticated Git before remote publication; prohibits force-push and ancestry-only `ours` merges; and executes real Git fixtures for tree preservation and conflict rejection. The first inventory run after merge is not considered complete merely because it reports zero findings; unavailable SBOMs and incomplete dependency materialization remain explicit defects to repair. ## References diff --git a/docs/doctoring/maturin-download-response-lifecycle-20261001.md b/docs/doctoring/maturin-download-response-lifecycle-20261001.md index b6765daae8..ec73c8b89a 100644 --- a/docs/doctoring/maturin-download-response-lifecycle-20261001.md +++ b/docs/doctoring/maturin-download-response-lifecycle-20261001.md @@ -27,18 +27,30 @@ still produced no gate because the workflow admitted only PRs whose base was The downloader now closes every returned response in one unconditional nested `finally` block. An opener-raised `HTTPError` remains independently closed by its handler. Tests assert closure for both an HTTP 503 response and an HTTP 502 -exception. A workflow contract now requires both verifier paths in pull-request +exception. The error fixture preserves the real `HTTPError.close()` and observes +its body closing; replacing the method with a spy had suppressed the actual +close and leaked pytest's temporary capture object until a later test, where +warnings-fatal execution correctly rejected the `ResourceWarning`. A workflow +contract now requires both verifier paths in pull-request and protected-branch triggers, and a separate contract admits stacked PR bases while the push trigger remains restricted to protected `main`. The repair does not change admitted hosts, the one-hop redirect contract, credentials, request timeout, byte bounds, digests, or error mapping. +Exact-tree verification then found that the verifier's archive extractor and +CLI entry point lacked docstrings even though the PR claimed complete +production docstring coverage. A RED regression now binds both symbols, the +two trust-boundary docstrings close the omission, and Trusted uv measures all +of `scripts/ci` instead of only the materializer module. + ## Evidence and remaining gates - Hosted RED: 5,314 passed, 5 skipped, 40 subtests; 18,775 statements with 5 missing, 7,652 branches with 2 partial; total 99%. -- Local owner GREEN: 17 focused tests; 107/107 statements and 34/34 branches; - `git diff --check` clean. +- Local integrated GREEN: 18 lifecycle/prescreen tests; complete warnings-fatal + suite 5,364 passed, 5 skipped, 40 subtests; 18,767/18,767 statements and + 7,648/7,648 branches; verifier 107/107 statements and 34/34 branches; + production docstrings 1,456/1,456; `git diff --check` clean. - Required before acceptance: complete exact-head hosted suite, security and CodeQL verdicts, qualifying independent approval, ordinary owner integration, then ordinary merge-forward into #1653 and fresh consumer Checks. diff --git a/docs/doctoring/opencode-authorized-draft-review-admission.md b/docs/doctoring/opencode-authorized-draft-review-admission.md new file mode 100644 index 0000000000..247cf8eb79 --- /dev/null +++ b/docs/doctoring/opencode-authorized-draft-review-admission.md @@ -0,0 +1,57 @@ +# Authorized Draft review admission + +## Incident + +At `.github#2546@db81de7a09268eb0abfc5b5c675ddd9a0a38a3e2`, the explicit Draft-review +path had two individually valid halves that did not compose. The +agent-mention workflow wrote a one-day artifact named +`cwl-draft-review-request---`, and the merge scheduler +used that exact marker to authorize review-only dispatch. The central receiver +then required `live_draft=false` unconditionally before acquiring its lease. +The authorized request therefore could not reach semantic review. + +This was a control-plane contract defect, not a reason to make the pull request +Ready or to weaken ordinary merge admission. + +## Invariants + +- The scheduler sends `draft_review_only` as a JSON boolean derived from live + PR metadata; the receiver rejects any non-boolean envelope before external + token exchange. +- Ready work is admitted only with `draft_review_only=false`. +- Draft work is admitted only with `draft_review_only=true` and a freshly + fetched, exact repository/PR/head, non-expired central artifact. +- Artifact pagination or schema ambiguity, missing/expired markers, and any + live state/base/head mismatch fail before Contents lease mutation. +- Every pre-mutation authority recheck also re-fetches the marker. +- A clean Draft emits an exact-head formal `COMMENTED` review with the explicit + `DRAFT_REVIEW_COMPLETE` result; it never emits `APPROVED` authority. +- Only that exact-head result plus its review-only explanation satisfies the + Draft scheduler, so stale and generic comments cannot suppress a new request. +- Draft review-only work does not create a merge receipt, publish Ready status, + dispatch Noema, invoke the merge scheduler, or wake a merge-required OpenCode + workflow. +- A Draft-to-Ready transition invalidates Draft authority before lease access; + a validated Draft that changes later still publishes only a non-authorizing + comment. +- The Ready-only Required producer always sends typed `draft_review_only=false`; + omitted and malformed types fail closed at the receiver. +- Because the durable admission identity intentionally remains + repository/PR/head/component, reconciliation retires an exact-head Draft + completion lease when that pull request becomes Ready without an approving + verdict. The same head can then acquire a fresh Ready review lease. + +## Executable acceptance + +The regression suite executes the extracted production shell. It proves that +an exact authorized Draft reaches lease validation, malformed/mismatched/ +expired artifacts stop before lease access, ordinary Drafts remain rejected, +the dispatch payload distinguishes Ready and Draft work, and the Required +workflow wake is structurally disabled for a validated Draft. It also proves +that the Draft path cannot publish approval authority or start any Ready-only +follow-up, while its exact-head formal completion comment prevents an unbounded +same-head redispatch loop. A transition regression proves Draft completion, +same-head Ready retirement, and fresh Ready redispatch. The complete affected scheduler and receiver suite +must pass both locally and on the exact published head. Hosted security, +provenance, and independent semantic review remain required before ordinary +protected integration. diff --git a/docs/doctoring/opencode-coverage-approval-reuse-20261001.md b/docs/doctoring/opencode-coverage-approval-reuse-20261001.md new file mode 100644 index 0000000000..8ebeaf56f8 --- /dev/null +++ b/docs/doctoring/opencode-coverage-approval-reuse-20261001.md @@ -0,0 +1,70 @@ +# OpenCode coverage approval-reuse gate + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; fresh exact-head +hosted Checks and qualifying independent approval remain mandatory. + +## Failure evidence and root cause + +CodeRabbit review thread `PRRT_kwDOS_C14s6nmf3Q` identified a fail-open edge in +the current PR head: `publish_blockers_after_model_unavailable` and +`scripts/ci/opencode_dispatch_status.py` required only the coverage job result +`success` before reusing an existing same-head OpenCode approval. A later audit +also found the merge-scheduler's direct approval-gate invocation omitted the +coverage summary. The coverage +producer intentionally completes successfully for an honest `NOT MEASURED` +result so that it can publish diagnostics without fabricating a source defect. +Consequently, job success alone is not proof that the current dispatch measured +and passed coverage. + +The causal owner is the central `.github` OpenCode review boundary, not a +consumer repository or the model provider. The defect was an incomplete +evidence contract between the coverage producer and the approval/status +consumers. + +## Test-first repair + +The RED regression added missing, `NOT MEASURED`, malformed, and duplicate +coverage-decision cases and failed because no decision validator existed. The +repair introduces one shared Python validator that accepts exactly one +`- Result: PASS` line, applies it before existing-approval reuse and status +publication, and mirrors the same exact rule in the workflow shell path. The +workflow now supplies the current coverage summary to both consumers. + +The focused approval, security-boundary, workflow-contract, executable shell, +and reviewed-blob suites pass 186 tests with 1 optional LLVM-platform skip. +This includes concurrent exact-head commits +`87ffafa2f6b19080c01f6ee24b987b37cb92dcb8`, +`0bcded6b08af4554541223438d046bc412c4b093`, and +`feb88e34c9197275a58310306492069e25eb0b67`; their additional environment, +prefixed-result, contradictory-result, implementation, and coverage cases were +preserved rather than overwritten. After integration, the +warnings-as-errors repository suite passes 5,255 tests, 5 optional skips, and 40 +subtests. No timeout, coverage threshold, exact-head rule, independent-review +rule, or required Check is relaxed. Missing or ambiguous evidence remains a +failure, while an honest unmeasured result remains diagnostic rather than being +relabeled as a code finding. + +Fresh exact-head Trusted uv run +[`36751696675`](https://github.com/ContextualWisdomLab/.github/actions/runs/36751696675), +job `110011676248`, then exposed a test-contract omission: the suite passed, but +`scripts/ci/opencode_existing_approval_gate.py:241-242` remained unexecuted, so +the repository coverage gate reported 18,252 statements with 2 misses and +7,498 branches with 1 partial branch (99%). The repair executes the real CLI +with `NOT MEASURED` and asserts its fail-closed diagnostic. It does not exclude +the branch or lower the 100% threshold. The exact hosted command reproduced +locally with 5,255 passed, 5 optional skips, and 40 subtests, covering all +18,252 production statements and 7,498 branches at 100% with zero misses or +partial branches. + +## Operational consequence + +Existing approvals remain reusable only when all of the following are true: + +1. the approval is valid for the exact live head; +2. the coverage evidence job completed successfully; and +3. its current summary contains exactly one authoritative `PASS` decision; and +4. every approval consumer, including merge scheduling, receives that summary. + +Fresh hosted execution on the repaired head is still required. Predecessor +GREEN, skipped CodeQL, pending dispatch verdicts, or this local result do not +authorize merge. diff --git a/docs/doctoring/opencode-same-head-dispatch-idempotency.md b/docs/doctoring/opencode-same-head-dispatch-idempotency.md new file mode 100644 index 0000000000..af75351b3c --- /dev/null +++ b/docs/doctoring/opencode-same-head-dispatch-idempotency.md @@ -0,0 +1,154 @@ +# OpenCode exact-head dispatch idempotency + +## Incident + +On 2026-09-30, `ContextualWisdomLab/.github#2545` remained on exact head +`9a4af5e438283a31dc05814d6bc2818caee782a3` while the required OpenCode wake +path requested review execution more than once. Central dispatch run +[`36776536447`](https://github.com/ContextualWisdomLab/.github/actions/runs/36776536447) +was already queued for that exact repository, PR, and head. A later same-head +request created run +[`36778773766`](https://github.com/ContextualWisdomLab/.github/actions/runs/36778773766), +and the receiver's PR-keyed `cancel-in-progress: true` concurrency retired the +older run. No PR head change or substantive review result justified losing its +queue position. + +The required workflow correctly checked for an existing current-head formal +review, but, when that receipt was absent, posted `repository_dispatch` +unconditionally. The merge scheduler already deduplicated active same-head +OpenCode runs; this direct required-check path bypassed that guard. + +## Repair contract + +Before posting a new dispatch, the trusted required-check path now: + +1. validates that the target PR is live, open, ready, and still on the event + head; +2. evaluates the existing formal-review receipt predicate; +3. exchanges the existing repository-scoped OpenCode App token; +4. inventories `requested`, `waiting`, `pending`, `queued`, and `in_progress` + runs of the canonical `opencode-review-dispatch.yml` receiver twice, so a + state transition during the inventory remains observable; +5. validates each returned run's identity fields and matches its protected + exact `repository#PR@head` title plus workflow path and trigger; +6. records an exact-head execution only after every accepted older-head + cancellation reaches terminal `completed/cancelled`; and +7. re-fetches the PR immediately before dispatch, retiring the request if + state, draft status, or head authority changed. + +Run-list failures and malformed run records fail closed. The receiver has no +native concurrency group: GitHub replaces an older pending group member even +when `cancel-in-progress` is false, so native concurrency cannot preserve every +distinct callback payload. Producers deduplicate exact-head work from trusted +inventory. Before a current-head POST, the required workflow revalidates live +repository/PR/head authority and cancels only canonical older-head central +runs. A refused cancellation, failed status lookup, invalid state, terminal +non-cancelled conclusion, or accepted cancellation that remains active after +the bounded status reads fails closed without a new dispatch. Those reads do +not sleep inside the required job: the nonblocking capacity contract releases +the runner and lets a later trusted scheduler admission retry instead of +holding scarce capacity while GitHub converges. A final live-authority read +guards the POST. + +The merge scheduler separately binds native concurrency to the exact PR head +and uses GitHub's bounded `queue: max` FIFO instead of lossy single-pending +replacement. `synchronize` and `closed` events run a metadata-only cleanup job +with `actions: write`; it inventories every active status, revalidates the live +PR/head immediately before each mutation, cancels only predecessor-head work +(or all final-head work on close), and accepts completion only after GitHub +reports `completed/cancelled`. Two complete status passes prevent a state +transition from escaping between filtered queries; every response reconciles +the collected row count with `total_count` and fails closed if GitHub's +filtered-search ceiling truncates the inventory. The inventory spans every +event for this workflow and then binds candidates through PR association and +head SHA; filtering the API to `pull_request_target` would strand predecessor +`pull_request_review` runs in their old exact-head group. + +Producer observation and POST are not atomic, so the receiver first authorizes +the exact actor/sender pair, repository allowlist membership, and complete +payload shape before any OIDC exchange or central mutation. It then acquires a +central repository-owned lease after full live state/draft/base/head metadata +validation and before source +materialization, coverage, or model execution. The lease uses one file per +repository/PR on the dedicated `opencode-dispatch-leases` branch. GitHub's +Contents API compares the observed blob SHA during update: simultaneous cache +misses or terminal-owner takeovers can commit only one owner, while a loser +reloads the authoritative file and defers to the active exact-head run. A +different-head run may replace an owner only after a fresh live PR/head check +immediately before the compare-and-swap. Every recorded owner is validated +against its canonical workflow, event, title, and head; a rerun with the same +GitHub run ID retains its own lease. The lease file remains as auditable +bounded state and is updated, not multiplied, on later heads. + +Lease ownership alone is not a durable completion receipt. Immediately after +acquiring or retaining the exact-PR lease, the receiver revalidates live +repository/PR/head authority and evaluates the formal exact-head review receipt +from the trusted default-branch helper. An existing receipt returns +`admitted=false` before source materialization, coverage, or model execution; +an unavailable or malformed receipt lookup fails closed. + +The dedicated admission job alone holds `contents: write` for that lease +branch; later metadata validation and source materialization return to +`contents: read`. +The required target-repository job retains read-only Actions and contents +authority; central cancellation uses the existing repository-scoped App token. +After a formal receipt, the privileged publisher uses the repository-wide run +inventory and matches the intended PR number plus +`pull_requests[].head.sha`. It deliberately does not treat the run-level +`head_sha` as the PR head because `pull_request_target` executes trusted +default-branch workflow code. GitHub caps every filtered workflow-run search at +1,000 results, so the publisher recursively bisects the PR-lifetime `created` +range until each interval is within the API bound, then requires the sum of +every interval's `total_count` to equal the collected rows. An interval with +more than 1,000 runs in a single second fails closed. The publisher revalidates +live open/non-draft head authority immediately before every mutation and reruns +each matching failed Required OpenCode job. Therefore a duplicate +producer that lost the lease does not need its own callback payload or a +polling runner. Human review events do not enter or cancel the required +workflow. The repair does not weaken the required verdict, accept predecessor +evidence, or broaden model/provider permissions. + +## Executable evidence + +`tests/test_opencode_required_verdict_regression.py` executes the production +shell step. Its RED fixture proved that a queued exact-head run still produced +a second dispatch. The GREEN cases cover all five active admission states, +transition-safe two-pass inventory, malformed and unavailable inventory +fail-closure, current-head preservation, older-head non-suppression, +no-active-run dispatch, existing formal receipts, and a head movement between +initial validation and the mutation boundary. Queue-contract tests prohibit +lossy native receiver concurrency; execution fixtures prove exact older-head +cancellation, deduplication, asynchronous cancellation continuation, and +fail-closure when a cancellation is refused, remains active, returns an invalid +state, or terminates with a non-cancelled conclusion. Scheduler cleanup +fixtures also prove concurrent-head-movement preservation, transition-safe +two-pass discovery, review-event predecessor discovery, +inventory-completeness rejection, and bounded terminal cancellation +verification. +Receiver fixtures execute absent, active-owner, terminal-owner, self-rerun, +different-head takeover, and branch-initialization-race lease paths. +A two-process fixture starts simultaneous cache misses against an atomic fake +Contents API and proves exactly one `admitted=true` result. The independent +negative fixtures prove that unauthorized envelopes perform no OIDC or GitHub +API call and that closed, draft, or mismatched base/head metadata performs no +Contents mutation. Wake fixtures use a trusted-base run-level SHA distinct from +the PR head and cover multiple failures, another PR sharing the same commit, +authority movement between mutations, partial rerun failure, recursive +partitioning above 1,000 results, and detected pagination truncation. The +independent byte-for-byte reviewer pin was regenerated from the repaired +receiver as exact Git blob +`5c87c74863ef6872c1ef7136d5b330071920c09e`. + +Local exact-tree verification on the stacked successor base +`86ddef63ed306d4c7d56d051d9a72570e7d358a5`: + +- required-workflow, nonblocking capacity, queue, receiver, and integrity-pin + contracts after independent-review repair: `233 passed`; +- complete Python 3.14 warnings-fatal suite: `5362 passed, 5 skipped, 40 + subtests passed`, with + owned production `18767/18767` statements and `7648/7648` branches, + Docstring `100%`, and zero warnings. + +Protected merge still requires hosted exact-head security and quality Checks, +zero unresolved review threads, qualifying independent approval, and ordinary +branch protection. diff --git a/docs/doctoring/repository-metadata-shallow-ancestry.md b/docs/doctoring/repository-metadata-shallow-ancestry.md new file mode 100644 index 0000000000..03e0f61eb1 --- /dev/null +++ b/docs/doctoring/repository-metadata-shallow-ancestry.md @@ -0,0 +1,45 @@ +# Repository Metadata Reconcile shallow-ancestry RCA + +## Status and exact evidence + +Status: Proposed on `ContextualWisdomLab/.github#2536`. Exact-head run +[`36720930491`](https://github.com/ContextualWisdomLab/.github/actions/runs/36720930491), +job `109905558240`, checked out +`737fc6fd3b536495a7d5f8bbbae9d0474771d21f` and failed the repository-wide +suite at +`tests/test_github_api_url_boundary.py::test_documented_opener_lineage_references_published_commits`. +The exact assertion reported evidence commit +`57477289ebec5631b0c48f0bc419f336dbe19deb` as an invalid object. + +## Root cause + +The workflow verified the exact requested revision but left the pinned +`actions/checkout` input `fetch-depth` at its default value of `1`. The G-17 +contract intentionally calls `git cat-file` and `git merge-base --is-ancestor` +for published commits named by the product-gap baseline. A depth-one object +database cannot answer that ancestry question after an ordinary merge even +when the evidence commit is genuinely reachable. The earlier local full-history +run therefore did not reproduce the hosted runner's incomplete Git object +database. + +This is a workflow-fixture defect, not a product-source defect and not stale +evidence. Exact-head checkout alone and history availability are distinct +contracts. + +## RED, repair, and verification + +The new workflow regression failed first because the validation checkout did +not contain `fetch-depth: 0`. Commit +`3bc859c73ed67074df13b2e01aa89dff2159e260` adds that single checkout input and +the regression. The focused workflow and ancestry suites then passed 37 tests. +The exact revision assertion and `persist-credentials: false` remain in place; +the apply job's credential and write boundaries are unchanged. + +Hosted exact-head revalidation remains required after publication. A queued, +skipped, pending, cancelled, or predecessor result is not passing evidence. + +## Reference + +actions/checkout contributors. (2026). *Checkout V7: Fetch all history for all +tags and branches* [Computer software documentation]. GitHub. +https://github.com/actions/checkout/blob/main/README.md#fetch-all-history-for-all-tags-and-branches diff --git a/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md index d0f1cbbb2b..c6c3ff67b6 100644 --- a/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md +++ b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md @@ -101,57 +101,3 @@ https://github.com/advisories/GHSA-chgr-c6px-7xpp Open Source Vulnerabilities. (2026). *CVE-2026-102274: PyJWT RSA JWK Set availability failure*. https://osv.dev/vulnerability/CVE-2026-102274 - -## 2026-10-01 urllib3 audit follow-up - -Python Security run `36733279716`, job `109949358063`, found two newly -published vulnerabilities in urllib3 2.7.0: CVE-2026-97687 permits target TLS -policy to weaken or replace HTTPS proxy TLS policy, and CVE-2026-97689 permits -an unbounded chunk-size line to consume memory in streaming clients. Both are -fixed in urllib3 2.8.0. The same vulnerable transitive pin appeared in the -pip-audit and Strix hash locks, so this remains one central security-owner -repair rather than two consumer workarounds. - -The repair adds urllib3 2.8.0 to both source inputs and regenerates both locks -with their recorded uv commands. A contract requires exactly one 2.8.0 row in -each source input and generated lock. Comparison against exact predecessor -`d1aa3659fca527a6c7330151f3ab4df3d7578391` shows no unrelated package-version -movement. Repeated compilation produced identical SHA-256 digests, and -pip-audit 2.10.1 reported no known vulnerabilities for either generated lock. -These local results are not merge authority: exact-head hosted security Checks, -terminal authenticated CodeQL evidence, independent approval, and ordinary -protected merge remain required. - -## 2026-10-01 PyJWT recursion denial-of-service follow-up - -Security Scan run [36741151937](https://github.com/ContextualWisdomLab/.github/actions/runs/36741151937) -found GHSA-42vr-xj54-vc7v / -CVE-2026-101918 in PyJWT 2.14.0. Dependency Review job `109975641239` and OSV -job `109975641271` both rejected that shared Strix lock. An attacker-controlled, -deeply nested unsigned JWT payload can exhaust Python recursion during unverified -payload parsing in `PyJWKClient.get_signing_key_from_jwt`, before key lookup, -and raise an uncaught request-level exception; -the available evidence does not establish a process crash or authentication -bypass. PyJWT 2.15.0 contains the upstream fix. - -The canonical-owner repair advances the explicit source pin and generated lock -to 2.15.0. Lock regeneration changes only the PyJWT version and its wheel/sdist -hashes; urllib3 2.8.0, PyO3 0.29.2, and the single-purpose cryptography override -remain unchanged. The existing parity test was first changed to require 2.15.0 -and failed against the 2.14.0 source and lock before implementation. Hosted -exact-head Security, CodeQL, independent approval, ordinary protected merge, -and immutable consumer-pin advancement remain release gates. - -Local verification used the hosted-workflow Python 3.12 line. The focused -source/lock contract passed 5 tests and the warnings-as-errors repository suite -passed 5,167 tests, 6 skips, and 40 subtests. Repeating the recorded `uv 0.12.18` -compile command was byte-identical at lock SHA-256 `76443a3300d0…`; a -hash-enforced, no-dependency installation loaded PyJWT 2.15.0 and urllib3 2.8.0. -`pip-audit 2.10.1` reported no known vulnerabilities. The repository's existing -97% docstring baseline remains a separate HOLD and is not represented as green. -An independent review found no remaining Critical, Important, or Minor finding -after correcting the advisory's attack-vector wording. - -GitHub. (2026). *PyJWT has a denial of service vulnerability via maliciously -crafted JWT token with deeply nested payload* (GHSA-42vr-xj54-vc7v). -https://github.com/jpadilla/pyjwt/security/advisories/GHSA-42vr-xj54-vc7v diff --git a/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md new file mode 100644 index 0000000000..adebc5c8b2 --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md @@ -0,0 +1,48 @@ +# Shared Strix PyJWT recursion security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +## Failure scene and causal owner + +Exact-head Security Scan run +[`36740858208`](https://github.com/ContextualWisdomLab/.github/actions/runs/36740858208), +job `109974634074`, evaluated +`d76ab4238591cc33b329881782e2759f3f5d51be`. Its dependency-review support +check reached GitHub successfully and the other scanner jobs passed. The +dependency-review action then rejected `requirements-strix-ci.txt` because +PyJWT 2.14.0 is affected by GHSA-42vr-xj54-vc7v, an unauthenticated +`RecursionError` denial of service in pre-verification payload parsing. + +This is a shared Strix CI-runtime lock, so the central `.github` security and +review bounded context is the causal owner. It is not a transient network +failure, a consumer defect, or stale predecessor evidence. The dependency +review remains fail-closed; no severity threshold or workflow gate changes. + +## RED to repair + +The retained `test_strix_pyjwt_security_pin_is_an_explicit_lock_input` +contract first failed with one failure and four passing dependency tests when +it required PyJWT 2.15.1 but both the source and lock still selected 2.14.0. +The repair advances the explicit source pin and its two PyPI artifact hashes to +2.15.1. PyJWT's upstream changelog records the recursion hardening in 2.15.0; +the signed 2.15.1 release includes that fix and adds a Base64URL-padding +correction. + +The focused dependency contract passes five tests after the repair. pip-audit +2.10.1's strict exact-pin audit reports no known vulnerabilities. The final +local tree passes 5,236 tests, 5 optional skips, and 40 subtests in the +warnings-as-errors repository suite; Ruff and `git diff --check` also pass. +Completion still requires fresh hosted Security Scan, Python Security, and +every other applicable exact-head check plus an independent review. + +## References + +PyJWT maintainers. (2026, September 28). *PyJWT 2.15.1* [Software release]. +GitHub. https://github.com/jpadilla/pyjwt/releases/tag/2.15.1 + +PyJWT maintainers. (2026). *Changelog* [Software documentation]. GitHub. +https://github.com/jpadilla/pyjwt/blob/2.15.1/CHANGELOG.rst + +Python Package Index. (2026, September 28). *PyJWT 2.15.1* [Package release]. +https://pypi.org/project/PyJWT/2.15.1/ diff --git a/docs/doctoring/shared-security-baseline-urllib3-20260930.md b/docs/doctoring/shared-security-baseline-urllib3-20260930.md new file mode 100644 index 0000000000..f18070e1aa --- /dev/null +++ b/docs/doctoring/shared-security-baseline-urllib3-20260930.md @@ -0,0 +1,48 @@ +# Shared Python CI urllib3 2.8.0 security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +## Failure scene and causal owner + +After `ContextualWisdomLab/.github#2536` became Ready, exact-head Python +Security run +[`36737059681`](https://github.com/ContextualWisdomLab/.github/actions/runs/36737059681), +job `109961499214`, audited the pull-request merge revision whose head was +`88143f95d36be9b08550b52b86bd2baeefa0fe86`. The job found urllib3 2.7.0 in +both `requirements-pip-audit-ci-hashes.txt` and +`requirements-strix-ci-hashes.txt`. pip-audit reported CVE-2026-97687, +CVE-2026-97688, and CVE-2026-97689, each fixed in 2.8.0. The first can apply +target TLS policy or credentials to an HTTPS proxy; the latter two permit CPU +or memory denial of service through hostile chunked streaming responses. + +These are shared `.github` CI-runtime locks, so the central security/review +bounded context is the causal owner. No product repository may suppress the +audit or copy a mutable proposed lock. + +## RED to repair + +The retained regression +`test_python_security_inputs_pin_patched_urllib3` first failed because neither +source input constrained urllib3. The repair adds the exact `urllib3==2.8.0` +constraint to both source inputs, regenerates both locks with their recorded +`uv pip compile --generate-hashes` commands, and requires source/lock parity in +both runtime graphs. The generated artifact hashes are +`0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3` +and `63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63`. + +The change does not alter pip-audit severity, failure classification, network +policy, or any workflow gate. Both regenerated locks returned `No known +vulnerabilities found` under pip-audit 2.10.1's strict exact-pin audit. The +focused dependency contract passes 5 tests, and the exact local tree passes +5,236 tests with 5 optional skips and 40 subtests while warnings are errors. +`git diff --check` passes. Completion still requires a fresh exact-head Python +Security result plus all other applicable checks and independent review. + +## References + +urllib3 maintainers. (2026, September 15). *urllib3 2.8.0* [Software +release]. GitHub. https://github.com/urllib3/urllib3/releases/tag/2.8.0 + +urllib3 maintainers. (2026, September 15). *Security advisories* [Security +advisory index]. GitHub. https://github.com/urllib3/urllib3/security/advisories diff --git a/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md b/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md deleted file mode 100644 index fc573fc8e1..0000000000 --- a/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md +++ /dev/null @@ -1,53 +0,0 @@ -# Shared Strix LiteLLM credential-exfiltration RCA - -## Incident binding - -On 2026-10-01, `.github` PR #2531 exact head -`516471fbe7d4e93a50c7bbba20402447f06f8d8b` failed Python Security run -`36799069276`, job `110169140365`. The unmodified -`requirements-strix-ci-hashes.txt` selected LiteLLM 1.94.1, and `pip-audit` -reported CVE-2026-84377 / GHSA-3cv6-jpf6-8222. The advisory describes an -authenticated request-body routing override that can redirect an upstream call, -exfiltrate configured provider credentials, and reach internal services. - -This is a canonical control-plane dependency defect, not a product-PR finding -and not an audit-service transient. The same exact head also failed Trivy on the -Noema document reader's `fast-uri` 3.1.7 and `ip-address` 10.7.0. Stacked PR -#2545 already owns and proves the minimal Node transitive repair, so its ordinary -commit is preserved in the repaired #2531 ancestry instead of being copied or -reimplemented. - -## Test-first repair - -The RED contract -`test_strix_litellm_security_pin_is_an_explicit_lock_input` first failed because -the source input did not own a LiteLLM pin. The minimal repair: - -1. selects `litellm==1.94.3` in `requirements-strix-ci.txt`, the first patched - release in the retained 1.94 line; -2. regenerates `requirements-strix-ci-hashes.txt` with the repository's declared - `uv pip compile --generate-hashes` command; -3. requires exact source/lock parity so a future resolver run cannot silently - restore an affected release; and -4. preserves #2545's `fast-uri==3.1.8` and `ip-address==10.7.1` source overrides, - lock, and nested-copy regression contract as unchanged ancestry. - -No scanner finding is ignored or suppressed. `pip-audit` over the repaired -hash lock reports no known vulnerabilities. At repaired exact head -`fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, -Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run -`36800615444` are terminal GREEN. CodeQL run `36800615319` remains fail-closed: -both language jobs recorded `verdict=pending` while the exact-head dispatch job -succeeded. A qualifying independent approval and ordinary protected merge are -still required; local or partial hosted evidence is not merge authorization. - -## References - -BerriAI. (2026, August 26). *Authenticated SSRF and provider-credential -exfiltration via unvalidated request-body routing parameters* -(GHSA-3cv6-jpf6-8222) [Security advisory]. GitHub. -https://github.com/BerriAI/litellm/security/advisories/GHSA-3cv6-jpf6-8222 - -National Institute of Standards and Technology. (2026). *CVE-2026-84377*. -National Vulnerability Database. -https://nvd.nist.gov/vuln/detail/CVE-2026-84377 diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 674a5d0b5a..958d793145 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -33,11 +33,17 @@ Empty non-draft pull requests are closed by the existing metadata-only former standalone required workflow was removed so the same PR no longer consumes a second runner for the same metadata decision. -The central `security-scan.yml` and `sast-semgrep.yml` pull-request triggers are -base-ref agnostic. They therefore also run for stacked pull requests targeting a -feature branch; the organization ruleset's protected-ref scope remains an -independent administrative control and is not weakened by this trigger -coverage. +The central `security-scan.yml`, `sast-semgrep.yml`, and `codeql-pr.yml` +pull-request triggers are base-ref agnostic so they do not hard-code a default +branch name. That trigger shape does not widen ruleset `18156473`: its +`ref_name.include=["~DEFAULT_BRANCH"]` scope injects these required workflows +only when the pull request targets the repository default branch. Stacked pull +requests targeting a feature branch therefore do not materialize these required +workflows through this ruleset. They receive fresh evidence after the +prerequisite merges and the dependent pull request is retargeted or synchronized +onto the default branch. A repository that runs one of these files natively may +have broader trigger coverage, but that is separate from organization-ruleset +injection. Stacked pull requests are audited by organization ruleset `CWL Stacked OpenCode required workflow` (`21732164`) in `evaluate` mode. It @@ -50,6 +56,8 @@ the ref update before a `pull_request_target.synchronize` run can exist for the new commit, so it rejects both initial branch creation and later review fixes. Exact-head OpenCode evidence remains a merge requirement enforced by the normal PR procedure while a target-ref-scoped enforcement design is developed. +Evaluate-mode observations are audit evidence, not passing required-check or +merge-authorization evidence. ## OpenCode required workflow posture @@ -171,14 +179,17 @@ see the historical marker above. ### Audit tool coverage -`scripts/ci/audit_central_required_workflows.py` defines all nine canonical -required workflow paths (`codeql-pr.yml` deliberately excluded, per the -2026-09-03 correction above) and treats the live policy as an exact -inventory: every required path must appear exactly once with repository id -`1274066402` and `refs/heads/main`, while any additional well-formed workflow -path — including a re-added `codeql-pr.yml` — is reported as -`unexpected workflow present in required set` drift instead of silently -passing. A malformed workflow entry (not an object, or missing a string +`scripts/ci/audit_central_required_workflows.py` defines all ten canonical +required workflow paths and treats the live policy as an exact inventory: +every required path must appear exactly once with repository id `1274066402` +and `refs/heads/main`, while any additional well-formed workflow path is +reported as `unexpected workflow present in required set` drift instead of +silently passing. The 2026-09-03 nine-path exclusion was historical: it removed +the incompatible `codeql-pr.yml` that embedded CodeQL actions. ADR-0025 later +re-admitted the path only after it became a lightweight dispatch producer and +verdict consumer; native `codeql-scan-dispatch.yml` remains outside the +required-workflow ruleset and owns the CodeQL actions. A malformed workflow +entry (not an object, or missing a string `path`) is now reported by its index (`central required workflow entry N is malformed`) instead of being silently skipped, so a structurally broken ruleset payload surfaces as loud audit failures rather than a quietly diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b4882cee5a..fc7525d280 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,10 +1,27 @@ # Product and Technical Gap Baseline +## 2026-10-01 SBOM publication lineage incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-SBOM-PUBLICATION-TREE-01 | **Source repair proposed; protected integration and post-merge publication proof pending** | Scheduler run `36836856075`, job `110286232014`, published `.github#1678@7598436eb9126db7bdff00fcf31f6fda15d0f58f` with an ancestry-only `ours` merge. The prior owner repairs remained ancestors while 23 non-inventory paths disappeared from the tree. OSV `110286490852`, dependency-review `110286490849`, Trivy `110286490623`, and pip-audit `110286478880` then failed on restored vulnerable pins. Independent review also reproduced a generated commit that carried `docs/sbom/reviewer-notes.md`, an uncommitted root owner-file side effect, and a prior publication lineage that changed then reverted an owner file. Final-tree comparison accepted the reverted pair and made both unauthorized commits ancestors of the next publication. | Canonical owner is `.github/workflows/sbom-inventory-scheduler.yml` plus `scripts/ci/reconcile_sbom_publication_lineage.sh`. Restrict generated staging and generated first-parent deltas to `docs/sbom/inventory.json` plus `docs/sbom/inventory.md`, reject every other tracked or untracked worktree side effect, and inspect every commit reachable only from the prior publication head against its first parent before publication mutation. Only a generated-only predecessor may enter ordinary two-parent reconciliation, with the current inventory authoritative and every other conflict fail-closed. RED/GREEN real-Git contracts, exact-head hosted Checks, ordinary protected merge, then a forward repair of #1678 and fresh scheduled publication tree comparison are required. | + +## 2026-10-01 authorized Draft review admission + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The agent-mention workflow and scheduler created an exact repository/PR/head durable Draft-review request, but the central receiver unconditionally required `live_draft=false`; every explicitly authorized Draft review therefore stopped before lease admission and could never produce the semantic review it requested. Once admitted, the clean-verdict publisher also attempted `APPROVED` before its downstream Draft receipt guard, which could grant merge authority to review-only work. Finally, a completed Draft lease shared the Ready identity and could suppress same-head Ready review forever | `.github#2546@db81de7a09268eb0abfc5b5c675ddd9a0a38a3e2`; `.github/workflows/agent-mention-opencode-dispatch.yml` marker `cwl-draft-review-request---`; `.github/workflows/opencode-review-dispatch.yml` former `live_authority_matches` and unconditional clean-verdict approval; mode-independent admission identity in `scripts/ci/review_admission_controller.py`; local RED contracts in `tests/test_opencode_required_verdict_regression.py` and `tests/test_pr_review_merge_scheduler.py` | Carry a typed review-only boolean in every producer payload, reject omitted/malformed authority before token exchange, and admit a live Draft only after re-fetching a non-expired exact marker from the central Actions artifact API. Revalidate state/base/head plus marker before every lease mutation. Convert a clean Draft verdict to an exact-head `DRAFT_REVIEW_COMPLETE` formal comment before publication; accept only that source-backed comment as Draft scheduler completion, retire that lease if the same head becomes Ready without approval, and structurally skip formal merge receipt, Ready status, Noema, merge-scheduler, and Required-workflow wake follow-ups. See [RCA and executable acceptance](doctoring/opencode-authorized-draft-review-admission.md) | **Proposed / local RED→GREEN complete; exact-head hosted Checks and independent semantic review required** | + +## 2026-10-01 OpenCode same-head dispatch idempotency + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| A repeated required-check wake for unchanged `.github#2545@9a4af5e438283a31dc05814d6bc2818caee782a3` cancelled queued central dispatch `36776536447` when same-head run `36778773766` was created, resetting queue position without new source authority | [Run 36776536447](https://github.com/ContextualWisdomLab/.github/actions/runs/36776536447); [run 36778773766](https://github.com/ContextualWisdomLab/.github/actions/runs/36778773766); protected `main@37b10243cec3d160ecc9c1be75c71428b160a703` | At the central `.github` owner, inventory all five active admission states twice, validate run identity and fail closed on ambiguity, remove lossy native receiver concurrency, retire only canonical older-head central runs after live repository/PR/head validation, and require every accepted cancellation to reach `completed/cancelled` before current-head dispatch. Preserve merge-scheduler exact-head admissions with bounded `queue: max`; on `synchronize` or `closed`, a metadata-only job inventories every active state twice across all PR-associated scheduler triggers, rejects invalid or `total_count`-incomplete results, revalidates live PR/head authority before each mutation, retires only predecessor work, and proves terminal cancellation. Because producer inventory and POST are not atomic, authorize actor/sender/target and payload shape before OIDC, then acquire one repository/PR lease through the Contents API's blob-SHA compare-and-swap only after full live state/draft/base/head validation; active same-head losers terminate cheaply, self-reruns and terminal owners recover, and a freshly revalidated live head may replace a fully validated older-head owner. After lease acquisition or retention, revalidate live repository/PR/head authority and the trusted formal exact-head receipt; an existing receipt must return `admitted=false` before source materialization, coverage, or model execution, and an unavailable or malformed lookup fails closed. Isolate central `contents: write` in a dedicated lease job and keep later jobs read-only. After formal receipt, use repository-wide run inventory, bind PR number and `pull_requests[].head.sha` rather than the trusted-base run-level SHA, recursively partition the PR-lifetime `created` range below GitHub's 1,000-result ceiling, verify `total_count` completeness, and revalidate before each failed-job rerun so duplicate callbacks are unnecessary. See [RCA and executable acceptance](doctoring/opencode-same-head-dispatch-idempotency.md) | **Proposed / local RED→GREEN complete; exact-head hosted Checks and independent approval required** | ## 2026-10-01 Maturin response-lifecycle coverage closure | Gap | Exact evidence | Action | Status | |---|---|---|---| -| `.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4` passed all 5,314 tests but failed the complete branch gate because the canonical Maturin downloader left five error-path statements and two branches unexecuted; the owner workflow omitted both verifier paths and stacked PR bases | Trusted uv Materializer run `36811202519`, job `110206427182`; `verify_release_maturin_tool_assets.py` 95%, missing lines 104 and 106-112 plus branch 114→116; no owner run at #2530 predecessor `8cf2ea5f73976d47b2267fb52ac28284323404b7` because its base was #2531 rather than `main` | Repair canonical successor `.github#2530`: exercise non-200 and opener-raised `HTTPError` closure, replace the impossible nullable-response finalizer with one unconditional response-owned close scope, add source/test and stacked-PR trigger contracts to the complete gate while retaining protected-main push scope, preserve all network and fail-closed boundaries, then ordinary-merge the accepted owner head into #1653 | **Proposed / hosted RED reproduced; focused verifier coverage GREEN locally; path and stacked-admission contracts RED→GREEN; exact-head hosted full-suite, security, CodeQL, and independent approval required** | +| `.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4` passed all 5,314 tests but failed the complete branch gate because the canonical Maturin downloader left five error-path statements and two branches unexecuted; the owner workflow omitted both verifier paths and stacked PR bases | Trusted uv Materializer run `36811202519`, job `110206427182`; `verify_release_maturin_tool_assets.py` 95%, missing lines 104 and 106-112 plus branch 114→116; no owner run at #2530 predecessor `8cf2ea5f73976d47b2267fb52ac28284323404b7` because its base was #2531 rather than `main` | Repair canonical successor `.github#2530`: exercise non-200 and opener-raised `HTTPError` closure, replace the impossible nullable-response finalizer with one unconditional response-owned close scope, document the archive/CLI trust boundaries, expand the trusted docstring gate to all `scripts/ci`, add source/test and stacked-PR trigger contracts to the complete gate while retaining protected-main push scope, preserve all network and fail-closed boundaries, then ordinary-merge the accepted owner head into #1653 | **Proposed / hosted RED reproduced; focused verifier coverage and 100% production docstrings GREEN locally; path and stacked-admission contracts RED→GREEN; exact-head hosted full-suite, security, CodeQL, and independent approval required** | ## 2026-10-01 bounded Maturin release downloader SAST closure @@ -22,7 +39,7 @@ | Gap | Exact evidence | Action | Status | |---|---|---|---| -| The generated Noema document-reader lock selected `fast-uri` 3.1.7 and `ip-address` 10.7.0 after CVE-2026-86472, CVE-2026-101911, and CVE-2026-101912 were published | Security Scan run `36773087489`; Trivy job `110084194330`; exact predecessor `.github#2530@a99784219305d1b6e14cf76f0acea30c5ee45e21` | At the central `.github` owner, regenerate only the two transitive entries to `fast-uri` 3.1.8 and the first patched `ip-address` 10.7.1 release, scan every hoisted or nested lock entry in a regression contract, reproduce with `npm ci`, and require a zero-vulnerability npm audit | **Proposed / local RED→GREEN and audit complete; exact-head hosted security and independent approval required** | +| The generated Noema document-reader lock selected `fast-uri` 3.1.7 and `ip-address` 10.7.0 after CVE-2026-86472, CVE-2026-101911, and CVE-2026-101912 were published | Security Scan run `36773087489`; Trivy job `110084194330`; exact predecessor `.github#2530@a99784219305d1b6e14cf76f0acea30c5ee45e21` | At the central `.github` owner, regenerate the two transitive entries to `fast-uri` 3.1.8 and `ip-address` 10.7.2, scan every hoisted or nested lock entry in a regression contract, reproduce with `npm ci`, and require a zero-vulnerability npm audit | **Proposed / local RED→GREEN and audit complete; exact-head hosted security and independent approval required** | ## 2026-10-01 shared Strix LiteLLM credential-exfiltration closure @@ -50,11 +67,34 @@ 작성 기준일: **2026-08-26 10:35 KST** 대상: **ContextualWisdomLab/.github** 중앙 거버넌스·자동화 레포지터리와 이를 소비하는 naruon 생태계 -현재 보호된 `main`: `826b92394c63deb6981c3a8d16a724d71f85a0d7` -현재 열린 PR 수: **107** (아래 표에 이 스냅샷의 전체 목록 포함; live API 재수집) +2026-08-26 스냅샷의 보호 `main`: `826b92394c63deb6981c3a8d16a724d71f85a0d7` +2026-08-26 스냅샷 기준 현재 열린 PR 수: **107** (아래 표에 당시 전체 목록 포함) 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-10-01 live-base diff incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-AGENT-QUALITY-LIVE-BASE-01 | **Proposed — live-base source repaired; stacked-owner admission repaired; hosted verification pending** | `.github#1678@b9651115…`의 Agent Review Runtime Quality CI run `36804488453`, job `110185716853`은 이벤트에 고정된 과거 base `f2506388…`와 exact head를 비교해, 현재 protected `main@37b10243…`에 이미 존재하는 CSV CRLF·라이선스 fixture 공백 407건을 PR delta로 오인했다. Canonical owner `.github#2530@dc54310c…`를 일반 두-parent 병합한 #2547 head `b66036e3…`에서는 workflow path가 바뀌었는데도 `pull_request.branches: [main]` 때문에 owner workflow 자체가 시작되지 않았다. | Canonical workflow owner는 `.github/workflows/agent-review-runtime-quality-ci.yml`이다. changed-path 선택과 최종 whitespace gate는 각각 base ref를 즉시 다시 fetch한 뒤 exact head와의 merge-base를 사용한다. stacked-base 계약 RED 뒤 base 제한 한 줄만 제거해 path filter·read-only 권한·exact-head checkout·PR-stable concurrency를 보존했다. 새 exact-head hosted Checks·독립 리뷰·ordinary protected merge 후 #1678이 owner commit을 일반 병합하고 exact-head run을 다시 통과해야 한다. | + +### 2026-10-01 stacked required-workflow scope delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-STACKED-REQUIRED-WORKFLOW-SCOPE-01 | **Proposed — owner RED reproduced; 94 focused contracts GREEN; hosted acceptance pending** | `ContextualWisdomLab/OpenCode#3@c15dabc5…`가 feature-base stack에서 hosted test만 materialize하고 중앙 Security/SAST/CodeQL은 생성하지 않았다. 활성 ruleset `18156473`의 `ref_name.include=["~DEFAULT_BRANCH"]`와 evaluate-only stacked ruleset `21732164`가 원인이며, base-ref-agnostic workflow trigger가 ruleset injection scope를 넓힌다는 rollout 및 세 workflow 주석은 관측과 모순됐다. `.github#2537@f79c8f2e…`는 역사적 RED이고, 재수집한 repair base는 `.github#2537@3d2656ba…`다. scope·ruleset audit·CodeQL·Security·docs-only·Draft admission 묶음은 수정 tree에서 94 passed다. | Canonical owner는 `ContextualWisdomLab/.github`의 rollout 문서와 중앙 workflow 주석이다. Ruleset scope와 native trigger scope를 분리한 ordinary commit을 #2537 exact head에 게시한 뒤 후속 `.github#2548`에 비강제 ordinary merge로 계보를 전달한다. 두 PR은 hosted exact-head Checks와 독립 승인 전까지 Draft/Proposed이며, stacked consumer는 선행 PR 병합 후 default branch로 retarget/synchronize해 fresh required evidence를 받아야 한다. | +| CONTROL-CODEQL-ACTIVE-DISPATCH-IDEMPOTENCY-01 | **Proposed — live RED reproduced; owner-union local GREEN; hosted acceptance pending** | `.github#2531@7900ba4c…`의 required run `36804208074` attempt 1은 exact central run `36804251663`을 만들었지만 terminal receipt가 아직 없었다. attempt 2 coordinator가 동일 title의 run `36815888197`을 다시 생성했고, 앞선 durable queued run은 2초 뒤 취소됐다. owner repair `49d337105…`는 protected path/event, full repo/PR/head/base/required-run/source title, trusted actor와 다섯 active state를 모두 일치시킨 run이 있으면 OIDC/token/POST 전에 재사용한다. wrong title/path/event/actor/triggering actor와 terminal run은 recovery를 막지 않는다. `.github#2549@1e2d7533…` 위 ordinary two-parent owner-union tree에서 focused 249 passed다. | Canonical owner는 `.github/workflows/codeql-pr.yml` coordinator다. Consumer는 중복 억제를 복사하지 않는다. #2549 successor가 #2537 repair ancestry를 보존한다. Fresh hosted exact-head Checks와 독립 승인 전까지 두 PR은 Draft/Proposed다. | +| CONTROL-CODEQL-PREVALIDATION-CANCELLATION-01 | **Proposed — contract RED→GREEN; protected rollout pending** | `codeql-scan-dispatch.yml`의 repository/PR workflow concurrency는 actor·head·base·required-run·source validation보다 먼저 평가됐다. 따라서 늦게 도착한 stale payload가 current handler를 취소한 뒤 자기 자신은 validation에서 거절될 수 있었다. repair tree는 이 pre-validation cancellation을 제거하고 trusted metadata validation/settlement를 `CWL central control`에, 실제 matrix scan만 `CWL central CodeQL`에 둔다. owner-union 전체 warnings-fatal suite는 5,437 passed, 5 optional skips, 40 subtests이고 production 18,882 statements·7,712 branches 및 Docstring은 모두 100%다. | Canonical owner는 protected central handler다. Scheduler의 freshly revalidated Draft/closed retirement와 coordinator exact-active dedupe가 lifecycle을 소유하며, runner capacity나 scanner evidence를 우회하지 않는다. | +| CONTROL-CENTRAL-DISPATCH-RETIREMENT-01 | **Proposed — owner-union local GREEN; protected rollout pending** | `.github#2537@3d2656ba…`의 scheduler repair와 후속 CodeQL admission repair `49d337105…`를 `.github#2549@1e2d7533…` 위 ordinary two-parent union이 보존한다. Scheduler는 protected CodeQL/OpenCode/Strix run-name을 파싱하고 destructive boundary에서 run과 PR을 다시 읽어 closed 또는 stale-head run만 취소한다. Current-head, malformed, unrelated, unreadable authority는 보존한다. | Canonical owner는 `.github` merge scheduler다. 이 stale retirement는 retry나 plan-level capacity restoration이 아니며, 새 #2549 exact-head hosted evidence와 independent review가 필요하다. | + +### 2026-10-01 OpenCode approval-order delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-OPENCODE-LATEST-REVIEW-01 | **Proposed — exact head `ba55414b…`; local RED→GREEN; hosted exact-head acceptance pending** | `.github#2536@18c886cb…`에서 existing-approval gate가 같은 head의 최신 `CHANGES_REQUESTED`를 건너뛰고 과거 validated `APPROVED`를 재사용하는 RED를 재현했다. Gate를 dispatch-status와 동일한 latest-decision authority로 정렬한 focused suite는 44 passed다. 후속 coverage 회귀가 stale-head와 unknown-actor skip 분기를 실제 실행하며 최종 warning-fatal coverage suite는 5,257 passed, 5 optional skips, 40 subtests, 18,252/18,252 statements와 7,498/7,498 branches다. 첫 수리는 ordinary one-parent commit `ba55414b…`로 force 없이 게시됐다. | Canonical owner는 중앙 `scripts/ci/opencode_existing_approval_gate.py`다. 후속 보안 delta를 ordinary descendant로 게시하고 hosted quality/review Checks와 qualifying independent approval을 새 exact head에서 확인한다. queued/skipped/pending은 acceptance evidence가 아니다. | +| CONTROL-NOEMA-DOCUMENT-LOCK-01 | **Proposed — hosted Security RED repaired and integrated locally; republish pending** | exact-head Security Scan run `36779214593`, Trivy job `110104871060`이 중앙 Noema document-reader lock의 `fast-uri` 3.1.7(CVE-2026-86472)과 `ip-address` 10.7.0(CVE-2026-101911, CVE-2026-101912)을 검출했다. 회귀는 vulnerable lock에서 1 failed / 11 passed / 2 skipped였고, `fast-uri` 3.1.8 및 `ip-address` 10.7.2로 재생성한 lock에서 12 passed / 2 skipped다. `npm audit --omit=dev --audit-level=moderate`는 109 production dependencies와 vulnerability 0을 보고했고 통합 full coverage도 100%다. | Canonical owner는 중앙 `scripts/ci/noema-document-reader/package-lock.json`이다. ordinary descendant로 #2536을 갱신한 뒤 fresh exact-head Security/quality/review Checks를 수집한다. | +| CONTROL-STRIX-LITELLM-LOCK-01 | **Proposed — hosted Python Security RED repaired and integrated locally; republish pending** | exact-head Python Security run `36779214017`, pip-audit job `110104825199`이 Strix hash lock의 LiteLLM 1.94.1에서 CVE-2026-84377을 검출했다. Source input에 fixed 1.94-line release `litellm==1.94.3`을 명시하고 기존 uv command/override로 hash lock을 재생성했다. 새 source/lock parity regression은 old lock에서 RED였다. strict exact-pin pip-audit는 known vulnerability 0이며 통합 full coverage도 100%다. | Canonical owner는 중앙 `requirements-strix-ci.txt`와 생성 `requirements-strix-ci-hashes.txt`다. ordinary descendant를 게시하고 fresh exact-head Python Security 및 review Checks를 수집한다. | + ### 2026-09-30 central coverage owner stack delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | @@ -3725,3 +3765,243 @@ verdict-shape acceptance, and qualifying independent approval. **Gap / failure scene.** The v2 handler names a run with `head/base/required-run/producer-source`, but its required-workflow fallback looked up only `head/base/required-run`. When authenticated status publication is unavailable, a completed clean handler job could not be found and a rerun ended false RED. Omitting the producer source would also allow a regenerated live merge revision to reuse predecessor evidence. **Action / evidence.** Correct the fallback lookup to include the live merge source and retain fail-closed base, head, required-run, workflow-path, job-name, GHAS-identity, and SARIF checks. The test-first repair reproduced two failures, then passed 96 focused workflow-contract tests; the new edge case rejects a stale merge-source title. Ruff E9/F/I on the changed dispatch-contract file and `git diff --check` pass. Fresh hosted Checks and a qualifying independent approval are still required on the unchanged executable delta before merge. + +## 2026-09-30 Repository Metadata Reconcile shallow-ancestry fixture + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +**Context Map / owner.** The central `.github` metadata-maintenance bounded +context owns its workflow and repository-wide evidence contracts. Git object +ancestry is local runner evidence; no product repository may fabricate or copy +that result. + +**Gap / RCA.** Exact-head run +[`36720930491`](https://github.com/ContextualWisdomLab/.github/actions/runs/36720930491), +job `109905558240`, checked out +`737fc6fd3b536495a7d5f8bbbae9d0474771d21f` at depth one. The full suite then +failed because documented G-17 evidence commit +`57477289ebec5631b0c48f0bc419f336dbe19deb` was absent from that shallow object +database. This was a workflow-fixture defect: the test deliberately proves +reachability with `git cat-file` and `git merge-base --is-ancestor`, while the +workflow supplied only the exact tip object. The earlier local full-history run +masked the hosted condition. + +**RED → GREEN / action.** A new contract first failed on the missing complete- +history input. Commit `3bc859c73ed67074df13b2e01aa89dff2159e260` +sets `fetch-depth: 0` only on the validation checkout; exact revision +verification, `persist-credentials: false`, apply credentials, and all gates +remain unchanged. The focused workflow plus G-17 ancestry suites pass 37 tests. +Publication must trigger fresh checks on the new exact head; predecessor GREEN +and queued/skipped/pending conclusions do not authorize merge. + +## 2026-09-30 Shared Python CI urllib3 security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks and qualifying independent +review. + +**Context Map / owner.** Central `.github` owns the pip-audit and Strix +hash-locked CI runtimes. urllib3 and its security advisories are upstream +evidence; product repositories consume only an ordinarily integrated central +workflow revision. + +**Gap / RCA.** Exact-head Python Security run +[`36737059681`](https://github.com/ContextualWisdomLab/.github/actions/runs/36737059681), +job `109961499214`, found urllib3 2.7.0 vulnerable to CVE-2026-97687, +CVE-2026-97688, and CVE-2026-97689 in both the pip-audit installer lock and +the Strix runtime lock. The upstream 2.8.0 release fixes the corresponding +HTTPS-proxy TLS-policy crossover and chunked-stream CPU/memory denial-of-service +issues. Because neither source input constrained urllib3, unrelated dependency +resolution could retain the vulnerable transitive version. + +**RED → repair.** The retained regression first failed because the two source +inputs had no urllib3 row. Both sources now require `urllib3==2.8.0`; both +generated locks carry the same exact version and PyPI artifact hashes. The +detailed evidence and APA 7th references are in +[`docs/doctoring/shared-security-baseline-urllib3-20260930.md`](doctoring/shared-security-baseline-urllib3-20260930.md). +Both exact-pin audits return no known vulnerabilities; the focused dependency +contract passes 5 tests and the warnings-as-errors full suite passes 5,236 +tests, 5 optional skips, and 40 subtests. No audit threshold, failure mode, or +workflow gate changes. Fresh exact-head Python Security and the remaining +applicable checks are mandatory; the failed predecessor and any skipped or +pending result are not acceptance evidence. + +## 2026-09-30 Shared Strix PyJWT recursion security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks and qualifying independent +review. + +**Context Map / owner.** Central `.github` owns the hash-locked Strix CI +runtime. PyJWT and its signed package artifacts are upstream evidence; product +repositories consume only an ordinarily integrated central workflow revision. + +**Gap / RCA.** Exact-head Security Scan run +[`36740858208`](https://github.com/ContextualWisdomLab/.github/actions/runs/36740858208), +job `109974634074`, found PyJWT 2.14.0 affected by GHSA-42vr-xj54-vc7v in +`requirements-strix-ci.txt`. The dependency-review support probe succeeded and +the run's Gitleaks, Scorecard, Trivy, and OSV jobs passed, isolating the actual +failure to PyJWT's unauthenticated nested-payload recursion path rather than a +permissions or network condition. + +**RED → repair.** The existing source/lock parity contract first failed with +the old 2.14.0 pin. Both surfaces now select PyJWT 2.15.1 with the signed PyPI +artifact hashes. Upstream documents the recursion hardening in 2.15.0; 2.15.1 +retains it and fixes Base64URL padding. Detailed evidence and APA 7th references +are in +[`docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md`](doctoring/shared-security-baseline-pyjwt-recursion-20260930.md). +No dependency-review threshold, fail-closed behavior, or workflow gate changes. +The focused dependency contract passes 5 tests; pip-audit 2.10.1's strict +exact-pin audit reports no known vulnerabilities; and the warnings-as-errors +repository suite passes 5,236 tests, 5 optional skips, and 40 subtests. Ruff and +`git diff --check` pass. Fresh exact-head hosted security evidence remains +mandatory. + +## 2026-10-01 OpenCode coverage approval-reuse evidence contract + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks, resolution of the actionable +review thread, and qualifying independent approval. + +**Context Map / owner.** Central `.github` owns the OpenCode coverage producer, +existing-approval gate, and repository-dispatch status publisher. Consumer +repositories receive only the released workflow contract and must not reinterpret +advisory job success as a passing coverage decision. + +**Gap / RCA.** CodeRabbit review thread `PRRT_kwDOS_C14s6nmf3Q` showed that two +approval-reuse paths checked only whether the coverage job concluded `success`; +a subsequent call-site audit found that the merge-scheduler approval gate did +not receive the summary at all. +The producer deliberately uses a successful job to publish an honest +`NOT MEASURED` diagnostic, so this result is necessary but not sufficient +approval evidence. A same-head approval could therefore be reused without a +current unique `PASS` decision. + +**RED → repair.** New regressions reject missing, `NOT MEASURED`, malformed, +and duplicate decisions. A shared validator and the workflow shell guard now +require exactly one `- Result: PASS` line before either existing-approval reuse +or success-status publication; the current summary is passed explicitly across +all approval consumers. The integrated focused suite passes 186 tests with 1 optional +LLVM-platform skip, including concurrent exact-head test commit +`87ffafa2f6b19080c01f6ee24b987b37cb92dcb8` and implementation commit +`0bcded6b08af4554541223438d046bc412c4b093`. Detailed evidence is in +[`docs/doctoring/opencode-coverage-approval-reuse-20261001.md`](doctoring/opencode-coverage-approval-reuse-20261001.md). +The warnings-as-errors repository suite passes 5,255 tests, 5 optional skips, +and 40 subtests. No threshold or required gate changed. Fresh exact-head hosted +evidence remains mandatory before integration. + +**Hosted follow-up.** Exact-head Trusted uv run +[`36751696675`](https://github.com/ContextualWisdomLab/.github/actions/runs/36751696675), +job `110011676248`, passed the tests but correctly failed the 100% repository +coverage gate because the new CLI rejection at +`scripts/ci/opencode_existing_approval_gate.py:241-242` had no direct caller. +The added regression executes a `NOT MEASURED` decision and verifies the +fail-closed diagnostic. No coverage exclusion or threshold reduction was used; +the exact hosted command now covers all 18,252 production statements and 7,498 +branches at 100% locally, with 5,255 passed, 5 optional skips, and 40 subtests. +Fresh exact-head hosted revalidation is required. + +## 2026-10-01 CodeQL Draft event materialization + +**Status:** Proposed owner repair; protected integration, hosted exact-head +evidence, and qualifying independent approval remain mandatory. + +**Context Map / owner.** `ContextualWisdomLab/.github` owns the central CodeQL +required-workflow and dispatch contract. `ContextualWisdomLab/Orgmetra` is a +consumer canary and does not copy or override the owner workflow. + +**Gap / RCA.** Unchanged-head Ready transitions on Orgmetra PRs #235, #259, +#448, and #100 produced no new central CodeQL run. Ruleset consumers receive +opened/synchronize/reopened launches but not `ready_for_review`; the CodeQL +entry job nevertheless skipped every Draft event and claimed Ready would +re-run the same head. At the same time, native-owner `converted_to_draft` +events must enter per-PR concurrency to retire stale work without starting a +replacement scan. A single blanket Draft predicate encoded incompatible +halves of that lifecycle. + +**Action / evidence.** The owner now distinguishes event and repository: +consumer Draft heads materialize CodeQL evidence, native-owner Draft heads +remain runner-free, and `converted_to_draft`/`closed` only retire stale work. +Authenticated status, exact head/base/source/run identity, GHAS, SARIF, and +terminal-verdict requirements are unchanged. The RED-first matrix produced +two intended failures; the focused Draft-control plus queue suite passes 86 +tests, and the warnings-fatal repository suite passes 5,261 tests, five +optional-platform skips, and 40 subtests. The residual OpenCode, Strix, and Noema unchanged-head Ready +materialization Gap remains open at the central review owner. Full exact-tree +and hosted evidence remain required and are not inferred from either +predecessor PR. + +## 2026-10-01 Central dispatch stale-run retirement + +**Status:** Proposed owner repair; local GREEN, protected integration and +hosted exact-head evidence pending. + +**Context Map / owner.** `ContextualWisdomLab/.github` owns central CodeQL, +OpenCode, Strix, and PR scheduler admission. Product repositories supply PR +identity and consume released workflow behavior; they do not cancel the +owner's receiver runs or copy its scheduler. + +**Gap / RCA.** A live `.github` Actions snapshot contained 457 queued and two +in-progress `repository_dispatch` runs. In the newest 100 queued runs, 38 +targeted superseded heads and five targeted closed PRs (CodeQL: 20 superseded, +three closed; OpenCode: 18 superseded, two closed). Workflow-level +`cancel-in-progress` could not retire them because Draft/close produced no new +central dispatch in the same group. The scheduler also returned on Draft before +calling stale-run cleanup, while closed PRs were absent from its open-PR scan. +Representative obsolete runs included `.github#2548` OpenCode `36842116744` +and CodeQL `36842027523`, both targeting `a9b20a…` after the live head moved to +`2583cc…`; closed examples included AppGuardrail #1365 CodeQL `36818449683`. + +**Action / evidence.** The owner now parses only exact protected CodeQL, +OpenCode, and Strix dispatch workflow paths and their repository/PR/head +run-name contracts. Before force-cancel it re-fetches the active run and PR; +the inventory and destructive-boundary validation both recognize all five +GitHub active states (`queued`, `in_progress`, `waiting`, `pending`, and +`requested`) so state transitions cannot evade cleanup. Then +only closed targets or head mismatches authorize cancellation. Current-head, +malformed, unrelated, and unreadable-authority cases fail closed. Consumer +repository tokens are not accepted as central Actions authority; cross-repo +cleanup requires an explicit organization token and otherwise fails closed. +Draft cleanup precedes Draft skip. Only `converted_to_draft` and `closed` transition events +are newly admitted to the bounded scheduler control job; closed cleanup returns +before review, branch, auto-merge, or merge behavior, and ordinary Draft events +still assign no runner. The RED-first tests reproduced all three missing paths; +all 458 focused scheduler and admission tests pass with warnings fatal. Required +checks, review admission, scanner verdicts, concurrency keys, and merge policy +are unchanged. Fresh hosted exact-head checks and independent review remain +mandatory. + +## 2026-10-01 CodeQL exact-active admission and pre-validation cancellation + +**Status:** Proposed owner repair; focused local GREEN, protected integration, +hosted exact-head evidence, and qualifying independent approval pending. + +**Context Map / owner.** `ContextualWisdomLab/.github` owns both the lightweight +required-workflow producer and the protected native CodeQL handler. Product +repositories consume the released contract and neither inspect central queues +nor manufacture admission receipts. + +**Gap / RCA.** Consumer `.github#2531@7900ba4c…` required run `36804208074` +created exact central run `36804251663`. It remained queued without a terminal +receipt. Attempt 2 therefore dispatched the identical title as run +`36815888197`; repository/PR workflow concurrency cancelled the first run two +seconds later and moved the same durable work to the back of the queue. A fresh +census found 406 queued `repository_dispatch` runs, 291 owned by the CodeQL +handler. The handler's shared concurrency key also acted before protected +actor and live PR identity validation, so arrival order—not validated +authority—could select the surviving run. + +**Action / evidence.** The coordinator now paginates handler runs no earlier +than the exact required run, then suppresses a new POST only for a trusted +`opencode-agent` run matching the protected workflow path, event, full +repo/PR/head/base/required-run/source title, and one of `queued`, +`in_progress`, `waiting`, `pending`, or `requested`. The check happens before +OIDC exchange. Inexact, untrusted, or terminal runs still permit recovery, +preserving the earlier attempt-2 startup-failure repair. The handler removes +unvalidated workflow-level cancellation; metadata validation and settlement +use the control pool while only the matrix scan uses the CodeQL pool. RED-first +contracts now pass with the surrounding CodeQL and queue suite (184 passed). +No terminal receipt, SARIF/GHAS proof, attempt ceiling, review gate, or merge +policy was weakened. This reduces self-inflicted churn; it does not claim to +increase the organization plan ceiling or accept queued/skipped evidence. diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index 7270540502..171878f750 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -1833,9 +1833,9 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich -pyjwt==2.15.0 \ - --hash=sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8 \ - --hash=sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4 +pyjwt==2.15.1 \ + --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ + --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 # via # -r requirements-strix-ci.txt # mcp diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 3a31a6c2c1..76a28b18b0 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,12 +1,12 @@ strix-agent==1.5.3 litellm==1.94.3 anyio==4.14.2 -pyjwt==2.15.0 +pyjwt==2.15.1 openai[httpx2]==2.54.0 -urllib3==2.8.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<8.0.0 cryptography==50.0.0 python-multipart==0.0.32 pyasn1==0.6.4 +urllib3==2.8.0 diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json index fe692cdf0e..2a0b8215b0 100644 --- a/scripts/ci/noema-document-reader/package-lock.json +++ b/scripts/ci/noema-document-reader/package-lock.json @@ -669,9 +669,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.1.tgz", - "integrity": "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/scripts/ci/noema-document-reader/package.json b/scripts/ci/noema-document-reader/package.json index 2408b5854b..aa4fc0d3ff 100644 --- a/scripts/ci/noema-document-reader/package.json +++ b/scripts/ci/noema-document-reader/package.json @@ -5,9 +5,5 @@ "dependencies": { "@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0" - }, - "overrides": { - "fast-uri": "3.1.8", - "ip-address": "10.7.1" } } diff --git a/scripts/ci/opencode_dispatch_status.py b/scripts/ci/opencode_dispatch_status.py index 25cbb75b7a..bbb2a26a51 100644 --- a/scripts/ci/opencode_dispatch_status.py +++ b/scripts/ci/opencode_dispatch_status.py @@ -11,11 +11,13 @@ try: from opencode_existing_approval_gate import ( OPENCODE_APP_APPROVAL_AUTHORS, + coverage_summary_rejection_reason, review_rejection_reason, ) except ModuleNotFoundError: # pragma: no cover - package import path from scripts.ci.opencode_existing_approval_gate import ( OPENCODE_APP_APPROVAL_AUTHORS, + coverage_summary_rejection_reason, review_rejection_reason, ) @@ -43,6 +45,7 @@ def decide_status( *, model_outcome: str, coverage_result: str, + coverage_summary: str, expected_head: str, pull_request: dict[str, Any], reviews: Sequence[dict[str, Any]], @@ -51,6 +54,8 @@ def decide_status( live_head = str((pull_request.get("head") or {}).get("sha") or "") if coverage_result != "success": reason = "OpenCode coverage evidence did not pass for the current head." + elif coverage_summary_rejection_reason(coverage_summary): + reason = "OpenCode coverage decision is missing, non-passing, or ambiguous." elif not expected_head or live_head.lower() != expected_head.lower(): reason = "OpenCode status target is stale or the live PR head is unavailable." elif not _has_current_approval(reviews, expected_head): @@ -71,6 +76,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--model-outcome", required=True) parser.add_argument("--coverage-result", required=True) + parser.add_argument("--coverage-summary", required=True) parser.add_argument("--expected-head", required=True) parser.add_argument("--pull-request-file", required=True, type=Path) parser.add_argument("--reviews-file", required=True, type=Path) @@ -89,6 +95,7 @@ def main(argv: Sequence[str] | None = None) -> int: decide_status( model_outcome=args.model_outcome, coverage_result=args.coverage_result, + coverage_summary=args.coverage_summary, expected_head=args.expected_head, pull_request=pull_request, reviews=reviews, diff --git a/scripts/ci/opencode_existing_approval_gate.py b/scripts/ci/opencode_existing_approval_gate.py index 6712c02282..075716a048 100644 --- a/scripts/ci/opencode_existing_approval_gate.py +++ b/scripts/ci/opencode_existing_approval_gate.py @@ -5,6 +5,7 @@ import argparse import json +import os import re import sys from typing import Any, TextIO @@ -46,6 +47,23 @@ ) +def coverage_summary_rejection_reason(summary: str) -> str | None: + """Explain why a coverage summary cannot authorize approval reuse.""" + decisions = [line for line in summary.splitlines() if line.startswith("- Result:")] + if not decisions: + return "coverage decision is missing" + if len(decisions) != 1: + return "coverage decision is duplicated or contradictory" + if decisions[0] != "- Result: PASS": + return "coverage decision is not PASS" + return None + + +def coverage_decision_is_pass(summary: str) -> bool: + """Return whether a summary has one authoritative PASS decision.""" + return coverage_summary_rejection_reason(summary) is None + + def flatten_reviews(document: object) -> list[dict[str, Any]]: """Flatten REST pagination output while rejecting malformed review entries.""" if not isinstance(document, list): @@ -156,13 +174,12 @@ def has_reusable_real_model_approval( log: TextIO, approval_authors: frozenset[str] = APPROVAL_AUTHORS, ) -> bool: - """Return whether reviews contain a real-model approval for the exact head.""" + """Return whether the latest exact-head OpenCode decision is reusable.""" candidate_count = 0 for review in reversed(reviews): - state = str(review.get("state") or "").upper() commit_id = str(review.get("commit_id") or "") login = str((review.get("user") or {}).get("login") or "") - if state != "APPROVED" or commit_id.lower() != head_sha.lower(): + if commit_id.lower() != head_sha.lower(): continue if login not in KNOWN_PUBLICATION_ACTORS: continue @@ -182,9 +199,11 @@ def has_reusable_real_model_approval( return True print( "existing-approval gate rejected same-head review " - f"id={review_id} author={login}: {reason}", + f"id={review_id} author={login}; latest same-head review is authoritative: " + f"{reason}", file=log, ) + break print( "existing-approval gate found no reusable real-model approval " @@ -198,6 +217,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace: """Parse existing-approval gate command-line arguments.""" parser = argparse.ArgumentParser() parser.add_argument("--head", required=True) + parser.add_argument( + "--coverage-summary", + default=os.environ.get("COVERAGE_EVIDENCE_SUMMARY", ""), + ) parser.add_argument( "--require-opencode-app", action="store_true", @@ -214,6 +237,10 @@ def main(argv: list[str]) -> int: "existing-approval gate requires a 40-character head SHA", file=sys.stderr ) return 2 + coverage_error = coverage_summary_rejection_reason(args.coverage_summary) + if coverage_error: + print(f"existing-approval gate rejected evidence: {coverage_error}", file=sys.stderr) + return 1 try: reviews = flatten_reviews(json.load(sys.stdin)) except (json.JSONDecodeError, ValueError) as exc: diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 5a86bd24c8..12b4a60957 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -70,6 +70,34 @@ def admit(self, component: str, repository: str, pr: dict[str, Any]) -> bool: def lease(state): """Apply this request to `state` and record any lease it wins.""" + prior_record = state.records.get(request.identity) + if ( + component == "opencode" + and not bool(pr.get("isDraft")) + and prior_record is not None + and has_current_head_draft_review_completion(pr) + and not has_current_head_approval(pr) + and not has_current_head_changes_requested(pr) + and ( + prior_record.status == "complete" + or ( + prior_record.status == "dispatched" + and opencode_progress_state( + pr, stale_after_minutes=DEFAULT_STALE_OPENCODE_MINUTES + ) + != "running" + ) + ) + ): + # The durable identity intentionally omits Draft/Ready mode. + # Retire only a terminal prior lease at the moment a Ready + # dispatch asks for admission; an in-flight Ready lease keeps + # its bounded worker slot on subsequent reconciliations. + updated_records = dict(state.records) + updated_records[request.identity] = RequestRecord( + prior_record.request, "stale" + ) + state = type(state)(updated_records, dict(state.latest_sequences)) plan = plan_dispatches( state, [request], @@ -103,7 +131,14 @@ def reconcile_state(state): continue terminal = ( record.request.component == "opencode" - and (has_current_head_approval(pr) or has_current_head_changes_requested(pr)) + and ( + has_current_head_approval(pr) + or has_current_head_changes_requested(pr) + or ( + bool(pr.get("isDraft")) + and has_current_head_draft_review_completion(pr) + ) + ) ) or ( record.request.component == "strix" and strix_evidence_state(pr) == "complete" @@ -178,6 +213,7 @@ def live_dispatch_head_matches(repo: str, pr: dict[str, Any]) -> bool: PULL_REQUEST_FIELDS_FRAGMENT = """\ fragment SchedulerPullRequestFields on PullRequest { number + state title author { login } isDraft @@ -353,8 +389,16 @@ def live_dispatch_head_matches(repo: str, pr: dict[str, Any]) -> bool: "OpenCode Review Dispatch", } OPENCODE_REVIEW_WORKFLOW_PATH = ".github/workflows/opencode-review.yml" +CENTRAL_DISPATCH_WORKFLOW_PATHS = { + "CodeQL Scan Dispatch": ".github/workflows/codeql-scan-dispatch.yml", + "OpenCode Review": ".github/workflows/opencode-review-dispatch.yml", + "OpenCode Review Dispatch": ".github/workflows/opencode-review-dispatch.yml", + "Required OpenCode Review": ".github/workflows/opencode-review-dispatch.yml", + "Strix Security Scan": ".github/workflows/strix.yml", +} REST_UNKNOWN_GITHUB_ACTIONS_WORKFLOW = "__unknown_github_actions_workflow__" RUNNING_CHECK_STATES = {"PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"} +ACTIVE_WORKFLOW_RUN_STATUSES = ("queued", "in_progress", "waiting", "pending", "requested") FAILED_CHECK_CONCLUSIONS = {"FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE"} ACTION_REQUIRED_CONCLUSIONS = {"ACTION_REQUIRED"} GIT_REF_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") @@ -2296,6 +2340,28 @@ def has_current_head_changes_requested(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "CHANGES_REQUESTED") +def has_current_head_draft_review_completion(pr: dict[str, Any]) -> bool: + """Return whether OpenCode completed an exact-head Draft review-only request. + + A clean Draft cannot receive merge approval authority. The central reviewer + therefore publishes a formal COMMENTED review carrying an explicit result + marker and explanatory sentence. Both markers are required so an ordinary + status comment cannot suppress a later explicit Draft review request. + """ + for review in reversed((pr.get("reviews") or {}).get("nodes") or []): + if not is_opencode_review(review) or not review_matches_current_head(review, pr): + continue + body = review.get("body") or "" + if ( + (review.get("state") or "").upper() == "COMMENTED" + and "- Result: DRAFT_REVIEW_COMPLETE" in body + and "Draft review-only request completed without publishing merge approval authority." + in body + ): + return True + return False + + def latest_current_head_coverage_change_request( pr: dict[str, Any], ) -> dict[str, Any] | None: @@ -3121,6 +3187,13 @@ def require_github_actions_control_actor(action: str) -> None: ) +def github_actions_control_available() -> bool: + """Return whether this process has the bounded Actions-control authority.""" + return os.environ.get("GITHUB_ACTIONS") == "true" and bool( + os.environ.get("SCHEDULER_ACTIONS_TOKEN") + ) + + def rerun_actions_job(repo: str, job_id: str, *, dry_run: bool, action: str) -> None: """Ask GitHub Actions to rerun an existing required-workflow job.""" if dry_run: @@ -3584,10 +3657,10 @@ def _fresh_open_pr_for_cancellation(repo: str, number: int) -> dict[str, Any]: def _fresh_active_run_for_cancellation(run_repo: str, run_id: str) -> dict[str, Any]: """Return fresh active workflow-run evidence immediately before cancellation.""" payload = gh_api_json(f"repos/{run_repo}/actions/runs/{run_id}") - if not isinstance(payload, dict) or str(payload.get("status") or "").lower() not in { - "queued", - "in_progress", - }: + if ( + not isinstance(payload, dict) + or str(payload.get("status") or "").lower() not in ACTIVE_WORKFLOW_RUN_STATUSES + ): raise ValueError(f"workflow run {run_repo}#{run_id} is not active") return payload @@ -3710,6 +3783,122 @@ def cancel_one(run_ref: tuple[str, str]) -> str | None: return [run_id for run_id in results if run_id is not None] +def central_dispatch_run_target( + run_data: dict[str, Any], target_repo: str +) -> tuple[int, str] | None: + """Return the trusted target PR and head encoded by a central dispatch run. + + GitHub executes ``repository_dispatch`` on the receiver's default branch, + so ``head_sha`` identifies that branch rather than the target pull request. + The protected central workflows instead place the validated target identity + in ``run-name``. Only exact workflow paths and their declared title shapes + are accepted here; malformed or unrelated runs remain untouched. + """ + if run_data.get("event") != "repository_dispatch": + return None + run_path = str(run_data.get("path") or "") + display_title = str(run_data.get("display_title") or "") + for title, expected_path in CENTRAL_DISPATCH_WORKFLOW_PATHS.items(): + if run_path != expected_path: + continue + prefix = f"{title} {target_repo}#" + if not display_title.startswith(prefix): + continue + identity = display_title.removeprefix(prefix) + match = re.fullmatch(r"([1-9][0-9]*)@([0-9a-fA-F]{40})(/[^\s]+)?", identity) + if match is None: + return None + suffix = match.group(3) + if title == "CodeQL Scan Dispatch": + if suffix is None: + return None + elif suffix is not None: + return None + return int(match.group(1)), validate_git_sha(match.group(2)).lower() + return None + + +def _central_dispatch_run_still_stale( + target_repo: str, + run_repo: str, + run_id: str, + expected_number: int, +) -> bool: + """Return whether a central run is stale or targets a now-closed PR. + + Both the run and pull request are re-fetched immediately before the + destructive boundary. Any missing or contradictory authority fails + closed and preserves the run. + """ + try: + run_data = _fresh_active_run_for_cancellation(run_repo, run_id) + identity = central_dispatch_run_target(run_data, target_repo) + if identity is None or identity[0] != expected_number: + raise ValueError("central dispatch run no longer has the expected trusted identity") + number, dispatched_head = identity + live_pr = gh_api_json(f"repos/{target_repo}/pulls/{number}") + if not isinstance(live_pr, dict): + raise TypeError("pull request authority is not an object") + state = str(live_pr.get("state") or "").lower() + if state == "closed": + return True + if state != "open": + raise ValueError(f"pull request state {state!r} is not authoritative") + live_head = validate_git_sha( + str(((live_pr.get("head") or {}).get("sha")) or "") + ).lower() + except (KeyError, RuntimeError, TypeError, ValueError) as exc: + print( + f"::warning::Preserving central dispatch run {run_repo}#{run_id}: " + f"live central-run revalidation failed closed ({exc})." + ) + return False + return dispatched_head != live_head + + +def cancel_stale_central_dispatch_runs( + repo: str, + *, + pr: dict[str, Any], + dry_run: bool, +) -> list[str]: + """Cancel trusted central dispatches for one stale or closed pull request.""" + if dry_run: + return [] + target_repo = validate_github_repository(repo) + selected_number = int(pr["number"]) + run_repo = repository_dispatch_target(target_repo) + candidates: list[tuple[str, int]] = [] + for run_data in active_workflow_runs( + run_repo, + ACTIVE_WORKFLOW_RUN_STATUSES, + event="repository_dispatch", + ): + identity = central_dispatch_run_target(run_data, target_repo) + run_id = run_data.get("id") + if identity is None or not run_id: + continue + number, _ = identity + if number != selected_number: + continue + candidates.append((str(run_id), number)) + + if not candidates: + return [] + require_github_actions_control_actor("force-cancel-stale-central-dispatch-runs") + + cancelled: list[str] = [] + for run_id, number in candidates: + if not _central_dispatch_run_still_stale( + target_repo, run_repo, run_id, number + ): + continue + failures = force_cancel_workflow_runs(run_repo, [run_id]) + if run_id not in failures: + cancelled.append(run_id) + return cancelled + + def discover_opencode_required_run_id(repo: str, head_sha: str) -> int | None: @@ -3862,6 +4051,7 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr "pr_base_sha": base_sha, "pr_head_ref": head_ref, "pr_head_sha": head_sha, + "draft_review_only": bool(pr.get("isDraft")), } complete_paginated_pr_contexts(target_repo, pr) required_run_id = matching_actions_run_id(pr, is_opencode_check_run) @@ -4233,7 +4423,11 @@ def dispatch_draft_review_only( # alone as a verdict would make a failed dispatch attempt permanently # block every later explicit retry. Only an actual current-head formal # review is a verdict. - if has_current_head_approval(pr) or has_current_head_changes_requested(pr): + if ( + has_current_head_approval(pr) + or has_current_head_changes_requested(pr) + or has_current_head_draft_review_completion(pr) + ): return Decision( number, "skip", @@ -4334,6 +4528,22 @@ def inspect_pr( """Decide and optionally act on one pull request's merge-readiness state.""" number = pr["number"] base_ref = pr.get("baseRefName") + pr_state = str(pr.get("state") or "OPEN").upper() + + # Cleanup is independent of Ready/Draft admission. In particular, a Draft + # transition must retire superseded direct and central runs before the + # ordinary Draft skip below; otherwise those runs keep scarce runners until + # they eventually start and discover that their target head is obsolete. + # Local invocations without the scheduler's bounded Actions credential keep + # their historical read/merge behavior and never attempt control-plane + # cancellation. + if pr_state != "OPEN": + if github_actions_control_available(): + cancel_stale_central_dispatch_runs(repo, pr=pr, dry_run=dry_run) + return Decision(number, "skip", f"{pr_state.lower()} PR") + if pr.get("isDraft") and github_actions_control_available(): + cancel_stale_central_dispatch_runs(repo, pr=pr, dry_run=dry_run) + cancel_stale_pr_runs(repo, pr, dry_run=dry_run) recovered_startup_runs = ( recover_current_head_startup_failures(repo, pr, dry_run=False) diff --git a/scripts/ci/reconcile_sbom_publication_lineage.sh b/scripts/ci/reconcile_sbom_publication_lineage.sh new file mode 100755 index 0000000000..7305c49b78 --- /dev/null +++ b/scripts/ci/reconcile_sbom_publication_lineage.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "usage: $0 PREVIOUS_HEAD GENERATED_INVENTORY_HEAD" >&2 + exit 64 +fi + +previous_head="$1" +generated_inventory_head="$2" +inventory_paths=( + docs/sbom/inventory.json + docs/sbom/inventory.md +) + +if [ "$(git rev-parse HEAD)" != "$generated_inventory_head" ]; then + echo "generated inventory head is not the checked-out head" >&2 + exit 1 +fi + +git cat-file -e "${previous_head}^{commit}" +git cat-file -e "${generated_inventory_head}^{commit}" + +unexpected_worktree_status="$( + git status --porcelain=v1 --untracked-files=all -- \ + . \ + ':(exclude)docs/sbom/inventory.json' \ + ':(exclude)docs/sbom/inventory.md' +)" +if [ -n "$unexpected_worktree_status" ]; then + echo "working tree contains non-inventory change:" >&2 + echo "$unexpected_worktree_status" >&2 + exit 1 +fi + +generated_inventory_parent="$(git rev-parse "${generated_inventory_head}^")" +mapfile -d '' -t generated_change_paths < <( + git diff --name-only -z "$generated_inventory_parent" "$generated_inventory_head" +) + +for generated_change_path in "${generated_change_paths[@]}"; do + case "$generated_change_path" in + docs/sbom/inventory.json|docs/sbom/inventory.md) ;; + *) + echo "generated inventory head contains non-inventory change: $generated_change_path" >&2 + exit 1 + ;; + esac +done + +if git merge-base --is-ancestor "$previous_head" HEAD; then + exit 0 +fi + +lineage_base="$(git merge-base "$previous_head" "$generated_inventory_head")" +mapfile -d '' -t prior_change_paths < <( + git diff --name-only -z "$lineage_base" "$previous_head" +) + +for prior_change_path in "${prior_change_paths[@]}"; do + case "$prior_change_path" in + docs/sbom/inventory.json|docs/sbom/inventory.md) ;; + *) + echo "prior publication head contains non-inventory change: $prior_change_path" >&2 + exit 1 + ;; + esac +done + +# A final-tree diff is insufficient here: an untrusted publication branch can +# change an owner file and revert it before its head, leaving no final delta +# while still making both commits ancestors of the next publication. Inspect +# each reachable commit against its first parent so merge-resolution changes +# and hidden change/revert pairs cannot cross the inventory-only boundary. +mapfile -t prior_history_commits < <( + git rev-list "${lineage_base}..${previous_head}" +) + +for prior_history_commit in "${prior_history_commits[@]}"; do + prior_history_parent="$(git rev-parse "${prior_history_commit}^1")" + mapfile -d '' -t prior_history_change_paths < <( + git diff --name-only -z "$prior_history_parent" "$prior_history_commit" + ) + for prior_history_change_path in "${prior_history_change_paths[@]}"; do + case "$prior_history_change_path" in + docs/sbom/inventory.json|docs/sbom/inventory.md) ;; + *) + echo \ + "prior publication history contains non-inventory change: $prior_history_change_path" \ + >&2 + exit 1 + ;; + esac + done +done + +merge_completed=true +if ! git merge \ + --no-commit \ + --no-ff \ + "$previous_head"; then + merge_completed=false +fi + +mapfile -d '' -t conflict_paths < <( + git diff --name-only --diff-filter=U -z +) + +if [ "$merge_completed" = false ] && [ "${#conflict_paths[@]}" -eq 0 ]; then + git merge --abort || true + echo "publication lineage merge failed without resolvable inventory conflicts" >&2 + exit 1 +fi + +for conflict_path in "${conflict_paths[@]}"; do + case "$conflict_path" in + docs/sbom/inventory.json|docs/sbom/inventory.md) ;; + *) + git merge --abort || true + echo "non-inventory conflict: $conflict_path" >&2 + exit 1 + ;; + esac +done + +git restore \ + --source="$generated_inventory_head" \ + --staged \ + --worktree \ + -- \ + "${inventory_paths[@]}" + +if [ -n "$(git diff --name-only --diff-filter=U)" ]; then + git merge --abort || true + echo "publication lineage merge retained unresolved conflicts" >&2 + exit 1 +fi + +git diff --check +git commit -m "chore: preserve SBOM inventory publication lineage" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e53741bd51..e3d4ce826a 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -587,11 +587,12 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { record_failure "opencode required workflow bootstrap condition detection must survive a job block larger than the pipe buffer" fi rm -f "$large_bootstrap_fixture" - assert_file_contains "$workflow_file" 'needs.validate-pr-metadata.outputs.target_repository' "opencode review scopes concurrency by the live validated target repository" - assert_file_contains "$workflow_file" 'needs.validate-pr-metadata.outputs.pr_number || github.run_id' "opencode review scopes concurrency by the live validated PR with a non-PR fallback" + assert_file_contains "$workflow_file" 'admit-exact-head-dispatch:' "opencode review admits one exact-head receiver before expensive work" + assert_file_contains "$workflow_file" 'admitted: ${{ steps.single_flight.outputs.admitted }}' "opencode review exports its atomic exact-head lease decision" + assert_file_contains "$workflow_file" "needs.admit-exact-head-dispatch.outputs.admitted == 'true'" "opencode review gates expensive work on the exact-head lease" + assert_file_contains "$workflow_file" 'opencode-dispatch-leases' "opencode review uses a durable exact-head lease instead of lossy native concurrency" + assert_file_contains "$workflow_file" "Wake every failed exact-head Required OpenCode workflow" "opencode review preserves distinct same-head admissions for deterministic receipt recovery" assert_file_not_contains "$workflow_file" "format('pr-{0}-{1}'" "opencode review does not keep stale head-specific concurrency groups" - assert_file_contains "$workflow_file" 'opencode-review-${{' "opencode review uses the workflow-repository-PR group prefix" - assert_file_contains "$workflow_file" 'cancel-in-progress: true' "opencode review cancels stale in-progress review attempts when a newer PR event arrives" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "opencode pull_request coverage execution materializes the exact base/head merge tree" assert_file_contains "$workflow_file" "stale OpenCode run: event head=" "opencode review side effects are skipped for stale heads" assert_file_not_contains "$workflow_file" "github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name" "opencode never treats a same-repository pull_request_target head as authorization to execute PR-controlled code" @@ -614,7 +615,18 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" "actions: read" "opencode review workflow can read failed Actions logs without Actions write scope" assert_file_contains "$workflow_file" "checks: read" "opencode review workflow can read failed check-run annotations for line-specific findings" assert_file_contains "$workflow_file" "contents: read" "opencode review workflow uses read-only repository contents permission" - assert_file_not_contains "$workflow_file" "contents: write" "opencode review workflow does not need repository contents write scope" + local admission_job validation_job + admission_job="$(awk '/^ admit-exact-head-dispatch:$/ { emit=1 } emit && /^ [A-Za-z0-9_-]+:$/ && $0 !~ /^ admit-exact-head-dispatch:$/ { exit } emit { print }' "$workflow_file")" + validation_job="$(awk '/^ validate-pr-metadata:$/ { emit=1 } emit && /^ [A-Za-z0-9_-]+:$/ && $0 !~ /^ validate-pr-metadata:$/ { exit } emit { print }' "$workflow_file")" + if ! grep -Fq -- "contents: write" <<<"$admission_job"; then + record_failure "opencode review scopes repository contents write permission to the atomic lease job" + fi + if grep -Fq -- "contents: write" <<<"$validation_job"; then + record_failure "opencode review metadata validation must not receive repository contents write permission" + fi + if ! grep -Fq -- "contents: read" <<<"$validation_job"; then + record_failure "opencode review metadata validation keeps read-only repository contents permission" + fi assert_file_contains "$workflow_file" "pull-requests: write" "opencode review workflow may use github-actions[bot] for same-repository review-thread, update-branch, auto-merge, and merge follow-up" assert_file_contains "$workflow_file" "issues: write" "opencode review workflow can publish or update overview comments through the job token" assert_file_contains "$workflow_file" "statuses: write" "opencode review workflow can read status contexts and publish the repository_dispatch status evidence it owns" @@ -1593,7 +1605,10 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates repository-local recovery from PR runs" assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != ''" "scheduler scopes targeted manual queue scans to the requested PR" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "queue: max" "scheduler preserves distinct same-head admissions up to the documented pending limit" + assert_file_not_contains "$workflow_file" "cancel-in-progress:" "scheduler avoids native pending-run replacement" + assert_file_contains "$workflow_file" "cancel-superseded-pr-runs:" "scheduler retires only revalidated predecessor-head runs" + assert_file_contains "$workflow_file" "Cancel revalidated predecessor scheduler runs" "scheduler keeps predecessor cleanup metadata-only and explicit" assert_file_not_contains "$workflow_file" 'github.event.workflow_run' "scheduler does not poll required-check completion through follow-up workflow runs" assert_file_contains "$workflow_file" "github.event.client_payload.trigger_reviews != false" "scheduler enables review dispatch by default for default-branch dispatch events" assert_file_contains "$workflow_file" "github.event_name == 'schedule' || github.event_name == 'push'" "scheduler can dispatch a bounded OpenCode review from native or recovery events" diff --git a/scripts/ci/verify_release_maturin_tool_assets.py b/scripts/ci/verify_release_maturin_tool_assets.py index a59507ac83..2aa9767d8a 100644 --- a/scripts/ci/verify_release_maturin_tool_assets.py +++ b/scripts/ci/verify_release_maturin_tool_assets.py @@ -120,6 +120,7 @@ def _download(filename: str) -> bytes: def _binary(raw: bytes, filename: str) -> bytes: + """Extract the single bounded Maturin executable from its reviewed archive.""" if filename.endswith(".zip"): with zipfile.ZipFile(io.BytesIO(raw)) as archive: members = archive.infolist() @@ -168,6 +169,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: def main() -> None: + """Verify reviewed Maturin assets from the network or an explicit local root.""" parser = argparse.ArgumentParser() parser.add_argument("--asset-root", type=Path) args = parser.parse_args() diff --git a/tests/test_actions_queue_health_contract.py b/tests/test_actions_queue_health_contract.py index 4b2a49a58f..6935fa2d7a 100644 --- a/tests/test_actions_queue_health_contract.py +++ b/tests/test_actions_queue_health_contract.py @@ -24,9 +24,19 @@ def test_queue_health_workflow_is_scheduled_read_only_and_pinned() -> None: collect_permissions = workflow.split(" collect:\n", 1)[1].split( " permissions:\n", 1 )[1].split(" steps:\n", 1)[0] - assert collect_permissions == " contents: read\n actions: read\n" + assert collect_permissions == ( + " contents: read\n" + " actions: read\n" + " id-token: write\n" + ) + assert "Exchange OpenCode app token for cross-repo reads" in workflow + assert "id: queue_read_app_token" in workflow + assert "OIDC_AUDIENCE: opencode-github-action" in workflow + assert "audience=${OIDC_AUDIENCE}" in workflow + assert "/exchange_github_app_token" in workflow assert ( - "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }}" + "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN " + "|| steps.queue_read_app_token.outputs.token }}" in workflow ) assert "GH_TOKEN: ${{ github.token }}" not in workflow diff --git a/tests/test_agent_review_runtime_quality_consolidation.py b/tests/test_agent_review_runtime_quality_consolidation.py index 47884f9068..40efcc39d2 100644 --- a/tests/test_agent_review_runtime_quality_consolidation.py +++ b/tests/test_agent_review_runtime_quality_consolidation.py @@ -90,6 +90,16 @@ def test_changelog_only_edits_do_not_boot_the_consolidated_runner() -> None: assert ' - "CHANGELOG.md"' not in trigger +def test_runtime_quality_admits_stacked_pull_requests() -> None: + """A canonical owner base must not suppress exact-head quality evidence.""" + + pull_request_trigger = _workflow_text().split(" pull_request:\n", 1)[1].split( + "\nconcurrency:\n", 1 + )[0] + + assert "branches:" not in pull_request_trigger + + def test_consolidated_workflow_preserves_all_contract_suites() -> None: """Keep the retired Noema, OpenCode, and Strix evidence in one job.""" @@ -128,8 +138,8 @@ def test_consolidated_workflow_preserves_all_contract_suites() -> None: assert required_path in workflow -def test_exact_head_is_verified_before_selected_suites_run() -> None: - """Reject a checkout that differs from the pull request's current head.""" +def test_exact_head_uses_live_base_merge_base_for_changed_paths() -> None: + """Ignore stale event base SHAs while preserving exact-head selection.""" workflow = _workflow_text() selector = workflow.split( @@ -137,7 +147,58 @@ def test_exact_head_is_verified_before_selected_suites_run() -> None: )[1].split("- name: Install exact hash-verified base dependencies", 1)[0] assert 'test "$(git rev-parse HEAD)" = "$HEAD_SHA"' in selector - assert 'git diff --name-only "$BASE_SHA...$HEAD_SHA"' in selector + assert "BASE_REF: ${{ github.event.pull_request.base.ref }}" in selector + assert ( + 'change_base_sha="$(git merge-base ' + '\"refs/remotes/origin/$BASE_REF\" \"$HEAD_SHA\")"' + in selector + ) + assert 'git diff --name-only "$change_base_sha...$HEAD_SHA"' in selector + assert "github.event.pull_request.base.sha" not in selector + + +def test_whitespace_gate_uses_live_base_merge_base() -> None: + """Check only the current PR delta when an old event base is stale.""" + + workflow = _workflow_text() + self_test_step = workflow.split( + "- name: Verify consolidated workflow contract", 1 + )[1] + + assert "BASE_REF: ${{ github.event.pull_request.base.ref }}" in self_test_step + assert "HEAD_SHA: ${{ github.event.pull_request.head.sha }}" in self_test_step + assert ( + 'change_base_sha="$(git merge-base ' + '\"refs/remotes/origin/$BASE_REF\" \"$HEAD_SHA\")"' + in self_test_step + ) + assert 'git diff --check "$change_base_sha...$HEAD_SHA"' in self_test_step + assert "github.event.pull_request.base.sha" not in self_test_step + + +def test_live_base_is_refetched_before_each_merge_base_decision() -> None: + """Prevent a base advance during the job from reviving stale diff evidence.""" + + workflow = _workflow_text() + live_base_fetch = ( + 'git fetch --no-tags --prune origin ' + '"refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF"' + ) + merge_base = ( + 'change_base_sha="$(git merge-base ' + '\"refs/remotes/origin/$BASE_REF\" \"$HEAD_SHA\")"' + ) + + assert workflow.count(live_base_fetch) == 2 + for workflow_section in ( + workflow.split("- name: Select affected contract suites", 1)[1].split( + "- name: Install exact hash-verified base dependencies", 1 + )[0], + workflow.split("- name: Verify consolidated workflow contract", 1)[1], + ): + assert workflow_section.index(live_base_fetch) < workflow_section.index( + merge_base + ) def test_review_repair_suite_is_selected_and_conditionally_executed() -> None: diff --git a/tests/test_close_empty_pr_queue_pressure.py b/tests/test_close_empty_pr_queue_pressure.py index 6da88f63f1..972adffe22 100644 --- a/tests/test_close_empty_pr_queue_pressure.py +++ b/tests/test_close_empty_pr_queue_pressure.py @@ -5,7 +5,6 @@ import pytest - WORKFLOWS = Path(__file__).parents[1] / ".github/workflows" @@ -23,14 +22,26 @@ def test_closed_pull_request_does_not_allocate_a_noop_runner( filename: str, evidence_job: str, ) -> None: - """PR-stable concurrency retires close work without a no-op runner.""" + """Close work takes no evidence runner; scheduler may take its cleanup runner.""" workflow = (WORKFLOWS / filename).read_text(encoding="utf-8") concurrency = workflow.split("concurrency:", 1)[1].split("permissions:", 1)[0] assert "closed" in workflow assert "github.event.pull_request.number" in concurrency - assert "github.event.pull_request.head.sha" not in concurrency - assert re.search(r"(?m)^[ \t]+cancel-in-progress:[ \t]+\S", concurrency) - assert "cancel-closed-pr-runs:" not in workflow - assert "github.event.action != 'closed'" in workflow + if filename == "pr-review-merge-scheduler.yml": + assert "github.event.pull_request.head.sha" in concurrency + assert "queue: max" in concurrency + assert "cancel-in-progress:" not in concurrency + assert "cancel-superseded-pr-runs:" in workflow + cleanup = workflow.split(" cancel-superseded-pr-runs:", 1)[1].split( + " scan-pr-queue:", 1 + )[0] + assert "actions: write" in cleanup + assert "actions/checkout" not in cleanup + assert "github.event.action == 'closed'" in workflow + else: + assert "github.event.pull_request.head.sha" not in concurrency + assert re.search(r"(?m)^[ \t]+cancel-in-progress:[ \t]+\S", concurrency) + assert "cancel-closed-pr-runs:" not in workflow + assert "github.event.action != 'closed'" in workflow assert evidence_job in workflow diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 82569e89c7..36203f3ef2 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -253,6 +253,29 @@ def _completed_dispatch_run( } +def _active_dispatch_run( + *, + title: str, + status: str = "queued", + actor: str = "opencode-agent[bot]", + triggering_actor: str | None = None, + event: str = "repository_dispatch", + path: str = ".github/workflows/codeql-scan-dispatch.yml", + run_id: int = 36804251663, +) -> dict: + """Return one trusted active central CodeQL dispatch workflow-run fixture.""" + return { + "id": run_id, + "event": event, + "path": path, + "status": status, + "display_title": title, + "name": title, + "actor": {"login": actor}, + "triggering_actor": {"login": triggering_actor or actor}, + } + + def _run_verdict_read( tmp_path: Path, statuses: list[dict], @@ -833,6 +856,7 @@ def _write_coordinator_fakes( pull: dict, jobs: dict, statuses: list[dict], + dispatch_runs: dict, ) -> tuple[Path, Path, Path]: """Install fake gh/curl binaries and return (bin, post_log, post_body).""" fake_bin = tmp_path / "bin" @@ -868,9 +892,11 @@ def _write_coordinator_fakes( " */pulls/*) body=$FAKE_PULL_JSON ;;\n" " */statuses) body=$FAKE_STATUSES_JSON ;;\n" " */actions/runs/*/jobs) body=$FAKE_JOBS_JSON ;;\n" + " */actions/runs/*) body='{\"created_at\":\"2026-10-01T02:05:00Z\"}' ;;\n" + " */codeql-scan-dispatch.yml/runs) body=$FAKE_DISPATCH_RUNS_JSON ;;\n" " *) exit 1 ;;\n" "esac\n" - 'if [ -n "${jq_filter}" ]; then printf \'%s\\n\' "$body" | jq -c "$jq_filter"; else printf \'%s\\n\' "$body"; fi\n', + 'if [ -n "${jq_filter}" ]; then printf \'%s\\n\' "$body" | jq -r "$jq_filter"; else printf \'%s\\n\' "$body"; fi\n', encoding="utf-8", ) fake_gh.chmod(0o755) @@ -899,6 +925,7 @@ def _run_coordinator( pull: dict | None = None, jobs: dict | None = None, statuses: list[dict] | None = None, + dispatch_runs: dict | None = None, env_overrides: dict[str, str] | None = None, ) -> tuple[subprocess.CompletedProcess[str], Path, Path]: """Execute the coordinator dispatch block against fixture-backed APIs.""" @@ -932,7 +959,11 @@ def _run_coordinator( } statuses = statuses if statuses is not None else [] fake_bin, post_log, post_body = _write_coordinator_fakes( - tmp_path, pull=pull, jobs=jobs, statuses=statuses + tmp_path, + pull=pull, + jobs=jobs, + statuses=statuses, + dispatch_runs=dispatch_runs or {"workflow_runs": []}, ) script = _extract_run_block( WORKFLOW_PATH.read_text(encoding="utf-8"), COORDINATOR_STEP_NAME @@ -943,6 +974,9 @@ def _run_coordinator( "FAKE_PULL_JSON": json.dumps(pull), "FAKE_JOBS_JSON": json.dumps(jobs), "FAKE_STATUSES_JSON": json.dumps(statuses), + "FAKE_DISPATCH_RUNS_JSON": json.dumps( + dispatch_runs or {"workflow_runs": []} + ), "FAKE_POST_LOG": str(post_log), "FAKE_POST_BODY": str(post_body), "FAKE_CURL_LOG": str(tmp_path / "curl.log"), @@ -1022,6 +1056,77 @@ def test_codeql_coordinator_skips_dispatch_when_every_language_has_a_verdict( assert "already have authenticated terminal verdicts" in result.stdout +def test_codeql_coordinator_preserves_exact_active_dispatch_without_oidc( + tmp_path: Path, +) -> None: + """A later attempt must not replace exact work already admitted centrally.""" + title = _dispatch_scan_title(required_run_id="99") + for status in ("queued", "in_progress", "waiting", "pending", "requested"): + case_dir = tmp_path / status + result, post_log, post_body = _run_coordinator( + case_dir, + dispatch_runs={ + "workflow_runs": [_active_dispatch_run(title=title, status=status)] + }, + env_overrides={ + "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "", + "ACTIONS_ID_TOKEN_REQUEST_URL": "", + }, + ) + + assert result.returncode == 0, result.stderr + result.stdout + assert not post_log.exists() + assert not post_body.exists() or post_body.read_text(encoding="utf-8") == "" + assert not (case_dir / "curl.log").exists() + assert "preserving exact active dispatch" in result.stdout + + +def test_codeql_coordinator_does_not_trust_inexact_or_untrusted_active_runs( + tmp_path: Path, +) -> None: + """Only the exact trusted active handler may suppress recovery dispatch.""" + exact_title = _dispatch_scan_title(required_run_id="99") + cases = { + "wrong-title": _active_dispatch_run( + title=_dispatch_scan_title(required_run_id="100") + ), + "wrong-actor": _active_dispatch_run( + title=exact_title, + actor="untrusted-bot[bot]", + ), + "wrong-triggering-actor": _active_dispatch_run( + title=exact_title, + triggering_actor="untrusted-bot[bot]", + ), + "wrong-event": _active_dispatch_run( + title=exact_title, + event="workflow_dispatch", + ), + "wrong-path": _active_dispatch_run( + title=exact_title, + path=".github/workflows/untrusted-dispatch.yml", + ), + "terminal": _active_dispatch_run( + title=exact_title, + status="completed", + ), + } + + for case_name, dispatch_run in cases.items(): + result, post_log, post_body = _run_coordinator( + tmp_path / case_name, + dispatch_runs={"workflow_runs": [dispatch_run]}, + ) + + assert result.returncode == 0, result.stderr + result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/.github/dispatches" + ] + assert json.loads(post_body.read_text(encoding="utf-8"))["event_type"] == ( + "codeql-scan-v2" + ) + + def test_codeql_coordinator_fails_closed_when_a_shard_job_id_is_missing( tmp_path: Path, ) -> None: diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 74c01d7989..38c7b05eb0 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -12,6 +12,7 @@ import json import os +import re import shutil import subprocess import sys @@ -21,10 +22,6 @@ from scripts.ci import audit_central_required_workflows as ruleset_audit from tests.test_opencode_workflow_shell_syntax import _extract_run_block -from tests.test_required_workflow_queue_contract import ( - workflow_level_cancels_in_progress, - workflow_level_concurrency_group, -) REPO_ROOT = Path(__file__).resolve().parents[1] WORKFLOW_PATH = REPO_ROOT / ".github/workflows/codeql-scan-dispatch.yml" @@ -110,24 +107,18 @@ def test_codeql_scan_dispatch_keeps_current_head_language_shards_independent(): ``required_language`` in the concurrency group was the 2026-09-05 workaround after contextual-orchestrator#1049 / run 33938784437 cancelled sibling scans. Independence now comes from ``strategy.fail-fast: false`` - on this run's language matrix, so the group can be - ``{workflow}-{repository}-{PR}`` and ``cancel-in-progress: true`` only - drops a superseded HEAD of the same pull request. + on this run's language matrix. Unvalidated workflow-level cancellation is + forbidden because a delayed stale payload could evict current exact work. """ workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - group_value = workflow_level_concurrency_group(workflow) header = workflow.split("\non:", 1)[0] scan = workflow.split(" scan:\n", 1)[1] strategy = scan.split(" strategy:\n", 1)[1].split(" steps:\n", 1)[0] - assert "github.event.client_payload.target_repository" in group_value - assert "github.event.client_payload.pr_number" in group_value - assert "github.event.client_payload.required_language" not in group_value - assert "unknown-language" not in group_value + assert not re.search(r"(?m)^concurrency:", workflow) assert "required_language" not in header assert "fail-fast: false" in strategy assert "include: ${{ fromJSON(needs.validate-dispatch.outputs.matrix) }}" in strategy - assert workflow_level_cancels_in_progress(workflow) def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_request: dict) -> subprocess.CompletedProcess[str]: @@ -976,28 +967,23 @@ def test_codeql_scan_dispatch_is_not_in_the_required_workflow_ruleset_scope(): assert ".github/workflows/codeql-scan-dispatch.yml" not in required_paths -def test_codeql_scan_dispatch_run_name_versions_source_without_changing_concurrency() -> None: - """v2 adds source identity while both protocols retain one PR writer. +def test_codeql_scan_dispatch_run_name_versions_exact_source_without_early_cancellation() -> None: + """v2 exposes exact source identity without cancelling before validation. The required shard cannot read client_payload. Encoding those fields in run-name lets it reject a same-head retarget or a different waiting - required run. The #2008/#2009 group stays repository+PR so a newer HEAD - of the same pull request still cancels its predecessor. + required run. Workflow concurrency cannot safely act on payload identity + before the protected handler validates the actor and live pull request. """ workflow = WORKFLOW_PATH.read_text(encoding="utf-8") header = workflow.split("\non:", 1)[0] - group_value = workflow_level_concurrency_group(workflow) assert "github.event.client_payload.pr_head_sha" in header assert "github.event.client_payload.pr_base_sha" in header assert "github.event.client_payload.required_run_id" in header assert "github.event.client_payload.producer_source_sha" in header assert "github.event.action == 'codeql-scan-v2'" in header - assert "github.event.client_payload.pr_base_sha" not in group_value - assert "github.event.client_payload.required_run_id" not in group_value - assert "github.event.client_payload.target_repository" in group_value - assert "github.event.client_payload.pr_number" in group_value - assert "github.event.action" not in group_value + assert not re.search(r"(?m)^concurrency:", workflow) def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> None: diff --git a/tests/test_control_workflows_skip_draft_prs.py b/tests/test_control_workflows_skip_draft_prs.py index c7340b1f62..980f960a72 100644 --- a/tests/test_control_workflows_skip_draft_prs.py +++ b/tests/test_control_workflows_skip_draft_prs.py @@ -1,11 +1,14 @@ -"""Control-pool review workflows must not occupy a runner for draft pull requests. +"""Control workflows admit Draft events only for required security evidence. On 2026-09-29, 325 of 836 queued control-pool runs were for draft PRs; each -only concluded "draft, no verdict required". Entry jobs now skip drafts at -the job level, so no runner is assigned. This is safe only because every -workflow re-runs on `ready_for_review` (same head), where the real gate runs, -and because merge readiness comes from an opencode-agent review, not from -these check results. +only concluded "draft, no verdict required". Review workflows skip ordinary +Draft events at the job level. The scheduler admits only Draft-transition and +close events to retire stale central dispatches. CodeQL is different: +organization ruleset consumers do not +receive an unchanged-head `ready_for_review` launch, so consumer Draft heads +must materialize security evidence while the native owner still saves its +runner. Draft conversion and close events only enter concurrency to retire +stale work. """ from __future__ import annotations @@ -21,13 +24,9 @@ WORKFLOWS = [ "opencode-review.yml", "strix.yml", - "codeql-pr.yml", "pr-review-merge-scheduler.yml", ] -DRAFT_GUARD = ( - "(github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' " - "|| github.event.action == 'closed')" -) +DRAFT_GUARD = "github.event.pull_request.draft != true" def _load(name: str) -> dict: @@ -77,6 +76,86 @@ def test_entry_jobs_skip_draft_prs(name: str) -> None: assert DRAFT_GUARD in str(job.get("if", "")), f"{name}:{job_name} lacks the draft guard" +def test_codeql_materializes_only_consumer_draft_heads() -> None: + """Consumer Drafts scan; owner Drafts and retirement events take no runner.""" + doc = _load("codeql-pr.yml") + assert "converted_to_draft" in _pr_types(doc) + assert str(doc["jobs"]["detect-languages"].get("if", "")) == ( + "github.event.action != 'closed' && " + "github.event.action != 'converted_to_draft' && " + "(github.event.pull_request.draft != true || " + "github.event.pull_request.base.repo.full_name != 'ContextualWisdomLab/.github')" + ) + + +QUEUE_RETIREMENT_WORKFLOWS = [ + "codeql-pr.yml", + "pr-review-merge-scheduler.yml", + "sast-semgrep.yml", + "security-scan.yml", + "python-security.yml", +] + + +@pytest.mark.parametrize("name", QUEUE_RETIREMENT_WORKFLOWS) +def test_converted_to_draft_retires_queued_run_at_admission(name: str) -> None: + """Draft transitions retire work; only the scheduler takes a cleanup runner.""" + doc = _load(name) + assert "converted_to_draft" in _pr_types(doc) + for job_name, job in _entry_jobs(doc).items(): + if _is_cancellation_job(job_name, job): + continue + condition = str(job.get("if", "")) + if name == "pr-review-merge-scheduler.yml": + assert "github.event.action == 'converted_to_draft'" in condition + assert "github.event.action == 'closed'" in condition + elif name != "codeql-pr.yml": + assert "github.event.pull_request.draft != true" in condition + else: + assert "github.event.action != 'converted_to_draft'" in condition + if name != "pr-review-merge-scheduler.yml": + assert "github.event.action == 'converted_to_draft'" not in condition + + +def test_scheduler_retirement_events_are_cleanup_only() -> None: + """Draft/close admission reaches scheduler cleanup, not review or merge work.""" + scheduler = (REPO_ROOT / "scripts/ci/pr_review_merge_scheduler_core.py").read_text( + encoding="utf-8" + ) + closed_start = scheduler.index('if pr_state != "OPEN":') + draft_start = scheduler.index( + 'if pr.get("isDraft") and github_actions_control_available():' + ) + assert scheduler.index("cancel_stale_central_dispatch_runs", closed_start) < scheduler.index( + 'return Decision(number, "skip", f"{pr_state.lower()} PR")', closed_start + ) + assert scheduler.index("cancel_stale_central_dispatch_runs", draft_start) < scheduler.index( + 'if pr.get("isDraft"):', draft_start + ) + + +def test_scheduler_central_cleanup_requires_explicit_actions_authority() -> None: + """A consumer token must not masquerade as central Actions authority.""" + workflow = (REPO_ROOT / ".github/workflows/pr-review-merge-scheduler.yml").read_text( + encoding="utf-8" + ) + scheduler = (REPO_ROOT / "scripts/ci/pr_review_merge_scheduler_core.py").read_text( + encoding="utf-8" + ) + + assert ( + "SCHEDULER_ACTIONS_TOKEN: ${{ github.repository == " + "'ContextualWisdomLab/.github' && github.token || " + "secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || '' }}" + in workflow + ) + availability = scheduler.split("def github_actions_control_available", 1)[1].split( + "\n\ndef ", 1 + )[0] + assert 'os.environ.get("SCHEDULER_ACTIONS_TOKEN")' in availability + assert 'os.environ.get("GH_TOKEN")' not in availability + + def test_opencode_verdict_gate_still_runs_on_ready_for_review() -> None: """The fail-closed verdict gate is untouched for ready (non-draft) events.""" doc = _load("opencode-review.yml") diff --git a/tests/test_coverage_incomplete_summary.py b/tests/test_coverage_incomplete_summary.py new file mode 100644 index 0000000000..0163175d9f --- /dev/null +++ b/tests/test_coverage_incomplete_summary.py @@ -0,0 +1,83 @@ +"""Execute the actual decision block: missing evidence must not be called PASS.""" + +from pathlib import Path +import subprocess + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + + +def summary(failures: int, not_measured: int) -> str: + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index(' append "## Coverage Decision"') + end = text.index(' coverage_output_file=', start) + block = "\n".join(line[10:] for line in text[start:end].splitlines()) + script = ( + f"set -eu\nfailures={failures}; not_measured={not_measured}; " + "measured_any=1; r_peer_check_required=0\n" + 'append(){ printf "%s\\n" "$*"; }\n' + block + ) + return subprocess.run(["bash", "-c", script], check=True, capture_output=True, text=True).stdout + + +def test_timeout_is_incomplete_not_success_evidence() -> None: + output = summary(0, 1) + assert "- Result: NOT MEASURED" in output + assert "- Result: PASS" not in output + assert "supported repository test suites passed" not in output + assert "does not replace required verification" in output + + +def test_real_failure_wins_over_incomplete_measurements() -> None: + assert "- Result: FAIL" in summary(1, 1) + + +def test_complete_success_still_passes() -> None: + assert "- Result: PASS" in summary(0, 0) + + +def approval_prefix(decision: str) -> subprocess.CompletedProcess[str]: + """Run the real APPROVE preconditions with a successful advisory job.""" + text = WORKFLOW.read_text(encoding="utf-8") + helper_start = text.index(" coverage_decision_is_pass() {") + helper_end = text.index("\n }", helper_start) + len("\n }") + helper = "\n".join( + line[10:] for line in text[helper_start:helper_end].splitlines() + ) + start = text.index(" APPROVE)") + len(" APPROVE)") + end = text.index(" if request_changes_for_merge_conflict_if_present", start) + block = "\n".join(line[14:] for line in text[start:end].splitlines()) + script = ( + "set -eu\nCOVERAGE_EVIDENCE_RESULT=success\n" + 'COVERAGE_EVIDENCE_SUMMARY="$1"\n' + 'stop_approval_without_review(){ printf "%s\\n" "$1"; exit 1; }\n' + + helper + + "\n" + + block + + '\nprintf "approval_allowed\\n"\n' + ) + return subprocess.run(["bash", "-c", script, "test", decision], capture_output=True, text=True) + + +def test_successful_advisory_job_cannot_approve_unmeasured_coverage() -> None: + result = approval_prefix(summary(0, 1)) + assert result.returncode == 1 + assert "COVERAGE_NOT_MEASURED" in result.stdout + assert "approval_allowed" not in result.stdout + + +def test_missing_or_ambiguous_decision_fails_closed_before_approval() -> None: + for decision in ( + "", + "- Result: UNKNOWN", + "prefix - Result: PASS", + "- Result: PASS (assumed)", + "- Result: PASS\n- Result: NOT MEASURED", + "- Result: PASS\n- Result: PASS", + ): + assert approval_prefix(decision).returncode == 1, decision + + +def test_complete_decision_can_reach_remaining_approval_checks() -> None: + result = approval_prefix(summary(0, 0)) + assert result.returncode == 0 + assert "approval_allowed" in result.stdout diff --git a/tests/test_current_head_coalescer_self_cancellation.py b/tests/test_current_head_coalescer_self_cancellation.py index e49193d4df..55de2228f2 100644 --- a/tests/test_current_head_coalescer_self_cancellation.py +++ b/tests/test_current_head_coalescer_self_cancellation.py @@ -9,8 +9,8 @@ ) -def test_current_head_coalescer_shares_pr_scoped_scheduler_admission() -> None: - """The integrated step reuses PR-scoped scheduler admission and its runner.""" +def test_current_head_coalescer_preserves_exact_head_scheduler_admission() -> None: + """The scheduler queues one exact head and retires predecessors explicitly.""" workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8") coalescer = workflow_text.split("\n scan-pr-queue:\n", 1)[1] concurrency_block = workflow_text.split("\nconcurrency:\n", 1)[1].split( @@ -24,11 +24,18 @@ def test_current_head_coalescer_shares_pr_scoped_scheduler_admission() -> None: assert "Retire redundant queued exact-head runs" in coalescer assert "github.repository == 'ContextualWisdomLab/.github'" in coalescer - assert "github.event.pull_request.head.sha" not in concurrency_block + assert "github.event.pull_request.head.sha" in concurrency_block + assert "github.event.client_payload.pr_head_sha" in concurrency_block assert "github.event.pull_request.number" in concurrency_block - assert any( - line.startswith("cancel-in-progress:") - and "github.event_name == 'pull_request_target'" in line - for line in active_lines - ) - assert "queue: max" not in workflow_text + assert not any(line.startswith("cancel-in-progress:") for line in active_lines) + assert "queue: max" in concurrency_block + + cleanup = workflow_text.split("\n cancel-superseded-pr-runs:\n", 1)[1].split( + "\n scan-pr-queue:\n", 1 + )[0] + assert "github.event.action == 'synchronize'" in cleanup + assert "github.event.action == 'closed'" in cleanup + assert "actions: write" in cleanup + assert "actions/checkout" not in cleanup + assert "live_target_matches" in cleanup + assert "did not reach completed/cancelled" in cleanup diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index a0f691e2a2..74f91d1e7a 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -256,7 +256,9 @@ def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): assert not re.search(r"(?m)^ needs:", semgrep) job_if = re.search(r"(?m)^ if: (.*)$", semgrep) assert job_if is not None - assert job_if.group(1) == "github.event.action != 'closed'" + assert job_if.group(1) == ( + "github.event.action != 'closed' && github.event.pull_request.draft != true" + ) assert "pull-requests: read" in semgrep assert "id: scope" in semgrep assert semgrep.count("steps.scope.outputs.code == 'true'") == 5 diff --git a/tests/test_noema_document_reader_dependency_security.py b/tests/test_noema_document_reader_dependency_security.py deleted file mode 100644 index e7511447eb..0000000000 --- a/tests/test_noema_document_reader_dependency_security.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Keep the Noema document reader above known vulnerable transitive releases.""" - -from __future__ import annotations - -import json -from pathlib import Path - -LOCK_FILE = ( - Path(__file__).resolve().parents[1] - / "scripts" - / "ci" - / "noema-document-reader" - / "package-lock.json" -) -PACKAGE_FILE = LOCK_FILE.with_name("package.json") - - -def _locked_versions(lock_data: dict[str, object], package_name: str) -> list[tuple[int, ...]]: - """Return every hoisted or nested locked release for one package.""" - package_records = lock_data["packages"] - assert isinstance(package_records, dict) - path_suffix = f"node_modules/{package_name}" - release_versions: list[tuple[int, ...]] = [] - for package_path, package_data in package_records.items(): - if package_path != path_suffix and not package_path.endswith(f"/{path_suffix}"): - continue - assert isinstance(package_data, dict) - release_versions.append(tuple(int(part) for part in package_data["version"].split("."))) - return release_versions - - -def test_document_reader_transitives_include_security_fixes() -> None: - """Reject releases affected by the September 2026 URI and IP advisories.""" - lock_data = json.loads(LOCK_FILE.read_text(encoding="utf-8")) - for package_name, minimum_version in { - "fast-uri": (3, 1, 8), - "ip-address": (10, 7, 1), - }.items(): - locked_versions = _locked_versions(lock_data, package_name) - assert locked_versions - assert all(version >= minimum_version for version in locked_versions) - - -def test_nested_vulnerable_transitive_is_detected() -> None: - """Do not let a patched hoisted package hide a vulnerable nested copy.""" - lock_data = { - "packages": { - "node_modules/ip-address": {"version": "10.7.1"}, - "node_modules/parent/node_modules/ip-address": {"version": "10.7.0"}, - } - } - assert _locked_versions(lock_data, "ip-address") == [(10, 7, 1), (10, 7, 0)] - - -def test_document_reader_source_owns_transitive_security_fixes() -> None: - """Require source overrides so lock regeneration preserves the repair.""" - package_data = json.loads(PACKAGE_FILE.read_text(encoding="utf-8")) - - assert package_data["overrides"] == { - "fast-uri": "3.1.8", - "ip-address": "10.7.1", - } diff --git a/tests/test_noema_document_reader_runtime_dependencies.py b/tests/test_noema_document_reader_runtime_dependencies.py index 6d2441cf02..f985a88ad3 100644 --- a/tests/test_noema_document_reader_runtime_dependencies.py +++ b/tests/test_noema_document_reader_runtime_dependencies.py @@ -19,7 +19,7 @@ @pytest.mark.parametrize( ("package_name", "safe_version"), - (("fast-uri", "3.1.8"), ("ip-address", "10.7.1")), + (("fast-uri", "3.1.8"), ("ip-address", "10.7.2")), ) def test_noema_document_reader_uses_exclusive_safe_runtime_dependency( package_name: str, safe_version: str diff --git a/tests/test_noema_document_review_context.py b/tests/test_noema_document_review_context.py index f380cafafb..874bf81b18 100644 --- a/tests/test_noema_document_review_context.py +++ b/tests/test_noema_document_review_context.py @@ -101,6 +101,8 @@ def test_hosted_reader_bundle_is_pinned_and_local(): assert package["dependencies"] == {"@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0"} assert lock["packages"]["node_modules/hwp-mcp"]["version"] == "0.3.0" assert lock["packages"]["node_modules/@rhwp/core"]["version"] == "0.7.7" + assert lock["packages"]["node_modules/fast-uri"]["version"] == "3.1.8" + assert lock["packages"]["node_modules/ip-address"]["version"] == "10.7.2" assert "requirements-noema-document-ci-hashes.txt" in workflow assert "python3 -m pip install --quiet --require-hashes --no-deps" in workflow assert "requirements-noema-document-ci-hashes.txt" in quality_workflow diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 8a80d65461..1dac46bdf7 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1225,7 +1225,41 @@ def test_opencode_model_exhaustion_retry_stays_owned_by_central_scheduler(): workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text(encoding="utf-8") assert "opencode-exhausted-retry:" not in workflow assert "RETRY_DISPATCH_TOKEN" not in workflow - assert "contents: write" not in workflow + admission_job = workflow.split(" admit-exact-head-dispatch:\n", 1)[1].split( + "\n validate-pr-metadata:", 1 + )[0] + validation_job = workflow.split(" validate-pr-metadata:\n", 1)[1].split( + "\n coverage-evidence:", 1 + )[0] + review_job = workflow.split(" opencode-review-target:\n", 1)[1] + assert admission_job.count("contents: write") == 1 + assert "opencode-dispatch-leases" in admission_job + assert "contents: write" not in validation_job + assert "contents: write" not in review_job + + +def test_receiver_rechecks_formal_receipt_after_exact_head_lease(): + """A completed owner must not let a duplicate receiver repeat model work.""" + workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( + encoding="utf-8" + ) + admission_job = workflow.split(" admit-exact-head-dispatch:\n", 1)[1].split( + "\n validate-pr-metadata:", 1 + )[0] + + assert "opencode_review_receipt_gate.py?ref=${GITHUB_SHA}" in admission_job + assert "definite_receipt_state()" in admission_job + assert 'gate["evaluate_receipts"](' in admission_job + assert 'echo "admitted=false" >>"$GITHUB_OUTPUT"' in admission_job + assert admission_job.count('echo "admitted=true" >>"$GITHUB_OUTPUT"') == 1 + + lease_acquired = admission_job.index( + 'echo "Central exact-head dispatch ${GITHUB_RUN_ID} acquired the atomic lease."' + ) + receipt_recheck = admission_job.index( + "complete_receipt_admission", lease_acquired + ) + assert lease_acquired < receipt_recheck def test_sandbox_git_config_env_trusts_only_the_validated_worktree(tmp_path): @@ -1854,19 +1888,13 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "run_opencode_review_model_pool.sh" in workflow assert "rekick_model_pool_on_exhaustion" not in workflow assert "publish stage performs no duplicate model-catalog pass" in workflow - # The review job's own group, addressed by its indentation: the workflow - # also carries a workflow-level admission group (pinned in - # tests/test_required_workflow_queue_contract.py), so splitting on the - # first "concurrency:" would read that one instead of this one. - concurrency_contract = workflow.split("\n concurrency:", 1)[1].split( - "\n runs-on:", 1 - )[0] - assert "needs.validate-pr-metadata.outputs.target_repository" in concurrency_contract - assert "needs.validate-pr-metadata.outputs.pr_number || github.run_id" in concurrency_contract - assert "format('pr-{0}-{1}'" not in concurrency_contract - assert "github.event.client_payload.pr_head_sha" not in concurrency_contract - assert "github.event.client_payload.pr_number" not in concurrency_contract - assert "github.event.pull_request" not in concurrency_contract + # GitHub native concurrency replaces an older pending member even with + # cancel-in-progress disabled. Producer inventory and live-head retirement + # own admission, so the receiver intentionally has no concurrency group. + workflow_header = workflow.split("permissions:", 1)[0] + review_job = workflow.split("\n opencode-review-target:\n", 1)[1] + assert not re.search(r"(?m)^concurrency:", workflow_header) + assert not re.search(r"(?m)^ concurrency:", review_job) assert "OPENCODE_MODEL_CANDIDATES" in workflow model_pool_runner = Path("scripts/ci/run_opencode_review_model_pool.sh").read_text( encoding="utf-8" @@ -2420,16 +2448,21 @@ def test_merge_scheduler_uses_escalating_mutation_credentials(): assert "secrets.PR_REVIEW_MERGE_TOKEN" in workflow assert "secrets.OPENCODE_APPROVE_TOKEN" in workflow assert "steps.scheduler_app_token.outputs.token" in workflow - for token_name in ("SCHEDULER_ACTIONS_TOKEN", "SCHEDULER_READ_TOKEN"): - assert ( - f"{token_name}: ${{{{ github.event_name == 'repository_dispatch' " + assert ( + "SCHEDULER_ACTIONS_TOKEN: ${{ github.repository == " + "'ContextualWisdomLab/.github' && github.token || " + "secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || '' }}" + in workflow + ) + assert ( + "SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' " "&& github.event.client_payload.target_repository != '' && " "github.event.client_payload.target_repository != github.repository && " "(secrets.PR_REVIEW_MERGE_TOKEN || " "secrets.OPENCODE_APPROVE_TOKEN || " "steps.scheduler_app_token.outputs.token) || github.token }}" in workflow - ) + ) assert "SCHEDULER_MUTATION_TOKEN_SOURCE" in workflow assert 'default: "1"' in workflow assert 'review_dispatch_limit="-1"' in workflow @@ -2456,10 +2489,13 @@ def test_merge_scheduler_uses_escalating_mutation_credentials(): assert 'check_delay="$((check_attempt * 2))"' in workflow assert "steps.review_followup.outputs.proceed != 'false'" in workflow assert "Native events and the explicit org-sweep recovery remain authoritative." in workflow - assert ( - "github.event_name == 'pull_request_review' || " - "github.event_name == 'repository_dispatch'" in workflow - ) + concurrency = workflow.split("\nconcurrency:\n", 1)[1].split( + "\npermissions:\n", 1 + )[0] + assert "queue: max" in concurrency + assert "cancel-in-progress:" not in concurrency + assert "github.event.pull_request.head.sha" in concurrency + assert "github.event.client_payload.pr_head_sha" in concurrency def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch(): @@ -2513,6 +2549,7 @@ def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch( assert "using %s token" in status_step assert "scripts/ci/opencode_dispatch_status.py" in status_step assert "COVERAGE_EVIDENCE_RESULT" in status_step + assert '--coverage-summary "${COVERAGE_EVIDENCE_SUMMARY:-}"' in status_step assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}"' in status_step assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews"' in status_step assert '[ "${OPENCODE_MODEL_POOL_OUTCOME:-}" != "success" ] &&' not in status_step @@ -3080,6 +3117,8 @@ def test_opencode_model_pool_failure_uses_only_existing_real_model_approval(): assert "no duplicate APPROVE review was posted" in workflow assert "opencode_existing_approval_gate.py" in workflow assert '--head "$HEAD_SHA"' in workflow + assert '--coverage-summary "$COVERAGE_EVIDENCE_SUMMARY"' in workflow + assert workflow.count('--coverage-summary "$COVERAGE_EVIDENCE_SUMMARY"') == 2 assert "--require-opencode-app" in workflow assert ( "same-head real-model OpenCode approval with passed adversarial evidence" diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 9b91ee1f77..797817dab4 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -68,6 +68,7 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): changed_files.chmod(0o600) monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) + monkeypatch.setenv("COVERAGE_EVIDENCE_SUMMARY", "- Result: PASS") monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) monkeypatch.setenv( "OPENCODE_ARTIFACT_MANIFEST_SHA256", @@ -133,6 +134,51 @@ def review(**overrides): return value +@pytest.mark.parametrize( + "summary", + ( + "## Coverage Decision\n\n- Result: NOT MEASURED\n", + "## Coverage Decision\n\n", + "- Result: PASS\n- Result: PASS\n", + "- Result: PASSING\n", + ), +) +def test_coverage_summary_rejects_every_non_unique_pass(summary): + """Reusable approval requires one exact passing coverage decision.""" + assert gate.coverage_summary_rejection_reason(summary) + + +def test_coverage_summary_accepts_one_exact_pass(): + """Diagnostic prose may surround the one authoritative PASS line.""" + assert ( + gate.coverage_summary_rejection_reason( + "## Coverage Decision\n\n- Result: PASS\n\n- Rust: measured\n" + ) + is None + ) + + +@pytest.mark.parametrize( + "summary", + ( + "", + "- Result: NOT MEASURED", + "prefix - Result: PASS", + "- Result: PASS (assumed)", + "- Result: PASS\n- Result: PASS", + "- Result: PASS\n- Result: NOT MEASURED", + ), +) +def test_coverage_decision_rejects_missing_ambiguous_or_incomplete_summary(summary): + """Existing approval reuse requires one exact PASS decision.""" + assert gate.coverage_decision_is_pass(summary) is False + + +def test_coverage_decision_accepts_one_exact_pass_line(): + """One exact PASS line is reusable coverage evidence.""" + assert gate.coverage_decision_is_pass("detail\n- Result: PASS\n") is True + + @pytest.mark.parametrize("payload", [[review()], [[review()]]]) def test_flatten_reviews_and_accept_real_model_approval(payload): reviews = gate.flatten_reviews(payload) @@ -300,10 +346,9 @@ def test_has_reusable_real_model_approval_logs_rejected_candidates(): log = io.StringIO() assert not gate.has_reusable_real_model_approval( [ - review(state="COMMENTED"), + fallback, review(commit_id="b" * 40), review(user={"login": "unknown"}), - fallback, ], HEAD, log=log, @@ -342,6 +387,20 @@ def test_opencode_app_only_mode_accepts_app_approval(): assert "author=opencode-agent[bot]" in log.getvalue() +def test_newer_same_head_changes_requested_revokes_reusable_approval(): + """The latest exact-head OpenCode decision supersedes historical approval.""" + log = io.StringIO() + older_approval = review(id=7) + newer_rejection = review(id=8, state="CHANGES_REQUESTED") + + assert not gate.has_reusable_real_model_approval( + [older_approval, newer_rejection], + HEAD, + log=log, + ) + assert "latest same-head review" in log.getvalue() + + def test_adversarial_validation_rejects_circular_or_unanchored_evidence(): weak = { "status": "passed", @@ -413,30 +472,41 @@ def test_adversarial_validation_rejects_forged_traversal_receipt(): def test_parse_args_and_main(monkeypatch, capsys): - args = gate.parse_args(["--head", HEAD]) + coverage_args = ["--coverage-summary", "- Result: PASS"] + args = gate.parse_args(["--head", HEAD, *coverage_args]) assert args.head == HEAD assert not args.require_opencode_app - strict_args = gate.parse_args(["--head", HEAD, "--require-opencode-app"]) + strict_args = gate.parse_args( + ["--head", HEAD, *coverage_args, "--require-opencode-app"] + ) assert strict_args.require_opencode_app monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps([[review()]]))) - assert gate.main(["--head", HEAD]) == 0 + assert gate.main(["--head", HEAD, *coverage_args]) == 0 + + monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) + assert gate.main( + ["--head", HEAD, "--coverage-summary", "- Result: NOT MEASURED"] + ) == 1 + assert "coverage decision is not PASS" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("not-json")) - assert gate.main(["--head", HEAD]) == 2 + assert gate.main(["--head", HEAD, *coverage_args]) == 2 assert "could not parse reviews" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) - assert gate.main(["--head", "short"]) == 2 + assert gate.main(["--head", "short", *coverage_args]) == 2 assert "40-character" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) - assert gate.main(["--head", HEAD]) == 1 + assert gate.main(["--head", HEAD, *coverage_args]) == 1 monkeypatch.setattr( sys, "stdin", io.StringIO(json.dumps([[review(user={"login": "github-actions[bot]"})]])), ) - assert gate.main(["--head", HEAD, "--require-opencode-app"]) == 1 + assert gate.main( + ["--head", HEAD, *coverage_args, "--require-opencode-app"] + ) == 1 diff --git a/tests/test_opencode_required_rerun_capacity.py b/tests/test_opencode_required_rerun_capacity.py index c85bc24e3c..f2438c46d4 100644 --- a/tests/test_opencode_required_rerun_capacity.py +++ b/tests/test_opencode_required_rerun_capacity.py @@ -7,6 +7,7 @@ import os from pathlib import Path import subprocess +import textwrap from tests.test_opencode_required_verdict_regression import HEAD, fail_closed_script @@ -15,6 +16,28 @@ DISPATCH = Path(".github/workflows/opencode-review-dispatch.yml") +def wake_failed_required_runs_script() -> str: + """Extract the exact production wake step shell.""" + dispatch = DISPATCH.read_text(encoding="utf-8") + step = dispatch.split( + " - name: Wake every failed exact-head Required OpenCode workflow\n", 1 + )[1].split("\n\n - name:", 1)[0] + return textwrap.dedent(step.split(" run: |\n", 1)[1]) + + +def required_run(run_id: int, *, pr_number: int = 7) -> dict[str, object]: + """Build one exact-head required-workflow list record.""" + return { + "id": run_id, + "event": "pull_request_target", + "path": ".github/workflows/opencode-review.yml", + "head_sha": "c" * 40, + "status": "completed", + "conclusion": "failure", + "pull_requests": [{"number": pr_number, "head": {"sha": HEAD}}], + } + + def test_required_job_releases_runner_until_exact_run_wakeup() -> None: required = REQUIRED.read_text(encoding="utf-8") target = required.split(" opencode-review-target:\n", 1)[1].split( @@ -29,18 +52,355 @@ def test_required_job_releases_runner_until_exact_run_wakeup() -> None: assert "will rerun this failed job" in target -def test_dispatch_wakes_only_the_exact_failed_current_head_run() -> None: +def test_dispatch_wakes_every_exact_failed_current_head_run() -> None: dispatch = DISPATCH.read_text(encoding="utf-8") - wake = dispatch.split(" - name: Wake exact-head required OpenCode workflow\n", 1)[1].split( - "\n\n - name:", 1 - )[0] + wake = dispatch.split( + " - name: Wake every failed exact-head Required OpenCode workflow\n", 1 + )[1].split("\n\n - name:", 1)[0] - assert "github.event.client_payload.required_run_id != ''" in wake - assert "select(.id == $run_id)" in wake - assert 'select(.event == "pull_request_target")' in wake - assert 'select(.path == ".github/workflows/opencode-review.yml")' in wake - assert "select(.head_sha == $head)" in wake + assert "github.event.client_payload.required_run_id != ''" not in wake + assert '.event == "pull_request_target"' in wake + assert '.path == ".github/workflows/opencode-review.yml"' in wake + assert "(.head.sha | ascii_downcase) == ($head | ascii_downcase)" in wake + assert ".number == $pr" in wake + assert "unique_by(.id)" in wake assert "rerun-failed-jobs" in wake + assert "actions/workflows/opencode-review.yml/runs" not in wake + assert "actions/runs?event=pull_request_target&created=" in wake + assert "range_total" in wake + assert "expected_total" in wake + assert "live_authority_matches" in wake + + +def test_dispatch_wake_binds_shared_head_runs_to_exact_pull_request( + tmp_path: Path, +) -> None: + """Wake all failures for this PR and none for another PR sharing its head.""" + calls = tmp_path / "calls" + fake_gh = tmp_path / "gh" + inventory = { + "total_count": 3, + "workflow_runs": [ + required_run(41), + required_run(42, pr_number=8), + required_run(43), + ] + } + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + printf '%s' "$LIVE_PR" +elif [[ "$*" == *"repos/owner/repo/actions/runs?"* ]]; then + printf '%s' "$RUN_INVENTORY" +elif [[ "$*" == *"rerun-failed-jobs"* ]]; then + exit 0 +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "CALLS": str(calls), + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + "LIVE_PR": json.dumps( + { + "state": "open", + "draft": False, + "created_at": "2026-09-30T00:00:00Z", + "head": {"sha": HEAD}, + } + ), + "RUN_INVENTORY": json.dumps(inventory), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + reruns = [line for line in calls.read_text().splitlines() if "rerun-failed-jobs" in line] + assert reruns == [ + "api -X POST repos/owner/repo/actions/runs/41/rerun-failed-jobs", + "api -X POST repos/owner/repo/actions/runs/43/rerun-failed-jobs", + ] + assert calls.read_text().count("api repos/owner/repo/pulls/7") == 3 + + +def test_dispatch_wake_stops_when_live_authority_moves_between_mutations( + tmp_path: Path, +) -> None: + """Revalidate the live open head immediately before every rerun POST.""" + calls = tmp_path / "calls" + authority_reads = tmp_path / "authority-reads" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + count=0 + [[ ! -f "$AUTHORITY_READS" ]] || count="$(cat "$AUTHORITY_READS")" + count=$((count + 1)) + printf '%s' "$count" >"$AUTHORITY_READS" + if [[ "$count" -lt 3 ]]; then head="$PR_HEAD_SHA"; else head="$(printf 'd%.0s' {1..40})"; fi + jq -cn --arg head "$head" '{state:"open",draft:false,created_at:"2026-09-30T00:00:00Z",head:{sha:$head}}' +elif [[ "$*" == *"repos/owner/repo/actions/runs?"* ]]; then + printf '%s' "$RUN_INVENTORY" +elif [[ "$*" == *"rerun-failed-jobs"* ]]; then + exit 0 +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "CALLS": str(calls), + "AUTHORITY_READS": str(authority_reads), + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + "RUN_INVENTORY": json.dumps( + { + "total_count": 2, + "workflow_runs": [required_run(41), required_run(43)], + } + ), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "authority changed before Required OpenCode rerun 43" in result.stdout + reruns = [line for line in calls.read_text().splitlines() if "rerun-failed-jobs" in line] + assert reruns == ["api -X POST repos/owner/repo/actions/runs/41/rerun-failed-jobs"] + + +def test_dispatch_wake_reports_partial_rerun_api_failure(tmp_path: Path) -> None: + """A later rerun failure must fail the publisher after recording prior work.""" + calls = tmp_path / "calls" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg head "$PR_HEAD_SHA" '{state:"open",draft:false,created_at:"2026-09-30T00:00:00Z",head:{sha:$head}}' +elif [[ "$*" == *"repos/owner/repo/actions/runs?"* ]]; then + printf '%s' "$RUN_INVENTORY" +elif [[ "$*" == *"actions/runs/41/rerun-failed-jobs"* ]]; then + exit 0 +elif [[ "$*" == *"actions/runs/43/rerun-failed-jobs"* ]]; then + exit 22 +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "CALLS": str(calls), + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + "RUN_INVENTORY": json.dumps( + { + "total_count": 2, + "workflow_runs": [required_run(41), required_run(43)], + } + ), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 22 + reruns = [line for line in calls.read_text().splitlines() if "rerun-failed-jobs" in line] + assert reruns == [ + "api -X POST repos/owner/repo/actions/runs/41/rerun-failed-jobs", + "api -X POST repos/owner/repo/actions/runs/43/rerun-failed-jobs", + ] + + +def test_dispatch_wake_partitions_inventory_above_github_search_cap( + tmp_path: Path, +) -> None: + """Bisect GitHub's overflow sentinel before collecting bounded pages.""" + calls = tmp_path / "calls" + probe_count = tmp_path / "probe-count" + page_count = tmp_path / "page-count" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg head "$PR_HEAD_SHA" '{state:"open",draft:false,created_at:"2026-09-30T00:00:00Z",head:{sha:$head}}' +elif [[ "$*" == *"actions/runs?"* && "$*" == *"per_page=1" ]]; then + count=0 + [[ ! -f "$PROBE_COUNT" ]] || count="$(cat "$PROBE_COUNT")" + count=$((count + 1)) + printf '%s' "$count" >"$PROBE_COUNT" + case "$count" in + 1) printf '%s' '{"total_count":"2,500+","workflow_runs":[]}' ;; + 2) printf '%s' '{"total_count":1,"workflow_runs":[]}' ;; + 3) printf '%s' '{"total_count":0,"workflow_runs":[]}' ;; + *) exit 96 ;; + esac +elif [[ "$*" == *"api --paginate"* && "$*" == *"per_page=100"* ]]; then + count=0 + [[ ! -f "$PAGE_COUNT" ]] || count="$(cat "$PAGE_COUNT")" + count=$((count + 1)) + printf '%s' "$count" >"$PAGE_COUNT" + if [[ "$count" -eq 1 ]]; then printf '%s' "$ONE_RUN"; else printf '%s' '{"workflow_runs":[]}'; fi +elif [[ "$*" == *"actions/runs/41/rerun-failed-jobs"* ]]; then + exit 0 +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "CALLS": str(calls), + "PROBE_COUNT": str(probe_count), + "PAGE_COUNT": str(page_count), + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + "ONE_RUN": json.dumps({"workflow_runs": [required_run(41)]}), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert probe_count.read_text() == "3" + assert page_count.read_text() == "2" + assert "actions/runs/41/rerun-failed-jobs" in calls.read_text() + + +def test_dispatch_wake_fails_when_overflow_shares_one_second(tmp_path: Path) -> None: + """A non-partitionable overflow sentinel fails before run selection.""" + fake_gh = tmp_path / "gh" + fake_date = tmp_path / "date" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg head "$PR_HEAD_SHA" '{state:"open",draft:false,created_at:"2026-09-30T00:00:00Z",head:{sha:$head}}' +elif [[ "$*" == *"actions/runs?"* && "$*" == *"per_page=1" ]]; then + printf '%s' '{"total_count":"2,500+","workflow_runs":[]}' +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_date.write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "-u +%Y-%m-%dT%H:%M:%SZ" ]]; then + printf '%s\n' '2026-09-30T00:00:00Z' +elif [[ "$*" == *"+%s"* ]]; then + printf '%s\n' '1' +else + exit 98 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + fake_date.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "More than 1,000 pull_request_target runs share one second" in result.stderr + + +def test_dispatch_wake_rejects_paginated_inventory_truncation(tmp_path: Path) -> None: + """Compare API total_count with collected rows before selecting mutations.""" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg head "$PR_HEAD_SHA" '{state:"open",draft:false,created_at:"2026-09-30T00:00:00Z",head:{sha:$head}}' +elif [[ "$*" == *"actions/runs?"* && "$*" == *"per_page=1" ]]; then + printf '%s' '{"total_count":2,"workflow_runs":[]}' +elif [[ "$*" == *"api --paginate"* && "$*" == *"per_page=100"* ]]; then + printf '%s' "$ONE_RUN" +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", wake_failed_required_runs_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}", + "GH_TOKEN": "token", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "GH_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "PR_HEAD_SHA": HEAD, + "ONE_RUN": json.dumps({"workflow_runs": [required_run(41)]}), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "inventory was truncated: expected=2 collected=1" in result.stderr def test_native_cancellation_runs_before_runner_admission() -> None: diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index c764ad0ad2..9a080bb915 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -52,6 +52,26 @@ def admission_script() -> str: return textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n\n changed-scope:", 1)[0]) +def central_single_flight_script() -> str: + """Extract the receiver's deterministic exact-head winner selection.""" + workflow = DISPATCH_WORKFLOW.read_text(encoding="utf-8") + step = workflow.split( + " - name: Admit one exact-head central dispatch\n", 1 + )[1] + block = step.split(" run: |\n", 1)[1].split("\n\n - name:", 1)[0] + return textwrap.dedent(block) + + +def central_authorization_script() -> str: + """Extract the unprivileged authorization boundary before token exchange.""" + workflow = DISPATCH_WORKFLOW.read_text(encoding="utf-8") + step = workflow.split( + " - name: Authorize repository dispatch envelope\n", 1 + )[1] + block = step.split(" run: |\n", 1)[1].split("\n\n - name:", 1)[0] + return textwrap.dedent(block) + + def test_stale_opencode_event_never_reaches_review_concurrency(tmp_path: Path) -> None: """A delayed old synchronize event is retired by live-head admission.""" fake_gh = tmp_path / "gh" @@ -82,16 +102,677 @@ def test_stale_opencode_event_never_reaches_review_concurrency(tmp_path: Path) - assert "retired a stale event" in result.stdout -def test_opencode_dispatch_uses_the_same_target_repo_pr_group() -> None: - """PR and repository_dispatch review jobs compute the same group text.""" +def test_opencode_dispatch_never_uses_lossy_native_concurrency() -> None: + """The receiver must not let GitHub replace a pending exact-head run.""" required = WORKFLOW.read_text(encoding="utf-8") dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") assert "opencode-review-${{" in required - assert "opencode-review-${{" in dispatched - assert "needs.validate-pr-metadata.outputs.target_repository" in dispatched - assert "needs.validate-pr-metadata.outputs.pr_number || github.run_id" in dispatched - assert workflow_level_cancels_in_progress(dispatched) - assert dispatched.index("validate-pr-metadata:") < dispatched.index(" concurrency:") + header = dispatched.split("permissions:", 1)[0] + review_job = dispatched.split("\n opencode-review-target:\n", 1)[1] + required_job = required.split("\n opencode-review-target:\n", 1)[1] + assert not re.search(r"(?m)^concurrency:", header) + assert not re.search(r"(?m)^ concurrency:", review_job) + required_permissions = required_job.split(" permissions:\n", 1)[1].split( + " steps:", 1 + )[0] + assert "actions: write" not in required_permissions + admission_job = dispatched.split("\n admit-exact-head-dispatch:\n", 1)[1].split( + "\n validate-pr-metadata:\n", 1 + )[0] + validation_job = dispatched.split("\n validate-pr-metadata:\n", 1)[1].split( + "\n coverage-evidence:\n", 1 + )[0] + assert "contents: write" in admission_job + assert "contents: write" not in validation_job + assert "contents: read" in validation_job + assert "admitted: ${{ steps.single_flight.outputs.admitted }}" in admission_job + assert "needs.admit-exact-head-dispatch.outputs.admitted == 'true'" in dispatched + assert "pull_request_review:" not in required + assert "Wake every failed exact-head Required OpenCode workflow" in dispatched + + +@pytest.mark.parametrize( + ("override", "error_text"), + ( + ({"DISPATCH_SENDER": "untrusted"}, "rejected actor="), + ( + {"TARGET_REPOSITORY": "ContextualWisdomLab/unlisted"}, + "rejected target=", + ), + ({"SUPPLIED_HEAD_SHA": "mutable"}, "malformed PR identity metadata"), + ({"DRAFT_REVIEW_ONLY": "yes"}, "malformed draft-review authority"), + ), +) +def test_central_authorization_rejects_before_any_token_or_lease_access( + tmp_path: Path, override: dict[str, str], error_text: str +) -> None: + """Untrusted envelopes stop before OIDC exchange or Contents API mutation.""" + workflow = DISPATCH_WORKFLOW.read_text(encoding="utf-8") + authorize = workflow.index( + " - name: Authorize repository dispatch envelope\n" + ) + exchange = workflow.index( + " - name: Exchange OpenCode app token for target repository metadata reads\n" + ) + lease = workflow.index(" - name: Admit one exact-head central dispatch\n") + assert authorize < exchange < lease + + calls = tmp_path / "external-calls" + for command in ("curl", "gh"): + fake_command = tmp_path / command + fake_command.write_text( + "#!/usr/bin/env bash\nprintf '%s\\n' \"$0 $*\" >>\"$EXTERNAL_CALLS\"\nexit 99\n", + encoding="utf-8", + ) + fake_command.chmod(0o755) + env = { + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "EXTERNAL_CALLS": str(calls), + "EVENT_NAME": "repository_dispatch", + "DISPATCH_ACTOR": "opencode-agent[bot]", + "DISPATCH_SENDER": "opencode-agent[bot]", + "ALLOWED_DISPATCH_ACTOR": "opencode-agent[bot]", + "ALLOWED_DISPATCH_TARGETS": "ContextualWisdomLab/example", + "TARGET_REPOSITORY": "ContextualWisdomLab/example", + "PR_NUMBER": "7", + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "DRAFT_REVIEW_ONLY": "false", + **override, + } + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_authorization_script()], + env=env, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert error_text in result.stdout + assert not calls.exists() + + +def test_central_admission_accepts_only_an_exact_authorized_draft_marker( + tmp_path: Path, +) -> None: + """An explicit Draft review reaches lease admission only with its live marker.""" + calls = tmp_path / "calls" + marker_name = f"cwl-draft-review-request-owner-repo-7-{HEAD}" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:true,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == "api repos/ContextualWisdomLab/.github/actions/artifacts?name=$MARKER_NAME&per_page=100" ]]; then + jq -cn --arg name "$MARKER_NAME" '{total_count:1,artifacts:[{id:91,name:$name,expired:false}]}' +elif [[ "$*" == *"git/ref/heads/opencode-dispatch-leases"* ]]; then + printf '{}' +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "CALLS": str(calls), + "MARKER_NAME": marker_name, + "GITHUB_OUTPUT": str(tmp_path / "github-output"), + "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "DRAFT_REVIEW_ONLY": "true", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "Central OpenCode lease branch identity was malformed" in result.stdout + assert calls.read_text(encoding="utf-8").splitlines()[:3] == [ + "api repos/owner/repo/pulls/7", + ( + "api repos/ContextualWisdomLab/.github/actions/artifacts?" + f"name={marker_name}&per_page=100" + ), + "api repos/ContextualWisdomLab/.github/git/ref/heads/opencode-dispatch-leases", + ] + + +@pytest.mark.parametrize( + "artifact_payload", + ( + '{"total_count":1,"artifacts":[{"id":91,"name":"wrong","expired":false}]}', + '{"total_count":1,"artifacts":[{"id":91,"name":"MARKER","expired":true}]}', + '{"total_count":"1","artifacts":[]}', + ), +) +def test_central_admission_rejects_draft_without_a_valid_live_marker( + tmp_path: Path, artifact_payload: str +) -> None: + """Malformed, mismatched, or expired Draft authority fails before lease access.""" + calls = tmp_path / "calls" + marker_name = f"cwl-draft-review-request-owner-repo-7-{HEAD}" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:true,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == *"/actions/artifacts?name="* ]]; then + printf '%s' "$ARTIFACT_PAYLOAD" | sed "s/MARKER/$MARKER_NAME/g" +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "CALLS": str(calls), + "MARKER_NAME": marker_name, + "ARTIFACT_PAYLOAD": artifact_payload, + "GITHUB_OUTPUT": str(tmp_path / "github-output"), + "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "DRAFT_REVIEW_ONLY": "true", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "authority changed before atomic OpenCode admission" in result.stdout + assert all("opencode-dispatch-leases" not in call for call in calls.read_text().splitlines()) + + +def test_central_admission_revalidates_draft_marker_before_lease_mutation( + tmp_path: Path, +) -> None: + """A marker that expires after admission cannot authorize a Contents write.""" + calls = tmp_path / "calls" + marker_reads = tmp_path / "marker-reads" + marker_name = f"cwl-draft-review-request-owner-repo-7-{HEAD}" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:true,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == *"/actions/artifacts?name="* ]]; then + read_count=0 + [[ ! -f "$MARKER_READS" ]] || read_count="$(cat "$MARKER_READS")" + read_count=$((read_count + 1)) + printf '%s' "$read_count" >"$MARKER_READS" + if [[ "$read_count" -eq 1 ]]; then marker_expired=false; else marker_expired=true; fi + jq -cn --arg name "$MARKER_NAME" --argjson expired "$marker_expired" \ + '{total_count:1,artifacts:[{id:91,name:$name,expired:$expired}]}' +elif [[ "$*" == *"git/ref/heads/opencode-dispatch-leases"* ]]; then + jq -cn --arg sha "$GITHUB_SHA" '{object:{type:"commit",sha:$sha}}' +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" != *"--method PUT"* ]]; then + exit 1 +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "CALLS": str(calls), + "MARKER_READS": str(marker_reads), + "MARKER_NAME": marker_name, + "GITHUB_OUTPUT": str(tmp_path / "github-output"), + "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "DRAFT_REVIEW_ONLY": "true", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "authority changed before the atomic OpenCode lease mutation" in result.stdout + assert marker_reads.read_text(encoding="utf-8") == "2" + assert all("--method PUT" not in call for call in calls.read_text().splitlines()) + + +@pytest.mark.parametrize( + "live_override", + ( + {"state": "closed"}, + {"draft": True}, + {"base": {"ref": "release", "sha": "b" * 40, "repo": {"full_name": "owner/repo"}}}, + {"base": {"ref": "main", "sha": "d" * 40, "repo": {"full_name": "owner/repo"}}}, + {"head": {"ref": "other", "sha": HEAD, "repo": {"full_name": "owner/repo"}}}, + {"head": {"ref": "feature", "sha": "d" * 40, "repo": {"full_name": "owner/repo"}}}, + ), +) +def test_central_admission_rejects_changed_live_identity_before_contents_mutation( + tmp_path: Path, live_override: dict[str, object] +) -> None: + """Every live state/base/head mismatch fails before the central lease write.""" + live_pr: dict[str, object] = { + "state": "open", + "draft": False, + "base": { + "ref": "main", + "sha": "b" * 40, + "repo": {"full_name": "owner/repo"}, + }, + "head": { + "ref": "feature", + "sha": HEAD, + "repo": {"full_name": "owner/repo"}, + }, + } + live_pr.update(live_override) + calls = tmp_path / "calls" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + printf '%s' "$LIVE_PR" + exit 0 +fi +exit 97 +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "CALLS": str(calls), + "LIVE_PR": json.dumps(live_pr), + "GITHUB_OUTPUT": str(tmp_path / "github-output"), + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "authority changed before atomic OpenCode admission" in result.stdout + assert calls.read_text().splitlines() == ["api repos/owner/repo/pulls/7"] + + +def test_central_admission_rejects_draft_request_after_pr_becomes_ready( + tmp_path: Path, +) -> None: + """Draft-only authority cannot survive a Draft-to-Ready state transition.""" + calls = tmp_path / "calls" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$CALLS" +if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:false,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' + exit 0 +fi +exit 97 +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "CALLS": str(calls), + "GITHUB_OUTPUT": str(tmp_path / "github-output"), + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "DRAFT_REVIEW_ONLY": "true", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert "authority changed before atomic OpenCode admission" in result.stdout + assert calls.read_text().splitlines() == ["api repos/owner/repo/pulls/7"] + + +@pytest.mark.parametrize( + ( + "lease_state", + "owner_run_id", + "owner_head", + "owner_status", + "receipt_present", + "expected_admitted", + ), + ( + ("absent", 41, HEAD, "", False, True), + ("present", 41, HEAD, "in_progress", False, False), + ("present", 41, HEAD, "completed", False, True), + ("present", 41, HEAD, "completed", True, False), + ("present", 42, HEAD, "in_progress", False, True), + ("present", 42, HEAD, "in_progress", True, False), + ("present", 41, "d" * 40, "in_progress", False, True), + ("present", 43, "d" * 40, "in_progress", False, True), + ), +) +def test_central_dispatch_single_flight_uses_atomic_contents_lease( + tmp_path: Path, + lease_state: str, + owner_run_id: int, + owner_head: str, + owner_status: str, + receipt_present: bool, + expected_admitted: bool, +) -> None: + """One atomic lease owner reaches expensive work; terminal leases recover.""" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$FAKE_CALLS" +if [[ "$*" == *"git/ref/heads/opencode-dispatch-leases"* ]]; then + printf '{"object":{"type":"commit","sha":"%s"}}' "$GITHUB_SHA" +elif [[ "$*" == *"repos/owner/repo/pulls/7"* && "$*" != *"/reviews"* ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:false,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == *"opencode_review_receipt_gate.py?ref="* ]]; then + base64 <"$RECEIPT_HELPER_SOURCE" | tr -d '\n' +elif [[ "$*" == *"repos/owner/repo/pulls/7/reviews"* ]]; then + if [[ "$FAKE_RECEIPT_PRESENT" == "true" ]]; then + jq -cn --arg head "$HEAD_SHA" '[[{id:91,user:{login:"opencode-agent[bot]"},state:"CHANGES_REQUESTED",commit_id:$head,body:"## Pull request overview"}]]' + else + printf '[[]]' + fi +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" != *"--method PUT"* ]]; then + if [[ "$FAKE_LEASE_STATE" == "absent" ]]; then exit 1; fi + owner_title="OpenCode Review Dispatch owner/repo#7@${FAKE_OWNER_HEAD}" + content="$(jq -cn --argjson owner "$FAKE_OWNER_ID" --arg title "$owner_title" --arg head "$FAKE_OWNER_HEAD" \ + '{owner_run_id:$owner,exact_title:$title,head_sha:$head}')" + encoded="$(printf '%s' "$content" | base64 | tr -d '\n')" + jq -cn --arg encoded "$encoded" --arg sha "$(printf 'b%.0s' {1..40})" \ + '{sha:$sha,encoding:"base64",content:$encoded}' +elif [[ "$*" == *"actions/runs/"* ]]; then + owner_title="OpenCode Review Dispatch owner/repo#7@${FAKE_OWNER_HEAD}" + jq -cn --argjson owner "$FAKE_OWNER_ID" --arg status "$FAKE_OWNER_STATUS" --arg title "$owner_title" \ + '{id:$owner,path:".github/workflows/opencode-review-dispatch.yml",event:"repository_dispatch",display_title:$title,status:$status}' +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" == *"--method PUT"* ]]; then + jq -cn --arg sha "$(printf 'c%.0s' {1..40})" '{content:{sha:$sha}}' +else + printf 'unexpected gh call: %s\n' "$*" >&2 + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + output = tmp_path / "github-output" + calls = tmp_path / "calls" + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "FAKE_CALLS": str(calls), + "FAKE_LEASE_STATE": lease_state, + "FAKE_OWNER_ID": str(owner_run_id), + "FAKE_OWNER_HEAD": owner_head, + "FAKE_OWNER_STATUS": owner_status, + "FAKE_RECEIPT_PRESENT": str(receipt_present).lower(), + "RECEIPT_HELPER_SOURCE": str(RECEIPT_HELPER.resolve()), + "GITHUB_OUTPUT": str(output), + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "EXACT_TITLE": f"OpenCode Review Dispatch owner/repo#7@{HEAD}", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + output_lines = output.read_text(encoding="utf-8").splitlines() + assert output_lines[0] == f"admitted={str(expected_admitted).lower()}" + expected_mutation = owner_run_id != 42 and not ( + owner_head == HEAD and owner_status == "in_progress" + ) + assert any("--method PUT" in call for call in calls.read_text().splitlines()) is expected_mutation + + +def test_atomic_contents_lease_admits_one_concurrent_receiver(tmp_path: Path) -> None: + """Two simultaneous cache misses converge on one compare-and-swap owner.""" + fake_gh = tmp_path / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == *"git/ref/heads/opencode-dispatch-leases"* ]]; then + printf '{"object":{"type":"commit","sha":"%s"}}' "$GITHUB_SHA" +elif [[ "$*" == *"repos/owner/repo/pulls/7"* && "$*" != *"/reviews"* ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:false,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == *"opencode_review_receipt_gate.py?ref="* ]]; then + base64 <"$RECEIPT_HELPER_SOURCE" | tr -d '\n' +elif [[ "$*" == *"repos/owner/repo/pulls/7/reviews"* ]]; then + printf '[[]]' +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" != *"--method PUT"* ]]; then + [[ -f "$LEASE_OWNER" ]] || exit 1 + owner="$(cat "$LEASE_OWNER")" + content="$(jq -cn --argjson owner "$owner" --arg title "$EXACT_TITLE" --arg head "$HEAD_SHA" \ + '{owner_run_id:$owner,exact_title:$title,head_sha:$head}')" + encoded="$(printf '%s' "$content" | base64 | tr -d '\n')" + jq -cn --arg encoded "$encoded" --arg sha "$(printf 'd%.0s' {1..40})" \ + '{sha:$sha,encoding:"base64",content:$encoded}' +elif [[ "$*" == *"actions/runs/"* ]]; then + [[ "$*" =~ actions/runs/([0-9]+) ]] || exit 96 + owner="${BASH_REMATCH[1]}" + jq -cn --argjson owner "$owner" --arg title "$EXACT_TITLE" \ + '{id:$owner,path:".github/workflows/opencode-review-dispatch.yml",event:"repository_dispatch",display_title:$title,status:"in_progress"}' +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" == *"--method PUT"* ]]; then + payload="$(cat)" + if mkdir "$LEASE_MUTEX" 2>/dev/null; then + printf '%s' "$payload" | jq -r '.content' | base64 --decode | jq -r '.owner_run_id' >"$LEASE_OWNER" + jq -cn --arg sha "$(printf 'e%.0s' {1..40})" '{content:{sha:$sha}}' + else + sleep 0.05 + exit 1 + fi +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + base_env = { + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "EXACT_TITLE": f"OpenCode Review Dispatch owner/repo#7@{HEAD}", + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + "RECEIPT_HELPER_SOURCE": str(RECEIPT_HELPER.resolve()), + "LEASE_MUTEX": str(tmp_path / "lease-mutex"), + "LEASE_OWNER": str(tmp_path / "lease-owner"), + } + processes: list[tuple[subprocess.Popen[str], Path]] = [] + for run_id in (41, 42): + output = tmp_path / f"output-{run_id}" + process = subprocess.Popen( # noqa: S603 + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **base_env, + "GITHUB_RUN_ID": str(run_id), + "GITHUB_OUTPUT": str(output), + }, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + processes.append((process, output)) + admissions: list[str] = [] + for process, output in processes: + stdout, stderr = process.communicate(timeout=10) + assert process.returncode == 0, f"{stdout}\n{stderr}" + admissions.append(output.read_text(encoding="utf-8").splitlines()[0]) + assert sorted(admissions) == ["admitted=false", "admitted=true"] + + +def test_atomic_lease_branch_initialization_accepts_a_concurrent_creator( + tmp_path: Path, +) -> None: + """A ref-create conflict must re-read the exact central lease branch.""" + fake_gh = tmp_path / "gh" + ref_reads = tmp_path / "ref-reads" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == *"git/ref/heads/opencode-dispatch-leases"* ]]; then + reads=0 + [[ ! -f "$REF_READS" ]] || reads="$(cat "$REF_READS")" + reads=$((reads + 1)) + printf '%s' "$reads" >"$REF_READS" + [[ "$reads" -gt 1 ]] || exit 1 + printf '{"object":{"type":"commit","sha":"%s"}}' "$GITHUB_SHA" +elif [[ "$*" == *"repos/owner/repo/pulls/7"* && "$*" != *"/reviews"* ]]; then + jq -cn --arg base "$SUPPLIED_BASE_SHA" --arg head "$HEAD_SHA" \ + '{state:"open",draft:false,base:{ref:"main",sha:$base,repo:{full_name:"owner/repo"}},head:{ref:"feature",sha:$head,repo:{full_name:"owner/repo"}}}' +elif [[ "$*" == *"opencode_review_receipt_gate.py?ref="* ]]; then + base64 <"$RECEIPT_HELPER_SOURCE" | tr -d '\n' +elif [[ "$*" == *"repos/owner/repo/pulls/7/reviews"* ]]; then + printf '[[]]' +elif [[ "$*" == *"git/refs"* && "$*" == *"--method POST"* ]]; then + cat >/dev/null + exit 1 +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" != *"--method PUT"* ]]; then + exit 1 +elif [[ "$*" == *"contents/opencode-dispatch-leases/"* && "$*" == *"--method PUT"* ]]; then + cat >/dev/null + jq -cn --arg sha "$(printf 'f%.0s' {1..40})" '{content:{sha:$sha}}' +else + exit 97 +fi +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + output = tmp_path / "github-output" + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", central_single_flight_script()], + env={ + **os.environ, + "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", + "REF_READS": str(ref_reads), + "GITHUB_OUTPUT": str(output), + "GITHUB_RUN_ID": "42", + "GITHUB_SHA": "a" * 40, + "TARGET_REPOSITORY": "owner/repo", + "PR_NUMBER": "7", + "HEAD_SHA": HEAD, + "SUPPLIED_BASE_REF": "main", + "SUPPLIED_BASE_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": HEAD, + "GH_TOKEN": "token", + "TARGET_READ_TOKEN": "target-token", + "RECEIPT_HELPER_SOURCE": str(RECEIPT_HELPER.resolve()), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert output.read_text(encoding="utf-8").splitlines() == ["admitted=true"] + assert ref_reads.read_text(encoding="utf-8") == "2" def review(*, state: str, commit_id: str = HEAD, body: str = "") -> dict[str, object]: @@ -104,6 +785,22 @@ def review(*, state: str, commit_id: str = HEAD, body: str = "") -> dict[str, ob } +def active_dispatch( + *, status: str, head_sha: str = HEAD, run_id: int = 42 +) -> dict[str, object]: + """Build one complete central OpenCode workflow-run identity.""" + return { + "id": run_id, + "name": "OpenCode Review Dispatch", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{head_sha}", + "path": ".github/workflows/opencode-review-dispatch.yml", + "event": "repository_dispatch", + "status": status, + "head_sha": "c" * 40, + "pull_requests": [], + } + + def runtime_verdict(reviews: list[dict[str, object]], head_sha: str = HEAD) -> str: """Execute the jq program embedded in the required workflow.""" jq = shutil.which("jq") @@ -311,6 +1008,8 @@ def test_required_workflow_cannot_succeed_with_an_echo_only_placeholder() -> Non assert "id-token: write" in target_job.split(" steps:\n", 1)[0] assert 'event_type:"opencode-review"' in workflow assert "required_run_id:$required_run_id" in workflow + assert "--argjson draft_review_only false" in workflow + assert "draft_review_only:$draft_review_only" in workflow dispatch_step = target_job.split( " - name: Request current-head OpenCode review execution", 1 )[1].split(" - name: Fail closed", 1)[0] @@ -669,19 +1368,70 @@ def test_fail_closed_step_checks_once_for_a_non_draft_pr(tmp_path: Path) -> None @pytest.mark.parametrize( - ("reviews", "dispatches"), ( - ([{"id": 7, **review(state="APPROVED", body="## Verdict\nApprove")}], 0), - ([{"id": 8, **review(state="CHANGES_REQUESTED", body="## Verdict\nRequest changes")}], 0), - ([], 1), - ([{"id": 9, **review(state="APPROVED", commit_id="b" * 40, body="## Verdict\nApprove")}], 1), - ([{"id": 10, **review(state="APPROVED", body="## Pull request overview\n\ndeterministic fallback approval")}], 1), + "reviews", + "active_runs", + "later_active_runs", + "revalidated_head", + "lookup_failure", + "expected_returncode", + "dispatches", + ), + ( + ([{"id": 7, **review(state="APPROVED", body="## Verdict\nApprove")}], [], None, HEAD, False, 0, 0), + ([{"id": 8, **review(state="CHANGES_REQUESTED", body="## Verdict\nRequest changes")}], [], None, HEAD, False, 0, 0), + ([], [], None, HEAD, False, 0, 1), + ([], [], None, "e" * 40, False, 0, 0), + ([], [active_dispatch(status="queued")], None, HEAD, False, 0, 0), + ([], [active_dispatch(status="in_progress")], None, HEAD, False, 0, 0), + ([], [active_dispatch(status="requested")], None, HEAD, False, 0, 0), + ([], [active_dispatch(status="waiting")], None, HEAD, False, 0, 0), + ([], [active_dispatch(status="pending")], None, HEAD, False, 0, 0), + ( + [], + [ + active_dispatch(status="queued"), + active_dispatch(status="queued", head_sha="d" * 40, run_id=48), + ], + None, + HEAD, + False, + 0, + 0, + ), + ([], [active_dispatch(status="queued", head_sha="d" * 40)], None, HEAD, False, 0, 1), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=45)], None, HEAD, False, 1, 0), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=46)], None, HEAD, False, 1, 0), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=47)], None, HEAD, False, 1, 0), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=49)], None, HEAD, False, 0, 1), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=50)], None, HEAD, False, 1, 0), + ([], [active_dispatch(status="queued", head_sha="d" * 40, run_id=51)], None, "e" * 40, False, 0, 0), + ([], [], [active_dispatch(status="pending")], HEAD, False, 0, 0), + ( + [], + [{"id": 44, "path": ".github/workflows/opencode-review-dispatch.yml", "event": "repository_dispatch", "status": "queued"}], + None, + HEAD, + False, + 1, + 0, + ), + ([], [], None, HEAD, True, 1, 0), + ([{"id": 9, **review(state="APPROVED", commit_id="b" * 40, body="## Verdict\nApprove")}], [], None, HEAD, False, 0, 1), + ([{"id": 10, **review(state="APPROVED", body="## Pull request overview\n\ndeterministic fallback approval")}], [], None, HEAD, False, 0, 1), ), ) def test_scheduler_wake_reuses_trusted_receipt_predicate( - tmp_path: Path, reviews: list[dict[str, object]], dispatches: int + tmp_path: Path, + reviews: list[dict[str, object]], + active_runs: list[dict[str, object]], + later_active_runs: list[dict[str, object]] | None, + revalidated_head: str, + lookup_failure: bool, + expected_returncode: int, + dispatches: int, ) -> None: - """Only missing, stale, or fallback-only evidence wakes the scheduler.""" + """Only missing, stale, inactive evidence wakes one exact-head execution.""" fake_bin = tmp_path / "bin" fake_bin.mkdir() calls = tmp_path / "dispatches" @@ -690,11 +1440,61 @@ def test_scheduler_wake_reuses_trusted_receipt_predicate( """#!/usr/bin/env bash set -euo pipefail if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then - printf '%s' "$LIVE_PR_JSON" + count=0 + [[ ! -f "$LIVE_PR_CALLS" ]] || count="$(cat "$LIVE_PR_CALLS")" + count=$((count + 1)) + printf '%s' "$count" >"$LIVE_PR_CALLS" + if [[ "$count" -eq 1 ]]; then + printf '%s' "$LIVE_PR_JSON" + else + printf '%s' "$REVALIDATED_LIVE_PR_JSON" + fi elif [[ "$*" == *"contents/scripts/ci/opencode_review_receipt_gate.py"* ]]; then python3 -c 'import base64, pathlib, sys; sys.stdout.write(base64.b64encode(pathlib.Path(sys.argv[1]).read_bytes()).decode())' "$REAL_RECEIPT_HELPER" elif [[ "$*" == *"/pulls/7/reviews"* ]]; then printf '[%s]' "$FAKE_REVIEWS" +elif [[ "$*" == *"actions/workflows/opencode-review-dispatch.yml/runs"* ]]; then + if [[ "$FAKE_ACTIVE_LOOKUP_FAILURE" == "true" ]]; then + exit 19 + fi + count=0 + [[ ! -f "$ACTIVE_RUN_CALLS" ]] || count="$(cat "$ACTIVE_RUN_CALLS")" + count=$((count + 1)) + printf '%s' "$count" >"$ACTIVE_RUN_CALLS" + runs_request="${!#}" + active_status="${runs_request#*status=}" + active_status="${active_status%%&*}" + if [[ "$count" -le 5 ]]; then + active_source="$FAKE_ACTIVE_RUNS" + else + active_source="$FAKE_LATER_ACTIVE_RUNS" + fi + active_selection="$(jq -c --arg status "$active_status" '[.[] | select(.status == $status)]' <<<"$active_source")" + printf '{"workflow_runs":%s}' "$active_selection" +elif [[ "$*" == *"repos/ContextualWisdomLab/.github/actions/runs/"*"/cancel"* ]]; then + [[ "$*" =~ actions/runs/([0-9]+) ]] || exit 90 + run_id="${BASH_REMATCH[1]}" + printf '%s\n' "$run_id" >>"$CANCEL_CALLS" + [[ "$run_id" != "45" ]] || exit 19 +elif [[ "$*" == *"repos/ContextualWisdomLab/.github/actions/runs/"* ]]; then + [[ "$*" =~ actions/runs/([0-9]+) ]] || exit 91 + run_id="${BASH_REMATCH[1]}" + count_file="$RUN_STATE_CALLS/$run_id" + count=0 + [[ ! -f "$count_file" ]] || count="$(cat "$count_file")" + count=$((count + 1)) + printf '%s' "$count" >"$count_file" + if [[ "$run_id" == "46" ]]; then + printf 'mystery\t' + elif [[ "$run_id" == "47" ]]; then + printf 'queued\t' + elif [[ "$run_id" == "49" && "$count" -eq 1 ]]; then + printf 'in_progress\t' + elif [[ "$run_id" == "50" ]]; then + printf 'completed\tsuccess' + else + printf 'completed\tcancelled' + fi elif [[ "$*" == *"repos/ContextualWisdomLab/.github/dispatches"* ]]; then cat >/dev/null printf 'dispatch\n' >>"$DISPATCH_CALLS" @@ -711,12 +1511,25 @@ def test_scheduler_wake_reuses_trusted_receipt_predicate( encoding="utf-8", ) fake_curl.chmod(0o755) + fake_sleep = fake_bin / "sleep" + fake_sleep.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8") + fake_sleep.chmod(0o755) + run_state_calls = tmp_path / "run-state-calls" + run_state_calls.mkdir() env = { **os.environ, "PATH": f"{fake_bin}{os.pathsep}{os.environ['PATH']}", "REAL_RECEIPT_HELPER": str(RECEIPT_HELPER.resolve()), "FAKE_REVIEWS": json.dumps(reviews), + "FAKE_ACTIVE_RUNS": json.dumps(active_runs), + "FAKE_LATER_ACTIVE_RUNS": json.dumps( + active_runs if later_active_runs is None else later_active_runs + ), + "FAKE_ACTIVE_LOOKUP_FAILURE": str(lookup_failure).lower(), + "ACTIVE_RUN_CALLS": str(tmp_path / "active-run-calls"), + "CANCEL_CALLS": str(tmp_path / "cancel-calls"), "DISPATCH_CALLS": str(calls), + "RUN_STATE_CALLS": str(run_state_calls), "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "request", "ACTIONS_ID_TOKEN_REQUEST_URL": "https://token.example", "OIDC_AUDIENCE": "opencode-github-action", @@ -734,13 +1547,40 @@ def test_scheduler_wake_reuses_trusted_receipt_predicate( "LIVE_PR_JSON": json.dumps( {"draft": False, "head": {"sha": HEAD}, "state": "open"} ), + "REVALIDATED_LIVE_PR_JSON": json.dumps( + {"draft": False, "head": {"sha": revalidated_head}, "state": "open"} + ), + "LIVE_PR_CALLS": str(tmp_path / "live-pr-calls"), } result = subprocess.run( ["bash", "-c", request_review_script()], env=env, text=True, capture_output=True ) - assert result.returncode == 0, result.stderr + assert result.returncode == expected_returncode, result.stderr actual = calls.read_text(encoding="utf-8").count("dispatch") if calls.exists() else 0 assert actual == dispatches + cancel_calls = tmp_path / "cancel-calls" + actual_cancel_ids = ( + sorted(set(cancel_calls.read_text(encoding="utf-8").splitlines())) + if cancel_calls.exists() + else [] + ) + expected_cancel_ids = ( + [] + if revalidated_head != HEAD + else sorted( + { + str(run["id"]) + for run in [*active_runs, *(later_active_runs or [])] + if isinstance(run.get("display_title"), str) + and str(run["display_title"]).startswith( + "OpenCode Review Dispatch owner/repo#7@" + ) + and str(run["display_title"]).lower() + != f"OpenCode Review Dispatch owner/repo#7@{HEAD}".lower() + } + ) + ) + assert actual_cancel_ids == expected_cancel_ids def test_formal_receipt_wake_reruns_the_immediately_failed_required_job() -> None: @@ -754,13 +1594,14 @@ def test_formal_receipt_wake_reruns_the_immediately_failed_required_job() -> Non assert "rerun-failed-jobs" in dispatched assert "id: formal_review_receipt" in dispatched assert "steps.formal_review_receipt.outcome == 'success'" in dispatched - assert "github.event.client_payload.required_run_id != ''" in dispatched - assert 'gh api "repos/${GH_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in dispatched - assert "select(.id == $run_id)" in dispatched - assert 'select(.event == "pull_request_target")' in dispatched - assert 'select(.path == ".github/workflows/opencode-review.yml")' in dispatched - assert "select(.head_sha == $head)" in dispatched - wake_step = dispatched.split("Wake exact-head required OpenCode workflow", 1)[1].split("\n\n - name:", 1)[0] + assert "actions/runs?event=pull_request_target" in dispatched + assert '.event == "pull_request_target"' in dispatched + assert '.path == ".github/workflows/opencode-review.yml"' in dispatched + assert "(.head.sha | ascii_downcase) == ($head | ascii_downcase)" in dispatched + wake_step = dispatched.split( + "Wake every failed exact-head Required OpenCode workflow", 1 + )[1].split("\n\n - name:", 1)[0] + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in wake_step target_job = dispatched.split(" opencode-review-target:\n", 1)[1] target_permissions = target_job.split(" env:\n", 1)[0] assert "actions: write" in target_permissions @@ -771,32 +1612,81 @@ def test_formal_receipt_wake_reruns_the_immediately_failed_required_job() -> Non assert "steps.opencode_app_token.outputs.token" not in wake_step assert "WAKE_TOKEN_SOURCE" in wake_step assert '"$WAKE_TOKEN_SOURCE" = "unavailable"' in wake_step - assert "--paginate" not in wake_step - # Identity is the immutable target-repository run id plus event/path/head; - # do not depend on context-specific title or workflow_url rendering. + assert "--paginate" in wake_step + # Inventory identity is event/path/head/PR; do not depend on context-specific + # title or workflow_url rendering. + assert ".number == $pr" in wake_step assert "display_title ==" not in wake_step assert ".name | startswith(" not in wake_step assert 'workflow_url | contains("/actions/required_workflows/")' not in wake_step -def wake_selector(run: dict[str, object], *, head: str = HEAD, run_id: int = 42) -> str: - """Execute the wake step's run-validation jq program in isolation.""" +def test_authorized_draft_review_cannot_publish_approval_or_run_merge_followups() -> None: + """Review-only Draft work publishes prose but cannot create approval authority.""" + dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") + fast_approval = dispatched.split( + " - name: Publish central OpenCode fast approval", 1 + )[1].split(" - name: Publish OpenCode review outcome", 1)[0] + publication = dispatched.split( + " - name: Publish OpenCode review outcome", 1 + )[1].split(" - name: Enforce current-head formal OpenCode review receipt", 1)[0] + formal_receipt = dispatched.split( + " - name: Enforce current-head formal OpenCode review receipt", 1 + )[1].split(" - name: Wake every failed exact-head Required OpenCode workflow", 1)[0] + status_publication = dispatched.split( + " - name: Publish repository_dispatch OpenCode status", 1 + )[1].split(" - name: Dispatch Noema after current-head OpenCode approval", 1)[0] + noema_handoff = dispatched.split( + " - name: Dispatch Noema after current-head OpenCode approval", 1 + )[1].split(" - name: Run merge scheduler after approval", 1)[0] + merge_followup = dispatched.split( + " - name: Run merge scheduler after approval", 1 + )[1] + + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in fast_approval + assert "PR_DRAFT: ${{ needs.validate-pr-metadata.outputs.is_draft }}" in publication + assert ( + 'if [ "$event" = "APPROVE" ] && [ "$PR_DRAFT" = "true" ]; then' + in publication + ) + assert 'event="COMMENT"' in publication + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in formal_receipt + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in status_publication + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in noema_handoff + assert "needs.validate-pr-metadata.outputs.is_draft == 'false'" in merge_followup + + +def test_dispatch_preserves_draft_review_authority_type_before_validation() -> None: + """Falsy non-booleans cannot be normalized into Ready review authority.""" + dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") + raw_binding = "toJSON(github.event.client_payload.draft_review_only)" + assert dispatched.count(raw_binding) == 3 + assert "draft_review_only || false" not in dispatched + + +def wake_selector(run: dict[str, object], *, head: str = HEAD) -> str: + """Execute the wake inventory's fail-closed jq program in isolation.""" jq = shutil.which("jq") if jq is None: pytest.skip("jq is required to execute the production wake selector") dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") - marker = """jq -r --arg head "$PR_HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" '""" + marker = """jq -c -s --arg head "$PR_HEAD_SHA" --argjson pr "$PR_NUMBER" '""" start = dispatched.index(marker) + len(marker) - end = dispatched.index("\n ')", start) + end = dispatched.index("\n ' <<<\"$run_pages\")", start) result = subprocess.run( - [jq, "-r", "--arg", "head", head, "--argjson", "run_id", str(run_id), dispatched[start:end]], - input=json.dumps(run), + [jq, "-c", "-s", "--arg", "head", head, "--argjson", "pr", "7", dispatched[start:end]], + input=json.dumps({"workflow_runs": [run]}), text=True, capture_output=True, check=False, ) - assert result.returncode == 0, result.stderr - return result.stdout.strip() + if result.returncode != 0: + return "" + inventory = json.loads(result.stdout) + if not inventory: + return "" + selected = inventory[0] + return f"{selected['id']}\t{selected['status']}\t{selected['conclusion']}" def required_run(*, run_id: int = 42, head_sha: str = HEAD, path: str = ".github/workflows/opencode-review.yml") -> dict[str, object]: @@ -810,7 +1700,9 @@ def required_run(*, run_id: int = 42, head_sha: str = HEAD, path: str = ".github """ return { "id": run_id, - "head_sha": head_sha, + # pull_request_target executes the trusted default-branch workflow, so + # run-level head_sha is not the pull request head. + "head_sha": "c" * 40, "event": "pull_request_target", "name": "Required OpenCode Review", "display_title": "Fix an unrelated example bug", @@ -821,11 +1713,12 @@ def required_run(*, run_id: int = 42, head_sha: str = HEAD, path: str = ".github ), "status": "completed", "conclusion": "failure", + "pull_requests": [{"number": 7, "head": {"sha": head_sha}}], } def test_wake_selector_matches_the_referenced_run_without_name_or_display_title() -> None: - """The exact-id, exact-head run is matched using only id/event/path/head_sha.""" + """An exact-head run is matched using only id/event/path/head_sha.""" assert wake_selector(required_run()) == "42\tcompleted\tfailure" @@ -847,7 +1740,9 @@ def test_wake_selector_rejects_a_referenced_run_for_a_different_workflow() -> No def test_formal_receipt_wakes_the_exact_head_failed_required_run(tmp_path: Path) -> None: """Execute the production wake script end-to-end against a fake GitHub API.""" dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") - step = dispatched.split(" - name: Wake exact-head required OpenCode workflow\n", 1)[1] + step = dispatched.split( + " - name: Wake every failed exact-head Required OpenCode workflow\n", 1 + )[1] run_block = step.split(" run: |\n", 1)[1].split("\n\n - name:", 1)[0] script = textwrap.dedent(run_block) calls = tmp_path / "calls" @@ -856,8 +1751,15 @@ def test_formal_receipt_wakes_the_exact_head_failed_required_run(tmp_path: Path) f"""#!/usr/bin/env bash set -euo pipefail printf '%s\\n' "$*" >>"$FAKE_CALLS" +if [[ "$*" == "api repos/ContextualWisdomLab/example/pulls/7" ]]; then + printf '%s' '{{"state":"open","draft":false,"created_at":"2026-09-30T00:00:00Z","head":{{"sha":"{HEAD}"}}}}' + exit 0 +fi if [[ "$*" == *"actions/runs/42/rerun-failed-jobs"* ]]; then exit 0; fi -if [[ "$*" == *"actions/runs/42"* ]]; then printf '%s\\n' '{json.dumps(required_run())}'; exit 0; fi +if [[ "$*" == *"actions/runs?event=pull_request_target"* ]]; then + printf '%s\\n' '{{"total_count":1,"workflow_runs":[{json.dumps(required_run())}]}}' + exit 0 +fi exit 1 """, encoding="utf-8", @@ -870,9 +1772,9 @@ def test_formal_receipt_wakes_the_exact_head_failed_required_run(tmp_path: Path) "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", "FAKE_CALLS": str(calls), "GH_REPOSITORY": "ContextualWisdomLab/example", + "PR_NUMBER": "7", "GH_TOKEN": "actions-write-token", "PR_HEAD_SHA": HEAD, - "REQUIRED_RUN_ID": "42", "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", }, capture_output=True, @@ -882,14 +1784,16 @@ def test_formal_receipt_wakes_the_exact_head_failed_required_run(tmp_path: Path) assert result.returncode == 0, result.stderr recorded = calls.read_text(encoding="utf-8") assert "actions/runs/42/rerun-failed-jobs" in recorded - assert "repos/ContextualWisdomLab/example/actions/runs/42" in recorded - assert "--paginate" not in recorded + assert "actions/runs?event=pull_request_target" in recorded + assert "--paginate" in recorded def test_sibling_formal_receipt_fails_closed_without_actions_token() -> None: """A sibling wake without either Actions-capable PAT fails before GitHub I/O.""" dispatched = DISPATCH_WORKFLOW.read_text(encoding="utf-8") - step = dispatched.split(" - name: Wake exact-head required OpenCode workflow\n", 1)[1] + step = dispatched.split( + " - name: Wake every failed exact-head Required OpenCode workflow\n", 1 + )[1] script = textwrap.dedent( step.split(" run: |\n", 1)[1].split("\n\n - name:", 1)[0] ) @@ -900,7 +1804,6 @@ def test_sibling_formal_receipt_fails_closed_without_actions_token() -> None: "GH_TOKEN": "", "GH_REPOSITORY": "ContextualWisdomLab/example", "PR_HEAD_SHA": HEAD, - "REQUIRED_RUN_ID": "42", "WAKE_TOKEN_SOURCE": "unavailable", }, capture_output=True, diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 2f968654bb..3c7abbff78 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -507,6 +507,7 @@ def test_dispatch_status_requires_live_current_head_approval_and_coverage( decision = dispatch_status.decide_status( model_outcome="success", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=[review], @@ -529,6 +530,7 @@ def test_dispatch_status_latest_current_head_decision_is_authoritative( decision = dispatch_status.decide_status( model_outcome="success", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=reviews, @@ -546,6 +548,7 @@ def test_dispatch_status_reuses_verified_approval_after_current_pool_exhaustion( decision = dispatch_status.decide_status( model_outcome="exhausted", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=[approval_review(head)], @@ -579,6 +582,7 @@ def test_dispatch_status_fails_closed_without_validated_approval( decision = dispatch_status.decide_status( model_outcome=model_outcome, coverage_result=coverage_result, + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": observed_head}}, reviews=[approval_review(head, **review_overrides)], @@ -588,6 +592,55 @@ def test_dispatch_status_fails_closed_without_validated_approval( assert decision["description"] +@pytest.mark.parametrize( + "coverage_summary", + ("- Result: NOT MEASURED", "", "- Result: PASS\n- Result: PASS"), +) +def test_dispatch_status_rejects_nonpassing_coverage_summary( + coverage_summary: str, + trusted_dispatch_status_artifacts: None, +) -> None: + """A successful advisory job cannot replace a unique PASS decision.""" + head = "a" * 40 + decision = dispatch_status.decide_status( + model_outcome="success", + coverage_result="success", + coverage_summary=coverage_summary, + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[approval_review(head)], + ) + + assert decision["state"] == "failure" + assert "coverage decision" in decision["description"].lower() + + +@pytest.mark.parametrize( + "coverage_summary", + ( + "", + "- Result: NOT MEASURED", + "- Result: PASS\n- Result: PASS", + "- Result: PASS\n- Result: FAIL", + ), +) +def test_dispatch_status_rejects_non_pass_coverage_decisions( + coverage_summary: str, + trusted_dispatch_status_artifacts: None, +) -> None: + """Job success cannot publish success without one exact PASS decision.""" + head = "a" * 40 + decision = dispatch_status.decide_status( + model_outcome="exhausted", + coverage_result="success", + coverage_summary=coverage_summary, + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[approval_review(head)], + ) + assert decision["state"] == "failure" + + def test_dispatch_status_cli_and_evidence_shape_validation( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, @@ -605,6 +658,8 @@ def test_dispatch_status_cli_and_evidence_shape_validation( "success", "--coverage-result", "success", + "--coverage-summary", + "- Result: PASS", "--expected-head", head, "--pull-request-file", diff --git a/tests/test_opencode_workflow_shell_syntax.py b/tests/test_opencode_workflow_shell_syntax.py index 3e30633eb7..b18b207ca4 100644 --- a/tests/test_opencode_workflow_shell_syntax.py +++ b/tests/test_opencode_workflow_shell_syntax.py @@ -262,6 +262,7 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): "DEFAULT_BRANCH": "main", "TARGET_REPOSITORY_INPUT": "ContextualWisdomLab/naruon", "TARGET_PR_NUMBER": "1179", + "TARGET_HEAD_SHA_INPUT": "4afd4af7ad343660356791873d940aa2846f40c2", "TARGET_BASE_BRANCH_INPUT": "develop", "ALLOWED_TARGET_REPOSITORIES": ( "ContextualWisdomLab/.github, ContextualWisdomLab/naruon" @@ -302,6 +303,22 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): assert "absent from the configured exact allowlist" in rejected.stdout assert not output.exists() + stale_head_env = { + **env, + "TARGET_HEAD_SHA_INPUT": "a" * 40, + } + stale_head = subprocess.run( + [bash], + input=script, + text=True, + capture_output=True, + check=False, + env=stale_head_env, + ) + assert stale_head.returncode == 1 + assert "head does not match the live PR" in stale_head.stdout + assert not output.exists() + output.unlink(missing_ok=True) cross_repo_pull = { **pull, diff --git a/tests/test_org_required_workflow_scope_contract.py b/tests/test_org_required_workflow_scope_contract.py index cdfd2b2c42..b5a731467d 100644 --- a/tests/test_org_required_workflow_scope_contract.py +++ b/tests/test_org_required_workflow_scope_contract.py @@ -20,3 +20,24 @@ def test_doctoring_records_documentation_gate_closed() -> None: doctoring = Path("docs/doctoring/code-scanning-required-workflow-audit.md").read_text(encoding="utf-8") assert "## Documentation reconciliation" in doctoring assert "## Outstanding documentation gate" not in doctoring + + +def test_unfiltered_triggers_do_not_overclaim_stacked_ruleset_coverage() -> None: + """Workflow triggers must not be presented as widening ruleset ref scope.""" + rollout = Path("docs/org-required-workflow-rollout.md").read_text(encoding="utf-8") + workflows = { + name: Path(f".github/workflows/{name}").read_text(encoding="utf-8") + for name in ("security-scan.yml", "sast-semgrep.yml", "codeql-pr.yml") + } + rollout_words = " ".join(rollout.split()) + + assert "They therefore also run for stacked pull requests" not in rollout + assert "does not widen ruleset `18156473`" in rollout + assert "do not materialize these required workflows" in rollout_words + + assert "stacked PRs must receive the same" not in workflows["security-scan.yml"] + assert "Scan every PR base ref" not in workflows["sast-semgrep.yml"] + assert "would also block coverage for\n # stacked PRs" not in workflows["codeql-pr.yml"] + for workflow in workflows.values(): + workflow_words = " ".join(workflow.replace("#", "").split()) + assert "does not widen ruleset 18156473" in workflow_words diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 1bfc13292e..e01ae2a5a6 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "09bbf8181a443f7a5630ec91ca958440c5dcfc68" +REVIEW_DISPATCH_BLOB_SHA = "f416c71e3f9961ad82754324c90a3c62c2c5da92" def _workflow_text(path: Path) -> str: diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index abe5a411c7..a277025923 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -1894,6 +1894,259 @@ def test_cancel_stale_opencode_runs_dry_run_skips_lookup_and_mutation(monkeypatc assert calls == [] +def test_central_dispatch_cleanup_cancels_stale_and_closed_but_preserves_current(monkeypatch): + """Known central dispatches are retired only after fresh run and PR authority.""" + old_head = "a" * 40 + live_head = "b" * 40 + closed_head = "c" * 40 + runs = [ + { + "id": 101, + "status": "queued", + "event": "repository_dispatch", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{old_head}", + }, + { + "id": 102, + "status": "in_progress", + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "display_title": f"CodeQL Scan Dispatch owner/repo#7@{live_head}/base/required/source", + }, + { + "id": 103, + "status": "queued", + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "display_title": f"CodeQL Scan Dispatch owner/repo#8@{closed_head}/base/required/source", + }, + { + "id": 104, + "status": "queued", + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "display_title": "CodeQL Scan Dispatch owner/repo#7@not-a-sha/base/required/source", + }, + { + "id": 109, + "status": "waiting", + "event": "repository_dispatch", + "path": ".github/workflows/strix.yml", + "display_title": f"Strix Security Scan owner/repo#7@{old_head}", + }, + ] + monkeypatch.setattr(sched, "repository_dispatch_target", lambda _repo: "ContextualWisdomLab/.github") + inventory_calls = [] + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: inventory_calls.append((_args, _kwargs)) or runs, + ) + monkeypatch.setattr( + sched, + "_fresh_active_run_for_cancellation", + lambda _repo, run_id: next(run for run in runs if str(run["id"]) == run_id), + ) + + def fresh_pr(_repo, number): + if number == 7: + return {"state": "open", "draft": True, "head": {"sha": live_head}} + return {"state": "closed", "draft": False, "head": {"sha": closed_head}} + + monkeypatch.setattr(sched, "gh_api_json", lambda path: fresh_pr("owner/repo", int(path.rsplit("/", 1)[1]))) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + cancelled = [] + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda repo, run_ids: cancelled.append((repo, list(run_ids))) or {}, + ) + + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=False + ) == ["101", "109"] + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=8), dry_run=False + ) == ["103"] + assert cancelled == [ + ("ContextualWisdomLab/.github", ["101"]), + ("ContextualWisdomLab/.github", ["109"]), + ("ContextualWisdomLab/.github", ["103"]), + ] + assert all( + call_args[1] == ("queued", "in_progress", "waiting", "pending", "requested") + for call_args, _kwargs in inventory_calls + ) + + +def test_central_dispatch_cleanup_fails_closed_when_live_pr_is_unreadable(monkeypatch, capsys): + """A transient authority read failure never authorizes central-run cancellation.""" + old_head = "a" * 40 + run = { + "id": 105, + "status": "queued", + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "display_title": f"CodeQL Scan Dispatch owner/repo#7@{old_head}/base/required/source", + } + monkeypatch.setattr(sched, "repository_dispatch_target", lambda _repo: "ContextualWisdomLab/.github") + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_args, **_kwargs: [run]) + monkeypatch.setattr(sched, "_fresh_active_run_for_cancellation", lambda *_args: run) + monkeypatch.setattr( + sched, "gh_api_json", lambda _path: (_ for _ in ()).throw(RuntimeError("outage")) + ) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + cancelled = [] + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda repo, run_ids: cancelled.append((repo, list(run_ids))) or {}, + ) + + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=False + ) == [] + assert cancelled == [] + assert "live central-run revalidation failed closed" in capsys.readouterr().out + + +@pytest.mark.parametrize( + "run_data", + ( + { + "event": "pull_request_target", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}", + }, + { + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "display_title": f"CodeQL Scan Dispatch owner/repo#7@{'a' * 40}", + }, + { + "event": "repository_dispatch", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}/extra", + }, + { + "event": "repository_dispatch", + "path": ".github/workflows/unrelated.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}", + }, + ), +) +def test_central_dispatch_target_rejects_untrusted_title_shapes(run_data): + """Only exact protected repository_dispatch workflow identities are trusted.""" + assert sched.central_dispatch_run_target(run_data, "owner/repo") is None + + +@pytest.mark.parametrize( + ("run", "live_pr"), + ( + ( + { + "event": "repository_dispatch", + "status": "queued", + "path": ".github/workflows/unrelated.yml", + "display_title": "unrelated", + }, + {"state": "open", "head": {"sha": "b" * 40}}, + ), + ( + { + "event": "repository_dispatch", + "status": "queued", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#8@{'a' * 40}", + }, + {"state": "open", "head": {"sha": "b" * 40}}, + ), + ( + { + "event": "repository_dispatch", + "status": "queued", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}", + }, + [], + ), + ( + { + "event": "repository_dispatch", + "status": "queued", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}", + }, + {"state": "unknown", "head": {"sha": "b" * 40}}, + ), + ), +) +def test_central_dispatch_revalidation_preserves_conflicting_authority( + monkeypatch, capsys, run, live_pr +): + """Malformed, retargeted, and nonauthoritative fresh state all fail closed.""" + monkeypatch.setattr(sched, "_fresh_active_run_for_cancellation", lambda *_args: run) + monkeypatch.setattr(sched, "gh_api_json", lambda _path: live_pr) + + assert not sched._central_dispatch_run_still_stale( + "owner/repo", "ContextualWisdomLab/.github", "106", 7 + ) + assert "live central-run revalidation failed closed" in capsys.readouterr().out + + +def test_central_dispatch_cleanup_dry_run_and_empty_or_filtered_inventory(monkeypatch): + """Dry-run performs no lookup; empty and other-PR inventories require no actor.""" + calls = [] + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda *_args, **_kwargs: calls.append("lookup") or [], + ) + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=True + ) == [] + assert calls == [] + + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=False + ) == [] + assert calls == ["lookup"] + + other_pr_run = { + "id": 107, + "event": "repository_dispatch", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#8@{'a' * 40}", + } + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_args, **_kwargs: [other_pr_run]) + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=False + ) == [] + + +def test_central_dispatch_cleanup_preserves_failed_cancellation(monkeypatch): + """A proven stale run is not reported retired when GitHub rejects cancellation.""" + run = { + "id": 108, + "event": "repository_dispatch", + "path": ".github/workflows/opencode-review-dispatch.yml", + "display_title": f"OpenCode Review Dispatch owner/repo#7@{'a' * 40}", + } + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_args, **_kwargs: [run]) + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda _action: None) + monkeypatch.setattr(sched, "_central_dispatch_run_still_stale", lambda *_args: True) + monkeypatch.setattr( + sched, + "force_cancel_workflow_runs", + lambda _repo, run_ids: {str(run_ids[0]): "rejected"}, + ) + + assert sched.cancel_stale_central_dispatch_runs( + "owner/repo", pr=make_pr(number=7), dry_run=False + ) == [] + + def test_context_review_and_check_helpers(monkeypatch): monkeypatch.delenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", raising=False) assert sched.context_nodes({}) == [] @@ -5606,11 +5859,45 @@ def fake_run_with_env(args, *, stdin=None, env=None): "pr_base_sha": base_sha, "pr_head_ref": "feature", "pr_head_sha": head_sha, + "draft_review_only": False, "required_run_id": 42, }, } +def test_draft_review_dispatch_carries_explicit_review_only_authority(monkeypatch): + """The receiver can distinguish an authorized Draft review from merge work.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GH_TOKEN", "opencode-app-token") + monkeypatch.setattr(sched, "active_opencode_run_refs", lambda *args: ([], [])) + monkeypatch.setattr(sched, "_cancel_revalidated_review_run_refs", lambda *args: ([], [])) + monkeypatch.setattr(sched, "review_dispatch_admitted", lambda *args: True) + monkeypatch.setattr(sched, "live_dispatch_head_matches", lambda *args: True) + monkeypatch.setattr(sched, "complete_paginated_pr_contexts", lambda *args: None) + monkeypatch.setattr(sched, "matching_actions_run_id", lambda *args: None) + monkeypatch.setattr(sched, "discover_opencode_required_run_id", lambda *args: None) + monkeypatch.setattr(sched, "reset_active_workflow_runs_cache", lambda: None) + dispatch_payloads = [] + monkeypatch.setattr( + sched, + "run_github_dispatch", + lambda _args, stdin=None: dispatch_payloads.append(json.loads(stdin)), + ) + pull_request = make_pr( + isDraft=True, + baseRefOid="b" * 40, + headRefOid="a" * 40, + ) + + assert ( + sched.dispatch_opencode_review( + "owner/repo", "OpenCode Review", pull_request, dry_run=False + ) + == "dispatched" + ) + assert dispatch_payloads[0]["client_payload"]["draft_review_only"] is True + + def test_central_required_workflow_waits_without_cross_repo_dispatch_credential(monkeypatch): monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "ContextualWisdomLab/.github") monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REF", "main") @@ -7870,6 +8157,147 @@ def test_draft_pr_still_skipped_by_default_and_without_trigger_reviews(monkeypat assert allowed_without_trigger.reason == "draft PR" +def test_draft_pr_retires_stale_runs_before_skip(monkeypatch): + """Draft admission must not bypass stale direct or central run cleanup.""" + calls = [] + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("SCHEDULER_ACTIONS_TOKEN", "workflow-token") + monkeypatch.setattr( + sched, + "cancel_stale_central_dispatch_runs", + lambda repo, *, pr, dry_run: calls.append(("central", repo, pr["number"], dry_run)) + or [], + raising=False, + ) + monkeypatch.setattr( + sched, + "cancel_stale_pr_runs", + lambda repo, pr, *, dry_run: calls.append(("direct", repo, pr["number"], dry_run)) + or [], + ) + monkeypatch.setattr( + sched, "recover_current_head_startup_failures", lambda *_args, **_kwargs: [] + ) + + decision = inspect( + make_pr(isDraft=True, headRefOid="a" * 40), + dry_run=False, + trigger_reviews=False, + ) + + assert decision.action == "skip" + assert calls == [ + ("central", "owner/repo", 1, False), + ("direct", "owner/repo", 1, False), + ] + + +def test_closed_pr_retires_central_runs_then_stops_admission(monkeypatch): + """Closed-event inspection cleans central runs without any merge-queue action.""" + calls = [] + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("SCHEDULER_ACTIONS_TOKEN", "workflow-token") + monkeypatch.setattr( + sched, + "cancel_stale_central_dispatch_runs", + lambda repo, *, pr, dry_run: calls.append(("central", repo, pr["number"], dry_run)) + or [], + ) + monkeypatch.setattr( + sched, + "cancel_stale_pr_runs", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("closed PR must not enter direct open-PR cleanup") + ), + ) + + decision = inspect( + make_pr( + state="CLOSED", + headRefOid="a" * 40, + files={"totalCount": 1, "nodes": [{"path": "README.md"}]}, + ), + dry_run=False, + ) + + assert decision.action == "skip" + assert decision.reason == "closed PR" + assert calls == [("central", "owner/repo", 1, False)] + + +def test_closed_pr_without_actions_authority_stops_without_cleanup(monkeypatch): + """A local or underprivileged close event fails closed before all queue work.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.delenv("SCHEDULER_ACTIONS_TOKEN", raising=False) + monkeypatch.setattr( + sched, + "cancel_stale_central_dispatch_runs", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("cleanup requires explicit Actions authority") + ), + ) + + decision = inspect(make_pr(state="CLOSED"), dry_run=False) + + assert decision.action == "skip" + assert decision.reason == "closed PR" + + +def test_workflow_name_rest_fallback_paginates_and_filters(monkeypatch): + """REST workflow identity retains only complete suite/name pairs across pages.""" + first_page = [ + {"check_suite_id": 1, "name": "OpenCode Review"}, + {"check_suite_id": None, "name": "ignored"}, + {"check_suite_id": 2, "name": " "}, + ] + [{"check_suite_id": index, "name": f"workflow-{index}"} for index in range(3, 100)] + calls = [] + + def fake_api(path): + calls.append(path) + return { + "workflow_runs": first_page + if "&page=1" in path + else [{"check_suite_id": 100, "name": "last"}] + } + + monkeypatch.setattr(sched, "gh_api_json", fake_api) + + names = sched.fetch_workflow_names_by_check_suite_rest("owner/repo", "a" * 40) + + assert names[1] == "OpenCode Review" + assert names[100] == "last" + assert 2 not in names + assert len(calls) == 2 + + +def test_workflow_name_rest_fallback_handles_only_inaccessible_actions(monkeypatch): + """Actions read denial degrades to unknown identity; other API faults propagate.""" + monkeypatch.setattr( + sched, + "gh_api_json", + lambda _path: (_ for _ in ()).throw(RuntimeError("Resource not accessible by integration")), + ) + assert sched.fetch_workflow_names_by_check_suite_rest("owner/repo", "a" * 40) == {} + + monkeypatch.setattr( + sched, + "gh_api_json", + lambda _path: (_ for _ in ()).throw(RuntimeError("network failure")), + ) + with pytest.raises(RuntimeError, match="network failure"): + sched.fetch_workflow_names_by_check_suite_rest("owner/repo", "a" * 40) + + +def test_auto_merge_wait_reason_reports_blocked_review_policy() -> None: + """BLOCKED mergeability identifies an unresolved GitHub review policy.""" + assert "reviewDecision is REVIEW_REQUIRED" in sched.auto_merge_wait_reason( + "BLOCKED", {"reviewDecision": "REVIEW_REQUIRED"} + ) + assert "reviewDecision is" not in sched.auto_merge_wait_reason( + "BLOCKED", {"reviewDecision": "APPROVED"} + ) + + def test_draft_pr_review_request_marker_continues_dispatch_without_the_cli_flag(monkeypatch): """A later scheduler pass with no repository_dispatch client_payload of its own (the Strix-completion workflow_run that follows an initial @@ -8282,6 +8710,67 @@ def test_draft_pr_review_only_dispatch_skips_when_a_current_head_verdict_exists( "draft PR review-only dispatch; current-head OpenCode verdict already exists" ) + draft_completion = { + **opencode_review("COMMENTED", "head"), + "body": ( + "OpenCode review\n\n" + "- Result: DRAFT_REVIEW_COMPLETE\n" + "- Head SHA: `head`\n\n" + "Draft review-only request completed without publishing merge approval authority." + ), + } + completed_draft = make_pr(isDraft=True, reviews={"nodes": [draft_completion]}) + completed_decision = inspect(completed_draft, allow_draft_review_dispatch=True) + assert completed_decision.action == "skip" + assert completed_decision.reason == ( + "draft PR review-only dispatch; current-head OpenCode verdict already exists" + ) + + +def test_draft_review_completion_requires_exact_head_and_both_formal_markers(): + """A stale or generic comment cannot suppress an explicit Draft review.""" + complete_body = ( + "OpenCode review\n\n" + "- Result: DRAFT_REVIEW_COMPLETE\n\n" + "Draft review-only request completed without publishing merge approval authority." + ) + stale = { + **opencode_review("COMMENTED", "old-head"), + "body": complete_body, + } + missing_explanation = { + **opencode_review("COMMENTED", "head"), + "body": "OpenCode review\n\n- Result: DRAFT_REVIEW_COMPLETE", + } + generic = { + **opencode_review("COMMENTED", "head"), + "body": "OpenCode is still running.", + } + + assert not sched.has_current_head_draft_review_completion( + make_pr(isDraft=True, reviews={"nodes": [stale]}) + ) + assert not sched.has_current_head_draft_review_completion( + make_pr(isDraft=True, reviews={"nodes": [missing_explanation]}) + ) + assert not sched.has_current_head_draft_review_completion( + make_pr(isDraft=True, reviews={"nodes": [generic]}) + ) + assert sched.has_current_head_draft_review_completion( + make_pr( + isDraft=True, + reviews={ + "nodes": [ + { + **opencode_review("COMMENTED", "head"), + "body": complete_body, + }, + generic, + ] + }, + ) + ) + def test_draft_pr_review_only_dispatch_retries_a_failed_required_check_with_no_verdict(): """A completed-but-failed required-workflow check is not a posted review: @@ -10989,6 +11478,51 @@ def test_bounded_admission_persists_leases_and_completes_only_current_head( assert [record.status for record in persisted.records.values()].count("dispatched") == 1 +def test_ready_transition_retires_same_head_draft_completion_lease(tmp_path): + """A Draft comment cannot permanently suppress Ready review on the same head.""" + state_path = tmp_path / "admission.json" + gate = sched.SchedulerAdmissionGate(state_path, sequence=78, dispatch_budget=1) + completion = { + **opencode_review("COMMENTED", "a" * 40), + "body": ( + "OpenCode review\n\n" + "- Result: DRAFT_REVIEW_COMPLETE\n\n" + "Draft review-only request completed without publishing merge approval authority." + ), + } + draft_pr = make_pr( + number=7, + isDraft=True, + headRefOid="a" * 40, + reviews={"nodes": [completion]}, + ) + assert gate.admit("opencode", "ContextualWisdomLab/example", draft_pr) + gate.reconcile("ContextualWisdomLab/example", [draft_pr]) + + from scripts.ci.review_admission_controller import load_state_file + + completed = next(iter(load_state_file(state_path).records.values())) + assert completed.status == "complete" + + ready_pr = { + **draft_pr, + "isDraft": False, + "statusCheckRollup": { + "contexts": {"nodes": [opencode_check(status="IN_PROGRESS")]} + }, + } + ready_gate = sched.SchedulerAdmissionGate( + state_path, sequence=79, dispatch_budget=1 + ) + assert ready_gate.admit("opencode", "ContextualWisdomLab/example", ready_pr) + admitted = next(iter(load_state_file(state_path).records.values())) + assert admitted.status == "dispatched" + + ready_gate.reconcile("ContextualWisdomLab/example", [ready_pr]) + preserved = next(iter(load_state_file(state_path).records.values())) + assert preserved.status == "dispatched" + + def test_actual_opencode_dispatch_path_obeys_one_shared_admission_budget( monkeypatch, tmp_path ): diff --git a/tests/test_repository_metadata_workflow.py b/tests/test_repository_metadata_workflow.py index 7b41a667d9..52251f751b 100644 --- a/tests/test_repository_metadata_workflow.py +++ b/tests/test_repository_metadata_workflow.py @@ -16,3 +16,14 @@ def test_metadata_apply_uses_dedicated_least_privilege_credential() -> None: assert "secrets.PR_REVIEW_MERGE_TOKEN" not in apply_source assert "Require dedicated repository settings credential" in apply_source assert 'test -n "${GH_TOKEN}"' in apply_source + + +def test_metadata_validation_checks_out_published_evidence_ancestry() -> None: + """The full-suite lineage guard must receive every published ancestor object.""" + source = WORKFLOW.read_text(encoding="utf-8") + validate_source = source.split("jobs:\n validate:", 1)[1].split("\n apply:", 1)[0] + checkout_source = validate_source.split("- name: Check out exact revision", 1)[1].split( + "- name: Verify exact revision", 1 + )[0] + + assert "fetch-depth: 0" in checkout_source diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py index ab7ede7aa4..ad80e2ff40 100644 --- a/tests/test_required_review_runner_image_contract.py +++ b/tests/test_required_review_runner_image_contract.py @@ -85,8 +85,8 @@ def test_opencode_review_dispatch_uses_explicit_supported_image(self) -> None: follow-up sweep as still open). """ workflow = OPENCODE_REVIEW_DISPATCH.read_text(encoding="utf-8") - self.assertEqual(workflow.count("group: CWL central OpenCode"), 3) - self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 3) + self.assertEqual(workflow.count("group: CWL central OpenCode"), 4) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 4) self.assertNotIn("runs-on: ubuntu-latest", workflow) self.assertNotIn("runs-on: ubuntu-24.04", workflow) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index a24b3c7d0c..d20b7c1974 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -7,12 +7,10 @@ import subprocess import sys import textwrap -import time from pathlib import Path import pytest - REPO_ROOT = Path(__file__).resolve().parents[1] @@ -24,8 +22,7 @@ def workflow_text(name: str) -> str: def test_central_dispatch_and_control_jobs_use_dedicated_groups() -> None: """Central-only workflows cannot fall back into the general Ubuntu pool.""" for name, group, jobs in ( - ("codeql-scan-dispatch.yml", "CWL central CodeQL", 3), - ("opencode-review-dispatch.yml", "CWL central OpenCode", 3), + ("opencode-review-dispatch.yml", "CWL central OpenCode", 4), ("agent-mention-router.yml", "CWL central control", 2), ("hourly-review-repair.yml", "CWL central control", 1), ): @@ -33,11 +30,42 @@ def test_central_dispatch_and_control_jobs_use_dedicated_groups() -> None: assert text.count(f" runs-on:\n group: {group}\n labels: [self-hosted, linux, x64]") == jobs assert "runs-on: ubuntu-24.04" not in text + codeql = workflow_text("codeql-scan-dispatch.yml") + codeql_group = " runs-on:\n group: CWL central CodeQL\n labels: [self-hosted, linux, x64]" + control_group = " runs-on:\n group: CWL central control\n labels: [self-hosted, linux, x64]" + validate_block = codeql.split(" validate-dispatch:\n", 1)[1].split( + "\n scan:\n", 1 + )[0] + scan_block = codeql.split(" scan:\n", 1)[1].split( + "\n settle-required-run:\n", 1 + )[0] + settlement_block = codeql.split(" settle-required-run:\n", 1)[1] + assert codeql.count(codeql_group) == 1 + assert codeql.count(control_group) == 2 + assert control_group in validate_block + assert codeql_group in scan_block + assert control_group in settlement_block + assert "runs-on: ubuntu-24.04" not in codeql + + +def test_codeql_dispatch_does_not_cancel_from_unvalidated_arrival_order() -> None: + """A delayed stale payload must not evict current exact-head CodeQL work.""" + workflow = workflow_text("codeql-scan-dispatch.yml") + header = workflow.split("\npermissions:\n", 1)[0] + + # Native workflow concurrency is evaluated before actor and live-PR + # validation. Any shared payload-derived group therefore lets arrival order + # cancel work that has not yet been proven stale. + assert not re.search(r"(?m)^concurrency:", header) + def test_reusable_scheduler_keeps_consumer_runner_access() -> None: """Reusable trusted schedulers share control capacity without PR execution.""" text = workflow_text("pr-review-merge-scheduler.yml") - selector = next(line for line in text.splitlines() if line.strip().startswith("runs-on:")) + scan_job = text.split("\n scan-pr-queue:\n", 1)[1] + selector = next( + line for line in scan_job.splitlines() if line.strip().startswith("runs-on:") + ) assert selector.strip() == "runs-on:" assert " runs-on:\n group: CWL central control\n labels: [self-hosted, linux, x64]" in text assert "fromJSON" not in selector @@ -251,12 +279,23 @@ def test_merge_scheduler_uses_native_auto_merge_after_required_checks() -> None: assert "github.event_name == 'repository_dispatch' && github.run_id" not in ( concurrency_contract ) - # Anchored, not a substring: this workflow's value is an expression rather - # than a constant, so it cannot use the boolean helper, but a commented-out - # setting must not satisfy it either. - assert re.search(r"(?m)^[ \t]+cancel-in-progress:[ \t]+\$\{\{", concurrency_contract) + assert "queue: max" in concurrency_contract + assert "cancel-in-progress:" not in concurrency_contract + assert "github.event.pull_request.head.sha" in concurrency_contract + assert "github.event.client_payload.pr_head_sha" in concurrency_contract assert "github.event_name == 'repository_dispatch'" in concurrency_contract + cleanup_job = workflow.split(" cancel-superseded-pr-runs:", 1)[1].split( + " scan-pr-queue:", 1 + )[0] + assert "actions: write" in cleanup_job + assert "actions/checkout" not in cleanup_job + assert "github.event.pull_request.number" in cleanup_job + assert "github.event.pull_request.head.sha" in cleanup_job + assert ".pull_requests[]?" in cleanup_job + assert ".head.sha" in cleanup_job + assert "force-cancel" in cleanup_job + def test_merge_scheduler_provides_same_repository_dispatch_credential() -> None: """Guard the runner-token dispatch credential for central review workflows. @@ -356,8 +395,8 @@ def test_privileged_review_retries_use_default_branch_repository_dispatch() -> N assert '"gh",\n "workflow",\n "run"' not in autofix_scheduler -def test_privileged_review_dispatch_coalesces_superseded_runs_before_admission() -> None: - """A superseded dispatch must be cancelled while queued, not after it takes a runner. +def test_privileged_review_dispatch_avoids_lossy_native_concurrency() -> None: + """The receiver must never let GitHub replace pending exact-head work. ``opencode-review-dispatch.yml`` carried its concurrency group only on the long ``opencode-review-target`` job. A job-level group is not evaluated @@ -369,25 +408,16 @@ def test_privileged_review_dispatch_coalesces_superseded_runs_before_admission() while they queued, every one of them after ``coverage-source-tree`` and ``coverage-evidence`` had already run. - The workflow-level group is keyed by the dispatched pull request, matching - ``codeql-scan-dispatch.yml``'s workflow-level group and the job-level group - this workflow keeps for the review job itself. + GitHub native concurrency retains at most one pending run per group and + replaces an older pending member even when ``cancel-in-progress`` is false. + The receiver therefore uses no native concurrency group. Trusted producer + deduplication and live-head cleanup remain the admission authorities. """ workflow = workflow_text("opencode-review-dispatch.yml") header = workflow.split("permissions:", 1)[0] - concurrency_contract = header.split("concurrency:", 1)[1] - group_value = workflow_level_concurrency_group(workflow) - - assert re.search(r"(?m)^concurrency:", header) - assert "opencode-review-dispatch-" in group_value - assert ( - "github.event.client_payload.target_repository || github.repository" - in group_value - ) - assert "github.event.client_payload.pr_number || github.run_id" in group_value - assert workflow_level_cancels_in_progress(workflow) - assert "github.event.client_payload.pr_head_sha" not in concurrency_contract - assert re.search(r"(?m)^ concurrency:", workflow) + assert not re.search(r"(?m)^concurrency:", header) + review_job = workflow.split("\n opencode-review-target:\n", 1)[1] + assert not re.search(r"(?m)^ concurrency:", review_job) @pytest.mark.parametrize( @@ -674,6 +704,8 @@ def test_required_opencode_dispatch_does_not_wait_on_merge_scheduler() -> None: assert 'event_type:"opencode-review"' in dispatch assert 'event_type:"merge-scheduler"' not in dispatch assert 'required_run_id:$required_run_id' in dispatch + assert "--argjson draft_review_only false" in dispatch + assert "draft_review_only:$draft_review_only" in dispatch for field in ( "target_repository", "pr_number", @@ -1131,9 +1163,23 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - assert "actions: write" in cleanup_job assert "actions/checkout" not in cleanup_job assert "cleanup skipped" not in cleanup_job + elif filename == "pr-review-merge-scheduler.yml": + assert "cancel-closed-pr-runs:" not in workflow + concurrency_contract = workflow.split("concurrency:", 1)[1].split( + "permissions:", 1 + )[0] + assert "github.event.pull_request.number" in concurrency_contract + assert "github.event.pull_request.head.sha" in concurrency_contract + assert "queue: max" in concurrency_contract + assert "cancel-in-progress:" not in concurrency_contract + assert "cancel-superseded-pr-runs:" in workflow + cleanup_job = workflow.split( + " cancel-superseded-pr-runs:", 1 + )[1].split(" scan-pr-queue:", 1)[0] + assert "actions: write" in cleanup_job + assert "actions/checkout" not in cleanup_job elif filename in { "codeql-pr.yml", - "pr-review-merge-scheduler.yml", "python-security.yml", "sast-semgrep.yml", "security-scan.yml", @@ -1149,7 +1195,10 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - ) else: raise AssertionError(f"unclassified close-event workflow: {filename}") - assert "github.event.action != 'closed'" in workflow + if filename == "pr-review-merge-scheduler.yml": + assert "github.event.action == 'closed'" in workflow + else: + assert "github.event.action != 'closed'" in workflow if filename in {"noema-review.yml", "strix.yml"}: assert "github.event.action != 'converted_to_draft'" in workflow @@ -1517,6 +1566,231 @@ def test_merge_scheduler_has_no_workflow_run_trigger() -> None: assert "workflow_run:" not in workflow.split("workflow_call:", 1)[0] +def _run_merge_scheduler_cleanup( + tmp_path: Path, + pull_states: list[dict[str, object]], + run_states: list[dict[str, object]], + inventory_responses: list[dict[str, object]] | None = None, +) -> tuple[subprocess.CompletedProcess[str], str]: + """Execute predecessor cleanup against stateful GitHub API fixtures.""" + if shutil.which("jq") is None: + pytest.skip("jq is required to execute the production cleanup") + step = workflow_step( + workflow_text("pr-review-merge-scheduler.yml"), + "Cancel revalidated predecessor scheduler runs", + ) + run_block = step.split(" run: |\n", 1)[1].split( + "\n scan-pr-queue:", 1 + )[0] + script = textwrap.dedent(run_block) + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + calls = tmp_path / "calls" + pulls = tmp_path / "pulls" + runs = tmp_path / "runs" + inventories = tmp_path / "inventories" + pulls.write_text( + "\n".join(json.dumps(state) for state in pull_states) + "\n", + encoding="utf-8", + ) + runs.write_text( + "\n".join(json.dumps(state) for state in run_states) + "\n", + encoding="utf-8", + ) + if inventory_responses is None: + default_inventory = { + "total_count": 1, + "workflow_runs": [ + { + "id": 100, + "status": "queued", + "pull_requests": [ + { + "number": 7, + "head": {"sha": "b" * 40}, + } + ], + } + ], + } + inventory_responses = [default_inventory] * 10 + inventories.write_text( + "\n".join(json.dumps(response) for response in inventory_responses) + "\n", + encoding="utf-8", + ) + fake_gh = fake_bin / "gh" + fake_gh.write_text( + '''#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"$FAKE_CALLS" +next_line() { + local source="$1" count_file="${1}.count" count=0 + [[ ! -f "$count_file" ]] || count="$(cat "$count_file")" + count=$((count + 1)) + printf '%s' "$count" >"$count_file" + sed -n "${count}p" "$source" +} +if [[ "$*" == *"/pulls/7"* ]]; then + next_line "$FAKE_PULLS" +elif [[ "$*" == *"actions/workflows/pr-review-merge-scheduler.yml/runs"* ]]; then + printf '[%s]\n' "$(next_line "$FAKE_INVENTORIES")" +elif [[ "$*" == *"actions/runs/100/force-cancel"* ]]; then + exit 0 +elif [[ "$*" == *"actions/runs/100"* ]]; then + state="$(next_line "$FAKE_RUNS")" + jq -r '[.status // "", .conclusion // ""] | @tsv' <<<"$state" +else + exit 1 +fi +''', + encoding="utf-8", + ) + fake_gh.chmod(0o755) + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "PATH": f"{fake_bin}{os.pathsep}{os.environ['PATH']}", + "FAKE_CALLS": str(calls), + "FAKE_PULLS": str(pulls), + "FAKE_RUNS": str(runs), + "FAKE_INVENTORIES": str(inventories), + "GH_TOKEN": "synthetic-actions-token", + "GITHUB_RUN_ID": "999", + "TARGET_REPOSITORY": "owner/repo", + "TARGET_PR_NUMBER": "7", + "TARGET_PR_HEAD_SHA": "a" * 40, + "TARGET_ACTION": "synchronize", + }, + capture_output=True, + text=True, + check=False, + ) + return result, calls.read_text(encoding="utf-8") + + +def _live_scheduler_pull(*, head_sha: str = "a" * 40) -> dict[str, object]: + """Build one live pull-request response for scheduler cleanup evidence.""" + return { + "base": {"repo": {"full_name": "owner/repo"}}, + "number": 7, + "state": "open", + "head": {"sha": head_sha}, + } + + +def test_scheduler_cleanup_revalidates_target_after_run_selection(tmp_path: Path) -> None: + """A concurrent head advance after selection prevents cancellation.""" + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull(), _live_scheduler_pull(head_sha="c" * 40)], + [{"status": "completed", "conclusion": "cancelled"}], + ) + assert result.returncode == 0, result.stderr + assert calls.count("/pulls/7") == 2 + assert "/actions/runs/100/force-cancel" not in calls + + +def test_scheduler_cleanup_fails_when_accepted_cancel_never_finishes( + tmp_path: Path, +) -> None: + """An accepted POST is not terminal cancellation evidence.""" + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull()] * 8, + [{"status": "in_progress", "conclusion": None}] * 6, + ) + assert result.returncode == 1 + assert calls.count("actions/runs/100 --jq") == 6 + assert "did not reach completed/cancelled" in result.stdout + + +def test_scheduler_cleanup_verifies_accepted_cancelled_state(tmp_path: Path) -> None: + """Finish only after GitHub reports completed/cancelled.""" + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull()] * 8, + [ + {"status": "in_progress", "conclusion": None}, + {"status": "completed", "conclusion": "cancelled"}, + ], + ) + assert result.returncode == 0, result.stderr + assert calls.count("actions/runs/100 --jq") == 2 + assert "Verified cancelled scheduler run 100." in result.stdout + + +def test_scheduler_cleanup_second_inventory_pass_catches_state_transition( + tmp_path: Path, +) -> None: + """A run moving between filtered states remains visible on the second pass.""" + empty_inventory = {"total_count": 0, "workflow_runs": []} + transitioned_inventory = { + "total_count": 1, + "workflow_runs": [ + { + "id": 100, + "status": "in_progress", + "pull_requests": [ + {"number": 7, "head": {"sha": "b" * 40}} + ], + } + ], + } + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull()] * 8, + [{"status": "completed", "conclusion": "cancelled"}], + [empty_inventory] * 5 + + [empty_inventory] * 4 + + [transitioned_inventory], + ) + assert result.returncode == 0, result.stderr + assert calls.count("actions/workflows/pr-review-merge-scheduler.yml/runs") == 10 + assert "/actions/runs/100/force-cancel" in calls + + +def test_scheduler_cleanup_inventory_covers_review_event_runs(tmp_path: Path) -> None: + """A new head must retire predecessor runs triggered by PR reviews too.""" + review_inventory = { + "total_count": 1, + "workflow_runs": [ + { + "id": 100, + "event": "pull_request_review", + "status": "queued", + "pull_requests": [ + {"number": 7, "head": {"sha": "b" * 40}}, + ], + } + ], + } + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull()] * 8, + [{"status": "completed", "conclusion": "cancelled"}], + [review_inventory] * 10, + ) + assert result.returncode == 0, result.stderr + assert "event=pull_request_target" not in calls + assert "/actions/runs/100/force-cancel" in calls + + +def test_scheduler_cleanup_fails_closed_on_truncated_inventory( + tmp_path: Path, +) -> None: + """Never treat GitHub's filtered-search ceiling as a complete snapshot.""" + result, calls = _run_merge_scheduler_cleanup( + tmp_path, + [_live_scheduler_pull()], + [{"status": "completed", "conclusion": "cancelled"}], + [{"total_count": 1001, "workflow_runs": []}], + ) + assert result.returncode == 1 + assert "inventory was incomplete" in result.stdout + assert "/force-cancel" not in calls + + def test_review_events_can_dispatch_after_threads_are_resolved() -> None: """Let the scheduler dispatch OpenCode when a review event clears its last blocker.""" workflow = workflow_text("pr-review-merge-scheduler.yml") diff --git a/tests/test_sbom_inventory_scheduler_contract.py b/tests/test_sbom_inventory_scheduler_contract.py index f181dd0891..433b18b612 100644 --- a/tests/test_sbom_inventory_scheduler_contract.py +++ b/tests/test_sbom_inventory_scheduler_contract.py @@ -1,9 +1,11 @@ """Executable contract for the central SBOM inventory scheduler.""" +import subprocess from pathlib import Path WORKFLOW = Path(".github/workflows/sbom-inventory-scheduler.yml") +LINEAGE_RECONCILER = Path("scripts/ci/reconcile_sbom_publication_lineage.sh") def _workflow_text() -> str: @@ -20,6 +22,31 @@ def _step_body(name: str) -> str: return workflow[start : next_step if next_step != -1 else len(workflow)] +def _git( + repository_path: Path, + *arguments: str, + check: bool = True, +) -> subprocess.CompletedProcess[str]: + """Run one isolated Git command and return its captured result.""" + return subprocess.run( + ["git", *arguments], + cwd=repository_path, + check=check, + text=True, + capture_output=True, + ) + + +def _commit_file(repository_path: Path, relative_path: str, content: str, message: str) -> str: + """Write and commit one fixture file, then return the resulting commit SHA.""" + target_path = repository_path / relative_path + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_text(content, encoding="utf-8") + _git(repository_path, "add", relative_path) + _git(repository_path, "commit", "-m", message) + return _git(repository_path, "rev-parse", "HEAD").stdout.strip() + + def test_sbom_inventory_scheduler_runs_hourly() -> None: """Organization license evidence must refresh once each hour.""" workflow = _workflow_text() @@ -70,3 +97,276 @@ def test_sbom_inventory_scheduler_does_not_force_push() -> None: publication_step = _step_body("Open or update inventory PR") assert "--force" not in publication_step assert "--force-with-lease" not in publication_step + assert "--strategy=ours" not in publication_step + assert "reconcile_sbom_publication_lineage.sh" in publication_step + + +def test_sbom_inventory_publication_fails_closed_before_overwriting_prior_repairs( + tmp_path: Path, +) -> None: + """A refresh must stop before treating prior owner repairs as generated data.""" + repository_path = tmp_path / "publication-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "dependency.lock", "vulnerable\n", "base") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + + _git(repository_path, "switch", "-c", "publication") + _commit_file(repository_path, "dependency.lock", "repaired\n", "repair dependency") + previous_head = _commit_file( + repository_path, + "tests/security-regression.txt", + "repair stays covered\n", + "cover repair", + ) + + _git(repository_path, "switch", "main") + _commit_file(repository_path, "application.txt", "protected main update\n", "advance main") + generated_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "fresh inventory\n", + "generate inventory", + ) + + result = subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), previous_head, generated_head], + cwd=repository_path, + check=False, + text=True, + capture_output=True, + ) + + assert result.returncode != 0 + assert "prior publication head contains non-inventory change" in result.stderr + assert _git(repository_path, "rev-parse", "HEAD").stdout.strip() == generated_head + assert (repository_path / "application.txt").read_text(encoding="utf-8") == "protected main update\n" + assert (repository_path / "docs/sbom/inventory.md").read_text(encoding="utf-8") == "fresh inventory\n" + assert not (repository_path / ".git" / "MERGE_HEAD").exists() + + +def test_sbom_inventory_publication_merges_generated_only_lineage(tmp_path: Path) -> None: + """A generated-only predecessor may merge while the fresh inventory remains authoritative.""" + repository_path = tmp_path / "generated-only-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "application.txt", "base\n", "base") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + + _git(repository_path, "switch", "-c", "publication") + previous_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "previous inventory\n", + "previous inventory", + ) + + _git(repository_path, "switch", "main") + _commit_file(repository_path, "application.txt", "protected main update\n", "advance main") + generated_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "fresh inventory\n", + "generate inventory", + ) + + subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), previous_head, generated_head], + cwd=repository_path, + check=True, + text=True, + capture_output=True, + ) + + assert (repository_path / "application.txt").read_text(encoding="utf-8") == "protected main update\n" + assert (repository_path / "docs/sbom/inventory.md").read_text(encoding="utf-8") == "fresh inventory\n" + assert _git(repository_path, "merge-base", "--is-ancestor", previous_head, "HEAD").returncode == 0 + assert len(_git(repository_path, "show", "-s", "--format=%P", "HEAD").stdout.split()) == 2 + + +def test_sbom_inventory_publication_rejects_generated_non_inventory_paths( + tmp_path: Path, +) -> None: + """A generated commit must not acquire authority over neighboring owner files.""" + repository_path = tmp_path / "generated-owner-path-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "application.txt", "base\n", "base") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + + _git(repository_path, "switch", "-c", "publication") + previous_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "previous inventory\n", + "previous inventory", + ) + + _git(repository_path, "switch", "main") + _commit_file(repository_path, "application.txt", "protected main update\n", "advance main") + (repository_path / "docs/sbom/inventory.md").write_text( + "fresh inventory\n", + encoding="utf-8", + ) + (repository_path / "docs/sbom/reviewer-notes.md").write_text( + "must remain product-owned\n", + encoding="utf-8", + ) + _git( + repository_path, + "add", + "docs/sbom/inventory.md", + "docs/sbom/reviewer-notes.md", + ) + _git(repository_path, "commit", "-m", "generate inventory with owner path") + generated_head = _git(repository_path, "rev-parse", "HEAD").stdout.strip() + + result = subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), previous_head, generated_head], + cwd=repository_path, + check=False, + text=True, + capture_output=True, + ) + + assert result.returncode != 0 + assert "generated inventory head contains non-inventory change" in result.stderr + assert _git(repository_path, "rev-parse", "HEAD").stdout.strip() == generated_head + assert not (repository_path / ".git" / "MERGE_HEAD").exists() + + +def test_sbom_inventory_publication_rejects_reverted_owner_history( + tmp_path: Path, +) -> None: + """A clean final tree must not smuggle owner commits into publication ancestry.""" + repository_path = tmp_path / "reverted-owner-history-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "owner.txt", "protected\n", "base owner") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + + _git(repository_path, "switch", "-c", "publication") + owner_commit = _commit_file(repository_path, "owner.txt", "smuggled\n", "smuggle owner change") + _git(repository_path, "revert", "--no-edit", owner_commit) + previous_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "previous inventory\n", + "previous inventory", + ) + + _git(repository_path, "switch", "main") + _commit_file(repository_path, "application.txt", "protected main update\n", "advance main") + generated_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "fresh inventory\n", + "generate inventory", + ) + + result = subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), previous_head, generated_head], + cwd=repository_path, + check=False, + text=True, + capture_output=True, + ) + + assert result.returncode != 0 + assert "prior publication history contains non-inventory change" in result.stderr + assert _git(repository_path, "rev-parse", "HEAD").stdout.strip() == generated_head + ancestry = _git( + repository_path, + "merge-base", + "--is-ancestor", + owner_commit, + "HEAD", + check=False, + ) + assert ancestry.returncode != 0 + assert not (repository_path / ".git" / "MERGE_HEAD").exists() + + +def test_sbom_inventory_publication_rejects_non_inventory_workspace_side_effects( + tmp_path: Path, +) -> None: + """An uncommitted generator side effect outside the inventory fails closed.""" + repository_path = tmp_path / "generated-workspace-side-effect-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "owner.txt", "protected\n", "base owner") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + generated_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "fresh inventory\n", + "generate inventory", + ) + (repository_path / "owner.txt").write_text("generator side effect\n", encoding="utf-8") + + result = subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), generated_head, generated_head], + cwd=repository_path, + check=False, + text=True, + capture_output=True, + ) + + assert result.returncode != 0 + assert "working tree contains non-inventory change" in result.stderr + assert _git(repository_path, "rev-parse", "HEAD").stdout.strip() == generated_head + assert not (repository_path / ".git" / "MERGE_HEAD").exists() + + +def test_sbom_inventory_publication_fails_closed_on_non_inventory_conflict( + tmp_path: Path, +) -> None: + """A non-inventory conflict must abort instead of choosing either writer silently.""" + repository_path = tmp_path / "conflict-repository" + repository_path.mkdir() + _git(repository_path, "init", "-b", "main") + _git(repository_path, "config", "user.name", "SBOM Fixture") + _git(repository_path, "config", "user.email", "sbom-fixture@example.invalid") + _commit_file(repository_path, "dependency.lock", "base\n", "base") + _commit_file(repository_path, "docs/sbom/inventory.md", "base inventory\n", "base inventory") + _commit_file(repository_path, "docs/sbom/inventory.json", "{}\n", "base inventory json") + + _git(repository_path, "switch", "-c", "publication") + previous_head = _commit_file(repository_path, "dependency.lock", "publication repair\n", "repair") + + _git(repository_path, "switch", "main") + _commit_file(repository_path, "dependency.lock", "protected main repair\n", "advance main") + generated_head = _commit_file( + repository_path, + "docs/sbom/inventory.md", + "fresh inventory\n", + "generate inventory", + ) + + result = subprocess.run( + [str(LINEAGE_RECONCILER.resolve()), previous_head, generated_head], + cwd=repository_path, + check=False, + text=True, + capture_output=True, + ) + + assert result.returncode != 0 + assert "prior publication head contains non-inventory change" in result.stderr + assert _git(repository_path, "rev-parse", "HEAD").stdout.strip() == generated_head + assert not (repository_path / ".git" / "MERGE_HEAD").exists() diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index 42efce621e..c410748b39 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -67,11 +67,22 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: self.assertIn("|| '\"ubuntu-24.04\"'", selector) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: - """Require validation, scan, and attempt wake jobs in the dedicated group.""" + """Keep only heavy scans on CodeQL capacity and metadata on control.""" workflow = CODEQL_SCAN_DISPATCH.read_text(encoding="utf-8") + validate_block = workflow.split(" validate-dispatch:\n", 1)[1].split( + "\n scan:\n", 1 + )[0] + scan_block = workflow.split(" scan:\n", 1)[1].split( + "\n settle-required-run:\n", 1 + )[0] + settlement_block = workflow.split(" settle-required-run:\n", 1)[1] self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("group: CWL central CodeQL"), 3) + self.assertEqual(workflow.count("group: CWL central CodeQL"), 1) + self.assertEqual(workflow.count("group: CWL central control"), 2) self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 3) + self.assertIn("group: CWL central control", validate_block) + self.assertIn("group: CWL central CodeQL", scan_block) + self.assertIn("group: CWL central control", settlement_block) def test_python_security_uses_explicit_supported_image(self) -> None: """Require all three Python Security jobs to pin Ubuntu 24.04.""" diff --git a/tests/test_stacked_pr_security_workflow_contract.py b/tests/test_stacked_pr_security_workflow_contract.py index 9ee655381b..a558957998 100644 --- a/tests/test_stacked_pr_security_workflow_contract.py +++ b/tests/test_stacked_pr_security_workflow_contract.py @@ -22,8 +22,4 @@ def test_security_workflows_run_for_stacked_pull_requests() -> None: break pull_request_block.append(line) assert "pull_request:" in workflow - assert ( - "# Scan every PR base ref" in workflow - or "# Do not restrict the base ref" in workflow - ) assert not any(line.strip().startswith("branches:") for line in pull_request_block) diff --git a/tests/test_strix_runtime_dependencies.py b/tests/test_strix_runtime_dependencies.py index 6f664daa20..c2c6a5eb68 100644 --- a/tests/test_strix_runtime_dependencies.py +++ b/tests/test_strix_runtime_dependencies.py @@ -3,7 +3,9 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] -def _locked_requirement_versions(requirements_text: str, package_name: str) -> list[str]: +def _locked_requirement_versions( + requirements_text: str, package_name: str +) -> list[str]: """Return every exact version row for one normalized package name.""" package_versions = [] for requirement_line in requirements_text.splitlines(): @@ -53,7 +55,7 @@ def test_strix_anyio_security_pin_is_an_explicit_lock_input() -> None: def test_strix_pyjwt_security_pin_is_an_explicit_lock_input() -> None: - """Keep PyJWT above the GHSA-42vr-xj54-vc7v parser DoS fix.""" + """Keep the patched PyJWT version reproducible from the source input.""" requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( encoding="utf-8" ) @@ -61,12 +63,12 @@ def test_strix_pyjwt_security_pin_is_an_explicit_lock_input() -> None: REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" ).read_text(encoding="utf-8") - assert _locked_requirement_versions(requirements, "pyjwt") == ["2.15.0"] - assert _locked_requirement_versions(requirements_lock, "pyjwt") == ["2.15.0"] + assert _locked_requirement_versions(requirements, "pyjwt") == ["2.15.1"] + assert _locked_requirement_versions(requirements_lock, "pyjwt") == ["2.15.1"] def test_strix_litellm_security_pin_is_an_explicit_lock_input() -> None: - """Keep LiteLLM outside the CVE-2026-84377 credential leak range.""" + """Keep the patched LiteLLM credential boundary reproducible.""" requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( encoding="utf-8" ) @@ -78,19 +80,16 @@ def test_strix_litellm_security_pin_is_an_explicit_lock_input() -> None: assert _locked_requirement_versions(requirements_lock, "litellm") == ["1.94.3"] -def test_shared_urllib3_security_pin_is_an_explicit_lock_input() -> None: - """Keep both audited dependency closures above the urllib3 CVE fixes.""" - requirement_paths = ( - "requirements-pip-audit-ci.txt", - "requirements-pip-audit-ci-hashes.txt", - "requirements-strix-ci.txt", - "requirements-strix-ci-hashes.txt", +def test_python_security_inputs_pin_patched_urllib3() -> None: + """Keep both audited runtimes on the urllib3 security release.""" + requirement_pairs = ( + ("requirements-pip-audit-ci.txt", "requirements-pip-audit-ci-hashes.txt"), + ("requirements-strix-ci.txt", "requirements-strix-ci-hashes.txt"), ) - for requirement_path in requirement_paths: - requirements_text = (REPOSITORY_ROOT / requirement_path).read_text( - encoding="utf-8" - ) - assert _locked_requirement_versions(requirements_text, "urllib3") == [ - "2.8.0" - ] + for source_name, lock_name in requirement_pairs: + source_text = (REPOSITORY_ROOT / source_name).read_text(encoding="utf-8") + lock_text = (REPOSITORY_ROOT / lock_name).read_text(encoding="utf-8") + + assert _locked_requirement_versions(source_text, "urllib3") == ["2.8.0"] + assert _locked_requirement_versions(lock_text, "urllib3") == ["2.8.0"] diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index ed4ad55681..7c27766c5d 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -146,7 +146,7 @@ def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> assert "python -m coverage report" in workflow assert "python -m coverage run -m pytest tests -q" in workflow assert "unset COVERAGE_RCFILE" in workflow - assert "python -m interrogate --fail-under 100" in workflow + assert "run: python -m interrogate --fail-under 100 scripts/ci\n" in workflow assert "python -m compileall -q" in workflow required_tests = ( diff --git a/tests/test_verify_release_maturin_tool_assets.py b/tests/test_verify_release_maturin_tool_assets.py index 6aa84a0827..6dabcbad97 100644 --- a/tests/test_verify_release_maturin_tool_assets.py +++ b/tests/test_verify_release_maturin_tool_assets.py @@ -204,9 +204,14 @@ def open(self, _request, timeout): transport_error = urllib.error.HTTPError( "https://github.com/asset", 502, "Bad Gateway", {}, io.BytesIO() ) - monkeypatch.setattr( - transport_error, "close", lambda: closed.append("http-error") - ) + original_transport_close = transport_error.close + + def record_transport_close(): + """Record the close call without replacing the real resource cleanup.""" + closed.append("http-error") + original_transport_close() + + monkeypatch.setattr(transport_error, "close", record_transport_close) class ErrorOpener: def open(self, _request, timeout): @@ -221,6 +226,7 @@ def open(self, _request, timeout): with pytest.raises(ValueError, match="HTTP 502"): verifier._download("maturin-x86_64-pc-windows-msvc.zip") assert closed == ["response", "http-error"] + assert transport_error.closed def test_maturin_download_rejects_unlisted_name_before_network(monkeypatch): @@ -334,6 +340,12 @@ def verify(evidence, reader, fetch): assert captured["raw"] == b"asset" +def test_maturin_verifier_has_complete_docstrings(): + """Every production entry point explains its trust-boundary responsibility.""" + assert verifier._binary.__doc__ + assert verifier.main.__doc__ + + def test_maturin_process_entrypoint_uses_the_bounded_downloader(monkeypatch): archives, assets, _, evidence = _asset_case() from scripts.ci import scan_release_native_links as scanner