diff --git a/.github/actions/noema-review/two_phase.py b/.github/actions/noema-review/two_phase.py index c850da81b9..2815d7a050 100755 --- a/.github/actions/noema-review/two_phase.py +++ b/.github/actions/noema-review/two_phase.py @@ -167,20 +167,16 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i changed_files = gate.fetch_changed_files(repo, number) changed_paths = tuple(file_path for file_path, _status in changed_files) review_context = gate.build_review_context(repo, number, pull_request, changed_files) - try: - verdict = gate.call_llm( - repo, - number, - pull_request, - diff, - truncated, - expected, - review_context, - changed_paths, - ) - except gate.NoemaTransportError as exc: - _emit_transport_capacity_outputs(exc, expected_head=expected) - raise + verdict = gate.call_llm( + repo, + number, + pull_request, + diff, + truncated, + expected, + review_context, + changed_paths, + ) _write_envelope( path, @@ -200,45 +196,6 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i return 0 -def _emit_transport_capacity_outputs( - exc: gate.NoemaTransportError, - *, - expected_head: str, -) -> None: - """Publish typed capacity evidence for the workflow's bounded re-dispatch step.""" - retry_attempt = gate.current_transport_retry_attempt() - delay = gate.transport_redispatch_delay_seconds( - transport_retry_attempt=retry_attempt, - head_sha=expected_head, - retry_after_seconds=exc.retry_after_seconds, - ) - eligible = bool(exc.capacity_unavailable and delay is not None) - outputs = { - "transport_capacity_unavailable": "true" if exc.capacity_unavailable else "false", - "transport_retry_eligible": "true" if eligible else "false", - "prepared": "false", - } - if type(exc.http_status) is int: - outputs["transport_http_status"] = str(exc.http_status) - if type(exc.provider_attempt_count) is int: - outputs["provider_attempt_count"] = str(exc.provider_attempt_count) - if delay is not None: - outputs["transport_retry_delay_seconds"] = str(delay) - outputs["transport_retry_next_attempt"] = str(retry_attempt + 1) - gate.append_github_output(outputs) - if eligible: - print( - "::notice::Noema provider capacity unavailable after gateway failover; " - f"bounded continuation re-dispatch is eligible in {delay}s " - f"(attempt {retry_attempt + 1}/{gate.MAX_TRANSPORT_REDISPATCH_ATTEMPTS})." - ) - elif exc.capacity_unavailable: - print( - "::error::Noema provider capacity unavailable after gateway failover; " - "automatic re-dispatch budget is exhausted. Review remains required." - ) - - def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> int: """Publish a prepared verdict only with fresh exact-head/base reviewer authority.""" expected = _canonical_head(expected_head) diff --git a/.github/workflows/actions-queue-health.yml b/.github/workflows/actions-queue-health.yml deleted file mode 100644 index 2084765946..0000000000 --- a/.github/workflows/actions-queue-health.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: GitHub Actions queue health - -on: - schedule: - - cron: "7 * * * *" - -concurrency: - group: github-actions-queue-health - cancel-in-progress: false - -permissions: - contents: read - actions: read - -jobs: - collect: - name: Collect exact-head queue evidence - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - actions: read - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - name: Checkout trusted queue-health source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Collect read-only repository and runner evidence - env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} - run: | - if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN is required for cross-repository queue reads." - exit 1 - fi - echo "::add-mask::$GH_TOKEN" - python3 scripts/ci/actions_queue_health.py \ - --allowlist config/actions_queue_health_repositories.json \ - --output-json "$RUNNER_TEMP/actions-queue-health.json" \ - --output-html "$RUNNER_TEMP/actions-queue-health.html" - - - name: Upload queue-health evidence - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: github-actions-queue-health-${{ github.run_id }} - path: | - ${{ runner.temp }}/actions-queue-health.json - ${{ runner.temp }}/actions-queue-health.html - if-no-files-found: error diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index a601e25522..6c6efc3dd1 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -11,11 +11,6 @@ on: - "tests/test_noema_two_phase_handoff.py" - "tests/test_noema_refreshed_app_identity.py" - "tests/test_noema_token_lifetime_stale_run_contract.py" - - "scripts/ci/noema_review_document.py" - - "scripts/ci/noema_hwp_mcp_reader.mjs" - - "scripts/ci/noema-document-reader/package.json" - - "scripts/ci/noema-document-reader/package-lock.json" - - "tests/test_noema_document_review_context.py" - "docs/doctoring/noema-review-token-lifetime.md" - "docs/product-technical-gap-baseline.md" - ".github/workflows/opencode-review-dispatch.yml" @@ -29,13 +24,10 @@ on: - "docs/doctoring/strix-legal-git-paths.md" - "docs/doctoring/strix-model-behavior-error.md" - "docs/doctoring/strix-quality-timeout-fixtures.md" - - "docs/doctoring/strix-evidence-binding-2159-2168.md" - "scripts/ci/strix_quick_gate.sh" - - "scripts/ci/strix_evidence_binding.py" - "scripts/ci/test_strix_quick_gate.sh" - "tests/test_docs_only_pr_runner_admission.py" - "tests/test_strix_changed_path_policy.py" - - "tests/test_strix_evidence_binding.py" - "tests/test_strix_model_behavior_error.py" - "tests/test_strix_nvidia_nim_not_found_fallback.py" - "tests/test_strix_workflow_dependency_hashes.py" @@ -111,8 +103,6 @@ on: - "docs/doctoring/exact-artifact-sbom-quality-runner-consolidation-20260903.md" - "CHANGELOG.d/20260903-exact-artifact-quality-runner-consolidation.md" - "requirements-opencode-review-ci-hashes.txt" - - "requirements-noema-document-ci.txt" - - "requirements-noema-document-ci-hashes.txt" # PR validation only: a new head cancels only an older run of this workflow # for the same repository and pull request. @@ -148,9 +138,7 @@ jobs: with: python-version: "3.14" cache: pip - cache-dependency-path: | - requirements-opencode-review-ci-hashes.txt - requirements-noema-document-ci-hashes.txt + cache-dependency-path: requirements-opencode-review-ci-hashes.txt - name: Select affected contract suites id: affected_suites @@ -193,11 +181,6 @@ jobs: tests/test_noema_two_phase_handoff.py|\ tests/test_noema_refreshed_app_identity.py|\ tests/test_noema_token_lifetime_stale_run_contract.py|\ - scripts/ci/noema_review_document.py|\ - scripts/ci/noema_hwp_mcp_reader.mjs|\ - scripts/ci/noema-document-reader/package.json|\ - scripts/ci/noema-document-reader/package-lock.json|\ - tests/test_noema_document_review_context.py|\ docs/doctoring/noema-review-token-lifetime.md) noema_suite=true ;; @@ -213,13 +196,10 @@ jobs: docs/doctoring/strix-legal-git-paths.md|\ docs/doctoring/strix-model-behavior-error.md|\ docs/doctoring/strix-quality-timeout-fixtures.md|\ - docs/doctoring/strix-evidence-binding-2159-2168.md|\ scripts/ci/strix_quick_gate.sh|\ - scripts/ci/strix_evidence_binding.py|\ scripts/ci/test_strix_quick_gate.sh|\ tests/test_docs_only_pr_runner_admission.py|\ tests/test_strix_changed_path_policy.py|\ - tests/test_strix_evidence_binding.py|\ tests/test_strix_model_behavior_error.py|\ tests/test_strix_nvidia_nim_not_found_fallback.py|\ tests/test_strix_workflow_dependency_hashes.py|\ @@ -230,10 +210,6 @@ jobs: noema_suite=true opencode_suite=true ;; - requirements-noema-document-ci.txt|\ - requirements-noema-document-ci-hashes.txt) - noema_suite=true - ;; .github/workflows/pr-review-merge-scheduler.yml) queue_suite=true review_repair_suite=true @@ -349,13 +325,7 @@ jobs: if: steps.affected_suites.outputs.noema == 'true' || steps.affected_suites.outputs.opencode == 'true' || steps.affected_suites.outputs.review_repair == 'true' || steps.affected_suites.outputs.exact_artifact == 'true' run: >- python -m pip install --disable-pip-version-check --require-hashes - -r requirements-opencode-review-ci-hashes.txt -r requirements-noema-document-ci-hashes.txt - - - name: Install exact Noema document dependencies - if: steps.affected_suites.outputs.noema == 'true' - run: >- - python -m pip install --disable-pip-version-check --require-hashes --no-deps - -r requirements-noema-document-ci-hashes.txt + -r requirements-opencode-review-ci-hashes.txt - name: Verify Noema token-lifetime contracts if: steps.affected_suites.outputs.noema == 'true' @@ -365,15 +335,13 @@ jobs: tests/test_noema_reviewer_token_lifetime.py \ tests/test_noema_two_phase_handoff.py \ tests/test_noema_refreshed_app_identity.py \ - tests/test_noema_token_lifetime_stale_run_contract.py \ - tests/test_noema_document_review_context.py + tests/test_noema_token_lifetime_stale_run_contract.py python -m compileall -q \ .github/actions/noema-review/two_phase.py \ tests/test_noema_reviewer_token_lifetime.py \ tests/test_noema_two_phase_handoff.py \ tests/test_noema_refreshed_app_identity.py \ - tests/test_noema_token_lifetime_stale_run_contract.py \ - tests/test_noema_document_review_context.py + tests/test_noema_token_lifetime_stale_run_contract.py - name: Verify OpenCode Rust coverage toolchain contract if: steps.affected_suites.outputs.opencode == 'true' @@ -400,16 +368,13 @@ jobs: python -m pytest -q \ tests/test_docs_only_pr_runner_admission.py \ tests/test_strix_changed_path_policy.py \ - tests/test_strix_evidence_binding.py \ tests/test_strix_model_behavior_error.py \ tests/test_strix_nvidia_nim_not_found_fallback.py \ tests/test_strix_workflow_dependency_hashes.py \ tests/test_strix_quality_timeout_fixture_budget.py bash scripts/ci/test_strix_quick_gate.sh python -m compileall -q \ - scripts/ci/strix_evidence_binding.py \ tests/test_strix_changed_path_policy.py \ - tests/test_strix_evidence_binding.py \ tests/test_strix_model_behavior_error.py \ tests/test_strix_nvidia_nim_not_found_fallback.py \ tests/test_strix_workflow_dependency_hashes.py \ diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index cc13d2d87e..c21c8446df 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -152,8 +152,6 @@ jobs: permissions: contents: read id-token: write - pull-requests: read - statuses: read strategy: fail-fast: false matrix: ${{ fromJSON(needs.detect-languages.outputs.matrix) }} @@ -237,35 +235,17 @@ jobs: ')" if [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then jobs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/runs/${run_id}/jobs")" - dispatch_job="$(printf '%s' "$jobs_json" | jq -c --arg name "$expected_job" ' + job_conclusion="$(printf '%s' "$jobs_json" | jq -r --arg name "$expected_job" ' [.[] | .jobs[] | select(.name == $name)] - | if length == 1 then .[0] else empty end + | if length == 1 then .[0].conclusion else empty end ')" - if [ -n "$dispatch_job" ]; then - gate_conclusion="$(printf '%s' "$dispatch_job" | jq -r ' - (.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate") | .conclusion) // empty - ')" - case "$gate_conclusion" in - success) - echo "verdict=success" >>"$GITHUB_OUTPUT" - echo "Found completed CodeQL dispatch scan gate for ${LANGUAGE}: success." - exit 0 - ;; - failure|cancelled|skipped) - echo "verdict=failure" >>"$GITHUB_OUTPUT" - echo "Found completed CodeQL dispatch scan gate for ${LANGUAGE}: failure." - exit 0 - ;; - esac - job_conclusion="$(printf '%s' "$dispatch_job" | jq -r '.conclusion // empty')" - case "$job_conclusion" in - success) - echo "verdict=success" >>"$GITHUB_OUTPUT" - echo "Found completed CodeQL dispatch scan job for ${LANGUAGE}: success." - exit 0 - ;; - esac - fi + case "$job_conclusion" in + success|failure) + echo "verdict=${job_conclusion}" >>"$GITHUB_OUTPUT" + echo "Found completed CodeQL dispatch scan job for ${LANGUAGE}: ${job_conclusion}." + exit 0 + ;; + esac fi if [ "$RUN_ATTEMPT" != "1" ]; then @@ -318,8 +298,6 @@ jobs: contents: read id-token: write actions: read - pull-requests: read - statuses: read steps: - name: Dispatch current-head CodeQL scan env: diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 45cfcc75fc..c94fdf55c2 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -12,24 +12,17 @@ # Exercise this handler end-to-end by POSTing a real repository_dispatch # event instead -- that always runs the default-branch version. name: CodeQL Scan Dispatch -# LEGACY_V1_REMOVAL_CONDITION: remove codeql-scan:legacy-v1 only after the -# protected v2 producer has landed and every in-flight v1 required run has -# reached a terminal conclusion. Both protocols share this protected handler. run-name: >- CodeQL Scan Dispatch ${{ github.event.client_payload.target_repository || github.repository }}#${{ github.event.client_payload.pr_number || 'event' }}@${{ - github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || github.sha }}/${{ - github.event.action == 'codeql-scan-v2' && - format('{0}/{1}/{2}', github.event.client_payload.pr_base_sha || 'none', - github.event.client_payload.required_run_id || github.run_id, - github.event.client_payload.producer_source_sha || 'missing-source') || - format('{0}/{1}', github.event.client_payload.pr_base_sha || 'none', - github.event.client_payload.required_run_id || github.run_id) }} + github.event.client_payload.pr_head_sha || github.sha }}/${{ + github.event.client_payload.pr_base_sha || 'none' }}/${{ + github.event.client_payload.required_run_id || github.run_id }} on: repository_dispatch: - types: [codeql-scan, codeql-scan-v2] + types: [codeql-scan] concurrency: group: >- @@ -59,10 +52,6 @@ jobs: matrix: ${{ steps.validate.outputs.matrix }} required_run_id: ${{ steps.validate.outputs.required_run_id }} required_jobs: ${{ steps.validate.outputs.required_jobs }} - rerun_mode: ${{ steps.validate.outputs.rerun_mode }} - rerun_schema: ${{ steps.validate.outputs.rerun_schema }} - producer_source_sha: ${{ steps.validate.outputs.producer_source_sha }} - dispatch_protocol: ${{ steps.validate.outputs.dispatch_protocol }} steps: - name: Exchange OpenCode app token for target repository metadata reads id: metadata_read_app_token @@ -151,23 +140,15 @@ jobs: DISPATCH_ACTOR: ${{ github.triggering_actor }} DISPATCH_SENDER: ${{ github.event.sender.login || '' }} ALLOWED_DISPATCH_ACTOR: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_ACTOR }} - DISPATCH_PROTOCOL: ${{ github.event.action }} 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_ENVELOPE: ${{ toJSON(github.event.client_payload.pr_head) }} - SUPPLIED_HEAD_SCHEMA: ${{ github.event.client_payload.pr_head.schema || '' }} - SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head.ref || github.event.client_payload.pr_head_ref || '' }} - SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || '' }} - SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} - SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} - SUPPLIED_PRODUCER_SOURCE_SHA: ${{ github.event.client_payload.producer_source_sha || '' }} + SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} + SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} SUPPLIED_MATRIX: ${{ toJSON(github.event.client_payload.matrix) }} SUPPLIED_REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id || '' }} SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }} - SUPPLIED_RERUN_MODE: ${{ github.event.client_payload.rerun_mode || '' }} - SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }} # Pre-#2008 payloads still send scalar required_job_id + # required_language with a one-shard matrix. Synthesize # required_jobs from those only when the array is empty. @@ -196,86 +177,14 @@ jobs: fi printf 'Authorized repository_dispatch actor=%s sender=%s target=%s.\n' "$DISPATCH_ACTOR" "$DISPATCH_SENDER" "$TARGET_REPOSITORY" - case "$DISPATCH_PROTOCOL" in - codeql-scan) - dispatch_protocol=legacy-v1 - if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ] || - [ -n "$SUPPLIED_PRODUCER_SOURCE_SHA" ] || - { [ -n "$SUPPLIED_RERUN_REQUEST" ] && [ "$SUPPLIED_RERUN_REQUEST" != "null" ]; } || - [ -n "$SUPPLIED_RERUN_MODE" ]; then - echo "::error::Legacy CodeQL dispatch rejected v2-only identity fields." - exit 1 - fi - ;; - codeql-scan-v2) - dispatch_protocol=v2 - if [ "$SUPPLIED_HEAD_ENVELOPE" = "null" ]; then - echo "::error::CodeQL v2 dispatch requires the versioned pr_head envelope." - exit 1 - fi - ;; - *) - echo "::error::CodeQL dispatch protocol is unsupported." - exit 1 - ;; - esac - - if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ]; then - if [ "$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r ' - type == "object" - and ((.ref | type) == "string") - and ((.sha | type) == "string") - ' 2>/dev/null || true)" != "true" ]; then - printf '::error::repository_dispatch supplied invalid pr_head envelope; ref and sha must be strings.\n' - exit 1 - fi - envelope_schema_type="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema | type')" - if [ "$envelope_schema_type" = "null" ]; then - printf '::error::repository_dispatch supplied unsupported pr_head schema=.\n' - exit 1 - fi - if [ "$envelope_schema_type" != "string" ]; then - printf '::error::repository_dispatch supplied invalid pr_head envelope; schema must be a string.\n' - exit 1 - fi - envelope_schema="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema')" - envelope_ref="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.ref')" - envelope_sha="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.sha')" - if [ "$envelope_schema" != "1" ]; then - printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$envelope_schema" - exit 1 - fi - if [ "$SUPPLIED_HEAD_SCHEMA" != "$envelope_schema" ] || - [ "$SUPPLIED_HEAD_REF" != "$envelope_ref" ] || - [ "$SUPPLIED_HEAD_SHA" != "$envelope_sha" ]; then - printf '::error::repository_dispatch pr_head envelope disagrees with extracted workflow inputs.\n' - exit 1 - fi - if { [ -n "$SUPPLIED_LEGACY_HEAD_REF" ] || [ -n "$SUPPLIED_LEGACY_HEAD_SHA" ]; } && - { [ "$SUPPLIED_LEGACY_HEAD_REF" != "$envelope_ref" ] || - [ "$SUPPLIED_LEGACY_HEAD_SHA" != "$envelope_sha" ]; }; then - printf '::error::repository_dispatch rejected conflicting nested and legacy pr_head identity.\n' - exit 1 - fi - elif [ -n "$SUPPLIED_HEAD_SCHEMA" ]; then - printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$SUPPLIED_HEAD_SCHEMA" - exit 1 - fi - if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" exit 1 fi - if [ "$dispatch_protocol" = v2 ] && - ! [[ "$SUPPLIED_PRODUCER_SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "::error::CodeQL producer source is missing or malformed." - exit 1 - fi matrix_json="$(printf '%s' "$SUPPLIED_MATRIX" | jq -c '.' 2>/dev/null || true)" jobs_json="$(printf '%s' "$SUPPLIED_REQUIRED_JOBS" | jq -c '.' 2>/dev/null || true)" - rerun_request_json="$(printf '%s' "$SUPPLIED_RERUN_REQUEST" | jq -c '.' 2>/dev/null || true)" if [ -z "$matrix_json" ] || [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length >= 1')" != "true" ] || [ "$(printf '%s' "$matrix_json" | jq '[.[] | select((.language | type == "string") and (.language | test("^[a-z0-9-]+$")) and (."build-mode" | type == "string"))] | length == ($ARGS.positional[0] | tonumber)' --args "$(printf '%s' "$matrix_json" | jq 'length')")" != "true" ] || @@ -283,45 +192,6 @@ jobs: printf '::error::CodeQL scan dispatch matrix must contain at least one valid language/build-mode shard with unique languages. matrix=%s\n' "${SUPPLIED_MATRIX:-}" exit 1 fi - rerun_mode="${SUPPLIED_RERUN_MODE:-failed}" - rerun_schema="legacy-0" - if [ "$rerun_mode" != "failed" ] && [ "$rerun_mode" != "all" ]; then - printf '::error::CodeQL rerun mode is invalid.\n' - exit 1 - fi - if [ -n "$rerun_request_json" ] && [ "$rerun_request_json" != "null" ]; then - if [ -n "$jobs_json" ] && [ "$(printf '%s' "$jobs_json" | jq '(. != null) and (. != [])')" = "true" ] || - [ -n "$SUPPLIED_RERUN_MODE" ] || [ -n "$SUPPLIED_REQUIRED_JOB_ID" ] || - [ -n "$SUPPLIED_REQUIRED_LANGUAGE" ]; then - printf '::error::CodeQL dispatch rejected conflicting legacy and nested rerun envelopes.\n' - exit 1 - fi - rerun_schema_type="$(printf '%s' "$rerun_request_json" | jq -r '.schema | type')" - if [ "$rerun_schema_type" = "null" ]; then - printf '::error::unsupported CodeQL rerun schema=.\n' - exit 1 - fi - if [ "$rerun_schema_type" != "string" ]; then - printf '::error::CodeQL rerun schema must be a string.\n' - exit 1 - fi - rerun_schema="$(printf '%s' "$rerun_request_json" | jq -r '.schema')" - if [ "$rerun_schema" != "1" ]; then - printf '::error::unsupported CodeQL rerun schema=%s.\n' "$rerun_schema" - exit 1 - fi - if [ "$(printf '%s' "$rerun_request_json" | jq ' - type == "object" - and ((keys | sort) == ["mode", "required_jobs", "schema"]) - and (.mode == "failed" or .mode == "all") - and (.required_jobs | type == "array") - ')" != "true" ]; then - printf '::error::CodeQL rerun mode or required job envelope is invalid.\n' - exit 1 - fi - rerun_mode="$(printf '%s' "$rerun_request_json" | jq -r '.mode')" - jobs_json="$(printf '%s' "$rerun_request_json" | jq -c '.required_jobs')" - fi if [ -z "$jobs_json" ] || [ "$(printf '%s' "$jobs_json" | jq '(. == null) or (. == [])')" = "true" ]; then if [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length == 1')" = "true" ] && @@ -344,7 +214,6 @@ jobs: )) and (($jobs | map(.language) | sort) == ($matrix | map(.language) | sort)) and (($jobs | map(.language) | unique | length) == ($jobs | length)) - and (($jobs | map(.job_id | tostring) | unique | length) == ($jobs | length)) ')" != "true" ]; then printf '::error::CodeQL wake identity is missing, non-canonical, or does not match the dispatched languages one-to-one.\n' exit 1 @@ -362,7 +231,6 @@ 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_merge_commit_sha="$(jq -r '.merge_commit_sha // empty' <<<"$pull_request_json")" live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" if [ "$live_state" != "open" ] || @@ -385,26 +253,6 @@ jobs: 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 fi - if [ "$dispatch_protocol" = v2 ]; then - if ! [[ "$live_merge_commit_sha" =~ ^[0-9a-fA-F]{40}$ ]] || - [ "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" != "${live_merge_commit_sha,,}" ]; then - echo "::error::CodeQL producer revision does not match the live pull request merge revision." - exit 1 - fi - producer_commit_json="$(gh api "repos/${TARGET_REPOSITORY}/git/commits/${SUPPLIED_PRODUCER_SOURCE_SHA}")" - if ! printf '%s' "$producer_commit_json" | jq -e \ - --arg source "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" \ - --arg base "${live_base_sha,,}" \ - --arg head "${live_head_sha,,}" ' - ((.sha // "" | ascii_downcase) == $source) - and ((.parents // []) | length == 2) - and ((.parents[0].sha // "" | ascii_downcase) == $base) - and ((.parents[1].sha // "" | ascii_downcase) == $head) - ' >/dev/null; then - echo "::error::CodeQL producer revision is not the exact live base/head merge." - exit 1 - fi - fi { printf 'target_repository=%s\n' "$TARGET_REPOSITORY" @@ -417,10 +265,6 @@ jobs: printf '%s\n' "$matrix_json" echo "EOF" printf 'required_run_id=%s\n' "$SUPPLIED_REQUIRED_RUN_ID" - printf 'rerun_mode=%s\n' "$rerun_mode" - printf 'rerun_schema=%s\n' "$rerun_schema" - printf 'producer_source_sha=%s\n' "$SUPPLIED_PRODUCER_SOURCE_SHA" - printf 'dispatch_protocol=%s\n' "$dispatch_protocol" echo "required_jobs<>"$GITHUB_OUTPUT" - name: Re-validate live pull request metadata before privileged scan - id: live_metadata env: GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} @@ -541,7 +384,7 @@ jobs: exit 1 fi - - name: Fetch the pinned CodeQL SARIF gate and GHAS identity scripts + - name: Fetch the pinned CodeQL SARIF gate script env: GH_TOKEN: ${{ github.token }} WORKFLOW_SHA: ${{ github.workflow_sha }} @@ -550,9 +393,6 @@ jobs: gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/codeql_sarif_gate.py?ref=${WORKFLOW_SHA}" \ --jq .content | base64 --decode >"$RUNNER_TEMP/codeql_sarif_gate.py" python3 -c "import ast; ast.parse(open('$RUNNER_TEMP/codeql_sarif_gate.py').read())" - gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/codeql_ghas_configuration_identity.py?ref=${WORKFLOW_SHA}" \ - --jq .content | base64 --decode >"$RUNNER_TEMP/codeql_ghas_configuration_identity.py" - python3 -c "import ast; ast.parse(open('$RUNNER_TEMP/codeql_ghas_configuration_identity.py').read())" - name: Materialize pull request head for CodeQL scan env: @@ -587,51 +427,17 @@ jobs: id: gate run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch - - name: Verify GHAS base/head CodeQL configuration identity - id: ghas_configuration_identity - if: steps.gate.outcome == 'success' - env: - GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} - TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} - PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }} - BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }} - BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} - HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} - LANGUAGE: ${{ matrix.language }} - run: | - set -euo pipefail - # Default setup often lands a fast language before a slower one on the - # same PR head; GHAS can settle "configuration not found" for the - # slower base identity in that window (#2133). Bounded polling waits - # for the scanned language's exact base identity on this exact head - # before any terminal dispatch status is published. - base_ref="$BASE_REF" - case "$base_ref" in - refs/*) ;; - *) base_ref="refs/heads/${base_ref}" ;; - esac - head_ref="refs/pull/${PR_NUMBER}/head" - python3 "$RUNNER_TEMP/codeql_ghas_configuration_identity.py" \ - --repository "$TARGET_REPOSITORY" \ - --base-ref "$base_ref" \ - --base-sha "$BASE_SHA" \ - --head-ref "$head_ref" \ - --head-sha "$HEAD_SHA" \ - --language "$LANGUAGE" - - name: Preserve CodeQL SARIF evidence - id: sarif_upload if: always() && hashFiles('codeql-results-dispatch/**/*.sarif') != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: codeql-dispatch-${{ matrix.language }}-${{ github.run_id }}-${{ github.run_attempt }} path: codeql-results-dispatch - if-no-files-found: error retention-days: 7 - name: Publish CodeQL dispatch status id: publish_status - if: always() && steps.live_metadata.outcome == 'success' + if: always() env: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} GITHUB_STATUS_READ_TOKEN: ${{ github.token }} @@ -639,36 +445,14 @@ jobs: OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} - BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} - REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} - PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} - DISPATCH_PROTOCOL: ${{ needs.validate-dispatch.outputs.dispatch_protocol }} LANGUAGE: ${{ matrix.language }} GATE_OUTCOME: ${{ steps.gate.outcome }} - GHAS_IDENTITY_OUTCOME: ${{ steps.ghas_configuration_identity.outcome }} - SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }} run: | set -euo pipefail - if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then - echo "::error::CodeQL SARIF evidence was not preserved; terminal status publication and exact-run settlement are blocked." - exit 1 - fi case "$GATE_OUTCOME" in success) - case "${GHAS_IDENTITY_OUTCOME:-skipped}" in - success) - state="success" - description="CodeQL dispatch scan passed with continuous GHAS configuration identity" - ;; - failure) - state="failure" - description="CodeQL gate passed but GHAS base/head configuration identity is incomplete" - ;; - *) - state="error" - description="CodeQL gate passed without GHAS configuration identity proof (${GHAS_IDENTITY_OUTCOME:-unknown})" - ;; - esac + state="success" + description="CodeQL dispatch scan passed (no unsuppressed Medium+ findings)" ;; failure) state="failure" @@ -679,20 +463,6 @@ jobs: description="CodeQL dispatch scan did not produce a verdict (${GATE_OUTCOME:-unknown})" ;; esac - case "$DISPATCH_PROTOCOL" in - legacy-v1) - receipt_context="codeql-dispatch/${LANGUAGE}" - receipt_description="$description" - ;; - v2) - receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}" - receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}" - ;; - *) - echo "::error::CodeQL status publication rejected an unknown dispatch protocol." - exit 1 - ;; - esac post_status() { token_label="$1" @@ -704,34 +474,13 @@ jobs: status_error="$(mktemp)" if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${HEAD_SHA}" \ -f state="$state" \ - -f context="$receipt_context" \ - -f description="$receipt_description" \ + -f context="codeql-dispatch/${LANGUAGE}" \ + -f description="$description" \ -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ >"$status_response" 2>"$status_error"; then - actual_creator="$(jq -r '.creator.login // "" | ascii_downcase' "$status_response" 2>/dev/null || true)" - creator_trusted=false - case "$token_label" in - target-app-token|pr-review-merge-token|opencode-approve-token) - case "$actual_creator" in - opencode-agent|opencode-agent\[bot\]) creator_trusted=true ;; - esac - ;; - github-token) - if [ "${TARGET_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && - [ "${GITHUB_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && - [ "$actual_creator" = "github-actions[bot]" ]; then - creator_trusted=true - fi - ;; - esac - if [ "$creator_trusted" = true ]; then - rm -f "$status_response" "$status_error" - echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." - return 0 - fi rm -f "$status_response" "$status_error" - echo "::notice::CodeQL dispatch status publish using ${token_label} returned unexpected creator=${actual_creator:-missing}; trying the next configured credential." - return 1 + echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." + return 0 fi error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" rm -f "$status_response" "$status_error" @@ -757,280 +506,79 @@ jobs: fi if [ "$GATE_OUTCOME" = "success" ]; then - echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The exact completed scan and preserved SARIF artifact remain the authenticated fallback evidence." + echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The completed dispatch scan job remains the evidence for this head." exit 0 fi echo "::error::Could not publish the CodeQL dispatch status after all configured credentials failed; the exact required job will remain failed and will not be woken with stale or missing evidence." exit 1 - settle-required-run: - name: settle exact required run - needs: [validate-dispatch, scan] - if: >- - always() - && needs.validate-dispatch.result == 'success' - && needs.scan.result != 'cancelled' - && needs.scan.result != 'skipped' - runs-on: ubuntu-24.04 - timeout-minutes: 8 - permissions: - actions: write - contents: read - id-token: write - steps: - - name: Exchange OpenCode app token for run settlement - id: target_app_token + - name: Wake exact CodeQL required job + if: >- + always() + && steps.publish_status.outcome == 'success' + && needs.validate-dispatch.outputs.target_repository != '' + && needs.validate-dispatch.outputs.pr_number != '' + && needs.validate-dispatch.outputs.head_sha != '' + && needs.validate-dispatch.outputs.required_run_id != '' + && needs.validate-dispatch.outputs.required_jobs != '' 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: Settle exact CodeQL required run - env: - TARGET_APP_WAKE_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} - PR_REVIEW_MERGE_WAKE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} - OPENCODE_APPROVE_WAKE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} - GITHUB_WAKE_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || '' }} - HANDLER_READ_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }} - BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }} - BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} - HEAD_REF: ${{ needs.validate-dispatch.outputs.head_ref }} HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} REQUIRED_JOBS: ${{ needs.validate-dispatch.outputs.required_jobs }} - RERUN_MODE: ${{ needs.validate-dispatch.outputs.rerun_mode }} - RERUN_SCHEMA: ${{ needs.validate-dispatch.outputs.rerun_schema }} - MAX_CODEQL_RERUN_ATTEMPT: "48" - PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} + REQUIRED_LANGUAGE: ${{ matrix.language }} + WAKE_TOKEN_SOURCE: ${{ needs.validate-dispatch.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 - - run_api() { - token_label="$1" - token="$2" - shift 2 - if [ -z "$token" ]; then - return 1 - fi - api_response="" - if api_response="$(GH_TOKEN="$token" gh api "$@")"; then - printf '%s\n' "$api_response" - echo "::notice::CodeQL settlement API used ${token_label}." >&2 - return 0 - fi - echo "::notice::CodeQL settlement API using ${token_label} did not succeed." >&2 - return 1 - } - - github_api() { - run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" || - run_api "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" "$@" || - run_api "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" "$@" || - run_api "github-token" "$GITHUB_WAKE_TOKEN" "$@" - } - - if ! pull="$(github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then - echo "::error::CodeQL settlement could not read the current pull request." + if [ -z "${GH_TOKEN:-}" ] || [ "$WAKE_TOKEN_SOURCE" = "unavailable" ]; then + echo "::error::Actions-capable CodeQL wake credential is unavailable." exit 1 fi - if [ "$(printf '%s' "$pull" | jq -r '.state // empty')" != "open" ] || - [ "$(printf '%s' "$pull" | jq -r '.base.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || - [ "$(printf '%s' "$pull" | jq -r '.base.ref // empty')" != "$BASE_REF" ] || - [ "$(printf '%s' "$pull" | jq -r '.base.sha // empty')" != "$BASE_SHA" ] || - [ "$(printf '%s' "$pull" | jq -r '.head.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || - [ "$(printf '%s' "$pull" | jq -r '.head.ref // empty')" != "$HEAD_REF" ] || - [ "$(printf '%s' "$pull" | jq -r '.head.sha // empty')" != "$HEAD_SHA" ]; then - echo "::error::CodeQL settlement rejected a closed PR, changed base, or stale head." + REQUIRED_JOB_ID="$(printf '%s' "$REQUIRED_JOBS" | jq -r --arg lang "$REQUIRED_LANGUAGE" ' + [.[] | select(.language == $lang) | .job_id | tostring] + | if length == 1 and (.[0] | test("^[1-9][0-9]*$")) then .[0] else empty end + ')" + if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]] || + ! [[ "$REQUIRED_JOB_ID" =~ ^[1-9][0-9]*$ ]] || + ! [[ "$REQUIRED_LANGUAGE" =~ ^[a-z0-9-]+$ ]]; then + echo "::error::CodeQL wake identity is non-canonical." exit 1 fi - if ! required_run="$(github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")"; then - echo "::error::CodeQL settlement could not read the required run." + pull="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" + live_state="$(printf '%s' "$pull" | jq -r '.state // empty')" + live_head="$(printf '%s' "$pull" | jq -r '.head.sha // empty')" + if [ "$live_state" != "open" ] || [ "$live_head" != "$HEAD_SHA" ]; then + echo "::error::CodeQL wake rejected a closed PR or stale head." exit 1 fi - if [ "$(printf '%s' "$required_run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' + + run="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")" + run_identity="$(printf '%s' "$run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' select(.id == $run_id) | select(.event == "pull_request") | select(.path == ".github/workflows/codeql-pr.yml") | select(.head_sha == $head) + | .id // empty + ')" + expected_name="CodeQL compatibility analysis (${REQUIRED_LANGUAGE})" + job="$(gh api "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}")" + job_identity="$(printf '%s' "$job" | jq -r --arg head "$HEAD_SHA" --arg name "$expected_name" --argjson run_id "$REQUIRED_RUN_ID" --argjson job_id "$REQUIRED_JOB_ID" ' + select(.id == $job_id) + | select(.run_id == $run_id) + | select(.head_sha == $head) + | select(.name == $name) | select(.status == "completed" and .conclusion == "failure") - | select((.run_attempt | type) == "number") - | select(.run_attempt == (.run_attempt | floor) and .run_attempt >= 1) | .id // empty - ')" != "$REQUIRED_RUN_ID" ]; then - echo "::error::CodeQL settlement rejected the required run identity." - exit 1 - fi - - required_run_attempt="$(printf '%s' "$required_run" | jq -r '.run_attempt')" - if ! [[ "$MAX_CODEQL_RERUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || - [ "$required_run_attempt" -ge "$MAX_CODEQL_RERUN_ATTEMPT" ]; then - rerun_languages="$(printf '%s' "$REQUIRED_JOBS" | jq -r 'map(.language) | sort | join(",")')" - printf '::error::codeql_settlement phase=pre_mutation reason=rerun_budget_exhausted run_id=%s run_attempt=%s max_rerun_attempt=%s rerun_schema=%s languages=%s handler_run_id=%s handler_run_attempt=%s\n' \ - "$REQUIRED_RUN_ID" "$required_run_attempt" "$MAX_CODEQL_RERUN_ATTEMPT" \ - "$RERUN_SCHEMA" "$rerun_languages" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" + ')" + if [ "$run_identity" != "$REQUIRED_RUN_ID" ] || + [ "$job_identity" != "$REQUIRED_JOB_ID" ]; then + echo "::error::CodeQL wake rejected missing or ambiguous exact run/job identity." exit 1 fi - if ! required_job_pages="$(github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100")"; then - echo "::error::CodeQL settlement could not read the required jobs." - exit 1 - fi - required_job_list="$(printf '%s' "$required_job_pages" | jq -c '[.[] | .jobs[]?]')" - while IFS= read -r required_job; do - language="$(printf '%s' "$required_job" | jq -r '.language // empty')" - job_id="$(printf '%s' "$required_job" | jq -r '.job_id // empty')" - expected_name="CodeQL compatibility analysis (${language})" - match_count="$(printf '%s' "$required_job_list" | jq --arg language "$language" --arg name "$expected_name" --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" --argjson job_id "$job_id" --arg mode "$RERUN_MODE" ' - [.[] | select( - .id == $job_id - and .run_id == $run_id - and .head_sha == $head - and .name == $name - and .status == "completed" - and ( - ($mode == "failed" and .conclusion == "failure") - or ($mode == "all" and (.conclusion == "success" or .conclusion == "failure")) - ) - )] | length - ')" - if [ "$match_count" -ne 1 ]; then - echo "::error::CodeQL settlement rejected missing or ambiguous exact job identity for ${language}." - exit 1 - fi - done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') - - required_job_ids="$(printf '%s' "$REQUIRED_JOBS" | jq -c '[.[].job_id]')" - if [ "$RERUN_MODE" = "failed" ] && - [ "$(printf '%s' "$required_job_list" | jq --argjson required_ids "$required_job_ids" ' - [.[] | .id as $id | select(.status == "completed" and .conclusion == "failure" and ($required_ids | index($id) | not))] | length - ')" -ne 0 ]; then - echo "::error::CodeQL settlement rejected unrelated failed jobs outside the exact language map." - exit 1 - fi - - if ! handler_job_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100")" || - ! handler_artifact_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100")"; then - echo "::error::CodeQL settlement could not read exact handler evidence." - exit 1 - fi - handler_jobs="$(printf '%s' "$handler_job_pages" | jq -c '[.[] | .jobs[]?]')" - handler_artifacts="$(printf '%s' "$handler_artifact_pages" | jq -c '[.[] | .artifacts[]?]')" - while IFS= read -r required_job; do - language="$(printf '%s' "$required_job" | jq -r '.language')" - expected_job_name="CodeQL dispatch scan (${language})" - expected_artifact_name="codeql-dispatch-${language}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - handler_job_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" ' - [.[] | select( - .name == $name - and .status == "completed" - and (.conclusion == "success" or .conclusion == "failure") - and .run_attempt == $attempt - and ([.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate" and (.conclusion == "success" or .conclusion == "failure"))] | length) == 1 - and ([.steps[]? | select(.name == "Preserve CodeQL SARIF evidence" and .conclusion == "success")] | length) == 1 - )] | length - ')" - handler_artifact_count="$(printf '%s' "$handler_artifacts" | jq --arg name "$expected_artifact_name" ' - [.[] | select(.name == $name and (.expired == false) and (.size_in_bytes > 0))] | length - ')" - if [ "$handler_job_count" -ne 1 ] || [ "$handler_artifact_count" -ne 1 ]; then - echo "::error::CodeQL settlement rejected incomplete handler gate or SARIF evidence for ${language}." - exit 1 - fi - done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') - - case "$RERUN_MODE" in - failed) rerun_endpoint="rerun-failed-jobs" ;; - all) rerun_endpoint="rerun" ;; - *) - echo "::error::CodeQL settlement rejected an unsupported rerun mode." - exit 1 - ;; - esac - - post_wake() { - token_label="$1" - token="$2" - if [ -z "$token" ]; then - return 1 - fi - if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/${rerun_endpoint}" >/dev/null; then - echo "Re-ran exact CodeQL required run ${REQUIRED_RUN_ID} mode=${RERUN_MODE} head=${HEAD_SHA} using ${token_label}." - return 0 - fi - echo "::notice::CodeQL settlement POST using ${token_label} did not succeed." - return 1 - } - - if post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" || - post_wake "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" || - post_wake "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" || - post_wake "github-token" "$GITHUB_WAKE_TOKEN"; then - exit 0 - fi - - echo "::error::CodeQL settlement could not enqueue verified run-wide recovery." - exit 1 + gh api -X POST "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}/rerun" >/dev/null + echo "Re-ran exact failed CodeQL job ${REQUIRED_JOB_ID} for ${REQUIRED_LANGUAGE} on ${HEAD_SHA}." diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 9be705a50c..f8ab55c896 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -75,75 +75,6 @@ jobs: echo "admitted=true" >>"$GITHUB_OUTPUT" echo "Exact live Noema head admitted for ${TARGET_REPOSITORY}#${PR_NUMBER}." - changed-scope: - name: Detect changed scope - # Same job-level docs/image-only gate as strix.yml, security-scan.yml, - # sast-semgrep.yml, and codeql-pr.yml (see - # docs/doctoring/required-workflow-path-filter-boundary.md): the org - # ruleset ignores every `on:` filter when it runs this workflow in - # another repository, so the doc/image-only decision has to be made in - # a job and consumed through `needs`, not the trigger. Noema review - # previously ran its full model-review chain for every PR event - # including docs/changelog-only diffs; this closes that gap using the - # identical classifier already used elsewhere. Fails OPEN: an - # unreadable, empty, or truncated file list reviews everything. Not - # gated on repository_dispatch's own admission below: a repository_dispatch - # retry carries no `github.event.pull_request`, so this job's own - # PR/REPO lookup naturally falls through to "scan everything" for that - # path, matching strix.yml's identical repository_dispatch behavior. - if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: - contents: read - pull-requests: read - outputs: - code: ${{ steps.scope.outputs.code }} - deps: ${{ steps.scope.outputs.deps }} - steps: - - name: Classify changed paths - id: scope - env: - GH_TOKEN: ${{ github.token }} - REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }} - PR: ${{ github.event.pull_request.number }} - EXPECTED_FILES: ${{ github.event.pull_request.changed_files }} - shell: bash - run: | - set -uo pipefail - code=true - deps=true - if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then - changed="" - for attempt in 1 2 3; do - if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then - break - fi - changed="" - sleep $((attempt * 3)) - done - # GitHub caps /pulls/N/files at 3000 entries; a short list would hide - # source files behind a doc-only verdict, so require an exact count. - if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then - code=false - deps=false - while IFS= read -r changed_path; do - case "$changed_path" in - *.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;; - *) code=true ;; - esac - case "$changed_path" in - requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;; - esac - done <<<"$changed" - else - echo "::notice::changed-scope could not read a complete PR file list; scanning everything." - fi - fi - echo "code=${code}" >> "$GITHUB_OUTPUT" - echo "deps=${deps}" >> "$GITHUB_OUTPUT" - echo "changed-scope code=${code} deps=${deps}" - cancel-closed-pr-runs: if: >- github.event_name == 'pull_request_target' && @@ -325,7 +256,7 @@ jobs: noema-review: name: noema-review - needs: [admit-current-head, changed-scope] + needs: [admit-current-head] runs-on: ubuntu-24.04 # No job-level timeout-minutes here, deliberately. This job's "Prepare # Noema model verdict" step calls two_phase.py's call_llm synchronously @@ -363,15 +294,7 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }} - # Empty PR_NUMBER already means "skip the review body" below (see the - # next step); a docs/image-only diff reuses that exact same, already - # fully-tested skip path by clearing it here too instead of adding a - # second, separately-gated condition to every downstream step. Fails - # OPEN: changed-scope's own output defaults to 'true' (or is empty - # when that job itself was skipped for a non-pull_request_target - # event), so this only ever clears PR_NUMBER on a proven docs/image-only - # diff. - PR_NUMBER: ${{ (needs.changed-scope.outputs.code != 'false' && (github.event.pull_request.number || github.event.client_payload.pr_number)) || '' }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }} steps: - name: Skip events without pull request context @@ -727,34 +650,6 @@ jobs: set -euo pipefail bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh" - - name: Provision local reviewed HWP document reader - if: env.PR_NUMBER != '' - env: - NPM_CONFIG_IGNORE_SCRIPTS: "true" - run: | - set -euo pipefail - node_major="$(node -p 'process.versions.node.split(".")[0]')" - case "$node_major" in - 20|22) ;; - *) - echo "::error::Noema HWP reader requires Node.js 20 or 22; found ${node_major:-missing}." - exit 1 - ;; - esac - python3 -m pip install --quiet --require-hashes --no-deps \ - -r "$GITHUB_WORKSPACE/requirements-noema-document-ci-hashes.txt" - reader_root="${RUNNER_TEMP}/noema-document-reader" - rm -rf "$reader_root" - mkdir -p "$reader_root" - cp "$GITHUB_WORKSPACE/scripts/ci/noema-document-reader/package.json" \ - "$GITHUB_WORKSPACE/scripts/ci/noema-document-reader/package-lock.json" \ - "$reader_root/" - ( - cd "$reader_root" - npm ci --ignore-scripts --omit=dev --no-audit --no-fund - ) - echo "NOEMA_HWP_MCP_SOURCE=$reader_root/node_modules/hwp-mcp" >>"$GITHUB_ENV" - - name: Prepare Noema model verdict if: env.PR_NUMBER != '' id: noema_prepare @@ -763,7 +658,6 @@ jobs: NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_token.outputs['installation-id'] }} - NOEMA_TRANSPORT_RETRY_ATTEMPT: ${{ github.event.client_payload.transport_retry_attempt || 0 }} run: | set -euo pipefail if [ -z "${PR_NUMBER:-}" ]; then @@ -794,53 +688,6 @@ jobs: echo "::notice::Noema model phase produced no publishable envelope; publication is skipped." fi - - name: Schedule bounded Noema transport re-dispatch - if: >- - failure() - && env.PR_NUMBER != '' - && steps.noema_prepare.outputs.transport_capacity_unavailable == 'true' - && steps.noema_prepare.outputs.transport_retry_eligible == 'true' - env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || github.token }} - DELAY_SECONDS: ${{ steps.noema_prepare.outputs.transport_retry_delay_seconds }} - NEXT_ATTEMPT: ${{ steps.noema_prepare.outputs.transport_retry_next_attempt }} - PROVIDER_ATTEMPT_COUNT: ${{ steps.noema_prepare.outputs.provider_attempt_count || '' }} - TRANSPORT_HTTP_STATUS: ${{ steps.noema_prepare.outputs.transport_http_status || '' }} - run: | - set -euo pipefail - if ! [[ "${DELAY_SECONDS}" =~ ^[1-9][0-9]*$ ]] || [ "${DELAY_SECONDS}" -gt 300 ]; then - echo "::error::Noema transport re-dispatch refused a non-bounded delay." - exit 1 - fi - if ! [[ "${NEXT_ATTEMPT}" =~ ^[1-9][0-9]*$ ]]; then - echo "::error::Noema transport re-dispatch refused a malformed attempt counter." - exit 1 - fi - echo "::notice::Noema provider capacity unavailable (http_status=${TRANSPORT_HTTP_STATUS:-unknown}, provider_attempt_count=${PROVIDER_ATTEMPT_COUNT:-unknown}); waiting ${DELAY_SECONDS}s before same-head continuation re-dispatch ${NEXT_ATTEMPT}." - sleep "${DELAY_SECONDS}" - live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" - live_head="$(jq -r '.head.sha // empty' <<<"$live_pr")" - live_state="$(jq -r '.state // empty' <<<"$live_pr")" - if [ "${live_head,,}" != "${EXPECTED_HEAD_SHA,,}" ] || [ "$live_state" != "open" ]; then - echo "::notice::Noema transport re-dispatch retired because the live head moved or closed." - exit 0 - fi - jq -n \ - --arg target_repository "$TARGET_REPOSITORY" \ - --argjson pr_number "$PR_NUMBER" \ - --arg pr_head_sha "$EXPECTED_HEAD_SHA" \ - --argjson transport_retry_attempt "$NEXT_ATTEMPT" \ - '{ - event_type: "noema-review", - client_payload: { - target_repository: $target_repository, - pr_number: $pr_number, - pr_head_sha: $pr_head_sha, - transport_retry_attempt: $transport_retry_attempt - } - }' | gh api -X POST "repos/${TARGET_REPOSITORY}/dispatches" --input - - echo "::notice::Scheduled Noema transport continuation re-dispatch for ${TARGET_REPOSITORY}#${PR_NUMBER} at ${EXPECTED_HEAD_SHA} (attempt ${NEXT_ATTEMPT})." - - name: Upload contextual-orchestrator sidecar evidence on failure if: failure() && env.PR_NUMBER != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/opencode-review-coalesce-tick.yml b/.github/workflows/opencode-review-coalesce-tick.yml deleted file mode 100644 index 35f1da22c7..0000000000 --- a/.github/workflows/opencode-review-coalesce-tick.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: OpenCode Review Coalesce Tick - -# Push-burst coalescing. The required review workflows dispatch a full -# multi-hour OpenCode review chain on every `synchronize` push, even when -# several pushes land within seconds of each other -- measured across 4 org -# repositories (419 consecutive-push gaps): density roughly halves right at -# 300s, the clearest inflection point in an otherwise continuous -# distribution. See docs/doctoring/actions-capacity-root-cause-20260917.md. -# -# This tick is the only thing that dispatches a synchronize-triggered -# OpenCode review once coalescing is turned on -- see -# scripts/ci/pr_review_merge_scheduler_core.py's coalesce_enabled() and -# head_stable_for_seconds(), which gate dispatch_opencode_review() itself. -# The job below is skipped (no runner) unless the -# `OPENCODE_REVIEW_COALESCE_ENABLED` repository variable is "true": merging -# this file changes nothing by default. The gate is job-scoped on purpose: -# a step-scoped gate (#2232) forced an inert job onto the org runner queue -# and run 35219385415 sat `queued` for 3h+ with the flag still false -# (docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md). A job-level -# `if:` still produces a completed/`skipped` run record (live: 35191169833 -# finished in 1s) without competing for the plan concurrent-job ceiling. -# When the flag is on, multi-hour admission delay is covered by the -# scheduler fail-open in recent_coalesce_tick_completed() (#2233). -# -# GitHub's required-workflow ruleset only propagates pull_request_target-family -# events to sibling repositories, never `schedule:` (confirmed live, -# docs/doctoring/required-workflow-path-filter-boundary.md) -- a per-repo -# cron committed only here would fire solely for this repository's own PRs. -# This job instead lists every open PR across the organization from this one -# repository in a single run (org-wide GraphQL search), then re-invokes the -# existing, unmodified per-repo scheduler script once per repository that has -# an open PR -- reusing 100% of its existing same-head dedup, admission -# budget, live-head revalidation, and now the coalescing gate itself, instead -# of duplicating any of that logic here. - -on: - schedule: - - cron: "*/5 * * * *" - -concurrency: - # Non-stacking: at most one tick runs at a time, and at most one more - # waits behind it (GitHub Actions concurrency queues, it does not stack - # unboundedly). cancel-in-progress stays false so a tick already in the - # middle of dispatching is never cut off mid-repository. - group: opencode-review-coalesce-tick - cancel-in-progress: false - -permissions: - contents: read - -jobs: - coalesce-tick: - # Skip before runner admission when coalescing is off. Do not move this - # gate back to step scope: that reintroduces multi-hour queue wait for - # an inert echo under the org concurrent-job ceiling. - if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true' - runs-on: ubuntu-24.04 - timeout-minutes: 4 - permissions: - actions: write - checks: read - contents: write - id-token: write - pull-requests: write - statuses: read - env: - GH_TOKEN: ${{ github.token }} - SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github - SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }} - OPENCODE_REVIEW_COALESCE_ENABLED: "true" - OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS: ${{ vars.OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS || '300' }} - steps: - - name: Checkout scheduler scripts - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | - scripts/ci - sparse-checkout-cone-mode: false - - - name: List organization repositories with open pull requests - id: repos - run: | - set -euo pipefail - repos_file="${RUNNER_TEMP}/coalesce-repos.txt" - : >"$repos_file" - cursor="" - for page in 1 2 3 4 5 6 7 8 9 10; do - cursor_arg=() - if [ -n "$cursor" ]; then - cursor_arg=(-f "cursor=$cursor") - fi - response="$(gh api graphql -f query=' - query($cursor: String) { - search(query: "org:ContextualWisdomLab is:pr is:open draft:false", type: ISSUE, first: 100, after: $cursor) { - nodes { ... on PullRequest { repository { nameWithOwner } } } - pageInfo { hasNextPage endCursor } - } - }' "${cursor_arg[@]}" 2>/dev/null || echo '{}')" - printf '%s' "$response" | jq -r '.data.search.nodes[]?.repository.nameWithOwner // empty' >>"$repos_file" - has_next="$(printf '%s' "$response" | jq -r '.data.search.pageInfo.hasNextPage // false')" - [ "$has_next" = "true" ] || break - cursor="$(printf '%s' "$response" | jq -r '.data.search.pageInfo.endCursor')" - done - sort -u "$repos_file" -o "$repos_file" - echo "count=$(wc -l <"$repos_file" | tr -d ' ')" >>"$GITHUB_OUTPUT" - cat "$repos_file" - - - name: Dispatch a coalesced OpenCode review for each stabilized head - env: - REPOS_FILE: ${{ runner.temp }}/coalesce-repos.txt - run: | - set -euo pipefail - if [ ! -s "$REPOS_FILE" ]; then - echo "No open pull requests found org-wide; nothing to coalesce this tick." - exit 0 - fi - while IFS= read -r repo; do - [ -n "$repo" ] || continue - default_branch="$(gh api "repos/${repo}" --jq '.default_branch' 2>/dev/null || echo main)" - # Scoped to review dispatch only: this tick's job is coalescing, - # not merge scheduling or branch freshness, which stay owned by - # the regular per-push/per-review scheduler invocations. - python3 scripts/ci/pr_review_merge_scheduler.py \ - --repo "$repo" \ - --base-branch "$default_branch" \ - --review-workflow "Required OpenCode Review" \ - --review-dispatch-limit -1 \ - --branch-update-limit 0 \ - --no-enable-auto-merge \ - --no-update-branches \ - --trigger-reviews \ - || echo "::warning::Coalesce tick pass failed for ${repo}; continuing with remaining repositories." - done <"$REPOS_FILE" diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index cbc8d21439..d86497b3f4 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -39,28 +39,13 @@ permissions: jobs: validate-pr-metadata: name: validate-pr-metadata - # 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 - # executes untrusted PR-head content or holds a write-capable token -- - # they sit on the same side of the trust boundary that keeps - # coverage-evidence (untrusted test/build execution, `actions: read` - # only) and opencode-review-target (privileged review-publication - # writes) isolated. Folding them removes one of the three needs:-chained - # job-to-job runner-queue re-entries this workflow used to pay under - # saturation; see docs/doctoring/actions-capacity-root-cause-20260917.md - # 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' runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 8 permissions: contents: read pull-requests: read id-token: write - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true outputs: target_repository: ${{ steps.validate.outputs.target_repository }} pr_number: ${{ steps.validate.outputs.pr_number }} @@ -256,12 +241,26 @@ jobs: } >>"$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" + coverage-source-tree: + name: coverage-source-tree + needs: [validate-pr-metadata] + if: >- + needs.validate-pr-metadata.result == 'success' + && github.event_name == 'repository_dispatch' + runs-on: ubuntu-24.04 + timeout-minutes: 12 + permissions: + contents: read + id-token: write + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- github.event_name == 'repository_dispatch' - && steps.validate.outputs.target_repository != '' - && steps.validate.outputs.target_repository != github.repository + && needs.validate-pr-metadata.outputs.target_repository != '' + && needs.validate-pr-metadata.outputs.target_repository != github.repository env: OIDC_AUDIENCE: opencode-github-action OPENCODE_API_BASE_URL: https://api.opencode.ai @@ -329,10 +328,10 @@ jobs: - name: Materialize pull request merge tree for coverage measurement env: GH_TOKEN: ${{ steps.coverage_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} - TARGET_REPOSITORY: ${{ steps.validate.outputs.target_repository }} - PR_NUMBER: ${{ steps.validate.outputs.pr_number }} - PR_BASE_SHA: ${{ steps.validate.outputs.base_sha }} - PR_HEAD_SHA: ${{ steps.validate.outputs.head_sha }} + TARGET_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }} + PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} + PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} + PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar run: | @@ -390,9 +389,11 @@ jobs: coverage-evidence: name: coverage-evidence - needs: [validate-pr-metadata] + needs: [validate-pr-metadata, coverage-source-tree] if: >- - needs.validate-pr-metadata.result == 'success' + always() + && needs.validate-pr-metadata.result == 'success' + && needs.coverage-source-tree.result != 'cancelled' && github.event_name == 'repository_dispatch' runs-on: ubuntu-24.04 timeout-minutes: 300 @@ -457,6 +458,12 @@ jobs: printf 'Materialized trusted coverage contract at %s from validated ref %s.\n' \ "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" "$TRUSTED_SOURCE_REF" + - name: Report coverage source materialization failure + if: needs.coverage-source-tree.result != 'success' + run: | + echo "::error::Coverage source tree could not be materialized; see the coverage-source-tree job log for the exact target repository, base SHA, head SHA, and fetch or merge failure." + exit 1 + - name: Download materialized pull request merge tree uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -633,7 +640,6 @@ jobs: coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build" trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt" trusted_base_python_installer="${GITHUB_WORKSPACE}/scripts/ci/install_base_python_locks.py" - trusted_vcs_import_root_resolver="${GITHUB_WORKSPACE}/scripts/ci/resolve_opencode_base_vcs_import_root.sh" if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then echo "::error::Trusted coverage requirements must be a regular non-symlink file." exit 1 @@ -642,10 +648,6 @@ jobs: echo "::error::Trusted base Python lock installer must be a regular non-symlink file." exit 1 fi - if [ ! -f "$trusted_vcs_import_root_resolver" ] || [ -L "$trusted_vcs_import_root_resolver" ]; then - echo "::error::Trusted VCS import-root resolver must be a regular non-symlink file." - exit 1 - fi sudo rm -rf "$coverage_build_dir" mkdir -p "$coverage_build_dir" chmod 0700 "$coverage_build_dir" @@ -653,8 +655,6 @@ jobs: "$coverage_build_dir/requirements-opencode-review-ci-hashes.txt" install -m 0755 "$trusted_base_python_installer" \ "$coverage_build_dir/install-base-python-locks.py" - install -m 0755 "$trusted_vcs_import_root_resolver" \ - "$coverage_build_dir/resolve-opencode-base-vcs-import-root.sh" python_change_files="${RUNNER_TEMP}/opencode-python-change-files" if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff \ --name-only --diff-filter=ACMRTUXBD -z "$PR_BASE_SHA" HEAD \ @@ -706,18 +706,6 @@ jobs: --base-sha "$PR_BASE_SHA" \ --head-sha "$PR_HEAD_SHA" \ --output-dir "$coverage_build_dir/base-javascript-packages" - # Vendors the base commit's Cargo dependency closure so `cargo llvm-cov` and any - # PyO3/maturin extension a Python test suite imports can build offline inside the - # `--network=none` sandbox below. Confirmed live on fast-mlsirm PRs #1868-#1892: with - # no vendored crates, `cargo llvm-cov` failed on `index.crates.io` DNS resolution and - # the generic Python coverage path failed at collection with `ImportError: cannot - # import name '_core'`, both surfacing as an indistinguishable "Coverage gate: failure" - # even when the pull request itself introduced no regression. - python3 -I "$GITHUB_WORKSPACE/scripts/ci/materialize_base_rust_dependencies.py" \ - --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$PR_BASE_SHA" \ - --output-dir "$coverage_build_dir/base-rust-dependencies" \ - --vendor-dir-for-config /opt/base-rust-dependencies/vendor cat >"$coverage_build_dir/Dockerfile" <<'DOCKERFILE' FROM docker.io/library/python:3.14-slim@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 ENV DEBIAN_FRONTEND=noninteractive @@ -807,16 +795,15 @@ jobs: corepack npm cache verify --cache /opt/npm-cache; \ chmod -R a+rX /opt/corepack /opt/npm-cache /opt/pnpm-store; \ rm -rf /tmp/base-javascript-packages - COPY requirements-opencode-review-ci-hashes.txt requirements-noema-document-ci-hashes.txt /tmp/ + COPY requirements-opencode-review-ci-hashes.txt /tmp/requirements-opencode-review-ci-hashes.txt RUN python3 -m pip install \ --break-system-packages \ --disable-pip-version-check \ --require-hashes \ --only-binary=:all: \ - -r /tmp/requirements-opencode-review-ci-hashes.txt -r /tmp/requirements-noema-document-ci-hashes.txt \ - && rm -f /tmp/requirements-opencode-review-ci-hashes.txt /tmp/requirements-noema-document-ci-hashes.txt + -r /tmp/requirements-opencode-review-ci-hashes.txt \ + && rm -f /tmp/requirements-opencode-review-ci-hashes.txt COPY base-python-requirements /tmp/base-python-requirements - COPY resolve-opencode-base-vcs-import-root.sh /usr/local/libexec/resolve-opencode-base-vcs-import-root.sh RUN set -eu; \ mkdir -p /opt/base-vcs-dependencies; \ site_packages="$(python3 -c 'import site; print(site.getsitepackages()[0])')"; \ @@ -824,8 +811,6 @@ jobs: : >"$path_file"; \ dependency_index=0; \ dependency_list=/tmp/base-vcs-dependencies.tsv; \ - resolver=/usr/local/libexec/resolve-opencode-base-vcs-import-root.sh; \ - test -x "$resolver"; \ jq -r '.[] | [.import_name, .repository, .commit] | @tsv' \ /tmp/base-python-requirements/vcs-manifest.json >"$dependency_list"; \ while IFS="$(printf '\t')" read -r import_name repository commit; do \ @@ -839,18 +824,65 @@ jobs: git -C "$destination" checkout --quiet --detach FETCH_HEAD; \ test "$(git -C "$destination" rev-parse HEAD)" = "$commit"; \ rm -rf -- "$destination/.git"; \ - python_root="$("$resolver" "$destination" "$import_name" "$repository")"; \ + import_root=''; \ + python_root=''; \ + candidate_count=0; \ + for candidate in \ + "$destination/src/$import_name" \ + "$destination/src/$import_name.py" \ + "$destination/$import_name" \ + "$destination/$import_name.py"; do \ + if [ -e "$candidate" ] || [ -L "$candidate" ]; then \ + import_root="$candidate"; \ + candidate_count=$((candidate_count + 1)); \ + fi; \ + done; \ + if [ "$candidate_count" -ne 1 ]; then \ + printf 'locked VCS source %s has a missing or ambiguous import root for %s\n' \ + "$repository" "$import_name" >&2; \ + exit 1; \ + fi; \ + if [ -L "$import_root" ] \ + || { [ -d "$import_root" ] \ + && { [ ! -f "$import_root/__init__.py" ] \ + || [ -L "$import_root/__init__.py" ]; }; }; then \ + printf 'locked VCS source %s has a namespace or linked import root for %s\n' \ + "$repository" "$import_name" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type l -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains a symbolic-link layout\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type f \ + \( -name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.dylib' \) \ + -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains a compiled extension\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type d \ + \( -name '*.dist-info' -o -name '*.egg-info' \) \ + -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains installed distribution metadata\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + case "$import_root" in \ + "$destination/src/"*) python_root="$destination/src" ;; \ + *) python_root="$destination" ;; \ + esac; \ printf '%s\n' "$python_root" >>"$path_file"; \ dependency_index=$((dependency_index + 1)); \ done <"$dependency_list"; \ - rm -f -- "$dependency_list" "$resolver"; \ + rm -f -- "$dependency_list"; \ chmod -R a+rX /opt/base-vcs-dependencies "$path_file" COPY install-base-python-locks.py /usr/local/libexec/install-base-python-locks.py RUN python3 -I /usr/local/libexec/install-base-python-locks.py \ --requirements-root /tmp/base-python-requirements \ && rm -rf /tmp/base-python-requirements \ && rm -f /usr/local/libexec/install-base-python-locks.py - COPY base-rust-dependencies /opt/base-rust-dependencies DOCKERFILE if ! docker build --pull --no-cache --network=default \ --tag "$coverage_tool_image" \ @@ -935,17 +967,6 @@ jobs: fi mkdir -p "$RUNNER_TEMP" /work/.opencode-sandbox-home /work/.opencode-sandbox-cache chown "$OPENCODE_SANDBOX_UID:$OPENCODE_SANDBOX_GID" /work/.opencode-sandbox-home /work/.opencode-sandbox-cache - # `run_and_capture`/`run_and_capture_advisory` below pin CARGO_HOME to - # /work/.opencode-sandbox-home/.cargo, which lives on the mutable /work bind mount, not - # the read-only image -- so the baked offline vendor config from - # /opt/base-rust-dependencies (see materialize_base_rust_dependencies.py) has to be - # copied there explicitly rather than set as an image ENV default. - if [ -s /opt/base-rust-dependencies/cargo-config.toml ]; then - mkdir -p /work/.opencode-sandbox-home/.cargo - install -m 0444 /opt/base-rust-dependencies/cargo-config.toml \ - /work/.opencode-sandbox-home/.cargo/config.toml - chown -R "$OPENCODE_SANDBOX_UID:$OPENCODE_SANDBOX_GID" /work/.opencode-sandbox-home/.cargo - fi chmod 0700 "$RUNNER_TEMP" : >"$GITHUB_OUTPUT" chmod 0600 "$GITHUB_OUTPUT" @@ -1224,37 +1245,10 @@ jobs: --workflow-dir "$workflow_dir" } - project_is_maturin_project() { - local pyproject="${1}/pyproject.toml" - [ -f "$pyproject" ] || return 1 - grep -Eq '^\s*build-backend\s*=\s*"maturin' "$pyproject" - } - - # Builds the PyO3/maturin extension module (e.g. `fast_mlsirm._core`) fully offline - # before pytest runs, using the Cargo vendor config baked in by - # materialize_base_rust_dependencies.py (see the /work/.opencode-sandbox-home/.cargo - # setup above). Without this, coverage collection fails with `ImportError: cannot - # import name '_core'` because nothing else in the sandbox ever builds the compiled - # extension. CARGO_BUILD_JOBS=1 keeps the offline build within the sandbox's memory - # budget. - build_maturin_extension_if_needed() { - local project_dir="$1" - project_is_maturin_project "$project_dir" || return 0 - run_and_capture "Offline PyO3/maturin extension build (${project_dir})" \ - env CARGO_NET_OFFLINE=true CARGO_BUILD_JOBS=1 \ - bash -c 'set -eu - cd "$1" - dist_dir="$(mktemp -d)" - python3 -m maturin build --offline --release -o "$dist_dir" - python3 -m pip install --user --no-index --no-deps --force-reinstall "$dist_dir"/*.whl - rm -rf "$dist_dir"' bash "$project_dir" - } - run_python_test_coverage() { local measured_projects=0 while IFS= read -r project_dir; do measured_projects=1 - build_maturin_extension_if_needed "$project_dir" configured_commands_json="$(configured_python_ci_test_commands "$project_dir")" if [ -n "$configured_commands_json" ]; then while IFS= read -r configured_command_json; do @@ -5299,13 +5293,7 @@ jobs: publish_fallback_diff_review() { local body_file event body_file="$(mktemp)" - # A COMMENT here can never satisfy opencode_review_receipt_gate.py's - # FORMAL_STATES, so the required workflow's "Fail closed without a - # current-head OpenCode verdict" job never sees a receipt, the - # rerun step gated on that receipt is skipped, and the required - # check fails closed forever instead of settling on an honest - # verdict. - event="REQUEST_CHANGES" + event="COMMENT" python3 scripts/ci/opencode_review_surfaces.py build-fallback-review \ --changed-files-file "${OPENCODE_CHANGED_FILES_FILE}" \ --source-root "${OPENCODE_SOURCE_WORKDIR}" \ @@ -5316,10 +5304,9 @@ jobs: >"$body_file" printf '\n%s\n\n%s\n' "## Review outcome" "Coverage is a gate, not the review. This body reviews the changed product files." >>"$body_file" create_pull_review "$event" "$(cat "$body_file")" - # create_pull_review REQUEST_CHANGES rewrites the status comment to - # Gate result: REQUEST_CHANGES. Restore the coverage gate so a miss - # never looks finished; next action stays "fix coverage evidence, - # then rerun". + # create_pull_review COMMENT rewrites the status comment to Gate + # result: COMMENT. Restore the coverage gate so a miss never looks + # finished; next action stays "fix coverage evidence, then rerun". request_changes_for_coverage_evidence_failure rm -f "$body_file" } diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index ec94e6d24e..19ea58003f 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -33,22 +33,7 @@ permissions: jobs: required-workflow-bootstrap: name: required-workflow-bootstrap - # Folded together with the former admit-current-head job (2026-09-17): - # both only ever read PR metadata via the default `github.token` (no - # untrusted PR-content execution, no elevated token), the same trust - # level, and admit-current-head was not itself a required branch- - # protection context (this job's name is, so it stays). Folding removes - # one needs:-chained job-to-job runner-queue re-entry; measured on - # .github PR #2183 run 35042040116: this exact boundary cost 4h46m of - # queue wait between two single-digit-second jobs. See - # docs/doctoring/actions-capacity-root-cause-20260917.md for the - # underlying measurement methodology. runs-on: ubuntu-24.04 - permissions: - contents: read - pull-requests: read - outputs: - admitted: ${{ steps.live_head.outputs.admitted }} steps: - name: Materialize the required review workflow run: >- @@ -241,27 +226,27 @@ jobs: TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || 0 }} PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - # The base branch's tip SHA at event time -- already-reviewed, - # already-merged state. Threaded through so the issue #2193 - # research/data artifact path declaration can be resolved only - # from here, never from the untrusted PR head; see - # `evaluate_pull_request`'s `base_ref` parameter. - PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }} EVENT_ACTION: ${{ github.event.action || 'unknown' }} run: | set -euo pipefail - base_ref_args=() - if [ -n "$PULL_REQUEST_BASE_SHA" ]; then - base_ref_args=(--base-ref "$PULL_REQUEST_BASE_SHA") - fi python3 .cwl-required-source/scripts/ci/pingora_edge_policy.py \ --repository "$TARGET_REPOSITORY" \ --pull-request "$PULL_REQUEST_NUMBER" \ --head-sha "$PULL_REQUEST_HEAD_SHA" \ --event-action "$EVENT_ACTION" \ - --api-url "https://api.github.com" \ - "${base_ref_args[@]}" + --api-url "https://api.github.com" + admit-current-head: + name: admit-current-head + needs: [required-workflow-bootstrap] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + admitted: ${{ steps.live_head.outputs.admitted }} + permissions: + contents: read + pull-requests: read + steps: - name: Admit only the exact live OpenCode head id: live_head env: @@ -291,73 +276,10 @@ jobs: echo "admitted=true" >>"$GITHUB_OUTPUT" echo "Exact live OpenCode head admitted for ${TARGET_REPOSITORY}#${PR_NUMBER}." - changed-scope: - name: Detect changed scope - # Same job-level docs/image-only gate as strix.yml, security-scan.yml, - # sast-semgrep.yml, and codeql-pr.yml (see - # docs/doctoring/required-workflow-path-filter-boundary.md): the org - # ruleset ignores every `on:` filter when it runs this workflow in - # another repository, so the doc/image-only decision has to be made in - # a job and consumed through `needs`, not the trigger. OpenCode review - # previously dispatched its full multi-hour coverage+model chain for - # every PR event including docs/changelog-only diffs; this closes that - # gap using the identical classifier. Fails OPEN: an unreadable, empty, - # or truncated file list reviews everything. - if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: - contents: read - pull-requests: read - outputs: - code: ${{ steps.scope.outputs.code }} - deps: ${{ steps.scope.outputs.deps }} - steps: - - name: Classify changed paths - id: scope - env: - GH_TOKEN: ${{ github.token }} - REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }} - PR: ${{ github.event.pull_request.number }} - EXPECTED_FILES: ${{ github.event.pull_request.changed_files }} - shell: bash - run: | - set -uo pipefail - code=true - deps=true - if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then - # No retry loop here, unlike the identical classifier elsewhere - # (e.g. strix.yml): this required workflow is contract-tested to - # never retry or poll (tests/test_opencode_required_verdict_regression.py), - # so a single failed read falls straight through to the - # already-safe "scan everything" fallback below instead. - changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename' || true)" - # GitHub caps /pulls/N/files at 3000 entries; a short list would hide - # source files behind a doc-only verdict, so require an exact count. - if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then - code=false - deps=false - while IFS= read -r changed_path; do - case "$changed_path" in - *.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;; - *) code=true ;; - esac - case "$changed_path" in - requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;; - esac - done <<<"$changed" - else - echo "::notice::changed-scope could not read a complete PR file list; scanning everything." - fi - fi - echo "code=${code}" >> "$GITHUB_OUTPUT" - echo "deps=${deps}" >> "$GITHUB_OUTPUT" - echo "changed-scope code=${code} deps=${deps}" - coverage-source-tree: name: coverage-source-tree - needs: [required-workflow-bootstrap] - if: needs.required-workflow-bootstrap.outputs.admitted == 'true' + needs: [required-workflow-bootstrap, admit-current-head] + if: needs.admit-current-head.outputs.admitted == 'true' runs-on: ubuntu-24.04 steps: - run: >- @@ -376,8 +298,8 @@ jobs: # `admit-current-head` directly lets the two run in parallel. The `if:` below # restates the admission gate this job previously inherited transitively # through coverage-source-tree, so an unadmitted head still skips it. - needs: [required-workflow-bootstrap] - if: needs.required-workflow-bootstrap.outputs.admitted == 'true' + needs: [required-workflow-bootstrap, admit-current-head] + if: needs.admit-current-head.outputs.admitted == 'true' runs-on: ubuntu-24.04 steps: - run: >- @@ -395,8 +317,8 @@ jobs: # created until its `needs:` finish, so this link cost a further 12h13m of # queue wait on naruon#1528 (run 33581213805). Admission is still enforced # directly by this job's own `if:` below, not inherited through that edge. - needs: [required-workflow-bootstrap, changed-scope] - if: needs.required-workflow-bootstrap.outputs.admitted == 'true' + needs: [admit-current-head] + if: needs.admit-current-head.outputs.admitted == 'true' runs-on: ubuntu-24.04 permissions: contents: read @@ -404,7 +326,7 @@ jobs: id-token: write steps: - name: Request current-head OpenCode review execution - if: github.event.action != 'closed' && needs.changed-scope.outputs.code != 'false' + if: github.event.action != 'closed' env: GH_TOKEN: ${{ github.token }} OIDC_AUDIENCE: opencode-github-action @@ -516,17 +438,12 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_ACTION: ${{ github.event.action }} PR_DRAFT: ${{ github.event.pull_request.draft }} - CHANGED_SCOPE_CODE: ${{ needs.changed-scope.outputs.code }} run: | set -euo pipefail if [ "$PR_ACTION" = "closed" ]; then echo "PR closed; a current-head OpenCode verdict is not required." exit 0 fi - if [ "${CHANGED_SCOPE_CODE:-true}" = "false" ]; then - echo "PR contains no reviewable code changes (docs/image-only diff); a current-head OpenCode verdict is not required." - exit 0 - fi if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict." exit 1 diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index d98a72e605..d32918cf45 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -122,7 +122,6 @@ jobs: contents: write id-token: write pull-requests: write - statuses: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml index 1788cfd40b..8453895027 100644 --- a/.github/workflows/python-security.yml +++ b/.github/workflows/python-security.yml @@ -227,30 +227,6 @@ jobs: run: | set -euo pipefail status=0 - audit_log="$(mktemp)" - - # Run one pip-audit invocation and classify a non-zero exit. pip-audit - # 2.10.1 prints "Found N known vulnerabilit(y|ies) ... in N package(s)" - # on stderr only when it has findings (pip_audit/_cli.py); any other - # non-zero exit is an audit-service/transport failure (advisory query - # traceback, resolver/_fatal error) and must not be reported as a - # vulnerability finding (#2158). Both keep the gate closed. - run_audit() { - local label="$1" - shift - echo "::group::pip-audit ${label}" - if pip-audit "$@" 2>&1 | tee "${audit_log}"; then - echo "::endgroup::" - return 0 - fi - echo "::endgroup::" - status=1 - if grep -Eq "Found [0-9]+ known vulnerabilit" "${audit_log}"; then - echo "::error::pip-audit found known-vulnerable Python dependencies in ${label}. Remediate the pins listed above." - else - echo "::error::pip-audit could not complete for ${label}: $(grep -Ev '^[[:space:]]*$' "${audit_log}" | tail -n 1). This is an audit-service/transport failure, not a vulnerability finding; rerun the job before treating it as a security result." - fi - } # Audit every discovered requirements file. while IFS= read -r req; do @@ -275,11 +251,15 @@ jobs: base="${req%.txt}" unhashed_base="${base%-hashes}" if [ "$base" != "$unhashed_base" ] && [ -f "${unhashed_base}-overrides.txt" ]; then - run_audit "-r ${req} (--disable-pip --no-deps: overridden lock)" --strict --desc=on --no-deps --disable-pip -r "${req}" + echo "::group::pip-audit -r ${req} (--disable-pip --no-deps: overridden lock)" + pip-audit --strict --desc=on --no-deps --disable-pip -r "${req}" || status=1 + echo "::endgroup::" elif [ "$base" = "$unhashed_base" ] && [ -f "${unhashed_base}-overrides.txt" ]; then echo "::notice::Skipping pip-audit for ${req}: it is the raw input to an overridden lock (${unhashed_base}-hashes.txt), never itself a pip install --require-hashes target, and its compiled hashes file is audited separately with full resolution." else - run_audit "-r ${req}" --strict --desc=on -r "${req}" + echo "::group::pip-audit -r ${req}" + pip-audit --strict --desc=on -r "${req}" || status=1 + echo "::endgroup::" fi done < <(find . -type f -name 'requirements*.txt' -not -path './.git/*') @@ -287,10 +267,12 @@ jobs: if find . -maxdepth 2 -type f \ \( -name 'pyproject.toml' -o -name 'pylock.*.toml' \) \ -not -path './.git/*' -print -quit | grep -q .; then - run_audit ". (project manifest)" --strict --desc=on . + echo "::group::pip-audit . (project manifest)" + pip-audit --strict --desc=on . || status=1 + echo "::endgroup::" fi if [ "${status}" != "0" ]; then - echo "::error::pip-audit failed for at least one input; the per-input errors above say whether it was a finding or an audit-service failure." + echo "::error::pip-audit reported known-vulnerable Python dependencies." exit 1 fi diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..12b7013da3 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -38,36 +38,25 @@ permissions: contents: read jobs: - semgrep: - name: Semgrep (multi-language SAST) + changed-scope: + name: Detect changed scope # The org ruleset IGNORES every `on:` filter (paths, branches, types) when it # runs this workflow in another repository, and a trigger-level skip would # leave `.github`'s classic required contexts Pending forever. Both - # mechanisms honour a job that runs and concludes on its own, so the - # doc/image-only decision is made by the classifier step below and consumed - # by the expensive steps' `if:` guards. See + # mechanisms honour a JOB-level skip, so the doc/image-only decision is made + # 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. - # The gate lives inside this job as a step-level guard (one runner, not two). if: github.event.action != 'closed' runs-on: ubuntu-24.04 + timeout-minutes: 5 permissions: contents: read pull-requests: read - security-events: write - actions: read - env: - # Deterministic, no telemetry: registry rules are fetched but no scan data - # is sent back. - SEMGREP_SEND_METRICS: "off" - # Semgrep OSS 1.169.0. Keep the immutable manifest reference in one - # place so hosted scans and local reproduction cannot drift. - SEMGREP_IMAGE: "semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" + outputs: + code: ${{ steps.scope.outputs.code }} + deps: ${{ steps.scope.outputs.deps }} steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - name: Classify changed paths id: scope env: @@ -110,15 +99,35 @@ jobs: echo "code=${code}" >> "$GITHUB_OUTPUT" echo "deps=${deps}" >> "$GITHUB_OUTPUT" echo "changed-scope code=${code} deps=${deps}" + + semgrep: + name: Semgrep (multi-language SAST) + needs: changed-scope + if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' + runs-on: ubuntu-24.04 + permissions: + contents: read + security-events: write + actions: read + env: + # Deterministic, no telemetry: registry rules are fetched but no scan data + # is sent back. + SEMGREP_SEND_METRICS: "off" + # Semgrep OSS 1.169.0. Keep the immutable manifest reference in one + # place so hosted scans and local reproduction cannot drift. + SEMGREP_IMAGE: "semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit - name: Checkout exact submitted revision - if: steps.scope.outputs.code == 'true' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Verify exact submitted revision - if: steps.scope.outputs.code == 'true' env: EXPECTED_CHECKOUT_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }} EXPECTED_CHECKOUT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} @@ -131,7 +140,6 @@ jobs: fi echo "SAST_CHECKOUT scanner=semgrep repository=${EXPECTED_CHECKOUT_REPOSITORY} expected_sha=${EXPECTED_CHECKOUT_SHA} actual_sha=${actual_sha}" - name: Verify pinned Semgrep manifest - if: steps.scope.outputs.code == 'true' run: | set -euo pipefail if [[ "${SEMGREP_IMAGE}" =~ ^semgrep/semgrep@sha256:[0-9a-f]{64}$ ]]; then @@ -143,7 +151,6 @@ jobs: fi - name: Run Semgrep (SARIF) id: semgrep - if: steps.scope.outputs.code == 'true' run: | set +e echo "Using ${SEMGREP_IMAGE}" @@ -212,7 +219,7 @@ jobs: echo "SEMGREP_ENGINE_FAILURE rc=${SEMGREP_RC:-missing}: Semgrep failed without a WARNING/ERROR SARIF result; inspect the scan command output above." fi - name: Enforce Semgrep gate (fail on Medium+ findings) - if: always() && steps.scope.outputs.code == 'true' && (steps.semgrep_sarif.outputs.finding_count != '0' || steps.semgrep.outputs.rc != '0') + if: always() && (steps.semgrep_sarif.outputs.finding_count != '0' || steps.semgrep.outputs.rc != '0') env: SEMGREP_RC: ${{ steps.semgrep.outputs.rc }} SEMGREP_FINDING_COUNT: ${{ steps.semgrep_sarif.outputs.finding_count }} diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..500e22b4ab 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -164,7 +164,7 @@ jobs: with: scan-args: | --format=json - --output-file=old-results.json + --output=old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -182,7 +182,7 @@ jobs: with: scan-args: | --format=json - --output-file=old-results.json + --output=old-results.json --no-resolve --allow-no-lockfiles -r @@ -215,7 +215,7 @@ jobs: with: scan-args: | --format=json - --output-file=new-results.json + --output=new-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -233,7 +233,7 @@ jobs: with: scan-args: | --format=json - --output-file=new-results.json + --output=new-results.json --no-resolve --allow-no-lockfiles -r @@ -286,7 +286,7 @@ jobs: uses: google/osv-scanner-action/osv-reporter-action@8e5cf47b818121e8b405931c82126c2630b0b20d # v2.3.8 with: scan-args: | - --output-files=results.sarif + --output=results.sarif --old=old-results.json --new=new-results.json --gh-annotations=true @@ -323,10 +323,6 @@ jobs: uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: results.sarif - # The exact head checkout lives in `source`, not the workspace root; - # without this binding upload-sarif logs "does not appear to be a git - # repository" twice and falls back to server-derived commit identity. - checkout_path: ${{ github.workspace }}/source # results.sarif is produced after checkout of the pull request head. # Uploading it against refs/pull/*/merge can race GitHub's synthetic # merge ref and fail with "commit_oid is not a merge commit". diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 2da382f934..56883d5669 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -43,11 +43,7 @@ **Vulnerability:** Denial of Service / Availability **Learning:** Strix security scanners crashed when the backend LLM returned an 'internal server error' HTTP 500 response. This was because 'internal server error' string match was missing from the `is_llm_api_connection_error` function in the Strix retry gate. **Prevention:** Always include `internal server error` in string match conditions when handling HTTP API Connection exceptions for LLM backends to ensure proper fail-closed and retry handling. -## 2024-05-19 - Path Traversal Vulnerability in Unbounded Regex Expressions -**Vulnerability:** Unbounded regular expressions for repository and organization names, such as `^[A-Za-z0-9_.-]+$`, allowed path traversal if user data ended in `.` or `..`. -**Learning:** End-of-string anchors within unbounded lookaheads (e.g. `(?!.*(?:\.\.|\.$|^\.))`) unintentionally fail matches when valid data is followed by trailing text. Bounding validation solely to the captured characters requires simpler lookaheads combined with character-class repetition. -**Prevention:** Always use negative lookaheads without end-of-string anchors (e.g. `^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$`) to prevent path traversal in parameters used for URL construction or file access. -## 2024-05-20 - Unhandled 502 Bad Gateway causing DoS in LLM integration -**Vulnerability:** Denial of Service / Availability -**Learning:** Strix security scanners crashed when the backend LLM returned an 'HTTP Error 502: Bad Gateway' response. This was because 'bad gateway' string match and generic 'APIError' were missing from the `is_llm_api_connection_error` function in the Strix retry gate. -**Prevention:** Always include `bad gateway` and `APIError` in string match conditions when handling HTTP API Connection exceptions for LLM backends to ensure proper fail-closed and retry handling. +## 2026-09-12 - Prevent Command Injection via Explicit shell=False in Subprocess +**Vulnerability:** Command Injection & SSRF Bypass Risk (Implicit Shell Execution) +**Learning:** Functions executing system commands, like `_probe_isolation_capability` using `subprocess.run`, implicitly default to `shell=False`. However, not explicitly declaring it allows security linters (like Bandit) to report false positives, and obscures the security posture against command injection if untrusted inputs were to reach the execution arguments. +**Prevention:** Always explicitly define `shell=False` in `subprocess.run()` and `subprocess.Popen()` calls, even when it is the default behavior. Ensure corresponding unit tests explicitly verify this configuration by asserting `kwargs.get("shell") is False` in mock implementations. diff --git a/CHANGELOG.d/20260914-pingora-declared-artifact-paths.md b/CHANGELOG.d/20260914-pingora-declared-artifact-paths.md deleted file mode 100644 index e5f375c909..0000000000 --- a/CHANGELOG.d/20260914-pingora-declared-artifact-paths.md +++ /dev/null @@ -1,3 +0,0 @@ -### Pingora edge policy admits declared research/data artifact paths - -- `scripts/ci/pingora_edge_policy.py` previously admitted binary or non-UTF-8 content only by path shape (`DOCUMENTATION_DIRECTORIES` via `_is_known_documentation_path`, plus the `evidence`/`figures` publication directories from #2149), so a research repository's raw data and fitted-model artefacts kept elsewhere by deliberate, owner-approved design -- e.g. `ContextualWisdomLab/late-life-anxiety-reanalysis`'s `local/` and evidence-preservation paths -- were rejected on path shape alone, with no route except relocating them under `docs/` (already done once, for 66 images) or leaving the PR unmergeable. `evaluate_pull_request` now accepts an optional `base_ref` and, when given, resolves a new `.github/edge-policy-artifact-paths.txt` declaration (one relative path prefix per line, no globs, capped at `MAX_DECLARED_ARTIFACT_PREFIXES=64` entries and `MAX_DECLARED_ARTIFACT_PREFIX_DEPTH=8` segments) **only from that base ref, never the pull-request head** -- a PR that adds or widens the declaration gets no benefit from it until that change is itself reviewed and merged, proven by a same-PR self-authorization regression test. The declaration replaces only the path-shape test: `_runtime_path_rule` matches stay rejected inside a declared prefix exactly as inside `docs/` today, and a suffix with no `BINARY_DOCUMENT_MAGIC` entry (most research-data formats have none -- `.xlsx`, `.sav`, `.rds`, `.npz`, …) is admitted only on the stricter "no diff patch + fetched bytes are not valid UTF-8" evidence, so a file that decodes as valid UTF-8 is always still content-scanned, never silently admitted. `.hwpx`/`.pdf`/`.png` under a declared prefix keep the existing structural-evidence checks. A malformed declaration (absolute path, `..`, bare `.`/`/`, a glob character, or over either bound) is a hard `PolicyError` naming the offending entry; a repository with no declaration file at all behaves identically to before this feature existed. `evaluate_pull_request` now also emits a `::notice::` naming the declared prefix and the base ref it came from whenever a declared-prefix admission occurs, so a reviewer can trace it back to the reviewed declaration. `.github/workflows/opencode-review.yml`'s `pull_request_target`-derived `github.event.pull_request.base.sha` is threaded through as `--base-ref` with no new permissions. `tests/test_pingora_edge_policy.py` adds coverage for base-ref admission, the self-authorization refusal, runtime-form and valid-UTF-8 rejection inside a declared prefix, every malformed-declaration shape, and the no-declaration regression guard; `tests/test_pingora_edge_workflow_contract.py` pins the new workflow wiring. `pingora_edge_policy.py` remains 100% branch coverage and 100% `interrogate` docstring coverage. Refs #2193, #2149, #2116. diff --git a/CHANGELOG.d/20260917-codeql-versioned-handler-bootstrap.md b/CHANGELOG.d/20260917-codeql-versioned-handler-bootstrap.md deleted file mode 100644 index 55741503cc..0000000000 --- a/CHANGELOG.d/20260917-codeql-versioned-handler-bootstrap.md +++ /dev/null @@ -1,14 +0,0 @@ -## Changed - -- Add a backward-compatible `codeql-scan`/`codeql-scan-v2` protocol bridge to - the single protected CodeQL dispatch handler. Legacy clients keep their - exact title, payload, and status context while v2 requires source/base/head - provenance. Language scans are `actions:read`; one post-matrix settlement - revalidates the live PR, required run/jobs, handler gate steps, and SARIF - artifacts before one run-wide rerun. The legacy path has an explicit - protected-v2/in-flight-drain/zero-caller removal condition. Failed - credential attempts retain their diagnostics but cannot leak an HTTP error - body into a later successful API response. Nested rerun authority is bound - to string schema `"1"`, and settlement stops before mutation when the - required run reaches attempt 48, preserving capacity below GitHub's limit of - 50 re-runs. ADR-0025. diff --git a/CHANGELOG.d/20260917-coverage-vcs-python-root-helper.md b/CHANGELOG.d/20260917-coverage-vcs-python-root-helper.md deleted file mode 100644 index 6f85d0d7dc..0000000000 --- a/CHANGELOG.d/20260917-coverage-vcs-python-root-helper.md +++ /dev/null @@ -1,15 +0,0 @@ -### Coverage image VCS import-root resolver is executable and contract-proven - -- #2123 already admitted immutable `python/` layouts so the trusted coverage - tool image no longer dies at docker step #17 on `fast-mlsirm@09f762ded` - (`python/fast_mlsirm`). The #2157 follow-up extracts that exact admission logic into - `scripts/ci/resolve_opencode_base_vcs_import_root.sh`, which - `opencode-review-dispatch.yml` installs into the coverage build context and the - Dockerfile `COPY`s/executes — so candidate discovery cannot silently drift inside an - untested HEREDOC. `tests/test_opencode_vcs_python_source_root_contract.py` proves - `python/` package and single-module layouts resolve, `src/` and repository-root - layouts still resolve, and missing/ambiguous/namespace/compiled trees still fail - closed with the historical diagnostics. Doctoring - `docs/doctoring/opencode-vcs-python-source-root.md` and gap - `CONTROL-OPENCODE-VCS-PYROOT-01` record #2123 supersession; issue #2157 stays open - until a consumer `coverage-evidence` job past step #17 is linked. Refs #2157, #2123. diff --git a/CHANGELOG.d/20260917-maturin-offline-coverage-build.md b/CHANGELOG.d/20260917-maturin-offline-coverage-build.md deleted file mode 100644 index 3d636602f7..0000000000 --- a/CHANGELOG.d/20260917-maturin-offline-coverage-build.md +++ /dev/null @@ -1,19 +0,0 @@ -### Coverage sandbox builds PyO3/maturin extensions offline before pytest - -- `maturin==1.15.0` (MIT/Apache-2.0) is added to `requirements-opencode-review-ci.txt` / - `requirements-opencode-review-ci-hashes.txt` (hashes verified against PyPI JSON metadata for the - exact release), closing the last gap `materialize_base_rust_dependencies.py` (#2222, #2223) left - open: the base commit's Cargo dependency graph was vendored for `cargo llvm-cov`, but nothing - ever built the PyO3 extension itself, so `python3 -m coverage run -m pytest` kept failing - collection with `ImportError: cannot import name '_core'` on 8 of the last 10 fast-mlsirm - fallbacks (fast-mlsirm#1907). `.github/workflows/opencode-review-dispatch.yml`'s - `run_python_test_coverage` now calls a new `build_maturin_extension_if_needed` helper for every - tracked Python project whose `pyproject.toml` declares `build-backend = "maturin"`: it runs - `maturin build --offline --release` against the vendored Cargo dependencies with - `CARGO_NET_OFFLINE=true CARGO_BUILD_JOBS=1` (the sandbox is memory-constrained), then - `pip install --user --no-index --no-deps` installs the built wheel before pytest runs, entirely - inside the existing `--network=none` sandbox. `tests/test_maturin_offline_build_contract.py` - proves both halves of the claim against a real PyO3 fixture crate: the vendored-offline build - produces an importable `_core` extension, and a dependency only a pull request's head added - (never seen by the base-commit materializer) is never fetched -- the offline build fails closed - on the missing crate instead of reaching the network. Refs fast-mlsirm#1907. diff --git a/CHANGELOG.md b/CHANGELOG.md index 34281625cb..707c18532e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,3 @@ -### Noema transport capacity schedules a bounded continuation re-dispatch - -- After gateway failover, HTTP 429/5xx no longer end only as a permanent required-check failure with `caller attempts=1`. ADR-0031 classifies that class as `provider_capacity_unavailable`, keeps the single gateway request per job, surfaces `provider_attempt_count` from the orchestrator error envelope, and authorizes at most two same-head `repository_dispatch` retries after a capped `Retry-After` or deterministic 60–180 s jitter. Review is never skipped. Refs #2165. - -### Strix evidence binding distinguishes PR-delta from baseline and fails closed on false remediation - -- Required Strix on `.github#2106` attributed findings against base-identical `scripts/ci/pingora_edge_policy.py` / `scripts/ci/contextual_orchestrator_review_policy.py` as if they were PR-introduced (#2159). Separately, LineageWeave Strix run `34746057545` claimed a fix was "already applied" after `apply_patch` missed `/workspace/backend/app/main.py` (#2168). `scripts/ci/strix_evidence_binding.py` now classifies findings as `pr_delta` / `repository_baseline` / `context_dependency` / `unmapped` against the authenticated changed-file inventory (renames + hunks), and remediation claims fail closed unless workspace bytes or a source commit receipt prove the edit. The gate labels decisions with `evidence_scope=` and sanitizes report artifacts after each attempt. Contract tests: `tests/test_strix_evidence_binding.py`; doctoring: `docs/doctoring/strix-evidence-binding-2159-2168.md`. - -### OpenCode coverage admits immutable `python/` VCS source roots - -- Central OpenCode coverage run [34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) failed before executing `contextual-orchestrator#1149`: the trusted image builder resolved VCS packages only from repository root or `src/`, while the exact immutable `fast-mlsirm@09f762ded35786dd1078222a4577ff09d649816f` exposes `fast_mlsirm` from `python/fast_mlsirm`. The builder now admits the explicit `python/` source root, retains the one-and-only-one import-root invariant, symlink/namespace/compiled-artifact/installed-metadata rejection, exact commit verification, and the later credential-free networkless sandbox. Contract tests pin both package and single-module `python/` layouts. Refs `contextual-orchestrator#1149`. Exact-head Runtime Quality [job 103581110552](https://github.com/ContextualWisdomLab/.github/actions/runs/34704176931/job/103581110552) then caught the required independent workflow-blob trust pin still naming the predecessor blob; `683cb053` advances only that pin to exact blob `f315683208d57ba89a2942502c525abe7355e2fd`. - -### Contextual-orchestrator pin advance removes the implicit 90 s model request timeout - -- Advanced the central sidecar's pinned immutable CO revision from `414f2297` to protected `main@767e67fbc6b881a452761f32abb69b9971b9b03b`, carrying contextual-orchestrator#1053 into Strix, OpenCode, and Noema. Root cause: `ModelClient.__init__` defaulted `timeout=90`, and the review gateway constructed its client without a timeout, so long structured-output completions on NVIDIA NIM (`google/gemma-4-31b-it`) hit `TimeoutError` at exactly 90 s on every attempt; the orchestrator then cycled circuit open/reset on the same route for ~20 min and answered `502 provider_connection_error` (fast-mlsirm#1860 run 34748511702, sidecar artifact 10315556637: 15 of 27 failed attempts at 90.0 s; fast-mlsirm#1825 run 34752130895 same signature). #1053 removes the implicit deadline (null by default, administrator `model_timeout_seconds` per model) and was merged under the infrastructure exception because the pre-fix sidecar was failing its own Noema/OpenCode gates. Hosted acceptance is the first Noema/OpenCode/Strix run on this pin against a consumer PR; not claimed here. Refs ContextualWisdomLab/contextual-orchestrator#1053, ContextualWisdomLab/fast-mlsirm#1860. - -### Pingora edge policy admits HWPX evidence documents without UTF-8 decoding - -- `scripts/ci/pingora_edge_policy.py`'s `BINARY_DOCUMENT_MAGIC` only knew `.pdf` and `.png`, and `_is_binary_documentation_asset` only admitted a `doc`/`docs`/`documentation` directory, so a ZIP-based `.hwpx` evidence attachment under `evidence/` matched neither rule and fell through to the strict UTF-8 decode every other candidate gets. Observed on ContextualWisdomLab/late-life-anxiety-reanalysis#10, head `a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700409497`, job `103571044859`: "Pingora edge policy could not establish complete evidence: Runtime policy candidate evidence/reviewer_response_draft.hwpx is not valid UTF-8". The fix adds `.hwpx` (`PK\x03\x04`) to `BINARY_DOCUMENT_MAGIC` and extends `_is_binary_documentation_asset` to admit an `.hwpx` under an `evidence` path segment, gated on a bounded container check in the new `_is_complete_hwpx` -- unprefixed ZIP, exact EOCD record, unique members with `mimetype` first, a stored (not deflated) `mimetype` entry exactly `application/hwp+zip`, and a non-empty, unencrypted `Contents/content.hpf` manifest -- so no document body is ever parsed or rendered and no malware inspection is implied. The runtime-path guard and the Nginx-runtime-text fallback scan for disguised or malformed archives are unchanged. `tests/test_pingora_hwpx_evidence.py` runs the production policy boundary offline: RED (test-only apply) showed 3 failing / 19 passing; GREEN (full patch) showed 90 passing across that file plus `tests/test_pingora_edge_policy.py` and `tests/test_pingora_edge_workflow_contract.py`. Branch coverage of the touched module is 100% (388 statements, 174 branches, 0 missed) and `interrogate scripts/ci -q` reports 100.0% docstrings. Hosted acceptance still requires a newly loaded central source SHA to re-run the consumer's exact head bootstrap. Refs ContextualWisdomLab/.github#2116. - -### Review policy ZDR feed keys routes by the wrong field, catalog always empty under `--require-zdr` - -- `_load_zdr_endpoints` (`scripts/ci/contextual_orchestrator_review_policy.py`, introduced by 17052a7ca / #1360) built ZDR route keys from `endpoint.get("model_name")`, but on the real `https://openrouter.ai/api/v1/endpoints/zdr` feed `model_name` is a human display string (e.g. "DeepSeek: DeepSeek V4.1 Flash") while `model_id` is the slug contextual-orchestrator discovery reports as `model` (e.g. `inclusionai/ling-3.0-flash-vl:free`). No live-feed key ever matched `is_zdr_model(...)`, so every `--require-zdr` consumer (every private/internal caller, per ADR-0003) saw an empty catalog and failed closed with `PolicyError: no attested ZDR model route is available with the ZDR policy; orchestrator/free would fail closed`. Confirmed as the cause of `noema-review` and `strix` failing on `ContextualWisdomLab/late-life-anxiety-reanalysis#10` (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, runs `34700409452`/`103571267389` and `34700409446`/`103571829483`) against central `fb17ef556f94f673234aa557254ae52779e9a7b0`. `_load_zdr_endpoints` now keys on `model_id`, with no fallback to the display name; the three existing fixtures that put slugs into `model_name` (masking the bug since #1360) now carry the real feed schema. Offline reproduction against a 60-row consumer discovery snapshot and the live 859-entry ZDR feed: before, `--require-zdr --pool free` exits 1 with the `PolicyError` above; after, it exits 0 and selects 3 attested `openrouter` ZDR routes (`inclusionai/ling-3.0-flash-{vl,sante,fin}:free`, served by `Novita`). Hosted acceptance on the private consumer's exact head is still required and is not claimed here. Refs ContextualWisdomLab/late-life-anxiety-reanalysis#10, ContextualWisdomLab/.github#2122. - -### CodeQL required workflow denies private consumers a read they need for their own PR - -- `.github/workflows/codeql-pr.yml`'s `analyze-head` and `dispatch-current-head` jobs called `gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` and later `repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses` while holding only `contents: read` (plus `id-token: write`, and `actions: read` on the coordinator job) -- reads GitHub's REST contract gates behind the `pull-requests: read` and `statuses: read` fine-grained permissions on a private repository. Public consumers never surfaced this because GET on a public repository needs no such grant, but private consumer ContextualWisdomLab/late-life-anxiety-reanalysis's PR #10 (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700410434`) failed both required-workflow jobs (`103571590442`, `103571810868`) at their first API call with `gh: Resource not accessible by integration (HTTP 403)`. Both jobs now also hold `pull-requests: read` and `statuses: read`; no write permission is added anywhere, and `actions: write` stays absent, so `tests/test_codeql_pr_workflow_contract.py::test_codeql_required_workflow_does_not_gain_actions_write` needed no change. New regression test `test_codeql_pr_jobs_hold_read_grants_private_consumers_need` pins the exact grant set. See `docs/doctoring/codeql-pr-private-consumer-read-permissions.md`. Refs ContextualWisdomLab/late-life-anxiety-reanalysis#10. - ### Failed-check finding names the Strix sandbox instead of the gateway - `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935. @@ -96,7 +68,6 @@ - Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up. ## [Unreleased] -- **Bind GitHub REST redirect evidence to both production opener chains.** `.github#2279` now feeds a synthetic same-authority 302 through the CodeQL identity and Strix evidence clients' real module-level openers, proving the redirect target is never contacted and the bearer header is never forwarded. Removing `_RejectRedirects` from either opener makes the contract fail on the forbidden second request. Four stale Strix HTTP/transport/JSON fixtures now patch that same production seam; direct handler unit cases and standalone CodeQL materialization remain unchanged. - **Define an evidence-backed repository README quality standard.** Added `docs/repository-readme-quality-standard.md` as the shared review contract for product-first structure, code-current onboarding, authority boundaries, durable quality signals, and repository/source/dependency license due diligence. Product repositories continue to own their own README prose; the standard is linked from the root documentation map and does not centralize or generate product claims. - Include merge-scheduler entrypoint, core, and regression-test changes in the existing runtime-quality workflow's trigger and suite selector. Scheduler diff --git a/config/actions_queue_health_repositories.json b/config/actions_queue_health_repositories.json deleted file mode 100644 index ac38aeb412..0000000000 --- a/config/actions_queue_health_repositories.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "repositories": [ - "ContextualWisdomLab/.github", - "ContextualWisdomLab/ConceptWeave", - "ContextualWisdomLab/ELUNVERA", - "ContextualWisdomLab/LineageWeave", - "ContextualWisdomLab/OriginWeave", - "ContextualWisdomLab/TEPP", - "ContextualWisdomLab/contextual-orchestrator", - "ContextualWisdomLab/disksage", - "ContextualWisdomLab/fast-mlsirm", - "ContextualWisdomLab/mhtml-etl-gateway", - "ContextualWisdomLab/naruon", - "ContextualWisdomLab/noema", - "ContextualWisdomLab/pg-llm-batch", - "ContextualWisdomLab/quarantine-sandbox-runtime" - ] -} diff --git a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md index 6629675f14..9b0749f258 100644 --- a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md +++ b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md @@ -24,7 +24,7 @@ all five, and auto-optimize routing by cost. 1. **Vendoring, pinned**: `scripts/ci/contextual_orchestrator_review_sidecar.sh` clones `ContextualWisdomLab/contextual-orchestrator` at an exact SHA - (`767e67fbc6b881a452761f32abb69b9971b9b03b` today) into `RUNNER_TEMP`. The + (`414f22973658c4ddc3d4320fcf7acd9b4e8ba991` today) into `RUNNER_TEMP`. The source's `requirements.lock` is installed with `--require-hashes` and `--no-deps`, so dependency resolution cannot silently move the reviewed runtime. @@ -259,18 +259,6 @@ all five, and auto-optimize routing by cost. fault. Accepted-size and tool-schema probes call the pinned client's deterministic mock response explicitly and therefore perform no provider call. -- **2026-09-13 amendment: advance the governed runtime pin to remove the - implicit 90 s model request timeout.** The vendored pin advances from - `414f22973658c4ddc3d4320fcf7acd9b4e8ba991` to - `767e67fbc6b881a452761f32abb69b9971b9b03b`, the commit that merges - `contextual-orchestrator#1053`. Under the previous pin `ModelClient` - defaulted to `timeout=90`, so every NVIDIA NIM `google/gemma-4-31b-it` - attempt in the Noema sidecar ended in `TimeoutError` at exactly 90 s (15 of - 27 attempts in fast-mlsirm#1860 run 34748511702) and the gateway surfaced - `502 provider_connection_error` after ~20 min of circuit retries. #1053 makes - the model timeout null by default and administrator-configured per model - (`model_timeout_seconds`), matching this ADR's rule that model inference - carries no wall-clock deadline. - **2026-09-06 amendment: advance the governed runtime pin to fix `orchestrator/free` retry-stacking.** The vendored pin advances from `2e414d15ba58f28597751b625a8a2f00fc9fadcf` to diff --git a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md index b9a156417b..5a11894767 100644 --- a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md +++ b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md @@ -1,6 +1,6 @@ # 0025 — Restore central CodeQL as a required workflow via repository_dispatch -**Status:** Proposed, amended 2026-09-12 (versioned handler-first bootstrap) · **Date:** 2026-09-03 · **Owner intent recorded:** loop-brief item 41 +**Status:** Proposed, amended 2026-09-07 (one dispatch per pull request; language independence is the handler job matrix) · **Date:** 2026-09-03 · **Owner intent recorded:** loop-brief item 41 ## Problem @@ -306,55 +306,3 @@ blocker for this one. required `workflows` list (admin:org PUT, same mechanism used to remove it) and verify a real PR observes a successful, correctly-named required check before declaring this ADR's status Accepted. - -## 2026-09-12 amendment: versioned handler-first bootstrap - -The initial rollout created a protected-branch/client dependency cycle. A -candidate producer can dispatch a stronger evidence envelope, but -`repository_dispatch` always executes the handler from protected `main`. -Conversely, landing the stronger handler first would reject the protected -client's legacy payload and status context. This ADR therefore adopts a -staged protocol on the single canonical handler; it does not create a copied -workflow or permit branch-selected execution. - -The protected bootstrap accepts exactly two event types: - -- `codeql-scan` is temporary legacy v1. It keeps the protected client's - current run title, top-level `required_jobs`, and - `codeql-dispatch/` status context. It rejects nested `pr_head`, - `producer_source_sha`, `rerun_request`, and explicit `rerun_mode` fields so - a v2 caller cannot downgrade its identity checks. -- `codeql-scan-v2` is the proposed v2 contract. The event type is the version - discriminator and consumes no `client_payload` property. It requires the - versioned head envelope, exact synthetic merge `producer_source_sha`, live - 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 -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. - -Live evidence for the amendment is recorded in -`docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md`. In short, -handler run `34684228601` completed both language scans but its matrix-owned -legacy wakes raced: Actions started the required run and Python received HTTP -403. Later same-tuple handler runs were repeatedly cancelled by concurrency, -including `34684575249`, leaving a clean scan without a converged terminal -receipt. This is a settlement-timing defect, not a CodeQL finding. - -Landing sequence is normative: - -1. Land this dual-event, legacy-compatible handler from fresh protected main. -2. Non-force restack the complete successor (#2040), switch its producer to - `codeql-scan-v2`, and generate fresh exact-head end-to-end evidence. -3. Keep legacy v1 until the protected v2 producer is live, all in-flight v1 - required runs are terminal, and repository-wide caller inventory is zero; - then remove v1 with its bridge tests in a separate proven cleanup. - -The ADR remains **Proposed** until that sequence passes ordinary protection -and a real consumer reaches a successful required CodeQL conclusion. Open PR -code is not production authority. diff --git a/docs/adr/0030-ci-centralization-scope-given-plan-ceiling.md b/docs/adr/0030-ci-centralization-scope-given-plan-ceiling.md deleted file mode 100644 index 2dbe8a7070..0000000000 --- a/docs/adr/0030-ci-centralization-scope-given-plan-ceiling.md +++ /dev/null @@ -1,82 +0,0 @@ -# 0030. CI centralization: what it can and cannot fix, given the plan-level concurrency ceiling - -## Status - -Proposed (informational/scoping ADR — no workflow behavior changes yet) - -## Context - -`docs/ci-baseline-20260916.md` measured 24h of Actions runs across all 79 org repos (9,353 runs, -400 (repo, workflow, trigger) groups) to quantify PR queue stalls, starting from the observed -symptom of `fast-mlsirm` PRs sitting with 20+ checks `QUEUED` and 0 completed for extended periods. - -That baseline reproduces, live and two weeks later, the exact signature already recorded in -[`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`](../doctoring/actions-plan-concurrency-ceiling-20260903.md): -single-digit `in_progress` runs against triple/quadruple-digit `queued` runs, org-wide -(`fast-mlsirm`: 8 vs 220; `.github`: 6 vs 220 at measurement time). That record's root-cause finding — -a plan-level concurrent-job ceiling (user-reported 58-60/60 at the time), not workflow-file -duplication — is not something a workflow change in this repository can lift. It also explicitly -warns that a large workflow-consolidation project undertaken on the theory that it fixes the queue -"would be solving the wrong layer of the problem, at real cost." - -This ADR exists so the next PR against this effort starts from that constraint instead of -re-discovering it, and scopes what centralization *is* still good for. - -## What GitHub's mechanisms actually do (for reference) - -- **Reusable workflows (`workflow_call`)** ([GitHub docs](https://docs.github.com/en/actions/using-workflows/reusing-workflows)): - let a thin per-repo caller invoke a workflow defined once in `.github`. Reduces file drift and the - number of independent `.yml` files to keep security-equivalent across repos. Does **not** change - how many jobs the org can run concurrently — each `workflow_call` job still consumes one slot - against the same org-wide ceiling as any other job. -- **Concurrency groups with `cancel-in-progress`** ([GitHub docs](https://docs.github.com/en/actions/using-jobs/using-concurrency)): - cancel a stale run when a newer one starts in the same group. This *does* directly reduce - concurrent-job pressure, by retiring superseded work instead of letting it sit `queued` (or worse, - `in_progress`) behind newer pushes. This is the one lever here that actually shrinks the number of - jobs competing for the ceiling, not just the number of files. -- **Organization required-workflow rulesets** ([GitHub docs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets#require-workflows-to-pass-before-merging)): - run one canonical workflow file's job graph in every target repo's context; already how this repo - centralizes `opencode-review`, `strix`, `admit-current-head`, etc. Centralizes *maintenance*, not - *capacity*. -- **Usage limits** ([GitHub docs](https://docs.github.com/en/actions/administering-github-actions/usage-limits-billing-and-administration#usage-limits)): - the concurrent-job ceiling is a plan/billing property (GitHub Free/Team/Enterprise tiers set - different concurrent-job maximums), not something exposed or changeable via the Actions or - rulesets APIs. Confirmed via this session's own `gh api` exploration: no REST or GraphQL field - surfaces the org's current ceiling; it's Settings → Billing → Plans and usage only. - -## Decision - -1. **Do not scope further work here as "fix the queue by centralizing more workflows."** The baseline - shows that lever is largely already pulled (org-required workflows already cover - opencode-review/strix/noema/sast/codeql/secrets; concurrency groups already exist on every - event-triggered required workflow that isn't a reusable `workflow_call` target or an - `issue_comment`/`schedule` trigger — see baseline doc for the file-by-file check). -2. **The ceiling itself is an org-owner billing decision** (raise plan tier, buy additional included - concurrency, or provision runners with a separate capacity pool), per the 2026-09-03 doctoring - record. This ADR does not propose a workflow change to address it, because none exists. -3. **The one remaining code-level lever that reduces total *concurrent job count per PR head*, and - therefore genuinely helps under a fixed ceiling, is folding required checks that are - `needs:`-serial or logically redundant into fewer jobs/runners** — the pattern already used for - `sast-semgrep.yml` (2026-09-13 fold, see baseline doc and - `docs/product-technical-gap-baseline.md`) and for the `opencode-review.yml` chain-depth cut - (`#1910`). Any future PR in this space should look for the same fold opportunity rather than - proposing new centralization for its own sake. -4. **`bandscope`'s 89% cancellation rate across all 7 of its required workflows** (91 runs each, - ~80 cancelled, in the 24h baseline) is the one concrete duplication/thrash signal this baseline - surfaced and is not yet explained — worth a scoped follow-up investigation (what's re-triggering - pushes that often on that repo) before proposing a fix, since the cause is unconfirmed. - -## Consequences - -- No PR follows directly from this ADR: the smallest safe next step this session could find - (add missing `concurrency:` blocks) was already done org-wide, and consolidating further reusable - workflows would add maintenance surface without moving the KPI this task defined (p95 queue time, - jobs per PR head) — that KPI is dominated by the plan ceiling, not file count. -- The KPI itself needs a caveat added wherever it's used: run-level `created_at` → `run_started_at` - queue time is close to 0 for nearly every workflow in this org (see baseline doc) because a run's - status flips to `in_progress` as soon as one job starts, even while other jobs in the same run sit - `queued`. Any future measurement of this KPI should use job- or check-suite-level `started_at`, - not run-level, or it will systematically under-report the stall. -- Escalating the plan-ceiling question to the org owner (or confirming it's already been acted on - since 2026-09-03) is the highest-leverage next action, and is outside what a repository-scoped PR - can do. diff --git a/docs/adr/0031-noema-transport-capacity-redispatch.md b/docs/adr/0031-noema-transport-capacity-redispatch.md deleted file mode 100644 index c0ecc51b81..0000000000 --- a/docs/adr/0031-noema-transport-capacity-redispatch.md +++ /dev/null @@ -1,71 +0,0 @@ -# ADR-0031: Noema transport-capacity failures schedule a bounded continuation re-dispatch - -- **Status:** Accepted -- **Date:** 2026-09-17 -- **Scope:** `scripts/ci/noema_review_gate.py`, `.github/actions/noema-review/two_phase.py`, `.github/workflows/noema-review.yml` -- **Issue:** ContextualWisdomLab/.github#2165 -- **Does not amend:** ADR-0003 (gateway owns provider failover), ADR-0005 (no model-path wall-clock timeout; superseded attempt ceilings stay historical) - -## Problem - -`Required Noema Review` is an organization required check. The Noema caller issues -exactly one gateway request and delegates provider discovery, repair, and failover to -contextual-orchestrator (ADR-0003). When every free-pool route is transiently -unavailable or rate-limited, the gateway returns a terminal HTTP 429 or 5xx after it has -already exhausted its own failover chain. The caller then fails closed with -`NoemaTransportError` and `caller attempts=1`. That is correct for review integrity — -the check must not be skipped — but it leaves consumer PRs permanently BLOCKED until a -human re-dispatches into a healthier window, even when the PR's own code checks are green -(#2165 evidence on four-pillars and fast-mlsirm). - -Holding the same job open to retry the model call would occupy a scarce Actions runner -for provider capacity that the gateway already reported as exhausted. Product goal -directive §8 and ADR-0005 forbid converting elapsed inference time into a local -model-failure verdict or restoring fixed model-path attempt ceilings. - -## Decision - -1. **Classify, do not re-interpret.** HTTP 429 and 5xx from the already-failed-over - gateway are typed as `provider_capacity_unavailable`. Malformed model output, 4xx - other than 429, and local validation failures stay terminal review failures. The - required check still fails; review is never skipped or auto-approved. -2. **One gateway request per job stays the contract.** `call_llm` does not gain a caller-side retry loop. Provider failover remains contextual-orchestrator's job. -3. **Continuation re-dispatch is the recovery lever.** When the failure is - `provider_capacity_unavailable` and the run's `transport_retry_attempt` is below the - bound (`MAX_TRANSPORT_REDISPATCH_ATTEMPTS = 2`), the workflow schedules exactly one - same-head `repository_dispatch` (`noema-review`) with an incremented attempt counter - after a short jitter delay. The new job is a fresh admission/continuation; the failed - job remains failed evidence for that attempt. -4. **Jitter is post-failure scheduling, not a model timeout.** Prefer a whole-seconds - `Retry-After` from the gateway error when present and in `[1, 300]`. Otherwise use a - deterministic delay in `[60, 180]` seconds derived from the exact head SHA and attempt - number so concurrent capacity failures do not stampede the free pool. That sleep runs - only in the post-failure scheduling step and never wraps `opener.open`. -5. **Surface gateway attempt evidence.** When the error envelope carries an `attempts` - list (orchestrator failover telemetry), the public Actions warning and exception text - include `provider_attempt_count=` alongside the existing last-attempt fields so - capacity incidents are distinguishable from code-review verdicts without dumping raw - provider bodies. - -## Consequences - -- A capacity storm produces at most three Noema jobs per head (initial + two automatic - re-dispatches) before failing closed for operator intervention. -- Runner occupancy for a dead pool is one failed inference plus ≤180 s of scheduling - jitter, not another multi-hour in-job wait on the same slot. -- Gateway routing bugs (for example a non-transient 400 on one ready route while others - remain unused) are **out of scope** here; they belong to contextual-orchestrator route - selection, not this caller (#2165 consumer notes on vision-model 400s). -- Private-target ZDR pool exhaustion (#2148) shares the classification and attempt - evidence, but does not widen this ADR's re-dispatch bound. - -## Alternatives considered - -- **In-job retry of `call_llm`.** Rejected: duplicates gateway failover, burns the runner - against an exhausted pool, and conflicts with the single-request contract tests. -- **Mark the required check neutral/success on capacity loss.** Rejected: weakens - "review cannot be skipped." -- **Unbounded re-dispatch.** Rejected: amplifies 429 pressure (#2165 filing notes). -- **Rely only on the merge scheduler's next tick.** Deferred as a complementary path; - it does not give the Noema workflow its own bounded, evidence-typed recovery when the - scheduler is not looking at that head. diff --git a/docs/ci-baseline-20260916.csv b/docs/ci-baseline-20260916.csv deleted file mode 100644 index 9cee0a61ac..0000000000 --- a/docs/ci-baseline-20260916.csv +++ /dev/null @@ -1,401 +0,0 @@ -repo,workflow,event,runs,still_queued_now,cancelled,startup_failure,queue_p50_s,queue_p95_s,queue_max_s,dur_p50_s,dur_p95_s -LineageWeave,.github/workflows/tests.yml,pull_request,259,0,132,0,0.0,0.0,0.0,217.5,19032.2 -OriginWeave,.github/workflows/ci.yml,pull_request,189,0,50,0,0.0,0.0,0.0,7.0,2939.9 -.github,.github/workflows/opencode-review-dispatch.yml,repository_dispatch,137,0,4,0,0.0,0.0,0.0,15059.0,64003.4 -.github,.github/workflows/codeql-scan-dispatch.yml,repository_dispatch,128,0,0,0,0.0,0.0,0.0,30598.0,34865.0 -pingora-gateway,.github/workflows/supply-chain.yml,pull_request,113,0,26,0,0.0,0.0,0.0,2.5,14268.6 -pingora-gateway,.github/workflows/ci.yml,pull_request,113,0,27,0,0.0,0.0,0.0,7.0,14920.9 -fast-mlsirm,.github/workflows/ci.yml,pull_request,103,0,35,0,0.0,0.0,0.0,16837.0,38459.0 -.github,.github/workflows/agent-mention-router.yml,issue_comment,100,0,0,0,0.0,0.0,0.0,1.0,10.0 -bandscope,.github/workflows/sast-semgrep.yml,pull_request,91,0,79,0,0.0,0.0,0.0,82.0,16524.8 -bandscope,.github/workflows/ci.yml,pull_request,91,0,80,0,0.0,0.0,0.0,53.0,6683.0 -bandscope,.github/workflows/codeql-pr.yml,pull_request,91,0,81,0,0.0,0.0,0.0,52.0,6683.0 -bandscope,.github/workflows/sbom.yml,pull_request,91,0,80,0,0.0,0.0,0.0,56.5,10902.9 -bandscope,.github/workflows/security-scan.yml,pull_request,91,0,81,0,0.0,0.0,0.0,53.0,6683.0 -bandscope,.github/workflows/build-baseline.yml,pull_request,91,0,79,0,0.0,0.0,0.0,102.0,16667.0 -bandscope,.github/workflows/opencode-review.yml,pull_request_target,91,0,81,0,0.0,0.0,0.0,54.0,6682.0 -bandscope,.github/workflows/noema-review.yml,pull_request_target,91,0,81,0,0.0,0.0,0.0,78.0,16500.8 -bandscope,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,91,0,79,0,0.0,0.0,0.0,81.5,16271.5 -bandscope,.github/workflows/strix.yml,pull_request_target,91,0,81,0,0.0,0.0,0.0,78.0,16655.3 -bandscope,dynamic/github-code-quality/codeql,dynamic,90,0,67,0,0.0,0.0,0.0,3491.0,16723.8 -fast-mlsirm,dynamic/github-code-scanning/codeql,dynamic,74,0,16,0,0.0,0.0,0.0,13077.0,18377.5 -quarantine-sandbox-runtime,.github/workflows/ci.yml,pull_request,74,0,69,0,0.0,0.0,0.0,143.0,20039.4 -disksage,.github/workflows/release.yml,pull_request,73,0,0,0,0.0,0.0,0.0,2.0,3.4 -disksage,.github/workflows/test.yml,pull_request,72,0,52,0,0.0,0.0,0.0,2253.0,14104.0 -TEPP,.github/workflows/ci.yml,pull_request,72,0,15,0,0.0,0.0,0.0,2.0,13105.0 -fast-mlsirm,.github/workflows/codeql.yml,pull_request,68,0,0,0,0.0,0.0,0.0,14445.0,18564.5 -fast-mlsirm,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,68,0,12,0,0.0,0.0,0.0,13925.0,19356.6 -fast-mlsirm,.github/workflows/strix.yml,pull_request_target,68,0,20,0,0.0,0.0,0.0,19045.0,40863.0 -fast-mlsirm,.github/workflows/noema-review.yml,pull_request_target,68,0,20,0,0.0,0.0,0.0,19162.5,31834.2 -fast-mlsirm,.github/workflows/opencode-review.yml,pull_request_target,68,0,21,0,0.0,0.0,0.0,30313.5,46277.8 -fast-mlsirm,.github/workflows/codeql-pr.yml,pull_request,67,0,22,0,0.0,0.0,0.0,31176.0,50321.3 -fast-mlsirm,.github/workflows/security-scan.yml,pull_request,67,0,17,0,0.0,0.0,0.0,25723.0,33874.0 -fast-mlsirm,.github/workflows/sast-semgrep.yml,pull_request,67,0,12,0,0.0,0.0,0.0,13808.0,19099.8 -LineageWeave,dynamic/github-code-quality/codeql,dynamic,53,0,38,0,0.0,0.0,0.0,3700.0,14349.2 -LineageWeave,.github/workflows/codeql-pr.yml,pull_request,52,0,45,0,0.0,0.0,0.0,1473.0,9029.6 -LineageWeave,.github/workflows/noema-review.yml,pull_request_target,52,0,46,0,0.0,0.0,0.0,1558.0,19357.5 -LineageWeave,.github/workflows/opencode-review.yml,pull_request_target,52,0,46,0,0.0,0.0,0.0,1558.0,19357.8 -LineageWeave,.github/workflows/sast-semgrep.yml,pull_request,52,0,45,0,0.0,0.0,0.0,1718.5,14341.4 -LineageWeave,.github/workflows/security-scan.yml,pull_request,52,0,46,0,0.0,0.0,0.0,1558.0,19360.3 -LineageWeave,.github/workflows/strix.yml,pull_request_target,52,0,47,0,0.0,0.0,0.0,1472.0,9030.3 -LineageWeave,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,52,0,45,0,0.0,0.0,0.0,1718.5,13820.0 -LineageWeave,dynamic/github-code-scanning/codeql,dynamic,52,0,38,0,0.0,0.0,0.0,3423.0,14226.6 -accounting-information-platform,.github/workflows/security-scan.yml,pull_request,43,0,42,0,0.0,0.0,0.0,2722.5,3767.3 -accounting-information-platform,.github/workflows/codeql-pr.yml,pull_request,43,0,42,0,0.0,0.0,0.0,2722.0,3767.3 -accounting-information-platform,.github/workflows/ci.yml,pull_request,43,0,42,0,0.0,0.0,0.0,2723.0,3767.3 -accounting-information-platform,.github/workflows/sast-semgrep.yml,pull_request,43,0,42,0,0.0,0.0,0.0,2722.0,3767.3 -accounting-information-platform,.github/workflows/noema-review.yml,pull_request_target,43,0,42,0,0.0,0.0,0.0,2721.5,3769.2 -accounting-information-platform,.github/workflows/strix.yml,pull_request_target,43,0,42,0,0.0,0.0,0.0,2722.0,3769.2 -accounting-information-platform,.github/workflows/opencode-review.yml,pull_request_target,43,0,42,0,0.0,0.0,0.0,2722.0,3769.2 -accounting-information-platform,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,43,0,42,0,0.0,0.0,0.0,2721.5,3768.3 -accounting-information-platform,dynamic/github-code-quality/codeql,dynamic,43,0,41,0,0.0,0.0,0.0,3378.0,3879.0 -Orgmetra,.github/workflows/foundation-ci.yml,pull_request,41,0,36,0,0.0,0.0,0.0,409.0,11994.2 -Orgmetra,.github/workflows/sast-semgrep.yml,pull_request,41,0,36,0,0.0,0.0,0.0,409.0,11704.9 -Orgmetra,.github/workflows/codeql-pr.yml,pull_request,41,0,38,0,0.0,0.0,0.0,410.0,26380.4 -Orgmetra,.github/workflows/security-scan.yml,pull_request,41,0,38,0,0.0,0.0,0.0,409.0,14796.5 -Orgmetra,.github/workflows/strix.yml,pull_request_target,41,0,39,0,0.0,0.0,0.0,410.0,14797.6 -Orgmetra,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,41,0,37,0,0.0,0.0,0.0,409.0,11904.7 -Orgmetra,.github/workflows/opencode-review.yml,pull_request_target,41,0,38,0,0.0,0.0,0.0,410.0,14797.6 -Orgmetra,.github/workflows/noema-review.yml,pull_request_target,41,0,39,0,0.0,0.0,0.0,410.0,14797.6 -Orgmetra,dynamic/github-code-quality/codeql,dynamic,41,0,32,0,0.0,0.0,0.0,6667.0,12578.6 -newsdom-api,dynamic/github-code-quality/codeql,dynamic,40,0,0,0,0.0,0.0,0.0,13123.5,18989.8 -newsdom-api,.github/workflows/scorecards.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13225.0,19198.8 -newsdom-api,.github/workflows/container-image.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13146.0,19554.7 -newsdom-api,.github/workflows/tests.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13132.0,19251.9 -newsdom-api,.github/workflows/sast-semgrep.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13363.0,19582.3 -newsdom-api,.github/workflows/security-scan.yml,pull_request,39,0,2,0,0.0,0.0,0.0,28601.0,34408.5 -newsdom-api,.github/workflows/clusterfuzzlite.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13775.0,19581.5 -newsdom-api,.github/workflows/codeql.yml,pull_request,39,0,0,0,0.0,0.0,0.0,13232.0,19226.1 -newsdom-api,.github/workflows/codeql-pr.yml,pull_request,39,0,18,0,0.0,0.0,0.0,47789.0,58592.1 -newsdom-api,.github/workflows/strix.yml,pull_request_target,39,0,13,0,0.0,0.0,0.0,31266.0,45547.2 -newsdom-api,.github/workflows/noema-review.yml,pull_request_target,39,0,12,0,0.0,0.0,0.0,31776.0,46323.4 -newsdom-api,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,39,0,0,0,0.0,0.0,0.0,13251.0,19824.7 -newsdom-api,.github/workflows/opencode-review.yml,pull_request_target,39,0,13,0,0.0,0.0,0.0,33498.0,46099.0 -naruon,.github/workflows/docker-publish.yml,pull_request,35,0,1,0,0.0,0.0,0.0,13998.0,20734.4 -naruon,.github/workflows/sast-semgrep.yml,pull_request,35,0,17,0,0.0,0.0,0.0,7513.0,18756.7 -naruon,.github/workflows/bandit.yml,pull_request,35,0,0,0,0.0,0.0,0.0,13353.0,18958.2 -naruon,.github/workflows/security-scan.yml,pull_request,35,0,22,0,0.0,0.0,0.0,3291.0,33264.2 -naruon,.github/workflows/codeql-pr.yml,pull_request,35,0,24,0,0.0,0.0,0.0,2598.0,45980.7 -naruon,.github/workflows/app-ci.yml,pull_request,35,0,17,0,0.0,0.0,0.0,8014.0,18519.0 -naruon,.github/workflows/opencode-review.yml,pull_request_target,35,0,24,0,0.0,0.0,0.0,3290.0,36546.2 -naruon,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,35,0,17,0,0.0,0.0,0.0,7824.0,18184.4 -naruon,.github/workflows/noema-review.yml,pull_request_target,35,0,24,0,0.0,0.0,0.0,3290.0,29846.2 -naruon,.github/workflows/strix.yml,pull_request_target,35,0,25,0,0.0,0.0,0.0,3862.5,30509.3 -naruon,dynamic/github-code-quality/codeql,dynamic,35,0,8,0,0.0,0.0,0.0,12626.5,19167.2 -naruon,dynamic/github-code-scanning/codeql,dynamic,35,0,8,0,0.0,0.0,0.0,12551.5,18902.5 -pingora-gateway,.github/workflows/sast-semgrep.yml,pull_request,33,0,31,0,0.0,0.0,0.0,34.0,5156.1 -pingora-gateway,.github/workflows/security-scan.yml,pull_request,33,0,32,0,0.0,0.0,0.0,33.0,5155.7 -pingora-gateway,.github/workflows/codeql-pr.yml,pull_request,33,0,31,0,0.0,0.0,0.0,32.0,1990.0 -pingora-gateway,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,33,0,31,0,0.0,0.0,0.0,33.0,5155.7 -pingora-gateway,.github/workflows/strix.yml,pull_request_target,33,0,32,0,0.0,0.0,0.0,33.0,5155.7 -pingora-gateway,.github/workflows/opencode-review.yml,pull_request_target,33,0,32,0,0.0,0.0,0.0,33.0,5155.2 -pingora-gateway,.github/workflows/noema-review.yml,pull_request_target,33,0,32,0,0.0,0.0,0.0,33.0,5154.7 -.github,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,31,0,13,0,0.0,0.0,0.0,12612.5,17817.8 -.github,.github/workflows/opencode-review.yml,pull_request_target,31,0,13,0,0.0,0.0,0.0,16588.5,45368.3 -.github,.github/workflows/strix.yml,pull_request_target,31,0,16,0,0.0,0.0,0.0,16651.5,47312.1 -.github,.github/workflows/noema-review.yml,pull_request_target,31,0,14,0,0.0,0.0,0.0,17100.0,47291.0 -codec-carver,dynamic/github-code-scanning/codeql,dynamic,30,0,0,0,0.0,0.0,0.0,13311.0,18710.5 -.github,.github/workflows/codeql-pr.yml,pull_request,30,0,14,0,0.0,39570.3,74723.0,17101.0,53333.8 -.github,.github/workflows/security-scan.yml,pull_request,30,0,10,0,0.0,0.0,0.0,24974.0,33218.0 -.github,.github/workflows/sast-semgrep.yml,pull_request,30,0,7,0,0.0,0.0,0.0,12978.0,17406.8 -TEPP,.github/workflows/docs-quality.yml,pull_request,30,0,9,0,0.0,0.0,0.0,7.0,9507.8 -codec-carver,.github/workflows/ci.yml,pull_request,29,0,12,0,0.0,0.0,0.0,33855.0,45463.2 -codec-carver,.github/workflows/codeql-pr.yml,pull_request,29,0,10,0,0.0,0.0,0.0,42959.0,51301.5 -codec-carver,.github/workflows/fuzz.yml,pull_request,29,0,13,0,0.0,0.0,0.0,33855.0,45520.2 -codec-carver,.github/workflows/sast-semgrep.yml,pull_request,29,0,0,0,0.0,0.0,0.0,13011.0,19129.0 -codec-carver,.github/workflows/security-scan.yml,pull_request,29,0,0,0,0.0,0.0,0.0,29678.0,33718.2 -codec-carver,.github/workflows/opencode-review.yml,pull_request_target,29,0,13,0,0.0,0.0,0.0,33854.0,46398.0 -codec-carver,.github/workflows/noema-review.yml,pull_request_target,29,0,10,0,0.0,0.0,0.0,31606.0,46769.4 -codec-carver,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,29,0,0,0,0.0,0.0,0.0,13330.0,18777.0 -codec-carver,.github/workflows/strix.yml,pull_request_target,29,0,13,0,0.0,0.0,0.0,31924.0,43385.0 -html4tree,.github/workflows/codeql-pr.yml,pull_request,28,0,8,0,0.0,0.0,0.0,46483.5,53759.1 -html4tree,.github/workflows/sast-semgrep.yml,pull_request,28,0,1,0,0.0,0.0,0.0,13886.0,18559.6 -html4tree,.github/workflows/security-scan.yml,pull_request,28,0,1,0,0.0,0.0,0.0,27262.0,33155.8 -html4tree,.github/workflows/ci.yml,pull_request,28,0,0,0,0.0,0.0,0.0,13438.0,18516.7 -html4tree,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,28,0,1,0,0.0,0.0,0.0,13567.0,19175.3 -html4tree,.github/workflows/noema-review.yml,pull_request_target,28,0,5,0,0.0,0.0,0.0,30727.0,45249.5 -html4tree,.github/workflows/opencode-review.yml,pull_request_target,28,0,8,0,0.0,0.0,0.0,42808.0,46311.8 -html4tree,.github/workflows/strix.yml,pull_request_target,28,0,11,0,0.0,0.0,0.0,40996.0,46570.2 -html4tree,dynamic/github-code-scanning/codeql,dynamic,28,0,0,0,0.0,0.0,0.0,13326.0,18305.3 -linux-cluster-ops,.github/workflows/security-scan.yml,pull_request,28,0,19,0,0.0,0.0,0.0,56.0,31120.5 -linux-cluster-ops,.github/workflows/fuzz.yml,pull_request,28,0,16,0,0.0,0.0,0.0,70.0,17127.0 -linux-cluster-ops,.github/workflows/pr-governance.yml,pull_request,28,0,16,0,0.0,0.0,0.0,70.0,17782.2 -linux-cluster-ops,.github/workflows/auto-approve.yml,pull_request,28,0,0,0,0.0,0.0,0.0,13343.0,18725.8 -linux-cluster-ops,.github/workflows/lint.yml,pull_request,28,0,16,0,0.0,0.0,0.0,69.0,16816.2 -linux-cluster-ops,.github/workflows/sast-semgrep.yml,pull_request,28,0,16,0,0.0,0.0,0.0,70.0,17000.6 -linux-cluster-ops,.github/workflows/codeql-pr.yml,pull_request,28,0,21,0,0.0,0.0,0.0,55.0,44896.0 -linux-cluster-ops,.github/workflows/noema-review.yml,pull_request_target,28,0,18,0,0.0,0.0,0.0,56.0,31393.1 -linux-cluster-ops,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,28,0,16,0,0.0,0.0,0.0,69.0,16850.0 -linux-cluster-ops,.github/workflows/opencode-review.yml,pull_request_target,28,0,22,0,0.0,0.0,0.0,56.0,32158.4 -linux-cluster-ops,.github/workflows/strix.yml,pull_request_target,28,0,20,0,0.0,0.0,0.0,56.0,31511.2 -linux-cluster-ops,dynamic/github-code-quality/codeql,dynamic,28,0,9,0,0.0,0.0,0.0,7462.0,17863.9 -argos,.github/workflows/security-scan.yml,pull_request,28,0,0,0,0.0,0.0,0.0,27228.0,32293.0 -argos,.github/workflows/codeql-pr.yml,pull_request,28,0,9,0,0.0,0.0,0.0,45556.5,56291.4 -argos,.github/workflows/sast-semgrep.yml,pull_request,28,0,0,0,0.0,0.0,0.0,14091.0,18689.0 -argos,.github/workflows/opencode-review.yml,pull_request_target,28,0,10,0,0.0,0.0,0.0,31111.0,44705.0 -argos,.github/workflows/noema-review.yml,pull_request_target,28,0,8,0,0.0,0.0,0.0,29582.5,36558.2 -argos,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,28,0,0,0,0.0,0.0,0.0,13655.0,18463.0 -argos,.github/workflows/strix.yml,pull_request_target,28,0,10,0,0.0,0.0,0.0,29579.0,34131.0 -pg-erd-cloud,.github/workflows/sast-semgrep.yml,pull_request,27,0,0,0,0.0,0.0,0.0,13093.0,19860.0 -pg-erd-cloud,.github/workflows/security-scan.yml,pull_request,27,0,2,0,0.0,0.0,0.0,27269.0,33290.5 -pg-erd-cloud,.github/workflows/ci.yml,pull_request,27,0,0,0,0.0,0.0,0.0,13114.0,19561.0 -pg-erd-cloud,.github/workflows/codeql-pr.yml,pull_request,27,0,8,0,0.0,0.0,0.0,45668.0,57635.5 -pg-erd-cloud,.github/workflows/opencode-review.yml,pull_request_target,27,0,11,0,0.0,0.0,0.0,34429.0,46992.2 -pg-erd-cloud,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,27,0,0,0,0.0,0.0,0.0,13506.0,20266.0 -pg-erd-cloud,.github/workflows/noema-review.yml,pull_request_target,27,0,9,0,0.0,0.0,0.0,33513.0,46996.7 -pg-erd-cloud,.github/workflows/strix.yml,pull_request_target,27,0,12,0,0.0,0.0,0.0,32706.0,47164.0 -pg-erd-cloud,dynamic/github-code-quality/codeql,dynamic,27,0,0,0,0.0,0.0,0.0,13221.0,19816.0 -argos,.github/workflows/ci.yml,pull_request,27,0,0,0,0.0,0.0,0.0,13452.0,18679.4 -clearfolio,.github/workflows/fuzz.yml,pull_request,23,0,0,0,0.0,0.0,0.0,13224.0,19062.9 -clearfolio,.github/workflows/ci.yml,pull_request,23,0,0,0,0.0,0.0,0.0,13060.0,19164.3 -clearfolio,.github/workflows/security-scan.yml,pull_request,23,0,1,0,0.0,0.0,0.0,28825.5,34303.2 -clearfolio,.github/workflows/codeql-pr.yml,pull_request,23,0,8,0,0.0,0.0,0.0,46882.0,58446.0 -clearfolio,.github/workflows/sast-semgrep.yml,pull_request,23,0,0,0,0.0,0.0,0.0,13113.0,18941.8 -clearfolio,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,23,0,0,0,0.0,0.0,0.0,13165.5,20027.2 -clearfolio,.github/workflows/strix.yml,pull_request_target,23,0,6,0,0.0,0.0,0.0,34157.5,48279.6 -clearfolio,.github/workflows/opencode-review.yml,pull_request_target,23,0,3,0,0.0,0.0,0.0,44570.0,47051.0 -clearfolio,.github/workflows/noema-review.yml,pull_request_target,23,0,3,0,0.0,0.0,0.0,31859.0,39944.1 -clearfolio,dynamic/github-code-scanning/codeql,dynamic,23,0,0,0,0.0,0.0,0.0,13092.5,18674.2 -appguardrail,dynamic/github-code-quality/codeql,dynamic,23,0,0,0,0.0,0.0,0.0,13855.0,18666.0 -.github,.github/workflows/python-security.yml,pull_request,23,0,7,0,0.0,0.0,0.0,25142.0,33339.0 -appguardrail,.github/workflows/codeql-pr.yml,pull_request,22,0,5,0,0.0,0.0,0.0,44130.5,53044.5 -appguardrail,.github/workflows/security-process.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13506.5,18873.3 -appguardrail,.github/workflows/retention-audit-coverage.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13460.0,18773.5 -appguardrail,.github/workflows/pinned-https-coverage.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13340.5,18850.9 -appguardrail,.github/workflows/openssf-evidence-coverage.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13730.5,18954.5 -appguardrail,.github/workflows/security-scan.yml,pull_request,22,0,1,0,0.0,0.0,0.0,27195.0,33461.2 -appguardrail,.github/workflows/sast-semgrep.yml,pull_request,22,0,0,0,0.0,0.0,0.0,14403.5,19699.2 -appguardrail,.github/workflows/tests.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13832.0,19384.8 -appguardrail,.github/workflows/scan-path-context-coverage.yml,pull_request,22,0,0,0,0.0,0.0,0.0,13573.0,18941.6 -appguardrail,.github/workflows/opencode-review.yml,pull_request_target,22,0,8,0,0.0,0.0,0.0,35098.0,44504.0 -appguardrail,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,22,0,0,0,0.0,0.0,0.0,13748.0,19109.3 -appguardrail,.github/workflows/strix.yml,pull_request_target,22,0,5,0,0.0,0.0,0.0,29769.5,42607.4 -appguardrail,.github/workflows/noema-review.yml,pull_request_target,22,0,3,0,0.0,0.0,0.0,29152.0,39568.5 -appguardrail,dynamic/github-code-scanning/codeql,dynamic,22,0,0,0,0.0,0.0,0.0,13466.5,19246.3 -.github,dynamic/github-code-quality/codeql,dynamic,22,0,2,0,0.0,0.0,0.0,12650.0,17718.4 -seedream_evasepic,.github/workflows/codeql-pr.yml,pull_request,22,0,8,0,0.0,0.0,0.0,46580.0,57799.5 -seedream_evasepic,.github/workflows/security-scan.yml,pull_request,22,0,0,0,0.0,0.0,0.0,28705.0,32615.6 -seedream_evasepic,.github/workflows/sast-semgrep.yml,pull_request,22,0,0,0,0.0,0.0,0.0,14594.0,18607.2 -seedream_evasepic,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,22,0,0,0,0.0,0.0,0.0,14728.0,18680.8 -seedream_evasepic,.github/workflows/strix.yml,pull_request_target,22,0,5,0,0.0,0.0,0.0,36423.5,46095.3 -seedream_evasepic,.github/workflows/opencode-review.yml,pull_request_target,22,0,6,0,0.0,0.0,0.0,38074.0,46243.6 -seedream_evasepic,.github/workflows/noema-review.yml,pull_request_target,22,0,3,0,0.0,0.0,0.0,31570.5,45136.0 -seedream_evasepic,dynamic/github-code-scanning/codeql,dynamic,22,0,0,0,0.0,0.0,0.0,13704.0,18623.8 -TEPP,.github/workflows/codeql-pr.yml,pull_request,21,0,18,0,0.0,0.0,0.0,359.5,46877.4 -TEPP,.github/workflows/sast-semgrep.yml,pull_request,21,0,15,0,0.0,0.0,0.0,723.0,18755.0 -TEPP,.github/workflows/security-scan.yml,pull_request,21,0,16,0,0.0,0.0,0.0,601.0,34719.0 -TEPP,.github/workflows/strix.yml,pull_request_target,21,0,17,0,0.0,0.0,0.0,478.0,39798.5 -TEPP,.github/workflows/noema-review.yml,pull_request_target,21,0,17,0,0.0,0.0,0.0,600.5,39748.6 -TEPP,.github/workflows/opencode-review.yml,pull_request_target,21,0,17,0,0.0,0.0,0.0,359.0,35856.5 -TEPP,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,21,0,15,0,0.0,0.0,0.0,723.0,18661.0 -TEPP,dynamic/github-code-quality/codeql,dynamic,21,0,9,0,0.0,0.0,0.0,13463.0,19148.0 -wardnet,.github/workflows/ci.yml,pull_request,20,0,7,0,0.0,0.0,0.0,11849.5,19802.2 -.github,.github/workflows/pr-review-merge-scheduler.yml,pull_request_review,18,0,3,0,0.0,0.0,0.0,13057.5,19023.8 -.github,.github/workflows/hourly-review-repair.yml,schedule,17,0,0,0,0.0,0.0,0.0,25900.0,27509.6 -seedream_evasepic,.github/workflows/cli-ux.yml,pull_request,17,0,0,0,0.0,0.0,0.0,13506.0,18492.6 -life-os,.github/workflows/verify-plugin-operator-replay-sql-snapshot.yml,push,17,0,0,0,0.0,0.0,0.0,19262.0,20229.2 -.github,.github/workflows/pr-review-autofix.yml,repository_dispatch,16,0,2,0,0.0,0.0,0.0,14361.0,34108.0 -pg-llm-batch,.github/workflows/release-acceptance.yml,pull_request,15,0,2,0,0.0,0.0,0.0,13312.5,16059.1 -pg-llm-batch,.github/workflows/ci.yml,pull_request,15,0,2,0,0.0,0.0,0.0,13721.0,16895.8 -scopeweave,.github/workflows/fuzz.yml,pull_request,13,0,0,0,0.0,0.0,0.0,13394.0,16658.0 -scopeweave,.github/workflows/server-tests.yml,pull_request,13,0,0,0,0.0,0.0,0.0,13637.5,17001.5 -scopeweave,.github/workflows/security-scan.yml,pull_request,13,0,0,0,0.0,0.0,0.0,29650.0,32303.2 -scopeweave,.github/workflows/sast-semgrep.yml,pull_request,13,0,0,0,0.0,0.0,0.0,13735.0,16900.8 -scopeweave,.github/workflows/codeql-pr.yml,pull_request,13,0,6,0,0.0,0.0,0.0,46752.5,51025.5 -scopeweave,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,13,0,0,0,0.0,0.0,0.0,13772.0,16637.8 -scopeweave,.github/workflows/opencode-review.yml,pull_request_target,13,0,5,0,0.0,0.0,0.0,35355.0,44709.7 -scopeweave,.github/workflows/noema-review.yml,pull_request_target,13,0,3,0,0.0,0.0,0.0,31973.0,36544.7 -scopeweave,.github/workflows/strix.yml,pull_request_target,13,0,3,0,0.0,0.0,0.0,30805.0,35426.4 -scopeweave,dynamic/github-code-quality/codeql,dynamic,13,0,0,0,0.0,0.0,0.0,13748.5,17116.8 -scopeweave,dynamic/github-code-scanning/codeql,dynamic,13,0,0,0,0.0,0.0,0.0,13876.5,16890.0 -noema,dynamic/github-code-quality/codeql,dynamic,13,0,9,0,0.0,0.0,0.0,2433.0,15446.0 -noema,dynamic/github-code-scanning/codeql,dynamic,13,0,9,0,0.0,0.0,0.0,2433.0,15402.2 -life-os,.github/workflows/sast-semgrep.yml,pull_request,13,0,8,0,0.0,0.0,0.0,622.0,16544.8 -life-os,.github/workflows/appguardrail.yml,pull_request,13,0,7,0,0.0,0.0,0.0,97.5,16689.1 -life-os,.github/workflows/security-scan.yml,pull_request,13,0,9,0,0.0,0.0,0.0,622.0,29886.5 -life-os,.github/workflows/codeql-pr.yml,pull_request,13,0,8,0,0.0,0.0,0.0,145.5,45894.2 -life-os,.github/workflows/commercial-readiness.yml,pull_request,13,0,7,0,0.0,0.0,0.0,97.5,16615.1 -life-os,.github/workflows/ci.yml,pull_request,13,0,8,0,0.0,0.0,0.0,97.5,28761.6 -life-os,.github/workflows/noema-review.yml,pull_request_target,13,0,9,0,0.0,0.0,0.0,153.0,28033.0 -life-os,.github/workflows/opencode-review.yml,pull_request_target,13,0,9,0,0.0,0.0,0.0,153.0,38923.0 -life-os,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,13,0,8,0,0.0,0.0,0.0,622.0,16650.1 -life-os,.github/workflows/strix.yml,pull_request_target,13,0,9,0,0.0,0.0,0.0,622.5,33408.0 -life-os,dynamic/github-code-quality/codeql,dynamic,13,0,3,0,0.0,0.0,0.0,14938.0,17131.5 -fast-mlsirm,.github/workflows/cflite_pr.yml,pull_request,12,0,1,0,0.0,0.0,0.0,10.0,17479.7 -wardnet,.github/workflows/noema-review.yml,pull_request_target,12,0,8,0,0.0,0.0,0.0,12793.5,30650.5 -wardnet,.github/workflows/strix.yml,pull_request_target,12,0,5,0,0.0,0.0,0.0,11476.5,37712.8 -wardnet,.github/workflows/opencode-review.yml,pull_request_target,12,0,8,0,0.0,0.0,0.0,12793.5,43202.7 -wardnet,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,12,0,5,0,0.0,0.0,0.0,11333.0,16597.0 -wardnet,dynamic/github-code-scanning/codeql,dynamic,12,0,4,0,0.0,0.0,0.0,11619.0,16562.0 -noema,.github/workflows/patch-validator-image.yml,pull_request,11,0,9,0,0.0,0.0,0.0,473.0,13296.5 -noema,.github/workflows/ci.yml,pull_request,11,0,9,0,0.0,0.0,0.0,473.0,13315.0 -noema,.github/workflows/reviewer-ci.yml,pull_request,11,0,9,0,0.0,0.0,0.0,473.0,13288.5 -noema,.github/workflows/security-scan.yml,pull_request,11,0,9,0,0.0,0.0,0.0,377.0,19177.2 -wardnet,.github/workflows/sast-semgrep.yml,pull_request,10,0,4,0,0.0,0.0,0.0,11649.0,17000.0 -wardnet,.github/workflows/security-scan.yml,pull_request,10,0,3,0,0.0,0.0,0.0,10878.5,24466.9 -wardnet,.github/workflows/codeql-pr.yml,pull_request,10,0,7,0,0.0,0.0,0.0,12793.5,32378.1 -life-os,.github/workflows/verify-plugin-delivery-attempt-row-collection.yml,push,10,0,0,0,0.0,0.0,0.0,17843.0,20364.2 -late-life-anxiety-reanalysis,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,10,0,6,0,0.0,0.0,0.0,6163.0,15521.6 -late-life-anxiety-reanalysis,.github/workflows/strix.yml,pull_request_target,10,0,6,0,0.0,0.0,0.0,1853.0,25980.0 -late-life-anxiety-reanalysis,.github/workflows/opencode-review.yml,pull_request_target,10,0,6,0,0.0,0.0,0.0,1853.0,13238.2 -late-life-anxiety-reanalysis,.github/workflows/noema-review.yml,pull_request_target,10,0,6,0,0.0,0.0,0.0,1853.0,25116.9 -late-life-anxiety-reanalysis,dynamic/github-code-quality/codeql,dynamic,10,0,3,0,0.0,0.0,0.0,9971.0,14166.8 -late-life-anxiety-reanalysis,dynamic/github-code-scanning/codeql,dynamic,10,0,3,0,0.0,0.0,0.0,10307.5,14224.5 -wardnet,.github/workflows/fuzz.yml,pull_request,9,0,2,0,0.0,0.0,0.0,12250.5,17298.5 -xtrmLLMBatchPython,dynamic/github-code-quality/codeql,dynamic,8,0,0,0,0.0,0.0,0.0,16709.5,19057.2 -linux-cluster-ops,.github/workflows/pr-governance-body-edit.yml,pull_request,8,0,5,0,0.0,0.0,0.0,3116.0,17151.5 -nonnest2,.github/workflows/R-CMD-check.yaml,pull_request,7,0,0,0,0.0,0.0,0.0,13249.0,18898.2 -nonnest2,.github/workflows/security-scan.yml,pull_request,7,0,1,0,0.0,0.0,0.0,27969.5,31668.5 -nonnest2,.github/workflows/sast-semgrep.yml,pull_request,7,0,0,0,0.0,0.0,0.0,13923.5,19003.2 -nonnest2,.github/workflows/codeql-pr.yml,pull_request,7,0,3,0,0.0,0.0,0.0,39259.0,45262.0 -nonnest2,.github/workflows/opencode-review.yml,pull_request_target,7,0,4,0,0.0,0.0,0.0,29912.0,35630.0 -nonnest2,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,7,0,0,0,0.0,0.0,0.0,13660.5,19018.5 -nonnest2,.github/workflows/noema-review.yml,pull_request_target,7,0,1,0,0.0,0.0,0.0,29715.0,35329.3 -nonnest2,.github/workflows/strix.yml,pull_request_target,7,0,1,0,0.0,0.0,0.0,28509.5,32403.1 -nonnest2,dynamic/github-code-scanning/codeql,dynamic,7,0,0,0,0.0,0.0,0.0,13149.5,18562.5 -xtrmLLMBatchPython,.github/workflows/codeql-pr.yml,pull_request,7,0,2,0,0.0,0.0,0.0,44849.0,45144.2 -xtrmLLMBatchPython,.github/workflows/python-security.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18176.0,18876.8 -xtrmLLMBatchPython,.github/workflows/a2z-compliance.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18519.0,18625.0 -xtrmLLMBatchPython,.github/workflows/jsonl-governance.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18504.0,19448.2 -xtrmLLMBatchPython,.github/workflows/security-scan.yml,pull_request,7,0,2,0,0.0,0.0,0.0,29509.0,32998.7 -xtrmLLMBatchPython,.github/workflows/ci.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18243.0,18712.4 -xtrmLLMBatchPython,.github/workflows/postgres_smoke.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18207.0,18531.0 -xtrmLLMBatchPython,.github/workflows/sast-semgrep.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18547.0,19416.2 -xtrmLLMBatchPython,.github/workflows/validate-compliance.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18546.0,18588.4 -xtrmLLMBatchPython,.github/workflows/env-guard.yml,pull_request,7,0,0,0,0.0,0.0,0.0,18293.0,18520.8 -xtrmLLMBatchPython,.github/workflows/opencode-review.yml,pull_request_target,7,0,4,0,0.0,0.0,0.0,29729.0,33153.6 -xtrmLLMBatchPython,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,7,0,0,0,0.0,0.0,0.0,18353.0,18521.0 -xtrmLLMBatchPython,.github/workflows/noema-review.yml,pull_request_target,7,0,1,0,0.0,0.0,0.0,28805.5,32551.8 -xtrmLLMBatchPython,.github/workflows/strix.yml,pull_request_target,7,0,1,0,0.0,0.0,0.0,29493.0,33129.9 -xtrmLLMBatchPython,.github/workflows/python-security.yml,push,7,0,0,0,0.0,0.0,0.0,18362.0,18723.0 -xtrmLLMBatchPython,.github/workflows/jsonl-governance.yml,push,7,0,0,0,0.0,0.0,0.0,18387.0,18777.6 -.github,.github/workflows/agent-review-runtime-quality-ci.yml,pull_request,7,0,1,0,0.0,0.0,0.0,13044.0,13289.6 -.github,.github/workflows/agent-mention-router.yml,schedule,7,0,1,0,0.0,0.0,0.0,19658.0,25098.2 -ContextualWisdomLab.github.io,dynamic/github-code-quality/codeql,dynamic,7,0,0,0,0.0,0.0,0.0,13758.5,14189.5 -semantic-data-portal,.github/workflows/fuzz.yml,pull_request,7,0,2,0,0.0,0.0,0.0,12668.5,17507.8 -life-os,.github/workflows/verify-habit-review-hostile-sql-evidence.yml,push,7,0,0,0,0.0,0.0,0.0,14837.0,15583.0 -LineageWeave,.github/workflows/repair-877-tick-i18n.yml,push,7,0,0,0,0.0,0.0,0.0,13379.5,18035.6 -mightyETL,.github/workflows/hourly-pr-disposition.yml,schedule,6,0,0,0,0.0,0.0,0.0,13418.0,16671.0 -appguardrail,.github/workflows/commercial-readiness-loop.yml,schedule,6,0,0,0,0.0,0.0,0.0,13969.0,16609.4 -appguardrail,.github/workflows/org-security-failure-collector.yml,schedule,6,0,3,0,0.0,0.0,0.0,18815.5,30330.8 -.github,.github/workflows/repository-metadata-reconcile.yml,schedule,6,0,0,0,0.0,0.0,0.0,14056.0,21230.0 -.github,.github/workflows/organization-commercial-readiness-loop.yml,schedule,6,0,0,0,0.0,0.0,0.0,13551.0,23118.0 -.github,.github/workflows/sbom-inventory-scheduler.yml,schedule,6,0,0,0,0.0,0.0,0.0,13612.0,19855.8 -ContextualWisdomLab.github.io,.github/workflows/security-scan.yml,pull_request,6,0,0,0,0.0,0.0,0.0,29145.0,32324.5 -ContextualWisdomLab.github.io,.github/workflows/codeql-pr.yml,pull_request,6,0,2,0,0.0,0.0,0.0,46376.0,51269.3 -ContextualWisdomLab.github.io,.github/workflows/sast-semgrep.yml,pull_request,6,0,0,0,0.0,0.0,0.0,14282.5,15023.4 -ContextualWisdomLab.github.io,.github/workflows/noema-review.yml,pull_request_target,6,0,1,0,0.0,0.0,0.0,33804.0,39044.1 -ContextualWisdomLab.github.io,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,6,0,0,0,0.0,0.0,0.0,13872.5,14172.6 -ContextualWisdomLab.github.io,.github/workflows/strix.yml,pull_request_target,6,0,1,0,0.0,0.0,0.0,33203.0,34945.2 -ContextualWisdomLab.github.io,.github/workflows/opencode-review.yml,pull_request_target,6,0,2,0,0.0,0.0,0.0,37315.5,39775.2 -ContextualWisdomLab.github.io,dynamic/github-code-scanning/codeql,dynamic,6,0,0,0,0.0,0.0,0.0,13922.0,14412.8 -contextual-orchestrator,.github/workflows/opencode-hourly-loop.yml,schedule,6,0,0,0,0.0,0.0,0.0,16304.0,19399.0 -contextual-orchestrator,.github/workflows/provider-catalog-sync.yml,schedule,6,0,0,0,0.0,0.0,0.0,14133.0,18716.8 -fast-mlsirm,.github/workflows/ci.yml,push,6,0,0,0,0.0,0.0,0.0,, -noema,.github/workflows/hourly-commercial-readiness.yml,schedule,6,0,2,0,0.0,0.0,0.0,11471.0,15701.0 -keyverse,.github/workflows/hourly-product-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,13572.0,22078.4 -ThreadWeave,.github/workflows/hourly-product-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,14038.0,21582.6 -ThreadWeave,.github/workflows/actions-registry-audit.yml,schedule,6,0,4,0,0.0,0.0,0.0,11354.0,17553.2 -ThreadWeave,.github/workflows/hourly-pr-maintenance.yml,schedule,6,0,0,0,0.0,0.0,0.0,13881.0,16881.6 -saju-caldav,.github/workflows/hourly-product-loop.yml,schedule,6,0,0,0,0.0,0.0,0.0,13102.0,15790.4 -life-os,.github/workflows/ai-proposal-live-conformance.yml,schedule,6,0,0,0,0.0,0.0,0.0,13511.0,22065.4 -life-os,.github/workflows/verify-habit-rule-change-authority.yml,push,6,0,0,0,0.0,0.0,0.0,, -life-os,.github/workflows/commercial-readiness.yml,schedule,6,0,0,0,0.0,0.0,0.0,13260.0,16172.8 -life-os,.github/workflows/opencode-commercial-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,14658.0,18183.2 -life-os,.github/workflows/verify-plugin-delivery-status-k6.yml,push,6,0,4,0,0.0,0.0,0.0,93.0,15575.0 -life-os,.github/workflows/verify-planning-task-completion-sql-snapshot.yml,push,6,0,0,0,0.0,0.0,0.0,13580.0,16021.5 -four-pillars,.github/workflows/hourly-nim-product-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,13132.0,20991.2 -four-pillars,.github/workflows/hourly-product-loop.yml,schedule,6,0,0,0,0.0,0.0,0.0,12898.0,21232.4 -DiagramWeave,.github/workflows/hourly-product-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,16057.0,24340.4 -DiagramWeave,.github/workflows/hourly-pr-maintenance.yml,schedule,6,0,0,0,0.0,0.0,0.0,14006.0,16935.6 -OriginWeave,.github/workflows/hourly-product-development.yml,schedule,6,0,0,0,0.0,0.0,0.0,13593.0,22509.4 -mhtml-etl-gateway,.github/workflows/hourly-product-gap.yml,schedule,6,0,0,0,0.0,0.0,0.0,13942.0,20199.0 -LineageWeave,.github/workflows/prov-o-contract.yml,pull_request,6,0,1,0,0.0,0.0,0.0,5.5,10290.2 -LineageWeave,.github/workflows/ontology-pages.yml,pull_request,6,0,1,0,0.0,0.0,0.0,5.0,10747.8 -Orgmetra,.github/workflows/recovery-rehearsal-quality.yml,pull_request,6,0,4,0,0.0,0.0,0.0,1748.0,13628.8 -late-life-anxiety-reanalysis,.github/workflows/sast-semgrep.yml,pull_request,6,0,3,0,0.0,0.0,0.0,10991.5,14806.2 -late-life-anxiety-reanalysis,.github/workflows/codeql-pr.yml,pull_request,6,0,3,0,0.0,0.0,0.0,6163.0,7777.6 -late-life-anxiety-reanalysis,.github/workflows/security-scan.yml,pull_request,6,0,3,0,0.0,0.0,0.0,7059.0,28984.1 -aFIPC,.github/workflows/security-audit.yml,pull_request,5,0,0,0,0.0,0.0,0.0,14250.0,17808.6 -aFIPC,.github/workflows/sast-semgrep.yml,pull_request,5,0,0,0,0.0,0.0,0.0,14475.0,18166.0 -aFIPC,.github/workflows/r.yml,pull_request,5,0,0,0,0.0,0.0,0.0,13969.0,18161.4 -aFIPC,.github/workflows/code-quality.yml,pull_request,5,0,0,0,0.0,0.0,0.0,14282.0,17781.8 -aFIPC,.github/workflows/codeql-pr.yml,pull_request,5,0,1,0,0.0,0.0,0.0,41637.5,45473.1 -aFIPC,.github/workflows/security-scan.yml,pull_request,5,0,0,0,0.0,0.0,0.0,27337.0,31702.0 -aFIPC,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,5,0,0,0,0.0,0.0,0.0,14669.0,17903.8 -aFIPC,.github/workflows/noema-review.yml,pull_request_target,5,0,2,0,0.0,0.0,0.0,28500.0,45562.3 -aFIPC,.github/workflows/opencode-review.yml,pull_request_target,5,0,1,0,0.0,0.0,0.0,37697.5,45364.4 -aFIPC,.github/workflows/strix.yml,pull_request_target,5,0,1,0,0.0,0.0,0.0,29044.0,45910.1 -.github,.github/workflows/agent-mention-router-quality-ci.yml,pull_request,5,0,0,0,0.0,0.0,0.0,12597.0,17941.2 -fast-mlsirm,.github/workflows/hourly-pr-governance.yml,schedule,5,0,1,0,0.0,0.0,0.0,12239.0,17469.0 -life-os,.github/workflows/verify-plugin-delivery-status-row-collection.yml,push,5,0,0,0,0.0,0.0,0.0,15560.0,19519.4 -OriginWeave,dynamic/github-code-quality/codeql,dynamic,5,0,0,0,0.0,0.0,0.0,12464.0,13228.1 -newsdom-api,dynamic/dependabot/dependabot-updates,dynamic,4,0,0,0,0.0,0.0,0.0,, -.github,.github/workflows/pr-auto-rebase.yml,schedule,4,0,0,0,0.0,0.0,0.0,13212.0,17586.9 -.github,.github/workflows/agent-mention-opencode-dispatch.yml,repository_dispatch,4,0,3,0,0.0,0.0,0.0,8.0,9567.7 -OriginWeave,.github/workflows/sast-semgrep.yml,pull_request,4,0,2,0,0.0,0.0,0.0,54.0,11773.8 -OriginWeave,.github/workflows/codeql-pr.yml,pull_request,4,0,2,0,0.0,0.0,0.0,54.0,37268.1 -OriginWeave,.github/workflows/security-scan.yml,pull_request,4,0,2,0,0.0,0.0,0.0,54.0,22857.3 -OriginWeave,.github/workflows/noema-review.yml,pull_request_target,4,0,3,0,0.0,0.0,0.0,55.0,43007.5 -OriginWeave,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,4,0,2,0,0.0,0.0,0.0,55.0,10479.7 -OriginWeave,.github/workflows/strix.yml,pull_request_target,4,0,2,0,0.0,0.0,0.0,57.0,37261.2 -OriginWeave,.github/workflows/opencode-review.yml,pull_request_target,4,0,2,0,0.0,0.0,0.0,56.0,36768.8 -mhtml-etl-gateway,.github/workflows/ci.yml,pull_request,4,0,0,0,0.0,0.0,0.0,14738.0,15898.7 -mhtml-etl-gateway,.github/workflows/sast-semgrep.yml,pull_request,4,0,0,0,0.0,0.0,0.0,14396.0,14777.0 -mhtml-etl-gateway,.github/workflows/security-scan.yml,pull_request,4,0,0,0,0.0,0.0,0.0,26394.0,30830.2 -mhtml-etl-gateway,.github/workflows/codeql-pr.yml,pull_request,4,0,1,0,0.0,0.0,0.0,49160.0,52807.7 -mhtml-etl-gateway,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,4,0,0,0,0.0,0.0,0.0,13867.5,14871.1 -mhtml-etl-gateway,.github/workflows/noema-review.yml,pull_request_target,4,0,2,0,0.0,0.0,0.0,37435.5,45107.4 -mhtml-etl-gateway,.github/workflows/opencode-review.yml,pull_request_target,4,0,0,0,0.0,0.0,0.0,43863.0,46411.8 -mhtml-etl-gateway,.github/workflows/strix.yml,pull_request_target,4,0,2,0,0.0,0.0,0.0,41331.0,45745.2 -mhtml-etl-gateway,dynamic/github-code-quality/codeql,dynamic,4,0,0,0,0.0,0.0,0.0,13801.0,14291.1 -LineageWeave,.github/workflows/repair-866-report-axis-empty.yml,push,4,0,0,0,0.0,0.0,0.0,11981.0,14100.8 -.github,.github/workflows/repository-metadata-reconcile.yml,pull_request,3,0,0,0,0.0,0.0,0.0,12057.0,12861.6 -.github,.github/workflows/trusted-uv-materializer-quality-ci.yml,pull_request,3,0,0,0,0.0,0.0,0.0,12872.0,17439.5 -psychometrics-commons,.github/workflows/sbom-evidence.yml,pull_request,3,0,0,0,0.0,0.0,0.0,12978.0,17799.3 -psychometrics-commons,.github/workflows/supply-chain-provenance.yml,pull_request,3,0,0,0,0.0,0.0,0.0,13158.0,18282.6 -psychometrics-commons,.github/workflows/security-scan.yml,pull_request,3,0,0,0,0.0,0.0,0.0,26497.0,26497.0 -psychometrics-commons,.github/workflows/sast-semgrep.yml,pull_request,3,0,0,0,0.0,0.0,0.0,12926.0,18529.4 -psychometrics-commons,.github/workflows/codeql-pr.yml,pull_request,3,0,0,0,0.0,0.0,0.0,37971.0,37971.0 -psychometrics-commons,.github/workflows/ci.yml,pull_request,3,0,0,0,0.0,0.0,0.0,13190.0,18481.1 -psychometrics-commons,.github/workflows/strix.yml,pull_request_target,3,0,0,0,0.0,0.0,0.0,45929.0,45929.0 -psychometrics-commons,.github/workflows/noema-review.yml,pull_request_target,3,0,0,0,0.0,0.0,0.0,29760.0,29760.0 -psychometrics-commons,.github/workflows/opencode-review.yml,pull_request_target,3,0,0,0,0.0,0.0,0.0,37879.0,37879.0 -psychometrics-commons,.github/workflows/pr-review-merge-scheduler.yml,pull_request_target,3,0,0,0,0.0,0.0,0.0,13800.0,18307.2 -psychometrics-commons,dynamic/github-code-quality/codeql,dynamic,3,0,0,0,0.0,0.0,0.0,13020.0,18384.9 -naruon,dynamic/dependabot/dependabot-updates,dynamic,2,0,0,0,0.0,0.0,0.0,18749.5,18759.8 -.github,.github/workflows/pr-review-merge-scheduler.yml,repository_dispatch,2,0,0,0,0.0,0.0,0.0,15475.0,18490.0 -noema,.github/workflows/reviewer-ci.yml,push,2,0,0,0,0.0,0.0,0.0,14802.5,17439.0 -noema,.github/workflows/ci.yml,push,2,0,0,0,0.0,0.0,0.0,15140.5,17667.2 -keyverse,dynamic/dependabot/dependabot-updates,dynamic,2,0,0,0,0.0,0.0,0.0,, -LineageWeave,.github/workflows/converge-867-current-866.yml,push,2,0,0,0,0.0,0.0,0.0,5629.0,10695.1 -xtrmLLMBatchPython,.github/workflows/postgres_smoke.yml,push,1,0,0,0,0.0,0.0,0.0,18278.0,18278.0 -xtrmLLMBatchPython,.github/workflows/validate-compliance.yml,schedule,1,0,0,0,0.0,0.0,0.0,18826.0,18826.0 -clearfolio,.github/workflows/fuzz.yml,schedule,1,0,0,0,0.0,0.0,0.0,12308.0,12308.0 -scopeweave,.github/workflows/fuzz.yml,schedule,1,0,0,0,0.0,0.0,0.0,12896.0,12896.0 -codec-carver,.github/workflows/fuzz.yml,schedule,1,0,0,0,0.0,0.0,0.0,, -vooster,.github/workflows/world-health.yml,schedule,1,0,0,0,0.0,0.0,0.0,, -vooster,.github/workflows/verify.yml,schedule,1,0,0,0,0.0,0.0,0.0,13711.0,13711.0 -.github,.github/workflows/pr-review-merge-scheduler.yml,schedule,1,0,0,0,0.0,0.0,0.0,14219.0,14219.0 -.github,.github/workflows/audit-central-ruleset.yml,schedule,1,0,0,0,0.0,0.0,0.0,12049.0,12049.0 -.github,.github/workflows/product-performance-attestation-quality.yml,pull_request,1,0,0,0,0.0,0.0,0.0,11437.0,11437.0 -.github,.github/workflows/javascript-coverage-quality-ci.yml,pull_request,1,0,0,0,0.0,0.0,0.0,12656.0,12656.0 -hyosung-itx-slogan-brief,dynamic/github-code-scanning/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,11323.0,11323.0 -fast-mlsirm,.github/workflows/statistical-studies.yml,schedule,1,0,0,0,0.0,0.0,0.0,, -semantic-data-portal,.github/workflows/fuzz.yml,schedule,1,0,0,0,0.0,0.0,0.0,13564.0,13564.0 -noema,.github/workflows/private-vulnerability-reporting-audit.yml,schedule,1,0,0,0,0.0,0.0,0.0,12091.0,12091.0 -noema,.github/workflows/acquisition-readiness-scan.yml,schedule,1,0,0,0,0.0,0.0,0.0,15285.0,15285.0 -noema,.github/workflows/readiness-scan.yml,schedule,1,0,0,0,0.0,0.0,0.0,18336.0,18336.0 -wardnet,.github/workflows/fuzz.yml,schedule,1,0,0,0,0.0,0.0,0.0,14840.0,14840.0 -wardnet,.github/workflows/scorecard-analysis.yml,schedule,1,0,0,0,0.0,0.0,0.0,13194.0,13194.0 -feelanet-adfs,dynamic/github-code-scanning/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,11469.0,11469.0 -disksage,dynamic/github-code-quality/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,13352.0,13352.0 -free-router,dynamic/github-code-scanning/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,, -free-router,.github/workflows/model-catalog-sync.yml,schedule,1,0,0,0,0.0,0.0,0.0,13861.0,13861.0 -RankWeave,dynamic/github-code-quality/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,, -ThreadWeave,dynamic/github-code-quality/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,13060.0,13060.0 -life-os,.github/workflows/verify-planning-completion-http-restack.yml,push,1,0,0,0,0.0,0.0,0.0,, -life-os,.github/workflows/verify-planning-task-due-authority.yml,push,1,0,0,0,0.0,0.0,0.0,, -life-os,.github/workflows/verify-habit-definition-history.yml,push,1,0,0,0,0.0,0.0,0.0,13039.0,13039.0 -metering-billing-platform,dynamic/github-code-quality/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,12859.0,12859.0 -opencode,dynamic/github-code-quality/codeql,dynamic,1,0,0,0,0.0,0.0,0.0,, diff --git a/docs/ci-baseline-20260916.md b/docs/ci-baseline-20260916.md deleted file mode 100644 index 9f836229c6..0000000000 --- a/docs/ci-baseline-20260916.md +++ /dev/null @@ -1,106 +0,0 @@ -# CI baseline — 2026-09-16 - -Measurement window: last 24h ending 2026-09-16T12:57:58Z, all 79 non-archived -`ContextualWisdomLab` repositories, via `GET /repos/{owner}/{repo}/actions/runs?created=>=` -(REST, one repo at a time; GraphQL used only for the repo list). Raw run rows: 9,353 -across 400 (repo, workflow, event) groups. Full per-group detail: `ci-baseline-20260916.csv`. - -## Headline numbers (org-wide, run level) - -| Metric | Value | -|---|---| -| Runs in 24h | 9,353 | -| Cancelled/superseded | 4,000 (42.8%) | -| Run-level queue time (`created_at` → `run_started_at`), p50 / p95 | 0.0s / 0.0s | -| Run-level queue time, max observed | 74,723s (~20.8h), `.github` `codeql-pr.yml` | -| Distinct (repo, workflow, trigger) groups | 400 | - -**The run-level queue KPI is not the right signal here — read the note below before using it.** -`run_started_at` flips to non-null as soon as *any* job in the run leaves the queue, so a run with -one fast job and ten stuck jobs still reports ~0s queue time. This is why p50/p95 are 0.0s for -almost every group in the CSV even on repos with visibly stuck checks. - -## The real symptom: job/check-suite level queuing, confirmed live - -Live GraphQL check-suite query against `fast-mlsirm`'s 5 most recently updated open PRs -(2026-09-16, same session): - -| PR | Rollup state | GitHub Actions check suites, all `QUEUED` | -|---|---|---| -| #1886 | SUCCESS | 0 (only non-GH-Actions app suites, which stay QUEUED indefinitely and are not CI) | -| #1885 | SUCCESS | 0 | -| #1882 | PENDING | 11 | -| #1883 | PENDING | 11 | -| #1884 | PENDING | 11 | - -Newer PR heads (#1885, #1886) completed; older heads (#1882–#1884) sit with all 11 -GitHub-Actions-run check suites permanently `QUEUED`. That head-of-line pattern — a few heads -running, many stuck — reproduces exactly the signature already on record in -[`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`](doctoring/actions-plan-concurrency-ceiling-20260903.md): -single-digit `in_progress` against triple/quadruple-digit `queued`, org-wide. Re-checked live in this -session: - -| Repo | `in_progress` | `queued` | -|---|---|---| -| `fast-mlsirm` | 8 | 220 | -| `.github` | 6 | 220 | -| `bandscope` | 0 | 73 | -| `naruon` | 0 | 67 | - -That doctoring record's conclusion, dated 2026-09-03 and still consistent with this session's -2026-09-16 numbers: the primary bottleneck is a **plan-level concurrent-job ceiling** (user-reported -58-60/60 concurrent jobs in use at the time), not per-repo or per-workflow-file duplication. It -explicitly warns that a large cross-repo workflow-consolidation effort "would be solving the wrong -layer of the problem." This baseline does not contradict that finding — it corroborates it two weeks -later with the same queued≫in_progress shape. - -## Duration (`run_started_at` → completion), heaviest groups - -Excerpt (see CSV for all 400 rows). These durations mostly reflect **policy-accepted long model-review -runs** (see `docs/product-goal-directive.md` §8: OpenCode/Strix/Noema may legitimately run 2+ hours; -`#1889`/`#1890`/`#1892` timeout attempts were reverted on this evidence), not stalls: - -| Repo | Workflow | Trigger | Runs | Cancelled | Duration p50 | Duration p95 | -|---|---|---|---|---|---|---| -| fast-mlsirm | opencode-review.yml | pull_request_target | 68 | 21 (31%) | 8.4h | 12.9h | -| fast-mlsirm | strix.yml | pull_request_target | 68 | 20 (29%) | 5.3h | 11.4h | -| fast-mlsirm | noema-review.yml | pull_request_target | 68 | 20 (29%) | 5.3h | 8.8h | -| bandscope | strix.yml | pull_request_target | 91 | 81 (89%) | 78s | 4.6h | -| `.github` | opencode-review-dispatch.yml | repository_dispatch | 137 | 4 (3%) | 4.2h | 17.8h | - -`bandscope`'s 89% cancellation rate on `strix.yml` (and similarly high on its other 6 required -workflows, all pinned at 91 runs / ~80 cancelled) stands out as the one clear duplication/thrash -signal in this dataset: nearly every PR push on that repo cancels and re-triggers all 7 of its -required workflows, which is exactly the per-push-supersession pattern centralized concurrency -groups are meant to absorb — worth a follow-up look at what is re-triggering so often there. - -## Scheduled/hourly workflows per repo (event = `schedule`) - -22 repos run at least one scheduled workflow in the 24h window; `.github` itself runs 8 distinct -schedules (`agent-mention-router`, `audit-central-ruleset`, `hourly-review-repair`, -`organization-commercial-readiness-loop`, `pr-auto-rebase`, `pr-review-merge-scheduler`, -`repository-metadata-reconcile`, `sbom-inventory-scheduler`) — already the central scheduler this -task's Step 3 asked to consolidate *toward*. Per-repo counts, full list in the aggregate output; -most repos run 1-2 product-loop schedules of their own (`hourly-product-development.yml`, -`commercial-readiness*.yml`, etc.) that are product-specific automation, not CI/security gates, and -are out of scope for the CI-centralization goal. - -## Duplicate-check check - -No case was found in this 24h window where the *same* check category (e.g. CodeQL, Semgrep, secret -scan) runs from both a per-repo workflow file and an independent org-required workflow on the same -head for the same purpose — `docs/doctoring/ci-workflow-duplication-audit-20260902.md` (existing, -2026-09-02) already covers this ground in more depth than this session re-derived and found the same: -duplication is not the primary driver of queue depth. - -## What this baseline changes about the task's plan - -Given the above, the Step 2/3 "centralize workflows to fix queue stalls" framing needs one -correction before more PRs get written against it: **workflow centralization is real hygiene -(fewer files to keep in sync, one required-check set) but is not a fix for the current queue -depth**, per the existing, still-live doctoring finding. The concurrency-group gap search in this -session (`grep` across `.github/workflows/*.yml` for a missing `concurrency:` block) found no -event-triggered required workflow lacking one — the 6 files without a `concurrency:` block are all -`workflow_call` reusable workflows (concurrency is correctly the caller's job) or -`issue_comment`/`schedule`-triggered (not supersession-prone). That specific low-risk fix this task -proposed as the smallest first step is already done. diff --git a/docs/doctoring/actions-capacity-root-cause-20260917.md b/docs/doctoring/actions-capacity-root-cause-20260917.md deleted file mode 100644 index 75a463b98d..0000000000 --- a/docs/doctoring/actions-capacity-root-cause-20260917.md +++ /dev/null @@ -1,136 +0,0 @@ -# Doctoring record: the multi-hour review durations are inter-job global queue wait, not model/build time (2026-09-17) - -- **Date:** 2026-09-17 -- **Subject:** `docs/ci-baseline-20260916.md` measured multi-hour p50/p95 durations for the long - AI-review workflows (`opencode-review.yml` p50 8.4h/p95 12.9h, `strix.yml` p50 5.3h, - `noema-review.yml` p50 5.3h, `.github` `opencode-review-dispatch.yml` p50 4.2h) and this task's - original framing proposed capping concurrency for that job class. Maintainer steering asked for a - per-step time breakdown before any capping: is the duration model API latency, retries/backoff, - rate-limit waits, un-batched per-file/per-chunk calls, sleep/poll loops, repeated - dependency installs/builds, or duplicated coverage/test execution? This record answers that with - measured job-level timestamps from two completed runs of the workflow that does the actual heavy - work (`opencode-review-dispatch.yml` in `.github` — see "Where the work actually happens" below). -- **Decision record:** none yet — this is the root-cause measurement the next decision (whether a - capacity-reservation concurrency cap is still needed) should be based on. - -## Where the work actually happens - -`opencode-review.yml` is the `pull_request_target`-triggered required-check entry point that runs -in each target repo's context. Its `coverage-source-tree` and `coverage-evidence` jobs are -deliberately no-op placeholders — each is a single `echo` step with no `needs:` edge between them — -whose inline comment already documents why: a real `needs:` edge between two jobs that declare no -`outputs:` only orders two context holders, and "under a saturated queue each link waits out the -whole queue again," citing a prior measurement on `naruon#1528` (run 33581213805) where that exact -pattern cost 22h41m of pure queueing for two single-echo jobs before it was fixed by depending both -directly on `admit-current-head` so they run in parallel. The actual coverage measurement and review -publication happen in `opencode-review-dispatch.yml` (`.github`, `repository_dispatch`-triggered), -which `opencode-review.yml` invokes. That workflow's job chain is -`validate-pr-metadata` → `coverage-source-tree` → `coverage-evidence` → `opencode-review-target`, -with real (not placeholder) `needs:` edges: `coverage-source-tree` uploads a -`opencode-coverage-source` tarball artifact that `coverage-evidence` downloads, and -`opencode-review-target` consumes `coverage-evidence`'s output. - -## Measured evidence - -Job-level `started_at`/`completed_at` timestamps, `repos/ContextualWisdomLab/.github/actions/runs//jobs`, -gathered 2026-09-17 for two completed runs of `OpenCode Review Dispatch` (workflow id `322670888`): - -**Run 34931908846 (started 2026-09-15, during the saturated period this baseline documents):** - -| Job | Started | Completed | Job duration | Wait since prior job completed | -|---|---|---|---|---| -| `validate-pr-metadata` | 17:44:59 | 17:45:04 | 5s | — | -| `coverage-source-tree` | 21:50:17 | 21:50:24 | 7s | 4h05m13s | -| `coverage-evidence` | 01:27:35 (+1d) | 01:28:47 | 1m12s | 3h37m11s | -| `opencode-review-target` | 07:23:03 | 07:42:31 | 19m28s | 5h54m16s | - -Total wall time (first job start → last job completion): ~13h57m. Sum of actual job execution: -5s + 7s + 72s + 1168s ≈ **21 minutes (2.5% of wall time)**. Sum of inter-job queue wait: -**~13h36m (97.5% of wall time)**. - -**Run 34756591400 (started 2026-09-13, lighter load) — the identical 4-job chain:** - -| Job | Started | Completed | Wait since prior job completed | -|---|---|---|---| -| `validate-pr-metadata` | 12:22:15 | 12:22:20 | — | -| `coverage-source-tree` | 12:24:38 | 12:24:47 | 2m18s | -| `coverage-evidence` | 12:25:13 | 12:27:43 | 26s | -| `opencode-review-target` | 12:28:40 | 12:36:55 | 57s | - -Total wall time: 14m40s, essentially all of it job execution. The workflow's own logic and step -content did not change between these two runs — the ~57x difference in total wall time (13h57m vs -14m40s) is explained entirely by how long each job waited to be admitted to a runner, which tracks -org-wide Actions saturation at the time, not anything the workflow does. - -## What this rules out - -- **Model API latency / retries / rate-limit waits:** the `opencode-review-target` job — which is - where the actual model calls happen — took 19m28s and 8m15s respectively in the two sampled runs. - Consistent with ordinary model-review work, not a multi-hour stall. -- **Sleep/poll loops waiting on another run:** none exist in `strix.yml`, `noema-review.yml`, or - `opencode-review.yml`; `opencode-review-dispatch.yml`'s few `sleep 5`/`sleep 10` occurrences are - bounded (≤120s) retry backoffs for transient `gh api` failures during head-fetch/publication, not - busy-waits on another job or run. `opencode-review.yml`'s required job specifically forbids - `sleep `/`while :; do`/`poll_interval_seconds` and is contract-tested to stay that way - (`tests/test_opencode_required_rerun_capacity.py`); it wakes via a targeted - `repository_dispatch` callback instead of polling. -- **Repeated, cacheable dependency installs/builds:** real, but already the subject of active fixes - landed just before this session (`11a56305b` "build PyO3/maturin extensions offline before - coverage", `efc35f72b` "vendor Cargo deps offline for the coverage sandbox") — and even fully - un-cached, those builds run inside the `coverage-evidence` job, whose own execution time (72s and - 2m30s in the two samples) is a small fraction of the job's total wait. -- **Duplicated coverage/test execution:** `opencode-review.yml`'s own `coverage-source-tree`/ - `coverage-evidence` jobs do not re-run coverage; they are no-op placeholders that exist only to - keep a stable required-check name in branch protection, per their own inline comment. - -## What this confirms - -The dominant cost is **inter-job wait for a fresh runner inside a single workflow run**, compounding -once per `needs:` edge, under the org's global concurrent-job ceiling -(`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`). `opencode-review.yml` already -applied the available fix for this (parallelize independent placeholder jobs instead of chaining -them) after discovering the identical pattern on `naruon#1528`. The same fix is **not available** -for `opencode-review-dispatch.yml`'s chain, because unlike the placeholder jobs, these three jobs -have a genuine data dependency (source tree → build artifact → review) *and* a deliberate, -already-documented trust boundary: `coverage-evidence` runs untrusted PR-head test/build code with -only `actions: read` permission (its own inline comment: "No repository-content, identity, secret, -or write token is available to untrusted tests"), isolated from `coverage-source-tree`'s -`id-token: write` app-token exchange and `opencode-review-target`'s broad write permissions -(`issues: write`, `pull-requests: write`, `statuses: write`, `security-events: read`). Merging these -jobs to remove queue-wait would let untrusted PR content execute in a process that recently held (or -will hold) elevated/write-capable tokens — a security regression this task's rules explicitly -forbid trading against speed. Reducing job count is therefore not an available lever for this -specific chain; the queue-wait can only be reduced by changing how many jobs of this class compete -for runners at once, which is what a capacity-reservation concurrency cap (if adopted) would target -directly, with this measurement as its justification rather than a bypassed diagnosis step. - -## Bandscope re-trigger check (task item 3, partial) - -Checked whether `bandscope`'s 89% required-workflow cancellation rate wastes runner-seconds (jobs -cancelled after starting) or is pure pre-admission churn (cancelled before a runner is ever -assigned). Sampled commit and check-suite timestamps on 5 open `bandscope` PRs via GraphQL: pushes -arrive in bursts (6–10 commits within 5–10 minutes, single author identity, consistent with this -org's documented shared agent-session identity actively iterating on a PR — not a bot loop or -webhook misfire) and the concurrency-group cancellation for the prior commit's check suites completes -within 1–3 seconds of the next commit's check suites being created — i.e. before any of those jobs -could plausibly have reached `in_progress`. The high cancellation rate is the existing -`cancel-in-progress` concurrency groups working as designed against a fast push cadence; it is not -evidence of wasted runner-slot time and does not, by itself, justify a workflow change. No further -action taken on item 3 in this record; still open whether the push cadence itself (many small commits -per PR in a short window) is worth addressing for reasons other than Actions capacity (e.g. review -noise), which is outside this task's scope. - -## Audit trail - -- `repos/ContextualWisdomLab/.github/actions/runs/34931908846/jobs` and - `repos/ContextualWisdomLab/.github/actions/runs/34756591400/jobs` (REST, GitHub API, 2026-09-17). -- `.github/workflows/opencode-review.yml` lines ~290–319 (placeholder jobs and their inline - queue-wait comment citing `naruon#1528` run 33581213805). -- `.github/workflows/opencode-review-dispatch.yml` `coverage-source-tree`/`coverage-evidence`/ - `opencode-review-target` job definitions and their `permissions:` blocks. -- `tests/test_opencode_required_rerun_capacity.py` (event-driven wake contract, no polling). -- Commits `11a56305b`, `efc35f72b` (offline build caching already landed). -- GraphQL `checkSuites`/commit timestamps on `ContextualWisdomLab/bandscope` PRs #1227, #1188, - #1221, #1204, #1126 (2026-09-17). -- `docs/ci-baseline-20260916.md`, `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`, - `docs/adr/0030-ci-centralization-scope-given-plan-ceiling.md`. diff --git a/docs/doctoring/actions-queue-24h-remeasurement-20260917.md b/docs/doctoring/actions-queue-24h-remeasurement-20260917.md deleted file mode 100644 index 15131ace08..0000000000 --- a/docs/doctoring/actions-queue-24h-remeasurement-20260917.md +++ /dev/null @@ -1,135 +0,0 @@ -# Doctoring record: Actions queue remeasurement after #2232/#2233/#2235/#2236 (2026-09-17) - -- **Date:** 2026-09-17 -- **Subject:** After the coalesce-tick observability and fail-open repairs - (`#2232`, `#2233`) plus the same-day Strix/Noema evidence-binding merges - (`#2235`, `#2236`), remeasure org Actions queue depth, the concurrent-job - ceiling signal, and whether the five-minute coalesce tick now produces run - records under saturation. -- **Decision record:** none — diagnostic remeasurement only; does not authorize - plan-tier changes or further workflow edits by itself. -- **Measured at:** `2026-09-17T13:11:09Z`–`13:16:06Z` (UTC), via REST - (`gh api`), against the live `ContextualWisdomLab` organization. - -## Merge timeline (prerequisite) - -| PR | Merged (UTC) | Head (short) | Title | -|---|---|---|---| -| `#2232` | `2026-09-17T10:33:07Z` | `b49641744ed6` | step-scope coalesce tick gate so schedule produces run records | -| `#2233` | `2026-09-17T10:35:23Z` | `d35788d73ab5` | fail-open coalesce when tick has not completed recently | -| `#2235` | `2026-09-17T12:06:56Z` | `130ce425f74c` | Strix: bind findings/remediation claims to authenticated evidence | -| `#2236` | `2026-09-17T12:53:17Z` | `4fda7f504e58` | Noema: bounded transport-capacity re-dispatch | - -Protected `main` at measurement: `4fda7f504e58` (`#2236`). - -## 1. Org Actions queue depth - -Full census of all **66** non-archived, non-fork repositories -(`orgs/ContextualWisdomLab/repos`), summing -`actions/runs?status=in_progress|queued&per_page=1` → `.total_count`: - -| Metric | Value | -|---|---| -| Org-wide workflow runs `in_progress` (sum) | **48** | -| Org-wide workflow runs `queued` (sum) | **1,911** | -| `.github` alone | `in_progress=10`, `queued=342`–`343` | -| Open PRs org-wide (`search/issues` `is:pr is:open`) | **4,288** | -| `.github` schedule runs currently `queued` | **15** | - -Top queued repositories at the same sample: - -| Repository | `in_progress` | `queued` | -|---|---|---| -| `.github` | 10 | 342 | -| `codec-carver` | 11 | 131 | -| `fast-mlsirm` | 3 | 131 | -| `late-life-anxiety-reanalysis` | 0 | 121 | -| `newsdom-api` | 5 | 121 | -| `pg-erd-cloud` | 1 | 114 | -| `appguardrail` | 4 | 112 | -| `contextual-orchestrator` | 1 | 112 | -| `clearfolio` | 2 | 108 | - -**Reading:** the backlog shape is unchanged from the 2026-09-03 ceiling diagnosis -(`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`): single- to -low-double-digit `in_progress` against four-digit `queued` org-wide. The -coalesce/fail-open repairs did not drain the queue; they were never expected to. -Compared with that earlier 3-repo snapshot (`.github` 1,877 queued alone), -`.github`'s own queued count is lower (~342), but the org-wide sum remains -~1.9k with thousands of open PRs still feeding required workflows. - -## 2. Concurrent-job ceiling signal - -GitHub still does not expose the org plan concurrent-job quota through REST. -This remeasurement therefore corroborates the previously recorded **~60** -plan-level ceiling with live occupancy proxies: - -| Proxy | Value | Notes | -|---|---|---| -| Prior primary evidence | ~58–60 / 60 | User-observed billing UI, 2026-09-03 (same ceiling doc) | -| Org-wide runs `in_progress` | 48 / 66 repos | Run-level proxy; one run may hold multiple jobs | -| Jobs `in_progress` in 15 busiest repos | **42** | Sampled `runs?status=in_progress` → per-run `/jobs` | -| Hosted org runners API | `total_count=0` | No self-hosted pool; hosted plan quota is the ceiling | - -The occupancy band (mid-40s jobs/runs concurrently active while ~1.9k runs sit -`queued`) remains the signature of a hard org-wide concurrent-job ceiling, not -of a per-repository workflow defect. No billing-UI re-read was available to this -session; the **60** figure is carried forward from the prior primary evidence, -not independently re-derived from Settings → Actions. - -## 3. Coalesce tick run records since `#2232` - -Workflow: `.github/workflows/opencode-review-coalesce-tick.yml` (id `360129488`), -cron `*/5 * * * *`, concurrency group `opencode-review-coalesce-tick` with -`cancel-in-progress: false`. Repo variable -`OPENCODE_REVIEW_COALESCE_ENABLED=false` (coalescing still inert by design). - -| Observation | Evidence | -|---|---| -| Workflow `runs` total | `total_count=2` | -| Pre-`#2232` sample | `35191169833` at `2026-09-17T06:44:23Z`, `completed`/`skipped`, job skipped immediately (job-level gate era or equivalent) | -| Post-`#2232` sample | **`35219385415`** at `2026-09-17T12:07:50Z`, still `status=queued` at `13:16Z` | -| Post-merge job shape | Job `coalesce-tick` is `status=queued` (waiting for a runner), **not** immediately job-skipped — proves the step-scope gate admits the job into the runner queue | -| Flag still off | `vars.OPENCODE_REVIEW_COALESCE_ENABLED=false` → when the job eventually runs, the first step exits inert and remaining steps stay skipped | -| Schedule still enqueueing generally | `.github` `event=schedule&status=queued` → 15 runs (Daily Review Recovery, PR Auto Rebase, Required PR Review Merge Scheduler, this tick, …) | - -**Reading relative to `docs/doctoring/actions-schedule-run-records-20260917.md`:** -that earlier record measured `total_count=0` for this workflow while the gate was -job-scoped. After `#2232`, at least one schedule run record exists and is sitting -in the same org admission backlog as every other schedule job. Only one post-merge -tick run is present as of this sample (created ~94 minutes after `#2232` merged); -the workflow concurrency group (at most one active + one pending, no cancel of -in-flight) plus multi-hour runner wait explains why the five-minute cron does not -accumulate unbounded stacked run records while the first tick remains `queued`. - -`#2233`'s scheduler fail-open path is not exercised while the flag is `false` -(coalescing disabled → dispatch does not wait on tick completion). It remains -the safety net for the day the flag is flipped on under the same saturation. - -`#2235` / `#2236` are evidence-binding / transport repairs; they do not change -queue depth or tick observability. They are listed here only because this -remeasurement was gated on all four merges landing. - -## What this does / does not decide - -- **Does confirm:** step-scoped coalesce tick observability works under live - saturation (run + job enter `queued` instead of vanishing). -- **Does confirm:** org queue remains ceiling-bound (~48 concurrent runs / - ~42 sampled concurrent jobs vs ~1.9k queued). -- **Does not:** raise the plan tier, enable `OPENCODE_REVIEW_COALESCE_ENABLED`, - or claim the backlog is draining. -- **Still owner-only:** verify the exact concurrent-job quota on the org - Actions/Billing UI if the ~60 figure must be re-attested for a purchase - decision. - -## Audit trail - -- REST census script output: `/tmp/cwl-queue-census.json` (66-repo - `in_progress`/`queued` totals; ephemeral local cache for this session). -- `repos/ContextualWisdomLab/.github/actions/workflows/opencode-review-coalesce-tick.yml/runs` -- `repos/ContextualWisdomLab/.github/actions/runs/35219385415` (+ `/jobs`) -- `repos/ContextualWisdomLab/.github/actions/variables/OPENCODE_REVIEW_COALESCE_ENABLED` -- Prior related records: - `docs/doctoring/actions-schedule-run-records-20260917.md`, - `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`, - `docs/doctoring/actions-capacity-root-cause-20260917.md` diff --git a/docs/doctoring/actions-queue-cancelled-before-runner.md b/docs/doctoring/actions-queue-cancelled-before-runner.md deleted file mode 100644 index 983f36549a..0000000000 --- a/docs/doctoring/actions-queue-cancelled-before-runner.md +++ /dev/null @@ -1,56 +0,0 @@ -# Actions queue cancellation before runner assignment - -## Status - -Proposed owner-side diagnostic extension for `ContextualWisdomLab/.github#1150` and the organization Actions incident tracked by `ContextualWisdomLab/.github#712`. - -## Problem - -A current pull-request head can produce a terminal GitHub Actions run whose job was cancelled before any runner was assigned or any step executed. Treating that evidence as a generic terminal job loses the first non-executed boundary and can mislead incident triage even though it must never count as passing evidence. - -Observed organization evidence on 2026-09-02 includes `.github#1653`, where a current-head `Repository Metadata Reconcile` job terminated `cancelled` after previously showing `runner_id=0`, empty runner identity, and `steps=[]`. Separate ContextualWisdomLab repositories also reproduce zero-job `startup_failure` and long-lived unassigned queue states, so these states must remain distinct rather than being collapsed into a product-source failure. - -A second adapter-boundary case is `pull_request_target`: GitHub records the workflow run against the base commit while the linked pull-request object carries the exact pull-request head. A terminal diagnostic collector that searches only by the current pull-request `head_sha` therefore cannot see a target-triggered cancellation even when its linked pull-request identity is current. Conversely, once target evidence is collected, a concurrent push or close can make the identity snapshot used for classification stale unless the collector revalidates the PR view after all terminal/job reads. - -## Decision - -The queue-health collector keeps external GitHub conclusion values unchanged at the adapter boundary and adds a semantic internal/report classification. For exact current heads it now: - -- retains `startup_failure` and `cancelled` terminal diagnostics from the bounded exact-`head_sha` `status=completed` query for ordinary pull-request/head-bound runs, filtering the returned conclusion locally; -- performs a bounded `status=cancelled&event=pull_request_target` candidate read for the target-triggered cancellation case and retains a candidate only after the existing linked pull-request number/head identity resolver proves it belongs to an exact current head; -- does **not** send `status=startup_failure` to GitHub's workflow-run list endpoint because that value is not in the endpoint's documented `status`/conclusion filter enumeration; target-triggered zero-job startup-failure discovery therefore remains a separate unresolved diagnostic gap rather than being implemented through an invalid REST request; -- fetches job evidence only for retained current-head terminal diagnostics; -- re-reads the bounded open-PR identity view after terminal and job evidence collection and fails the repository snapshot if PR number/state/head identity differs from the view used for classification; -- classifies a job as `cancelled_before_runner_assignment` only when both the run and that job conclude `cancelled`, the job has no runner assignment, and it has zero executed/materialized steps; -- never reclassifies sibling jobs that concluded `skipped`, `success`, or another non-cancelled state merely because their parent run concluded `cancelled`; -- keeps ordinary exact-head zero-job startup failures as `startup_failure_before_job_materialization`; -- reports an additive `admission_state` and a summary count without changing any GitHub check conclusion or synthesizing success; -- recommends inspection of Actions runner admission, billing/usage, runner-group policy, scheduler capacity, concurrency, and cancellation provenance rather than leaf-source churn or gate weakening. - -## TDD lineage - -RED commit `af72a26e0d1d845a7b447a63c7d4de4867815a87` added the first deterministic regression whose current-head cancelled run has one job with `runner_id=0`, an empty runner name, and `steps=[]`. GREEN commit `79e0758d0583474934327039b065956976c64453` introduced the initial cancellation classification. - -RED commit `b4f95bc290e625649b8ce7ae59e157c3869466f2` then captured two successor defects found on the live writer: a `pull_request_target` cancellation whose run-level SHA is the base commit but whose linked pull-request head is current, and a skipped sibling job inside a cancelled run that must not be counted as a pre-runner cancellation incident. GREEN commit `5a4950bb996f80f7be2519432a3f5b74bea02d58` added target-event candidate collection with linked-head verification and required the matched job itself to conclude `CANCELLED` before applying the semantic incident classification. - -Primary-source verification then found that GitHub's documented repository workflow-run `status` filter accepts `completed`, `action_required`, `cancelled`, `failure`, `neutral`, `skipped`, `stale`, `success`, `timed_out`, `in_progress`, `queued`, `requested`, `waiting`, and `pending`, but not `startup_failure`. RED `b99839bdcffecccc88b364a9813676b3964535b9` rejects any attempted `status=startup_failure` request in the deterministic target-cancellation fixture. GREEN `f567b1182308e4b45e22bf2f13b214998f59f5d0` narrows target-event filtering to the supported `cancelled` conclusion while leaving ordinary exact-head `status=completed` collection and local `startup_failure` conclusion classification intact. - -Review of that successor exposed a final consistency-window defect: target cancellation/job reads occurred after the collector's prior `final_pull_requests` read, so a later push or close could allow stale target evidence to survive. RED `d3a11383ce717217ec4c80a5d65c84aa947570e3` changes the PR head only after target and job evidence has been read and requires fail-closed rejection. GREEN `7683d2219c8007f9e7fa6001c98d0944290fa756` adds the post-evidence identity read and rejects any number/state/head divergence before a repository snapshot is emitted. - -## Compatibility and risk - -This is an additive diagnostic-contract change. It does not mutate repository branches outside the canonical PR, cancel/rerun Actions, alter branch protection, change database state, or modify an external GitHub schema. `status`, `conclusion`, `runner_id`, and related GitHub payload keys remain vendor-owned adapter fields; organization-owned report vocabulary uses semantic multiword names. - -Exact-head completed-run searches retain the existing twenty-page / 1,000-result fail-closed ceiling. Because GitHub's repository workflow-run API does not expose a pull-request-number filter for `pull_request_target`, cancelled target-event candidates are read by supported `cancelled` status and event under the same bounded ceiling, then filtered by linked current-head identity before retention. If that bounded candidate set is exceeded, or if the post-evidence PR identity view changes, the repository becomes explicit incomplete collection evidence rather than silently truncating or preserving stale evidence. This is an availability trade-off, not permission to synthesize success or churn leaf repositories. - -A cancelled run with a runner-assigned, step-executing, or non-cancelled matched job remains ordinary terminal evidence and is not reclassified as a pre-runner admission failure. A `pull_request_target` startup failure that cannot be discovered through the supported target-cancellation query also remains incomplete evidence; it is not silently treated as healthy. - -## Primary-source traceability - -GitHub, Inc. (2026). *REST API endpoints for workflow runs*. GitHub Docs. Retrieved September 2, 2026, from https://docs.github.com/en/rest/actions/workflow-runs - -The documented endpoint contract is treated as the authority for request-filter vocabulary; live GitHub run payloads remain the authority for observed run conclusions. The distinction prevents an undocumented observed conclusion such as `startup_failure` from being incorrectly assumed to be a valid REST query-filter value. - -## Verification - -Only checks produced from the unchanged final `ContextualWisdomLab/.github#1150` head qualify. Queued, pending, cancelled, zero-job startup failures, predecessor checks, or stale reviews are incomplete evidence and must not be transferred to a newer head. The RED/GREEN lineage above documents source intent; hosted 100% statement/branch/docstring and required-workflow evidence must be re-established on the final exact head before ordinary merge. \ No newline at end of file diff --git a/docs/doctoring/actions-queue-health.md b/docs/doctoring/actions-queue-health.md deleted file mode 100644 index e7d08007a2..0000000000 --- a/docs/doctoring/actions-queue-health.md +++ /dev/null @@ -1,106 +0,0 @@ -# GitHub Actions queue-health evidence - -The scheduled `actions-queue-health.yml` workflow reads a fixed allowlist of -CWL repositories once per hour and publishes a JSON report plus a keyboard- -readable HTML report as an artifact. The collector uses only `gh api` reads -through the configured cross-repository `PR_REVIEW_MERGE_TOKEN` or -`OPENCODE_APPROVE_TOKEN`; it fails visibly when neither credential is present. -It does not cancel runs, mutate branches, dispatch workflows, or alter merge -gates, and it never relies on the central repository's scoped `GITHUB_TOKEN` -for sibling-repository reads. - -The report schema is `actions.queue_health.v1`. Each observed run records its -repository, pull-request number, head SHA, event, run attempt, concurrency -group (or an explicit unavailable marker), stable workflow identity, queue age, -job state, and runner assignment. When GitHub supplies a positive -`workflow_id`, the report exposes `workflow_identity` as `workflow_id:` and -uses that value for duplicate-lane grouping; `workflow_name` remains -presentation data. Older/offline v1 snapshots that lack `workflow_id` retain a -compatibility fallback of `workflow_name:`. A malformed present -`workflow_id` fails closed instead of being coerced. - -A run is `current_head` only when its linked open pull request and head SHA -match. The match compares the open pull request's head SHA against the *linked* -pull-request entry's head SHA carried on the run (`run.pull_requests[].head.sha`), -never against the run-level `head_sha`. `pull_request_target`-triggered runs -report the base-branch commit that was checked out as their run-level -`head_sha`, so comparing against that value would misclassify a genuinely -active, current required-workflow run as obsolete and skip its job evidence. -Stale linked runs are `obsolete`; runs without a pull-request link are -`unlinked`. Queued evidence remains incomplete even when a report is -successfully produced. GitHub's `waiting` job status (paused on an environment -or deployment approval) is also treated as pending evidence, distinct from a -runner-capacity blocker. - -Pull-request identity is sampled before and after the bounded active-run -sweeps. The repository snapshot is accepted only when the open pull-request -number/state/head view is unchanged. A push, closure, or other identity change -between those samples becomes repository-scoped incomplete evidence instead of -being allowed to invert current/obsolete classification. A pull-request -response with incomplete head/base identity retains one bounded retry after a -one-second delay. - -Queue age for a fetched job is measured from that job's own `created_at`, not -the parent run's, so a later job in an already in-progress run (for example one -gated by `needs:`) that only just became eligible is not measured against the -whole run's age and does not trigger a false capacity-breach alert. Every row -exports both `queue_age_started_at` and `queue_age_source` (`job_created_at` or -`run_created_at`) so consumers can reproduce the reported `queue_age_seconds`. -Requested, pending, and queued runs intentionally use run-level evidence when -GitHub has not supplied job detail. - -Two bounded active-status sweeps run in opposite orders and must agree before -the snapshot is accepted. This prevents historical completed runs from -exhausting the bound while rejecting evidence that changes between partitioned -reads. Each status read is capped at one 50-run page, limiting collection to ten -run-list calls per repository; exceeding the cap is reported as incomplete -evidence. Current-head `in_progress` and `waiting` runs make the additional jobs -API read needed to distinguish concrete runner assignment from an environment -or deployment approval wait. - -List endpoints use collector-controlled GitHub API pagination with at most 20 -explicit page reads; the collector never asks GitHub CLI to download an -unbounded page set and never requests page 21. Pull-request and job lists use -pages of 100 records; workflow-run lists use pages of 50 so a large Actions -queue does not require one oversized response. An incomplete, malformed, or -larger response is recorded as repository-scoped incomplete evidence and the -collector continues with the remaining allowlisted repositories; it never -silently claims that the visible page is the whole queue. The JSON and HTML -reports expose each collection error explicitly. - -Every external `gh api` read has a 30-second subprocess timeout, and the -collector job has a 30-minute execution ceiling. A timeout is typed as -incomplete queue evidence rather than success. Repository names reject `.` and -`..` path segments. Offline snapshots also reject duplicate repository entries -before counting runs so repeated input cannot inflate the reported queue. - -The default queue-age SLO is 900 seconds. A current-head job that remains -unassigned beyond that limit produces a warning and an explicit manual action -to inspect runner capacity, billing, runner-group policy, environment approval, -and concurrency saturation. The workflow intentionally remains read-only and -fail-closed when GitHub API or runner evidence is unavailable. Paged API reads -are not atomic; changing totals are retained only when the collected records -cover the largest observed total, and the report remains explicitly an -observation rather than a merge decision. - -Implementation ownership is intentionally split without duplicate collector -copies: `actions_queue_health_core.py` owns the shared bounded parsing and -reporting primitives, while the executable `actions_queue_health.py` entrypoint -owns stable pull/workflow identity and audit-provenance reconciliation. Tests -load the executable boundary used by the scheduled workflow. - -The allowlist is deliberately explicit in -`config/actions_queue_health_repositories.json`; adding a repository requires -review of its governance and data boundary. This first slice does not claim -that a queued run is obsolete or safe to cancel. - -## References - -GitHub. (n.d.). *REST API endpoints for workflow runs*. Retrieved August 20, -2026, from https://docs.github.com/en/rest/actions/workflow-runs - -Internet Engineering Task Force. (2022). *HTTP semantics* (RFC 9110). -https://www.rfc-editor.org/rfc/rfc9110 - -OWASP Foundation. (n.d.). *Path traversal*. Retrieved August 20, 2026, from -https://owasp.org/www-community/attacks/Path_Traversal diff --git a/docs/doctoring/actions-schedule-run-records-20260917.md b/docs/doctoring/actions-schedule-run-records-20260917.md deleted file mode 100644 index d82ab19568..0000000000 --- a/docs/doctoring/actions-schedule-run-records-20260917.md +++ /dev/null @@ -1,52 +0,0 @@ -# Doctoring record: scheduled workflows still enqueue run records under saturation; coalesce tick had none (2026-09-17) - -- **Date:** 2026-09-17 -- **Subject:** After org-wide Actions saturation (~01:32Z), operators observed queued - schedule runs sitting for 3+ hours and believed no new schedule records were being - created. The coalesce tick workflow (`opencode-review-coalesce-tick.yml`, id - `360129488`) showed zero runs while `OPENCODE_REVIEW_COALESCE_ENABLED=false`. -- **Decision record:** none — diagnostic plus a step-scoped gate repair for the tick - workflow. - -## Measured evidence - -REST sample gathered 2026-09-17 (`repos/ContextualWisdomLab/.github/actions/runs`): - -| Observation | Evidence | -|---|---| -| Schedule runs still created after 01:32Z | `35170930384` Repository Metadata Reconcile at `2026-09-17T01:32:08Z` (queued); `35182924821` Daily Review Recovery at `04:42:31Z` (queued); `35183563151` PR Auto Rebase at `04:52:33Z` (queued) | -| Six schedule runs currently queued | `status=queued&event=schedule` → `total_count=6` | -| Coalesce tick zero runs | `actions/workflows/opencode-review-coalesce-tick.yml/runs` → `total_count=0` | -| Coalesce flag off | repo variable `OPENCODE_REVIEW_COALESCE_ENABLED=false` | - -The org-wide stall is therefore **runner admission under the plan concurrent-job ceiling** -(`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`), not GitHub ceasing to -create schedule run records entirely. New schedule records continue to arrive; they -queue behind thousands of other jobs and rarely reach `in_progress`. - -## Coalesce tick zero-run root cause - -The tick workflow used a **job-level** `if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true'`. -When the variable is `false`, GitHub does not enqueue a workflow run for that schedule -event at all — confirmed live: zero runs since merge at `f9863d941` even though the -five-minute cron has elapsed many times. That made the tick invisible in the Actions UI -and prevented `recent_coalesce_tick_completed()` from ever observing a completed tick, -which would have blocked review dispatch indefinitely had coalescing stayed enabled without -the scheduler fail-open repair. - -## Repair - -1. **Scheduler fail-open** (`scripts/ci/pr_review_merge_scheduler_core.py`): when - coalescing is enabled but no tick completed within `600s` (2× the cron interval), - `dispatch_opencode_review()` dispatches immediately instead of returning `coalescing`. -2. **Tick observability** (`opencode-review-coalesce-tick.yml`): move the flag gate from - job scope to step scope so every cron produces a run record; only the substantive steps - are skipped when the variable is false. - -## Audit trail - -- `/tmp/gh-cache-lead/schedule-runs-all.json`, `/tmp/gh-cache-lead/schedule-queued.json` - (REST, 2026-09-17). -- `repos/ContextualWisdomLab/.github/actions/workflows/opencode-review-coalesce-tick.yml/runs`. -- `docs/doctoring/actions-capacity-root-cause-20260917.md`, - `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`. diff --git a/docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md b/docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md deleted file mode 100644 index 0427c80675..0000000000 --- a/docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md +++ /dev/null @@ -1,82 +0,0 @@ -# Doctoring record: coalesce tick run 35219385415 queued 3h+ for an inert job (2026-09-17) - -- **Date:** 2026-09-17 -- **Subject:** Why `OpenCode Review Coalesce Tick` run `35219385415` stayed - `status=queued` for more than two hours on `ContextualWisdomLab/.github`, - whether a lighter job or concurrency change is warranted, and the repair. -- **Decision record:** none — diagnostic plus a workflow gate correction. -- **PR:** this commit's pull request. - -## Live evidence (gathered 2026-09-17) - -| Observation | Evidence | -|---|---| -| Stuck run | `35219385415`, event `schedule`, created `2026-09-17T12:07:50Z`, still `status=queued` / `conclusion=null` at `15:32Z` (~3h25m) before operator cancel | -| Head at enqueue | `130ce425f74c` (post-`#2235`; coalesce tick workflow already on `main` via `#2232`) | -| Workflow inventory | Only **2** runs total for workflow id `360129488`: pre-gate `35191169833` and the stuck `35219385415` | -| Pre-`#2232` contrast | `35191169833` at `06:44:23Z` → `completed`/`skipped` by `06:44:24Z` (1s; job-level gate era) | -| Coalesce flag | repo variable `OPENCODE_REVIEW_COALESCE_ENABLED=false` (updated `2026-09-17T04:21:48Z`) | -| Runner label | workflow `runs-on: ubuntu-24.04` (not floating `ubuntu-latest`) | -| Concurrency | group `opencode-review-coalesce-tick`, `cancel-in-progress: false` | -| Org ceiling context | Prior same-day census: ~48 org-wide `in_progress` vs ~1,911 `queued` (`docs/doctoring/actions-queue-24h-remeasurement-20260917.md`); plan concurrent-job ceiling ~60 (`docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`) | - -Operator cancel of `35219385415` at `15:36:29Z` reached `completed`/`cancelled` so the concurrency group no longer holds a forever-pending inert tick. - -## Root cause - -Not a missing runner label, not a hung step, and not a defect in the -org-wide GraphQL / scheduler loop (those steps never started). - -`#2232` moved `OPENCODE_REVIEW_COALESCE_ENABLED` from a **job-level** `if:` to -a **step-level** gate so that every five-minute cron would still produce a -visible run record while coalescing stayed off. That succeeded at producing -records, but it also forced GitHub to **admit the job into the shared runner -queue** even when the only work would be an inert `echo` and `exit 0`. - -Under the org's plan concurrent-job ceiling the inert job sits behind ~10³ -other queued runs. `cancel-in-progress: false` is correct for an in-flight -org-wide dispatch (do not cut mid-repository), and with at most one active + -one pending member it also explains why the five-minute cron did not -accumulate unbounded stacked run records while `35219385415` remained the -active waiter. - -The earlier claim that a job-level `if:` "suppressed every run record" -(`docs/doctoring/actions-schedule-run-records-20260917.md`) does not hold -against `35191169833`, which is a completed/`skipped` schedule run from the -job-level-gate era. Skipped jobs still create run records; they simply do not -wait for a runner. - -## What is / is not warranted - -| Lever | Verdict | -|---|---| -| Lighter job when flag is false | **Yes** — restore job-level `if:` so disabled ticks skip before runner admission | -| Change `cancel-in-progress` to `true` | **No** — would cancel an in-flight org-wide dispatch mid-repository; does not shorten admission wait for the active waiter | -| Different `runs-on` label | **No** — already pinned to `ubuntu-24.04`; hosted labels share the same plan ceiling | -| Plan-tier / more concurrent jobs | Owner-only; still the only way to make an *enabled* tick admit quickly under saturation | -| Scheduler fail-open (`#2233`) | Keep — when the flag is on and a real tick queues for hours, dispatch must not defer forever | - -When coalescing is later enabled, a real tick still competes for the same -ceiling; that is accepted. `recent_coalesce_tick_completed()` must treat only -`conclusion=success` as a healthy tick so a disabled-era `skipped` run cannot -be mistaken for proof that coalesce dispatch is alive after the flag flips on. - -## Repair - -1. Restore job-level `if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true'` on - `coalesce-tick` and drop the step-scoped inert/echo gate. -2. Require `conclusion == "success"` in `recent_coalesce_tick_completed()`. -3. Cancel the stuck inert run (`35219385415`) so it no longer occupies the - concurrency group (done live during this investigation). - -## Audit trail - -- `repos/ContextualWisdomLab/.github/actions/runs/35219385415` -- `repos/ContextualWisdomLab/.github/actions/runs/35191169833` -- `repos/ContextualWisdomLab/.github/actions/workflows/360129488/runs` -- `repos/ContextualWisdomLab/.github/actions/variables/OPENCODE_REVIEW_COALESCE_ENABLED` -- Prior related records: - `docs/doctoring/actions-schedule-run-records-20260917.md`, - `docs/doctoring/actions-queue-24h-remeasurement-20260917.md`, - `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`, - `docs/doctoring/actions-capacity-root-cause-20260917.md` diff --git a/docs/doctoring/coalesce-tick-post-2242-live-verify-20260917.md b/docs/doctoring/coalesce-tick-post-2242-live-verify-20260917.md deleted file mode 100644 index 9cd0f5d240..0000000000 --- a/docs/doctoring/coalesce-tick-post-2242-live-verify-20260917.md +++ /dev/null @@ -1,149 +0,0 @@ -# Doctoring record: post-#2242 coalesce tick live verify + re-enable criteria (2026-09-17) - -- **Date:** 2026-09-17 -- **Subject:** After `#2242` restored the job-level - `OPENCODE_REVIEW_COALESCE_ENABLED` gate, confirm the next schedule ticks - finish as `completed`/`skipped` (or later `success` when enabled) within - seconds — not multi-hour `queued` — and state when the flag may safely be - flipped back to `true`. -- **Decision record:** none — live verification plus recommended re-enable - criteria. Flipping the repo variable remains an explicit operator action. -- **PR:** this commit's pull request. - -## Preconditions verified on `main` - -| Check | Evidence | -|---|---| -| `#2242` merged | `3449d0020ffac86315ecccfb9d5a1dd3bf421834` at `2026-09-17T16:14:00Z` | -| Job-level gate restored | `origin/main:.github/workflows/opencode-review-coalesce-tick.yml` has `if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true'` on `coalesce-tick`; no step-scoped inert `echo`/`exit 0` gate | -| Flag still off | repo variable `OPENCODE_REVIEW_COALESCE_ENABLED=false` (unchanged since `2026-09-17T04:21:48Z`) | -| Stuck inert waiter cleared | run `35219385415` reached `completed`/`cancelled` at `15:36:29Z` (concurrency group no longer holds a forever-pending inert tick) | -| Prior healthy skip baseline | run `35191169833` (job-level-gate era) `06:44:23Z` → `completed`/`skipped` by `06:44:24Z` (1s) | - -Repair intent (from `#2242` / `docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md`): -disabled ticks must skip **before runner admission** so they do not compete for -the plan concurrent-job ceiling. - -## Live observation window (post-merge) - -Sampled via -`repos/ContextualWisdomLab/.github/actions/workflows/opencode-review-coalesce-tick.yml/runs` -(workflow id `360129488`). - -| Sample time (UTC) | `total_count` | Newest run | Status / conclusion | Notes | -|---|---|---|---|---| -| `16:36Z` (~22m after merge) | 2 | `35219385415` | completed / cancelled | No post-`#2242` schedule run yet | -| `16:43Z` | 2 | same | same | Still only the pre-merge pair | -| `16:48Z` (~34m after merge) | 2 | same | same | Still no `queued` and no `skipped` successor | -| `17:06Z` (~52m after merge) | 3 | **`35249460935`** | **completed / skipped** | First post-`#2242` delivery | - -### Reading - -1. **No multi-hour `queued` inert tick has reappeared after `#2242`.** That is the - failure mode `#2242` fixed. Under the step-scoped gate, the first post-`#2232` - schedule delivery created `35219385415` and left it `queued` for ~3.5h. After - the job-level gate returned, the next delivered tick never entered `queued`. -2. **Schedule delivery still lags the `*/5` cron under saturation.** The first - post-merge delivery arrived at `16:54:29Z` (~40m after merge), consistent with - earlier same-day gaps (`06:44Z` → `12:07Z`, ~5.5h) documented in - `docs/doctoring/actions-queue-24h-remeasurement-20260917.md`. Missed intervals - are not backfilled as a stack of five-minute runs. -3. **Positive confirmation landed.** Run `35249460935` matches `#2242`'s - acceptance check: flag still `false`, `conclusion=skipped`, wall time 1s, - `head_sha=3449d0020ffa` (the `#2242` merge). - -### First post-`#2242` tick - -| Field | Value | -|---|---| -| Run id | `35249460935` (run_number 3) | -| Created / updated | `2026-09-17T16:54:29Z` → `2026-09-17T16:54:30Z` | -| Conclusion | `skipped` | -| Elapsed | **1s** | -| Head SHA | `3449d0020ffa` (`#2242` merge) | -| URL | https://github.com/ContextualWisdomLab/.github/actions/runs/35249460935 | - -## Recommended `OPENCODE_REVIEW_COALESCE_ENABLED` re-enable criteria - -Do **not** flip the variable to `true` until all of the following hold. These are -operator criteria, not code changes. - -### Must-have (gate health) - -1. **Post-`#2242` disabled-tick proof.** At least **one** (preferably **two**) - schedule runs on workflow `360129488` with - `conclusion=skipped`, wall time ≤ ~10s, and `head_sha` containing the - job-level gate (`≥ 3449d0020`). **Met** by `35249460935` (1s skip on - `3449d0020`); a second skipped delivery remains preferred before flip but - is not blocking once capacity criteria (#4–#5) are accepted. -2. **Job-level gate still on `main`.** - `if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true'` remains on the - `coalesce-tick` **job**, not moved back to a step. Contract: - `tests/test_opencode_review_coalesce_tick.py`. -3. **Fail-open still present.** `recent_coalesce_tick_completed()` still requires - `conclusion == "success"` and the scheduler still fail-opens when no fresh - successful tick exists (`#2233`). Skipped/cancelled ticks must never count as - coalesce liveness. - -### Should-have (capacity / blast radius) - -4. **Org admission headroom or accepted fail-open.** A live census of - org-wide `in_progress` vs plan concurrent-job ceiling (~60; see - `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md`) and queued - depth (`docs/doctoring/actions-queue-24h-remeasurement-20260917.md`). - - Prefer enable when `in_progress` is clearly below ceiling and queued depth - is not on the order of 10³, **or** - - Explicitly accept that enabled ticks may still sit `queued` for hours and - that `#2233` fail-open will temporarily bypass coalesce deferral until a - `success` tick completes. Enabling under deep saturation without that - acceptance recreates "reviews never dispatch" rather than "inert ticks - clog the queue." -5. **Operator watch on the first enabled ticks.** After flipping the variable, - watch the next 2–3 schedule deliveries until each reaches - `conclusion=success` (or a documented fail-open dispatch path fires). Do not - walk away after only seeing `queued`. -6. **No concurrent experiment that reintroduces step-scoped observability.** - Run-record hunger must not override the admission-skip contract. - -### Explicit non-criteria - -- **Do not** treat schedule-delivery lag (hours between cron fires) as a reason - to widen the tick job or move the gate to steps again. -- **Do not** set `cancel-in-progress: true` to "fix" admission delay — that - cancels mid-org dispatch (`docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md`). -- **Do not** enable solely because `#2242` merged; merge proves the code path, - not live schedule behavior under today's queue. - -### Suggested flip procedure - -```text -1. Confirm ≥1 post-#2242 skipped tick (table above filled). -2. Re-sample org in_progress / queued; decide accept-fail-open vs wait-for-relief. -3. gh variable set OPENCODE_REVIEW_COALESCE_ENABLED --body true -R ContextualWisdomLab/.github -4. Watch next ticks for conclusion=success; confirm recent_coalesce_tick_completed path. -5. If ticks queue for hours, leave flag on only if fail-open is observed healthy; else set false again. -``` - -## Verdict (as of `17:06Z`) - -| Claim | Status | -|---|---| -| `#2242` on `main` with job-level skip-before-admission | **Confirmed** | -| No post-merge multi-hour inert `queued` tick | **Confirmed** | -| Next tick completes `skipped` in seconds | **Confirmed** — `35249460935` in 1s | -| Must-have #1 (post-`#2242` disabled-tick proof) | **Met** | -| Safe to set `OPENCODE_REVIEW_COALESCE_ENABLED=true` now | **Not yet** — still need should-have capacity/fail-open acceptance (#4–#5); prefer a second skipped tick if schedule delivers one before flipping | - -## Audit trail - -- `ContextualWisdomLab/.github#2242` merge `3449d0020` @ `2026-09-17T16:14:00Z` -- `repos/ContextualWisdomLab/.github/actions/workflows/360129488/runs` -- `repos/ContextualWisdomLab/.github/actions/runs/35249460935` (post-`#2242` skipped) -- `repos/ContextualWisdomLab/.github/actions/runs/35219385415` -- `repos/ContextualWisdomLab/.github/actions/runs/35191169833` -- `repos/ContextualWisdomLab/.github/actions/variables/OPENCODE_REVIEW_COALESCE_ENABLED` -- Prior: - `docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md`, - `docs/doctoring/actions-queue-24h-remeasurement-20260917.md`, - `docs/doctoring/actions-schedule-run-records-20260917.md`, - `docs/doctoring/actions-plan-concurrency-ceiling-20260903.md` diff --git a/docs/doctoring/codeql-ghas-configuration-identity-2133.md b/docs/doctoring/codeql-ghas-configuration-identity-2133.md deleted file mode 100644 index 699d163c8d..0000000000 --- a/docs/doctoring/codeql-ghas-configuration-identity-2133.md +++ /dev/null @@ -1,62 +0,0 @@ -# GHAS CodeQL configuration identity continuity (#2133) - -## Symptom - -Wardnet PR `ContextualWisdomLab/wardnet#129` at exact head -`2cedf7098723cd12f59125e72f4354226165a112` completed its central current-head -CodeQL dispatch successfully while the GitHub Advanced Security CodeQL -comparison check was still terminal **neutral** with: - -> Code scanning cannot determine the alerts introduced by this pull request, -> because 1 configuration present on `refs/heads/main` was not found. -> Missing: `Default setup /language:rust`. - -Dispatch completion alone is therefore incomplete differential-analysis -evidence. - -## Root cause - -Protected bases that use GitHub CodeQL Default setup publish identities of the -form: - -```text -(analysis_key=dynamic/github-code-scanning/codeql:analyze, category=/language:) -``` - -GHAS pairs each such base identity with the same tuple on the PR head. Default -setup often finishes a fast language (for example `actions`) minutes before a -slower one (for example `rust`). The GHAS comparison can settle after the first -language lands and report `configuration not found` for every base language not -yet present on the head — even though the slower analysis later appears under -the correct identity on the same exact SHA. - -The central `#2106` handler stack correctly keeps `github/codeql-action/analyze` -at `upload: false` while Default setup owns code-scanning uploads (Default setup -blocks advanced CodeQL API uploads). Advanced uploads also use a different -`analysis_key`, so they cannot satisfy a Default setup base identity. The -central producer therefore cannot "impersonate" Default setup; it must prove -continuity of the identities Default setup already publishes. - -## Repair - -`scripts/ci/codeql_ghas_configuration_identity.py` is the executable pairing -contract: - -- positive: exact base/head SHAs sharing Default setup `/language:` pair; -- negative: base Default setup rust with only actions on the head fails closed; -- advanced-setup analysis keys are not interchangeable with Default setup. - -`.github/workflows/codeql-scan-dispatch.yml` fetches that script beside the -SARIF gate and, after the Medium+ gate, waits (bounded poll) until the scanned -language's base identity is present on the exact head before publishing the -`codeql-dispatch/` status. Least privilege stays `security-events: -read` for this verification; no leaf Default setup disablement and no synthetic -GHAS status. - -## Ownership boundary - -Cross-repository CodeQL evidence identity remains owned by the organization -central producer/handler/settlement layer (`codeql-pr.yml` → -`codeql-scan-dispatch.yml`, coordinated with `#2106` / `#2040` / `#1929`). Do -not copy CodeQL workflows into consumer repositories or reinterpret a transient -neutral GHAS comparison as GREEN. diff --git a/docs/doctoring/codeql-pr-private-consumer-read-permissions.md b/docs/doctoring/codeql-pr-private-consumer-read-permissions.md deleted file mode 100644 index fc0cf62d7e..0000000000 --- a/docs/doctoring/codeql-pr-private-consumer-read-permissions.md +++ /dev/null @@ -1,26 +0,0 @@ -# CodeQL required workflow denies private consumers a read they need — 2026-09-13 - -## Symptom - -Private consumer `ContextualWisdomLab/late-life-anxiety-reanalysis` PR #10 (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700410434`) failed both org-required `.github/workflows/codeql-pr.yml` jobs at their first API call, each with `gh: Resource not accessible by integration (HTTP 403)`. `CodeQL compatibility analysis (python)` (job `103571590442`), step "Read current-head CodeQL dispatch verdict", calls `gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` under `GH_TOKEN: ${{ github.token }}`; the runner printed effective token permissions of Contents: read, Metadata: read only (declared: `contents: read`, `id-token: write`). `Dispatch current-head CodeQL scan` (job `103571810868`) makes the same GET, then later reads `repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses`, under `contents: read`, `id-token: write`, `actions: read`. Public consumers (fast-mlsirm, pg-erd-cloud, naruon, html4tree) pass the identical workflow only because GET on a *public* repository needs no fine-grained grant; the defect is specific to private repositories. - -## Root cause - -Neither job declared the fine-grained read permissions GitHub's REST contract requires for these calls on a private repository: "Get a pull request" needs `pull-requests: read`; "List commit statuses for a reference" needs `statuses: read`. Missing both, the minted `GITHUB_TOKEN` had no read access to pull-request or status data on a private repo, and testing against public consumers never exercised the gap because anonymous-equivalent GETs on public repository resources are always permitted. - -## Repair - -Added `pull-requests: read` and `statuses: read` to the `analyze-head` and `dispatch-current-head` job `permissions:` blocks in `.github/workflows/codeql-pr.yml`, preserving declaration order (contents, id-token, [actions], pull-requests, statuses). No write permission is added anywhere; `actions: write` remains absent, still guarded by the existing `test_codeql_required_workflow_does_not_gain_actions_write` regression test. - -## Local evidence - -New test `test_codeql_pr_jobs_hold_read_grants_private_consumers_need` in `tests/test_codeql_pr_workflow_contract.py` slices both permission blocks the same way the neighboring `actions: write` guard does and asserts each holds exactly `pull-requests: read` and `statuses: read` with no `actions: write`. RED: 1 failed (`assert [] == ['read']`). GREEN: 1 passed. Combined focused run across the five CodeQL/required-workflow contract test files: 149 passed. Full repository suite and `actionlint` result are recorded in the pull request description. - -## Hosted acceptance still required - -This repair is unverified against GitHub's live permission enforcement. A newly loaded central SHA carrying this change must still pass both `analyze-head` and `dispatch-current-head` on the private consumer's exact current head before the defect is resolved end-to-end. Separately, the later `repository_dispatch` POST from `dispatch-current-head` to `ContextualWisdomLab/.github` using the OpenCode app token has not yet been exercised from a private consumer at all, and may surface a distinct scoping issue of its own once this read-permission blocker is cleared. - -## References - -- GitHub REST, "Get a pull request": https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request (fine-grained permission: `pull-requests: read`) -- GitHub REST, "List commit statuses for a reference": https://docs.github.com/en/rest/commits/statuses#list-commit-statuses-for-a-reference (fine-grained permission: `statuses: read`) diff --git a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md deleted file mode 100644 index 47b4482c9a..0000000000 --- a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md +++ /dev/null @@ -1,96 +0,0 @@ -# CodeQL versioned handler bootstrap — 2026-09-12 - -## Status - -Proposed repair from protected `main@691fb78932eff5fbe52db69077848134b0b4e053`. -No merge or production claim is made here. The complete consumer successor is -PR #2040, revalidated at current head -`6476b919d3febf79cc53e71d6d60f15d7e83ced4`. - -## Exact live evidence - -PR #2040 predecessor head `a9b18b4b24980c7ceb8b8cc0d143a24db20c90bf` -had successful Runtime Quality run `34684155351` (3,127 passed, 1 skipped, -21 subtests; 100% statement, branch, and public-doc coverage), Security run -`34684356405`, SAST run `34684356377`, and Python Security run `34684356416`. -It had no unresolved review threads. The later current head `6476b919...` -moves replay-guard tests without changing this handler source, but historical -hosted results are not inherited. The current head is Draft and had no -associated pull-request workflow runs in the connector snapshot. It therefore -remains unmergeable through ordinary protection. - -Protected handler run `34684228601` is the smallest causal trace. Its Actions -and Python scan, SARIF gate, artifact preservation, and status paths reached -terminal completion. The Actions shard then woke the shared required run. The -Python shard's independent wake received HTTP 403 because that run was no -longer in the terminal-failed state. Same-repository/PR handler runs -`34684373526`, `34684458709`, `34684518320`, and `34684575249` were then -cancelled by the stable concurrency group while retries kept dispatching. In -`34684575249`, Python produced clean scan evidence while its sibling and wake -path did not converge. The repeated consumer symptom was a dispatched success -with a pending terminal verdict. - -## Root cause - -The protected handler woke the required run independently from each matrix -scan job. The first wake changed the run state before the second language -could validate and mutate it. In addition, a candidate stronger consumer -could not prove itself against protected `main`: the old handler lacked its -source-bound title and base-bound receipt, while replacing the handler in one -step would reject the still-protected legacy producer. Re-running either side -alone reproduces the dependency cycle. - -## Repair contract - -One existing handler accepts `codeql-scan` legacy v1 and `codeql-scan-v2`. -The event type is the explicit protocol version, avoiding an eleventh -top-level `client_payload` property. v1 retains the current title, payload, -and status context byte-for-byte at the boundary, while rejecting all v2-only -identity fields. v2 requires the exact source/base/head evidence implemented -by #2040. Both use the same scan implementation and one post-matrix settlement -writer. No scan shard has `actions:write`. - -The bridge removal condition is executable policy: remove v1 only after a -protected v2 producer is live, every in-flight v1 required run is terminal, -and a caller inventory finds zero `codeql-scan` producers. Until then, v1 is a -bounded compatibility port, not production authority for v2 consumers. - -## Verification and next action - -The bootstrap contract executes both payload shapes, rejects v2-to-v1 -downgrade fields, verifies one actions writer after the matrix, and preserves -the legacy and base-bound contexts separately. The full repository suite and -hosted exact-head checks must pass before ordinary merge. After bootstrap -merge, #2040 must non-force absorb protected main, change only its producer -event to `codeql-scan-v2`, and generate new end-to-end evidence; existing -failed or queued runs are not inherited. - -PR #2106 review then exposed a credential-fallback contamination edge case: -`gh api` may emit an HTTP error body to stdout before returning nonzero, so a -failed credential's JSON could precede the later credential's successful -response. A generic `{"message":"Forbidden"}` body was already discarded by -the current `jq` projections and therefore was not RED. The corrected RED -fixture emits `{"state":"closed"}`, a field the PR validator consumes: before -the repair it is concatenated with the authorized response and rejects that -valid fallback. `run_api` now captures each attempt and emits its body only -after that exact attempt succeeds, preserving stderr diagnostics and the -existing credential order without a temporary-file lifecycle. - -The overlapping predecessor PR #2105 retained two additional fail-closed -guards that the first #2106 tree did not carry. Nested rerun authority now -requires the exact string schema `"1"`; missing, numeric, and unknown schemas -are rejected before checkout or mutation. The single settlement writer also -validates the required run's positive integer `run_attempt` and stops before -mutation when it reaches 48. GitHub documents that `run_attempt` begins at 1 -and increments for every re-run, while one workflow run permits at most 50 -re-runs; the cutoff therefore preserves attempts 49–51 for human recovery -rather than consuming the native allowance automatically. The structured -failure records the run, attempt, schema, languages, and handler identity. -This integrates the valid #2105 delta into the backward-compatible legacy/v2 -bridge rather than choosing either incomplete branch unchanged. - -GitHub. (2026). *Re-running workflows and jobs*. -https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs - -GitHub. (2026). *Variables reference*. -https://docs.github.com/en/actions/reference/workflows-and-actions/variables diff --git a/docs/doctoring/github-api-published-lineage-authority.md b/docs/doctoring/github-api-published-lineage-authority.md deleted file mode 100644 index 5f6a363848..0000000000 --- a/docs/doctoring/github-api-published-lineage-authority.md +++ /dev/null @@ -1,28 +0,0 @@ -# GitHub API evidence published-lineage authority - -Status: Proposed repair evidence for `.github` PR #2279. Hosted exact-head security and independent review remain mandatory. - -## Finding - -The first published-lineage contract checked that the documentation named intended replacement SHAs and omitted two known unreachable candidates. That established expected spelling but not repository reachability. A 40-hex identifier can satisfy those assertions while referring to no commit published in the repository, so the contract did not make G-17's evidence lineage independently reconstructable. - -Current-head review identified that gap and required the G-17 evidence identifiers themselves to resolve and belong to the current published branch ancestry. - -## RED → repair - -- Structural RED `c37db5405142da1d0fa2ae972cbacab28563c370` factors a G-17 evidence validator and adds a mutation control that substitutes the first evidence commit with the all-zero, commit-shaped identifier. The intentionally shape-only validator accepts that mutation, so the regression fails instead of giving false assurance. -- Minimal repair `b339370ed1e032527e504ca3500a2f0ca825ff77` keeps validation in the existing GitHub API authority contract. For every full SHA named in the single G-17 row it now requires both `git cat-file -e ^{commit}` and `git merge-base --is-ancestor HEAD` to succeed. The negative mutation therefore fails closed, while the documented published evidence must be resolvable in current history. - -The repair does not change either production HTTP client, credential handling, redirect policy, workflow threshold, or the standalone `$RUNNER_TEMP` CodeQL materialization boundary. It strengthens only executable evidence traceability. - -## Invariants - -1. G-17 has exactly one gap-register row. -2. Every full commit SHA named by that row resolves as a commit in the checked-out repository. -3. Every such evidence commit is an ancestor of the exact checked-out head; detached or unreachable object-store artifacts are not accepted as published lineage. -4. A syntactically valid but unreachable 40-hex identifier fails the contract. -5. Exact-head hosted CI/security gates and independent review remain distinct from this focused local invariant. - -## Rejected alternatives - -Checking only SHA syntax was rejected because it proves formatting rather than publication. Checking only that expected strings occur in Markdown was rejected because unreachable objects can still be named. GitHub API lookups were unnecessary for the repository-local invariant and would add network/credential authority to a test whose evidence is already in Git history. diff --git a/docs/doctoring/github-api-url-authority-2248.md b/docs/doctoring/github-api-url-authority-2248.md deleted file mode 100644 index 01db8f1f17..0000000000 --- a/docs/doctoring/github-api-url-authority-2248.md +++ /dev/null @@ -1,73 +0,0 @@ -# GitHub REST URL authority boundary for central CI clients - -Status: Proposed repair for `.github` issue #2248; exact-head hosted security and independent review remain mandatory. - -## Problem - -Protected `.github/main` at `64aa08d7fa487deacd41c761c36277ca68cab6c9` contains two central CI HTTP clients: - -- `scripts/ci/codeql_ghas_configuration_identity.py` for CodeQL analyses; -- `scripts/ci/strix_evidence_binding.py` for pull-request changed-file evidence. - -The whole-tree Semgrep gate reported `python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected` at both original dynamic `urlopen` sites, and Bandit B310 reported the same class. A comment-only suppression would not prove the security premise that bearer-authenticated requests stay inside GitHub REST authority. - -The first repair made the initial URL predicate executable, but exact-head CodeRabbit review then identified a second authority transition: Python's default `HTTPRedirectHandler` can construct a redirected request from the already-authorized request and preserve request headers, including `Authorization`. Validating only the first `https://api.github.com/...` URL therefore did not prevent a 3xx response from redirecting the bearer token to another authority. - -## Initial URL RED → repair - -Structural RED `4732f3e29ab8cd0b88506beecd4e70bdfaafb8da` requires both clients to reject, before network/file opener execution: - -- `http://api.github.com/...`; -- `https://api.github.com.evil.example/...`; -- `https://api.github.com@evil.example/...`; -- `https://api.github.com:443/...` because the canonical authority is exact; -- an otherwise canonical URL carrying a fragment; -- `file:///etc/passwd`. - -The production predicate requires scheme exactly `https`, network authority exactly `api.github.com`, an absolute path, and no fragment. The positive control proves exact `https://api.github.com/...` reaches the injected opener and decodes JSON normally. - -A temporary shared helper candidate was removed because `codeql-scan-dispatch.yml` materializes `codeql_ghas_configuration_identity.py` into `$RUNNER_TEMP` and executes it as a standalone file. The CodeQL helper therefore keeps its small fail-closed transport boundary self-contained instead of gaining a repository-local import dependency that the workflow does not materialize. - -## Redirect RED → repair - -CodeRabbit's current-head review of `9ba43f284da51bfa6aaa389d3fb67f8b232fbba5` correctly rejected the initial-only guard: default `urllib` redirect handling can create a new request after the first authority check and carry the bearer header to the new target. - -Structural redirect RED `7a00442cbfd01408068a060c2bebba84041a33eb` adds hostile redirect targets for a lookalike HTTPS host, `http://api.github.com/...`, and `file:///...`. The contract requires both clients' redirect handlers to return no redirected request while the original request retains its bearer header; the repair also blocks same-authority redirects so there is no unreviewed second authority transition at all. - -Production repair lineage: - -- `a2e9126416c96bb8c5fa1e00190a8eca45758883` replaces CodeQL's default `urlopen` transport with a local `OpenerDirector` whose `_RejectRedirects` handler refuses every redirect; -- `4c7bcbeb06e421b98b0992b62cac06eaae45a98c` applies the same fail-closed boundary to the Strix evidence client; -- `e06b6dd84b012db9c3fafc09d417a85f4aaeff4c` adds direct-handler hostile cases, canonical opener positive controls, and same-authority redirects to the refusal contract; -- `57477289ebec5631b0c48f0bc419f336dbe19deb` closes the remaining executable-binding gap: both actual module-level production openers receive a synthetic 302 through their real HTTPS open/response chains, and the regression proves transport sees exactly the original canonical request plus bearer and never receives a redirected request. - -The redirect repair removes the two dynamic `urlopen` sinks rather than broadening a Semgrep/Bandit suppression. A 3xx response now terminates as the opener's HTTP error path; no second request object is created and the bearer credential cannot be forwarded by redirect machinery. The executable proof patches only the actual opener's bounded HTTPS transport slot for a synthetic response; it does not replace `open()`, call the redirect handler directly as its oracle, or contact a network endpoint. - -## Production opener-chain RED → evidence repair - -Current-head review found that the direct `_RejectRedirects.redirect_request(...)` unit cases would remain green if either production `_GITHUB_API_OPENER` were accidentally rebuilt with Python's default redirect handler. Commit `57477289ebec5631b0c48f0bc419f336dbe19deb` therefore drives each public client path through its actual module-level opener. A synthetic HTTPS transport returns `302 Location: https://api.github.com/repos/ContextualWisdomLab/redirected`; the contract requires the client-specific HTTP error and exactly one transport call containing the original bearer header. - -Mutation RED temporarily replaced both `build_opener(_RejectRedirects())` constructions with `build_opener()`. Both new tests failed on the forbidden second request and recorded `Authorization='Bearer test-token'` at that redirect target. Restoring the production constructors made the complete authority file GREEN (`31 passed`, including malformed-authority parse failures for both clients and all four redirect target classes). This binds the executable claim to the production handler chain without adding network I/O, sharing runtime helpers, or changing the standalone CodeQL module. - -The broader focused run then exposed four pre-existing Strix fixtures still patching the removed module-level `urlopen` symbol: HTTP error, URL error, malformed JSON, and success. Their RED result was `2 failed, 77 passed` because monkeypatch setup stopped before those cases reached production. They now patch `binding._GITHUB_API_OPENER.open`, matching the real call path; the three-file CodeQL/Strix/authority suite passes in both normal and `GITHUB_ACTIONS=true` modes (`87 passed` each), with 100% statement and branch coverage across the two affected production modules. - -A clean worktree at predecessor `25f83aaee9eb97e423f6ef2467e722035bc2e362` reproduced those two Strix failures in the full suite (`2 failed, 3354 passed, 28 skipped, 40 subtests`) and the repository-wide pre-existing 98% coverage gate (`262` missed statements). The repair removes the two causal suite failures and all misses in the two affected production modules; it does not claim to close unrelated coverage debt in `actions_queue_health*`, Rust materialization, Noema document handling, or scheduler code. - -## Alternatives rejected - -Broad Semgrep/Bandit suppression, path exclusion, or threshold weakening were rejected because they hide unrelated findings. Revalidating only the final response URL was rejected because the unauthorized network contact would already have occurred. Preserving redirects while stripping only `Authorization` was rejected because the client would still contact a target outside the stated GitHub REST authority. A custom redirect-following policy was unnecessary for these CI reads; blocking redirects entirely is the smaller authority surface. - -## Evidence and acceptance - -Primary scanner rule inspected at [semgrep/semgrep-rules revision `40b8c63f75dc7c22c8a77482d73bfb864b146f7e`](https://github.com/semgrep/semgrep-rules/commit/40b8c63f75dc7c22c8a77482d73bfb864b146f7e): `python/lang/security/audit/dynamic-urllib-use-detected.yaml`. Python stdlib `HTTPRedirectHandler` behavior was inspected during review because redirect construction is the second network-authority decision that the original source predicate did not control. - -Acceptance requires all of the following on the exact PR head: - -1. `tests/test_github_api_url_boundary.py` passes initial hostile-authority, direct-handler redirect-refusal, actual-production-opener synthetic-302, and canonical positive-control cases for both clients; -2. existing CodeQL GHAS identity and Strix evidence-binding suites remain green; -3. Semgrep and Python/Bandit no longer report the #2248 baseline findings and introduce no replacement Medium+ finding; -4. no security rule, path, threshold, or required check is weakened; -5. independent current-head review confirms redirects cannot create a second request carrying the bearer token; -6. the standalone `$RUNNER_TEMP` CodeQL materialization contract remains intact. - -Hosted exact-head evidence is mandatory. Source inspection, structural RED/repair lineage, and review comments are not substitutes for repository/security GREEN. diff --git a/docs/doctoring/opencode-vcs-python-source-root.md b/docs/doctoring/opencode-vcs-python-source-root.md deleted file mode 100644 index 1ad7cc95b2..0000000000 --- a/docs/doctoring/opencode-vcs-python-source-root.md +++ /dev/null @@ -1,37 +0,0 @@ -# OpenCode immutable VCS `python/` source-root RCA - -Status: **Source repaired on protected `main` via #2123 (`ebc69a401`); image-path resolver extracted and contract-proven under #2157 follow-up.** Hosted consumer `coverage-evidence` past docker step #17 remains the issue-closure gate when a post-merge run is linked. - -## Incident and user-visible failure - -On 2026-09-12 UTC, central OpenCode dispatch [run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) validated `ContextualWisdomLab/contextual-orchestrator#1149` at exact head `684cf28fa59e800c0db4886a08f25dd2edd156fc`. Its `coverage-source-tree` job succeeded, but `coverage-evidence` job `103574547257` failed while building the trusted tool image, before any pull-request test or coverage command ran. OpenCode therefore published only a non-approving COMMENTED review, and the required receipt remained fail-closed. - -The failing dependency was the exact VCS pin `fast-mlsirm@09f762ded35786dd1078222a4577ff09d649816f` from the consumer's validated `pyproject.toml`. That commit contains `python/fast_mlsirm/__init__.py`; it does not expose the import package at repository root or under `src/`. - -## Root cause and boundary - -`opencode-review-dispatch.yml` enumerated only four trusted candidates: `src/`, `src/.py`, ``, and `.py`. The materializer had already authenticated the target repository, bound the dependency to an immutable commit, fetched that commit without tags, and verified `FETCH_HEAD` and `HEAD`; the failure was solely an incomplete source-layout contract in the central owner. - -#2123 added only `python/` and `python/.py`, then mapped a match to the repository's `python/` directory. It preserved the invariant that exactly one candidate may exist and continued to reject symlinked/namespace imports, any symlink layout, compiled extensions, installed distribution metadata, and ambiguous roots. It did not infer arbitrary paths from untrusted packaging metadata and did not execute dependency lifecycle code. - -The #2157 follow-up extracts that same admission logic into -`scripts/ci/resolve_opencode_base_vcs_import_root.sh`, which the coverage Dockerfile -`COPY`s and executes. Offline fixtures in -`tests/test_opencode_vcs_python_source_root_contract.py` prove the `python/` layout -succeeds, `src/` and root layouts still succeed, and missing/ambiguous/namespace/compiled -trees still fail closed — so the image-path algorithm no longer depends solely on an -untested HEREDOC. - -Rejected alternatives were: changing the consumer's valid immutable dependency pin; copying `fast-mlsirm` into the consumer; adding the whole repository to `PYTHONPATH`; recursively searching for a matching directory; or weakening/bypassing the OpenCode coverage gate. Each would move ownership, admit ambiguity, or hide the central defect. - -## RED → repair → verification gate - -- RED commit `b1fe97c477b56e148afbeeaed9a6b74338994b6b` requires both package and single-module `python/` candidates in the published workflow contract. -- Repair commit `af04581cea4ffc038c881c6ad101ea3e5842a664` adds those candidates and the corresponding `python_root` mapping. -- Hosted Runtime Quality [job `103581110552`](https://github.com/ContextualWisdomLab/.github/actions/runs/34704176931/job/103581110552) then failed the independent pairing contract because the changed workflow blob `f315683208d57ba89a2942502c525abe7355e2fd` no longer matched the reviewed predecessor pin. Commit `683cb053b3c6f1c7b3f293a74263ac9b13e9bdf1` advances only that exact pin; no hash check is removed or relaxed. -- #2123 merged to protected `main` as `ebc69a401` (2026-09-13). -- #2157 follow-up moves the resolver into `scripts/ci/resolve_opencode_base_vcs_import_root.sh` with executable fixtures and re-pins `REVIEW_DISPATCH_BLOB_SHA`. - -## Follow-up - -Rerun only consumer failures whose cause changed, beginning with `contextual-orchestrator` PRs that previously died at step #17. Verify that the trusted image builds from the same `fast-mlsirm` commit, the PR sandbox remains networkless and credential-free, coverage/docstring evidence executes, and a substantive exact-head review is published. Link that `coverage-evidence` job on #2157 before closing the issue. If any additional conventional source root is needed, add it through its own immutable fixture and one-root regression rather than generalized path discovery. diff --git a/docs/doctoring/pingora-hwpx-evidence-admission.md b/docs/doctoring/pingora-hwpx-evidence-admission.md deleted file mode 100644 index a8a0f9e98d..0000000000 --- a/docs/doctoring/pingora-hwpx-evidence-admission.md +++ /dev/null @@ -1,40 +0,0 @@ -# Pingora edge policy admits HWPX evidence documents (#2116) - -`scripts/ci/pingora_edge_policy.py` rejected the consumer's HWPX evidence -attachment before a merge verdict. `BINARY_DOCUMENT_MAGIC` only knew -`.pdf`/`.png`, and `_is_binary_documentation_asset` only admitted a -`doc`/`docs`/`documentation` directory, so the ZIP-based `.hwpx` under -`evidence/` matched neither rule and fell through to the strict UTF-8 -decode every other candidate gets. - -Source baseline: `fb17ef556f94f673234aa557254ae52779e9a7b0`. Consumer -evidence: ContextualWisdomLab/late-life-anxiety-reanalysis#10, head -`a1cd5bc6783c6510dfcf937f523c733366e82213`, run `34700409497`, job -`103571044859`. Reported failure: "Pingora edge policy could not establish -complete evidence: Runtime policy candidate -evidence/reviewer_response_draft.hwpx is not valid UTF-8". - -The repair adds `.hwpx` (`PK\x03\x04`) to `BINARY_DOCUMENT_MAGIC` and lets -`_is_binary_documentation_asset` also admit an `.hwpx` under an `evidence` -path segment, gated on a bounded HWPX container check: unprefixed ZIP, -exact EOCD record, unique members with `mimetype` first, a stored (not -deflated) `mimetype` entry exactly `application/hwp+zip`, and a non-empty, -unencrypted `Contents/content.hpf` manifest. Format evidence only -- no -document rendering or malware inspection. The runtime-path guard and the -Nginx-runtime-text fallback scan for disguised/malformed archives are -retained unchanged. - -Test evidence (offline, this branch): RED (test-only apply) 3 failing / 19 -passing in `tests/test_pingora_hwpx_evidence.py`; GREEN (full patch) 90 -passing across that file plus `tests/test_pingora_edge_policy.py` and -`tests/test_pingora_edge_workflow_contract.py`. Branch coverage of the -touched module: 100% (388 statements, 174 branches, 0 missed). -`interrogate scripts/ci -q`: 100.0% docstrings. Full suite passes, no new skips or warnings. - -Hosted acceptance still requires a newly loaded central source SHA to -re-run the consumer's exact head bootstrap; local tests prove the declared -classification and container logic, not a hosted admission outcome. - -## Reference - -Hancom. (n.d.). *한/글 문서 파일 형식: HWPX 포맷 구조 살펴보기*. https://tech.hancom.com/hwpxformat/ diff --git a/docs/doctoring/scheduler-status-read-permission.md b/docs/doctoring/scheduler-status-read-permission.md deleted file mode 100644 index 82e7b0cbf3..0000000000 --- a/docs/doctoring/scheduler-status-read-permission.md +++ /dev/null @@ -1,38 +0,0 @@ -# Scheduler commit-status read permission (#2120) - -The organization-required scheduler selected `github.token` for same-repository -reads but omitted `statuses` from `scan-pr-queue.permissions`. The private -consumer's combined-status GET consequently failed with HTTP 403 before a merge -verdict. `checks: read` does not grant classic commit-status access. - -Source baseline: `fb17ef556f94f673234aa557254ae52779e9a7b0`. -Consumer evidence: ContextualWisdomLab/late-life-anxiety-reanalysis#10, -head `3d1e3ae56e3ef6ca0a995b6082c4f4a13629e0f6`, run `34698738407`, -job `103566634488` (2026-09-12). The reported failing endpoint is -`GET /repos/{repository}/commits/{head}/status`. - -The repair adds only `statuses: read` to the existing scan job. Workflow defaults, -mutation credentials, cross-repository credential selection and fail-closed API -errors remain intact. It adds no status publication or App installation grant. -The existing credential-contract test now requires exactly `read` in that job's -permission block; its RED revision is `9521b6771`. - -Validation uses the existing pytest workflow/credential/status suites and -Actionlint's workflow validation. Local tests prove the declared contract, not a -hosted permission grant. After protected integration, validate a newly loaded -central source SHA and the consumer's exact current head: the combined-status -request must succeed, and missing checks or substantive failures must still block -merge. For reusable callers, every caller permission ceiling must also admit -status reads; the inspected consumer PR head has no `.github` tree, so do not -invent a repository-local caller or modify App permissions to compensate. - -Next integration review: 2026-09-13, because this prevents the current private -consumer's mandatory scheduler from evaluating status evidence. #2116's HWPX -classification remains a separate bootstrap repair. Reverting this one-line grant -restores the pre-existing 403 behavior; it is not a viable consumer repair. - -## Reference - -GitHub. (n.d.). *REST API endpoints for commit statuses: Get the combined status -for a specific reference*. Retrieved September 12, 2026, from -https://docs.github.com/en/rest/commits/statuses#get-the-combined-status-for-a-specific-reference diff --git a/docs/doctoring/strix-evidence-binding-2159-2168.md b/docs/doctoring/strix-evidence-binding-2159-2168.md deleted file mode 100644 index 2e6151a8ce..0000000000 --- a/docs/doctoring/strix-evidence-binding-2159-2168.md +++ /dev/null @@ -1,50 +0,0 @@ -# Strix evidence binding for PR-delta and remediation claims - -Status: accepted 2026-09-17 - -## Incidents - -### #2159 — PR-delta vs repository baseline - -Required Strix review on `.github#2106` reported source findings against -`scripts/ci/pingora_edge_policy.py` and -`scripts/ci/contextual_orchestrator_review_policy.py` even though both blobs -were base-identical across the authenticated PR tuple. The PR review lane -treated those observations as if they were introduced by the PR. - -### #2168 — false "already applied" remediation - -LineageWeave Strix run `34746057545` completed SUCCESS with a valid Medium -finding, but the report claimed the fix was "already applied" and -"syntax-verified" after `apply_patch` failed with -`WorkspaceReadNotFoundError` / `ApplyPatchFileNotFoundError` against -`/workspace/backend/app/main.py` instead of the materialized scan workspace. - -## Decision - -1. `scripts/ci/strix_evidence_binding.py` classifies each finding against an - authenticated changed-file inventory (including renames and hunk lines) as - `pr_delta`, `repository_baseline`, `context_dependency`, or `unmapped`. -2. The Strix gate labels blocking PR intersections as `evidence_scope=pr_delta` - and unchanged-path continuations as `evidence_scope=repository_baseline`. -3. After each attempt, the gate sanitizes report artifacts through the binder - so an `apply_patch` miss cannot remain summarized as "already applied". -4. Remediation states distinguish `finding_confirmed`, `fix_proposed`, - `fix_applied_in_scan_workspace`, `fix_validated`, - `fix_committed_to_source`, and `remediation_failed`. A fix is never marked - applied without workspace-byte proof or an exact source commit receipt. - -## Evidence and rollback - -Contract tests in `tests/test_strix_evidence_binding.py` cover changed-source, -base-identical, context-dependency, rename, stacked-base, stale-head, and -apply_patch-miss RED fixtures. Gate wiring is pinned by -`assert_strix_evidence_binding_contract` in -`scripts/ci/test_strix_quick_gate.sh`. Roll back only with an equivalent -fail-closed evidence binder; do not restore false PR-delta attribution or -false remediation claims. - -## References - -- ContextualWisdomLab/.github#2159 -- ContextualWisdomLab/.github#2168 diff --git a/docs/doctoring/zdr-feed-model-id-route-keys.md b/docs/doctoring/zdr-feed-model-id-route-keys.md deleted file mode 100644 index f219a97c56..0000000000 --- a/docs/doctoring/zdr-feed-model-id-route-keys.md +++ /dev/null @@ -1,25 +0,0 @@ -# OpenRouter ZDR feed route keys used the wrong field — 2026-09-13 - -## Symptom - -`noema-review` and `strix` both failed closed on `ContextualWisdomLab/late-life-anxiety-reanalysis#10` (head `a1cd5bc6783c6510dfcf937f523c733366e82213`, runs `34700409452`/job `103571267389` and `34700409446`/job `103571829483`) against central `fb17ef556f94f673234aa557254ae52779e9a7b0`, both exiting with `PolicyError: no attested ZDR model route is available with the ZDR policy; orchestrator/free would fail closed`. Every private/internal consumer runs `--require-zdr` (ADR-0003), so this is a hard boot failure, not a degraded catalog. - -## Root cause (feed schema) - -`_load_zdr_endpoints` (`scripts/ci/contextual_orchestrator_review_policy.py`) built route keys from `endpoint.get("model_name")`. On the real `https://openrouter.ai/api/v1/endpoints/zdr` feed (see OpenRouter's ZDR docs, https://openrouter.ai/docs/guides/features/zdr), `model_name` is a human display string (e.g. "DeepSeek: DeepSeek V4.1 Flash") while `model_id` is the slug contextual-orchestrator discovery reports as `model` (e.g. `inclusionai/ling-3.0-flash-vl:free`). No live-feed key ever matched `is_zdr_model(...)`, so the catalog was always empty under `--require-zdr`. The three fixtures in `tests/test_contextual_orchestrator_review_policy.py` put slugs into `model_name`, which is why this was invisible to tests since the keying was introduced in 17052a7ca (#1360, 2026-08-27). - -## Repair - -`_load_zdr_endpoints` now keys on `endpoint.get("model_id")`; there is no fallback to the display name, and rows missing `model_id` or `provider_name` are still skipped. The three fixtures were corrected to carry the real feed schema (`model_id` slug + a display-string `model_name`). `is_zdr_model` and `zdr_policy.py` are unchanged. - -## Offline reproduction (before / after) - -Discovery: a 60-row consumer snapshot (20 each openrouter/nvidia_nim/nvidia_nim_sub free rows). Feed: the live 859-entry `/api/v1/endpoints/zdr` response fetched 2026-09-13, carrying three matching `inclusionai/ling-3.0-flash-*:free` openrouter routes served by `Novita`. - -- Before: `--require-zdr --pool free` exits 1 with the `PolicyError` above. -- After: exits 0, `zdr_selected_count: 3`, selecting exactly `openrouter/inclusionai/ling-3.0-flash-{vl,sante,fin}:free`, all `zdr: true`. -- Without `--require-zdr`: `zdr_selected_count: 3` and those three routes rank first in the 12-route free catalog. - -## Hosted acceptance still required - -This is an offline fix against a static discovery/feed snapshot. It does not prove a newly loaded central SHA boots the sidecar on the private consumer's exact head, and it does not change how the gateway itself requests ZDR routing from OpenRouter — that remains a separate contextual-orchestrator (CO)-side check. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index e7fd78c563..619374a13d 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -63,59 +63,6 @@ This is a bounded binary-evidence classifier, not a general image renderer; visual fidelity and optional ancillary-chunk semantics are outside this gate. Other binary files remain unavailable evidence and fail closed. -## Declared research/data artifact paths - -The scanner's binary exemption is otherwise shaped by path only (`doc`/ -`docs`/`documentation`, plus the `evidence`/`figures` publication -directories). A research repository whose raw data and fitted-model -artefacts live elsewhere by deliberate, owner-approved design -- SPSS -`.sav` files, serialized model objects, compressed numeric arrays -- can -opt in without relocating that data under `docs/`. - -Add `.github/edge-policy-artifact-paths.txt` at the repository root: one -explicit relative path prefix per non-blank line, no globs or wildcards. -For example: - -``` -local -evidence/raw -``` - -**Security property.** `evaluate_pull_request` resolves this file only -from the pull request's *base ref* -- never its head. A pull request that -adds or widens the declaration is not self-authorizing: it gets no benefit -from that change until the change itself is reviewed and merged into the -base branch. This mirrors how the required workflow already treats every -other piece of policy evidence -- current-head content only, no -pull-request-controlled trust. - -**What the declaration replaces, and what it does not.** A file under a -declared prefix is admitted on exactly the same evidence documentation -paths already require: `_runtime_path_rule` matches (`Dockerfile`, -`nginx.conf`, service files, and the like) are rejected inside a declared -prefix exactly as inside `docs/` today, and any file that decodes as valid -UTF-8 is still fully content-scanned, never silently admitted. A file whose -suffix has a known magic byte (`.hwpx`, `.pdf`, `.png`) is verified by that -format's structural evidence; a file with no known magic entry (most -research-data formats) is admitted only on the stricter combination of "no -diff patch" and "the fetched bytes are not valid UTF-8" -- a text file can -never be mistaken for a binary artefact merely by sitting under a declared -prefix. - -**Bounds.** The declaration is capped at 64 entries and 8 path segments of -depth per entry (`MAX_DECLARED_ARTIFACT_PREFIXES` / -`MAX_DECLARED_ARTIFACT_PREFIX_DEPTH` in `scripts/ci/pingora_edge_policy.py`) --- parsing-safety bounds, not a product limit on how many locations a -repository may declare. An absolute path, a `..` traversal component, a -bare `.`/`/`, or a glob character in any entry is a hard `PolicyError` -naming the offending entry; a repository with no declaration file behaves -identically to before this feature existed. When a declared prefix admits a -file, the required workflow logs a `::notice::` naming the prefix and the -base ref the declaration was read from, so a reviewer can trace the -admission back to the reviewed declaration it relied on. - -Refs #2193, #2149, #2116. - ## Exception process There is no standing Nginx exception. A temporary exception requires a public ADR diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c617e3ad73..1cc9e20313 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,12 +7,6 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. -### 2026-09-13 current-head incident delta - -| Gap ID | 상태 | exact-head evidence | causal owner / next gate | -|---|---|---|---| -| CONTROL-OPENCODE-VCS-PYROOT-01 | **Source repaired on `main` (#2123 `ebc69a401`); image-path helper extracted + offline-proven under #2157 follow-up; hosted consumer step-#17 link still required to close the issue** | `ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `ContextualWisdomLab/fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유했다. #2123이 `python/` candidates를 추가해 `main`에 병합했고, #2157 follow-up은 동일 로직을 `scripts/ci/resolve_opencode_base_vcs_import_root.sh`로 추출해 `tests/test_opencode_vcs_python_source_root_contract.py` fixture로 증명한다. Issue #2157 종료는 post-`ebc69a401` consumer `coverage-evidence`가 docker step #17을 통과한 job id를 문서에 링크한 뒤에만 한다. | - ## 1. 근거와 범위 ### 1.1 우선순위가 높은 근거 @@ -100,7 +94,6 @@ flowchart LR | G-14 | release/changelog/version 증거가 각 PR에 분산되고 현재 central repo 보호 main의 release candidate가 명확하지 않다 | 운영자는 어떤 기능이 supportable release인지 확인할 수 없다 | merge 후 release readiness ledger, CHANGELOG, semantic version/tag, rollback/operability evidence를 함께 갱신한다 | | G-15 | 첨부파일 처리 경계가 제품별로 다르고, 1MB 상한은 업무 데이터와 맞지 않으며 미지원 MIME/컨테이너가 parser registry에서 명시적으로 pending/quarantine 되는지 확인되지 않았다. 현재 20MB 초과 파일 가능성과 PDF/HWP/HWPX·이미지·압축파일의 parse/sidecar 흐름을 하나의 exact contract로 묶지 못했다 | 큰 업무 첨부를 거부하거나 파싱 실패를 조용히 잃으면 고객의 메일·문서 업무가 중단된다 | naruon/newsdom-api 소유 PR에서 streaming upload, configurable bounded limit above 20MB, MIME sniffing, parser capability registry, quarantine/retry, source-position provenance, and ADR를 추가하고 size/unsupported-type/zip-bomb tests를 required evidence로 만든다 | | G-16 | Required Pingora policy treated a changed documentation PNG screenshot as UTF-8 runtime evidence | Valid UI evidence blocked otherwise valid product PRs before policy evaluation | This branch verifies bounded PNG magic before exemption while runtime paths and malformed assets continue to fail closed; protected-main delivery remains the release gate | -| G-17 | `.github#2279` blocked authenticated GitHub REST redirects in source, but redirect tests invoked `_RejectRedirects` directly and four Strix transport fixtures still patched the removed `urlopen` seam | A future opener-composition regression could forward a bearer token on a 3xx while redirect tests stayed green; Strix error mapping could fail before exercising production | Proposed `57477289ebec5631b0c48f0bc419f336dbe19deb` sends all four synthetic redirect classes through both real module-level openers; `663ffac390d27ab21daa58b91b624d3f00dce7de` moves every Strix fixture to the production opener; `9c19c6e00eafc028068719ab482282c1256f8893` adds malformed-authority coverage and records the owner evidence. Mutation RED proves the default opener contacts a second same-authority URL with the bearer header. The focused suite passes twice (`87 passed` normal and `GITHUB_ACTIONS=true`) with 100% statement/branch coverage on both affected modules. Exact-head hosted security and independent review remain required | ## 4. 열린 PR live inventory @@ -2785,41 +2778,6 @@ prose" convention already stated in `CLAUDE.md`. ## Item 41: CodeQL PR `startup_failure` blocking merges org-wide — dispatch-safe re-admission in progress -**2026-09-12 control-plane update — handler-first bootstrap Proposed.** -Protected `main@691fb78932eff5fbe52db69077848134b0b4e053` still runs the -legacy handler while complete successor #2040 is open at -`6476b919d3febf79cc53e71d6d60f15d7e83ced4` (Draft at the latest live -revalidation). Exact predecessor run `34684228601` -proved the current per-language wake cannot converge: Actions woke the shared -required run, then Python received HTTP 403; subsequent same-tuple handler -runs were cancelled and redispatched, including `34684575249`. This is a -canonical `.github` control-plane defect, not a consumer CodeQL finding. - -The minimum repair is one versioned handler, not a workflow copy. Temporary -`codeql-scan` v1 preserves the protected client title/payload/status contract; -`codeql-scan-v2` requires the source/base/head/SARIF evidence carried by -#2040. Both share one repository/PR concurrency identity and a single -post-matrix `actions:write` settlement. The scan matrix is read-only. v1 is -removed only after the protected v2 producer lands, all v1 attempts terminate, -and caller inventory reaches zero. Current status remains **Proposed**: -bootstrap PR ordinary merge, #2040 non-force restack, and a fresh successful -exact-head required CodeQL run are still required. ADR-0025 and -`docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md` carry the -decision and exact evidence. Settlement credential fallback releases only the -successful `gh api` body; its RED fixture uses a rejected -`{"state":"closed"}` document because a generic error message does not exercise -the consumed-field contamination path. - -The first overlapping successors were each incomplete in a different way: -#2105 required v2-only producer provenance from the still-protected legacy -client, while #2106 initially omitted #2105's nested-rerun schema and -attempt-exhaustion guards. The canonical #2106 integration preserves its -legacy/v2 event bridge and carries forward both valid #2105 guards: only string -schema `"1"` grants nested rerun authority, and the settlement writer stops -before mutation at required-run attempt 48. Status remains **Proposed** until -the integrated exact head passes hosted checks and independent review, lands -on protected `main`, and a fresh #2040 producer canary converges. - **2026-09-04 correction.** The emergency ruleset removal below fixed the old entrypoint, but became stale after `.github#1778` moved `github/codeql-action` into the native `codeql-scan-dispatch.yml` handler. Seven current PR heads then @@ -3279,8 +3237,8 @@ intended contract before rewriting the assertion — left for a dedicated follow ## Items 15/16/17 measurement: `Detect changed scope` gate jobs — 2 of 3 are pure runner overhead — 2026-09-05 -**Status:** Measured 2026-09-05; `sast-semgrep.yml` fixed 2026-09-13 (below); `strix.yml` deferred. Recorded so -the fix is grounded in real numbers rather than the intuition this measurement partly refuted. +**Status:** Measured, not yet fixed. Recorded so the fix is grounded in real numbers rather than the intuition +this measurement partly refuted. **Why measured.** Items 15/16/17 ask to remove needlessly-triggered workflows, consolidate workflow files ("bootup에도 시간이 듦"), and cut redundant steps; the standing complaint is the org's 60-concurrent-job @@ -3395,32 +3353,3 @@ queries the check-runs API at its own time, order-independently. The implementin their change was safe because they had scoped it narrowly, not because they had checked for the name collision — which is the more useful lesson: **a job name is unique only within one workflow file, and the same name in another file can carry the opposite safety property.** - -**Fixed for `sast-semgrep.yml`, 2026-09-13.** The standalone `changed-scope` job is gone; its -"Classify changed paths" step now runs inside the single consumer `semgrep` (after `harden-runner`, -which must audit the classifier's own `gh api` egress) and the four expensive steps plus the final -"Enforce Semgrep gate" step carry `steps.scope.outputs.code == 'true'`. The job keeps -`if: github.event.action != 'closed'` with no `needs.` term, so a doc-only PR's run still executes one -job that concludes `success` -- the load-bearing property from -[`required-workflow-path-filter-boundary.md`](doctoring/required-workflow-path-filter-boundary.md) is -preserved, and neither `Detect changed scope` nor `Semgrep (multi-language SAST)` is among `.github`'s -classic required contexts, so nothing goes Pending there. One trap the first draft would have shipped: -the enforce step's `always() && (... || steps.semgrep.outputs.rc != '0')` evaluates `rc` as the empty -string when `Run Semgrep` is step-skipped, which is `!= '0'` and would have failed every doc-only PR; -the guard on that step is what makes the fold safe. Net: one runner allocation per PR for this -workflow instead of two, org-wide. `strix.yml` (the other single-consumer gate) is deliberately left -alone -- it is a documented multi-PR hot-file collision zone. Contract: -`tests/test_docs_only_pr_runner_admission.py::test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level`, -`tests/test_required_security_runner_image_contract.py`. - -## 2026-09-19 GitHub API production-opener redirect proof - -**Status:** Proposed on `ContextualWisdomLab/.github#2279`; exact-head hosted checks and qualifying independent review remain mandatory. - -**Context Map / owner.** The central `.github` CI bounded context owns the bearer-authenticated CodeQL-analysis and Strix changed-file GitHub REST clients. GitHub remains the upstream REST authority. Product repositories consume only the released central workflow contract; they do not copy either client. - -**Gap.** Initial URL admission and direct `_RejectRedirects.redirect_request()` unit cases did not prove that each module-level production `OpenerDirector` actually retained the no-redirect handler chain. A future opener reconstruction could silently re-enable authenticated redirects while the prior tests stayed green. - -**Action.** Exact `57477289ebec5631b0c48f0bc419f336dbe19deb` adds a dependency-free synthetic-302 transport to `tests/test_github_api_url_boundary.py`. For both actual production openers, the case drives a canonical bearer request through the real HTTPS open/response chain, requires the typed HTTP-302 failure mapping, and proves transport receives exactly one original request; lookalike HTTPS, HTTP, `file:`, and same-authority redirect targets never receive a second request or bearer. Exact `e0b0b4d4fff5b6ea88236a1e91dcd7dbb3be09b5` repairs the doctoring claim so direct-handler coverage is not mislabeled as production-chain proof. - -**Evidence / remaining condition.** The standalone fixture mechanism was executed locally against Python stdlib and produced one canonical request followed by terminal HTTP 302 for every hostile target. This is mechanism evidence, not repository acceptance. Final authority requires focused/full exact-tree GREEN, fresh exact-head Security/SAST/Python Security/CodeQL/runtime-quality checks, no unresolved actionable review, ordinary protected-main integration, and downstream consumer validation. No scanner suppression, redirect allowlist widening, provider fallback, workflow gate weakening, or credential-boundary change is included. diff --git a/requirements-noema-document-ci-hashes.txt b/requirements-noema-document-ci-hashes.txt deleted file mode 100644 index 0fd5dd54d7..0000000000 --- a/requirements-noema-document-ci-hashes.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Generated with uv pip compile --generate-hashes --python-version 3.12 -# requirements-noema-document-ci.txt -defusedxml==0.7.1 \ - --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ - --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 diff --git a/requirements-noema-document-ci.txt b/requirements-noema-document-ci.txt deleted file mode 100644 index 09dd20d248..0000000000 --- a/requirements-noema-document-ci.txt +++ /dev/null @@ -1 +0,0 @@ -defusedxml==0.7.1 diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 116009874b..d8aaca3ad8 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -4,9 +4,9 @@ attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via interrogate -click==8.5.0 \ - --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ - --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via interrogate colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ @@ -137,88 +137,69 @@ coverage==7.15.4 \ # via # -r requirements-opencode-review-ci.txt # pytest-cov -hypothesis==6.168.0 \ - --hash=sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669 \ - --hash=sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9 \ - --hash=sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a \ - --hash=sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4 \ - --hash=sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc \ - --hash=sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76 \ - --hash=sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1 \ - --hash=sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d \ - --hash=sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099 \ - --hash=sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f \ - --hash=sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29 \ - --hash=sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c \ - --hash=sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01 \ - --hash=sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377 \ - --hash=sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7 \ - --hash=sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f \ - --hash=sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c \ - --hash=sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0 \ - --hash=sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d \ - --hash=sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517 \ - --hash=sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131 \ - --hash=sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507 \ - --hash=sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09 \ - --hash=sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9 \ - --hash=sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2 \ - --hash=sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67 \ - --hash=sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a \ - --hash=sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3 \ - --hash=sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063 \ - --hash=sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab \ - --hash=sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4 \ - --hash=sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b \ - --hash=sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6 \ - --hash=sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda \ - --hash=sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc \ - --hash=sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795 \ - --hash=sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475 \ - --hash=sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2 \ - --hash=sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e \ - --hash=sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f \ - --hash=sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4 \ - --hash=sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128 \ - --hash=sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7 \ - --hash=sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28 \ - --hash=sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236 \ - --hash=sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782 \ - --hash=sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8 \ - --hash=sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79 \ - --hash=sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e \ - --hash=sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780 \ - --hash=sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20 \ - --hash=sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27 \ - --hash=sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349 \ - --hash=sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef \ - --hash=sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9 \ - --hash=sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8 \ - --hash=sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93 \ - --hash=sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63 \ - --hash=sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8 \ - --hash=sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b \ - --hash=sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04 \ - --hash=sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887 \ - --hash=sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c \ - --hash=sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149 \ - --hash=sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0 \ - --hash=sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5 \ - --hash=sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1 \ - --hash=sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f \ - --hash=sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd \ - --hash=sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193 \ - --hash=sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7 \ - --hash=sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc \ - --hash=sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc \ - --hash=sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571 \ - --hash=sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d \ - --hash=sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213 \ - --hash=sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68 \ - --hash=sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441 \ - --hash=sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad \ - --hash=sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d \ - --hash=sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843 +hypothesis==6.163.0 \ + --hash=sha256:002a9709345892279fb0e81b5a05b72d08cfe81f937339827be0d588607ca9b0 \ + --hash=sha256:00d3091b28de83c5116e0ccd9a4bcb28ef61d2aace5df91093bb22434fd2350c \ + --hash=sha256:0a0c396244c13805edcb73ff467c4c8178ccefc41c4ef5ed00a68e612fd773e9 \ + --hash=sha256:0a933aca9ebf9daf951d07cf01200c94c321b6ee0b42cc7b67675c9686d914c2 \ + --hash=sha256:0cba5202f74e7e4cdb676d86f26e8cc1b4fdc88f7f58ba73c8ac45b6b22f3070 \ + --hash=sha256:213527755f0fc2b1f3721e73fd60023e2752a48f914e3e2df8d35111956ae5c8 \ + --hash=sha256:21e72e8d5818e5ef8cd6a2191c386e3fd1a6d9e3739cf97289b4d9b5dbc8e38d \ + --hash=sha256:2849c23b2e0fe2eef4c1ec336b01eac7ad7397c49fca43c264f59ec1e6046eac \ + --hash=sha256:28a6cc1c25a6cc9b6ec079eaabd32ac769994831ecddd57123ce43c9056dcf34 \ + --hash=sha256:31dc46c48aa53c3ec92d03120978ca7f19b9cf96d195ed3fc93503f1433c94a6 \ + --hash=sha256:320b076bf6436f971f1c73ee651e60001226d1b4e341f2c4a1ca87248261ca03 \ + --hash=sha256:331906cb029b6b360b8ebac3ec00c3cfa720037fe2efb294a503a1979c9a9a8f \ + --hash=sha256:34fc895691a2420595506eb17f3a104f2fa9039f013c0770a6cc2743ccaf6fed \ + --hash=sha256:3b6cee2afe6c67b31a4a64b63a876e0b020befdc61daabea80f7a0e14f19203a \ + --hash=sha256:3f3cceb4720a39127622fbf3bcebe1775b894372c53b5edddfdef10bbdeef9ec \ + --hash=sha256:40dfab6fe6a02a80abef81aebf88e53cd529e3f2f6ba3486b674a67b1f4a3512 \ + --hash=sha256:4159a1c2560e10de51b1c14956e277eb1b37526c9abef9e87c1e531760486448 \ + --hash=sha256:487ab8ec2f01a225d6a1e2ceadc5290cde2c691952bd2e7f76199cf82e06fb25 \ + --hash=sha256:4ab0dadc09c537d4ac57e564039dfe7daf09c98375306d54bfc0fd6c218efcca \ + --hash=sha256:50073f8e63c1e7d3403899755657a990d8bba7b5b5bff66b1c56796d4969bb28 \ + --hash=sha256:520480d4bd3a17557616c25923640953e360332c89d012fffcebd69857e674a9 \ + --hash=sha256:52f16840add2eb02c2416f3b83cec4f527b6c19699f2d31eff4859233c715526 \ + --hash=sha256:56ed585baab75cb98462c57ca88bbdc6a9d935a14118dd572fb476c3ecec2a06 \ + --hash=sha256:58be45d1737bf8c2e10cf29505c0f10f8a23d61bc82e4339182a6c8251cbc2d9 \ + --hash=sha256:59f5fdb8addb44c17520a60d50542d9db6ceba577bbf54efefa9c10ee20be140 \ + --hash=sha256:5a3ac6c62d49f7fe518dfe7fa924fa03aac839993702207802b0e45f9e1b0dab \ + --hash=sha256:67d1593941ede41052b4a35ec25b50d0e280358c7674ef7812d520010e7e8bdf \ + --hash=sha256:6ae63dec6d1d467b7f4737455f81a7a82f14a41c14510937fcfbc726a085b5f8 \ + --hash=sha256:7a3db868a943c814cc557104712d43bf609adfe5ea9f708f38377d366b4855f8 \ + --hash=sha256:7ca7b20bf38d51e15f7808b0239791c4792b1709ce0c63093acaff56a09c31e6 \ + --hash=sha256:7cb3d927360fe73f9a06d646e6082237142ee39c24679c7133d22bf06dd03b45 \ + --hash=sha256:7ef8954e37c80e0c46e6161eef1c72c71059b95250e620a77bd646f6c7a52a2d \ + --hash=sha256:8aac96db8a6c7ee43aba2ee0d3c43893da1fb7c38ed54790c1be2b6d8fd87b96 \ + --hash=sha256:8c5d1e6bad47edf6fb1d7406cf6d67314ac08325c63a49550d782a4596ea302b \ + --hash=sha256:9105c66ea8dbc108adc42058bb7b65bd953f53ee178bf63bf9ebb0cded6c8c96 \ + --hash=sha256:9be37b7ddf0af9e3f9112cd133afc34e78a56da1f96db5f2b4fc289fe1c4d1c3 \ + --hash=sha256:9c084749c115ea7918cf7efa144682783da17eec70d1276689182b871126e715 \ + --hash=sha256:9d23f0f3a14bb6e6f99c793d340196dba4af95ba25bfcab624d1794f540f5e27 \ + --hash=sha256:a16ebce774755a7a652bd44c62101dc914372ed1a98935969624848c9627b4a4 \ + --hash=sha256:a2a20e9835d3c4b293a709ee6ef769bcb18c6ed4ef337a9e251c1a9496d5e8be \ + --hash=sha256:a57352efa938889ea9992667a5014c0fc870d03945de71918574d1cf28276378 \ + --hash=sha256:ab34c61d9249f1a8129cb4276062c04e3e47b5be8de6446e7c7fe11362d6fe43 \ + --hash=sha256:b123b4995a7612f1130e2b2362c9a5d0568df887bf7e7bdb45c23af8cd5423c9 \ + --hash=sha256:b268211e625cd550e361fc387bf1db5deb1e9cae0ce4041116f0a0aafeef7c06 \ + --hash=sha256:b2ddcdaf6691101e06dc4a5add7b8c8fdf1e68daba599255a281f3f3550d3331 \ + --hash=sha256:b4ad2134405d5345434c22dea96bbc12c85abcfc3c253a8063dbc9ff01164555 \ + --hash=sha256:b839dfd1342bb50570cb0c66b80322307cdb468abf14faf5df4dab022bc1b9ce \ + --hash=sha256:b8f22fb8218ba6a452bf9000fc656e1ed57625d17cc8a3871a0fcea3b1b69ebf \ + --hash=sha256:bd312b15044b1c1a0920a5827a830559b2d1fa380851cedf509f8b835309c5b9 \ + --hash=sha256:c0ec3b709508ccd835d8ded1db025b7800618f2289a22a6bfd4927da5f4eb33c \ + --hash=sha256:c4f5be1482189c7b0a1dcac269fffe97a7d18cc04ac9a9a4d6613212dd87f38b \ + --hash=sha256:ca1b48bde68c528a79dec2a2859e05035802e5b1c9c3579f388c9de6ed6d0148 \ + --hash=sha256:d0838a28e9943d5b834ebae59b02adda76e2cd1e65caa808104c72102052057d \ + --hash=sha256:e165f6cc2075059b7c95dac1612bfb25494f72d90f56880e84c288b089f8a896 \ + --hash=sha256:e568a3d766b7ba8df00e0c33efc4c6530cde14fbc72daabe4824eed211ed7596 \ + --hash=sha256:ee47c2cb1be03a052ebd3549dad07f636a98b3ccfd7acbe5e17b3b7da0ab9e37 \ + --hash=sha256:f1fe222f50a1898e87a1e7323ab35f9e956278efabe4dd55a1342808206d05ad \ + --hash=sha256:f28ad27193c1fbcfb52ef2ee63d2b721563525089e80962b4268b306dac45507 \ + --hash=sha256:f2f1b67a48da86d3e41c9445367b49a49f7efdb60fc8b5e3593f05e6afb2efbe \ + --hash=sha256:f7f706df6839dcc53f20833f2933cbcd126fd2fdee7c312e053de49df4b64e44 \ + --hash=sha256:fae7305ae20fddeea09df317b920c45d3e20bfedbdb041f4db6ca5267c458189 \ + --hash=sha256:ffdda3006a383a48f71a23b4f2b3fae3fe1b09af67925d885985f7ec34d66bcb # via -r requirements-opencode-review-ci.txt iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ @@ -228,25 +209,9 @@ interrogate==1.7.0 \ --hash=sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 # via -r requirements-opencode-review-ci.txt -maturin==1.15.0 \ - --hash=sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136 \ - --hash=sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d \ - --hash=sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15 \ - --hash=sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1 \ - --hash=sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e \ - --hash=sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82 \ - --hash=sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1 \ - --hash=sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55 \ - --hash=sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a \ - --hash=sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176 \ - --hash=sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd \ - --hash=sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48 \ - --hash=sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7 \ - --hash=sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca - # via -r requirements-opencode-review-ci.txt -packaging==26.3 \ - --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ - --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via pytest pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ @@ -258,9 +223,9 @@ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 # via interrogate -pygments==2.21.0 \ - --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ - --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via pytest pytest==9.1.1 \ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index bf2112ed68..1e9a42f6a0 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -4,12 +4,6 @@ coverage==7.15.4 # collection. Matches the >=6.100 floor used by consumer repos (e.g. contextual-orchestrator). hypothesis>=6.100 interrogate==1.7.0 -# maturin (MIT/Apache-2.0, permissive) builds the PyO3 extension module for -# maturin/PyO3 projects (e.g. fast-mlsirm) inside the offline coverage sandbox, -# so `python3 -m coverage run -m pytest` can import the compiled `_core` -# module instead of failing collection with `ImportError: cannot import name -# '_core'`. See fast-mlsirm#1907. -maturin==1.15.0 pytest==9.1.1 pytest-cov==7.1.0 uv==0.12.7 diff --git a/scripts/ci/actions_queue_health.py b/scripts/ci/actions_queue_health.py deleted file mode 100644 index bb73698551..0000000000 --- a/scripts/ci/actions_queue_health.py +++ /dev/null @@ -1,592 +0,0 @@ -#!/usr/bin/env python3 -"""Queue-health CLI with stable identity and audit-provenance guarantees. - -The shared collector implementation lives in ``actions_queue_health_core.py``. -This entrypoint owns the consistency boundary that binds active-run evidence to -a stable pull-request view, carries stable workflow identity, and exports the -exact timestamp used for queue-age calculations. -""" - -from __future__ import annotations - -from datetime import datetime, timezone -import importlib.util -from pathlib import Path -import sys -from urllib.parse import quote - -_CORE_MODULE_PATH = Path(__file__).with_name("actions_queue_health_core.py") -_CORE_MODULE_SPEC = importlib.util.spec_from_file_location( - "actions_queue_health_core", _CORE_MODULE_PATH -) -if _CORE_MODULE_SPEC is None or _CORE_MODULE_SPEC.loader is None: # pragma: no cover - raise RuntimeError("unable to load queue-health core module") -_core_module = importlib.util.module_from_spec(_CORE_MODULE_SPEC) -sys.modules.setdefault("actions_queue_health_core", _core_module) -_CORE_MODULE_SPEC.loader.exec_module(_core_module) - -for core_symbol_name, core_symbol in vars(_core_module).items(): - if not core_symbol_name.startswith("__"): - globals()[core_symbol_name] = core_symbol - -_CORE_NORMALISE_RUN = _core_module._normalise_run -_CORE_BUILD_REPORT = _core_module.build_report -TERMINAL_DIAGNOSTIC_STATUSES = ("startup_failure", "cancelled", "failure") -TARGET_TERMINAL_DIAGNOSTIC_STATUSES = ("cancelled",) -TERMINAL_DIAGNOSTIC_MAX_API_PAGES = MAX_API_PAGES - - -def _normalise_run( - repository_name: str, - workflow_run: dict[str, Any], - workflow_jobs: list[dict[str, Any]], -) -> dict[str, Any]: - """Normalize one run while preserving stable GitHub workflow identity.""" - if not isinstance(workflow_run, dict): - raise QueueHealthError("workflow run must be an object") - workflow_id = workflow_run.get("workflow_id") - if workflow_id is not None and ( - isinstance(workflow_id, bool) - or not isinstance(workflow_id, int) - or workflow_id <= 0 - ): - raise QueueHealthError("workflow id must be a positive integer") - - normalized_run = _CORE_NORMALISE_RUN( - repository_name, workflow_run, workflow_jobs - ) - workflow_name = normalized_run["workflow_name"] - normalized_run["workflow_id"] = workflow_id - normalized_run["workflow_identity"] = ( - f"workflow_id:{workflow_id}" - if workflow_id is not None - else f"workflow_name:{workflow_name}" - ) - return normalized_run - - -_core_module._normalise_run = _normalise_run - - -def _read_pull_request_snapshot( - pulls_endpoint: str, *, runner: Runner -) -> list[dict[str, Any]]: - """Read and normalize one bounded open-pull-request identity snapshot.""" - pull_request_entries = _list_payload( - github_json(pulls_endpoint, paginate=True, runner=runner), - "pulls", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - return sorted( - (_normalise_pull_request(pull_request) for pull_request in pull_request_entries), - key=lambda pull_request: pull_request["number"], - ) - - -def _pull_request_identity_view( - pull_requests: list[dict[str, Any]], -) -> dict[int, tuple[str, str]]: - """Return the number/state/head view that must stay stable during collection.""" - return { - pull_request["number"]: ( - str(pull_request.get("state") or ""), - str(pull_request.get("head_sha") or ""), - ) - for pull_request in pull_requests - } - - -def collect_snapshot( - repositories: Sequence[str], - *, - runner: Runner = subprocess.run, - generated_at: str | None = None, -) -> dict[str, Any]: - """Collect active and pre-job terminal evidence bound to stable PR identities.""" - validated_repositories = sorted( - {_repository_name(repository_name) for repository_name in repositories} - ) - if len(validated_repositories) != len(repositories): - raise QueueHealthError("collection repository list contains duplicates") - - snapshot_timestamp = generated_at or datetime.now(timezone.utc).isoformat().replace( - "+00:00", "Z" - ) - parse_timestamp(snapshot_timestamp) - collected_repositories: list[dict[str, Any]] = [] - collection_errors: list[dict[str, str]] = [] - active_statuses = ("in_progress", "pending", "queued", "requested", "waiting") - - for repository_name in validated_repositories: - try: - repository_metadata = github_json( - f"repos/{repository_name}", runner=runner - ) - if not isinstance(repository_metadata, dict): - raise QueueHealthError( - f"repository metadata for {repository_name} is not an object" - ) - pulls_endpoint = ( - f"repos/{repository_name}/pulls?state=open&per_page={MAX_API_PAGE_SIZE}" - ) - try: - initial_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except IncompletePullRequestIdentity: - time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS) - initial_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except QueueHealthError as collection_error: - collection_errors.append( - {"repository": repository_name, "error": str(collection_error)} - ) - continue - - try: - active_snapshots: list[dict[int, dict[str, Any]]] = [] - for status_order in (active_statuses, tuple(reversed(active_statuses))): - active_snapshot: dict[int, dict[str, Any]] = {} - for workflow_status in status_order: - workflow_runs = _list_payload( - github_json( - f"repos/{repository_name}/actions/runs?status={workflow_status}" - f"&per_page={WORKFLOW_RUN_PAGE_SIZE}", - paginate=True, - max_pages=ACTIVE_RUN_MAX_API_PAGES, - runner=runner, - ), - "workflow_runs", - max_items=( - WORKFLOW_RUN_PAGE_SIZE * ACTIVE_RUN_MAX_API_PAGES - ), - ) - for workflow_run in workflow_runs: - workflow_run_id = workflow_run.get("id") - if ( - isinstance(workflow_run_id, bool) - or not isinstance(workflow_run_id, int) - or workflow_run_id <= 0 - ): - raise QueueHealthError( - "workflow run id must be a positive integer" - ) - active_snapshot[workflow_run_id] = workflow_run - active_snapshots.append(active_snapshot) - - first_snapshot, second_snapshot = active_snapshots - first_run_states = { - workflow_run_id: str(workflow_run.get("status") or "").upper() - for workflow_run_id, workflow_run in first_snapshot.items() - } - second_run_states = { - workflow_run_id: str(workflow_run.get("status") or "").upper() - for workflow_run_id, workflow_run in second_snapshot.items() - } - if first_run_states != second_run_states: - raise QueueHealthError( - "active workflow run snapshot changed during collection" - ) - - try: - final_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except IncompletePullRequestIdentity: - time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS) - try: - final_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except QueueHealthError as retry_error: - raise QueueHealthError( - "pull-request identity validation failed: " - f"{retry_error}" - ) from retry_error - - if ( - _pull_request_identity_view(initial_pull_requests) - != _pull_request_identity_view(final_pull_requests) - ): - raise QueueHealthError( - "pull-request identity snapshot changed during collection" - ) - - pull_requests_by_number = { - pull_request["number"]: pull_request - for pull_request in final_pull_requests - } - terminal_diagnostic_snapshot: dict[int, dict[str, Any]] = {} - current_head_shas = sorted( - {pull_request["head_sha"] for pull_request in final_pull_requests} - ) - for current_head_sha in current_head_shas: - encoded_head_sha = quote(current_head_sha, safe="") - workflow_runs = _list_payload( - github_json( - f"repos/{repository_name}/actions/runs?status=completed" - f"&head_sha={encoded_head_sha}" - f"&per_page={WORKFLOW_RUN_PAGE_SIZE}", - paginate=True, - max_pages=TERMINAL_DIAGNOSTIC_MAX_API_PAGES, - runner=runner, - ), - "workflow_runs", - max_items=( - WORKFLOW_RUN_PAGE_SIZE * TERMINAL_DIAGNOSTIC_MAX_API_PAGES - ), - ) - for workflow_run in workflow_runs: - if str(workflow_run.get("conclusion") or "").lower() not in ( - TERMINAL_DIAGNOSTIC_STATUSES - ): - continue - workflow_run_id = workflow_run.get("id") - if ( - isinstance(workflow_run_id, bool) - or not isinstance(workflow_run_id, int) - or workflow_run_id <= 0 - ): - raise QueueHealthError( - "workflow run id must be a positive integer" - ) - terminal_diagnostic_snapshot[workflow_run_id] = workflow_run - - for terminal_status in TARGET_TERMINAL_DIAGNOSTIC_STATUSES: - target_workflow_runs = _list_payload( - github_json( - f"repos/{repository_name}/actions/runs?status={terminal_status}" - "&event=pull_request_target" - f"&per_page={WORKFLOW_RUN_PAGE_SIZE}", - paginate=True, - max_pages=TERMINAL_DIAGNOSTIC_MAX_API_PAGES, - runner=runner, - ), - "workflow_runs", - max_items=( - WORKFLOW_RUN_PAGE_SIZE * TERMINAL_DIAGNOSTIC_MAX_API_PAGES - ), - ) - for workflow_run in target_workflow_runs: - normalized_candidate = _normalise_run( - repository_name, workflow_run, [] - ) - identity_state, _ = _run_identity( - normalized_candidate, pull_requests_by_number - ) - if identity_state != "current_head": - continue - terminal_diagnostic_snapshot[normalized_candidate["id"]] = ( - workflow_run - ) - - observed_snapshot = dict(second_snapshot) - observed_snapshot.update(terminal_diagnostic_snapshot) - runs_by_id: dict[int, dict[str, Any]] = {} - for workflow_run_id, workflow_run in observed_snapshot.items(): - normalized_run = _normalise_run( - repository_name, workflow_run, [] - ) - identity_state, _ = _run_identity( - normalized_run, pull_requests_by_number - ) - needs_job_evidence = ( - identity_state == "current_head" - and ( - normalized_run["status"] - in {"QUEUED", "IN_PROGRESS", "WAITING"} - or normalized_run["conclusion"] - in {status.upper() for status in TERMINAL_DIAGNOSTIC_STATUSES} - ) - ) - if not needs_job_evidence: - runs_by_id[workflow_run_id] = normalized_run - continue - - jobs_payload = github_json( - f"repos/{repository_name}/actions/runs/{workflow_run_id}/jobs" - f"?per_page={MAX_API_PAGE_SIZE}", - paginate=True, - runner=runner, - ) - workflow_jobs = _list_payload( - jobs_payload, - "jobs", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - runs_by_id[workflow_run_id] = _normalise_run( - repository_name, workflow_run, workflow_jobs - ) - - try: - post_evidence_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except IncompletePullRequestIdentity: - time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS) - try: - post_evidence_pull_requests = _read_pull_request_snapshot( - pulls_endpoint, runner=runner - ) - except QueueHealthError as retry_error: - raise QueueHealthError( - "pull-request identity validation failed: " - f"{retry_error}" - ) from retry_error - if ( - _pull_request_identity_view(final_pull_requests) - != _pull_request_identity_view(post_evidence_pull_requests) - ): - raise QueueHealthError( - "pull-request identity snapshot changed during evidence collection" - ) - except QueueHealthError as collection_error: - collection_errors.append( - {"repository": repository_name, "error": str(collection_error)} - ) - continue - - collected_repositories.append( - { - "full_name": repository_name, - "default_branch": str( - repository_metadata.get("default_branch") or "" - ), - "pull_requests": final_pull_requests, - "runs": sorted( - runs_by_id.values(), key=lambda workflow_run: workflow_run["id"] - ), - } - ) - - return { - "generated_at": snapshot_timestamp, - "repositories": collected_repositories, - "collection_errors": collection_errors, - } - - -def _normalized_snapshot_runs( - snapshot: dict[str, Any], -) -> dict[tuple[str, int], dict[str, Any]]: - """Index normalized run metadata for additive report provenance fields.""" - normalized_runs: dict[tuple[str, int], dict[str, Any]] = {} - snapshot_repositories = snapshot.get("repositories") - if not isinstance(snapshot_repositories, list): - return normalized_runs - for repository_entry in snapshot_repositories: - if not isinstance(repository_entry, dict): - continue - repository_name = repository_entry.get("full_name") - workflow_runs = repository_entry.get("runs") or [] - if not isinstance(repository_name, str) or not isinstance(workflow_runs, list): - continue - for workflow_run in workflow_runs: - if not isinstance(workflow_run, dict): - continue - workflow_jobs = workflow_run.get("jobs") or [] - if not isinstance(workflow_jobs, list): - workflow_jobs = [] - normalized_run = _normalise_run( - repository_name, workflow_run, workflow_jobs - ) - normalized_runs[(repository_name, normalized_run["id"])] = normalized_run - return normalized_runs - - -def build_report( - snapshot: dict[str, Any], - *, - now: datetime | None = None, - queue_age_slo_seconds: int = DEFAULT_QUEUE_AGE_SLO_SECONDS, -) -> dict[str, Any]: - """Build the v1 report with stable workflow identity and age provenance.""" - normalized_runs = _normalized_snapshot_runs(snapshot) - report = _CORE_BUILD_REPORT( - snapshot, - now=now, - queue_age_slo_seconds=queue_age_slo_seconds, - ) - - for report_row in report["runs"]: - run_metadata = normalized_runs.get( - (report_row["repository"], report_row["run_id"]) - ) - if run_metadata is None: # pragma: no cover - core report guarantees the row. - continue - report_row["workflow_id"] = run_metadata["workflow_id"] - report_row["workflow_identity"] = run_metadata["workflow_identity"] - report_row["run_conclusion"] = run_metadata.get("conclusion", "") - report_row["jobs_materialized"] = bool(run_metadata["jobs"]) - matching_job = next( - ( - workflow_job - for workflow_job in run_metadata["jobs"] - if workflow_job["id"] == report_row["job_id"] - ), - None, - ) - report_row["admission_state"] = ( - "runner_assigned" if report_row["runner_assigned"] else "runner_not_assigned" - ) - if matching_job and matching_job.get("created_at"): - report_row["queue_age_started_at"] = matching_job["created_at"] - report_row["queue_age_source"] = "job_created_at" - else: - report_row["queue_age_started_at"] = run_metadata.get("created_at", "") - report_row["queue_age_source"] = "run_created_at" - if ( - report_row["identity_state"] == "current_head" - and report_row["run_conclusion"] == "STARTUP_FAILURE" - and not report_row["jobs_materialized"] - ): - report_row["admission_state"] = "startup_failure_before_job_materialization" - report_row["blocker"] = "startup_failure_before_job_materialization" - report_row["recommended_action"] = ( - "inspect_actions_control_plane_without_leaf_bypass" - ) - elif ( - report_row["identity_state"] == "current_head" - and report_row["run_conclusion"] == "CANCELLED" - and matching_job is not None - and matching_job.get("conclusion") == "CANCELLED" - and not report_row["runner_assigned"] - and matching_job.get("steps_count") == 0 - ): - report_row["admission_state"] = "cancelled_before_runner_assignment" - report_row["blocker"] = "cancelled_before_runner_assignment" - report_row["recommended_action"] = ( - "inspect_actions_control_plane_without_leaf_bypass" - ) - elif ( - report_row["identity_state"] == "current_head" - and report_row["run_conclusion"] == "FAILURE" - and matching_job is not None - and matching_job.get("conclusion") == "FAILURE" - and not report_row["runner_assigned"] - and matching_job.get("steps_count") == 0 - ): - report_row["execution_state"] = "terminal_pre_execution_failure" - report_row["admission_state"] = "terminal_pre_execution_failure" - report_row["blocker"] = ( - "terminal_pre_execution_failure_before_runner_assignment" - ) - report_row["recommended_action"] = ( - "inspect_actions_control_plane_without_leaf_bypass" - ) - - current_pending_rows = [ - report_row - for report_row in report["runs"] - if report_row["is_pending"] - and report_row["identity_state"] == "current_head" - ] - lane_run_ids: dict[tuple[str, int, str], set[int]] = {} - lane_workflow_names: dict[tuple[str, int, str], str] = {} - for report_row in current_pending_rows: - lane_identity = ( - report_row["repository"], - report_row["pull_request_number"], - report_row["workflow_identity"], - ) - lane_run_ids.setdefault(lane_identity, set()).add(report_row["run_id"]) - lane_workflow_names.setdefault( - lane_identity, report_row["workflow_name"] - ) - - duplicate_pending_lanes = [ - { - "repository": lane_identity[0], - "pull_request_number": lane_identity[1], - "workflow_identity": lane_identity[2], - "workflow_name": lane_workflow_names[lane_identity], - "count": len(workflow_run_ids), - } - for lane_identity, workflow_run_ids in sorted(lane_run_ids.items()) - if len(workflow_run_ids) > 1 - ] - report["duplicate_pending_lanes"] = duplicate_pending_lanes - report["summary"]["duplicate_pending_lane_count"] = len( - duplicate_pending_lanes - ) - cancelled_before_runner_assignment_count = sum( - report_row.get("admission_state") == "cancelled_before_runner_assignment" - for report_row in report["runs"] - ) - report["summary"]["cancelled_before_runner_assignment_count"] = ( - cancelled_before_runner_assignment_count - ) - terminal_pre_execution_failure_count = sum( - report_row.get("admission_state") == "terminal_pre_execution_failure" - for report_row in report["runs"] - ) - report["summary"]["terminal_pre_execution_failure_count"] = ( - terminal_pre_execution_failure_count - ) - if cancelled_before_runner_assignment_count: - external_action = ( - "Inspect Actions runner admission, billing/usage, runner-group policy, " - "scheduler capacity, and cancellation provenance; cancelled pre-runner " - "evidence remains incomplete." - ) - if external_action not in report["summary"]["external_actions"]: - report["summary"]["external_actions"].append(external_action) - report["summary"]["external_actions"].sort() - if terminal_pre_execution_failure_count: - external_action = ( - "Inspect Actions control-plane admission, billing/usage, runner-group policy, " - "and scheduler state; terminal failure without runner assignment or executed " - "steps is not an executed product/security failure." - ) - if external_action not in report["summary"]["external_actions"]: - report["summary"]["external_actions"].append(external_action) - report["summary"]["external_actions"].sort() - return report - - -def main( - argv: Sequence[str] | None = None, *, stderr: TextIO = sys.stderr -) -> int: - """Collect or load a snapshot, write reports, and return a stable CLI status.""" - cli_arguments = parse_args(argv) - try: - queue_snapshot = ( - load_snapshot(cli_arguments.snapshot) - if cli_arguments.snapshot - else collect_snapshot(load_allowlist(cli_arguments.allowlist)) - ) - evaluation_time = ( - parse_timestamp(cli_arguments.now) - if cli_arguments.now - else datetime.now(timezone.utc) - ) - queue_report = build_report( - queue_snapshot, - now=evaluation_time, - queue_age_slo_seconds=cli_arguments.queue_age_slo_seconds, - ) - write_reports( - queue_report, cli_arguments.output_json, cli_arguments.output_html - ) - except (OSError, QueueHealthError, ValueError) as report_error: - print(f"ERROR: queue-health report failed: {report_error}", file=stderr) - return 2 - - breach_count = queue_report["summary"]["unassigned_slo_breached_count"] - if breach_count: - print( - "::warning::Actions queue-health found " - f"{breach_count} unassigned current-head SLO breach(es)." - ) - print( - "QUEUE_HEALTH_RESULT=" - f"observed={queue_report['summary']['observed_job_count']} " - f"pending={queue_report['summary']['pending_job_count']} " - f"slo_breaches={breach_count}" - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - exercised through CLI tests. - raise SystemExit(main()) diff --git a/scripts/ci/actions_queue_health_core.py b/scripts/ci/actions_queue_health_core.py deleted file mode 100644 index db3e5570ba..0000000000 --- a/scripts/ci/actions_queue_health_core.py +++ /dev/null @@ -1,855 +0,0 @@ -#!/usr/bin/env python3 -"""Produce a read-only, exact-head GitHub Actions queue-health report. - -The collector intentionally treats queued, cancelled, skipped, missing, and -unlinked evidence as incomplete. It never cancels runs, changes branches, or -turns an unavailable runner into a successful check. -""" - -from __future__ import annotations - -import argparse -from datetime import datetime, timezone -import html -import json -from pathlib import Path -import re -import subprocess -import sys -import time -from typing import Any, Callable, Sequence, TextIO - - -REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") -QUEUE_STATES = {"QUEUED", "IN_PROGRESS", "PENDING", "REQUESTED"} -TERMINAL_STATES = {"COMPLETED"} -DEFAULT_QUEUE_AGE_SLO_SECONDS = 900 -SCHEMA_VERSION = "actions.queue_health.v1" -MAX_API_PAGE_SIZE = 100 -WORKFLOW_RUN_PAGE_SIZE = 50 -MAX_API_PAGES = 20 -ACTIVE_RUN_MAX_API_PAGES = 1 -GITHUB_API_TIMEOUT_SECONDS = 30 -PULL_REQUEST_RETRY_DELAY_SECONDS = 1 -PAGINATED_PAGES_KEY = "_queue_health_pages" -Runner = Callable[..., subprocess.CompletedProcess[str]] - - -class QueueHealthError(ValueError): - """Raised when a queue-health input or trusted read is invalid.""" - - -class IncompletePullRequestIdentity(QueueHealthError): - """Raised when a pull-request read omits exact head or base identity.""" - - -def parse_timestamp(value: str) -> datetime: - """Parse an explicit UTC timestamp and reject ambiguous local time.""" - if not isinstance(value, str) or not value.strip(): - raise QueueHealthError("timestamp must be a non-empty string") - try: - parsed = datetime.fromisoformat(value.strip().replace("Z", "+00:00")) - except ValueError as exc: - raise QueueHealthError(f"invalid timestamp: {value!r}") from exc - if parsed.tzinfo is None: - raise QueueHealthError("timestamp must include a timezone") - return parsed.astimezone(timezone.utc) - - -def _repository_name(value: Any) -> str: - """Validate and return one owner/repository identifier.""" - if not isinstance(value, str) or not REPOSITORY_PATTERN.fullmatch(value): - raise QueueHealthError(f"invalid repository identifier: {value!r}") - if any(segment in {".", ".."} for segment in value.split("/")): - raise QueueHealthError(f"invalid repository identifier: {value!r}") - return value - - -def load_allowlist(path: Path) -> list[str]: - """Load a unique, sorted repository allowlist from a JSON array/object.""" - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - raise QueueHealthError(f"unable to load repository allowlist: {exc}") from exc - values = payload.get("repositories") if isinstance(payload, dict) else payload - if not isinstance(values, list) or not values: - raise QueueHealthError("repository allowlist must be a non-empty JSON array") - repositories = sorted({_repository_name(value) for value in values}) - if len(repositories) != len(values): - raise QueueHealthError("repository allowlist contains duplicates") - return repositories - - -def _list_payload( - payload: Any, - key: str, - *, - max_items: int = MAX_API_PAGE_SIZE * MAX_API_PAGES, -) -> list[dict[str, Any]]: - """Extract one bounded GitHub list response without accepting under-collection.""" - declared_total_counts: list[Any] = [] - if isinstance(payload, dict) and PAGINATED_PAGES_KEY in payload: - pages = payload[PAGINATED_PAGES_KEY] - if not isinstance(pages, list) or not pages or len(pages) > MAX_API_PAGES: - raise QueueHealthError(f"GitHub response field {key!r} exceeds the bounded page count") - page_values = [] - for page in pages: - if isinstance(page, list): - page_values.extend(page) - elif isinstance(page, dict): - page_items = page.get(key) - if not isinstance(page_items, list): - raise QueueHealthError(f"GitHub response field {key!r} page must contain an array") - page_values.extend(page_items) - if "total_count" in page: - declared_total_counts.append(page["total_count"]) - else: - raise QueueHealthError(f"GitHub response field {key!r} page must be an array or object") - values = page_values - else: - values = payload if isinstance(payload, list) else payload.get(key) if isinstance(payload, dict) else None - if isinstance(payload, dict) and "total_count" in payload: - declared_total_counts.append(payload["total_count"]) - if not isinstance(values, list) or not all(isinstance(value, dict) for value in values): - raise QueueHealthError(f"GitHub response field {key!r} must be an array of objects") - if isinstance(payload, dict) and PAGINATED_PAGES_KEY in payload: - record_identities: list[tuple[str, int]] = [] - for value in values: - record_id = value.get("id") - if not isinstance(record_id, bool) and isinstance(record_id, int) and record_id > 0: - record_identities.append(("id", record_id)) - continue - record_number = value.get("number") - if ( - not isinstance(record_number, bool) - and isinstance(record_number, int) - and record_number > 0 - ): - record_identities.append(("number", record_number)) - continue - else: - raise QueueHealthError( - f"GitHub response field {key!r} paginated records must have a positive integer id or number" - ) - if len(record_identities) != len(set(record_identities)): - raise QueueHealthError( - f"GitHub response field {key!r} contains a duplicate record identity across pages" - ) - if declared_total_counts: - if any(isinstance(total_count, bool) or not isinstance(total_count, int) for total_count in declared_total_counts): - raise QueueHealthError(f"GitHub response field {key!r} has invalid total counts") - total_count = max(declared_total_counts) - if total_count < len(values) or total_count > max_items: - raise QueueHealthError(f"GitHub response field {key!r} exceeds the bounded page size") - if PAGINATED_PAGES_KEY in payload and total_count != len(values): - raise QueueHealthError(f"GitHub response field {key!r} is incompletely paginated") - return values - - -def github_json( - path: str, - *, - paginate: bool = False, - max_pages: int = MAX_API_PAGES, - runner: Runner = subprocess.run, -) -> Any: - """Read one GitHub REST endpoint through ``gh`` without shell evaluation.""" - if not path.startswith("repos/"): - raise QueueHealthError(f"GitHub endpoint is outside repository scope: {path}") - pages: list[Any] = [] - page_size_match = re.search(r"(?:[?&])per_page=(\d+)(?:&|$)", path) - page_size = int(page_size_match.group(1)) if page_size_match else MAX_API_PAGE_SIZE - page_numbers = range(1, max_pages + 1) if paginate else range(1, 2) - for page_number in page_numbers: - page_path = path - if paginate and page_number > 1: - page_path = f"{path}{'&' if '?' in path else '?'}page={page_number}" - try: - result = runner( - ["gh", "api", page_path], - capture_output=True, - text=True, - check=False, - timeout=GITHUB_API_TIMEOUT_SECONDS, - ) - except subprocess.TimeoutExpired as exc: - raise QueueHealthError( - f"GitHub API read timed out after {GITHUB_API_TIMEOUT_SECONDS} seconds for {page_path}" - ) from exc - if result.returncode != 0: - detail = (result.stderr or result.stdout or "GitHub API read failed").strip() - raise QueueHealthError(f"GitHub API read failed for {page_path}: {detail[:400]}") - try: - payload = json.loads(result.stdout) - except json.JSONDecodeError as exc: - raise QueueHealthError(f"GitHub API returned invalid JSON for {page_path}") from exc - if not paginate: - return payload - pages.append(payload) - values = payload if isinstance(payload, list) else None - total_count = payload.get("total_count") if isinstance(payload, dict) else None - if isinstance(payload, dict): - values = next((value for value in payload.values() if isinstance(value, list)), None) - if not isinstance(values, list): - raise QueueHealthError(f"GitHub API page has no bounded array for {page_path}") - collected = sum( - len(page) if isinstance(page, list) else len(next((value for value in page.values() if isinstance(value, list)), [])) - for page in pages - ) - if (type(total_count) is int and total_count <= collected) or len(values) < page_size: - return {PAGINATED_PAGES_KEY: pages} - raise QueueHealthError( - f"GitHub API pagination exceeds {max_pages} pages for {path}" - ) - - -def _normalise_pull_request( - pull_request: dict[str, Any], *, allow_normalized: bool = False -) -> dict[str, Any]: - """Keep only exact-head identity fields needed for queue classification. - - Empty or missing ``head_sha``, ``base_ref``, ``base_repository``, or - ``updated_at`` values are treated as an incomplete identity — the same - as a missing ``head``/``base`` object — so a transient, partially - populated GitHub API response triggers the caller's bounded retry - instead of being silently accepted and later misclassifying an active - run as obsolete. - """ - if not isinstance(pull_request, dict): - raise QueueHealthError("pull request entry must be an object") - number = pull_request.get("number") - if allow_normalized and "head" not in pull_request and "base" not in pull_request: - if not all( - isinstance(pull_request.get(field), str) and pull_request.get(field) - for field in ("base_ref", "base_repository", "head_sha", "updated_at") - ): - raise IncompletePullRequestIdentity( - "normalized pull request identity fields must be non-empty strings" - ) - if isinstance(number, bool) or not isinstance(number, int) or number <= 0: - raise QueueHealthError("pull request number must be a positive integer") - return { - "number": number, - "state": pull_request.get("state", "open"), - "base_ref": pull_request["base_ref"], - "base_repository": pull_request["base_repository"], - "head_sha": pull_request["head_sha"], - "updated_at": pull_request["updated_at"], - } - head = pull_request.get("head") - base = pull_request.get("base") - if not isinstance(head, dict) or not isinstance(base, dict): - raise IncompletePullRequestIdentity("pull request head and base must be objects") - if isinstance(number, bool) or not isinstance(number, int) or number <= 0: - raise QueueHealthError("pull request number must be a positive integer") - head_sha = head.get("sha", "") - base_ref = base.get("ref", "") - base_repository = ( - (base.get("repo") or {}).get("full_name", "") if isinstance(base.get("repo"), dict) else "" - ) - updated_at = pull_request.get("updated_at", "") - if not all( - isinstance(value, str) and value for value in (head_sha, base_ref, base_repository, updated_at) - ): - raise IncompletePullRequestIdentity( - "pull request head, base, and updated_at identity fields must be non-empty" - ) - return { - "number": number, - "state": pull_request.get("state", "open"), - "base_ref": base_ref, - "base_repository": base_repository, - "head_sha": head_sha, - "updated_at": updated_at, - } - - -def _normalise_job(job: dict[str, Any]) -> dict[str, Any]: - """Keep job state and runner assignment evidence without log contents. - - Preserves the job's own ``created_at`` (when GitHub scheduled that - specific job) separately from the parent run's ``created_at``, so a - job that only became eligible after an earlier stage in the same - in-progress run finished is not measured against the whole run's age. - """ - if not isinstance(job, dict): - raise QueueHealthError("workflow job entry must be an object") - job_id = job.get("id") - if isinstance(job_id, bool) or not isinstance(job_id, int) or job_id <= 0: - raise QueueHealthError("job id must be a positive integer") - runner_id = job.get("runner_id") - if isinstance(runner_id, bool) or not isinstance(runner_id, int): - runner_id = 0 - if "steps" in job: - workflow_steps = job["steps"] - if workflow_steps is not None and not isinstance(workflow_steps, list): - raise QueueHealthError("workflow job steps must be an array or null") - steps_count = len(workflow_steps) if isinstance(workflow_steps, list) else None - else: - steps_count = job.get("steps_count") - if steps_count is not None and ( - isinstance(steps_count, bool) - or not isinstance(steps_count, int) - or steps_count < 0 - ): - raise QueueHealthError( - "normalized workflow job steps_count must be a non-negative integer or null" - ) - return { - "id": job_id, - "name": str(job.get("name") or "unnamed job"), - "status": str(job.get("status") or "").upper(), - "conclusion": str(job.get("conclusion") or "").upper(), - "runner_id": runner_id, - "runner_name": str(job.get("runner_name") or ""), - "created_at": str(job.get("created_at") or ""), - "steps_count": steps_count, - } - - -def _normalise_run(repository: str, run: dict[str, Any], jobs: list[dict[str, Any]]) -> dict[str, Any]: - """Keep run identity and job state required for deterministic reporting. - - Accepts a pull-request link either in GitHub's raw shape - (``{"number": ..., "head": {"sha": ...}}``) or in the flattened shape - this function itself emits (``{"number": ..., "head_sha": ...}``), so - re-normalising an already-normalised run loaded back from a collected - snapshot (as ``build_report`` does) does not silently zero out the - linked head SHA that exact-head identity resolution depends on. - """ - if not isinstance(run, dict): - raise QueueHealthError("workflow run entry must be an object") - if not isinstance(jobs, list) or not all(isinstance(job, dict) for job in jobs): - raise QueueHealthError("workflow run jobs must be an array of objects") - run_id = run.get("id") - if isinstance(run_id, bool) or not isinstance(run_id, int) or run_id <= 0: - raise QueueHealthError("workflow run id must be a positive integer") - pull_requests = run.get("pull_requests", []) - if pull_requests is None: - pull_requests = [] - if not isinstance(pull_requests, list) or not all(isinstance(item, dict) for item in pull_requests): - raise QueueHealthError("workflow run pull_requests must be an array of objects") - links = [] - for item in pull_requests: - number = item.get("number") - if isinstance(number, bool) or not isinstance(number, int) or number <= 0: - raise QueueHealthError("workflow run pull request number must be positive") - if "head" not in item and isinstance(item.get("head_sha"), str): - links.append({"number": number, "head_sha": item["head_sha"]}) - continue - head = item.get("head", {}) - if head is None: - head = {} - if not isinstance(head, dict): - raise QueueHealthError("workflow run pull request head must be an object") - links.append({"number": number, "head_sha": str(head.get("sha") or "")}) - return { - "repository": repository, - "id": run_id, - "workflow_name": str(run.get("name") or run.get("workflow_name") or "unnamed workflow"), - "event": str(run.get("event") or "unknown"), - "status": str(run.get("status") or "").upper(), - "conclusion": str(run.get("conclusion") or "").upper(), - "head_sha": str(run.get("head_sha") or ""), - "created_at": str(run.get("created_at") or ""), - "updated_at": str(run.get("updated_at") or ""), - "run_attempt": run.get("run_attempt", 1), - "concurrency_group": str(run.get("concurrency_group") or "unavailable_from_actions_api"), - "pull_requests": sorted(links, key=lambda item: item["number"]), - "jobs": sorted((_normalise_job(job) for job in jobs), key=lambda item: item["id"]), - } - - -def collect_snapshot( - repositories: Sequence[str], - *, - runner: Runner = subprocess.run, - generated_at: str | None = None, -) -> dict[str, Any]: - """Collect bounded queued/in-progress run and job data using read-only API calls.""" - validated = sorted({_repository_name(repository) for repository in repositories}) - if len(validated) != len(repositories): - raise QueueHealthError("collection repository list contains duplicates") - collected_repositories: list[dict[str, Any]] = [] - collection_errors: list[dict[str, str]] = [] - for repository in validated: - try: - metadata = github_json(f"repos/{repository}", runner=runner) - if not isinstance(metadata, dict): - raise QueueHealthError(f"repository metadata for {repository} is not an object") - pulls_endpoint = f"repos/{repository}/pulls?state=open&per_page={MAX_API_PAGE_SIZE}" - pull_requests = _list_payload( - github_json(pulls_endpoint, paginate=True, runner=runner), - "pulls", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - normalized_pull_requests = sorted( - (_normalise_pull_request(item) for item in pull_requests), - key=lambda item: item["number"], - ) - except IncompletePullRequestIdentity: - time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS) - try: - retry_pull_requests = _list_payload( - github_json(pulls_endpoint, paginate=True, runner=runner), - "pulls", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - normalized_pull_requests = sorted( - (_normalise_pull_request(item) for item in retry_pull_requests), - key=lambda item: item["number"], - ) - except QueueHealthError as retry_exc: - collection_errors.append( - { - "repository": repository, - "error": f"pull-request identity validation failed: {retry_exc}", - } - ) - continue - except QueueHealthError as exc: - collection_errors.append({"repository": repository, "error": str(exc)}) - continue - pull_requests_by_number = {item["number"]: item for item in normalized_pull_requests} - runs_by_id: dict[int, dict[str, Any]] = {} - try: - active_statuses = ("in_progress", "pending", "queued", "requested", "waiting") - snapshots: list[dict[int, dict[str, Any]]] = [] - for status_order in (active_statuses, tuple(reversed(active_statuses))): - snapshot: dict[int, dict[str, Any]] = {} - for status in status_order: - runs = _list_payload( - github_json( - f"repos/{repository}/actions/runs?status={status}" - f"&per_page={WORKFLOW_RUN_PAGE_SIZE}", - paginate=True, - max_pages=ACTIVE_RUN_MAX_API_PAGES, - runner=runner, - ), - "workflow_runs", - max_items=WORKFLOW_RUN_PAGE_SIZE * ACTIVE_RUN_MAX_API_PAGES, - ) - for run in runs: - run_id = run.get("id") - if isinstance(run_id, bool) or not isinstance(run_id, int) or run_id <= 0: - raise QueueHealthError("workflow run id must be a positive integer") - snapshot[run_id] = run - snapshots.append(snapshot) - first_snapshot, second_snapshot = snapshots - first_states = { - run_id: str(run.get("status") or "").upper() - for run_id, run in first_snapshot.items() - } - second_states = { - run_id: str(run.get("status") or "").upper() - for run_id, run in second_snapshot.items() - } - if first_states != second_states: - raise QueueHealthError("active workflow run snapshot changed during collection") - for run_id, run in second_snapshot.items(): - run_id = run.get("id") - candidate = _normalise_run(repository, run, []) - identity, _ = _run_identity(candidate, pull_requests_by_number) - if identity != "current_head" or candidate["status"] not in { - "IN_PROGRESS", - "WAITING", - }: - runs_by_id[run_id] = candidate - continue - jobs_payload = github_json( - f"repos/{repository}/actions/runs/{run_id}/jobs?per_page={MAX_API_PAGE_SIZE}", - paginate=True, - runner=runner, - ) - jobs = _list_payload( - jobs_payload, - "jobs", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - runs_by_id[run_id] = _normalise_run(repository, run, jobs) - except QueueHealthError as exc: - collection_errors.append({"repository": repository, "error": str(exc)}) - continue - collected_repositories.append( - { - "full_name": repository, - "default_branch": str(metadata.get("default_branch") or ""), - "pull_requests": normalized_pull_requests, - "runs": sorted(runs_by_id.values(), key=lambda item: item["id"]), - } - ) - timestamp = generated_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - parse_timestamp(timestamp) - return { - "generated_at": timestamp, - "repositories": collected_repositories, - "collection_errors": collection_errors, - } - - -def load_snapshot(path: Path) -> dict[str, Any]: - """Load a JSON snapshot for offline, deterministic report generation.""" - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - raise QueueHealthError(f"unable to load queue-health snapshot: {exc}") from exc - if not isinstance(payload, dict): - raise QueueHealthError("queue-health snapshot root must be an object") - return payload - - -def _run_identity(run: dict[str, Any], pull_requests: dict[int, dict[str, Any]]) -> tuple[str, int | None]: - """Resolve one run to current-head, obsolete, or unlinked identity. - - Compares the open pull request's head SHA against the *linked* - pull-request head SHA carried on the run (``run["pull_requests"][*] - ["head_sha"]``), never against the run-level ``head_sha``. For - ``pull_request_target``-triggered runs, GitHub reports the run-level - ``head_sha`` as the base-branch commit that was checked out, not the - pull request's head commit; only the linked pull-request entry carries - the real head SHA that was reviewed. Using the run-level value there - would misclassify a genuinely current, active required-workflow run as - ``obsolete`` and skip fetching its job evidence. - """ - links = run.get("pull_requests") or [] - for link in links: - number = link.get("number") - pull_request = pull_requests.get(number) - if pull_request and pull_request.get("head_sha") == link.get("head_sha"): - return "current_head", number - if links: - return "obsolete", links[0].get("number") - return "unlinked", None - - -def _job_state(job: dict[str, Any]) -> tuple[str, bool, bool]: - """Return normalized execution state, pending flag, and runner assignment. - - GitHub's ``waiting`` job status (a job paused on an environment or - deployment approval) is incomplete pending evidence just like - ``queued``/``in_progress`` — it must remain visible with its own - blocker and action rather than silently dropping out of the pending - count as unclassified ``unknown`` evidence. - """ - status = str(job.get("status") or "").upper() - conclusion = str(job.get("conclusion") or "").upper() - assigned = bool(job.get("runner_name")) or (isinstance(job.get("runner_id"), int) and job.get("runner_id", 0) > 0) - if status == "WAITING": - return "waiting_approval", True, assigned - if status in QUEUE_STATES: - return ("queued_assigned" if assigned else "queued_unassigned"), True, assigned - if status in TERMINAL_STATES or conclusion: - return "terminal", False, assigned - return "unknown", False, assigned - - -def _format_age(created_at: str, now: datetime) -> int: - """Return non-negative queue age seconds from an explicit timestamp.""" - created = parse_timestamp(created_at) - return max(0, int((now - created).total_seconds())) - - -def build_report( - snapshot: dict[str, Any], - *, - now: datetime | None = None, - queue_age_slo_seconds: int = DEFAULT_QUEUE_AGE_SLO_SECONDS, -) -> dict[str, Any]: - """Classify every observed job without treating incomplete evidence as success.""" - if queue_age_slo_seconds < 0: - raise QueueHealthError("queue age SLO must not be negative") - generated_at = parse_timestamp(snapshot.get("generated_at")) - if now is not None and (not isinstance(now, datetime) or now.tzinfo is None): - raise QueueHealthError("evaluation time must include a timezone") - report_now = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) - repositories = snapshot.get("repositories") - if not isinstance(repositories, list): - raise QueueHealthError("queue-health snapshot repositories must be an array") - raw_collection_errors = snapshot.get("collection_errors", []) - if raw_collection_errors is None: - raw_collection_errors = [] - if not isinstance(raw_collection_errors, list): - raise QueueHealthError("queue-health collection_errors must be an array") - collection_errors: list[dict[str, str]] = [] - for item in raw_collection_errors: - if not isinstance(item, dict): - raise QueueHealthError("queue-health collection error must be an object") - repository_name = _repository_name(item.get("repository")) - error = item.get("error") - if not isinstance(error, str) or not error: - raise QueueHealthError("queue-health collection error must contain text") - collection_errors.append({"repository": repository_name, "error": error}) - - rows: list[dict[str, Any]] = [] - seen_repositories: set[str] = set() - for repository in repositories: - if not isinstance(repository, dict): - raise QueueHealthError("queue-health repository entry must be an object") - full_name = _repository_name(repository.get("full_name")) - if full_name in seen_repositories: - raise QueueHealthError(f"duplicate repository entry {full_name}") - seen_repositories.add(full_name) - pull_request_entries = repository.get("pull_requests", []) - if pull_request_entries is None: - pull_request_entries = [] - if not isinstance(pull_request_entries, list): - raise QueueHealthError(f"pull requests for {full_name} must be an array") - pull_requests: dict[int, dict[str, Any]] = {} - for pull_request in pull_request_entries: - normalized = _normalise_pull_request(pull_request, allow_normalized=True) - if normalized["number"] in pull_requests: - raise QueueHealthError(f"duplicate pull request {normalized['number']} for {full_name}") - pull_requests[normalized["number"]] = normalized - runs = repository.get("runs", []) - if runs is None: - runs = [] - if not isinstance(runs, list): - raise QueueHealthError(f"runs for {full_name} must be an array") - run_ids: set[int] = set() - for raw_run in runs: - if not isinstance(raw_run, dict): - raise QueueHealthError("workflow run entry must be an object") - raw_jobs = raw_run.get("jobs", []) - if raw_jobs is None: - raw_jobs = [] - if not isinstance(raw_jobs, list): - raise QueueHealthError("workflow run jobs must be an array") - run = _normalise_run(full_name, raw_run, raw_jobs) - if run["id"] in run_ids: - raise QueueHealthError(f"duplicate workflow run {run['id']} for {full_name}") - run_ids.add(run["id"]) - identity, pull_request_number = _run_identity(run, pull_requests) - jobs = run["jobs"] - for job in jobs or [{"id": run["id"], "name": "run", "status": run.get("status")}]: - state, pending, assigned = _job_state(job) - # Prefer the job's own created_at: for a job with `needs:` - # dependencies inside an already in-progress run, GitHub - # sets it when the job became eligible, which can be long - # after the run itself started. Falling back to the run's - # created_at only applies to the synthetic run-level job - # used when no job evidence was fetched. - age_created_at = job.get("created_at") or run.get("created_at") - age_seconds = _format_age(age_created_at, report_now) - slo_breached = pending and age_seconds > queue_age_slo_seconds - if identity == "obsolete": - blocker = "obsolete_run_requires_identity_confirmed_cleanup" - action = "owner_cleanup_after_exact_identity_confirmation" - elif identity == "unlinked": - blocker = "run_not_linked_to_pull_request" - action = "reconcile_run_identity_before_cleanup" - elif state == "waiting_approval": - blocker = "environment_or_deployment_approval_required" - action = "reviewer_or_owner_approve_pending_environment_deployment" - elif pending and not assigned and slo_breached: - blocker = "external_runner_assignment_or_capacity" - action = "owner_check_runner_billing_policy_and_concurrency" - elif pending: - blocker = "current_head_required_evidence_incomplete" - action = "wait_for_runner_or_escalate_after_slo" - else: - blocker = None - action = "none" - rows.append( - { - "repository": full_name, - "workflow_name": run.get("workflow_name", "unnamed workflow"), - "run_id": run.get("id"), - "run_attempt": run.get("run_attempt", 1), - "job_id": job.get("id"), - "job_name": job.get("name", "unnamed job"), - "event": run.get("event", "unknown"), - "head_sha": run.get("head_sha", ""), - "pull_request_number": pull_request_number, - "identity_state": identity, - "status": job.get("status", ""), - "conclusion": job.get("conclusion", ""), - "execution_state": state, - "is_pending": pending, - "runner_assigned": assigned, - "created_at": run.get("created_at", ""), - "updated_at": run.get("updated_at", ""), - "queue_age_seconds": age_seconds, - "slo_breached": slo_breached, - "concurrency_group": run.get("concurrency_group", "unavailable_from_actions_api"), - "obsolete": identity == "obsolete", - "blocker": blocker, - "recommended_action": action, - } - ) - - rows.sort(key=lambda row: (row["repository"], row["run_id"], row["job_id"])) - pending = [row for row in rows if row["is_pending"]] - current_pending = [row for row in pending if row["identity_state"] == "current_head"] - lane_run_ids: dict[tuple[str, int, str], set[int]] = {} - for row in current_pending: - lane = ( - row["repository"], - row["pull_request_number"], - row["workflow_name"], - ) - lane_run_ids.setdefault(lane, set()).add(row["run_id"]) - duplicate_lanes = [ - { - "repository": key[0], - "pull_request_number": key[1], - "workflow_name": key[2], - "count": len(run_ids), - } - for key, run_ids in sorted(lane_run_ids.items()) - if len(run_ids) > 1 - ] - external_actions = sorted( - { - "Inspect GitHub-hosted runner assignment, Actions billing/usage, runner-group policy, environment approval, and concurrency saturation; queued evidence remains incomplete." - for row in rows - if row["blocker"] == "external_runner_assignment_or_capacity" - } - ) - summary = { - "observed_job_count": len(rows), - "pending_job_count": len(pending), - "current_head_pending_count": len(current_pending), - "unassigned_slo_breached_count": sum( - row["identity_state"] == "current_head" - and row["execution_state"] == "queued_unassigned" - and row["slo_breached"] - for row in rows - ), - "obsolete_job_count": sum(row["obsolete"] for row in rows), - "unlinked_job_count": sum(row["identity_state"] == "unlinked" for row in rows), - "duplicate_pending_lane_count": len(duplicate_lanes), - "terminal_job_count": sum(row["execution_state"] == "terminal" for row in rows), - "collection_error_count": len(collection_errors), - "external_actions": external_actions, - } - return { - "schema_version": SCHEMA_VERSION, - "generated_at": generated_at.isoformat().replace("+00:00", "Z"), - "evaluated_at": report_now.isoformat().replace("+00:00", "Z"), - "queue_age_slo_seconds": queue_age_slo_seconds, - "repositories": sorted(_repository_name(repository["full_name"]) for repository in repositories), - "collection_errors": collection_errors, - "summary": summary, - "duplicate_pending_lanes": duplicate_lanes, - "runs": rows, - "limitations": [ - "The Actions REST API does not expose the evaluated concurrency group for every run; unavailable values are reported explicitly.", - "This read-only slice never cancels runs or changes branch/check state.", - ], - } - - -def render_html(report: dict[str, Any]) -> str: - """Render a keyboard-readable HTML report with escaped untrusted fields.""" - summary = report["summary"] - rows = report["runs"] - table_rows = [] - for row in rows: - table_rows.append( - "" - + f'{html.escape(str(row["repository"]))}' - + "".join( - f"{html.escape(str(row[field]))}" - for field in ( - "workflow_name", - "run_id", - "job_name", - "identity_state", - "execution_state", - "head_sha", - "queue_age_seconds", - "blocker", - ) - ) - + "" - ) - body = "".join(table_rows) or 'No queued or in-progress jobs observed.' - collection_error_section = "" - if report.get("collection_errors"): - collection_error_section = ( - "

" - "Incomplete collection evidence

    " - + "".join( - "
  • " - + html.escape(str(item["repository"])) - + ": " - + html.escape(str(item["error"])) - + "
  • " - for item in report["collection_errors"] - ) - + "
" - ) - return ( - "\n" - '' - "GitHub Actions queue health" - "" - '
' - "

GitHub Actions queue health

" - + collection_error_section - + f"

Evaluated at ; queue-age SLO: {report['queue_age_slo_seconds']} seconds.

" - f"

Observed jobs: {summary['observed_job_count']}; current-head pending: {summary['current_head_pending_count']}; SLO breaches: {summary['unassigned_slo_breached_count']}.

" - '' - "" - + "".join(f"" for field in ( - "repository", "workflow_name", "run_id", "job_name", "identity_state", "execution_state", "head_sha", "queue_age_seconds", "blocker" - )) - + f"{body}
Run and job evidence; queued evidence is not a passing check.
{field.replace('_', ' ').title()}
\n" - ) - - -def write_reports(report: dict[str, Any], json_path: Path, html_path: Path) -> None: - """Write deterministic JSON and accessible HTML reports.""" - json_path.parent.mkdir(parents=True, exist_ok=True) - html_path.parent.mkdir(parents=True, exist_ok=True) - json_path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - html_path.write_text(render_html(report), encoding="utf-8") - - -def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: - """Parse live-collection or offline-report CLI arguments.""" - parser = argparse.ArgumentParser(description=__doc__) - source = parser.add_mutually_exclusive_group(required=True) - source.add_argument("--snapshot", type=Path) - source.add_argument("--allowlist", type=Path) - parser.add_argument("--output-json", type=Path, required=True) - parser.add_argument("--output-html", type=Path, required=True) - parser.add_argument("--queue-age-slo-seconds", type=int, default=DEFAULT_QUEUE_AGE_SLO_SECONDS) - parser.add_argument("--now", help="Explicit timezone-aware evaluation time for deterministic reports") - return parser.parse_args(argv) - - -def main(argv: Sequence[str] | None = None, *, stderr: TextIO = sys.stderr) -> int: - """Collect or load a snapshot, write reports, and return a stable CLI status.""" - args = parse_args(argv) - try: - snapshot = load_snapshot(args.snapshot) if args.snapshot else collect_snapshot(load_allowlist(args.allowlist)) - now = parse_timestamp(args.now) if args.now else datetime.now(timezone.utc) - report = build_report( - snapshot, - now=now, - queue_age_slo_seconds=args.queue_age_slo_seconds, - ) - write_reports(report, args.output_json, args.output_html) - except (OSError, QueueHealthError, ValueError) as exc: - print(f"ERROR: queue-health report failed: {exc}", file=stderr) - return 2 - breaches = report["summary"]["unassigned_slo_breached_count"] - if breaches: - print(f"::warning::Actions queue-health found {breaches} unassigned current-head SLO breach(es).") - print( - "QUEUE_HEALTH_RESULT=" - f"observed={report['summary']['observed_job_count']} " - f"pending={report['summary']['pending_job_count']} " - f"slo_breaches={breaches}" - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - exercised through the CLI tests. - raise SystemExit(main()) diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index 68e544a6cf..46332cfc2f 100755 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -110,7 +110,7 @@ f"repos/{CENTRAL_AUTOMATION_REPOSITORY}/actions/artifacts" ) LEDGER_ARTIFACT_PREFIX = "cwl-agent-invocation-" -REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") HEAD_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") BASE_BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") ACTOR_RE = re.compile(r"^[A-Za-z0-9-]+$") diff --git a/scripts/ci/agent_mention_sweep.py b/scripts/ci/agent_mention_sweep.py index 5b56fdcf4f..50e0a84f17 100755 --- a/scripts/ci/agent_mention_sweep.py +++ b/scripts/ci/agent_mention_sweep.py @@ -22,8 +22,8 @@ ) from redact_sensitive_log import redact_text -ORG_NAME_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") -REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +ORG_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") REPOSITORY_SOURCES = frozenset({"organization", "installation"}) REPOSITORY_ROTATION_SECONDS = 5 * 60 # The sweep-organization-agent-mentions job has a 900s (15-minute) GitHub diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py deleted file mode 100644 index 53e00c41c6..0000000000 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ /dev/null @@ -1,349 +0,0 @@ -#!/usr/bin/env python3 -"""Prove GHAS CodeQL base/head configuration identity continuity. - -GitHub Advanced Security computes PR-introduced alerts by pairing each CodeQL -configuration present on the protected base with the same identity on the PR -head. A Default setup baseline such as ``Default setup /language:rust`` is the -tuple ``(dynamic/github-code-scanning/codeql:analyze, /language:rust)``. When -the head is missing that identity, GHAS reports a neutral -``configuration not found`` result even if a central dispatch scan already -passed (ContextualWisdomLab/.github#2133). - -This module is the executable contract for that pairing rule. It never uploads -SARIF, never disables Default setup, and never synthesizes a status: callers -supply authenticated analysis payloads and receive a fail-closed verdict. -""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from typing import Any, Iterable, Mapping, Sequence - -DEFAULT_SETUP_ANALYSIS_KEY = "dynamic/github-code-scanning/codeql:analyze" -CODEQL_TOOL_NAME = "CodeQL" -GITHUB_API_AUTHORITY = "api.github.com" - - -class ConfigurationIdentityError(RuntimeError): - """Report a fail-closed GHAS configuration-identity contract failure.""" - - -class _RejectRedirects(urllib.request.HTTPRedirectHandler): - """Prevent authenticated GitHub REST requests from creating redirect requests.""" - - def redirect_request( - self, - _request: urllib.request.Request, - _file_pointer: Any, - _code: int, - _message: str, - _headers: Any, - _new_url: str, - ) -> None: - """Refuse every redirect so bearer headers never cross the reviewed authority.""" - return None - - -_GITHUB_API_OPENER = urllib.request.build_opener(_RejectRedirects()) - - -def language_category(language: str) -> str: - """Return the CodeQL category string GHAS uses for one language.""" - normalized = str(language or "").strip().lower() - if not normalized: - raise ConfigurationIdentityError("language is required for a GHAS category") - if any(ch.isspace() for ch in normalized) or "/" in normalized: - raise ConfigurationIdentityError(f"language is not a safe CodeQL category token: {language!r}") - return f"/language:{normalized}" - - -def configuration_identity(analysis_key: str, category: str) -> tuple[str, str]: - """Normalize one GHAS configuration identity as ``(analysis_key, category)``.""" - key = str(analysis_key or "").strip() - cat = str(category or "").strip() - if not key or not cat: - raise ConfigurationIdentityError("analysis_key and category are both required") - return key, cat - - -def default_setup_identity(language: str) -> tuple[str, str]: - """Return the Default setup identity GHAS shows as ``Default setup /language:X``.""" - return configuration_identity(DEFAULT_SETUP_ANALYSIS_KEY, language_category(language)) - - -def iter_codeql_identities( - analyses: Sequence[Mapping[str, Any]], - *, - commit_sha: str | None = None, -) -> set[tuple[str, str]]: - """Collect CodeQL ``(analysis_key, category)`` identities from analysis payloads. - - When ``commit_sha`` is set, only analyses bound to that exact commit are - kept. Non-mapping rows and non-CodeQL tools are ignored. - """ - wanted = str(commit_sha or "").strip().lower() or None - found: set[tuple[str, str]] = set() - for row in analyses: - if not isinstance(row, Mapping): - continue - tool = row.get("tool") - tool_name = "" - if isinstance(tool, Mapping): - tool_name = str(tool.get("name") or "") - elif isinstance(tool, str): - tool_name = tool - if tool_name != CODEQL_TOOL_NAME: - continue - if wanted is not None: - sha = str(row.get("commit_sha") or "").strip().lower() - if sha != wanted: - continue - try: - found.add( - configuration_identity( - str(row.get("analysis_key") or ""), - str(row.get("category") or ""), - ) - ) - except ConfigurationIdentityError: - continue - return found - - -def missing_base_identities( - base_identities: Iterable[tuple[str, str]], - head_identities: Iterable[tuple[str, str]], - *, - language: str | None = None, -) -> list[tuple[str, str]]: - """Return base identities absent from the head, optionally limited to one language.""" - base_set = {configuration_identity(*item) for item in base_identities} - head_set = {configuration_identity(*item) for item in head_identities} - missing = base_set - head_set - if language is not None: - category = language_category(language) - missing = {item for item in missing if item[1] == category} - return sorted(missing) - - -def pairing_ready( - base_analyses: Sequence[Mapping[str, Any]], - head_analyses: Sequence[Mapping[str, Any]], - *, - base_sha: str, - head_sha: str, - language: str, -) -> tuple[bool, list[tuple[str, str]]]: - """Return whether GHAS can pair base/head CodeQL identities for ``language``.""" - base_ids = iter_codeql_identities(base_analyses, commit_sha=base_sha) - head_ids = iter_codeql_identities(head_analyses, commit_sha=head_sha) - category = language_category(language) - base_for_language = {item for item in base_ids if item[1] == category} - if not base_for_language: - # No base configuration for this language means GHAS will not demand one - # on the head for introduced-alert computation of that language. - return True, [] - missing = missing_base_identities(base_for_language, head_ids, language=language) - return not missing, missing - - -def format_identity(identity: tuple[str, str]) -> str: - """Render one identity the way GHAS titles Default setup warnings.""" - analysis_key, category = identity - if analysis_key == DEFAULT_SETUP_ANALYSIS_KEY: - return f"Default setup {category}" - return f"{analysis_key} {category}" - - -def _require_github_api_url(url: str) -> str: - """Reject any REST target outside canonical HTTPS ``api.github.com`` authority.""" - try: - parsed = urllib.parse.urlsplit(url) - except ValueError as exc: - raise ConfigurationIdentityError( - "GitHub API URL must use canonical https://api.github.com authority" - ) from exc - if ( - parsed.scheme != "https" - or parsed.netloc != GITHUB_API_AUTHORITY - or not parsed.path.startswith("/") - or parsed.fragment - ): - raise ConfigurationIdentityError( - "GitHub API URL must use canonical https://api.github.com authority" - ) - return url - - -def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: - """GET one canonical GitHub REST URL without redirects, or fail closed.""" - url = _require_github_api_url(url) - request = urllib.request.Request( - url, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "cwl-codeql-ghas-configuration-identity", - }, - method="GET", - ) - try: - with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: - payload = response.read().decode("utf-8") - except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace")[-400:] - raise ConfigurationIdentityError( - f"GitHub API GET failed with HTTP {exc.code}: {body}" - ) from exc - except (urllib.error.URLError, TimeoutError, OSError) as exc: - raise ConfigurationIdentityError( - f"GitHub API transport failed: {type(exc).__name__}" - ) from exc - if not payload.strip(): - return [] - try: - return json.loads(payload) - except json.JSONDecodeError as exc: - raise ConfigurationIdentityError("GitHub API returned invalid JSON") from exc - - -def list_codeql_analyses( - repository: str, - *, - token: str, - ref: str | None = None, - per_page: int = 100, - timeout_seconds: int = 30, -) -> list[dict[str, Any]]: - """List code-scanning analyses for a repository, optionally filtered by ref.""" - if not repository or "/" not in repository: - raise ConfigurationIdentityError("repository must be owner/name") - if not token: - raise ConfigurationIdentityError("token is required to list analyses") - params: dict[str, str] = {"per_page": str(per_page), "tool_name": CODEQL_TOOL_NAME} - if ref: - params["ref"] = ref - query = urllib.parse.urlencode(params) - url = f"https://api.github.com/repos/{repository}/code-scanning/analyses?{query}" - payload = _request_json(url, token=token, timeout_seconds=timeout_seconds) - if not isinstance(payload, list): - raise ConfigurationIdentityError("code-scanning analyses response was not a list") - return [row for row in payload if isinstance(row, dict)] - - -def wait_for_language_pairing( - *, - repository: str, - token: str, - base_ref: str, - base_sha: str, - head_ref: str, - head_sha: str, - language: str, - attempts: int = 30, - sleep_seconds: float = 20.0, - sleeper: Any = time.sleep, -) -> list[tuple[str, str]]: - """Poll until the head carries every base CodeQL identity for ``language``. - - Returns the empty list on success. Raises ConfigurationIdentityError when - the budget is exhausted with identities still missing. - """ - if attempts < 1: - raise ConfigurationIdentityError("attempts must be at least 1") - last_missing: list[tuple[str, str]] = [] - for attempt in range(1, attempts + 1): - base_analyses = list_codeql_analyses(repository, token=token, ref=base_ref) - head_analyses = list_codeql_analyses(repository, token=token, ref=head_ref) - ready, missing = pairing_ready( - base_analyses, - head_analyses, - base_sha=base_sha, - head_sha=head_sha, - language=language, - ) - if ready: - return [] - last_missing = missing - rendered = ", ".join(format_identity(item) for item in missing) or "" - print( - f"GHAS configuration identity not yet continuous for {language} " - f"(attempt {attempt}/{attempts}): missing {rendered}", - file=sys.stderr, - ) - if attempt < attempts: - sleeper(sleep_seconds) - rendered = ", ".join(format_identity(item) for item in last_missing) or "" - raise ConfigurationIdentityError( - "GHAS cannot pair base/head CodeQL configuration identities for " - f"{language}; missing on head: {rendered}" - ) - - -def _build_parser() -> argparse.ArgumentParser: - """Build the CLI parser used by the CodeQL scan-dispatch handler.""" - parser = argparse.ArgumentParser( - description=( - "Fail closed unless the PR head publishes every protected-base " - "CodeQL configuration identity for one language." - ) - ) - parser.add_argument("--repository", required=True, help="owner/name target repository") - parser.add_argument("--base-ref", required=True, help="protected base ref, e.g. refs/heads/main") - parser.add_argument("--base-sha", required=True, help="exact protected base SHA") - parser.add_argument("--head-ref", required=True, help="PR head ref, e.g. refs/pull/1/head") - parser.add_argument("--head-sha", required=True, help="exact PR head SHA") - parser.add_argument("--language", required=True, help="CodeQL language token, e.g. rust") - parser.add_argument( - "--attempts", - type=int, - default=int(os.environ.get("GHAS_CONFIG_IDENTITY_ATTEMPTS", "30")), - help="bounded poll attempts while Default setup finishes slower languages", - ) - parser.add_argument( - "--sleep-seconds", - type=float, - default=float(os.environ.get("GHAS_CONFIG_IDENTITY_SLEEP_SECONDS", "20")), - help="delay between poll attempts in seconds", - ) - return parser - - -def main(argv: Sequence[str] | None = None) -> int: - """CLI entry: wait for GHAS base/head identity continuity, then exit 0/1.""" - parser = _build_parser() - args = parser.parse_args(argv) - token = str(os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") or "").strip() - try: - wait_for_language_pairing( - repository=args.repository, - token=token, - base_ref=args.base_ref, - base_sha=args.base_sha, - head_ref=args.head_ref, - head_sha=args.head_sha, - language=args.language, - attempts=args.attempts, - sleep_seconds=args.sleep_seconds, - ) - except ConfigurationIdentityError as exc: - print(f"::error::{exc}", file=sys.stderr) - return 1 - print( - "GHAS CodeQL configuration identity is continuous for " - f"{args.language} on {args.repository} " - f"(base={args.base_sha[:12]} head={args.head_sha[:12]})." - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - exercised through ``main`` tests - raise SystemExit(main()) diff --git a/scripts/ci/codeql_sarif_gate.py b/scripts/ci/codeql_sarif_gate.py index fb751ba90d..3b232c3bdb 100644 --- a/scripts/ci/codeql_sarif_gate.py +++ b/scripts/ci/codeql_sarif_gate.py @@ -33,66 +33,19 @@ def iter_sarif_files(root: Path) -> list[Path]: return sorted(root.rglob("*.sarif")) -UNRESOLVED_RULE_LEVEL = "unresolved-rule" - - -def _component_rules(result: dict[str, Any], tool: dict[str, Any]) -> list[Any] | None: - """Return the rules of the tool component a result references (SARIF 2.1.0 §3.54). - - No ``rule.toolComponent`` means the driver. Otherwise the reference selects one of - ``tool.extensions`` by ``index``, ``guid``, or ``name``; an unmatched reference - returns ``None`` so the caller can fail closed instead of consulting the wrong - component (issue #2150). - """ - reference = result.get("rule") if isinstance(result.get("rule"), dict) else {} - component_ref = reference.get("toolComponent") - if not isinstance(component_ref, dict): - return (tool.get("driver") or {}).get("rules") or [] - extensions = [ext for ext in tool.get("extensions") or [] if isinstance(ext, dict)] - index = component_ref.get("index") - if isinstance(index, int): - if 0 <= index < len(extensions): - return extensions[index].get("rules") or [] - return None - for key in ("guid", "name"): - wanted = component_ref.get(key) - if wanted is not None: - for extension in extensions: - if extension.get(key) == wanted: - return extension.get("rules") or [] - return None - return None - - -def _rule_for_result(result: dict[str, Any], tool: dict[str, Any]) -> dict[str, Any] | None: - """Resolve the SARIF rule definition a result references, or ``None`` if it cannot be. - - Resolution order inside the referenced component: ``rule.index`` (validated - against the declared id), then id lookup (``ruleId`` / ``rule.id``), then the - legacy ``ruleIndex``. Colliding ids across components stay distinct because - lookup never leaves the referenced component. - """ - rules = _component_rules(result, tool) - if rules is None: - return None - reference = result.get("rule") if isinstance(result.get("rule"), dict) else {} - declared_ids = {str(v) for v in (result.get("ruleId"), reference.get("id")) if v} - if len(declared_ids) > 1: - return None - declared_id = next(iter(declared_ids), "") - for index in (reference.get("index"), result.get("ruleIndex")): - if isinstance(index, int): - candidate = rules[index] if 0 <= index < len(rules) else None - if not isinstance(candidate, dict): - return None - if declared_id and str(candidate.get("id") or "") != declared_id: - return None +def _rule_for_result(result: dict[str, Any], rules: list[Any]) -> dict[str, Any]: + """Resolve the SARIF rule definition referenced by a result.""" + rules_by_id = { + str(rule.get("id") or ""): rule for rule in rules if isinstance(rule, dict) + } + rule = rules_by_id.get(str(result.get("ruleId") or ""), {}) + if rule: + return rule + rule_index = result.get("ruleIndex") + if isinstance(rule_index, int) and 0 <= rule_index < len(rules): + candidate = rules[rule_index] + if isinstance(candidate, dict): return candidate - if declared_id: - for rule in rules: - if isinstance(rule, dict) and str(rule.get("id") or "") == declared_id: - return rule - return None return {} @@ -103,16 +56,11 @@ def _is_medium_plus(score: float | None, level: str, security_rule: bool) -> boo return security_rule and level in SEVERITY_LEVELS -def _finding_from_result(result: dict[str, Any], tool: dict[str, Any]) -> Finding | None: - """Build a `Finding` for one SARIF result, or None if it doesn't gate the PR. - - A result whose rule reference cannot be resolved and that carries no explicit - security-severity gates as ``unresolved-rule`` rather than passing silently. - """ +def _finding_from_result(result: dict[str, Any], rules: list[Any]) -> Finding | None: + """Build a `Finding` for one SARIF result, or None if it doesn't gate the PR.""" if not isinstance(result, dict) or result.get("suppressions"): return None - resolved = _rule_for_result(result, tool) - rule = resolved or {} + rule = _rule_for_result(result, rules) result_properties = result.get("properties") or {} rule_properties = rule.get("properties") or {} raw_score = result_properties.get("security-severity", rule_properties.get("security-severity")) @@ -123,9 +71,7 @@ def _finding_from_result(result: dict[str, Any], tool: dict[str, Any]) -> Findin level = str(result.get("level") or (rule.get("defaultConfiguration") or {}).get("level") or "none").lower() tags = {str(tag).lower() for tag in rule_properties.get("tags") or []} security_rule = "security" in tags or any(tag.startswith("external/cwe/") for tag in tags) - if resolved is None and score is None: - level = UNRESOLVED_RULE_LEVEL - elif not _is_medium_plus(score, level, security_rule): + if not _is_medium_plus(score, level, security_rule): return None physical = ((result.get("locations") or [{}])[0].get("physicalLocation") or {}) artifact = (physical.get("artifactLocation") or {}).get("uri") or "unknown" @@ -149,12 +95,12 @@ def gather_findings(root: Path) -> tuple[list[Finding], int, int]: for path in paths: payload = json.loads(path.read_text(encoding="utf-8")) for run in payload.get("runs") or []: - tool = run.get("tool") if isinstance(run.get("tool"), dict) else {} + rules = ((run.get("tool") or {}).get("driver") or {}).get("rules") or [] for result in run.get("results") or []: if not isinstance(result, dict): continue total_results += 1 - finding = _finding_from_result(result, tool) + finding = _finding_from_result(result, rules) if finding is not None: findings.append(finding) return findings, total_results, len(paths) diff --git a/scripts/ci/contextual_orchestrator_review_policy.py b/scripts/ci/contextual_orchestrator_review_policy.py index 241f84bef7..e609e67ff3 100644 --- a/scripts/ci/contextual_orchestrator_review_policy.py +++ b/scripts/ci/contextual_orchestrator_review_policy.py @@ -466,23 +466,14 @@ def build_zdr_prioritized_catalog( def _load_zdr_endpoints(path: str | None) -> frozenset[str]: - """Load exact provider/model keys from an OpenRouter ZDR feed file. - - Each feed row's ``model_id`` is the discovery slug contextual-orchestrator - reports as ``model`` (e.g. ``"inclusionai/ling-3.0-flash-vl:free"``); - ``model_name`` is a human display string (e.g. "DeepSeek: DeepSeek V4.1 - Flash") and is never used to build a route key. ``provider_name`` is the - feed's serving-provider label (e.g. "Novita"). Rows missing either - ``model_id`` or ``provider_name`` are skipped; there is no fallback to - the display name. - """ + """Load exact provider/model keys from an OpenRouter ZDR feed file.""" if not path: return frozenset() payload = json.loads(Path(path).read_text(encoding="utf-8")) keys: set[str] = set() for endpoint in payload.get("data", []): provider = endpoint.get("provider_name") - model = endpoint.get("model_id") + model = endpoint.get("model_name") if provider and model: keys.add(_route_key(str(provider), str(model))) keys.add(_route_key("openrouter", str(model))) diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 3c2a1b51b9..38d9551a32 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -14,7 +14,7 @@ # (fail-closed zero-cost) pool. set -euo pipefail -ORCHESTRATOR_PIN_SHA="${ORCHESTRATOR_PIN_SHA:-767e67fbc6b881a452761f32abb69b9971b9b03b}" +ORCHESTRATOR_PIN_SHA="${ORCHESTRATOR_PIN_SHA:-414f22973658c4ddc3d4320fcf7acd9b4e8ba991}" ORCHESTRATOR_GIT_URL="${ORCHESTRATOR_GIT_URL:-https://github.com/ContextualWisdomLab/contextual-orchestrator.git}" # The Strix gate and Noema SSRF guard accept this one process-local origin. # Keep it fixed so an environment override cannot create an unvalidated sidecar. diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index 948c80cd01..ae40b85ac4 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -29,7 +29,6 @@ API_TIMEOUT_SECONDS = 30 CANCELLATION_POLL_ATTEMPTS = 6 CANCELLATION_POLL_INTERVAL_SECONDS = 1.0 -QUEUE_START_RACE_RE = re.compile(r"\bHTTP\s*409\b") class CoalescingRefused(RuntimeError): @@ -374,24 +373,7 @@ def _fetch_run(repo: str, run_id: int) -> dict[str, Any]: def _cancel_run(repo: str, run_id: int) -> None: """Cancel one run and prove GitHub reached its terminal cancelled state.""" - cancel_args = ["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{run_id}/cancel"] - try: - _run_json(cancel_args) - except RuntimeError as exc: - # GitHub can race a queued run into startup between the candidate - # fetch and POST, returning HTTP 409 instead of accepting cancel. - # Re-read the authoritative run state; never turn an unknown - # cancellation error into a successful result or another mutation. - if not QUEUE_START_RACE_RE.search(str(exc)): - raise - current = _fetch_run(repo, run_id) - if current.get("status") == "completed" and current.get("conclusion") == "cancelled": - return - if current.get("status") != "queued": - raise CoalescingRefused(f"workflow run {run_id} is no longer queued after HTTP 409") from exc - raise CoalescingRefused( - f"workflow run {run_id} remained queued after HTTP 409; preserving it" - ) from exc + _run_json(["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{run_id}/cancel"]) for attempt in range(CANCELLATION_POLL_ATTEMPTS): run_data = _fetch_run(repo, run_id) if run_data.get("status") == "completed" and run_data.get("conclusion") == "cancelled": diff --git a/scripts/ci/materialize_base_rust_dependencies.py b/scripts/ci/materialize_base_rust_dependencies.py deleted file mode 100644 index 6feec9cedf..0000000000 --- a/scripts/ci/materialize_base_rust_dependencies.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -"""Materialize an offline Cargo vendor directory from a validated base commit. - -The sandboxed coverage-measurement container runs with ``--network=none`` (see -``opencode-review-dispatch.yml``'s "Measure test and docstring evidence" step). Python and -JavaScript dependencies already have an offline path through -``materialize_base_python_requirements.py`` and ``materialize_base_javascript_packages.py``, which -run here -- on the runner, before the network-isolated container exists -- and bake a base-pinned -dependency closure into the trusted image. Rust/Cargo had no equivalent: every coverage run against -a Rust crate (directly via ``cargo llvm-cov``, or indirectly through a PyO3/maturin extension a -Python test suite imports) needed ``index.crates.io``, which the offline container can never reach. -Confirmed live across ``fast-mlsirm`` PRs #1868-#1892 (dispatch runs 34884397167 and siblings): -``cargo llvm-cov`` failed with ``Could not resolve host: index.crates.io``, and the generic Python -pytest path failed at collection with ``ImportError: cannot import name '_core'`` because nothing in -the sandbox ever builds the compiled extension. Both surfaced as a generic "Coverage gate: failure", -indistinguishable from a real regression in the pull request. - -This mirrors the Python materializer's trust model: only the validated base commit's Cargo -manifests are read (never the pull request's), and vendoring itself uses Cargo's own built-in -per-package checksum verification (every ``[[package]]`` entry in a lock file carries a -``checksum``), so no separate hash-pin parser is needed the way ``requirements*.txt`` needed one. -""" - -from __future__ import annotations - -import argparse -import json -import pathlib -import re -import subprocess -import sys -import tempfile - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - exercised by Python 3.10 CI. - import tomli as tomllib - - -SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") -CARGO_VENDOR_TIMEOUT_SECONDS = 600 - - -def _git(repo_root: pathlib.Path, *args: str) -> bytes: - """Run one read-only git command against the materialized repository.""" - completed = subprocess.run( - ["git", "-C", str(repo_root), *args], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if completed.returncode != 0: - stderr = completed.stderr.decode("utf-8", errors="replace").strip() - raise RuntimeError(f"git {args[0]} failed: {stderr}") - return completed.stdout - - -def _regular_cargo_blob_paths(repo_root: pathlib.Path, base_sha: str) -> list[str]: - """Return tracked, non-symlink ``Cargo.toml``/``Cargo.lock`` paths at ``base_sha``.""" - entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha) - paths: list[str] = [] - for raw_entry in entries.split(b"\0"): - if not raw_entry: - continue - metadata, separator, raw_path = raw_entry.partition(b"\t") - if not separator: - raise RuntimeError("git ls-tree returned a malformed entry") - fields = metadata.split() - if len(fields) != 3: - raise RuntimeError("git ls-tree returned malformed metadata") - mode, object_type, _object_id = ( - field.decode("ascii", errors="strict") for field in fields - ) - path = raw_path.decode("utf-8", errors="surrogateescape") - candidate = pathlib.PurePosixPath(path) - if ( - object_type != "blob" - or not mode.startswith("100") - or candidate.is_absolute() - or ".." in candidate.parts - ): - continue - if candidate.name in ("Cargo.toml", "Cargo.lock"): - paths.append(path) - return sorted(paths) - - -def _is_workspace_manifest(content: bytes) -> bool: - """Return whether one ``Cargo.toml`` blob declares a ``[workspace]`` table.""" - try: - parsed = tomllib.loads(content.decode("utf-8")) - except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc: - raise RuntimeError("could not parse a tracked base Cargo.toml") from exc - return "workspace" in parsed - - -def _select_vendor_root( - repo_root: pathlib.Path, base_sha: str, cargo_paths: list[str] -) -> str | None: - """Return the single directory ``cargo vendor`` should be invoked from, or ``None``. - - Only one topology is supported: a single Cargo workspace root, or a single standalone - crate with no workspace. Any other shape (independent multi-root layouts) fails closed - rather than guess which root's lock file is authoritative -- the same restraint - ``materialize_base_python_requirements.py`` takes with uv workspaces. - """ - manifests = [path for path in cargo_paths if path.endswith("Cargo.toml")] - locks = {path.rsplit("/", 1)[0] if "/" in path else "." for path in cargo_paths if path.endswith("Cargo.lock")} - workspace_dirs: list[str] = [] - for manifest_path in manifests: - content = _git(repo_root, "show", f"{base_sha}:{manifest_path}") - if _is_workspace_manifest(content): - manifest_dir = manifest_path.rsplit("/", 1)[0] if "/" in manifest_path else "." - workspace_dirs.append(manifest_dir) - - if len(workspace_dirs) == 1: - (root,) = workspace_dirs - if root in locks: - return root - raise RuntimeError( - f"base Cargo workspace root {root} has no sibling Cargo.lock" - ) - if len(workspace_dirs) > 1: - raise RuntimeError( - "base tree declares more than one Cargo workspace root; " - "Rust dependency vendoring needs exactly one" - ) - if len(locks) == 1: - (root,) = locks - manifest_path = "Cargo.toml" if root == "." else f"{root}/Cargo.toml" - if manifest_path in manifests: - return root - raise RuntimeError(f"base Cargo.lock at {root} has no sibling Cargo.toml") - if len(locks) > 1: - raise RuntimeError( - "base tree has more than one Cargo.lock with no single workspace root; " - "Rust dependency vendoring needs exactly one" - ) - return None - - -def _placeholder_target_paths(manifest_content: bytes) -> list[str]: - """Return package target source paths a manifest needs present to parse. - - ``cargo vendor`` never compiles anything -- it only resolves and downloads the locked - dependency graph -- but Cargo still refuses to *parse* a package manifest whose declared - targets do not exist on disk. Real source is never required for vendoring, so this returns - the conventional and any explicitly declared target paths; the caller writes empty - placeholder files at each one. - """ - try: - parsed = tomllib.loads(manifest_content.decode("utf-8")) - except (UnicodeDecodeError, tomllib.TOMLDecodeError): - return [] - if "package" not in parsed: - return [] - paths = {"src/lib.rs", "src/main.rs"} - lib_path = parsed.get("lib", {}).get("path") if isinstance(parsed.get("lib"), dict) else None - if isinstance(lib_path, str): - paths.add(lib_path) - for bin_target in parsed.get("bin", []) if isinstance(parsed.get("bin"), list) else []: - bin_path = bin_target.get("path") if isinstance(bin_target, dict) else None - if isinstance(bin_path, str): - paths.add(bin_path) - return sorted(paths) - - -def _reconstruct_base_tree( - repo_root: pathlib.Path, base_sha: str, cargo_paths: list[str], work_dir: pathlib.Path -) -> None: - """Write every tracked base Cargo manifest into ``work_dir`` at its repository path. - - Each package manifest's conventional/declared target paths also get an empty placeholder - file -- see :func:`_placeholder_target_paths` for why real source is never needed here. - """ - for path in cargo_paths: - content = _git(repo_root, "show", f"{base_sha}:{path}") - destination = work_dir / pathlib.Path(*pathlib.PurePosixPath(path).parts) - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(content) - if destination.name == "Cargo.toml": - for target_path in _placeholder_target_paths(content): - target_destination = destination.parent / pathlib.Path( - *pathlib.PurePosixPath(target_path).parts - ) - target_destination.parent.mkdir(parents=True, exist_ok=True) - if not target_destination.exists(): - target_destination.write_bytes(b"") - - -def _run_cargo_vendor( - manifest_path: pathlib.Path, vendor_dir: pathlib.Path -) -> subprocess.CompletedProcess[bytes]: - """Run ``cargo vendor`` for one reconstructed base manifest and return the result.""" - return subprocess.run( - [ - "cargo", - "vendor", - "--manifest-path", - str(manifest_path), - "--versioned-dirs", - str(vendor_dir), - ], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=CARGO_VENDOR_TIMEOUT_SECONDS, - ) - - -def materialize( - repo_root: pathlib.Path, - base_sha: str, - output_dir: pathlib.Path, - *, - vendor_dir_for_config: str | None = None, -) -> list[str]: - """Vendor the base commit's Cargo dependency closure into ``output_dir``. - - Returns the list of source-tree-relative ``Cargo.lock`` paths that were vendored. An empty - list means no Rust project (or no lock file) exists at the base commit, which is not an - error -- most repositories reviewed by this pipeline have no Rust code at all. - - ``vendor_dir_for_config`` overrides the ``directory = `` path written into - ``cargo-config.toml``. Vendoring runs on the runner (this materializer's own working - directory), but the vendored files are later copied into the trusted coverage image at a - fixed path; the emitted config must name that final in-image path, not the runner's - temporary one. - """ - if not SHA_RE.fullmatch(base_sha): - raise ValueError("base SHA must be exactly 40 hexadecimal characters") - if output_dir.exists() and output_dir.is_symlink(): - raise ValueError("output directory must not be a symlink") - output_dir.mkdir(parents=True, exist_ok=True) - - resolved_repo = repo_root.resolve() - cargo_paths = _regular_cargo_blob_paths(resolved_repo, base_sha) - vendor_root = _select_vendor_root(resolved_repo, base_sha, cargo_paths) - manifest: list[str] = [] - if vendor_root is not None: - with tempfile.TemporaryDirectory() as work_dir: - work_path = pathlib.Path(work_dir) - _reconstruct_base_tree(resolved_repo, base_sha, cargo_paths, work_path) - manifest_path = work_path / ( - "Cargo.toml" if vendor_root == "." else f"{vendor_root}/Cargo.toml" - ) - lock_path = "Cargo.lock" if vendor_root == "." else f"{vendor_root}/Cargo.lock" - vendor_dir = output_dir / "vendor" - try: - completed = _run_cargo_vendor(manifest_path, vendor_dir) - except (OSError, subprocess.TimeoutExpired) as exc: - raise RuntimeError( - f"could not run trusted cargo vendor for base manifest {lock_path}: " - f"{type(exc).__name__}" - ) from exc - if completed.returncode != 0: - stderr = completed.stderr.decode("utf-8", errors="replace") - normalized_stderr = " ".join(stderr.split()) - detail = normalized_stderr[:500] if normalized_stderr else ( - f"exit status {completed.returncode}" - ) - raise RuntimeError(f"cargo vendor failed for base lock {lock_path}: {detail}") - config_text = completed.stdout - if vendor_dir_for_config is not None: - config_text = config_text.replace( - str(vendor_dir).encode("utf-8"), - vendor_dir_for_config.encode("utf-8"), - ) - (output_dir / "cargo-config.toml").write_bytes(config_text) - manifest = [lock_path] - - (output_dir / "manifest.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - return manifest - - -def main(argv: list[str] | None = None) -> int: - """Materialize the base Cargo vendor directory and report what was selected.""" - parser = argparse.ArgumentParser() - parser.add_argument("--repo-root", required=True, type=pathlib.Path) - parser.add_argument("--base-sha", required=True) - parser.add_argument("--output-dir", required=True, type=pathlib.Path) - parser.add_argument("--vendor-dir-for-config", default=None) - args = parser.parse_args(argv) - - try: - manifest = materialize( - args.repo_root, - args.base_sha, - args.output_dir, - vendor_dir_for_config=args.vendor_dir_for_config, - ) - except (OSError, RuntimeError, ValueError) as exc: - print( - f"::error::Could not materialize base Rust dependencies: {exc}", file=sys.stderr - ) - return 1 - - if manifest: - print(f"Materialized trusted base Cargo vendor directory from {manifest[0]}.") - else: - print("No tracked Cargo.lock exists at the validated base SHA; Rust vendoring skipped.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json deleted file mode 100644 index 1026fd79a3..0000000000 --- a/scripts/ci/noema-document-reader/package-lock.json +++ /dev/null @@ -1,1315 +0,0 @@ -{ - "name": "noema-document-reader-runtime", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "noema-document-reader-runtime", - "version": "1.0.0", - "dependencies": { - "@rhwp/core": "0.7.7", - "hwp-mcp": "0.3.0" - } - }, - "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@rhwp/core": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@rhwp/core/-/core-0.7.7.tgz", - "integrity": "sha512-FHWTdOO+YPY4SSOaFrGGc98AkzrnQy+IIZYng3C00Wqg3+BcaN0uk0cYx0YS4bwtefrPsT6b15fcG6rpNU8iyw==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", - "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", - "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/hwp-mcp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/hwp-mcp/-/hwp-mcp-0.3.0.tgz", - "integrity": "sha512-+CYrAT5cKOpf6GCHyXRvw4SY/Z5GqBxO+Za92PyHCwseU8C6T3A3AR2bVv6hNQbG4SL6GJsgyfeaKjmpMut/Fw==", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "@rhwp/core": "0.7.x", - "jszip": "^3.10.1" - }, - "bin": { - "hwp-mcp": "dist/server.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/jszip": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", - "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "license": "MIT", - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/negotiator/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/zod": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.4.tgz", - "integrity": "sha512-AXSD6hvGdvRjajG/l1cC+d6IrhH+sjmPKtYeQdJIK8MFJl3LyClzS+o/YsVC+zQZPupAaeH5skwwm8YqYH7BqA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/scripts/ci/noema-document-reader/package.json b/scripts/ci/noema-document-reader/package.json deleted file mode 100644 index aa4fc0d3ff..0000000000 --- a/scripts/ci/noema-document-reader/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "noema-document-reader-runtime", - "private": true, - "version": "1.0.0", - "dependencies": { - "@rhwp/core": "0.7.7", - "hwp-mcp": "0.3.0" - } -} diff --git a/scripts/ci/noema_hwp_mcp_reader.mjs b/scripts/ci/noema_hwp_mcp_reader.mjs deleted file mode 100644 index ccd32683b7..0000000000 --- a/scripts/ci/noema_hwp_mcp_reader.mjs +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env node - -import { existsSync, readFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { join } from "node:path"; -import { pathToFileURL } from "node:url"; - -const [, , sourceRoot, filePath] = process.argv; -if (!sourceRoot || !filePath || !existsSync(sourceRoot)) { - process.stderr.write("hwp-mcp source directory and document path are required\n"); - process.exit(2); -} - -try { - const hwpPackage = JSON.parse(readFileSync(join(sourceRoot, "package.json"), "utf8")); - const require = createRequire(pathToFileURL(join(sourceRoot, "package.json"))); - const rhwpPackage = JSON.parse( - readFileSync(require.resolve("@rhwp/core/package.json"), "utf8"), - ); - if (hwpPackage.name !== "hwp-mcp" || hwpPackage.version !== "0.3.0") { - throw new Error("unexpected hwp-mcp package identity"); - } - if (rhwpPackage.name !== "@rhwp/core" || rhwpPackage.version !== "0.7.7") { - throw new Error("unexpected rhwp package identity"); - } - const documentModule = await import(pathToFileURL(join(sourceRoot, "dist/core/document.js"))); - const toolsModule = await import(pathToFileURL(join(sourceRoot, "dist/tools/read.js"))); - const document = await documentModule.openDocument(filePath); - documentModule.closeDocument(document); - const text = await toolsModule.readHwp({ file_path: filePath }); - if ( - !text || - /^(?:파일 읽기 오류|File not found|텍스트 추출 오류|text extraction error)/i.test(text) - ) { - throw new Error("hwp-mcp returned an extraction error"); - } - process.stdout.write(`${text}\n`); -} catch (error) { - process.stderr.write("hwp-mcp/rhwp document extraction failed\n"); - process.exit(1); -} diff --git a/scripts/ci/noema_review_document.py b/scripts/ci/noema_review_document.py deleted file mode 100644 index 17d3ca603c..0000000000 --- a/scripts/ci/noema_review_document.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Extract bounded review text from office documents without model access. - -DOCX is a ZIP/XML container whose text can be read with the Python standard -library. HWP and HWPX stay delegated to the reviewed hwp-mcp/rhwp reader; this -module only supplies a temporary local file and validates the subprocess -contract. -""" - -from __future__ import annotations - -import io -import os -import subprocess -import tempfile -import zipfile -from pathlib import PurePosixPath - -from defusedxml import ElementTree as ET -from defusedxml.common import DefusedXmlException - - -MAX_DOCUMENT_BYTES = 8 * 1024 * 1024 -MAX_DOCUMENT_ZIP_ENTRIES = 2048 -MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES = 64 * 1024 * 1024 -MAX_DOCUMENT_TEXT_BYTES = 256 * 1024 -HWP_READER_ENV = "NOEMA_HWP_MCP_SOURCE" -HWP_READER_TIMEOUT_SECONDS = 45 - -W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" -M_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" -W = f"{{{W_NS}}}" -M = f"{{{M_NS}}}" - - -class DocumentReadError(RuntimeError): - """A document could not be converted to bounded review text.""" - - -def extract_review_document(path: str, raw: bytes) -> str: - """Return text for one supported document path or fail closed. - - The input bytes are obtained from the exact GitHub content ref by the - caller. HWP/HWPX bytes are never decoded as UTF-8 and never sent to an - external service; the configured reader runs as a local subprocess only. - """ - if len(raw) > MAX_DOCUMENT_BYTES: - raise DocumentReadError("document exceeds the bounded 8 MiB review input") - suffix = PurePosixPath(path).suffix.lower() - if suffix == ".docx": - return _extract_docx(raw) - if suffix in {".hwp", ".hwpx"}: - return _extract_hwp_with_reviewed_reader(path, raw) - raise DocumentReadError(f"unsupported review document format: {suffix or ''}") - - -def _extract_docx(raw: bytes) -> str: - """Extract paragraphs, tables, and Office Math text from one DOCX.""" - try: - with zipfile.ZipFile(io.BytesIO(raw)) as archive: - infos = archive.infolist() - if len(infos) > MAX_DOCUMENT_ZIP_ENTRIES: - raise DocumentReadError("DOCX archive has too many entries") - if ( - sum(info.file_size for info in infos) - > MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES - ): - raise DocumentReadError( - "DOCX archive exceeds the bounded unpacked size" - ) - try: - document_xml = archive.read("word/document.xml") - except KeyError as exc: - raise DocumentReadError( - "DOCX archive has no word/document.xml" - ) from exc - except DocumentReadError: - raise - except (zipfile.BadZipFile, OSError, ValueError) as exc: - raise DocumentReadError("DOCX archive is malformed") from exc - - try: - root = ET.fromstring(document_xml) - except (ET.ParseError, DefusedXmlException) as exc: - raise DocumentReadError("DOCX document.xml is malformed") from exc - - body = root.find(f"{W}body") - if body is None: - raise DocumentReadError("DOCX document.xml has no document body") - - sections: list[str] = [] - table_number = 0 - for child in body: - if child.tag == f"{W}p": - text = _paragraph_text(child) - if text: - sections.append(text) - elif child.tag == f"{W}tbl": - table_number += 1 - table = _table_markdown(child, table_number) - if table: - sections.append(table) - - text = "\n\n".join(sections).strip() - if not text: - raise DocumentReadError("DOCX contains no readable text") - return _bounded_text(text) - - -def _paragraph_text(paragraph: ET.Element) -> str: - """Keep visible Word text, tabs, breaks, and Office Math runs.""" - parts: list[str] = [] - for element in paragraph.iter(): - if element.tag in {f"{W}t", f"{W}instrText", f"{M}t"}: - parts.append(element.text or "") - elif element.tag == f"{W}tab": - parts.append("\t") - elif element.tag in {f"{W}br", f"{W}cr"}: - parts.append("\n") - return "".join(parts).strip() - - -def _table_markdown(table: ET.Element, table_number: int) -> str: - """Render a DOCX table as bounded, reviewer-readable Markdown.""" - rows: list[list[str]] = [] - for row in table.findall(f"{W}tr"): - cells: list[str] = [] - for cell in row.findall(f"{W}tc"): - paragraphs = [_paragraph_text(p) for p in cell.findall(f".//{W}p")] - value = "\n".join(text for text in paragraphs if text).strip() - cells.append(value.replace("|", "\\|")) - if cells: - rows.append(cells) - if not rows: - return "" - - width = max(len(row) for row in rows) - normalized = [row + [""] * (width - len(row)) for row in rows] - lines = [f"### Table {table_number} ({len(normalized)} rows x {width} columns)"] - lines.append("| " + " | ".join(normalized[0]) + " |") - lines.append("| " + " | ".join("---" for _ in range(width)) + " |") - lines.extend("| " + " | ".join(row) + " |" for row in normalized[1:]) - return "\n".join(lines) - - -def _extract_hwp_with_reviewed_reader(path: str, raw: bytes) -> str: - """Delegate HWP/HWPX parsing to the reviewed hwp-mcp/rhwp source tree.""" - source = os.environ.get(HWP_READER_ENV, "").strip() - if not source: - raise DocumentReadError( - "reviewed hwp-mcp/rhwp reader is not configured; " - f"set {HWP_READER_ENV} to its trusted source directory" - ) - reader = os.path.join(os.path.dirname(__file__), "noema_hwp_mcp_reader.mjs") - with tempfile.NamedTemporaryFile( - prefix="noema-document-", suffix=PurePosixPath(path).suffix - ) as handle: - handle.write(raw) - handle.flush() - try: - completed = subprocess.run( - ["node", reader, source, handle.name], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - shell=False, - timeout=HWP_READER_TIMEOUT_SECONDS, - ) - except subprocess.TimeoutExpired as exc: - raise DocumentReadError( - "reviewed hwp-mcp/rhwp reader timed out after " - f"{HWP_READER_TIMEOUT_SECONDS} seconds" - ) from exc - except OSError as exc: - raise DocumentReadError( - "reviewed hwp-mcp/rhwp reader could not start" - ) from exc - if completed.returncode != 0: - raise DocumentReadError( - f"reviewed hwp-mcp/rhwp reader failed (exit {completed.returncode})" - ) - if len(completed.stdout) > MAX_DOCUMENT_TEXT_BYTES: - raise DocumentReadError( - "reviewed hwp-mcp/rhwp reader exceeded the bounded output" - ) - try: - text = completed.stdout.decode("utf-8").strip() - except UnicodeDecodeError as exc: - raise DocumentReadError( - "reviewed hwp-mcp/rhwp reader returned non-UTF-8 text" - ) from exc - if not text: - raise DocumentReadError("reviewed hwp-mcp/rhwp reader returned empty text") - return _bounded_text(text) - - -def _bounded_text(text: str) -> str: - """Bound reader output before it enters the review prompt.""" - encoded = text.encode("utf-8") - if len(encoded) <= MAX_DOCUMENT_TEXT_BYTES: - return text - clipped = encoded[:MAX_DOCUMENT_TEXT_BYTES].decode("utf-8", errors="ignore") - omitted = len(encoded) - len(clipped.encode("utf-8")) - return f"{clipped}\n[document text truncated; {omitted} bytes omitted]" - - -def _main() -> int: - """Provide a local, byte-safe smoke-test CLI for one document.""" - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("path") - args = parser.parse_args() - try: - with open(args.path, "rb") as handle: - text = extract_review_document(args.path, handle.read()) - except (OSError, DocumentReadError) as exc: - print(str(exc), file=os.sys.stderr) - return 1 - print(text) - return 0 - - -if __name__ == "__main__": - raise SystemExit(_main()) diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index c8709304fc..5ab7e830f3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -6,7 +6,6 @@ import argparse import ast import base64 -import binascii import hashlib import http.client import ipaddress @@ -21,11 +20,9 @@ import urllib.parse import urllib.request from collections.abc import Sequence -from pathlib import PurePosixPath from typing import Any from scripts.ci.opencode_review_normalize_output import changed_file_is_material -from scripts.ci.noema_review_document import DocumentReadError, extract_review_document PRIMARY_REVIEW_AUTHORS = { @@ -63,12 +60,6 @@ MAX_THREAD_BODY_CHARS = 1200 MAX_ALLOWED_LOCATIONS_JSON_BYTES = 32 * 1024 MAX_HTTP_ERROR_BODY_BYTES = 16 * 1024 -# ADR-0031: transport-capacity class after gateway failover (not caller retries). -TRANSPORT_CAPACITY_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) -MAX_TRANSPORT_REDISPATCH_ATTEMPTS = 2 -TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS = 60 -TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS = 180 -TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS = 300 DIFF_HUNK_RE = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@") SAFE_MODEL_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$") @@ -215,103 +206,6 @@ class NoemaModelOutputError(RuntimeError): class NoemaTransportError(RuntimeError): """Raised when the bounded review transport cannot produce usable evidence.""" - def __init__( - self, - message: str, - *, - capacity_unavailable: bool = False, - http_status: int | None = None, - provider_attempt_count: int | None = None, - retry_after_seconds: int | None = None, - ) -> None: - """Record typed transport metadata without embedding secrets in attributes.""" - super().__init__(message) - self.capacity_unavailable = capacity_unavailable - self.http_status = http_status - self.provider_attempt_count = provider_attempt_count - self.retry_after_seconds = retry_after_seconds - - -def is_provider_capacity_http_status(status: int | None) -> bool: - """Return whether an HTTP status is a post-failover provider-capacity class.""" - return type(status) is int and status in TRANSPORT_CAPACITY_HTTP_STATUSES - - -def parse_http_retry_after_seconds(headers: Any) -> int | None: - """Return a whole-seconds Retry-After delay capped for continuation scheduling. - - Only the delta-seconds form is accepted. HTTP-date values and out-of-range - numbers record nothing so a hostile header cannot invent an unbounded wait. - """ - get_header = getattr(headers, "get", None) - if not callable(get_header): - return None - try: - raw = get_header("Retry-After") - except Exception: # noqa: BLE001 - hostile header mappings are not evidence - return None - if not isinstance(raw, str) or not raw.strip().isdecimal(): - return None - seconds = int(raw.strip()) - if seconds < 1 or seconds > TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS: - return None - return seconds - - -def transport_redispatch_delay_seconds( - *, - transport_retry_attempt: int, - head_sha: str, - retry_after_seconds: int | None = None, -) -> int | None: - """Return the post-failure scheduling delay, or None when the re-dispatch bound is spent. - - ``transport_retry_attempt`` is the number of automatic capacity re-dispatches - already performed for this head (0 on the first failure). Prefer a capped - gateway ``Retry-After`` when present; otherwise use deterministic jitter in - ``[TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS, TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS]`` - keyed by head SHA and attempt so concurrent failures do not stampede. - """ - if transport_retry_attempt < 0 or transport_retry_attempt >= MAX_TRANSPORT_REDISPATCH_ATTEMPTS: - return None - if retry_after_seconds is not None: - if ( - type(retry_after_seconds) is int - and 1 <= retry_after_seconds <= TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS - ): - return retry_after_seconds - return None - digest = hashlib.sha256( - f"{head_sha.strip().lower()}:{transport_retry_attempt}".encode("utf-8") - ).digest() - span = ( - TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS - TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + 1 - ) - offset = int.from_bytes(digest[:4], "big") % span - return TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + offset - - -def current_transport_retry_attempt() -> int: - """Parse the workflow-supplied automatic re-dispatch counter, failing closed to 0.""" - raw = (os.environ.get("NOEMA_TRANSPORT_RETRY_ATTEMPT") or "0").strip() - if not raw.isdecimal(): - return 0 - value = int(raw) - return value if value <= 64 else 0 - - -def append_github_output(values: dict[str, str]) -> None: - """Append allowlisted step outputs when running under GitHub Actions.""" - path = (os.environ.get("GITHUB_OUTPUT") or "").strip() - if not path or not values: - return - with open(path, "a", encoding="utf-8") as handle: - for key, value in values.items(): - if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key): - continue - if any(ch in value for ch in ("\n", "\r", "\0")): - continue - handle.write(f"{key}={value}\n") def _stable_failure_diagnostic(exc: BaseException) -> str: @@ -842,7 +736,7 @@ def fetch_changed_files(repo: str, number: int) -> list[tuple[str, str]]: def fetch_file_content_at_ref(repo: str, path: str, ref: str) -> str: - """Fetch one repository file at an exact Git ref through GitHub.""" + """Fetch one repository text file at an exact Git ref through GitHub.""" encoded_path = urllib.parse.quote(path, safe="/") encoded_ref = urllib.parse.quote(ref, safe="") content = run( @@ -857,17 +751,7 @@ def fetch_file_content_at_ref(repo: str, path: str, ref: str) -> str: compact = "".join(content.split()) if not compact: return "" - try: - raw = base64.b64decode(compact, validate=True) - except (binascii.Error, ValueError) as exc: - raise RuntimeError("GitHub content response contained malformed base64") from exc - suffix = PurePosixPath(path).suffix.lower() - if suffix in {".docx", ".hwp", ".hwpx"}: - try: - return extract_review_document(path, raw) - except DocumentReadError as exc: - raise RuntimeError(f"document extraction failed: {exc}") from exc - return raw.decode("utf-8", errors="replace") + return base64.b64decode(compact).decode("utf-8", errors="replace") def fetch_merge_base_sha(repo: str, base_sha: str, head_sha: str) -> str: @@ -1469,7 +1353,6 @@ def _extract_http_error_telemetry(exc: urllib.error.HTTPError) -> dict[str, str if terminal_reason is not None: telemetry["terminal_reason"] = terminal_reason if isinstance(attempts, list) and attempts and len(attempts) <= 64: - telemetry["provider_attempt_count"] = len(attempts) last_attempt = attempts[-1] if isinstance(last_attempt, dict): provider_name = _safe_model_identifier(last_attempt.get("provider_name")) @@ -1496,7 +1379,6 @@ def _extract_http_error_served_model(exc: urllib.error.HTTPError) -> str | None: def _format_gateway_error_telemetry(telemetry: dict[str, str | int]) -> str: """Format only allowlisted scalar receipt fields for a public Actions log.""" ordered_keys = ( - "provider_attempt_count", "provider_name", "upstream_phase", "attempt_number", @@ -1752,12 +1634,8 @@ def call_llm( validate_substantive_verdict(verdict, diff, changed_paths) except (RuntimeError, urllib.error.URLError, http.client.HTTPException, OSError) as exc: gateway_telemetry: dict[str, str | int] = {} - http_status: int | None = None - retry_after_seconds: int | None = None if isinstance(exc, urllib.error.HTTPError): active_phase = "response_error" - http_status = exc.code if type(exc.code) is int else None - retry_after_seconds = parse_http_retry_after_seconds(exc.headers) gateway_telemetry = _extract_http_error_telemetry(exc) model_value = gateway_telemetry.get("served_model") served_model = model_value if isinstance(model_value, str) else None @@ -1765,32 +1643,16 @@ def call_llm( current_failure = _stable_failure_diagnostic(exc) model_note = served_model or "unknown" gateway_note = _format_gateway_error_telemetry(gateway_telemetry) - capacity_unavailable = is_provider_capacity_http_status(http_status) - capacity_note = ( - " outcome=provider_capacity_unavailable" - if capacity_unavailable - else "" - ) print( f"::warning::Noema gateway attempt outcome=failed phase={active_phase} " f"duration={elapsed:.1f}s served_model={model_note}; " "caller attempts=1 (gateway owns repair/failover)." - + capacity_note + (f" gateway {gateway_note}" if gateway_note else "") ) suffix = ( f"; caller attempts=1, duration={elapsed:.1f}s, " f"phase={active_phase}, served_model={model_note}" + (f", gateway {gateway_note}" if gateway_note else "") - + ( - ", outcome=provider_capacity_unavailable" - if capacity_unavailable - else "" - ) - ) - provider_attempt_count = gateway_telemetry.get("provider_attempt_count") - attempt_count = ( - provider_attempt_count if type(provider_attempt_count) is int else None ) if isinstance(exc, NoemaModelOutputError): raise NoemaModelOutputError( @@ -1798,11 +1660,7 @@ def call_llm( ) from None if isinstance(exc, (urllib.error.URLError, http.client.HTTPException, OSError)): raise NoemaTransportError( - f"Noema gateway transport failed: {type(exc).__name__}: {current_failure}{suffix}", - capacity_unavailable=capacity_unavailable, - http_status=http_status, - provider_attempt_count=attempt_count, - retry_after_seconds=retry_after_seconds, + f"Noema gateway transport failed: {type(exc).__name__}: {current_failure}{suffix}" ) from exc raise RuntimeError( f"Noema review failed closed: {current_failure}{suffix}" diff --git a/scripts/ci/noema_review_handoff.py b/scripts/ci/noema_review_handoff.py index a112e1e225..29f6142a98 100644 --- a/scripts/ci/noema_review_handoff.py +++ b/scripts/ci/noema_review_handoff.py @@ -26,7 +26,7 @@ from redact_sensitive_log import redact_text -REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") GH_COMMAND_TIMEOUT_SECONDS = 60.0 MAX_TRANSIENT_BACKOFF_MULTIPLIER = 4 diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index a8d5419c7e..9657bd2d4d 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -26,7 +26,7 @@ DEFAULT_ORGANIZATION = "ContextualWisdomLab" -ORGANIZATION_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +ORGANIZATION_RE = re.compile(r"^[A-Za-z0-9_.-]+$") ENTRYPOINT_MARKER = "# cwl-org-commercial-entrypoint: v1" CENTRAL_REPOSITORY = f"{DEFAULT_ORGANIZATION}/.github" CENTRAL_REPAIR_EVENT = "pr-review-fix-scheduler" diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index b53a68c6ae..33e58ed876 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -5,47 +5,16 @@ bounded UTF-8 file content through the GitHub REST API, then rejects active Nginx runtime artifacts while allowing documentation, license text, and source-level negative test fixtures. - -Issue #2193 -- declared research/data artifact paths: a consumer repository may -declare literal path prefixes (``ARTIFACT_PATH_DECLARATION_PATH``) that hold -binary research or data artefacts not shaped like documentation (raw response -workbooks, SPSS ``.sav`` files, serialized model objects, compressed numeric -arrays). That declaration is resolved *only* from the pull request's base ref, -never its head, so a pull request cannot self-authorize admission of its own -binary by adding or widening the declaration in the same diff -- see -``_load_artifact_path_declaration`` and ``evaluate_pull_request``'s ``base_ref`` -parameter. The declaration replaces only the path-shape test -(`_is_known_documentation_path`'s equivalent for declared prefixes); it never -substitutes for content evidence, and an active-runtime-named file -(`_runtime_path_rule`) stays rejected inside a declared prefix exactly as inside -``docs/`` today. - -Suffix decision: most research-data formats (``.xlsx``, ``.sav``, ``.rds``, -``.npz``, ...) have no entry in ``BINARY_DOCUMENT_MAGIC``, which only knows -``.hwpx``/``.pdf``/``.png``. Rather than grow that registry for every such -format, a file under a declared prefix whose suffix has no magic entry is -admitted on the stricter complement of the UTF-8 decode this module already -performs for every ordinarily-scanned file: no diff patch available, *and* the -fetched bytes fail to decode as UTF-8. That keeps the module's central -guarantee honest -- a file that decodes as valid UTF-8 is never treated as a -binary artifact, since scanning exactly that content is what this module -exists to do -- while still admitting genuinely opaque research binaries -without maintaining an open-ended magic-byte catalog. A suffix that *does* -have a magic entry keeps that entry's existing structural evidence check -(``_is_complete_png``, ``_is_complete_hwpx``, or the raw magic-prefix check for -``.pdf``) even under a declared prefix. """ from __future__ import annotations import argparse import base64 -import io import json import os import re import sys -import zipfile import zlib from dataclasses import dataclass from pathlib import PurePosixPath @@ -56,13 +25,8 @@ MAX_FILE_BYTES = 1_048_576 MAX_RESPONSE_BYTES = 16_777_216 -REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") SHA_RE = re.compile(r"^[0-9a-f]{40}$") -# A base ref threaded into evaluate_pull_request may be either a branch name -# (e.g. "main", "release/2026.09") or a commit SHA -- whatever the calling -# workflow already has on the pull_request event without new permissions. -# Bounded charset/length, no ".." traversal, and no leading/trailing "/". -BASE_REF_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,253}[A-Za-z0-9])?$") GITHUB_API_ORIGIN = "https://api.github.com" DOCUMENT_SUFFIXES = frozenset({".md", ".mdx", ".rst", ".adoc", ".txt"}) @@ -76,7 +40,6 @@ # (this org's own "attach the relevant paper PDF" convention) for a reason # that has nothing to do with the Nginx runtime policy this module enforces. BINARY_DOCUMENT_MAGIC = { - ".hwpx": (b"PK\x03\x04",), ".pdf": (b"%PDF-",), ".png": (b"\x89PNG\r\n\x1a\n",), } @@ -86,20 +49,6 @@ DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) DOCUMENTATION_ROOT_NAMES = frozenset({"readme", "changelog", "changes"}) -# Consumer-repository declaration of research/data artifact path prefixes -# (issue #2193). Resolved *only* from the pull request's base ref -- never -# its head -- so a PR cannot self-authorize admission of its own binary by -# adding or widening the declaration in the same diff; see -# `_load_artifact_path_declaration`. -ARTIFACT_PATH_DECLARATION_PATH = ".github/edge-policy-artifact-paths.txt" -# Parsing-safety bounds only, not a product limit on how many research/data -# artifact locations a repository may declare: they exist so a pathological -# declaration file cannot make policy evaluation walk an unbounded number of -# entries, or match against an unbounded path depth, for every changed file -# in every pull request the required workflow evaluates. -MAX_DECLARED_ARTIFACT_PREFIXES = 64 -MAX_DECLARED_ARTIFACT_PREFIX_DEPTH = 8 - RUNTIME_PATH_NAMES = frozenset({ "dockerfile", "containerfile", @@ -199,19 +148,6 @@ class ContentSizeExceededError(PolicyError): """ -class ArtifactDeclarationNotFoundError(PolicyError): - """Raised when the GitHub API reports no resource at a requested path. - - Distinguished from every other ``PolicyError`` cause via the source - HTTP 404 status specifically, so ``_load_artifact_path_declaration`` can - treat "no declaration file at this base ref" as the repository simply - not having opted into the research/data artifact-path exemption -- - identical to today's behavior -- while every other evidence failure - (malformed JSON, an invalid declared entry, a transient network error) - still fails the whole check closed exactly like any other ``PolicyError``. - """ - - OpenJson = Callable[[str, str], object] @@ -236,85 +172,6 @@ def _is_known_documentation_path(pure: PurePosixPath) -> bool: ) -def _parse_artifact_path_declaration(text: str) -> tuple[str, ...]: - """Parse a declared research/data artifact path-prefix list. - - One explicit path prefix per non-blank line; no globs or wildcards -- - every entry names a literal directory prefix, matched segment-wise by - ``_declared_prefix_for_path``. Rejects an absolute path, a ``..`` - traversal component, an empty entry, or a bare ``.``/``/``. Bounded by - ``MAX_DECLARED_ARTIFACT_PREFIXES`` (entry count) and - ``MAX_DECLARED_ARTIFACT_PREFIX_DEPTH`` (path segment depth) -- both are - parsing-safety bounds, not a product limit on how many locations a - repository may declare. A malformed entry always raises ``PolicyError`` - naming the offending entry; this never falls back to admitting nothing - or everything. - """ - - prefixes: list[str] = [] - for raw_line in text.splitlines(): - entry = raw_line.strip() - if not entry: - continue - if len(prefixes) >= MAX_DECLARED_ARTIFACT_PREFIXES: - raise PolicyError( - f"Artifact path declaration exceeds {MAX_DECLARED_ARTIFACT_PREFIXES} entries at {entry!r}" - ) - if entry.startswith("/") or entry in (".", "/"): - raise PolicyError(f"Artifact path declaration entry must be a relative path prefix: {entry!r}") - if any(char in entry for char in "*?[]"): - raise PolicyError(f"Artifact path declaration entry must not use glob syntax: {entry!r}") - parts = PurePosixPath(entry).parts - if not parts or any(part in ("", ".", "..") for part in parts): - raise PolicyError(f"Artifact path declaration entry is malformed: {entry!r}") - if len(parts) > MAX_DECLARED_ARTIFACT_PREFIX_DEPTH: - raise PolicyError( - f"Artifact path declaration entry exceeds depth {MAX_DECLARED_ARTIFACT_PREFIX_DEPTH}: {entry!r}" - ) - prefixes.append(entry) - return tuple(prefixes) - - -def _declared_prefix_for_path(path: str, declared_prefixes: Sequence[str]) -> str | None: - """Return the first declared prefix *path* falls under, else ``None``. - - Matched by path segment, not raw string prefix, so a declared ``local`` - does not also match an unrelated ``local-cache`` directory. - """ - - parts = PurePosixPath(path).parts - for prefix in declared_prefixes: - prefix_parts = PurePosixPath(prefix).parts - if parts[: len(prefix_parts)] == prefix_parts: - return prefix - return None - - -def _load_artifact_path_declaration( - *, api_url: str, repository: str, base_ref: str, token: str, opener: OpenJson -) -> tuple[str, ...]: - """Load and parse the research/data artifact path declaration at *base_ref*. - - Resolved **only** from the pull request's base ref -- never its head -- - so a pull request cannot self-authorize admission of its own binary by - adding or widening the declaration in the same diff: a PR that adds or - widens the declaration gets no benefit from it until that change is - itself reviewed and merged into the base branch. - - A declaration file absent from the base ref (HTTP 404) is not a policy - failure: it means the repository has not opted in, identical to today's - behavior before this feature existed. Every other failure to load or - parse it (malformed API shape, an invalid declared entry) still fails - the whole check closed via ``PolicyError``. - """ - - try: - content = _load_file_content(api_url, repository, ARTIFACT_PATH_DECLARATION_PATH, base_ref, token, opener) - except ArtifactDeclarationNotFoundError: - return () - return _parse_artifact_path_declaration(content) - - def _is_documentation_or_source_fixture(path: str) -> bool: """Return whether *path* is prose, license text, or scanner source fixture. @@ -354,7 +211,7 @@ def _is_documentation_or_source_fixture(path: str) -> bool: return False -def _is_binary_documentation_asset(changed: ChangedFile, declared_prefixes: Sequence[str] = ()) -> bool: +def _is_binary_documentation_asset(changed: ChangedFile) -> bool: """Return whether *changed* is a plausibly binary documentation asset. This is only the cheap, patch-presence pre-filter: GitHub's changed-files @@ -365,34 +222,16 @@ def _is_binary_documentation_asset(changed: ChangedFile, declared_prefixes: Sequ still confirm this with ``_binary_documentation_evidence_confirms`` before trusting it; a caller without one (this module's own unit tests calling this function directly) is only checking the necessary condition. - - *declared_prefixes* (issue #2193) is the base-ref-only research/data - artifact declaration: it replaces ONLY this function's path-shape test, - never the content evidence a caller still confirms below. A file whose - suffix is a recognized ``BINARY_DOCUMENT_MAGIC`` format (``.hwpx``/ - ``.pdf``/``.png``) is admitted under a declared prefix on the exact same - format evidence documentation paths already require. A file whose - suffix has no magic entry at all (research formats such as ``.xlsx``, - ``.sav``, ``.rds``, ``.npz`` have none) can ONLY be admitted through a - declared prefix, and only on the stricter "no patch + genuinely - non-UTF-8 bytes" evidence ``_binary_documentation_evidence_confirms`` - checks for that case -- a file that decodes as valid UTF-8 must never - be treated as a binary artifact, since that is exactly the case this - scanner exists to inspect. """ - if changed.patch_available or _runtime_path_rule(changed.path) is not None: + if changed.patch_available: return False pure = PurePosixPath(changed.path) - suffix = pure.suffix.lower() - declared_prefix = _declared_prefix_for_path(changed.path, declared_prefixes) - if suffix in BINARY_DOCUMENT_MAGIC: - return ( - _is_known_documentation_path(pure) - or (suffix == ".hwpx" and "evidence" in (part.lower() for part in pure.parts)) - or declared_prefix is not None - ) - return declared_prefix is not None + return ( + pure.suffix.lower() in BINARY_DOCUMENT_MAGIC + and _is_known_documentation_path(pure) + and _runtime_path_rule(changed.path) is None + ) def _runtime_path_rule(path: str) -> str | None: @@ -465,10 +304,6 @@ def _github_open_json(url: str, token: str) -> object: with github_opener.open(request, timeout=30) as response: payload = response.read(MAX_RESPONSE_BYTES + 1) except (HTTPError, URLError, TimeoutError) as exc: - if isinstance(exc, HTTPError) and exc.code == 404: - raise ArtifactDeclarationNotFoundError( - f"GitHub API reported no resource for policy evidence at {url}" - ) from exc raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc if len(payload) > MAX_RESPONSE_BYTES: raise PolicyError("GitHub API policy response exceeded the bounded response size") @@ -544,16 +379,10 @@ def _load_raw_file_bytes(api_url: str, repository: str, path: str, head_sha: str ``encoding: "none"`` with an accurate ``size`` and no ``content`` at all. Both are treated as the same size-exceeded evidence; every other response shape still fails closed. - - *head_sha* is also reused, unchanged, to fetch a base-ref-scoped file - (the issue #2193 artifact-path declaration): any git ref -- a commit SHA - or a branch name -- works here, so it is URL-encoded rather than assumed - to be the hex-only pull-request head SHA ``evaluate_pull_request`` - validates separately. """ encoded_path = quote(path, safe="/") - url = f"{api_url}/repos/{repository}/contents/{encoded_path}?ref={quote(head_sha, safe='')}" + url = f"{api_url}/repos/{repository}/contents/{encoded_path}?ref={head_sha}" payload = opener(url, token) if not isinstance(payload, Mapping): raise PolicyError(f"GitHub content evidence for {path} is not an object") @@ -615,16 +444,6 @@ def _binary_documentation_evidence_confirms( malformed API response, corrupt base64, a declared size that does not match the decoded bytes) propagates and fails the whole check closed, same as for any other file that needs scanning. - - A suffix with no ``BINARY_DOCUMENT_MAGIC`` entry only reaches this - branch when ``_is_binary_documentation_asset`` admitted it through a - declared research/data artifact prefix (issue #2193), which has no - magic byte to check. That case is confirmed by the strict complement of - the UTF-8 decode ``_load_file_content`` uses for every ordinarily-scanned - file: bytes that fail to decode as UTF-8 are genuinely binary evidence; - bytes that decode cleanly are never admitted this way, so a valid-UTF-8 - file cannot be mistaken for a binary artifact merely by sitting under a - declared prefix -- it still reaches the normal content scan instead. """ try: @@ -634,54 +453,9 @@ def _binary_documentation_evidence_confirms( suffix = PurePosixPath(changed.path).suffix.lower() if suffix == ".png": return _is_complete_png(raw) - if suffix == ".hwpx": - return _is_complete_hwpx(raw) - if suffix not in BINARY_DOCUMENT_MAGIC: - try: - raw.decode("utf-8") - except UnicodeDecodeError: - return True - return False return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) -def _is_complete_hwpx(raw: bytes) -> bool: - """Confirm a bounded HWPX container without extracting document content. - - Require an unprefixed ZIP, its exact end record, unique members, and the - stored HWPX MIME marker plus an unencrypted package manifest. This is - format evidence, not XML document validation or malware inspection. - """ - if not raw.startswith(BINARY_DOCUMENT_MAGIC[".hwpx"][0]): - return False - try: - with zipfile.ZipFile(io.BytesIO(raw)) as archive: - archive_entries = archive.infolist() - member_names = [member_info.filename for member_info in archive_entries] - end_offset = len(raw) - 22 - len(archive.comment) - if end_offset < 0 or raw[end_offset:end_offset + 4] != b"PK\x05\x06": - return False - if int.from_bytes(raw[end_offset + 20:end_offset + 22], "little") != len(archive.comment): - return False - if not archive_entries or archive_entries[0].header_offset != 0: - return False - if member_names[0] != "mimetype" or len(member_names) != len(set(member_names)): - return False - mimetype_info = archive.getinfo("mimetype") - manifest_info = archive.getinfo("Contents/content.hpf") - expected_mimetype = b"application/hwp+zip" - if mimetype_info.flag_bits & 1 or manifest_info.flag_bits & 1: - return False - if mimetype_info.compress_type != zipfile.ZIP_STORED or mimetype_info.file_size != len(expected_mimetype): - return False - if manifest_info.is_dir() or manifest_info.file_size == 0: - return False - with archive.open(mimetype_info) as mimetype_stream: - return mimetype_stream.read(len(expected_mimetype) + 1) == expected_mimetype - except (KeyError, UnicodeError, OSError, ValueError, NotImplementedError, zipfile.BadZipFile): - return False - - def _png_unfilter_row(filtered: bytes, previous: bytes, filter_type: int, bytes_per_pixel: int) -> bytes: """Reconstruct one PNG scanline for bounded indexed-pixel validation.""" @@ -826,20 +600,18 @@ def _is_complete_png(raw: bytes) -> bool: return False -def _needs_content_scan(changed: ChangedFile, declared_prefixes: Sequence[str] = ()) -> bool: +def _needs_content_scan(changed: ChangedFile) -> bool: """Return whether a changed final file can carry an active edge runtime. A claimed binary documentation asset (``_is_binary_documentation_asset``) exempts here on the cheap, offline pre-filter alone; ``evaluate_pull_request`` never actually relies on that -- it runs ``_binary_documentation_evidence_confirms`` - for that case before this function is even consulted. *declared_prefixes* - is the base-ref-only research/data artifact declaration from issue - #2193; it is passed straight through to ``_is_binary_documentation_asset``. + for that case before this function is even consulted. """ if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): return False - if _is_binary_documentation_asset(changed, declared_prefixes): + if _is_binary_documentation_asset(changed): return False if not changed.patch_available: return True @@ -861,20 +633,9 @@ def evaluate_pull_request( head_sha: str, event_action: str, token: str, - base_ref: str | None = None, opener: OpenJson = _github_open_json, ) -> tuple[Violation, ...]: - """Evaluate one pull request without checking out or executing its content. - - *base_ref* (issue #2193) is an optional pull-request base ref -- a - branch name or a commit SHA, whatever the calling workflow already has - on the ``pull_request`` event without new permissions. When given, the - research/data artifact path declaration at ``ARTIFACT_PATH_DECLARATION_PATH`` - is resolved from that ref (never from ``head_sha``) and its declared - prefixes are admitted on the same content-evidence terms as documentation - paths. Omitting it (the default) reproduces this module's exact prior - behavior: no declared prefixes, no declaration fetch at all. - """ + """Evaluate one pull request without checking out or executing its content.""" if event_action == "closed": return () @@ -886,18 +647,9 @@ def evaluate_pull_request( raise PolicyError("Pull-request head SHA is malformed") if not token: raise PolicyError("GITHUB_TOKEN is required for policy evidence") - if base_ref is not None and (".." in base_ref or not BASE_REF_RE.fullmatch(base_ref)): - raise PolicyError("Pull-request base ref is malformed") - resolved_api_url = api_url.rstrip("/") - declared_prefixes: tuple[str, ...] = () - if base_ref is not None: - declared_prefixes = _load_artifact_path_declaration( - api_url=resolved_api_url, repository=repository, base_ref=base_ref, token=token, opener=opener - ) - changed_files = _load_changed_files(resolved_api_url, repository, pull_request, token, opener) + changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) violations: list[Violation] = [] for changed in changed_files: - declared_prefix = _declared_prefix_for_path(changed.path, declared_prefixes) # A claimed binary documentation asset gets its own network-verified # check ahead of _needs_content_scan's patch-presence-only signal: # a missing patch does not by itself prove binary content (GitHub @@ -907,39 +659,23 @@ def evaluate_pull_request( # content genuinely exceeds the Contents API's size ceiling. A # removed file has no head content to fetch at all -- _needs_content_scan # already special-cases this the same way for every other file. - if changed.status != "removed" and _is_binary_documentation_asset(changed, declared_prefixes): + if changed.status != "removed" and _is_binary_documentation_asset(changed): if _binary_documentation_evidence_confirms( changed, - api_url=resolved_api_url, + api_url=api_url.rstrip("/"), repository=repository, head_sha=head_sha, token=token, opener=opener, ): - if declared_prefix is not None: - # Names the reviewed declaration this admission relied - # on, so a reviewer can trace it back to the base ref. - print(_declared_prefix_notice(changed.path, declared_prefix, base_ref)) continue - elif not _needs_content_scan(changed, declared_prefixes): + elif not _needs_content_scan(changed): continue - content = _load_file_content(resolved_api_url, repository, changed.path, head_sha, token, opener) + content = _load_file_content(api_url.rstrip("/"), repository, changed.path, head_sha, token, opener) violations.extend(scan_content(changed.path, content)) return tuple(violations) -def _declared_prefix_notice(path: str, prefix: str, base_ref: str) -> str: - """Render one bounded GitHub workflow notice for a declared-prefix admission.""" - - escaped_path = path.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A").replace(",", "%2C") - message = ( - f"CWL edge policy admitted a research/data artifact under declared prefix " - f"'{prefix}' (declaration read from base ref '{base_ref}')" - ) - message = message.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") - return f"::notice file={escaped_path}::{message}" - - def _annotation(violation: Violation) -> str: """Render one bounded GitHub workflow command annotation.""" @@ -958,15 +694,6 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--head-sha", required=True) parser.add_argument("--event-action", required=True) parser.add_argument("--api-url", default=GITHUB_API_ORIGIN) - parser.add_argument( - "--base-ref", - default=None, - help=( - "Pull-request base ref (branch name or commit SHA) used to resolve the " - "issue #2193 research/data artifact path declaration. Omit to disable " - "that declaration entirely (identical to this module's prior behavior)." - ), - ) return parser @@ -983,7 +710,6 @@ def main(argv: Sequence[str] | None = None, environ: Mapping[str, str] | None = head_sha=args.head_sha, event_action=args.event_action, token=env.get("GITHUB_TOKEN", ""), - base_ref=args.base_ref, ) except PolicyError as exc: print(f"::error::Pingora edge policy could not establish complete evidence: {exc}") diff --git a/scripts/ci/pr_auto_rebase.py b/scripts/ci/pr_auto_rebase.py index d28afd0ed8..c0f04c3aa2 100755 --- a/scripts/ci/pr_auto_rebase.py +++ b/scripts/ci/pr_auto_rebase.py @@ -80,7 +80,7 @@ ) -REPO_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") OPEN_PRS_PAGE_SIZE = 25 LABELS_PAGE_SIZE = 50 DEFAULT_MAX_PER_RUN = 10 diff --git a/scripts/ci/pr_review_autofix_context.py b/scripts/ci/pr_review_autofix_context.py index 51a36a53f6..cc7b6fb003 100755 --- a/scripts/ci/pr_review_autofix_context.py +++ b/scripts/ci/pr_review_autofix_context.py @@ -19,7 +19,7 @@ from scripts.ci.pr_review_fix_scheduler import current_head_failed_checks -REPO_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") _AUTOFIX_CONTROL_PREFIXES = (".github/", "scripts/ci/") _REPAIR_MODES = ("review", "rca", "conflict") diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py index 23c5c66a9d..bc2868c5a4 100755 --- a/scripts/ci/pr_review_fix_scheduler.py +++ b/scripts/ci/pr_review_fix_scheduler.py @@ -53,7 +53,7 @@ r"" ) -REPO_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") REPAIR_MODES = frozenset({"review", "rca", "conflict"}) AUTOFIX_RUN_NAME_RE = re.compile( r"^PR Review Autofix (?P[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)" diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 4489ee62a3..4df4dac3de 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -330,21 +330,6 @@ def live_dispatch_head_matches(repo: str, pr: dict[str, Any]) -> bool: # checks in the same operating window instead of leaving them for seven hours. DEFAULT_STALE_OPENCODE_MINUTES = 90 DEFAULT_COVERAGE_RETRY_FLOOR_MINUTES = 60 -# Derived from measured consecutive-push gaps across 4 org repositories -# (419 samples, 2026-09-17): density roughly halves right at 300s (155 -# gaps <=300s vs 31 in (300,600]), the clearest inflection point in an -# otherwise continuous, non-bimodal distribution. See -# docs/doctoring/actions-capacity-root-cause-20260917.md and the PR that -# introduced this constant for the full sample. Only takes effect when -# OPENCODE_REVIEW_COALESCE_ENABLED is set -- see -# head_stable_for_seconds() and its use in dispatch_opencode_review(). -DEFAULT_COALESCE_WINDOW_SECONDS = 300 -# Two 5-minute coalesce-tick cron periods: if no tick completed within this -# horizon, dispatch_opencode_review() fail-opens instead of deferring a head -# that is still inside the settling window. -DEFAULT_COALESCE_TICK_MAX_AGE_SECONDS = 600 -COALESCE_TICK_WORKFLOW_PATH = ".github/workflows/opencode-review-coalesce-tick.yml" -COALESCE_TICK_WORKFLOW_NAME = "OpenCode Review Coalesce Tick" DEFAULT_UPDATE_BRANCH_HEAD_POLL_ATTEMPTS = 6 DEFAULT_UPDATE_BRANCH_HEAD_POLL_SECONDS = 5.0 OPENCODE_WORKFLOW_NAMES = { @@ -359,7 +344,7 @@ def live_dispatch_head_matches(repo: str, pr: dict[str, Any]) -> bool: ACTION_REQUIRED_CONCLUSIONS = {"ACTION_REQUIRED"} GIT_REF_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") GIT_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") -GITHUB_REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +GITHUB_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") REVIEW_BODY_HEAD_SHA_RE = re.compile(r"Head SHA:\s*`([0-9a-fA-F]{40})`") CHECK_GATED_OPENCODE_CHANGE_REQUEST_MARKER = ( "OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed." @@ -1744,112 +1729,6 @@ def parse_github_datetime(value: str | None) -> datetime | None: return parsed.astimezone(timezone.utc) -def head_committed_at(pr: dict[str, Any]) -> datetime | None: - """Return the current head commit's committed timestamp, if known.""" - nodes = ((pr.get("commits") or {}).get("nodes")) or [] - if not nodes: - return None - commit = nodes[-1].get("commit") or {} - return parse_github_datetime(commit.get("committedDate")) - - -def head_stable_for_seconds( - pr: dict[str, Any], *, now: datetime | None = None -) -> float | None: - """Return how long the current head has existed, or None if unknown. - - Unknown (missing or unparseable commit timestamp) must never gate a - dispatch decision -- callers treat None as "stable" (fail open) so a - read gap here cannot silently withhold a legitimate review forever. - """ - committed_at = head_committed_at(pr) - if committed_at is None: - return None - return ((now or datetime.now(timezone.utc)) - committed_at).total_seconds() - - -def coalesce_window_seconds() -> int: - """Return the configured push-burst coalescing window in seconds.""" - raw = os.environ.get("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "") - try: - parsed = int(raw) - except ValueError: - return DEFAULT_COALESCE_WINDOW_SECONDS - return parsed if parsed >= 0 else DEFAULT_COALESCE_WINDOW_SECONDS - - -def coalesce_enabled() -> bool: - """Return whether push-burst coalescing is enabled for this invocation. - - Defaults to disabled: every existing caller (scan-pr-queue's per-push - and daily-cron invocations) keeps dispatching immediately, exactly as - today, unless this is explicitly turned on. - """ - return os.environ.get("OPENCODE_REVIEW_COALESCE_ENABLED", "").strip().lower() == "true" - - -def coalesce_tick_max_age_seconds() -> int: - """Return how recently a coalesce tick must have completed to keep deferring.""" - raw = os.environ.get("OPENCODE_REVIEW_COALESCE_TICK_MAX_AGE_SECONDS", "") - try: - parsed = int(raw) - except ValueError: - return DEFAULT_COALESCE_TICK_MAX_AGE_SECONDS - return parsed if parsed >= 0 else DEFAULT_COALESCE_TICK_MAX_AGE_SECONDS - - -def coalesce_tick_repository() -> str: - """Return the repository that hosts the scheduled coalesce tick workflow.""" - configured = (os.environ.get("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY") or "").strip() - return configured or "ContextualWisdomLab/.github" - - -def recent_coalesce_tick_completed( - repo: str, - *, - now: datetime | None = None, - max_age_seconds: int | None = None, -) -> bool: - """Return whether a coalesce tick completed within the fail-open horizon. - - The scheduled tick is the only path that dispatches synchronize-triggered - reviews once coalescing is enabled. When no tick has completed recently, - callers must fail open and dispatch immediately rather than defer forever. - """ - max_age = ( - coalesce_tick_max_age_seconds() - if max_age_seconds is None - else max_age_seconds - ) - if max_age <= 0: - return False - current = now or datetime.now(timezone.utc) - cutoff = current - timedelta(seconds=max_age) - created_filter = cutoff.strftime(">=%Y-%m-%dT%H:%M:%SZ") - tick_repo = repository_dispatch_target(validate_github_repository(repo)) - for run_data in active_workflow_runs( - tick_repo, - ("completed",), - event="schedule", - created=created_filter, - ): - if run_data.get("path") != COALESCE_TICK_WORKFLOW_PATH: - continue - # Skipped ticks (flag off, job-level gate) and cancelled/failed runs - # are not evidence that coalesce dispatch is alive — only a successful - # tick that took a runner and finished its org pass counts. - if str(run_data.get("conclusion") or "").lower() != "success": - continue - completed_at = parse_github_datetime( - run_data.get("updated_at") - or run_data.get("run_started_at") - or run_data.get("created_at") - ) - if completed_at is not None and completed_at >= cutoff: - return True - return False - - def check_run_recency_key( node: dict[str, Any], started_at: datetime | None, index: int ) -> tuple[int, datetime, int]: @@ -3052,8 +2931,6 @@ def post_update_branch_followup( return f"{head_note}; bounded admission budget is exhausted" if dispatch_result == "already_running": return f"{head_note}; same-head OpenCode workflow run is already active" - if dispatch_result == "coalescing": - return f"{head_note}; current head is within the push-burst coalescing window" return f"{head_note}; same-head Strix evidence is complete, so OpenCode review was dispatched" @@ -3809,29 +3686,6 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr the original event and leaves the review job skipped. Always use the default-branch dispatch entrypoint after same-head deduplication. """ - if coalesce_enabled(): - age_seconds = head_stable_for_seconds(pr) - window = coalesce_window_seconds() - if age_seconds is not None and age_seconds < window: - tick_recent = ( - recent_coalesce_tick_completed(coalesce_tick_repository()) - if not dry_run - else True - ) - if tick_recent: - print( - "OpenCode review dispatch coalesced: current head is " - f"{age_seconds:.0f}s old, below the {window}s push-burst " - "settling window; a later, stable-head pass will dispatch it." - ) - return "coalescing" - max_age = coalesce_tick_max_age_seconds() - print( - "OpenCode review dispatch coalesce fail-open: current head is " - f"{age_seconds:.0f}s old, below the {window}s push-burst " - "settling window, but no coalesce tick completed within " - f"{max_age}s; dispatching immediately." - ) if not dry_run: require_github_actions_control_actor("inspect-active-opencode-review") current_run_refs, stale_run_refs = active_opencode_run_refs(repo, workflow, pr) @@ -4306,12 +4160,6 @@ def dispatch_draft_review_only( "draft PR review-only dispatch; current head has completed Strix evidence; " "same-head OpenCode workflow run is already active", ) - if dispatch_result == "coalescing": - return Decision( - number, - "wait", - "draft PR review-only dispatch; current head is within the push-burst coalescing window", - ) return Decision( number, "review_dispatch", @@ -4423,12 +4271,6 @@ def inspect_pr( "wait", f"stacked PR onto {base_ref}; same-head OpenCode workflow run is already active", ) - if dispatch_result == "coalescing": - return Decision( - number, - "wait", - f"stacked PR onto {base_ref}; current head is within the push-burst coalescing window", - ) return Decision( number, "review_dispatch", @@ -4646,12 +4488,6 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio "wait", "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active", ) - if dispatch_result == "coalescing": - return decide( - "wait", - "current-head coverage evidence is complete, but the current head is within the " - "push-burst coalescing window", - ) return decide( "review_dispatch", "current-head OpenCode coverage blocker is cleared; same-head OpenCode re-dispatched", @@ -5067,12 +4903,6 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio "wait", "OpenCode review exceeded the status-check retry threshold, but a same-head workflow run is already active", ) - if dispatch_result == "coalescing": - return decide( - "wait", - "OpenCode review exceeded the status-check retry threshold, but the current head is within " - "the push-burst coalescing window", - ) return decide( "review_dispatch", f"OpenCode review exceeded {stale_opencode_minutes} minute retry threshold; same-head OpenCode re-dispatched", @@ -5123,12 +4953,6 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio "wait", "current head has completed Strix evidence; same-head OpenCode workflow run is already active", ) - if dispatch_result == "coalescing": - return decide( - "wait", - "current head has completed Strix evidence, but the current head is within the " - "push-burst coalescing window", - ) return decide( "review_dispatch", "current head has completed Strix evidence; same-head OpenCode dispatched", diff --git a/scripts/ci/reconcile_repository_labels.py b/scripts/ci/reconcile_repository_labels.py index 761cfb6749..d4585877c6 100644 --- a/scripts/ci/reconcile_repository_labels.py +++ b/scripts/ci/reconcile_repository_labels.py @@ -14,7 +14,7 @@ ORGANIZATION = "ContextualWisdomLab" -REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+$") class TaxonomyError(ValueError): diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 1570455818..36a910ffa8 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -21,7 +21,7 @@ ORGANIZATION = "ContextualWisdomLab" -REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+$") TOPIC_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,49}$") MAX_DESCRIPTION_CHARS = 350 PAGES_BASE_URL = f"https://{ORGANIZATION.casefold()}.github.io" diff --git a/scripts/ci/resolve_opencode_base_vcs_import_root.sh b/scripts/ci/resolve_opencode_base_vcs_import_root.sh deleted file mode 100755 index dcd0e8bc36..0000000000 --- a/scripts/ci/resolve_opencode_base_vcs_import_root.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# Resolve the single trusted Python import root for an immutable VCS checkout. -# -# Used by the OpenCode coverage tool image while materializing base VCS -# dependencies into opencode-base-vcs-dependencies.pth. The coverage builder -# already authenticated the repository, fetched an exact commit, and stripped -# .git; this helper only admits conventional source layouts and rejects -# ambiguous, namespaced, linked, compiled, or installed trees. -# -# Usage: -# resolve_opencode_base_vcs_import_root.sh -# -# On success, prints the python_root directory (parent that belongs on -# PYTHONPATH / .pth) to stdout. On failure, prints a diagnostic to stderr and -# exits non-zero with the same messages the trusted Dockerfile historically -# emitted for ContextualWisdomLab/.github#2157. -set -eu - -destination=${1:?destination is required} -import_name=${2:?import_name is required} -repository=${3:?repository is required} - -import_root='' -python_root='' -candidate_count=0 -for candidate in \ - "$destination/python/$import_name" \ - "$destination/python/$import_name.py" \ - "$destination/src/$import_name" \ - "$destination/src/$import_name.py" \ - "$destination/$import_name" \ - "$destination/$import_name.py"; do - if [ -e "$candidate" ] || [ -L "$candidate" ]; then - import_root="$candidate" - candidate_count=$((candidate_count + 1)) - fi -done -if [ "$candidate_count" -ne 1 ]; then - printf 'locked VCS source %s has a missing or ambiguous import root for %s\n' \ - "$repository" "$import_name" >&2 - exit 1 -fi -if [ -L "$import_root" ] \ - || { [ -d "$import_root" ] \ - && { [ ! -f "$import_root/__init__.py" ] \ - || [ -L "$import_root/__init__.py" ]; }; }; then - printf 'locked VCS source %s has a namespace or linked import root for %s\n' \ - "$repository" "$import_name" >&2 - exit 1 -fi -if find "$destination" -type l -print -quit | grep -q .; then - printf 'locked VCS source %s contains a symbolic-link layout\n' \ - "$repository" >&2 - exit 1 -fi -if find "$destination" -type f \ - \( -name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.dylib' \) \ - -print -quit | grep -q .; then - printf 'locked VCS source %s contains a compiled extension\n' \ - "$repository" >&2 - exit 1 -fi -if find "$destination" -type d \ - \( -name '*.dist-info' -o -name '*.egg-info' \) \ - -print -quit | grep -q .; then - printf 'locked VCS source %s contains installed distribution metadata\n' \ - "$repository" >&2 - exit 1 -fi -case "$import_root" in - "$destination/python/"*) python_root="$destination/python" ;; - "$destination/src/"*) python_root="$destination/src" ;; - *) python_root="$destination" ;; -esac -printf '%s\n' "$python_root" diff --git a/scripts/ci/review_admission_controller.py b/scripts/ci/review_admission_controller.py index dab99d6ae1..b8e7c208ab 100644 --- a/scripts/ci/review_admission_controller.py +++ b/scripts/ci/review_admission_controller.py @@ -12,7 +12,7 @@ from dataclasses import asdict, dataclass from pathlib import Path -REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") SHA_RE = re.compile(r"^[0-9a-f]{40}$") COMPONENT_ORDER = {"opencode": 0, "noema": 1, "strix": 2} ADMISSION_PERMISSIONS = ("contents: read", "pull-requests: read") diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index b0376c0822..9bca4a1522 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -240,6 +240,7 @@ def _probe_isolation_capability(backend: str) -> None: text=True, timeout=10, check=False, + shell=False, ) except (OSError, subprocess.TimeoutExpired) as exc: raise RuntimeError(f"bubblewrap capability probe could not run: {exc}") from exc diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py deleted file mode 100644 index 7319040df2..0000000000 --- a/scripts/ci/strix_evidence_binding.py +++ /dev/null @@ -1,756 +0,0 @@ -#!/usr/bin/env python3 -"""Bind Strix PR findings and remediation claims to authenticated evidence. - -Issue #2159: a PR security verdict must distinguish an exact base→head delta -finding from debt found in unchanged protected-base source. Findings attributed -to the PR delta must map to the authenticated changed-file inventory (including -renames) and, when a line is claimed, to a changed hunk. Unchanged paths are -published as ``repository_baseline`` or ``context_dependency`` evidence — never -described as introduced by the PR. - -Issue #2168: remediation prose must fail closed when ``apply_patch`` (or an -equivalent edit tool) misses the materialized scan workspace. A report may not -claim a fix was applied unless the changed bytes are re-read from that workspace -and the expected diff is present. Source-repository mutation requires an exact -commit receipt; isolated sandbox edits stay labeled as scan-workspace only. -""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from collections.abc import Callable, Mapping, Sequence -from dataclasses import asdict, dataclass -from enum import Enum -from pathlib import Path -from typing import Any -from urllib.error import HTTPError, URLError -from urllib.parse import urlsplit -from urllib.request import HTTPRedirectHandler, Request, build_opener - - -FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$") -HUNK_HEADER_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") -APPLY_PATCH_FAILURE_RE = re.compile( - r"(?:WorkspaceReadNotFoundError|ApplyPatchFileNotFoundError|" - r"apply_patch\s+missing\s+file|" - r"file\s+not\s+found:\s*/workspace/)", - re.IGNORECASE, -) -ALREADY_APPLIED_RE = re.compile( - r"(?:already\s+applied|fix\s+applied|syntax[- ]verified|" - r"remediation\s+(?:was\s+)?(?:already\s+)?applied)", - re.IGNORECASE, -) -SAFE_PATH_RE = re.compile(r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$))[A-Za-z0-9_./ \[\]@+-]+$") -MAX_CHANGED_FILES = 3_000 -MAX_PAGES = 31 -GITHUB_API_AUTHORITY = "api.github.com" - - -class EvidenceScope(str, Enum): - """Provenance label for one Strix source finding.""" - - PR_DELTA = "pr_delta" - REPOSITORY_BASELINE = "repository_baseline" - CONTEXT_DEPENDENCY = "context_dependency" - UNMAPPED = "unmapped" - - -class RemediationState(str, Enum): - """Machine-checkable remediation progress for one finding.""" - - FINDING_CONFIRMED = "finding_confirmed" - FIX_PROPOSED = "fix_proposed" - FIX_APPLIED_IN_SCAN_WORKSPACE = "fix_applied_in_scan_workspace" - FIX_VALIDATED = "fix_validated" - FIX_COMMITTED_TO_SOURCE = "fix_committed_to_source" - REMEDIATION_FAILED = "remediation_failed" - - -class EvidenceBindingError(ValueError): - """Raised when authenticated Strix evidence cannot be established.""" - - -class _RejectRedirects(HTTPRedirectHandler): - """Prevent authenticated GitHub REST requests from creating redirect requests.""" - - def redirect_request( - self, - _request: Request, - _file_pointer: Any, - _code: int, - _message: str, - _headers: Any, - _new_url: str, - ) -> None: - """Refuse every redirect so bearer headers never cross the reviewed authority.""" - return None - - -_GITHUB_API_OPENER = build_opener(_RejectRedirects()) -OpenJson = Callable[[str, str], Any] - - -@dataclass(frozen=True) -class PullRequestBinding: - """Authenticated repository/PR/base/head tuple for finding attribution.""" - - repository: str - pull_request: int - state: str - base_ref: str - base_sha: str - head_sha: str - - def require_live_open(self) -> None: - """Fail closed unless the binding names an open PR with full SHAs.""" - - if not isinstance(self.repository, str) or "/" not in self.repository: - raise EvidenceBindingError("repository must be owner/name") - if not isinstance(self.pull_request, int) or self.pull_request <= 0: - raise EvidenceBindingError("pull_request must be a positive integer") - if self.state != "open": - raise EvidenceBindingError("pull request must be live and open") - if not isinstance(self.base_ref, str) or not self.base_ref.strip(): - raise EvidenceBindingError("base_ref is required") - if not FULL_SHA_RE.fullmatch(self.base_sha): - raise EvidenceBindingError("base_sha must be a full 40-character commit SHA") - if not FULL_SHA_RE.fullmatch(self.head_sha): - raise EvidenceBindingError("head_sha must be a full 40-character commit SHA") - if self.base_sha == self.head_sha: - raise EvidenceBindingError("base_sha and head_sha must differ") - - -@dataclass(frozen=True) -class ChangedPath: - """One authenticated GitHub changed-file row with optional hunk lines.""" - - path: str - status: str - previous_path: str | None = None - changed_lines: frozenset[int] = frozenset() - patch_available: bool = True - - -@dataclass(frozen=True) -class FindingLocation: - """A source location claimed by a Strix finding.""" - - path: str - start_line: int | None = None - end_line: int | None = None - - -@dataclass(frozen=True) -class FindingScopeVerdict: - """Classification of one finding against the authenticated PR delta.""" - - scope: EvidenceScope - path: str - reason: str - - -@dataclass(frozen=True) -class ToolEvent: - """One scanner tool outcome that must constrain remediation claims.""" - - tool: str - success: bool - target_path: str - detail: str = "" - - -@dataclass(frozen=True) -class RemediationVerdict: - """Fail-closed remediation state derived from tools and workspace bytes.""" - - state: RemediationState - reason: str - allows_already_applied_claim: bool - - -def parse_changed_lines_from_patch(patch: str) -> frozenset[int]: - """Return 1-based head-side line numbers touched by a unified diff patch.""" - - lines: set[int] = set() - current: int | None = None - for raw in patch.splitlines(): - match = HUNK_HEADER_RE.match(raw) - if match: - start = int(match.group(1)) - count = int(match.group(2) or "1") - current = None if count == 0 else start - continue - if current is None: - continue - if raw.startswith("+") and not raw.startswith("+++"): - lines.add(current) - current += 1 - elif raw.startswith("-") and not raw.startswith("---"): - continue - elif raw.startswith("\\"): - continue - else: - current += 1 - return frozenset(lines) - - -def _require_safe_relative_path(path: str) -> str: - """Reject absolute, empty, and traversal paths before attribution.""" - - cleaned = path.strip() - if not cleaned or not SAFE_PATH_RE.fullmatch(cleaned): - raise EvidenceBindingError(f"finding path is unsafe: {path!r}") - return cleaned - - -def load_changed_paths_from_github( - api_url: str, - repository: str, - pull_request: int, - token: str, - opener: OpenJson | None = None, -) -> tuple[ChangedPath, ...]: - """Materialize the complete GitHub changed-file inventory with renames.""" - - open_json = opener or default_github_opener - files: list[ChangedPath] = [] - for page in range(1, MAX_PAGES + 1): - url = ( - f"{api_url.rstrip('/')}/repos/{repository}/pulls/{pull_request}/files" - f"?per_page=100&page={page}" - ) - payload = open_json(url, token) - if not isinstance(payload, list): - raise EvidenceBindingError("GitHub changed-file evidence is not a JSON array") - for item in payload: - if not isinstance(item, Mapping): - raise EvidenceBindingError("GitHub changed-file entry is not an object") - path = item.get("filename") - status = item.get("status") - raw_patch = item.get("patch") - previous = item.get("previous_filename") - if not isinstance(path, str) or not path or not isinstance(status, str): - raise EvidenceBindingError("GitHub changed-file entry has invalid fields") - if previous is not None and not isinstance(previous, str): - raise EvidenceBindingError("GitHub renamed entry has invalid previous_filename") - patch = "" if raw_patch is None else raw_patch - if not isinstance(patch, str): - raise EvidenceBindingError("GitHub changed-file patch must be a string or null") - files.append( - ChangedPath( - path=_require_safe_relative_path(path), - status=status, - previous_path=( - _require_safe_relative_path(previous) if previous else None - ), - changed_lines=( - parse_changed_lines_from_patch(patch) if raw_patch is not None else frozenset() - ), - patch_available=raw_patch is not None, - ) - ) - if len(files) > MAX_CHANGED_FILES: - raise EvidenceBindingError( - f"GitHub changed-file pagination exceeded {MAX_CHANGED_FILES} files" - ) - if len(payload) < 100: - return tuple(files) - raise EvidenceBindingError( # pragma: no cover - page cap is unreachable while MAX_CHANGED_FILES holds - f"GitHub changed-file pagination exceeded {MAX_CHANGED_FILES} files" - ) - - -def _require_github_api_url(url: str) -> str: - """Reject any REST target outside canonical HTTPS ``api.github.com`` authority.""" - - try: - parsed = urlsplit(url) - except ValueError as exc: - raise EvidenceBindingError( - "GitHub API URL must use canonical https://api.github.com authority" - ) from exc - if ( - parsed.scheme != "https" - or parsed.netloc != GITHUB_API_AUTHORITY - or not parsed.path.startswith("/") - or parsed.fragment - ): - raise EvidenceBindingError( - "GitHub API URL must use canonical https://api.github.com authority" - ) - return url - - -def default_github_opener(url: str, token: str) -> Any: - """Fetch one canonical GitHub API JSON document without redirects.""" - - if not token: - raise EvidenceBindingError("GitHub token is required for changed-file evidence") - url = _require_github_api_url(url) - request = Request( - url, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "contextualwisdomlab-strix-evidence-binding", - }, - method="GET", - ) - try: - with _GITHUB_API_OPENER.open(request, timeout=30) as response: - payload = response.read() - except HTTPError as exc: - raise EvidenceBindingError( - f"GitHub changed-file request failed with HTTP {exc.code}" - ) from exc - except URLError as exc: - raise EvidenceBindingError( - f"GitHub changed-file request failed: {type(exc).__name__}" - ) from exc - try: - return json.loads(payload.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError) as exc: - raise EvidenceBindingError("GitHub changed-file response is not JSON") from exc - - -def changed_path_index(changed_paths: Sequence[ChangedPath]) -> dict[str, ChangedPath]: - """Index changed paths by current and previous (rename) names.""" - - index: dict[str, ChangedPath] = {} - for entry in changed_paths: - index[entry.path] = entry - if entry.previous_path: - index[entry.previous_path] = entry - return index - - -def require_matching_report_head( - binding: PullRequestBinding, - report_head_sha: str | None, -) -> None: - """Reject stale-head reports that do not match the live PR head.""" - - binding.require_live_open() - if report_head_sha is None or not FULL_SHA_RE.fullmatch(report_head_sha): - raise EvidenceBindingError("report head SHA is missing or malformed") - if report_head_sha != binding.head_sha: - raise EvidenceBindingError( - "stale-head report: report head SHA does not match live PR head" - ) - - -def require_matching_report_base( - binding: PullRequestBinding, - report_base_sha: str | None, -) -> None: - """Reject stacked-base reports that do not match the authenticated base.""" - - binding.require_live_open() - if report_base_sha is None or not FULL_SHA_RE.fullmatch(report_base_sha): - raise EvidenceBindingError("report base SHA is missing or malformed") - if report_base_sha != binding.base_sha: - raise EvidenceBindingError( - "stacked-base report: report base SHA does not match authenticated base" - ) - - -def classify_finding_scope( - binding: PullRequestBinding, - changed_paths: Sequence[ChangedPath], - location: FindingLocation, - *, - context_dependency_paths: frozenset[str] = frozenset(), - report_head_sha: str | None = None, - report_base_sha: str | None = None, -) -> FindingScopeVerdict: - """Classify one finding as PR-delta, baseline, context, or unmapped.""" - - binding.require_live_open() - if not changed_paths and not context_dependency_paths: - raise EvidenceBindingError( - "authenticated changed-file inventory could not be established" - ) - if report_head_sha is not None: - require_matching_report_head(binding, report_head_sha) - if report_base_sha is not None: - require_matching_report_base(binding, report_base_sha) - - try: - path = _require_safe_relative_path(location.path) - except EvidenceBindingError: - return FindingScopeVerdict( - scope=EvidenceScope.UNMAPPED, - path=location.path, - reason="finding path provenance could not be established", - ) - - index = changed_path_index(changed_paths) - entry = index.get(path) - if entry is not None: - if location.start_line is None: - return FindingScopeVerdict( - scope=EvidenceScope.PR_DELTA, - path=entry.path, - reason="finding path is in the authenticated changed-file inventory", - ) - if location.start_line <= 0: - return FindingScopeVerdict( - scope=EvidenceScope.UNMAPPED, - path=entry.path, - reason="line-level evidence must use a positive line number", - ) - end_line = location.end_line if location.end_line is not None else location.start_line - if end_line < location.start_line: - return FindingScopeVerdict( - scope=EvidenceScope.UNMAPPED, - path=entry.path, - reason="finding line range is inverted", - ) - if not entry.patch_available: - # Truncated GitHub patches still prove the path changed; line - # membership cannot be denied, so path-level PR-delta stands. - return FindingScopeVerdict( - scope=EvidenceScope.PR_DELTA, - path=entry.path, - reason="changed path has no inline patch; path-level PR-delta attribution stands", - ) - claimed = set(range(location.start_line, end_line + 1)) - if claimed & set(entry.changed_lines): - return FindingScopeVerdict( - scope=EvidenceScope.PR_DELTA, - path=entry.path, - reason="finding line intersects an authenticated changed hunk", - ) - return FindingScopeVerdict( - scope=EvidenceScope.REPOSITORY_BASELINE, - path=entry.path, - reason=( - "path changed in the PR but the claimed line is outside every " - "authenticated changed hunk" - ), - ) - - if path in context_dependency_paths: - return FindingScopeVerdict( - scope=EvidenceScope.CONTEXT_DEPENDENCY, - path=path, - reason="finding is in unchanged dependency/context closure, not the PR delta", - ) - - return FindingScopeVerdict( - scope=EvidenceScope.REPOSITORY_BASELINE, - path=path, - reason="finding path is base-identical across the authenticated PR tuple", - ) - - -def detect_apply_patch_failures(log_text: str) -> tuple[ToolEvent, ...]: - """Extract apply_patch / workspace-miss failures from a Strix execution log.""" - - events: list[ToolEvent] = [] - for line in log_text.splitlines(): - if not APPLY_PATCH_FAILURE_RE.search(line): - continue - target = "unknown" - path_match = re.search( - r"(?:file not found:\s*|missing file:\s*|apply_patch missing file:\s*)" - r"(/workspace/\S+|\S+)", - line, - re.IGNORECASE, - ) - if path_match: - target = path_match.group(1).rstrip(".,;") - events.append( - ToolEvent( - tool="apply_patch", - success=False, - target_path=target, - detail=line.strip(), - ) - ) - return tuple(events) - - -def workspace_contains_expected_diff( - workspace_root: Path, - relative_path: str, - expected_snippet: str, -) -> bool: - """Return whether the exact scan workspace file contains the expected bytes.""" - - if not expected_snippet: - return False - try: - safe = _require_safe_relative_path(relative_path) - except EvidenceBindingError: - return False - candidate = workspace_root / safe - if candidate.is_symlink(): - return False - try: - resolved_root = workspace_root.resolve(strict=True) - resolved = candidate.resolve(strict=False) - resolved.relative_to(resolved_root) - except (ValueError, FileNotFoundError, OSError): - return False - if not candidate.is_file(): - return False - try: - text = candidate.read_text(encoding="utf-8", errors="replace") - except OSError: - return False - return expected_snippet in text - - -def classify_remediation( - *, - finding_confirmed: bool, - fix_proposed: bool, - tool_events: Sequence[ToolEvent], - workspace_root: Path | None, - relative_path: str | None, - expected_snippet: str | None, - source_commit_sha: str | None, - report_claims_already_applied: bool, -) -> RemediationVerdict: - """Derive remediation state; never mark applied after a failed edit tool.""" - - failures = [event for event in tool_events if not event.success] - if failures: - if report_claims_already_applied: - return RemediationVerdict( - state=RemediationState.REMEDIATION_FAILED, - reason=( - "report claims a fix was applied but apply_patch/edit failed " - f"({failures[0].detail or failures[0].target_path})" - ), - allows_already_applied_claim=False, - ) - return RemediationVerdict( - state=RemediationState.REMEDIATION_FAILED, - reason=f"edit tool failed: {failures[0].detail or failures[0].target_path}", - allows_already_applied_claim=False, - ) - - if source_commit_sha is not None: - if not FULL_SHA_RE.fullmatch(source_commit_sha): - raise EvidenceBindingError("source commit receipt must be a full SHA") - return RemediationVerdict( - state=RemediationState.FIX_COMMITTED_TO_SOURCE, - reason="exact source commit receipt is present", - allows_already_applied_claim=True, - ) - - if ( - workspace_root is not None - and relative_path is not None - and expected_snippet is not None - and workspace_contains_expected_diff(workspace_root, relative_path, expected_snippet) - ): - return RemediationVerdict( - state=RemediationState.FIX_APPLIED_IN_SCAN_WORKSPACE, - reason="expected diff bytes are present in the exact scan workspace", - allows_already_applied_claim=True, - ) - - if report_claims_already_applied: - return RemediationVerdict( - state=RemediationState.REMEDIATION_FAILED, - reason=( - "report claims a fix was applied without workspace-byte proof or " - "source commit receipt" - ), - allows_already_applied_claim=False, - ) - - if fix_proposed: - return RemediationVerdict( - state=RemediationState.FIX_PROPOSED, - reason="a fix was proposed but not proven applied in the scan workspace", - allows_already_applied_claim=False, - ) - - if finding_confirmed: - return RemediationVerdict( - state=RemediationState.FINDING_CONFIRMED, - reason="finding is confirmed without a proven remediation", - allows_already_applied_claim=False, - ) - - raise EvidenceBindingError("remediation evidence inputs are incomplete") - - -def sanitize_remediation_report_text(report_text: str, log_text: str) -> str: - """Rewrite false 'already applied' claims when apply_patch missed the workspace.""" - - failures = detect_apply_patch_failures(log_text) - if not failures or not ALREADY_APPLIED_RE.search(report_text): - return report_text - - marker = ( - "\n\n[strix-evidence-binding] Remediation claim rejected: apply_patch/" - "edit missed the materialized scan workspace " - f"({failures[0].target_path}). State={RemediationState.REMEDIATION_FAILED.value}. " - "Do not treat this finding as already repaired.\n" - ) - cleaned = ALREADY_APPLIED_RE.sub("remediation NOT applied", report_text) - return cleaned + marker - - -def pr_delta_findings_block_merge(verdicts: Sequence[FindingScopeVerdict]) -> bool: - """Return whether any authenticated PR-delta finding must block the PR lane.""" - - return any(verdict.scope is EvidenceScope.PR_DELTA for verdict in verdicts) - - -def baseline_only_findings(verdicts: Sequence[FindingScopeVerdict]) -> bool: - """Return whether every mapped finding is baseline or context dependency.""" - - mapped = [verdict for verdict in verdicts if verdict.scope is not EvidenceScope.UNMAPPED] - if not mapped: - return False - return all( - verdict.scope - in {EvidenceScope.REPOSITORY_BASELINE, EvidenceScope.CONTEXT_DEPENDENCY} - for verdict in mapped - ) - - -def verdict_to_jsonable(verdict: FindingScopeVerdict | RemediationVerdict) -> dict[str, Any]: - """Serialize a verdict dataclass for CI artifacts.""" - - payload = asdict(verdict) - for key, value in list(payload.items()): - if isinstance(value, Enum): - payload[key] = value.value - return payload - - -def _parse_binding(raw: Mapping[str, Any]) -> PullRequestBinding: - """Build a binding from a JSON object.""" - - return PullRequestBinding( - repository=str(raw["repository"]), - pull_request=int(raw["pull_request"]), - state=str(raw["state"]), - base_ref=str(raw["base_ref"]), - base_sha=str(raw["base_sha"]), - head_sha=str(raw["head_sha"]), - ) - - -def _parse_changed_paths(raw: Sequence[Mapping[str, Any]]) -> tuple[ChangedPath, ...]: - """Build changed-path rows from a JSON array.""" - - rows: list[ChangedPath] = [] - for item in raw: - lines = item.get("changed_lines", []) - if not isinstance(lines, list) or not all(isinstance(value, int) for value in lines): - raise EvidenceBindingError("changed_lines must be a list of integers") - previous = item.get("previous_path") - rows.append( - ChangedPath( - path=_require_safe_relative_path(str(item["path"])), - status=str(item["status"]), - previous_path=( - _require_safe_relative_path(str(previous)) if previous else None - ), - changed_lines=frozenset(lines), - patch_available=bool(item.get("patch_available", True)), - ) - ) - return tuple(rows) - - -def main(argv: Sequence[str] | None = None) -> int: - """CLI entry for gate and failed-check consumers.""" - - parser = argparse.ArgumentParser(description=__doc__) - sub = parser.add_subparsers(dest="command", required=True) - - classify = sub.add_parser("classify-finding", help="Classify one finding location") - classify.add_argument("--binding-json", required=True) - classify.add_argument("--changed-paths-json", required=True) - classify.add_argument("--path", required=True) - classify.add_argument("--start-line", type=int) - classify.add_argument("--end-line", type=int) - classify.add_argument("--context-path", action="append", default=[]) - classify.add_argument("--report-head-sha") - classify.add_argument("--report-base-sha") - - remediate = sub.add_parser("classify-remediation", help="Classify remediation state") - remediate.add_argument("--log-file") - remediate.add_argument("--report-file") - remediate.add_argument("--workspace") - remediate.add_argument("--relative-path") - remediate.add_argument("--expected-snippet") - remediate.add_argument("--source-commit-sha") - remediate.add_argument("--fix-proposed", action="store_true") - remediate.add_argument("--finding-confirmed", action="store_true", default=True) - - sanitize = sub.add_parser("sanitize-report", help="Strip false already-applied claims") - sanitize.add_argument("--report-file", required=True) - sanitize.add_argument("--log-file", required=True) - sanitize.add_argument("--output-file") - - args = parser.parse_args(argv) - try: - if args.command == "classify-finding": - binding = _parse_binding(json.loads(Path(args.binding_json).read_text(encoding="utf-8"))) - changed = _parse_changed_paths( - json.loads(Path(args.changed_paths_json).read_text(encoding="utf-8")) - ) - verdict = classify_finding_scope( - binding, - changed, - FindingLocation(args.path, args.start_line, args.end_line), - context_dependency_paths=frozenset(args.context_path), - report_head_sha=args.report_head_sha, - report_base_sha=args.report_base_sha, - ) - json.dump(verdict_to_jsonable(verdict), sys.stdout, indent=2, sort_keys=True) - sys.stdout.write("\n") - return 0 if verdict.scope is not EvidenceScope.UNMAPPED else 2 - - if args.command == "classify-remediation": - log_text = Path(args.log_file).read_text(encoding="utf-8") if args.log_file else "" - report_text = ( - Path(args.report_file).read_text(encoding="utf-8") if args.report_file else "" - ) - verdict = classify_remediation( - finding_confirmed=args.finding_confirmed, - fix_proposed=args.fix_proposed, - tool_events=detect_apply_patch_failures(log_text), - workspace_root=Path(args.workspace) if args.workspace else None, - relative_path=args.relative_path, - expected_snippet=args.expected_snippet, - source_commit_sha=args.source_commit_sha, - report_claims_already_applied=bool(ALREADY_APPLIED_RE.search(report_text)), - ) - json.dump(verdict_to_jsonable(verdict), sys.stdout, indent=2, sort_keys=True) - sys.stdout.write("\n") - return 0 if verdict.allows_already_applied_claim else 2 - - if args.command == "sanitize-report": - report_text = Path(args.report_file).read_text(encoding="utf-8") - log_text = Path(args.log_file).read_text(encoding="utf-8") - cleaned = sanitize_remediation_report_text(report_text, log_text) - if args.output_file: - Path(args.output_file).write_text(cleaned, encoding="utf-8") - else: - sys.stdout.write(cleaned) - return 0 - except (EvidenceBindingError, OSError, KeyError, TypeError, json.JSONDecodeError) as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 2 - - return 2 # pragma: no cover - argparse requires a subcommand - - -if __name__ == "__main__": # pragma: no cover - exercised via main() - raise SystemExit(main()) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index c7d3667465..c08f2fa36c 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -238,38 +238,6 @@ PY done } -# Issue #2168: reject "already applied" remediation prose when apply_patch -# missed the materialized scan workspace. Uses scripts/ci/strix_evidence_binding.py. -sanitize_remediation_evidence_claims() { - local log_file="$1" - local report_root="$2" - local binder="$REPO_ROOT/scripts/ci/strix_evidence_binding.py" - local report_file - - if [ ! -f "$binder" ] || [ -L "$binder" ]; then - echo "ERROR: Strix evidence binder is missing: $binder" >&2 - return 2 - fi - if [ -z "$log_file" ] || [ ! -f "$log_file" ] || [ -L "$log_file" ]; then - return 0 - fi - if [ -z "$report_root" ] || [ ! -d "$report_root" ] || [ -L "$report_root" ]; then - return 0 - fi - - while IFS= read -r -d '' report_file; do - python3 -I "$binder" sanitize-report \ - --report-file "$report_file" \ - --log-file "$log_file" \ - --output-file "$report_file" || { - echo "ERROR: Strix remediation evidence sanitizer failed for $report_file" >&2 - return 2 - } - done < <( - find "$report_root" \( -type f -name 'penetration_test_report.md' -o -type f -name 'vulnerabilities.json' -o -path '*/vulnerabilities/*.md' \) -print0 - ) -} - has_strix_report_failure_signal() { local report_root local report_log @@ -2341,7 +2309,7 @@ evaluate_pull_request_findings() { for changed_file in "${CHANGED_FILES[@]}"; do if vulnerability_record_intersects_changed_file "$vulnerability_location" "$vulnerability_start_line" "$vulnerability_end_line" "$changed_file"; then PR_FINDINGS_DECISION="block_changed" - echo "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." >&2 + echo "Strix finding intersects files changed in this pull request." >&2 return 1 fi done @@ -2392,7 +2360,7 @@ evaluate_pull_request_findings() { for changed_file in "${CHANGED_FILES[@]}"; do if vulnerability_record_intersects_changed_file "$vulnerability_location" "$vulnerability_start_line" "$vulnerability_end_line" "$changed_file"; then PR_FINDINGS_DECISION="block_changed" - echo "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." >&2 + echo "Strix finding intersects files changed in this pull request." >&2 return 1 fi done @@ -2414,7 +2382,7 @@ evaluate_pull_request_findings() { if [ "$found_baseline_threshold_finding" -eq 1 ]; then PR_FINDINGS_DECISION="allow_baseline" - echo "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." >&2 + echo "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." >&2 return 0 fi @@ -2972,8 +2940,6 @@ PY preserve_attempt_log "$model" "$rc" sanitize_known_strix_report_warnings "$STRIX_LOG" "$ACTIVE_REPORTS_DIR" "${resolved_target_path%/}/strix_runs" - sanitize_remediation_evidence_claims "$STRIX_LOG" "$ACTIVE_REPORTS_DIR" || return 2 - sanitize_remediation_evidence_claims "$STRIX_LOG" "${resolved_target_path%/}/strix_runs" || return 2 local report_failure_signal=0 if has_strix_report_failure_signal "$ACTIVE_REPORTS_DIR" "${resolved_target_path%/}/strix_runs"; then report_failure_signal=1 @@ -3011,8 +2977,8 @@ PY } is_llm_api_connection_error() { - if grep -Eiq 'litellm(\.exceptions)?\.(APIConnectionError|APIError)' "$STRIX_LOG" && - grep -Eiq '(GeminiException|Server disconnected without sending a response|LLM CONNECTION FAILED|Could not establish connection to the language model|bad gateway)' "$STRIX_LOG"; then + if grep -Eiq 'litellm(\.exceptions)?\.APIConnectionError' "$STRIX_LOG" && + grep -Eiq '(GeminiException|Server disconnected without sending a response|LLM CONNECTION FAILED|Could not establish connection to the language model)' "$STRIX_LOG"; then return 0 fi @@ -3043,7 +3009,7 @@ is_llm_api_connection_error() { # match was found earlier in the stream, silently suppressing a retry # that should have fired. Command substitution has no live reader to # close early, so awk always runs to completion. - if grep -Eiq '(openai|OpenAIException|LLM CONNECTION FAILED|Could not establish connection to the language model|internal server error|bad gateway)' <<<"$internal_server_error_blocks"; then + if grep -Eiq '(openai|OpenAIException|LLM CONNECTION FAILED|Could not establish connection to the language model|internal server error)' <<<"$internal_server_error_blocks"; then return 0 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 150b9102b3..6ea00c099f 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -489,17 +489,6 @@ assert_changed_file_membership_uses_cached_normalized_paths() { assert_file_contains "$GATE_SCRIPT" "for normalized_changed_file in \"\${NORMALIZED_CHANGED_FILES[@]}\"" "strix gate uses cached normalized paths for membership checks" } -assert_strix_evidence_binding_contract() { - assert_file_contains "$GATE_SCRIPT" "sanitize_remediation_evidence_claims" "strix gate sanitizes false already-applied remediation claims" - assert_file_contains "$GATE_SCRIPT" 'scripts/ci/strix_evidence_binding.py' "strix gate binds remediation evidence through the tested Python binder" - assert_file_contains "$GATE_SCRIPT" "evidence_scope=pr_delta" "strix gate labels PR-delta findings with authenticated provenance" - assert_file_contains "$GATE_SCRIPT" "evidence_scope=repository_baseline" "strix gate labels unchanged-path findings as repository_baseline" - assert_file_contains "$REPO_ROOT/scripts/ci/strix_evidence_binding.py" 'PR_DELTA = "pr_delta"' "strix evidence binder defines pr_delta scope" - assert_file_contains "$REPO_ROOT/scripts/ci/strix_evidence_binding.py" 'REMEDIATION_FAILED = "remediation_failed"' "strix evidence binder fails closed on apply_patch misses" - assert_file_contains "$REPO_ROOT/tests/test_strix_evidence_binding.py" "completely_base_identical_source_finding" "strix evidence binder has a RED fixture for base-identical findings" - assert_file_contains "$REPO_ROOT/tests/test_strix_evidence_binding.py" "apply_patch_miss_rejects_already_applied_claim" "strix evidence binder has a RED fixture for apply_patch misses" -} - assert_absent_endpoint_search_uses_canonical_target_path() { assert_file_contains "$GATE_SCRIPT" 'resolved_target_root="$(resolve_current_target_path "$TARGET_PATH" 2>/dev/null)"' "absent-endpoint search resolves canonical target root" assert_file_contains "$GATE_SCRIPT" 'candidate="${resolved_target_root%/}/$dir_entry"' "absent-endpoint search uses canonical target root" @@ -6567,7 +6556,7 @@ run_filtered_gate_case_if_requested() { "vertex_ai/gemini-2.5-pro" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/gemini-2.5-pro" \ "" \ @@ -7278,7 +7267,7 @@ EOS set -e assert_equals "1" "$rc" "case=pull-request-target-plaintext-runner-token-fails-closed exit code" - assert_file_contains "$output_log" "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." "case=pull-request-target-plaintext-runner-token-fails-closed output" + assert_file_contains "$output_log" "Strix finding intersects files changed in this pull request." "case=pull-request-target-plaintext-runner-token-fails-closed output" local call_count="0" if [ -f "$call_log" ]; then call_count="$(wc -l <"$call_log" | tr -d ' ')" @@ -9648,8 +9637,6 @@ assert_strix_gate_target_scope_separated assert_changed_file_membership_uses_cached_normalized_paths -assert_strix_evidence_binding_contract - assert_absent_endpoint_search_uses_canonical_target_path assert_strix_llm_file_read_is_literal_data @@ -11073,7 +11060,7 @@ run_gate_case "opencode-documented-env-api-key-fallback-success" \ "vertex_ai/opencode-env-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/opencode-env-primary" \ "" \ @@ -11124,7 +11111,7 @@ run_gate_case "pr-stale-source-claim-fallback-success" \ "vertex_ai/stale-source-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/stale-source-primary" \ "" \ @@ -11145,7 +11132,7 @@ run_gate_case "pr-stale-snapshot-snippet-fallback-success" \ "vertex_ai/stale-snapshot-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/stale-snapshot-primary" \ "" \ @@ -11166,7 +11153,7 @@ run_gate_case "pr-stale-source-plus-real-finding-blocks" \ "vertex_ai/stale-source-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/stale-source-primary" \ "" \ @@ -11187,7 +11174,7 @@ run_gate_case_allow_provider_signal "pr-changed-finding-with-retry-marker-blocks "vertex_ai/changed-finding-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/changed-finding-primary" \ "" \ @@ -11208,7 +11195,7 @@ run_gate_case "pr-stale-report-plus-inline-changed-finding-blocks" \ "vertex_ai/stale-inline-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/stale-inline-primary" \ "" \ @@ -11961,7 +11948,7 @@ run_gate_case "pr-baseline-critical-unchanged" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -11982,7 +11969,7 @@ run_gate_case "pr-baseline-critical-absolute-target" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12003,7 +11990,7 @@ run_gate_case "pr-baseline-critical-extensionless-dockerfile-target" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12024,7 +12011,7 @@ run_gate_case "pr-baseline-critical-subdir-target" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12048,7 +12035,7 @@ run_gate_case "pr-baseline-critical-subdir-boxed-target" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12072,7 +12059,7 @@ run_gate_case "pr-baseline-critical-subdir-endpoint" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12096,7 +12083,7 @@ run_gate_case "pr-baseline-critical-subdir-endpoint-bare-filename" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12120,7 +12107,7 @@ run_gate_case "pr-baseline-critical-subdir-narrative-backticked-file" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12168,7 +12155,7 @@ run_gate_case "pr-critical-changed" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12189,7 +12176,7 @@ run_gate_case "pr-changed-file-nonintersecting-line" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12209,7 +12196,7 @@ run_gate_case "pr-critical-changed-bracketed-next-route" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12230,7 +12217,7 @@ run_gate_case "pr-critical-changed-xml-file-location" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12251,7 +12238,7 @@ run_gate_case "pr-critical-changed-xml-file-location-space" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12272,7 +12259,7 @@ run_gate_case "pr-baseline-critical-narrative-backticked-service-file" \ "openai/gpt-4o-mini" \ "" \ "0" \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12314,7 +12301,7 @@ run_gate_case "pr-critical-changed-absolute-target" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12335,7 +12322,7 @@ run_gate_case "pr-critical-changed-internal-dotdir-target" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12356,7 +12343,7 @@ run_gate_case "pr-critical-changed-json-target" \ "vertex_ai/gemini-2.5-pro" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "vertex_ai/gemini-2.5-pro" \ "" \ @@ -12377,7 +12364,7 @@ run_gate_case "pr-critical-changed-subdir-target" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ @@ -12401,7 +12388,7 @@ run_gate_case "pr-critical-changed-subdir-endpoint" \ "openai/gpt-4o-mini" \ "" \ "1" \ - "Strix finding intersects files changed in this pull request (evidence_scope=pr_delta)." \ + "Strix finding intersects files changed in this pull request." \ "1" \ "openai/gpt-4o-mini" \ "https://example.invalid" \ diff --git a/scripts/ci/verify_exact_artifact_sbom_handoff.py b/scripts/ci/verify_exact_artifact_sbom_handoff.py index 97157bb905..4bc21fe579 100644 --- a/scripts/ci/verify_exact_artifact_sbom_handoff.py +++ b/scripts/ci/verify_exact_artifact_sbom_handoff.py @@ -16,7 +16,7 @@ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$") _SHA1_RE = re.compile(r"^[0-9a-f]{40}$") -_REPOSITORY_RE = re.compile(r"^(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$") +_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") _ARTIFACT_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") _CHECKSUM_RE = re.compile(r"^([0-9a-f]{64}) [ *]([^/\\]+)$") _MAX_JSON_BYTES = 16 * 1024 * 1024 diff --git a/tests/test_actions_queue_health.py b/tests/test_actions_queue_health.py deleted file mode 100644 index 6762e74890..0000000000 --- a/tests/test_actions_queue_health.py +++ /dev/null @@ -1,1222 +0,0 @@ -"""Contract and behavior tests for the read-only Actions queue collector.""" - -import importlib.util -from datetime import datetime, timezone -import io -import json -from pathlib import Path -from subprocess import CompletedProcess, TimeoutExpired - -import pytest - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -NOW = datetime(2026, 8, 19, 12, 0, tzinfo=timezone.utc) - - -def test_queue_health_module_path_is_independent_of_working_directory() -> None: - """Load production code from the repository root, not the caller's cwd.""" - expected = Path(__file__).resolve().parents[1] / "scripts/ci/actions_queue_health.py" - assert MODULE_PATH == expected - assert MODULE_PATH.is_file() - - -def pull_request(number: int = 1, head_sha: str = "head") -> dict: - """Return a compact open pull-request fixture.""" - return { - "number": number, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, - "head": {"sha": head_sha}, - "updated_at": "2026-08-19T11:00:00Z", - } - - -def workflow_run( - run_id: int, - *, - head_sha: str = "head", - pull_requests: list[dict] | None = None, - status: str = "queued", - jobs: list[dict] | None = None, - workflow_name: str = "required-check", - created_at: str = "2026-08-19T10:00:00Z", -) -> dict: - """Return one raw workflow-run fixture.""" - return { - "id": run_id, - "name": workflow_name, - "event": "pull_request", - "status": status, - "conclusion": "", - "head_sha": head_sha, - "created_at": created_at, - "updated_at": created_at, - "run_attempt": 1, - "pull_requests": pull_requests or [], - "jobs": jobs or [], - } - - -def job( - job_id: int, - *, - status: str = "queued", - conclusion: str | None = None, - runner_id: int | None = None, - runner_name: str | None = None, - name: str = "required-check", -) -> dict: - """Return one raw workflow-job fixture.""" - return { - "id": job_id, - "name": name, - "status": status, - "conclusion": conclusion, - "runner_id": runner_id, - "runner_name": runner_name, - "steps": [], - } - - -def report_snapshot() -> dict: - """Return a fixture covering current, obsolete, unlinked, and terminal jobs.""" - return { - "generated_at": "2026-08-19T11:00:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [pull_request()], - "runs": [ - workflow_run( - 10, - pull_requests=[{"number": 1, "head": {"sha": "head"}}], - jobs=[ - job(100), - job(101, runner_id=7, runner_name="runner-7"), - job(102, status="waiting"), - ], - ), - workflow_run( - 11, - head_sha="old", - pull_requests=[{"number": 1, "head": {"sha": "old"}}], - jobs=[job(110)], - ), - workflow_run(12, jobs=[job(120)]), - workflow_run( - 13, - pull_requests=[{"number": 1, "head": {"sha": "head"}}], - jobs=[], - workflow_name="required-check", - ), - workflow_run( - 14, - pull_requests=[{"number": 1, "head": {"sha": "head"}}], - status="completed", - jobs=[job(140, status="completed", conclusion="success")], - ), - ], - } - ], - } - - -@pytest.mark.parametrize("value", [None, "", " ", "not-a-time", "2026-08-19T12:00:00"]) -def test_parse_timestamp_rejects_ambiguous_or_invalid_values(value: object) -> None: - """Reject missing, malformed, and timezone-free timestamps.""" - with pytest.raises(queue_health.QueueHealthError): - queue_health.parse_timestamp(value) # type: ignore[arg-type] - - -def test_parse_timestamp_normalises_z_and_offsets() -> None: - """Normalize UTC and offset timestamps to the same instant.""" - assert queue_health.parse_timestamp("2026-08-19T12:00:00Z") == NOW - assert queue_health.parse_timestamp("2026-08-19T21:00:00+09:00") == NOW - - -@pytest.mark.parametrize("value", ["owner", "owner/repo/extra", "../..", "./repo", "owner/.", 1]) -def test_repository_name_rejects_non_repository_identifiers(value: object) -> None: - """Reject malformed and traversal-like repository identifiers.""" - with pytest.raises(queue_health.QueueHealthError): - queue_health._repository_name(value) - - -def test_load_allowlist_accepts_array_and_object_and_rejects_bad_inputs(tmp_path: Path) -> None: - """Load both supported allowlist shapes and reject unsafe input files.""" - array_path = tmp_path / "array.json" - array_path.write_text(json.dumps(["z/repo", "a/repo"]), encoding="utf-8") - assert queue_health.load_allowlist(array_path) == ["a/repo", "z/repo"] - - object_path = tmp_path / "object.json" - object_path.write_text(json.dumps({"repositories": ["a/repo"]}), encoding="utf-8") - assert queue_health.load_allowlist(object_path) == ["a/repo"] - - for name, payload in ( - ("empty.json", []), - ("missing-key.json", {}), - ("duplicate.json", ["a/repo", "a/repo"]), - ("invalid-repository.json", ["a repo"]), - ): - path = tmp_path / name - path.write_text(json.dumps(payload), encoding="utf-8") - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_allowlist(path) - - (tmp_path / "invalid.json").write_text("{", encoding="utf-8") - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_allowlist(tmp_path / "invalid.json") - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_allowlist(tmp_path / "missing.json") - - -@pytest.mark.parametrize( - "payload, key, expected", - [ - ([{"id": 1}], "items", [{"id": 1}]), - ({"items": [{"id": 2}]}, "items", [{"id": 2}]), - ({"items": [{"id": 3}], "total_count": 1}, "items", [{"id": 3}]), - ], -) -def test_list_payload_accepts_api_list_shapes(payload: object, key: str, expected: list[dict]) -> None: - """Accept the bounded list response shapes emitted by GitHub APIs.""" - assert queue_health._list_payload(payload, key) == expected - - -@pytest.mark.parametrize( - "payload", - [ - None, - {"items": "bad"}, - [{"id": 1}, "bad"], - {"items": [{"id": 1}], "total_count": 2001}, - {"items": [{"id": 1}], "total_count": 0}, - {"items": [{"id": 1}], "total_count": True}, - {"items": [{"id": 1}], "total_count": "1"}, - {"items": [{"id": 1}], "total_count": []}, - ], -) -def test_list_payload_rejects_untrusted_shapes(payload: object) -> None: - """Reject malformed, oversized, and dishonest list responses.""" - with pytest.raises(queue_health.QueueHealthError): - queue_health._list_payload(payload, "items") - - -def test_list_payload_flattens_bounded_paginated_responses() -> None: - """Flatten bounded pages while validating every page and total count.""" - assert queue_health._list_payload( - {"_queue_health_pages": [[{"id": 1}], [{"id": 2}]]}, "items" - ) == [{"id": 1}, {"id": 2}] - assert queue_health._list_payload( - {"_queue_health_pages": [{"items": [{"id": 3}], "total_count": 2}, {"items": [{"id": 4}], "total_count": 2}]}, - "items", - ) == [{"id": 3}, {"id": 4}] - for payload in ( - {"_queue_health_pages": []}, - {"_queue_health_pages": [[]] * (queue_health.MAX_API_PAGES + 1)}, - {"_queue_health_pages": [None]}, - {"_queue_health_pages": [{"items": "bad"}]}, - {"_queue_health_pages": [{"items": [{"id": 1}], "total_count": 3}, {"items": [{"id": 2}], "total_count": "2"}]}, - ): - with pytest.raises(queue_health.QueueHealthError): - queue_health._list_payload(payload, "items") - assert queue_health._list_payload( - {"_queue_health_pages": [{"items": [{"id": 1}], "total_count": 1}, {"items": [{"id": 2}], "total_count": 2}]}, - "items", - ) == [{"id": 1}, {"id": 2}] - - -def test_list_payload_rejects_incompletely_paginated_total_count() -> None: - """A declared total larger than collected pages cannot look healthy.""" - with pytest.raises(queue_health.QueueHealthError, match="incompletely paginated"): - queue_health._list_payload( - {"_queue_health_pages": [{"items": [{"id": 1}], "total_count": 2}]}, - "items", - ) - - -def test_list_payload_rejects_duplicate_identities_across_pages() -> None: - """Moving records between pages cannot conceal omitted queue evidence.""" - with pytest.raises(queue_health.QueueHealthError, match="duplicate record identity"): - queue_health._list_payload( - { - "_queue_health_pages": [ - {"items": [{"id": 1}], "total_count": 2}, - {"items": [{"id": 1}], "total_count": 2}, - ] - }, - "items", - ) - with pytest.raises(queue_health.QueueHealthError, match="positive integer id or number"): - queue_health._list_payload( - {"_queue_health_pages": [{"items": [{}], "total_count": 1}]}, - "items", - ) - - -def test_github_json_is_read_only_and_rejects_failures() -> None: - """Use safe read-only CLI arguments and fail closed on transport errors.""" - def success_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return one successful non-paginated API response.""" - assert args[0] == ["gh", "api", "repos/a/repo"] - assert kwargs == { - "capture_output": True, - "text": True, - "check": False, - "timeout": 30, - } - return CompletedProcess([], 0, "[{\"id\": 1}]", "") - - assert queue_health.github_json("repos/a/repo", runner=success_runner) == [{"id": 1}] - - def paginated_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return one successful paginated API response.""" - assert args[0] == ["gh", "api", "repos/a/repo"] - return CompletedProcess([], 0, "[{\"id\": 1}]", "") - - assert queue_health.github_json("repos/a/repo", paginate=True, runner=paginated_runner) == { - "_queue_health_pages": [[{"id": 1}]] - } - with pytest.raises(queue_health.QueueHealthError, match="no bounded array"): - queue_health.github_json( - "repos/a/repo", - paginate=True, - runner=lambda *args, **kwargs: CompletedProcess([], 0, '{}', ''), - ) - requested_pages: list[str] = [] - - def full_page_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - requested_pages.append(args[-1]) - return CompletedProcess(args, 0, json.dumps([{}] * 100), "") - - with pytest.raises(queue_health.QueueHealthError, match="exceeds 20 pages"): - queue_health.github_json("repos/a/repo?per_page=100", paginate=True, runner=full_page_runner) - assert requested_pages[-1].endswith("page=20") - assert not any(path.endswith("page=21") for path in requested_pages) - with pytest.raises(queue_health.QueueHealthError): - queue_health.github_json("orgs/a/repos", runner=success_runner) - - def failed_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return a failed API response with stderr evidence.""" - return CompletedProcess([], 1, "fallback", "api failed") - - with pytest.raises(queue_health.QueueHealthError, match="api failed"): - queue_health.github_json("repos/a/repo", runner=failed_runner) - - def stdout_failure_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return a failed API response with stdout-only evidence.""" - return CompletedProcess([], 1, "stdout failure", "") - - with pytest.raises(queue_health.QueueHealthError, match="stdout failure"): - queue_health.github_json("repos/a/repo", runner=stdout_failure_runner) - - def empty_failure_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return a failed API response without diagnostic text.""" - return CompletedProcess([], 1, "", "") - - with pytest.raises(queue_health.QueueHealthError, match="GitHub API read failed"): - queue_health.github_json("repos/a/repo", runner=empty_failure_runner) - - def invalid_json_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Return a successful process containing invalid JSON.""" - return CompletedProcess([], 0, "not json", "") - - with pytest.raises(queue_health.QueueHealthError, match="invalid JSON"): - queue_health.github_json("repos/a/repo", runner=invalid_json_runner) - - -def test_github_json_fails_closed_when_external_read_times_out() -> None: - """A stalled GitHub CLI read must not occupy the workflow indefinitely.""" - def timeout_runner(*args: object, **kwargs: object) -> CompletedProcess[str]: - """Raise the subprocess timeout seen by the production boundary.""" - raise TimeoutExpired(args[0], timeout=30) - - with pytest.raises(queue_health.QueueHealthError, match="timed out after 30 seconds"): - queue_health.github_json("repos/a/repo", runner=timeout_runner) - - -def test_normalise_pull_request_preserves_exact_head_identity() -> None: - """Retain exact pull-request identity and reject incomplete records.""" - normalized = queue_health._normalise_pull_request(pull_request()) - assert normalized["number"] == 1 - assert normalized["head_sha"] == "head" - assert normalized["base_repository"] == "owner/repo" - normalized_snapshot = queue_health._normalise_pull_request( - { - "number": 1, - "state": "open", - "base_ref": "main", - "base_repository": "owner/repo", - "head_sha": "head", - "updated_at": "2026-08-19T11:00:00Z", - }, - allow_normalized=True, - ) - assert normalized_snapshot == { - "number": 1, - "state": "open", - "base_ref": "main", - "base_repository": "owner/repo", - "head_sha": "head", - "updated_at": "2026-08-19T11:00:00Z", - } - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="normalized"): - queue_health._normalise_pull_request( - {"number": 1, "base_ref": "main"}, allow_normalized=True - ) - with pytest.raises(queue_health.QueueHealthError, match="positive integer"): - queue_health._normalise_pull_request( - { - "number": 0, - "base_ref": "main", - "base_repository": "owner/repo", - "head_sha": "head", - "updated_at": "2026-08-19T11:00:00Z", - }, - allow_normalized=True, - ) - for invalid in ({"number": True}, {"number": 0}, {"number": "1"}, "bad"): - with pytest.raises(queue_health.QueueHealthError): - queue_health._normalise_pull_request(invalid) # type: ignore[arg-type] - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="head and base"): - queue_health._normalise_pull_request({"number": 1, "head": {}, "base": "bad"}) - with pytest.raises(queue_health.QueueHealthError, match="positive integer"): - queue_health._normalise_pull_request({"number": 0, "head": {}, "base": {}}) - - -def test_normalise_pull_request_rejects_empty_identity_fields_instead_of_retrying_later() -> None: - """An empty API-provided identity field must retry, not silently pass through.""" - empty_head_sha = pull_request() - empty_head_sha["head"] = {"sha": ""} - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(empty_head_sha) - - empty_base_ref = pull_request() - empty_base_ref["base"]["ref"] = "" - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(empty_base_ref) - - empty_base_repository = pull_request() - empty_base_repository["base"]["repo"]["full_name"] = "" - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(empty_base_repository) - - missing_base_repo = pull_request() - del missing_base_repo["base"]["repo"] - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(missing_base_repo) - - empty_updated_at = pull_request() - empty_updated_at["updated_at"] = "" - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(empty_updated_at) - - empty_normalized = { - "number": 1, - "base_ref": "main", - "base_repository": "owner/repo", - "head_sha": "", - "updated_at": "2026-08-19T11:00:00Z", - } - with pytest.raises(queue_health.IncompletePullRequestIdentity, match="non-empty"): - queue_health._normalise_pull_request(empty_normalized, allow_normalized=True) - - -def test_normalise_job_preserves_runner_assignment_and_fails_closed() -> None: - """Retain runner evidence while rejecting malformed job identities.""" - normalized = queue_health._normalise_job(job(1, runner_id=3, runner_name="runner")) - assert normalized["runner_id"] == 3 - assert normalized["steps_count"] == 0 - assert queue_health._normalise_job( - {"id": 2, "status": "queued", "runner_id": "bad", "steps": []} - )["runner_id"] == 0 - assert queue_health._normalise_job({"id": 3, "runner_id": True})[ - "steps_count" - ] is None - assert queue_health._normalise_job({"id": 4, "steps": None})[ - "steps_count" - ] is None - assert queue_health._normalise_job({"id": 5, "steps_count": 2})[ - "steps_count" - ] == 2 - with pytest.raises(queue_health.QueueHealthError, match="steps must be an array"): - queue_health._normalise_job({"id": 6, "steps": "bad"}) - for invalid_steps_count in (True, -1, "2"): - with pytest.raises(queue_health.QueueHealthError, match="steps_count"): - queue_health._normalise_job( - {"id": 7, "steps_count": invalid_steps_count} - ) - for invalid in ({"id": True}, {"id": 0}, {"id": "1"}, "bad"): - with pytest.raises(queue_health.QueueHealthError): - queue_health._normalise_job(invalid) # type: ignore[arg-type] - - -def test_normalise_run_validates_links_jobs_and_fallback_names() -> None: - """Normalize workflow links and jobs with bounded fallback values.""" - normalized = queue_health._normalise_run( - "owner/repo", - { - "id": 1, - "workflow_name": "fallback-name", - "pull_requests": [{"number": 2, "head": {"sha": "sha"}}], - }, - [job(2)], - ) - assert normalized["workflow_name"] == "fallback-name" - assert normalized["pull_requests"] == [{"number": 2, "head_sha": "sha"}] - assert queue_health._normalise_run("owner/repo", {"id": 2, "pull_requests": None}, [])["pull_requests"] == [] - assert queue_health._normalise_run( - "owner/repo", {"id": 3, "pull_requests": [{"number": 1, "head": None}]}, [] - )["pull_requests"] == [{"number": 1, "head_sha": ""}] - # Re-normalising an already-normalised link (as build_report does when - # loading a snapshot collect_snapshot produced) must preserve head_sha - # rather than treating the flattened shape as missing "head". - assert queue_health._normalise_run( - "owner/repo", {"id": 4, "pull_requests": [{"number": 5, "head_sha": "flat-sha"}]}, [] - )["pull_requests"] == [{"number": 5, "head_sha": "flat-sha"}] - - for invalid_run, invalid_jobs in ( - ("bad", []), - ({"id": True}, []), - ({"id": 0}, []), - ({"id": 1}, "bad"), - ({"id": 1, "pull_requests": "bad"}, []), - ({"id": 1, "pull_requests": [{"number": 0}]}, []), - ({"id": 1, "pull_requests": [{"number": 1, "head": "bad"}]}, []), - ({"id": 1}, ["bad"]), - ): - with pytest.raises(queue_health.QueueHealthError): - queue_health._normalise_run("owner/repo", invalid_run, invalid_jobs) # type: ignore[arg-type] - - -def test_collect_snapshot_deduplicates_status_views_and_preserves_order( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Deduplicate status views and isolate malformed repository evidence.""" - queued_current = workflow_run(10, pull_requests=[{"number": 1, "head": {"sha": "head"}}]) - current = workflow_run( - 12, - status="in_progress", - pull_requests=[{"number": 1, "head": {"sha": "head"}}], - jobs=[job(100)], - ) - unlinked = workflow_run(11, jobs=[]) - responses = { - "repos/owner/repo": {"default_branch": "main"}, - "repos/owner/repo/pulls?state=open&per_page=100": [pull_request()], - "repos/owner/repo/actions/runs?per_page=50": [queued_current, current, unlinked], - "repos/owner/repo/actions/runs/10/jobs?per_page=100": {"jobs": []}, - "repos/owner/repo/actions/runs/12/jobs?per_page=100": {"jobs": [job(100)]}, - } - for status in ("in_progress", "pending", "queued", "requested", "waiting"): - responses[f"repos/owner/repo/actions/runs?status={status}&per_page=50"] = [ - run - for run in responses["repos/owner/repo/actions/runs?per_page=50"] - if run["status"] == status - ] - responses["repos/owner/repo/actions/runs?status=completed&head_sha=head&per_page=50"] = [] - responses["repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50"] = [] - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return the deterministic API response for each requested endpoint.""" - payload = responses[args[-1]] - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot(["owner/repo"], runner=runner, generated_at="2026-08-19T11:00:00Z") - assert snapshot["repositories"][0]["runs"][0]["id"] == 10 - assert [run["id"] for run in snapshot["repositories"][0]["runs"]] == [10, 11, 12] - assert snapshot["repositories"][0]["default_branch"] == "main" - report = queue_health.build_report(snapshot, now=NOW) - assert report["summary"]["observed_job_count"] == 3 - assert report["summary"]["current_head_pending_count"] == 2 - - with pytest.raises(queue_health.QueueHealthError): - queue_health.collect_snapshot(["owner/repo", "owner/repo"], runner=runner) - with pytest.raises(queue_health.QueueHealthError): - queue_health.collect_snapshot(["owner/repo"], runner=runner, generated_at="bad") - - bad_responses = dict(responses) - bad_responses["repos/owner/repo"] = [] - - def bad_metadata_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return malformed repository metadata for the isolation case.""" - payload = bad_responses[args[-1]] - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - bad_snapshot = queue_health.collect_snapshot(["owner/repo"], runner=bad_metadata_runner) - assert bad_snapshot["repositories"] == [] - assert bad_snapshot["collection_errors"][0]["repository"] == "owner/repo" - - invalid_run_responses = dict(responses) - invalid_run_responses["repos/owner/repo/actions/runs?status=queued&per_page=50"] = [ - {"id": 0, "status": "queued"} - ] - - def invalid_run_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return an invalid workflow-run identity for the isolation case.""" - payload = invalid_run_responses[args[-1]] - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - invalid_run_snapshot = queue_health.collect_snapshot(["owner/repo"], runner=invalid_run_runner) - assert invalid_run_snapshot["repositories"] == [] - assert invalid_run_snapshot["collection_errors"][0]["repository"] == "owner/repo" - - bad_pull = pull_request() - bad_pull["base"] = "temporarily incomplete" - retry_calls = 0 - requested_paths: list[str] = [] - sleep_calls: list[float] = [] - monkeypatch.setattr(queue_health.time, "sleep", sleep_calls.append) - - def retry_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return one incomplete pull response followed by a valid response.""" - nonlocal retry_calls - requested_paths.append(args[-1]) - payload = responses[args[-1]] - if args[-1] == "repos/owner/repo/pulls?state=open&per_page=100": - retry_calls += 1 - payload = [bad_pull] if retry_calls == 1 else payload - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - queue_health.collect_snapshot(["owner/repo"], runner=retry_runner) - assert retry_calls == 4 - pull_snapshot_indices = [ - request_index - for request_index, request_path in enumerate(requested_paths) - if request_path == "repos/owner/repo/pulls?state=open&per_page=100" - ] - first_run_request_index = next( - request_index - for request_index, request_path in enumerate(requested_paths) - if "/actions/runs?" in request_path - ) - assert pull_snapshot_indices[1] < first_run_request_index - assert sleep_calls == [queue_health.PULL_REQUEST_RETRY_DELAY_SECONDS] - - def persistent_bad_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return an incomplete pull response on every retry.""" - payload = ( - [bad_pull] - if args[-1] == "repos/owner/repo/pulls?state=open&per_page=100" - else responses[args[-1]] - ) - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - persistent_bad_snapshot = queue_health.collect_snapshot( - ["owner/repo"], runner=persistent_bad_runner - ) - assert persistent_bad_snapshot["repositories"] == [] - assert persistent_bad_snapshot["collection_errors"][0]["repository"] == "owner/repo" - - bad_number = pull_request(number=0) - - def invalid_pull_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return a pull request with an invalid number.""" - payload = ( - [bad_number] - if args[-1] == "repos/owner/repo/pulls?state=open&per_page=100" - else responses[args[-1]] - ) - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - invalid_pull_snapshot = queue_health.collect_snapshot( - ["owner/repo"], runner=invalid_pull_runner - ) - assert invalid_pull_snapshot["repositories"] == [] - assert invalid_pull_snapshot["collection_errors"][0]["repository"] == "owner/repo" - - -def test_collect_snapshot_retries_pull_request_with_empty_identity_fields( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """An empty head_sha in one API response must retry like a missing head/base.""" - queued_current = workflow_run(20, pull_requests=[{"number": 1, "head": {"sha": "head"}}]) - responses = { - "repos/owner/repo": {"default_branch": "main"}, - "repos/owner/repo/pulls?state=open&per_page=100": [pull_request()], - "repos/owner/repo/actions/runs?per_page=50": [queued_current], - } - for status in ("in_progress", "pending", "queued", "requested", "waiting"): - responses[f"repos/owner/repo/actions/runs?status={status}&per_page=50"] = ( - [queued_current] if status == "queued" else [] - ) - responses["repos/owner/repo/actions/runs?status=completed&head_sha=head&per_page=50"] = [] - responses["repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50"] = [] - responses["repos/owner/repo/actions/runs/20/jobs?per_page=100"] = {"jobs": []} - empty_identity_pull = pull_request() - empty_identity_pull["head"] = {"sha": ""} - retry_calls = 0 - sleep_calls: list[float] = [] - monkeypatch.setattr(queue_health.time, "sleep", sleep_calls.append) - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return one empty-identity pull response followed by a complete one.""" - nonlocal retry_calls - payload = responses[args[-1]] - if args[-1] == "repos/owner/repo/pulls?state=open&per_page=100": - retry_calls += 1 - payload = [empty_identity_pull] if retry_calls == 1 else payload - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot(["owner/repo"], runner=runner) - assert retry_calls == 4 - assert sleep_calls == [queue_health.PULL_REQUEST_RETRY_DELAY_SECONDS] - assert snapshot["collection_errors"] == [] - assert snapshot["repositories"][0]["pull_requests"][0]["head_sha"] == "head" - - def persistent_empty_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return an empty-identity pull response on every attempt.""" - payload = ( - [empty_identity_pull] - if args[-1] == "repos/owner/repo/pulls?state=open&per_page=100" - else responses[args[-1]] - ) - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - persistent_snapshot = queue_health.collect_snapshot(["owner/repo"], runner=persistent_empty_runner) - assert persistent_snapshot["repositories"] == [] - assert persistent_snapshot["collection_errors"][0]["repository"] == "owner/repo" - - -def test_collect_snapshot_and_build_report_preserve_linked_head_through_round_trip() -> None: - """The full collect -> build pipeline must not lose the linked head SHA. - - ``build_report`` re-normalises runs loaded from a collected snapshot; - this exercises the entire ``collect_snapshot`` -> ``build_report`` path - for a ``pull_request_target``-shaped run (run-level head_sha is the base - commit, the linked pull-request entry carries the real PR head) and - checks the run still resolves to ``current_head``. - """ - pull_request_target_run = workflow_run( - 70, - head_sha="base-branch-checkout-sha", - status="in_progress", - pull_requests=[{"number": 1, "head": {"sha": "pr-head-sha"}}], - jobs=[job(700, runner_id=9, runner_name="runner-9")], - ) - responses = { - "repos/owner/repo": {"default_branch": "main"}, - "repos/owner/repo/pulls?state=open&per_page=100": [pull_request(1, "pr-head-sha")], - "repos/owner/repo/actions/runs?per_page=50": [pull_request_target_run], - "repos/owner/repo/actions/runs/70/jobs?per_page=100": { - "jobs": [job(700, runner_id=9, runner_name="runner-9")] - }, - } - for status in ("in_progress", "pending", "queued", "requested", "waiting"): - responses[f"repos/owner/repo/actions/runs?status={status}&per_page=50"] = ( - [pull_request_target_run] if status == "in_progress" else [] - ) - responses["repos/owner/repo/actions/runs?status=completed&head_sha=pr-head-sha&per_page=50"] = [] - responses["repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50"] = [] - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return the deterministic API response for each requested endpoint.""" - payload = responses[args[-1]] - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot(["owner/repo"], runner=runner, generated_at="2026-08-19T11:00:00Z") - report = queue_health.build_report(snapshot, now=NOW) - row = report["runs"][0] - assert row["identity_state"] == "current_head" - assert row["obsolete"] is False - - -def test_collect_snapshot_isolates_repository_errors_and_reports_incomplete_evidence() -> None: - """Continue healthy collection while recording one repository's failure.""" - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return a rate-limit failure for one repository and valid data for another.""" - path = args[-1] - if path == "repos/bad/repo": - return CompletedProcess(args, 1, "", "rate limit") - if path == "repos/good/repo": - payload: object = {"default_branch": "main"} - elif path == "repos/good/repo/pulls?state=open&per_page=100": - payload = [] - elif "/actions/runs?status=" in path: - payload = [] - else: # pragma: no cover - a new endpoint must be explicitly governed - raise AssertionError(f"unexpected endpoint: {path}") - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - ["bad/repo", "good/repo"], runner=runner, generated_at="2026-08-19T11:00:00Z" - ) - assert [item["full_name"] for item in snapshot["repositories"]] == ["good/repo"] - assert snapshot["collection_errors"] == [ - {"repository": "bad/repo", "error": "GitHub API read failed for repos/bad/repo: rate limit"} - ] - - report = queue_health.build_report(snapshot, now=NOW) - assert report["summary"]["collection_error_count"] == 1 - assert report["collection_errors"] == snapshot["collection_errors"] - assert "bad/repo" in queue_health.render_html(report) - - -@pytest.mark.parametrize( - "collection_errors", - [ - "bad", - [None], - [{"repository": "../..", "error": "bad"}], - [{"repository": "owner/repo", "error": 1}], - ], -) -def test_build_report_rejects_malformed_collection_errors(collection_errors: object) -> None: - """Reject malformed collection errors without inventing missing evidence.""" - snapshot = report_snapshot() - snapshot["collection_errors"] = collection_errors - with pytest.raises(queue_health.QueueHealthError): - queue_health.build_report(snapshot, now=NOW) - - snapshot["collection_errors"] = None - assert queue_health.build_report(snapshot, now=NOW)["summary"]["collection_error_count"] == 0 - - -def test_collect_snapshot_bounds_workflow_run_payloads_to_fifty_items() -> None: - """Ignore large historical totals while retaining bounded active-run pagination.""" - requested_paths: list[str] = [] - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Return empty bounded run pages and record the requested endpoints.""" - path = args[-1] - requested_paths.append(path) - if path == "repos/owner/repo": - payload: object = {"default_branch": "main"} - elif path == "repos/owner/repo/pulls?state=open&per_page=100": - payload = [] - elif path == "repos/owner/repo/actions/runs?per_page=50": - payload = {"total_count": 2_001, "workflow_runs": []} - elif "/actions/runs?status=" in path: - status = path.split("status=", 1)[1].split("&", 1)[0] - if status == "cancelled": - payload = {"total_count": 0, "workflow_runs": []} - else: - run_id = ("in_progress", "pending", "queued", "requested", "waiting").index(status) - payload = { - "total_count": 1, - "workflow_runs": [workflow_run(100 + run_id, status=status)], - } - else: # pragma: no cover - a new endpoint must be explicitly governed - raise AssertionError(f"unexpected endpoint: {path}") - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - ["owner/repo"], runner=runner, generated_at="2026-08-19T11:00:00Z" - ) - - run_paths = [path for path in requested_paths if "/actions/runs?status=" in path] - assert "repos/owner/repo/actions/runs?per_page=50" not in requested_paths - assert run_paths == [ - *[ - f"repos/owner/repo/actions/runs?status={status}&per_page=50" - for status in ("in_progress", "pending", "queued", "requested", "waiting") - ], - *[ - f"repos/owner/repo/actions/runs?status={status}&per_page=50" - for status in ("waiting", "requested", "queued", "pending", "in_progress") - ], - "repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50", - ] - assert not any("page=2" in path for path in requested_paths) - assert {run["status"] for run in snapshot["repositories"][0]["runs"]} == { - "IN_PROGRESS", - "PENDING", - "QUEUED", - "REQUESTED", - "WAITING", - } - - queued_reads = 0 - - def changing_runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Expose a queue transition between the two bounded status sweeps.""" - nonlocal queued_reads - path = args[-1] - if path == "repos/owner/repo": - payload: object = {"default_branch": "main"} - elif path == "repos/owner/repo/pulls?state=open&per_page=100": - payload = [] - elif "/actions/runs?status=" in path: - status = path.split("status=", 1)[1].split("&", 1)[0] - if status == "queued": - queued_reads += 1 - payload = [workflow_run(900, status=status)] if queued_reads == 1 else [] - else: - payload = [] - else: # pragma: no cover - a new endpoint must be explicitly governed - raise AssertionError(f"unexpected endpoint: {path}") - if "--paginate" in args: - payload = [payload] - return CompletedProcess(args, 0, json.dumps(payload), "") - - changing_snapshot = queue_health.collect_snapshot(["owner/repo"], runner=changing_runner) - assert changing_snapshot["repositories"] == [] - assert changing_snapshot["collection_errors"] == [ - { - "repository": "owner/repo", - "error": "active workflow run snapshot changed during collection", - } - ] - - -def test_load_snapshot_and_identity_helpers(tmp_path: Path) -> None: - """Load offline snapshots and classify current, obsolete, and unlinked runs.""" - path = tmp_path / "snapshot.json" - path.write_text(json.dumps(report_snapshot()), encoding="utf-8") - assert queue_health.load_snapshot(path)["generated_at"] == "2026-08-19T11:00:00Z" - path.write_text("[]", encoding="utf-8") - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_snapshot(path) - path.write_text("{", encoding="utf-8") - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_snapshot(path) - with pytest.raises(queue_health.QueueHealthError): - queue_health.load_snapshot(tmp_path / "missing.json") - - current_run = {"head_sha": "head", "pull_requests": [{"number": 1, "head_sha": "head"}]} - assert queue_health._run_identity(current_run, {1: {"head_sha": "head"}}) == ("current_head", 1) - assert queue_health._run_identity(current_run, {1: {"head_sha": "other"}}) == ("obsolete", 1) - assert queue_health._run_identity({"pull_requests": []}, {}) == ("unlinked", None) - - # pull_request_target runs report the *base*-branch commit as their - # run-level head_sha; identity must use the linked entry's head_sha - # (preserved by _normalise_run) instead, or an active run looks obsolete. - base_triggered_run = { - "head_sha": "base-branch-checkout-sha", - "pull_requests": [{"number": 1, "head_sha": "pr-head-sha"}], - } - assert queue_health._run_identity(base_triggered_run, {1: {"head_sha": "pr-head-sha"}}) == ( - "current_head", - 1, - ) - stale_base_triggered_run = { - "head_sha": "base-branch-checkout-sha", - "pull_requests": [{"number": 1, "head_sha": "old-pr-head-sha"}], - } - assert queue_health._run_identity( - stale_base_triggered_run, {1: {"head_sha": "pr-head-sha"}} - ) == ("obsolete", 1) - - -def test_job_state_and_queue_age_cover_pending_terminal_and_unknown_paths() -> None: - """Classify job assignment states and calculate bounded queue age.""" - assert queue_health._job_state({"status": "queued", "runner_id": 1}) == ("queued_assigned", True, True) - assert queue_health._job_state({"status": "in_progress", "runner_name": "runner"}) == ( - "queued_assigned", - True, - True, - ) - assert queue_health._job_state({"status": "queued"}) == ("queued_unassigned", True, False) - assert queue_health._job_state({"status": "pending"}) == ("queued_unassigned", True, False) - assert queue_health._job_state({"status": "requested"}) == ("queued_unassigned", True, False) - assert queue_health._job_state({"status": "completed"}) == ("terminal", False, False) - assert queue_health._job_state({"status": "", "conclusion": "failure"}) == ("terminal", False, False) - # "waiting" (paused on an environment/deployment approval) is pending - # evidence, not unclassified "unknown" evidence that drops off the report. - assert queue_health._job_state({"status": "waiting"}) == ("waiting_approval", True, False) - assert queue_health._job_state({"status": "waiting", "runner_id": 3}) == ( - "waiting_approval", - True, - True, - ) - assert queue_health._format_age("2026-08-19T10:00:00Z", NOW) == 7200 - assert queue_health._format_age("2026-08-19T13:00:00Z", NOW) == 0 - with pytest.raises(queue_health.QueueHealthError): - queue_health._format_age("bad", NOW) - - -def test_build_report_classifies_exact_head_and_external_blockers() -> None: - """Report exact-head, stale, unlinked, terminal, and SLO evidence separately.""" - report = queue_health.build_report(report_snapshot(), now=NOW, queue_age_slo_seconds=900) - assert report["schema_version"] == "actions.queue_health.v1" - assert report["summary"]["observed_job_count"] == 7 - # job 102 (run 10) is "waiting" on an environment/deployment approval; it - # must be visible as pending evidence, not silently dropped. - assert report["summary"]["pending_job_count"] == 6 - assert report["summary"]["current_head_pending_count"] == 4 - assert report["summary"]["unassigned_slo_breached_count"] == 2 - assert report["summary"]["obsolete_job_count"] == 1 - assert report["summary"]["unlinked_job_count"] == 1 - assert report["summary"]["duplicate_pending_lane_count"] == 1 - assert report["summary"]["terminal_job_count"] == 1 - # Run 10 has two pending jobs and run 13 has one fallback row; the metric - # counts concurrent runs, not the number of pending jobs in those runs. - assert report["duplicate_pending_lanes"][0]["count"] == 2 - assert any(row["blocker"] == "obsolete_run_requires_identity_confirmed_cleanup" for row in report["runs"]) - assert any(row["blocker"] == "run_not_linked_to_pull_request" for row in report["runs"]) - waiting_row = next(row for row in report["runs"] if row["job_id"] == 102) - assert waiting_row["execution_state"] == "waiting_approval" - assert waiting_row["is_pending"] is True - assert waiting_row["blocker"] == "environment_or_deployment_approval_required" - assert waiting_row["recommended_action"] == "reviewer_or_owner_approve_pending_environment_deployment" - # A waiting-on-approval job must never be folded into the runner-capacity - # SLO metric, which is reserved for queued_unassigned jobs. - assert report["summary"]["unassigned_slo_breached_count"] == 2 - assert report["runs"] == sorted(report["runs"], key=lambda row: (row["repository"], row["run_id"], row["job_id"])) - assert queue_health.build_report(report_snapshot(), now=NOW, queue_age_slo_seconds=7200)["summary"]["unassigned_slo_breached_count"] == 0 - assert queue_health.build_report(report_snapshot(), queue_age_slo_seconds=0)["summary"]["observed_job_count"] == 7 - - -def test_build_report_treats_pull_request_target_linked_head_as_current() -> None: - """A pull_request_target run's base-commit head_sha must not look obsolete. - - For a ``pull_request_target``-triggered run, GitHub reports the checked - out *base*-branch commit as the run-level ``head_sha``, while the run's - linked pull-request entry still carries the real PR head SHA. The run - must classify as ``current_head`` (and have its job evidence inspected) - whenever that linked head SHA matches the currently open pull request. - """ - snapshot = { - "generated_at": "2026-08-19T11:00:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [pull_request(1, "pr-head-sha")], - "runs": [ - workflow_run( - 50, - head_sha="base-branch-checkout-sha", - pull_requests=[{"number": 1, "head": {"sha": "pr-head-sha"}}], - jobs=[job(500)], - workflow_name="opencode-review", - ), - ], - } - ], - } - report = queue_health.build_report(snapshot, now=NOW) - row = report["runs"][0] - assert row["identity_state"] == "current_head" - assert row["obsolete"] is False - assert row["blocker"] != "obsolete_run_requires_identity_confirmed_cleanup" - - -def test_build_report_measures_job_wait_from_job_created_at_not_run_creation() -> None: - """A freshly queued job inside an old in-progress run must not inherit its age.""" - old_run_created_at = "2026-08-19T08:00:00Z" - recent_job_created_at = "2026-08-19T11:55:00Z" - snapshot = { - "generated_at": "2026-08-19T11:00:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [pull_request()], - "runs": [ - workflow_run( - 60, - created_at=old_run_created_at, - status="in_progress", - pull_requests=[{"number": 1, "head": {"sha": "head"}}], - jobs=[ - job(600, status="in_progress", runner_id=1, runner_name="runner-1"), - { - "id": 601, - "name": "second-stage", - "status": "queued", - "conclusion": None, - "runner_id": None, - "runner_name": None, - "created_at": recent_job_created_at, - "steps": [], - }, - ], - ), - ], - } - ], - } - report = queue_health.build_report(snapshot, now=NOW, queue_age_slo_seconds=900) - second_stage = next(row for row in report["runs"] if row["job_id"] == 601) - assert second_stage["queue_age_seconds"] == 300 - assert second_stage["slo_breached"] is False - assert second_stage["blocker"] == "current_head_required_evidence_incomplete" - # The first-stage job is still measured against the run's own (old) - # creation time because it carries no job-level created_at of its own, - # but it is runner-assigned so it never counts as an unassigned breach. - first_stage = next(row for row in report["runs"] if row["job_id"] == 600) - assert first_stage["queue_age_seconds"] == 14400 - assert report["summary"]["unassigned_slo_breached_count"] == 0 - - -@pytest.mark.parametrize( - "snapshot, message", - [ - ({"generated_at": "2026-08-19T11:00:00Z", "repositories": "bad"}, "repositories"), - ({"generated_at": "2026-08-19T11:00:00Z", "repositories": ["bad"]}, "repository entry"), - ( - {"generated_at": "2026-08-19T11:00:00Z", "repositories": [{"full_name": "owner/repo", "pull_requests": "bad", "runs": []}]}, - "pull requests", - ), - ( - {"generated_at": "2026-08-19T11:00:00Z", "repositories": [{"full_name": "owner/repo", "pull_requests": [], "runs": "bad"}]}, - "runs", - ), - ( - {"generated_at": "2026-08-19T11:00:00Z", "repositories": [{"full_name": "owner/repo", "pull_requests": [], "runs": ["bad"]}]}, - "workflow run entry", - ), - ], -) -def test_build_report_rejects_malformed_snapshot_shapes(snapshot: dict, message: str) -> None: - """Reject malformed snapshot containers before counting jobs.""" - with pytest.raises(queue_health.QueueHealthError, match=message): - queue_health.build_report(snapshot, now=NOW) - - -def test_build_report_rejects_duplicate_and_invalid_entries() -> None: - """Reject duplicate identities and invalid report boundaries.""" - duplicate_repository = report_snapshot() - duplicate_repository["repositories"].append( - dict(duplicate_repository["repositories"][0]) - ) - with pytest.raises(queue_health.QueueHealthError, match="duplicate repository entry"): - queue_health.build_report(duplicate_repository, now=NOW) - - duplicate_pr = report_snapshot() - duplicate_pr["repositories"][0]["pull_requests"].append(pull_request(1, "other")) - with pytest.raises(queue_health.QueueHealthError, match="duplicate pull request"): - queue_health.build_report(duplicate_pr, now=NOW) - - duplicate_run = report_snapshot() - duplicate_run["repositories"][0]["runs"].append(workflow_run(10)) - with pytest.raises(queue_health.QueueHealthError, match="duplicate workflow run"): - queue_health.build_report(duplicate_run, now=NOW) - - invalid_jobs = report_snapshot() - invalid_jobs["repositories"][0]["runs"][0]["jobs"] = "bad" - with pytest.raises(queue_health.QueueHealthError, match="jobs"): - queue_health.build_report(invalid_jobs, now=NOW) - - with pytest.raises(queue_health.QueueHealthError, match="negative"): - queue_health.build_report(report_snapshot(), now=NOW, queue_age_slo_seconds=-1) - with pytest.raises(queue_health.QueueHealthError, match="timestamp"): - queue_health.build_report({"generated_at": "bad", "repositories": []}, now=NOW) - with pytest.raises(queue_health.QueueHealthError, match="evaluation time"): - queue_health.build_report(report_snapshot(), now=datetime(2026, 8, 19, 12, 0)) - - for key in ("pull_requests", "runs"): - null_entry = {"generated_at": "2026-08-19T11:00:00Z", "repositories": [{"full_name": "owner/repo", key: None}]} - assert queue_health.build_report(null_entry, now=NOW)["summary"]["observed_job_count"] == 0 - null_jobs = { - "generated_at": "2026-08-19T11:00:00Z", - "repositories": [{"full_name": "owner/repo", "runs": [{"id": 1, "created_at": "2026-08-19T10:00:00Z", "jobs": None}]}], - } - assert queue_health.build_report(null_jobs, now=NOW)["summary"]["observed_job_count"] == 1 - - -def test_render_and_write_reports_escape_fields_and_support_empty_reports(tmp_path: Path) -> None: - """Escape HTML fields and write both populated and empty reports.""" - report = queue_health.build_report(report_snapshot(), now=NOW) - report["runs"][0]["blocker"] = "" - rendered = queue_health.render_html(report) - assert "<script>" in rendered - assert 'owner/repo' in rendered - assert "queue-age SLO: 900 seconds" in rendered - - empty = queue_health.build_report({"generated_at": "2026-08-19T11:00:00Z", "repositories": []}, now=NOW) - assert "No queued or in-progress jobs observed." in queue_health.render_html(empty) - - json_path = tmp_path / "nested" / "report.json" - html_path = tmp_path / "nested" / "report.html" - queue_health.write_reports(report, json_path, html_path) - assert json.loads(json_path.read_text(encoding="utf-8"))["schema_version"] == "actions.queue_health.v1" - assert " None: - """Exercise snapshot mode, allowlist mode, and bounded CLI failures.""" - args = queue_health.parse_args( - ["--snapshot", "snapshot.json", "--output-json", "out.json", "--output-html", "out.html"] - ) - assert args.snapshot == Path("snapshot.json") - args = queue_health.parse_args( - ["--allowlist", "allowlist.json", "--output-json", "out.json", "--output-html", "out.html"] - ) - assert args.allowlist == Path("allowlist.json") - with pytest.raises(SystemExit): - queue_health.parse_args(["--snapshot", "a", "--allowlist", "b", "--output-json", "o", "--output-html", "h"]) - - snapshot_path = tmp_path / "snapshot.json" - snapshot_path.write_text(json.dumps(report_snapshot()), encoding="utf-8") - json_path = tmp_path / "out.json" - html_path = tmp_path / "out.html" - assert queue_health.main( - [ - "--snapshot", - str(snapshot_path), - "--output-json", - str(json_path), - "--output-html", - str(html_path), - "--now", - "2026-08-19T12:00:00Z", - ] - ) == 0 - assert "QUEUE_HEALTH_RESULT=" in capsys.readouterr().out - - empty_snapshot_path = tmp_path / "empty-snapshot.json" - empty_snapshot_path.write_text( - json.dumps({"generated_at": "2026-08-19T11:00:00Z", "repositories": []}), - encoding="utf-8", - ) - assert queue_health.main( - [ - "--snapshot", - str(empty_snapshot_path), - "--output-json", - str(json_path), - "--output-html", - str(html_path), - "--now", - "2026-08-19T12:00:00Z", - ] - ) == 0 - assert "::warning::" not in capsys.readouterr().out - - error = io.StringIO() - assert queue_health.main( - ["--snapshot", str(tmp_path / "missing.json"), "--output-json", "o", "--output-html", "h"], - stderr=error, - ) == 2 - assert "ERROR:" in error.getvalue() - - allowlist_path = tmp_path / "allowlist.json" - allowlist_path.write_text(json.dumps(["owner/repo"]), encoding="utf-8") - original_collect = queue_health.collect_snapshot - queue_health.collect_snapshot = lambda repositories: report_snapshot() # type: ignore[assignment] - try: - assert queue_health.main( - ["--allowlist", str(allowlist_path), "--output-json", str(json_path), "--output-html", str(html_path)] - ) == 0 - finally: - queue_health.collect_snapshot = original_collect - assert "QUEUE_HEALTH_RESULT=" in capsys.readouterr().out diff --git a/tests/test_actions_queue_health_cancelled_before_runner.py b/tests/test_actions_queue_health_cancelled_before_runner.py deleted file mode 100644 index 1827275f68..0000000000 --- a/tests/test_actions_queue_health_cancelled_before_runner.py +++ /dev/null @@ -1,322 +0,0 @@ -"""Regression coverage for workflow cancellation before runner assignment.""" - -from __future__ import annotations - -import importlib.util -import json -from datetime import datetime, timezone -from pathlib import Path -from subprocess import CompletedProcess - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -def test_collect_snapshot_classifies_cancelled_job_before_runner_assignment() -> None: - """A cancelled current-head job with no runner or steps stays explicit evidence.""" - repository_name = "owner/repo" - pull_request = { - "number": 17, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": repository_name}}, - "head": {"sha": "exact-head"}, - "updated_at": "2026-09-02T13:15:00Z", - } - cancelled_run = { - "id": 1701, - "name": "Repository Metadata Reconcile", - "workflow_id": 9017, - "event": "pull_request", - "status": "completed", - "conclusion": "cancelled", - "head_sha": "exact-head", - "created_at": "2026-09-02T13:00:00Z", - "updated_at": "2026-09-02T13:08:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 17, "head": {"sha": "exact-head"}}], - } - cancelled_job = { - "id": 17001, - "name": "validate", - "status": "completed", - "conclusion": "cancelled", - "runner_id": 0, - "runner_name": "", - "created_at": "2026-09-02T13:00:00Z", - "steps": [], - } - skipped_job = { - "id": 17002, - "name": "publish optional evidence", - "status": "completed", - "conclusion": "skipped", - "runner_id": 0, - "runner_name": "", - "created_at": "2026-09-02T13:00:00Z", - "steps": [], - } - missing_steps_job = { - "id": 17003, - "name": "cancelled without step evidence", - "status": "completed", - "conclusion": "cancelled", - "runner_id": 0, - "runner_name": "", - "created_at": "2026-09-02T13:00:00Z", - } - null_steps_job = { - **missing_steps_job, - "id": 17004, - "name": "cancelled with null step evidence", - "steps": None, - } - terminal_path = ( - f"repos/{repository_name}/actions/runs?status=completed" - "&head_sha=exact-head&per_page=50" - ) - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Return deterministic GitHub REST fixtures for the collector.""" - path = args[-1] - if path == f"repos/{repository_name}": - payload: object = {"default_branch": "main"} - elif path == f"repos/{repository_name}/pulls?state=open&per_page=100": - payload = [pull_request] - elif path == terminal_path: - payload = {"total_count": 1, "workflow_runs": [cancelled_run]} - elif path == f"repos/{repository_name}/actions/runs/1701/jobs?per_page=100": - payload = { - "total_count": 4, - "jobs": [ - cancelled_job, - skipped_job, - missing_steps_job, - null_steps_job, - ], - } - elif "status=startup_failure" in path: - raise AssertionError( - "GitHub workflow-run status filtering does not accept startup_failure" - ) - elif path.startswith(f"repos/{repository_name}/actions/runs?status="): - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - unexpected API expansion must fail loudly. - raise AssertionError(f"unexpected GitHub API path: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - [repository_name], - runner=runner, - generated_at="2026-09-02T13:16:00Z", - ) - - assert snapshot["collection_errors"] == [] - assert [run["id"] for run in snapshot["repositories"][0]["runs"]] == [1701] - - report = queue_health.build_report( - snapshot, - now=datetime(2026, 9, 2, 13, 16, tzinfo=timezone.utc), - ) - cancelled_row = next(row for row in report["runs"] if row["job_id"] == 17001) - assert cancelled_row["identity_state"] == "current_head" - assert cancelled_row["run_conclusion"] == "CANCELLED" - assert cancelled_row["jobs_materialized"] is True - assert cancelled_row["runner_assigned"] is False - assert cancelled_row["admission_state"] == "cancelled_before_runner_assignment" - assert cancelled_row["blocker"] == "cancelled_before_runner_assignment" - assert cancelled_row["recommended_action"] == ( - "inspect_actions_control_plane_without_leaf_bypass" - ) - - skipped_row = next(row for row in report["runs"] if row["job_id"] == 17002) - assert skipped_row["run_conclusion"] == "CANCELLED" - assert skipped_row["admission_state"] != "cancelled_before_runner_assignment" - for unavailable_step_job_id in (17003, 17004): - unavailable_step_row = next( - row for row in report["runs"] if row["job_id"] == unavailable_step_job_id - ) - assert unavailable_step_row["admission_state"] != ( - "cancelled_before_runner_assignment" - ) - assert report["summary"]["cancelled_before_runner_assignment_count"] == 1 - - -def test_collect_snapshot_retains_cancelled_pull_request_target_current_head() -> None: - """A target-triggered cancellation uses linked PR head identity, not base SHA.""" - repository_name = "owner/repo" - pull_request = { - "number": 23, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": repository_name}}, - "head": {"sha": "exact-target-head"}, - "updated_at": "2026-09-02T13:20:00Z", - } - cancelled_run = { - "id": 2301, - "name": "Target Review", - "workflow_id": 9023, - "event": "pull_request_target", - "status": "completed", - "conclusion": "cancelled", - "head_sha": "base-commit-sha", - "created_at": "2026-09-02T13:00:00Z", - "updated_at": "2026-09-02T13:05:00Z", - "run_attempt": 1, - "pull_requests": [ - {"number": 23, "head": {"sha": "exact-target-head"}} - ], - } - cancelled_job = { - "id": 23001, - "name": "review", - "status": "completed", - "conclusion": "cancelled", - "runner_id": 0, - "runner_name": "", - "created_at": "2026-09-02T13:00:00Z", - "steps": [], - } - head_terminal_path = ( - f"repos/{repository_name}/actions/runs?status=completed" - "&head_sha=exact-target-head&per_page=50" - ) - target_cancelled_path = ( - f"repos/{repository_name}/actions/runs?status=cancelled" - "&event=pull_request_target&per_page=50" - ) - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Model GitHub target runs whose run-level SHA is the base commit.""" - path = args[-1] - if path == f"repos/{repository_name}": - payload: object = {"default_branch": "main"} - elif path == f"repos/{repository_name}/pulls?state=open&per_page=100": - payload = [pull_request] - elif path == head_terminal_path: - payload = {"total_count": 0, "workflow_runs": []} - elif path == target_cancelled_path: - payload = {"total_count": 1, "workflow_runs": [cancelled_run]} - elif "status=startup_failure" in path: - raise AssertionError( - "GitHub workflow-run status filtering does not accept startup_failure" - ) - elif path == f"repos/{repository_name}/actions/runs/2301/jobs?per_page=100": - payload = {"total_count": 1, "jobs": [cancelled_job]} - elif path.startswith(f"repos/{repository_name}/actions/runs?status="): - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - unexpected API expansion must fail loudly. - raise AssertionError(f"unexpected GitHub API path: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - [repository_name], - runner=runner, - generated_at="2026-09-02T13:21:00Z", - ) - - assert snapshot["collection_errors"] == [] - assert [run["id"] for run in snapshot["repositories"][0]["runs"]] == [2301] - report = queue_health.build_report( - snapshot, - now=datetime(2026, 9, 2, 13, 21, tzinfo=timezone.utc), - ) - assert report["runs"][0]["identity_state"] == "current_head" - assert report["runs"][0]["admission_state"] == ( - "cancelled_before_runner_assignment" - ) - - -def test_collect_snapshot_rejects_head_change_after_target_evidence_read() -> None: - """Terminal evidence is rejected when its PR identity changes before completion.""" - repository_name = "owner/repo" - original_pull_request = { - "number": 29, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": repository_name}}, - "head": {"sha": "original-head"}, - "updated_at": "2026-09-02T13:22:00Z", - } - changed_pull_request = { - **original_pull_request, - "head": {"sha": "replacement-head"}, - "updated_at": "2026-09-02T13:24:00Z", - } - cancelled_run = { - "id": 2901, - "name": "Target Review", - "workflow_id": 9029, - "event": "pull_request_target", - "status": "completed", - "conclusion": "cancelled", - "head_sha": "base-commit-sha", - "created_at": "2026-09-02T13:00:00Z", - "updated_at": "2026-09-02T13:05:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 29, "head": {"sha": "original-head"}}], - } - cancelled_job = { - "id": 29001, - "name": "review", - "status": "completed", - "conclusion": "cancelled", - "runner_id": 0, - "runner_name": "", - "created_at": "2026-09-02T13:00:00Z", - "steps": [], - } - head_terminal_path = ( - f"repos/{repository_name}/actions/runs?status=completed" - "&head_sha=original-head&per_page=50" - ) - target_cancelled_path = ( - f"repos/{repository_name}/actions/runs?status=cancelled" - "&event=pull_request_target&per_page=50" - ) - pull_read_count = 0 - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Advance the PR head only after terminal/job evidence has been read.""" - nonlocal pull_read_count - path = args[-1] - if path == f"repos/{repository_name}": - payload: object = {"default_branch": "main"} - elif path == f"repos/{repository_name}/pulls?state=open&per_page=100": - pull_read_count += 1 - payload = [ - changed_pull_request if pull_read_count >= 3 else original_pull_request - ] - elif path == head_terminal_path: - payload = {"total_count": 0, "workflow_runs": []} - elif path == target_cancelled_path: - payload = {"total_count": 1, "workflow_runs": [cancelled_run]} - elif path == f"repos/{repository_name}/actions/runs/2901/jobs?per_page=100": - payload = {"total_count": 1, "jobs": [cancelled_job]} - elif "status=startup_failure" in path: - raise AssertionError( - "GitHub workflow-run status filtering does not accept startup_failure" - ) - elif path.startswith(f"repos/{repository_name}/actions/runs?status="): - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - unexpected API expansion must fail loudly. - raise AssertionError(f"unexpected GitHub API path: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - [repository_name], - runner=runner, - generated_at="2026-09-02T13:25:00Z", - ) - - assert snapshot["repositories"] == [] - assert snapshot["collection_errors"] == [ - { - "repository": repository_name, - "error": "pull-request identity snapshot changed during evidence collection", - } - ] - assert pull_read_count == 3 diff --git a/tests/test_actions_queue_health_contract.py b/tests/test_actions_queue_health_contract.py deleted file mode 100644 index ac7b27149a..0000000000 --- a/tests/test_actions_queue_health_contract.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Contract tests for the scheduled read-only Actions queue report.""" - -import json -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -def test_queue_health_workflow_is_scheduled_read_only_and_pinned() -> None: - """Keep the scheduled collector bounded, read-only, and supply-chain pinned.""" - workflow = (ROOT / ".github/workflows/actions-queue-health.yml").read_text(encoding="utf-8") - - assert 'cron: "7 * * * *"' in workflow - assert "workflow_dispatch:" not in workflow - assert "cancel-in-progress: false" in workflow - assert "timeout-minutes: 30" in workflow - assert "runs-on: ubuntu-24.04" in workflow - assert "actions: read" in workflow - assert "pull-requests: read" not in workflow - assert "contents: write" not in workflow - assert ( - "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }}" - in workflow - ) - assert "GH_TOKEN: ${{ github.token }}" not in workflow - assert "required for cross-repository queue reads" in workflow - assert "gh run cancel" not in workflow - assert "gh pr merge" not in workflow - assert "step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40" in workflow - assert "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" in workflow - assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in workflow - assert "actions_queue_health.py" in workflow - assert "actions_queue_health_repositories.json" in workflow - - -def test_queue_health_allowlist_is_explicit_and_bounded() -> None: - """Keep the first product slice limited to its reviewed repositories.""" - payload = json.loads( - (ROOT / "config/actions_queue_health_repositories.json").read_text(encoding="utf-8") - ) - assert payload == { - "repositories": [ - "ContextualWisdomLab/.github", - "ContextualWisdomLab/ConceptWeave", - "ContextualWisdomLab/ELUNVERA", - "ContextualWisdomLab/LineageWeave", - "ContextualWisdomLab/OriginWeave", - "ContextualWisdomLab/TEPP", - "ContextualWisdomLab/contextual-orchestrator", - "ContextualWisdomLab/disksage", - "ContextualWisdomLab/fast-mlsirm", - "ContextualWisdomLab/mhtml-etl-gateway", - "ContextualWisdomLab/naruon", - "ContextualWisdomLab/noema", - "ContextualWisdomLab/pg-llm-batch", - "ContextualWisdomLab/quarantine-sandbox-runtime", - ] - } diff --git a/tests/test_actions_queue_health_post_evidence_retry.py b/tests/test_actions_queue_health_post_evidence_retry.py deleted file mode 100644 index bc266fa96f..0000000000 --- a/tests/test_actions_queue_health_post_evidence_retry.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Regression tests for post-evidence pull-request identity retry semantics.""" - -import importlib.util -import json -from pathlib import Path -from subprocess import CompletedProcess - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health_post_evidence_retry", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -def _pull(head_sha: str = "head") -> dict: - """Return one complete raw open-pull-request identity fixture.""" - return { - "number": 1, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, - "head": {"sha": head_sha}, - "updated_at": "2026-09-02T14:00:00Z", - } - - -def _incomplete_pull() -> dict: - """Return a transiently incomplete identity fixture.""" - pull_request = _pull() - pull_request["head"] = {"sha": ""} - return pull_request - - -def _runner_with_post_evidence_identity_reads(*, persistent: bool): - """Return a runner that makes the post-evidence identity read incomplete.""" - pull_reads = 0 - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Serve stable queue evidence with a transient or persistent final identity gap.""" - nonlocal pull_reads - path = args[-1] - if path == "repos/owner/repo": - payload: object = {"default_branch": "main"} - elif path == "repos/owner/repo/pulls?state=open&per_page=100": - pull_reads += 1 - if pull_reads == 3 or (persistent and pull_reads >= 3): - payload = [_incomplete_pull()] - else: - payload = [_pull()] - elif "/actions/runs?" in path: - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - any new endpoint must be explicitly governed. - raise AssertionError(f"unexpected endpoint: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - return runner - - -def test_post_evidence_identity_read_retries_one_transient_incomplete_snapshot(monkeypatch) -> None: - """A transient incomplete post-evidence identity read receives one bounded retry.""" - monkeypatch.setattr(queue_health.time, "sleep", lambda _: None) - snapshot = queue_health.collect_snapshot( - ["owner/repo"], - runner=_runner_with_post_evidence_identity_reads(persistent=False), - generated_at="2026-09-02T14:00:00Z", - ) - assert snapshot["collection_errors"] == [] - assert len(snapshot["repositories"]) == 1 - assert snapshot["repositories"][0]["pull_requests"][0]["head_sha"] == "head" - - -def test_post_evidence_identity_read_fails_closed_after_retry_remains_incomplete(monkeypatch) -> None: - """Persistent incomplete post-evidence identity is repository-scoped failure.""" - monkeypatch.setattr(queue_health.time, "sleep", lambda _: None) - snapshot = queue_health.collect_snapshot( - ["owner/repo"], - runner=_runner_with_post_evidence_identity_reads(persistent=True), - generated_at="2026-09-02T14:00:00Z", - ) - assert snapshot["repositories"] == [] - assert len(snapshot["collection_errors"]) == 1 - assert snapshot["collection_errors"][0]["repository"] == "owner/repo" - assert "pull-request identity validation failed" in snapshot["collection_errors"][0]["error"] diff --git a/tests/test_actions_queue_health_queued_job_evidence.py b/tests/test_actions_queue_health_queued_job_evidence.py deleted file mode 100644 index db526b3874..0000000000 --- a/tests/test_actions_queue_health_queued_job_evidence.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Regression contract for queued current-head jobs that materialize after run start.""" - -from datetime import datetime, timezone -import importlib.util -import json -from pathlib import Path -from subprocess import CompletedProcess - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health_queued_job", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -def _pull_request() -> dict: - """Return the open PR whose current head owns the queued run.""" - return { - "number": 1, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, - "head": {"sha": "head"}, - "updated_at": "2026-09-17T02:55:00Z", - } - - -def _queued_run() -> dict: - """Return an old run whose downstream job only recently became eligible.""" - return { - "id": 910, - "workflow_id": 911, - "name": "required-check", - "event": "pull_request", - "status": "queued", - "conclusion": "", - "head_sha": "head", - "created_at": "2026-09-17T00:00:00Z", - "updated_at": "2026-09-17T02:58:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 1, "head": {"sha": "head"}}], - } - - -def _queued_job() -> dict: - """Return the current downstream job with its own later queue timestamp.""" - return { - "id": 912, - "name": "dispatch-current-head", - "status": "queued", - "conclusion": None, - "runner_id": None, - "runner_name": None, - "created_at": "2026-09-17T02:58:00Z", - "steps": [], - } - - -def test_queued_current_head_fetches_job_evidence_and_uses_job_queue_start() -> None: - """Time a materialized queued job from its own eligibility, not the parent run.""" - queued_run = _queued_run() - queued_job = _queued_job() - responses: dict[str, object] = { - "repos/owner/repo": {"default_branch": "main"}, - "repos/owner/repo/pulls?state=open&per_page=100": [_pull_request()], - "repos/owner/repo/actions/runs?status=queued&per_page=50": [queued_run], - "repos/owner/repo/actions/runs?status=completed&head_sha=head&per_page=50": [], - "repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50": [], - "repos/owner/repo/actions/runs/910/jobs?per_page=100": { - "total_count": 1, - "jobs": [queued_job], - }, - } - for status in ("in_progress", "pending", "requested", "waiting"): - responses[f"repos/owner/repo/actions/runs?status={status}&per_page=50"] = [] - - requested_paths: list[str] = [] - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Return deterministic REST payloads and retain the exact evidence reads.""" - path = args[-1] - requested_paths.append(path) - if path not in responses: - raise AssertionError(f"unexpected endpoint: {path}") - return CompletedProcess(args, 0, json.dumps(responses[path]), "") - - snapshot = queue_health.collect_snapshot( - ["owner/repo"], - runner=runner, - generated_at="2026-09-17T03:00:00Z", - ) - assert snapshot["collection_errors"] == [] - observed_run = snapshot["repositories"][0]["runs"][0] - assert "repos/owner/repo/actions/runs/910/jobs?per_page=100" in requested_paths - assert [job["id"] for job in observed_run["jobs"]] == [912] - assert observed_run["jobs"][0]["created_at"] == "2026-09-17T02:58:00Z" - - report = queue_health.build_report( - snapshot, - now=datetime(2026, 9, 17, 3, 0, tzinfo=timezone.utc), - ) - row = report["runs"][0] - assert row["job_id"] == 912 - assert row["queue_age_source"] == "job_created_at" - assert row["queue_age_started_at"] == "2026-09-17T02:58:00Z" - assert row["queue_age_seconds"] == 120 diff --git a/tests/test_actions_queue_health_snapshot_consistency.py b/tests/test_actions_queue_health_snapshot_consistency.py deleted file mode 100644 index b9711a09dd..0000000000 --- a/tests/test_actions_queue_health_snapshot_consistency.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Regression tests for stable queue-health identity and audit evidence.""" - -from datetime import datetime, timezone -import importlib.util -import json -from pathlib import Path -from subprocess import CompletedProcess - -import pytest - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health_consistency", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) -NOW = datetime(2026, 9, 2, 0, 0, tzinfo=timezone.utc) - - -def _pull(head_sha: str = "head") -> dict: - """Return one complete open pull-request identity fixture.""" - return { - "number": 1, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, - "head": {"sha": head_sha}, - "updated_at": "2026-09-01T23:00:00Z", - } - - -def _run(run_id: int, workflow_id: int, *, name: str = "shared-name") -> dict: - """Return one current-head queued workflow run with stable workflow identity.""" - return { - "id": run_id, - "workflow_id": workflow_id, - "name": name, - "event": "pull_request", - "status": "queued", - "conclusion": "", - "head_sha": "head", - "created_at": "2026-09-01T23:30:00Z", - "updated_at": "2026-09-01T23:30:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 1, "head": {"sha": "head"}}], - "jobs": [], - } - - -def _runner_with_pull_transition(final_pulls: list[dict]): - """Return a runner whose final pull read differs from its initial read.""" - pull_reads = 0 - - def runner(args: list[str], **kwargs: object) -> CompletedProcess[str]: - """Serve metadata, pull identities, and empty active-run partitions.""" - nonlocal pull_reads - path = args[-1] - if path == "repos/owner/repo": - payload: object = {"default_branch": "main"} - elif path == "repos/owner/repo/pulls?state=open&per_page=100": - pull_reads += 1 - payload = [_pull()] if pull_reads == 1 else final_pulls - elif "/actions/runs?status=" in path: - payload = [] - else: # pragma: no cover - any new endpoint must be explicitly governed. - raise AssertionError(f"unexpected endpoint: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - return runner - - -@pytest.mark.parametrize("final_pulls", [[_pull("new-head")], []]) -def test_collect_snapshot_rejects_pull_identity_changes_during_run_sweep( - final_pulls: list[dict], -) -> None: - """A concurrent push or closure cannot corrupt current-head classification.""" - snapshot = queue_health.collect_snapshot( - ["owner/repo"], - runner=_runner_with_pull_transition(final_pulls), - generated_at="2026-09-02T00:00:00Z", - ) - assert snapshot["repositories"] == [] - assert snapshot["collection_errors"] == [ - { - "repository": "owner/repo", - "error": "pull-request identity snapshot changed during collection", - } - ] - - -def test_distinct_workflow_ids_with_same_display_name_are_not_duplicate_lanes() -> None: - """Duplicate-lane evidence groups by stable workflow identity, not display name.""" - snapshot = { - "generated_at": "2026-09-01T23:45:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [_pull()], - "runs": [_run(100, 501), _run(101, 502)], - } - ], - } - report = queue_health.build_report(snapshot, now=NOW) - assert report["summary"]["duplicate_pending_lane_count"] == 0 - assert {row["workflow_id"] for row in report["runs"]} == {501, 502} - assert {row["workflow_identity"] for row in report["runs"]} == { - "workflow_id:501", - "workflow_id:502", - } - - -def test_same_workflow_id_across_runs_is_one_duplicate_lane() -> None: - """Two pending runs of one workflow remain a true duplicate execution lane.""" - snapshot = { - "generated_at": "2026-09-01T23:45:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [_pull()], - "runs": [_run(100, 501), _run(101, 501)], - } - ], - } - report = queue_health.build_report(snapshot, now=NOW) - assert report["summary"]["duplicate_pending_lane_count"] == 1 - assert report["duplicate_pending_lanes"] == [ - { - "repository": "owner/repo", - "pull_request_number": 1, - "workflow_identity": "workflow_id:501", - "workflow_name": "shared-name", - "count": 2, - } - ] - - -def test_queue_age_exports_the_timestamp_and_source_used_for_calculation() -> None: - """Report consumers can reproduce queue age from exported evidence.""" - run = _run(100, 501) - run["status"] = "in_progress" - run["jobs"] = [ - { - "id": 1000, - "name": "second-stage", - "status": "queued", - "conclusion": None, - "runner_id": None, - "runner_name": None, - "created_at": "2026-09-01T23:55:00Z", - "steps": [], - } - ] - report = queue_health.build_report( - { - "generated_at": "2026-09-01T23:56:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [_pull()], - "runs": [run], - } - ], - }, - now=NOW, - ) - row = report["runs"][0] - assert row["queue_age_started_at"] == "2026-09-01T23:55:00Z" - assert row["queue_age_source"] == "job_created_at" - assert row["queue_age_seconds"] == 300 - - -def test_invalid_present_workflow_id_fails_closed() -> None: - """Malformed stable workflow identity cannot silently fall back to a display name.""" - run = _run(100, 501) - run["workflow_id"] = "501" - with pytest.raises(queue_health.QueueHealthError, match="workflow id"): - queue_health.build_report( - { - "generated_at": "2026-09-01T23:45:00Z", - "repositories": [ - { - "full_name": "owner/repo", - "pull_requests": [_pull()], - "runs": [run], - } - ], - }, - now=NOW, - ) - - -def test_queue_health_workflow_does_not_grant_unused_pull_request_permission() -> None: - """The scheduler token keeps only permissions used outside the cross-repository token.""" - workflow = (ROOT / ".github/workflows/actions-queue-health.yml").read_text(encoding="utf-8") - assert "pull-requests: read" not in workflow diff --git a/tests/test_actions_queue_health_startup_failure.py b/tests/test_actions_queue_health_startup_failure.py deleted file mode 100644 index 7bd9cba97e..0000000000 --- a/tests/test_actions_queue_health_startup_failure.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Regression coverage for pre-job GitHub Actions startup failures.""" - -from __future__ import annotations - -import importlib.util -import json -from datetime import datetime, timezone -from pathlib import Path -from subprocess import CompletedProcess - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -def test_collect_snapshot_preserves_current_head_startup_failure_without_jobs() -> None: - """A terminal startup failure with zero jobs must remain visible and explicit.""" - repository_name = "owner/repo" - pull_request = { - "number": 7, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": repository_name}}, - "head": {"sha": "exact-head"}, - "updated_at": "2026-09-02T10:28:00Z", - } - startup_failure_run = { - "id": 701, - "name": "CodeQL PR", - "workflow_id": 9001, - "event": "pull_request", - "status": "completed", - "conclusion": "startup_failure", - "head_sha": "exact-head", - "created_at": "2026-09-02T10:28:00Z", - "updated_at": "2026-09-02T10:28:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 7, "head": {"sha": "exact-head"}}], - } - requested_paths: list[str] = [] - terminal_path = ( - f"repos/{repository_name}/actions/runs?status=completed" - "&head_sha=exact-head&per_page=50" - ) - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Return deterministic GitHub REST fixtures for the collector.""" - path = args[-1] - requested_paths.append(path) - if path == f"repos/{repository_name}": - payload: object = {"default_branch": "main"} - elif path == f"repos/{repository_name}/pulls?state=open&per_page=100": - payload = [pull_request] - elif path == terminal_path: - payload = {"total_count": 1, "workflow_runs": [startup_failure_run]} - elif path == f"repos/{repository_name}/actions/runs/701/jobs?per_page=100": - payload = {"total_count": 0, "jobs": []} - elif path.startswith(f"repos/{repository_name}/actions/runs?status="): - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - unexpected API expansion must fail loudly. - raise AssertionError(f"unexpected GitHub API path: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - [repository_name], - runner=runner, - generated_at="2026-09-02T10:30:00Z", - ) - - assert snapshot["collection_errors"] == [] - assert snapshot["repositories"][0]["runs"] == [ - { - "repository": repository_name, - "id": 701, - "workflow_name": "CodeQL PR", - "event": "pull_request", - "status": "COMPLETED", - "conclusion": "STARTUP_FAILURE", - "head_sha": "exact-head", - "created_at": "2026-09-02T10:28:00Z", - "updated_at": "2026-09-02T10:28:00Z", - "run_attempt": 1, - "concurrency_group": "unavailable_from_actions_api", - "pull_requests": [{"number": 7, "head_sha": "exact-head"}], - "jobs": [], - "workflow_id": 9001, - "workflow_identity": "workflow_id:9001", - } - ] - assert terminal_path in requested_paths - assert f"repos/{repository_name}/actions/runs/701/jobs?per_page=100" in requested_paths - - report = queue_health.build_report( - snapshot, - now=datetime(2026, 9, 2, 10, 30, tzinfo=timezone.utc), - ) - row = report["runs"][0] - assert row["identity_state"] == "current_head" - assert row["execution_state"] == "terminal" - assert row["run_conclusion"] == "STARTUP_FAILURE" - assert row["jobs_materialized"] is False - assert row["blocker"] == "startup_failure_before_job_materialization" - assert row["recommended_action"] == "inspect_actions_control_plane_without_leaf_bypass" - - -def test_collect_snapshot_retains_old_failure_for_unchanged_current_head() -> None: - """Current-head startup failures must not disappear merely because they are old.""" - repository_name = "owner/repo" - pull_request = { - "number": 8, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": repository_name}}, - "head": {"sha": "unchanged-head"}, - "updated_at": "2026-09-02T10:29:00Z", - } - old_current_failure = { - "id": 801, - "name": "CodeQL PR", - "workflow_id": 9001, - "event": "pull_request", - "status": "completed", - "conclusion": "startup_failure", - "head_sha": "unchanged-head", - "created_at": "2026-08-01T10:00:00Z", - "updated_at": "2026-08-01T10:00:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 8, "head": {"sha": "unchanged-head"}}], - } - terminal_path = ( - f"repos/{repository_name}/actions/runs?status=completed" - "&head_sha=unchanged-head&per_page=50" - ) - requested_paths: list[str] = [] - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Return an old but still current-head terminal failure by exact SHA.""" - path = args[-1] - requested_paths.append(path) - if path == f"repos/{repository_name}": - payload: object = {"default_branch": "main"} - elif path == f"repos/{repository_name}/pulls?state=open&per_page=100": - payload = [pull_request] - elif path == terminal_path: - payload = {"total_count": 1, "workflow_runs": [old_current_failure]} - elif path == f"repos/{repository_name}/actions/runs/801/jobs?per_page=100": - payload = {"total_count": 0, "jobs": []} - elif path.startswith(f"repos/{repository_name}/actions/runs?status="): - payload = {"total_count": 0, "workflow_runs": []} - else: # pragma: no cover - unexpected API expansion must fail loudly. - raise AssertionError(f"unexpected GitHub API path: {path}") - return CompletedProcess(args, 0, json.dumps(payload), "") - - snapshot = queue_health.collect_snapshot( - [repository_name], - runner=runner, - generated_at="2026-09-02T10:30:00Z", - ) - - assert snapshot["collection_errors"] == [] - assert [run["id"] for run in snapshot["repositories"][0]["runs"]] == [801] - assert terminal_path in requested_paths - assert not any("&created=" in path for path in requested_paths) diff --git a/tests/test_actions_queue_health_terminal_preexecution.py b/tests/test_actions_queue_health_terminal_preexecution.py deleted file mode 100644 index 05237ba340..0000000000 --- a/tests/test_actions_queue_health_terminal_preexecution.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Regression contracts for terminal failures that never obtained a runner.""" - -from datetime import datetime, timezone -import importlib.util -import json -from pathlib import Path -from subprocess import CompletedProcess - - -ROOT = Path(__file__).resolve().parents[1] -MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" -SPEC = importlib.util.spec_from_file_location("actions_queue_health_terminal", MODULE_PATH) -assert SPEC and SPEC.loader -queue_health = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(queue_health) - - -def _pull_request() -> dict: - """Return the exact open-PR identity used by the failed run.""" - return { - "number": 1, - "state": "open", - "base": {"ref": "main", "repo": {"full_name": "owner/repo"}}, - "head": {"sha": "head"}, - "updated_at": "2026-09-15T13:00:00Z", - } - - -def _terminal_failure_run() -> dict: - """Return a completed failure linked to the current pull-request head.""" - return { - "id": 900, - "workflow_id": 901, - "name": "required-check", - "event": "pull_request", - "status": "completed", - "conclusion": "failure", - "head_sha": "head", - "created_at": "2026-09-15T13:05:00Z", - "updated_at": "2026-09-15T13:06:00Z", - "run_attempt": 1, - "pull_requests": [{"number": 1, "head": {"sha": "head"}}], - } - - -def _terminal_failure_job() -> dict: - """Return a failed materialized job with no runner and no executed step.""" - return { - "id": 902, - "name": "required-check", - "status": "completed", - "conclusion": "failure", - "runner_id": None, - "runner_name": None, - "created_at": "2026-09-15T13:05:00Z", - "steps": [], - } - - -def test_terminal_preexecution_failure_survives_collection_and_is_not_product_failure() -> None: - """Keep failed zero-step jobs as explicit non-passing admission evidence.""" - failed_run = _terminal_failure_run() - failed_job = _terminal_failure_job() - responses: dict[str, object] = { - "repos/owner/repo": {"default_branch": "main"}, - "repos/owner/repo/pulls?state=open&per_page=100": [_pull_request()], - "repos/owner/repo/actions/runs?status=completed&head_sha=head&per_page=50": [failed_run], - "repos/owner/repo/actions/runs?status=cancelled&event=pull_request_target&per_page=50": [], - "repos/owner/repo/actions/runs/900/jobs?per_page=100": { - "total_count": 1, - "jobs": [failed_job], - }, - } - for status in ("in_progress", "pending", "queued", "requested", "waiting"): - responses[f"repos/owner/repo/actions/runs?status={status}&per_page=50"] = [] - - def runner(args: list[str], **_: object) -> CompletedProcess[str]: - """Return deterministic GitHub REST payloads for the regression specimen.""" - path = args[-1] - if path not in responses: - raise AssertionError(f"unexpected endpoint: {path}") - return CompletedProcess(args, 0, json.dumps(responses[path]), "") - - snapshot = queue_health.collect_snapshot( - ["owner/repo"], - runner=runner, - generated_at="2026-09-15T13:10:00Z", - ) - observed_runs = snapshot["repositories"][0]["runs"] - assert [run["id"] for run in observed_runs] == [900] - assert observed_runs[0]["jobs"][0]["steps_count"] == 0 - assert observed_runs[0]["jobs"][0]["runner_id"] == 0 - - report = queue_health.build_report( - snapshot, - now=datetime(2026, 9, 15, 13, 10, tzinfo=timezone.utc), - ) - row = report["runs"][0] - assert row["identity_state"] == "current_head" - assert row["run_conclusion"] == "FAILURE" - assert row["execution_state"] == "terminal_pre_execution_failure" - assert row["admission_state"] == "terminal_pre_execution_failure" - assert row["is_pending"] is False - assert row["runner_assigned"] is False - assert row["blocker"] == "terminal_pre_execution_failure_before_runner_assignment" - assert row["recommended_action"] == "inspect_actions_control_plane_without_leaf_bypass" - assert report["summary"]["terminal_pre_execution_failure_count"] == 1 - assert report["summary"]["terminal_job_count"] == 1 diff --git a/tests/test_agent_review_runtime_quality_consolidation.py b/tests/test_agent_review_runtime_quality_consolidation.py index 2b0e83312b..4592cfd166 100644 --- a/tests/test_agent_review_runtime_quality_consolidation.py +++ b/tests/test_agent_review_runtime_quality_consolidation.py @@ -103,7 +103,6 @@ def test_consolidated_workflow_preserves_all_contract_suites() -> None: "tests/test_opencode_rust_coverage_toolchain_contract.py", "tests/test_docs_only_pr_runner_admission.py", "tests/test_strix_changed_path_policy.py", - "tests/test_strix_evidence_binding.py", "tests/test_strix_model_behavior_error.py", "tests/test_strix_nvidia_nim_not_found_fallback.py", "tests/test_strix_workflow_dependency_hashes.py", diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py deleted file mode 100644 index 817cd56497..0000000000 --- a/tests/test_codeql_ghas_configuration_identity.py +++ /dev/null @@ -1,497 +0,0 @@ -"""Contract tests for GHAS CodeQL base/head configuration identity pairing.""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import pytest - -from scripts.ci import codeql_ghas_configuration_identity as identity - - -def _analysis( - *, - commit_sha: str, - category: str, - analysis_key: str = identity.DEFAULT_SETUP_ANALYSIS_KEY, - tool: str = "CodeQL", -) -> dict[str, Any]: - """Build one code-scanning analysis fixture row.""" - return { - "commit_sha": commit_sha, - "category": category, - "analysis_key": analysis_key, - "tool": {"name": tool}, - "ref": "refs/heads/main", - } - - -def test_default_setup_identity_matches_ghas_warning_title(): - """Default setup rust identity renders the way GHAS titles the #2133 warning.""" - item = identity.default_setup_identity("rust") - assert item == ( - "dynamic/github-code-scanning/codeql:analyze", - "/language:rust", - ) - assert identity.format_identity(item) == "Default setup /language:rust" - - -def test_pairing_ready_when_base_and_head_share_default_setup_language(): - """Matching Default setup identities on exact base/head SHAs are continuous.""" - base_sha = "a" * 40 - head_sha = "b" * 40 - base = [ - _analysis(commit_sha=base_sha, category="/language:rust"), - _analysis(commit_sha=base_sha, category="/language:actions"), - ] - head = [ - _analysis(commit_sha=head_sha, category="/language:rust"), - _analysis(commit_sha=head_sha, category="/language:actions"), - ] - - ready, missing = identity.pairing_ready( - base, - head, - base_sha=base_sha, - head_sha=head_sha, - language="rust", - ) - - assert ready is True - assert missing == [] - - -def test_pairing_not_ready_when_head_missing_base_default_setup_language(): - """Negative case: base Default setup rust with no head match fails closed.""" - base_sha = "c" * 40 - head_sha = "d" * 40 - base = [ - _analysis(commit_sha=base_sha, category="/language:rust"), - _analysis(commit_sha=base_sha, category="/language:actions"), - ] - # Head only published the fast actions shard — the #2133 race. - head = [_analysis(commit_sha=head_sha, category="/language:actions")] - - ready, missing = identity.pairing_ready( - base, - head, - base_sha=base_sha, - head_sha=head_sha, - language="rust", - ) - - assert ready is False - assert missing == [identity.default_setup_identity("rust")] - assert identity.format_identity(missing[0]) == "Default setup /language:rust" - - -def test_pairing_ignores_other_tools_and_unrelated_commits(): - """Non-CodeQL rows and other SHAs cannot satisfy or poison the contract.""" - base_sha = "e" * 40 - head_sha = "f" * 40 - base = [_analysis(commit_sha=base_sha, category="/language:rust")] - head = [ - _analysis(commit_sha=head_sha, category="/language:rust", tool="Semgrep OSS"), - _analysis(commit_sha="0" * 40, category="/language:rust"), - _analysis( - commit_sha=head_sha, - category="/language:rust", - analysis_key=".github/workflows/other.yml:analyze", - ), - ] - - ready, missing = identity.pairing_ready( - base, - head, - base_sha=base_sha, - head_sha=head_sha, - language="rust", - ) - - assert ready is False - assert missing == [identity.default_setup_identity("rust")] - - -def test_pairing_ready_when_base_has_no_language_configuration(): - """A language absent from the base does not demand a head configuration.""" - base_sha = "1" * 40 - head_sha = "2" * 40 - ready, missing = identity.pairing_ready( - [_analysis(commit_sha=base_sha, category="/language:actions")], - [], - base_sha=base_sha, - head_sha=head_sha, - language="rust", - ) - assert ready is True - assert missing == [] - - -def test_incompatible_analysis_keys_are_not_interchangeable(): - """Advanced-setup uploads do not satisfy a Default setup base identity.""" - base_sha = "3" * 40 - head_sha = "4" * 40 - base = [_analysis(commit_sha=base_sha, category="/language:rust")] - head = [ - _analysis( - commit_sha=head_sha, - category="/language:rust", - analysis_key=".github/workflows/codeql-scan-dispatch.yml:scan", - ) - ] - - ready, missing = identity.pairing_ready( - base, - head, - base_sha=base_sha, - head_sha=head_sha, - language="rust", - ) - - assert ready is False - assert missing == [identity.default_setup_identity("rust")] - - -def test_wait_for_language_pairing_succeeds_after_retry(monkeypatch): - """Bounded polling accepts a head identity that appears on a later attempt.""" - base_sha = "5" * 40 - head_sha = "6" * 40 - head_attempts = {"n": 0} - sleeps: list[float] = [] - - def fake_list(repository, *, token, ref=None, per_page=100, timeout_seconds=30): - del repository, token, per_page, timeout_seconds - if ref and ref.endswith("/main"): - return [_analysis(commit_sha=base_sha, category="/language:rust")] - head_attempts["n"] += 1 - if head_attempts["n"] == 1: - return [_analysis(commit_sha=head_sha, category="/language:actions")] - return [_analysis(commit_sha=head_sha, category="/language:rust")] - - monkeypatch.setattr(identity, "list_codeql_analyses", fake_list) - - missing = identity.wait_for_language_pairing( - repository="ContextualWisdomLab/wardnet", - token="opaque", - base_ref="refs/heads/main", - base_sha=base_sha, - head_ref="refs/pull/129/head", - head_sha=head_sha, - language="rust", - attempts=3, - sleep_seconds=0.01, - sleeper=sleeps.append, - ) - - assert missing == [] - assert sleeps == [0.01] - assert head_attempts["n"] == 2 - - -def test_wait_for_language_pairing_fails_closed_when_budget_exhausted(monkeypatch): - """Exhausted polls raise with the rendered Default setup identity.""" - base_sha = "7" * 40 - head_sha = "8" * 40 - - def fake_list(repository, *, token, ref=None, per_page=100, timeout_seconds=30): - del repository, token, per_page, timeout_seconds - if ref and "main" in ref: - return [_analysis(commit_sha=base_sha, category="/language:rust")] - return [_analysis(commit_sha=head_sha, category="/language:actions")] - - monkeypatch.setattr(identity, "list_codeql_analyses", fake_list) - - with pytest.raises(identity.ConfigurationIdentityError) as excinfo: - identity.wait_for_language_pairing( - repository="ContextualWisdomLab/wardnet", - token="opaque", - base_ref="refs/heads/main", - base_sha=base_sha, - head_ref="refs/pull/129/head", - head_sha=head_sha, - language="rust", - attempts=2, - sleep_seconds=0.0, - sleeper=lambda _seconds: None, - ) - - assert "Default setup /language:rust" in str(excinfo.value) - - -def test_main_cli_returns_zero_when_pairing_is_ready(monkeypatch, capsys): - """The handler CLI exits 0 only after continuity is proven.""" - base_sha = "9" * 40 - head_sha = "a" * 40 - - def fake_wait(**kwargs): - assert kwargs["language"] == "rust" - return [] - - monkeypatch.setenv("GH_TOKEN", "opaque") - monkeypatch.setattr(identity, "wait_for_language_pairing", fake_wait) - - code = identity.main( - [ - "--repository", - "ContextualWisdomLab/wardnet", - "--base-ref", - "refs/heads/main", - "--base-sha", - base_sha, - "--head-ref", - "refs/pull/129/head", - "--head-sha", - head_sha, - "--language", - "rust", - "--attempts", - "1", - ] - ) - - assert code == 0 - assert "configuration identity is continuous" in capsys.readouterr().out - - -def test_main_cli_returns_one_on_configuration_identity_error(monkeypatch, capsys): - """CLI maps ConfigurationIdentityError to a fail-closed exit status.""" - monkeypatch.setenv("GH_TOKEN", "opaque") - - def fake_wait(**kwargs): - del kwargs - raise identity.ConfigurationIdentityError("Default setup /language:rust missing") - - monkeypatch.setattr(identity, "wait_for_language_pairing", fake_wait) - - code = identity.main( - [ - "--repository", - "ContextualWisdomLab/wardnet", - "--base-ref", - "refs/heads/main", - "--base-sha", - "b" * 40, - "--head-ref", - "refs/pull/1/head", - "--head-sha", - "c" * 40, - "--language", - "rust", - ] - ) - - assert code == 1 - assert "Default setup /language:rust missing" in capsys.readouterr().err - - -def test_language_category_rejects_unsafe_tokens(): - """Category construction fails closed on empty or path-like language tokens.""" - with pytest.raises(identity.ConfigurationIdentityError): - identity.language_category("") - with pytest.raises(identity.ConfigurationIdentityError): - identity.language_category("rust/../actions") - - -def test_fixture_roundtrip_json_shapes_match_github_analyses_api(tmp_path: Path): - """Fixture files stay loadable as GitHub analyses API list payloads.""" - payload = [ - _analysis(commit_sha="d" * 40, category="/language:rust"), - _analysis(commit_sha="e" * 40, category="/language:actions"), - ] - path = tmp_path / "analyses.json" - path.write_text(json.dumps(payload), encoding="utf-8") - loaded = json.loads(path.read_text(encoding="utf-8")) - ids = identity.iter_codeql_identities(loaded, commit_sha="d" * 40) - assert ids == {identity.default_setup_identity("rust")} - - -def test_iter_codeql_identities_covers_string_tool_and_invalid_rows(): - """String tool names, non-mapping rows, and empty identities are skipped safely.""" - rows = [ - "not-a-mapping", - { - "commit_sha": "a" * 40, - "category": "/language:rust", - "analysis_key": identity.DEFAULT_SETUP_ANALYSIS_KEY, - "tool": "CodeQL", - }, - { - "commit_sha": "a" * 40, - "category": "/language:rust", - "analysis_key": identity.DEFAULT_SETUP_ANALYSIS_KEY, - "tool": 12, - }, - { - "commit_sha": "a" * 40, - "category": "", - "analysis_key": identity.DEFAULT_SETUP_ANALYSIS_KEY, - "tool": {"name": "CodeQL"}, - }, - { - "commit_sha": "b" * 40, - "category": "/language:rust", - "analysis_key": identity.DEFAULT_SETUP_ANALYSIS_KEY, - "tool": {"name": "CodeQL"}, - }, - ] - found = identity.iter_codeql_identities(rows, commit_sha="a" * 40) - assert found == {identity.default_setup_identity("rust")} - # No commit filter still accepts every well-formed CodeQL row. - assert identity.default_setup_identity("rust") in identity.iter_codeql_identities(rows) - - -def test_missing_base_identities_without_language_filter_returns_all_gaps(): - """Omitting language keeps every unmatched base identity.""" - missing = identity.missing_base_identities( - [ - identity.default_setup_identity("rust"), - identity.default_setup_identity("actions"), - ], - [identity.default_setup_identity("actions")], - ) - assert missing == [identity.default_setup_identity("rust")] - - -def test_format_identity_renders_advanced_setup_keys(): - """Non-default analysis keys keep their workflow identity in the warning text.""" - item = ( - ".github/workflows/codeql-scan-dispatch.yml:scan", - "/language:rust", - ) - assert ( - identity.format_identity(item) - == ".github/workflows/codeql-scan-dispatch.yml:scan /language:rust" - ) - - -def test_configuration_identity_requires_both_fields(): - """Empty analysis_key or category fails closed.""" - with pytest.raises(identity.ConfigurationIdentityError): - identity.configuration_identity("", "/language:rust") - with pytest.raises(identity.ConfigurationIdentityError): - identity.configuration_identity(identity.DEFAULT_SETUP_ANALYSIS_KEY, "") - - -def test_wait_rejects_non_positive_attempt_budget(): - """A zero attempt budget is a contract error, not a silent success.""" - with pytest.raises(identity.ConfigurationIdentityError): - identity.wait_for_language_pairing( - repository="ContextualWisdomLab/wardnet", - token="opaque", - base_ref="refs/heads/main", - base_sha="a" * 40, - head_ref="refs/pull/1/head", - head_sha="b" * 40, - language="rust", - attempts=0, - sleep_seconds=0.0, - sleeper=lambda _seconds: None, - ) - - -def test_list_codeql_analyses_and_request_json_paths(monkeypatch): - """list_codeql_analyses validates inputs and decodes successful JSON lists.""" - - class _Response: - def read(self) -> bytes: - return json.dumps( - [_analysis(commit_sha="a" * 40, category="/language:rust")] - ).encode() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb) -> None: - del exc_type, exc, tb - - def fake_open(request, timeout=30): - del timeout - assert "tool_name=CodeQL" in request.full_url - assert "ref=refs%2Fheads%2Fmain" in request.full_url - return _Response() - - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", fake_open) - rows = identity.list_codeql_analyses( - "ContextualWisdomLab/wardnet", - token="opaque", - ref="refs/heads/main", - ) - assert len(rows) == 1 - - with pytest.raises(identity.ConfigurationIdentityError): - identity.list_codeql_analyses("wardnet", token="opaque") - with pytest.raises(identity.ConfigurationIdentityError): - identity.list_codeql_analyses("ContextualWisdomLab/wardnet", token="") - - -def test_request_json_maps_http_and_transport_failures(monkeypatch): - """HTTP and transport failures become ConfigurationIdentityError.""" - - class _HTTPError(identity.urllib.error.HTTPError): - def read(self) -> bytes: - return b"denied" - - def raise_http(request, timeout=30): - del request, timeout - raise _HTTPError("https://api.github.com/x", 403, "forbidden", hdrs=None, fp=None) - - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_http) - with pytest.raises(identity.ConfigurationIdentityError) as excinfo: - identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) - assert "HTTP 403" in str(excinfo.value) - - def raise_url(request, timeout=30): - del request, timeout - raise identity.urllib.error.URLError("down") - - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_url) - with pytest.raises(identity.ConfigurationIdentityError): - identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) - - -def test_request_json_rejects_empty_and_invalid_payloads(monkeypatch): - """Empty bodies decode to [] and invalid JSON fails closed.""" - - class _Empty: - def read(self) -> bytes: - return b" " - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb) -> None: - del exc_type, exc, tb - - monkeypatch.setattr( - identity._GITHUB_API_OPENER, - "open", - lambda request, timeout=30: _Empty(), - ) - assert identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) == [] - - class _Bad: - def read(self) -> bytes: - return b"{not-json" - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb) -> None: - del exc_type, exc, tb - - monkeypatch.setattr( - identity._GITHUB_API_OPENER, - "open", - lambda request, timeout=30: _Bad(), - ) - with pytest.raises(identity.ConfigurationIdentityError): - identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) - - -def test_list_codeql_analyses_rejects_non_list_payload(monkeypatch): - """A non-list analyses response fails closed.""" - monkeypatch.setattr(identity, "_request_json", lambda url, token, timeout_seconds: {"ok": True}) - with pytest.raises(identity.ConfigurationIdentityError): - identity.list_codeql_analyses("ContextualWisdomLab/wardnet", token="opaque") diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index ed632efa48..dc67eef258 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -329,41 +329,6 @@ def test_codeql_pr_one_shot_read_accepts_the_opencode_agent_creator(tmp_path: Pa assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout -def test_codeql_pr_one_shot_read_accepts_clean_gate_when_wake_step_failed_job( - tmp_path: Path, -) -> None: - """A clean SARIF gate must not inherit failure from a wake-only dispatch job (#2141).""" - head_sha = _TEST_HEAD_SHA - title = _dispatch_scan_title(head_sha=head_sha) - dispatch_result, verdict_result = _run_verdict_read( - tmp_path, - statuses=[], - dispatch_runs={"workflow_runs": [_completed_dispatch_run(title=title)]}, - dispatch_jobs={ - "jobs": [ - { - "name": "CodeQL dispatch scan (python)", - "conclusion": "failure", - "steps": [ - { - "name": "Enforce CodeQL Medium+ SARIF gate", - "conclusion": "success", - }, - { - "name": "Wake exact CodeQL required job", - "conclusion": "failure", - }, - ], - } - ] - }, - ) - assert dispatch_result.returncode == 0, dispatch_result.stderr + dispatch_result.stdout - assert verdict_result.returncode == 0, verdict_result.stderr + verdict_result.stdout - assert "completed CodeQL dispatch scan gate for python: success" in dispatch_result.stdout - assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout - - def test_codeql_pr_one_shot_read_accepts_completed_dispatch_scan_job_when_status_unpublishable( tmp_path: Path, ) -> None: @@ -550,45 +515,6 @@ def test_codeql_required_workflow_does_not_gain_actions_write() -> None: assert "actions: write" not in coordinator_permissions -def test_codeql_pr_jobs_hold_read_grants_private_consumers_need() -> None: - """analyze-head and dispatch-current-head need pull-requests/statuses reads. - - Consumer evidence: ContextualWisdomLab/late-life-anxiety-reanalysis PR #10 - (head a1cd5bc6783c6510dfcf937f523c733366e82213, run 34700410434). Both - required-workflow jobs failed at their first API call with - `gh: Resource not accessible by integration (HTTP 403)`: - - job "CodeQL compatibility analysis (python)" (job 103571590442), step - "Read current-head CodeQL dispatch verdict", calling - `gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"` with only - `contents: read` + `id-token: write` (effective token printed by the - runner: Contents: read, Metadata: read). - - job "Dispatch current-head CodeQL scan" (job 103571810868), step - "Dispatch current-head CodeQL scan", the same GET plus a later read of - `repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses`, with - `contents: read`, `id-token: write`, `actions: read`. - - Public consumers (fast-mlsirm, pg-erd-cloud, naruon, html4tree) passed - only because GET on a public repository does not need the grant. - GitHub's REST contract requires the `pull-requests: read` fine-grained - permission for "Get a pull request" and `statuses: read` for "List commit - statuses for a reference" on private repositories. - """ - workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - shard_permissions = workflow.split(" analyze-head:\n", 1)[1].split( - " strategy:\n", 1 - )[0] - coordinator_permissions = workflow.split(" dispatch-current-head:\n", 1)[1].split( - " steps:\n", 1 - )[0] - - for block in (shard_permissions, coordinator_permissions): - assert re.findall(r"^ pull-requests: (\w+)$", block, re.MULTILINE) == [ - "read" - ] - assert re.findall(r"^ statuses: (\w+)$", block, re.MULTILINE) == ["read"] - assert "actions: write" not in block - - def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( tmp_path: Path, ) -> None: diff --git a/tests/test_codeql_sarif_gate.py b/tests/test_codeql_sarif_gate.py index 1ab542dd89..186b9c80f1 100644 --- a/tests/test_codeql_sarif_gate.py +++ b/tests/test_codeql_sarif_gate.py @@ -203,135 +203,3 @@ def test_script_entrypoint_exits_with_main_status(tmp_path, monkeypatch): runpy.run_path(str(Path("scripts/ci/codeql_sarif_gate.py")), run_name="__main__") assert exc_info.value.code == 0 - - -def _extension_run(results: list[dict], *, driver_rules: list | None = None) -> dict: - """A run shaped like a real CodeQL artifact: 0 driver rules, rules in a query-pack extension.""" - extension_rules = [{"id": f"py/filler-{n}"} for n in range(17)] + [ - { - "id": "py/incomplete-url-substring-sanitization", - "properties": {"security-severity": "7.8", "tags": ["security", "external/cwe/cwe-020"]}, - "defaultConfiguration": {"level": "warning"}, - } - ] - return { - "tool": { - "driver": {"name": "CodeQL", "rules": driver_rules or []}, - "extensions": [{"name": "codeql/python-queries", "rules": extension_rules}], - }, - "results": results, - } - - -def test_gather_findings_resolves_rules_from_the_referenced_extension(tmp_path): - """Issue #2150: a result whose rule lives in tool.extensions must gate, not fail open.""" - _write_sarif( - tmp_path / "ext.sarif", - [ - _extension_run( - [ - { - "ruleId": "py/incomplete-url-substring-sanitization", - "rule": {"id": "py/incomplete-url-substring-sanitization", "index": 17, "toolComponent": {"index": 0}}, - "message": {"text": "doi check"}, - "locations": [{"physicalLocation": {"artifactLocation": {"uri": "src/x.py"}, "region": {"startLine": 4}}}], - }, - { - "ruleId": "py/incomplete-url-substring-sanitization", - "rule": {"index": 17, "toolComponent": {"name": "codeql/python-queries"}}, - "message": {"text": "by component name"}, - }, - ] - ) - ], - ) - - findings, total_results, _ = gate.gather_findings(tmp_path) - - assert total_results == 2 - assert [(f.rule_id, f.score, f.level, f.path, f.line) for f in findings] == [ - ("py/incomplete-url-substring-sanitization", 7.8, "warning", "src/x.py", 4), - ("py/incomplete-url-substring-sanitization", 7.8, "warning", "unknown", 0), - ] - - -def test_gather_findings_keeps_colliding_rule_ids_per_component(tmp_path): - """The same rule id in the driver and an extension resolves to the referenced component's metadata.""" - _write_sarif( - tmp_path / "collide.sarif", - [ - _extension_run( - [ - {"ruleId": "shared/id", "message": {"text": "driver copy"}}, - {"ruleId": "shared/id", "rule": {"toolComponent": {"index": 0}}, "message": {"text": "extension copy"}}, - ], - driver_rules=[{"id": "shared/id", "defaultConfiguration": {"level": "note"}}], - ) - ], - ) - # extension gets a colliding scored rule appended - payload = json.loads((tmp_path / "collide.sarif").read_text(encoding="utf-8")) - payload["runs"][0]["tool"]["extensions"][0]["rules"].append( - {"id": "shared/id", "properties": {"security-severity": "9.1"}} - ) - (tmp_path / "collide.sarif").write_text(json.dumps(payload), encoding="utf-8") - - findings, _, _ = gate.gather_findings(tmp_path) - - assert [(f.message, f.score) for f in findings] == [("extension copy", 9.1)] - - -@pytest.mark.parametrize( - "result", - [ - {"ruleId": "py/x", "rule": {"index": 17, "toolComponent": {"index": 5}}}, - {"ruleId": "py/x", "rule": {"index": 17, "toolComponent": {"name": "codeql/no-such-pack"}}}, - {"ruleId": "py/x", "rule": {"index": 99, "toolComponent": {"index": 0}}}, - {"ruleId": "py/other", "rule": {"index": 17, "toolComponent": {"index": 0}}}, - {"ruleId": "py/x", "rule": {"id": "py/y", "toolComponent": {"index": 0}}}, - {"rule": {"index": 3, "toolComponent": {"guid": "00000000-0000-0000-0000-000000000000"}}}, - {"ruleId": "py/x", "rule": {"toolComponent": {}}}, - ], - ids=["bad-component-index", "bad-component-name", "bad-rule-index", "indexed-rule-id-mismatch", "ruleId-vs-rule-id-mismatch", "bad-component-guid", "empty-component-reference"], -) -def test_gather_findings_fails_closed_on_unresolvable_rule_references(tmp_path, result): - """A rule reference that cannot be resolved, with no severity evidence, gates instead of passing.""" - _write_sarif(tmp_path / "bad.sarif", [_extension_run([dict(result, message={"text": "m"})])]) - - findings, _, _ = gate.gather_findings(tmp_path) - - assert len(findings) == 1 - assert findings[0].level == "unresolved-rule" - assert findings[0].score is None - assert gate.format_finding(findings[0]).startswith("CODEQL_FINDING rule=") - - -def test_gather_findings_uses_result_score_even_when_rule_is_unresolvable(tmp_path): - """Explicit result-level security-severity still decides gating when the rule cannot be resolved.""" - _write_sarif( - tmp_path / "scored.sarif", - [_extension_run([{"ruleId": "py/x", "rule": {"toolComponent": {"index": 9}}, "properties": {"security-severity": "1.0"}}])], - ) - - findings, _, _ = gate.gather_findings(tmp_path) - - assert findings == [] - - -def test_gather_findings_gates_an_unreferenced_result_on_its_own_score(tmp_path): - """A result with no rule reference at all is judged purely on its result-level severity.""" - _write_sarif(tmp_path / "bare.sarif", [_extension_run([{"properties": {"security-severity": "6.0"}}])]) - - findings, _, _ = gate.gather_findings(tmp_path) - - assert [(f.rule_id, f.score, f.level) for f in findings] == [("unknown", 6.0, "none")] - - -def test_gather_findings_leaves_resolved_non_security_extension_rules_alone(tmp_path): - """A resolved extension rule with no security metadata keeps the existing non-gating semantics.""" - _write_sarif( - tmp_path / "style.sarif", - [_extension_run([{"rule": {"index": 3, "toolComponent": {"index": 0}}, "level": "note", "message": {"text": "style"}}])], - ) - - assert gate.gather_findings(tmp_path)[0] == [] diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 3769f48314..dd30c8506d 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -17,8 +17,6 @@ import sys from pathlib import Path -import pytest - 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 ( @@ -35,12 +33,10 @@ "Bind workflow inputs to live organization pull request metadata", "Exchange OpenCode app token for target repository content reads", "Re-validate live pull request metadata before privileged scan", - "Fetch the pinned CodeQL SARIF gate and GHAS identity scripts", + "Fetch the pinned CodeQL SARIF gate script", "Materialize pull request head for CodeQL scan", - "Verify GHAS base/head CodeQL configuration identity", "Publish CodeQL dispatch status", - "Exchange OpenCode app token for run settlement", - "Settle exact CodeQL required run", + "Wake exact CodeQL required job", ) @@ -71,7 +67,7 @@ def test_codeql_scan_dispatch_workflow_structure(): workflow = WORKFLOW_PATH.read_text(encoding="utf-8") assert "name: CodeQL Scan Dispatch" in workflow - assert "types: [codeql-scan, codeql-scan-v2]" in workflow + assert "types: [codeql-scan]" in workflow # No workflow_dispatch: test_no_central_workflow_exposes_branch_selected_manual_dispatch # (tests/test_required_workflow_queue_contract.py) forbids it on every # central workflow because it lets a caller pick an arbitrary ref to run @@ -82,13 +78,7 @@ def test_codeql_scan_dispatch_workflow_structure(): assert workflow.count("github/codeql-action/init@") == 1 assert workflow.count("github/codeql-action/analyze@") == 1 assert "scripts/ci/codeql_sarif_gate.py" in workflow - assert "scripts/ci/codeql_ghas_configuration_identity.py" in workflow - assert "Verify GHAS base/head CodeQL configuration identity" in workflow - assert 'receipt_context="codeql-dispatch/${LANGUAGE}"' in workflow - assert 'receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in workflow - assert '-f context="$receipt_context"' in workflow - assert "github.event.client_payload.producer_source_sha" in workflow - assert 'receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}"' in workflow + assert 'context="codeql-dispatch/${LANGUAGE}"' in workflow assert "OPENCODE_REPOSITORY_DISPATCH_ACTOR" in workflow # Deliberately NOT vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS: that allowlist # scopes a gradual ~12-repo OpenCode review rollout, while ruleset @@ -147,12 +137,7 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'endpoint="${!#}"\n' - 'case "$endpoint" in\n' - ' repos/ContextualWisdomLab/.github/compare/*) printf \'%s\\n\' "$FAKE_SOURCE_COMPARE_JSON" ;;\n' - ' repos/ContextualWisdomLab/*/git/commits/*) printf \'%s\\n\' "$FAKE_PRODUCER_COMMIT_JSON" ;;\n' - ' *) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' - 'esac\n', + 'printf \'%s\\n\' "$FAKE_PULL_JSON"\n', encoding="utf-8", ) fake_gh.chmod(0o755) @@ -162,36 +147,19 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull_request), - "FAKE_SOURCE_COMPARE_JSON": "{}", - "FAKE_PRODUCER_COMMIT_JSON": json.dumps( - { - "sha": "c" * 40, - "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], - } - ), "GITHUB_OUTPUT": str(output), "DISPATCH_ACTOR": "seonghobae", "DISPATCH_SENDER": "seonghobae", "ALLOWED_DISPATCH_ACTOR": "seonghobae", - "DISPATCH_PROTOCOL": "codeql-scan-v2", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", "SUPPLIED_BASE_REF": "main", "SUPPLIED_BASE_SHA": "a" * 40, - "SUPPLIED_HEAD_ENVELOPE": json.dumps( - {"schema": "1", "ref": "feature", "sha": "b" * 40} - ), - "SUPPLIED_HEAD_SCHEMA": "1", "SUPPLIED_HEAD_REF": "feature", "SUPPLIED_HEAD_SHA": "b" * 40, - "SUPPLIED_LEGACY_HEAD_REF": "", - "SUPPLIED_LEGACY_HEAD_SHA": "", - "SUPPLIED_PRODUCER_SOURCE_SHA": "c" * 40, "SUPPLIED_MATRIX": json.dumps([{"language": "python", "build-mode": "none"}]), "SUPPLIED_REQUIRED_RUN_ID": "42", "SUPPLIED_REQUIRED_JOBS": json.dumps([{"language": "python", "job_id": 43}]), - "SUPPLIED_RERUN_MODE": "", - "SUPPLIED_RERUN_REQUEST": "null", "SUPPLIED_REQUIRED_JOB_ID": "", "SUPPLIED_REQUIRED_LANGUAGE": "", **env_overrides, @@ -205,28 +173,11 @@ def _matching_pull_request() -> dict: """A live PR payload that matches the default supplied metadata in _run_validate_step.""" return { "state": "open", - "merge_commit_sha": "c" * 40, "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, } -def _legacy_dispatch_env() -> dict[str, str]: - """Exact environment produced by the protected pre-v2 CodeQL client.""" - return { - "DISPATCH_PROTOCOL": "codeql-scan", - "SUPPLIED_HEAD_ENVELOPE": "null", - "SUPPLIED_HEAD_SCHEMA": "", - "SUPPLIED_HEAD_REF": "feature", - "SUPPLIED_HEAD_SHA": "b" * 40, - "SUPPLIED_LEGACY_HEAD_REF": "feature", - "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, - "SUPPLIED_PRODUCER_SOURCE_SHA": "", - "SUPPLIED_RERUN_MODE": "", - "SUPPLIED_RERUN_REQUEST": "null", - } - - def test_codeql_scan_dispatch_validate_step_accepts_matching_live_metadata(tmp_path): """A dispatch whose metadata matches the live PR produces the expected GITHUB_OUTPUT.""" result = _run_validate_step(tmp_path, {}, _matching_pull_request()) @@ -238,407 +189,11 @@ def test_codeql_scan_dispatch_validate_step_accepts_matching_live_metadata(tmp_p assert "head_sha=" + "b" * 40 in output_text assert '[{"language":"python","build-mode":"none"}]' in output_text assert "required_run_id=42" in output_text - assert "dispatch_protocol=v2" in output_text - assert "producer_source_sha=" + "c" * 40 in output_text assert '"job_id":43' in output_text.replace(" ", "") assert "required_job_id=" not in output_text assert "required_language=" not in output_text -def test_codeql_scan_dispatch_accepts_exact_protected_legacy_payload(tmp_path): - """The handler-first bootstrap keeps the current protected producer live.""" - result = _run_validate_step( - tmp_path, - _legacy_dispatch_env(), - _matching_pull_request(), - ) - - assert result.returncode == 0, result.stdout + result.stderr - output_text = result.output_path.read_text(encoding="utf-8") - assert "dispatch_protocol=legacy-v1" in output_text - assert "producer_source_sha=\n" in output_text - - -@pytest.mark.parametrize( - ("field_name", "field_value"), - [ - ("SUPPLIED_PRODUCER_SOURCE_SHA", "c" * 40), - ( - "SUPPLIED_HEAD_ENVELOPE", - json.dumps({"schema": "1", "ref": "feature", "sha": "b" * 40}), - ), - ( - "SUPPLIED_RERUN_REQUEST", - json.dumps( - { - "mode": "failed", - "required_jobs": [{"language": "python", "job_id": 43}], - } - ), - ), - ], -) -def test_codeql_scan_dispatch_legacy_protocol_rejects_v2_only_fields( - tmp_path, field_name, field_value -) -> None: - """A v2 payload cannot downgrade by selecting the legacy event type.""" - legacy_env = _legacy_dispatch_env() - legacy_env[field_name] = field_value - result = _run_validate_step(tmp_path, legacy_env, _matching_pull_request()) - - assert result.returncode == 1 - assert "Legacy CodeQL dispatch rejected v2-only identity fields" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_rejects_unknown_head_schema(tmp_path): - """Unknown nested-head schema versions fail before metadata can be trusted.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps( - {"schema": "2", "ref": "feature", "sha": "b" * 40} - ), - "SUPPLIED_HEAD_SCHEMA": "2", - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "unsupported pr_head schema=2" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_accepts_versioned_head_envelope(tmp_path): - """Schema-one nested head metadata reaches the live validation success path.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps( - {"schema": "1", "ref": "feature", "sha": "b" * 40} - ), - "SUPPLIED_HEAD_SCHEMA": "1", - "SUPPLIED_HEAD_REF": "feature", - "SUPPLIED_HEAD_SHA": "b" * 40, - }, - _matching_pull_request(), - ) - - assert result.returncode == 0 - assert ( - "Validated current live metadata for ContextualWisdomLab/naruon#42: base=main/" - in result.stdout - ) - assert "head=feature/" in result.stdout - - -@pytest.mark.parametrize( - ("legacy_ref", "legacy_sha"), - [ - ("feature-wrong", "b" * 40), - ("feature", "c" * 40), - ("feature", ""), - ("", "b" * 40), - ], -) -def test_codeql_scan_dispatch_validate_step_rejects_conflicting_dual_head_identity( - tmp_path, legacy_ref, legacy_sha -): - """Nested identity cannot shadow an unequal or partial legacy representation.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps( - {"schema": "1", "ref": "feature", "sha": "b" * 40} - ), - "SUPPLIED_HEAD_SCHEMA": "1", - "SUPPLIED_HEAD_REF": "feature", - "SUPPLIED_HEAD_SHA": "b" * 40, - "SUPPLIED_LEGACY_HEAD_REF": legacy_ref, - "SUPPLIED_LEGACY_HEAD_SHA": legacy_sha, - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "conflicting nested and legacy pr_head identity" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_rejects_numeric_head_schema(tmp_path): - """The JSON envelope schema stays a version string, not a numeric alias.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps( - {"schema": 1, "ref": "feature", "sha": "b" * 40} - ), - "SUPPLIED_HEAD_SCHEMA": "1", - "SUPPLIED_HEAD_REF": "feature", - "SUPPLIED_HEAD_SHA": "b" * 40, - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "invalid pr_head envelope" in result.stdout - - -@pytest.mark.parametrize("missing_field", ["ref", "sha"]) -def test_codeql_scan_dispatch_validate_step_rejects_incomplete_head_envelope( - tmp_path, missing_field -): - """A present envelope cannot borrow a required value from legacy fields.""" - envelope = {"schema": "1", "ref": "feature", "sha": "b" * 40} - del envelope[missing_field] - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps(envelope), - "SUPPLIED_HEAD_SCHEMA": "1", - "SUPPLIED_LEGACY_HEAD_REF": "feature", - "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, - "SUPPLIED_HEAD_REF": "feature", - "SUPPLIED_HEAD_SHA": "b" * 40, - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "invalid pr_head envelope" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_rejects_unversioned_head_envelope(tmp_path): - """A nested head tuple without its schema version fails closed.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_HEAD_ENVELOPE": json.dumps({"ref": "feature", "sha": "b" * 40}), - "SUPPLIED_HEAD_SCHEMA": "", - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "unsupported pr_head schema=" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_accepts_nested_rerun_request(tmp_path): - """The bounded ten-key producer envelope normalizes mode and job identities.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_REQUIRED_JOBS": "null", - "SUPPLIED_RERUN_REQUEST": json.dumps( - { - "schema": "1", - "mode": "failed", - "required_jobs": [{"language": "python", "job_id": 43}], - } - ), - }, - _matching_pull_request(), - ) - - assert result.returncode == 0, result.stderr - output_text = result.output_path.read_text(encoding="utf-8") - assert "rerun_mode=failed" in output_text - assert "rerun_schema=1" in output_text - assert '"job_id":43' in output_text.replace(" ", "") - - -@pytest.mark.parametrize( - "rerun_request, expected_message", - [ - ( - {"mode": "failed", "required_jobs": [{"language": "python", "job_id": 43}]}, - "unsupported CodeQL rerun schema=", - ), - ( - { - "schema": "2", - "mode": "failed", - "required_jobs": [{"language": "python", "job_id": 43}], - }, - "unsupported CodeQL rerun schema=2", - ), - ( - { - "schema": 1, - "mode": "failed", - "required_jobs": [{"language": "python", "job_id": 43}], - }, - "CodeQL rerun schema must be a string", - ), - ], -) -def test_codeql_scan_dispatch_rejects_unversioned_or_unknown_nested_rerun_schema( - tmp_path, rerun_request, expected_message -): - """Nested rerun authority is accepted only under exact schema version one.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_REQUIRED_JOBS": "null", - "SUPPLIED_RERUN_REQUEST": json.dumps(rerun_request), - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert expected_message in result.stdout - - -def test_codeql_scan_dispatch_validate_step_binds_producer_revision(tmp_path): - """Only the exact live base/head merge revision can invoke the handler.""" - missing = _run_validate_step( - tmp_path / "missing", - {"SUPPLIED_PRODUCER_SOURCE_SHA": ""}, - _matching_pull_request(), - ) - wrong_revision = _run_validate_step( - tmp_path / "wrong-revision", - { - "SUPPLIED_PRODUCER_SOURCE_SHA": "d" * 40, - "FAKE_PRODUCER_COMMIT_JSON": json.dumps( - { - "sha": "d" * 40, - "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], - } - ), - }, - _matching_pull_request(), - ) - wrong_parents = _run_validate_step( - tmp_path / "wrong-parents", - { - "FAKE_PRODUCER_COMMIT_JSON": json.dumps( - { - "sha": "c" * 40, - "parents": [{"sha": "f" * 40}, {"sha": "b" * 40}], - } - ), - }, - _matching_pull_request(), - ) - - assert missing.returncode == 1 - assert wrong_revision.returncode == 1 - assert wrong_parents.returncode == 1 - assert "producer source" in missing.stdout.lower() - assert "producer revision" in wrong_revision.stdout.lower() - assert "producer revision" in wrong_parents.stdout.lower() - - -def test_codeql_scan_dispatch_accepts_exact_pull_request_merge_revision(tmp_path): - """Bind the producer revision to the live PR base/head merge, not handler ancestry.""" - merge_sha = "e" * 40 - pull_request = _matching_pull_request() - pull_request["merge_commit_sha"] = merge_sha - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_PRODUCER_SOURCE_SHA": merge_sha, - "FAKE_SOURCE_COMPARE_JSON": json.dumps( - { - "status": "diverged", - "behind_by": 1, - "base_commit": {"sha": "f" * 40}, - "merge_base_commit": {"sha": "f" * 40}, - } - ), - "FAKE_PRODUCER_COMMIT_JSON": json.dumps( - { - "sha": merge_sha, - "parents": [ - {"sha": "a" * 40}, - {"sha": "b" * 40}, - ], - } - ), - }, - pull_request, - ) - - assert result.returncode == 0, result.stdout + result.stderr - - -def test_codeql_scan_dispatch_validate_step_accepts_legacy_rerun_mode(tmp_path): - """An already queued top-level mode retains whole-attempt semantics.""" - result = _run_validate_step( - tmp_path, - {"SUPPLIED_RERUN_MODE": "all"}, - _matching_pull_request(), - ) - - assert result.returncode == 0, result.stderr - assert "rerun_mode=all" in result.output_path.read_text(encoding="utf-8") - - -def test_codeql_scan_dispatch_validate_step_rejects_conflicting_rerun_envelopes( - tmp_path, -): - """A caller cannot supply both legacy and nested rerun authority.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_RERUN_REQUEST": json.dumps( - { - "mode": "failed", - "required_jobs": [{"language": "python", "job_id": 43}], - } - ), - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "conflicting legacy and nested rerun envelopes" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_rejects_unknown_rerun_mode(tmp_path): - """Only the two run-wide GitHub rerun operations are accepted.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_REQUIRED_JOBS": "null", - "SUPPLIED_RERUN_REQUEST": json.dumps( - { - "schema": "1", - "mode": "one-job", - "required_jobs": [{"language": "python", "job_id": 43}], - } - ), - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "rerun mode" in result.stdout - - -def test_codeql_scan_dispatch_validate_step_rejects_duplicate_job_id(tmp_path): - """Two language labels cannot authorize mutation of the same required job.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_MATRIX": json.dumps( - [ - {"language": "python", "build-mode": "none"}, - {"language": "actions", "build-mode": "none"}, - ] - ), - "SUPPLIED_REQUIRED_JOBS": json.dumps( - [ - {"language": "python", "job_id": 43}, - {"language": "actions", "job_id": 43}, - ] - ), - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "wake identity is missing" in result.stdout - - def test_codeql_scan_dispatch_validate_step_rejects_actor_mismatch(tmp_path): """A dispatch from an unauthorized actor is rejected before any live PR read.""" result = _run_validate_step(tmp_path, {"DISPATCH_ACTOR": "someone-else"}, _matching_pull_request()) @@ -802,28 +357,6 @@ def test_codeql_scan_dispatch_validate_step_accepts_multi_language_payload(tmp_p assert '"job_id":43' in output_text.replace(" ", "") -def test_codeql_scan_dispatch_validate_step_rejects_unproven_matrix_subset(tmp_path): - """A partial scan cannot authorize waking an unscanned required language.""" - result = _run_validate_step( - tmp_path, - { - "SUPPLIED_MATRIX": json.dumps( - [{"language": "actions", "build-mode": "none"}] - ), - "SUPPLIED_REQUIRED_JOBS": json.dumps( - [ - {"language": "python", "job_id": 43}, - {"language": "actions", "job_id": 44}, - ] - ), - }, - _matching_pull_request(), - ) - - assert result.returncode == 1 - assert "does not match the dispatched languages one-to-one" in result.stdout - - def test_codeql_scan_dispatch_validate_step_accepts_legacy_single_language_payload(tmp_path): """A queued pre-cutover payload still validates after required_jobs became mandatory. @@ -836,7 +369,6 @@ def test_codeql_scan_dispatch_validate_step_accepts_legacy_single_language_paylo result = _run_validate_step( tmp_path / case_name, { - **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": empty_jobs, "SUPPLIED_REQUIRED_LANGUAGE": "python", "SUPPLIED_REQUIRED_JOB_ID": "43", @@ -860,7 +392,6 @@ def test_codeql_scan_dispatch_validate_step_ignores_legacy_fields_when_required_ result = _run_validate_step( tmp_path, { - **_legacy_dispatch_env(), "SUPPLIED_MATRIX": json.dumps( [ {"language": "python", "build-mode": "none"}, @@ -891,13 +422,12 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ """Empty required_jobs still fail closed when the scalar identity cannot be synthesized.""" missing_both = _run_validate_step( tmp_path / "missing-both", - {**_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "null"}, + {"SUPPLIED_REQUIRED_JOBS": "null"}, _matching_pull_request(), ) language_mismatch = _run_validate_step( tmp_path / "language-mismatch", { - **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "[]", "SUPPLIED_REQUIRED_LANGUAGE": "javascript-typescript", "SUPPLIED_REQUIRED_JOB_ID": "43", @@ -907,7 +437,6 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ multi_language_legacy = _run_validate_step( tmp_path / "multi-language-legacy", { - **_legacy_dispatch_env(), "SUPPLIED_MATRIX": json.dumps( [ {"language": "python", "build-mode": "none"}, @@ -923,7 +452,6 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ invalid_job_id = _run_validate_step( tmp_path / "invalid-job-id", { - **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "null", "SUPPLIED_REQUIRED_LANGUAGE": "python", "SUPPLIED_REQUIRED_JOB_ID": "0", @@ -977,8 +505,8 @@ 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_binds_base_and_required_run() -> None: + """Public run identity includes base SHA and required run id without changing concurrency. The required shard cannot read client_payload. Encoding those fields in run-name lets it reject a same-head retarget or a different waiting @@ -992,13 +520,10 @@ def test_codeql_scan_dispatch_run_name_versions_source_without_changing_concurre 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 def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> None: @@ -1009,169 +534,83 @@ def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> """ workflow = WORKFLOW_PATH.read_text(encoding="utf-8") publish = workflow.split(" - name: Publish CodeQL dispatch status\n", 1)[1].split( - "\n\n settle-required-run:\n", 1 + "\n - name: Wake exact CodeQL required job\n", 1 )[0] assert "GATE_OUTCOME" in publish assert 'if [ "$GATE_OUTCOME" = "success" ]; then' in publish - assert "exact completed scan and preserved SARIF artifact remain" in publish + assert "completed dispatch scan job remains the evidence" in publish assert "continue-on-error:" not in publish assert "cancel-in-progress: true" not in publish -def test_dispatch_publish_rejects_superseded_metadata_and_versions_context() -> None: - """A stale handler cannot poison HEAD and v2 cannot reuse a legacy status. - - Run 34235814716 proved that a scan can become superseded after initial - validation but before publication. #1902's evidence-complete producer is - integrated into the same successor, so publication requires successful - live-metadata revalidation and emits only the base-bound receipt. - """ +def test_dispatch_wakes_only_the_exact_failed_codeql_job() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - revalidate = workflow.split( - " - name: Re-validate live pull request metadata before privileged scan\n", - 1, - )[1].split(" - name: Fetch the pinned CodeQL SARIF gate and GHAS identity scripts\n", 1)[0] - publish = workflow.split(" - name: Publish CodeQL dispatch status\n", 1)[1].split( - "\n\n settle-required-run:\n", 1 + wake = workflow.split(" - name: Wake exact CodeQL required job\n", 1)[1].split( + "\n\n - name:", 1 )[0] - assert " id: live_metadata\n" in revalidate - assert "if: always() && steps.live_metadata.outcome == 'success'" in publish - assert 'receipt_context="codeql-dispatch/${LANGUAGE}"' in publish - assert 'receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in publish - assert '-f context="$receipt_context"' in publish - assert "SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }}" in publish - assert 'if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then' in publish - assert 'actual_creator="$(jq -r' in publish - assert "unexpected creator" in publish - - -def test_dispatch_settles_all_languages_with_one_run_wide_mutation() -> None: - workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - settlement = workflow.split(" settle-required-run:\n", 1)[1] - - assert "needs: [validate-dispatch, scan]" in settlement - assert "always()" in settlement.split(" runs-on:", 1)[0] - assert "actions: write" in settlement.split(" steps:\n", 1)[0] - assert 'github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in settlement - assert 'github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in settlement - assert 'github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100"' in settlement - assert "rerun-failed-jobs" in settlement - assert '"rerun"' in settlement - assert "actions/jobs/${REQUIRED_JOB_ID}/rerun" not in workflow - assert "sleep " not in settlement - - -def test_dispatch_settlement_has_only_trusted_actions_write_boundary() -> None: + assert "steps.publish_status.outcome == 'success'" in wake + assert 'gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in wake + assert 'gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in wake + assert 'gh api "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}"' in wake + assert 'select(.event == "pull_request")' in wake + assert 'select(.path == ".github/workflows/codeql-pr.yml")' in wake + assert "select(.head_sha == $head)" in wake + assert "select(.run_id == $run_id)" in wake + assert "select(.name == $name)" in wake + assert 'select(.status == "completed" and .conclusion == "failure")' in wake + assert 'actions/jobs/${REQUIRED_JOB_ID}/rerun' in wake + assert "rerun-failed-jobs" not in wake + assert "while " not in wake + assert "sleep " not in wake + + +def test_dispatch_wake_has_only_trusted_actions_write_boundary() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") scan = workflow.split(" scan:\n", 1)[1] scan_permissions = scan.split(" strategy:\n", 1)[0] - settlement = workflow.split(" settle-required-run:\n", 1)[1] - settlement_permissions = settlement.split(" steps:\n", 1)[0] - assert "actions: write" not in scan_permissions - assert "actions: read" in scan_permissions - assert "actions: write" in settlement_permissions + assert "actions: write" in scan_permissions assert "pull_request:" not in workflow assert "pull_request_target:" not in workflow - assert "needs.validate-dispatch.outputs.required_run_id" in settlement - assert "needs.validate-dispatch.outputs.required_jobs" in settlement + assert "needs.validate-dispatch.outputs.required_run_id != ''" in scan + assert "needs.validate-dispatch.outputs.required_jobs != ''" in scan assert "github.event.client_payload.required_job_id" not in scan -def _run_settlement_step( +def _run_wake_step( tmp_path: Path, *, pull: dict | None = None, run: dict | None = None, - required_jobs: list[dict] | None = None, - handler_jobs: list[dict] | None = None, - handler_artifacts: list[dict] | None = None, - extra_env: dict[str, str] | None = None, + job: dict | None = None, ) -> tuple[subprocess.CompletedProcess[str], Path]: - """Execute the run-wide settlement block against fixture-backed API responses.""" + """Execute the exact wake block against fixture-backed GitHub API responses.""" bash = shutil.which("bash") jq = shutil.which("jq") assert bash is not None and jq is not None, "bash and jq are required to run this test" head_sha = "b" * 40 - pull = pull or { - "state": "open", - "base": { - "repo": {"full_name": "ContextualWisdomLab/naruon"}, - "ref": "main", - "sha": "a" * 40, - }, - "head": { - "repo": {"full_name": "ContextualWisdomLab/naruon"}, - "ref": "feature", - "sha": head_sha, - }, - } + pull = pull or {"state": "open", "head": {"sha": head_sha}} run = run or { "id": 42, - "run_attempt": 1, "event": "pull_request", "path": ".github/workflows/codeql-pr.yml", "head_sha": head_sha, "status": "completed", "conclusion": "failure", } - required_jobs = required_jobs or [ - { - "id": 43, - "run_id": 42, - "head_sha": head_sha, - "name": "CodeQL compatibility analysis (python)", - "status": "completed", - "conclusion": "failure", - }, - { - "id": 44, - "run_id": 42, - "head_sha": head_sha, - "name": "CodeQL compatibility analysis (actions)", - "status": "completed", - "conclusion": "failure", - }, - ] - handler_jobs = handler_jobs or [ - { - "name": "CodeQL dispatch scan (python)", - "status": "completed", - "conclusion": "success", - "run_attempt": 1, - "steps": [ - {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, - {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, - ], - }, - { - "name": "CodeQL dispatch scan (actions)", - "status": "completed", - "conclusion": "success", - "run_attempt": 1, - "steps": [ - {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, - {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, - ], - }, - ] - handler_artifacts = handler_artifacts or [ - { - "name": "codeql-dispatch-python-100-1", - "expired": False, - "size_in_bytes": 10, - }, - { - "name": "codeql-dispatch-actions-100-1", - "expired": False, - "size_in_bytes": 10, - }, - ] + job = job or { + "id": 43, + "run_id": 42, + "head_sha": head_sha, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "failure", + } script = _extract_run_block( - WORKFLOW_PATH.read_text(encoding="utf-8"), "Settle exact CodeQL required run" + WORKFLOW_PATH.read_text(encoding="utf-8"), "Wake exact CodeQL required job" ) fake_bin = tmp_path / "bin" fake_bin.mkdir(parents=True) @@ -1181,34 +620,15 @@ def _run_settlement_step( "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'endpoint="${!#}"\n' - 'if printf \'%s\\n\' "$@" | grep -qx POST; then\n' - ' printf \'%s\\n\' "$endpoint" >>"$FAKE_POST_LOG"\n' - ' if [ -n "${FAKE_WAKE_POST_FAIL_TOKEN:-}" ] && ' - '[ "${GH_TOKEN:-}" = "$FAKE_WAKE_POST_FAIL_TOKEN" ]; then\n' - " exit 1\n" - " fi\n" - ' if [ -n "${FAKE_DENIED_TOKEN:-}" ] && ' - '[ "${GH_TOKEN:-}" = "$FAKE_DENIED_TOKEN" ]; then\n' - ' printf \'%s\\n\' "${FAKE_DENIED_BODY:-}"\n' - " exit 1\n" - " fi\n" - ' if [ "${FAKE_WAKE_POST_FAIL_ALL:-}" = "1" ]; then\n' - " exit 1\n" - " fi\n" - ' test "${FAKE_POST_EXIT:-0}" = 0 || exit "$FAKE_POST_EXIT"\n' + 'if [ "${2:-}" = "-X" ]; then\n' + ' test "$3" = POST\n' + ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' " exit 0\n" "fi\n" - 'if [ "${GH_TOKEN:-}" = "${FAKE_DENIED_TOKEN:-}" ]; then\n' - ' printf \'%s\\n\' "${FAKE_DENIED_BODY:-}"\n' - " exit 1\n" - "fi\n" - 'case "$endpoint" in\n' + 'case "$2" in\n' ' */pulls/*) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' - ' repos/ContextualWisdomLab/naruon/actions/runs/42/jobs*) printf \'%s\\n\' "$FAKE_REQUIRED_JOB_PAGES" ;;\n' - ' repos/ContextualWisdomLab/naruon/actions/runs/42) printf \'%s\\n\' "$FAKE_RUN_JSON" ;;\n' - ' repos/ContextualWisdomLab/.github/actions/runs/100/jobs*) printf \'%s\\n\' "$FAKE_HANDLER_JOB_PAGES" ;;\n' - ' repos/ContextualWisdomLab/.github/actions/runs/100/artifacts*) printf \'%s\\n\' "$FAKE_HANDLER_ARTIFACT_PAGES" ;;\n' + ' */actions/runs/*) printf \'%s\\n\' "$FAKE_RUN_JSON" ;;\n' + ' */actions/jobs/*) printf \'%s\\n\' "$FAKE_JOB_JSON" ;;\n' " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -1219,29 +639,12 @@ def _run_settlement_step( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull), "FAKE_RUN_JSON": json.dumps(run), - "FAKE_REQUIRED_JOB_PAGES": json.dumps([{"jobs": required_jobs}]), - "FAKE_HANDLER_JOB_PAGES": json.dumps([{"jobs": handler_jobs}]), - "FAKE_HANDLER_ARTIFACT_PAGES": json.dumps( - [{"artifacts": handler_artifacts}] - ), + "FAKE_JOB_JSON": json.dumps(job), "FAKE_POST_LOG": str(post_log), - "FAKE_POST_EXIT": "0", - "FAKE_DENIED_TOKEN": "", - "FAKE_DENIED_BODY": "", "GH_TOKEN": "fake-token", - "TARGET_APP_WAKE_TOKEN": "", - "PR_REVIEW_MERGE_WAKE_TOKEN": "", - "OPENCODE_APPROVE_WAKE_TOKEN": "", - "GITHUB_WAKE_TOKEN": "fake-token", - "HANDLER_READ_TOKEN": "handler-token", - "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", - "GITHUB_RUN_ID": "100", - "GITHUB_RUN_ATTEMPT": "1", + "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", - "BASE_REF": "main", - "BASE_SHA": "a" * 40, - "HEAD_REF": "feature", "HEAD_SHA": head_sha, "REQUIRED_RUN_ID": "42", "REQUIRED_JOBS": json.dumps( @@ -1250,161 +653,28 @@ def _run_settlement_step( {"language": "actions", "job_id": 44}, ] ), - "RERUN_MODE": "failed", - "RERUN_SCHEMA": "legacy-0", - "MAX_CODEQL_RERUN_ATTEMPT": "48", + "REQUIRED_LANGUAGE": "python", } - if extra_env: - env.update(extra_env) result = subprocess.run( [bash], input=script, text=True, capture_output=True, check=False, env=env ) return result, post_log -def test_dispatch_settlement_reruns_two_languages_once(tmp_path: Path) -> None: - result, post_log = _run_settlement_step(tmp_path) +def test_dispatch_wake_reruns_only_fixture_bound_exact_job(tmp_path: Path) -> None: + result, post_log = _run_wake_step(tmp_path) assert result.returncode == 0, result.stderr assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" + "repos/ContextualWisdomLab/naruon/actions/jobs/43/rerun" ] -@pytest.mark.parametrize("run_attempt", [48, 49, 50, 51]) -def test_dispatch_settlement_stops_before_github_rerun_ceiling( - tmp_path: Path, run_attempt: int -) -> None: - """An exhausted attempt budget fails before another Actions mutation.""" - result, post_log = _run_settlement_step( - tmp_path, - run={ - "id": 42, - "run_attempt": run_attempt, - "event": "pull_request", - "path": ".github/workflows/codeql-pr.yml", - "head_sha": "b" * 40, - "status": "completed", - "conclusion": "failure", - }, - extra_env={"RERUN_SCHEMA": "1"}, - ) - - assert result.returncode == 1 - assert not post_log.exists() - assert "phase=pre_mutation" in result.stdout - assert "reason=rerun_budget_exhausted" in result.stdout - assert "run_id=42" in result.stdout - assert f"run_attempt={run_attempt}" in result.stdout - assert "rerun_schema=1" in result.stdout - assert "languages=actions,python" in result.stdout - - -def test_dispatch_settlement_fails_closed_when_no_credential( - tmp_path: Path, -) -> None: - result, post_log = _run_settlement_step( - tmp_path, - extra_env={ - "GH_TOKEN": "", - "TARGET_APP_WAKE_TOKEN": "", - "PR_REVIEW_MERGE_WAKE_TOKEN": "", - "OPENCODE_APPROVE_WAKE_TOKEN": "", - "GITHUB_WAKE_TOKEN": "", - }, - ) - - assert result.returncode == 1 - assert "could not read the current pull request" in result.stdout - assert not post_log.exists() - - -def test_dispatch_settlement_falls_back_when_target_app_token_cannot_rerun( - tmp_path: Path, -) -> None: - """A nonempty App token without Actions write must not shadow fallbacks.""" - result, post_log = _run_settlement_step( - tmp_path, - extra_env={ - "TARGET_APP_WAKE_TOKEN": "forbidden-app-token", - "PR_REVIEW_MERGE_WAKE_TOKEN": "actions-write-token", - "OPENCODE_APPROVE_WAKE_TOKEN": "", - "GITHUB_WAKE_TOKEN": "", - "GH_TOKEN": "", - "FAKE_WAKE_POST_FAIL_TOKEN": "forbidden-app-token", - }, - ) - - assert result.returncode == 0, result.stderr - assert ( - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" - in post_log.read_text(encoding="utf-8") - ) - assert "pr-review-merge-token" in result.stdout - assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - ] - - -def test_dispatch_settlement_fails_closed_after_every_wake_is_denied( - tmp_path: Path, -) -> None: - """A clean scan is not authoritative until one exact-job wake is accepted.""" - result, post_log = _run_settlement_step( - tmp_path, - extra_env={ - "TARGET_APP_WAKE_TOKEN": "app-token", - "PR_REVIEW_MERGE_WAKE_TOKEN": "merge-token", - "OPENCODE_APPROVE_WAKE_TOKEN": "approve-token", - "GITHUB_WAKE_TOKEN": "github-token", - "GH_TOKEN": "", - "FAKE_WAKE_POST_FAIL_ALL": "1", - }, - ) - - assert result.returncode == 1 - assert "could not enqueue verified run-wide recovery" in result.stdout - assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - ] - - -def test_dispatch_settlement_retries_reads_with_next_configured_credential( - tmp_path: Path, -) -> None: - result, post_log = _run_settlement_step( - tmp_path, - extra_env={ - "GH_TOKEN": "target-token", - "TARGET_APP_WAKE_TOKEN": "target-token", - "PR_REVIEW_MERGE_WAKE_TOKEN": "fallback-token", - "OPENCODE_APPROVE_WAKE_TOKEN": "", - "GITHUB_WAKE_TOKEN": "", - "FAKE_DENIED_TOKEN": "target-token", - # Use a field consumed by the PR validator: a generic GitHub - # message body was already ignored and did not reproduce the bug. - "FAKE_DENIED_BODY": '{"state":"closed"}', - }, - ) - - assert result.returncode == 0, result.stderr - assert "pr-review-merge-token" in result.stdout - assert "jq:" not in result.stderr - assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", - ] - - -def test_dispatch_settlement_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: - stale_result, stale_log = _run_settlement_step( +def test_dispatch_wake_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: + stale_result, stale_log = _run_wake_step( tmp_path / "stale", pull={"state": "open", "head": {"sha": "c" * 40}} ) - closed_result, closed_log = _run_settlement_step( + closed_result, closed_log = _run_wake_step( tmp_path / "closed", pull={"state": "closed", "head": {"sha": "b" * 40}} ) @@ -1414,52 +684,10 @@ def test_dispatch_settlement_rejects_stale_head_and_closed_pr(tmp_path: Path) -> assert not closed_log.exists() -def test_dispatch_settlement_rejects_changed_repository_or_head_ref(tmp_path: Path) -> None: - """Settlement revalidates the complete live PR repository/ref identity.""" - wrong_repository, wrong_repository_log = _run_settlement_step( - tmp_path / "wrong-repository", - pull={ - "state": "open", - "base": {"repo": {"full_name": "ContextualWisdomLab/other"}, "ref": "main", "sha": "a" * 40}, - "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, - }, - ) - changed_ref, changed_ref_log = _run_settlement_step( - tmp_path / "changed-ref", - pull={ - "state": "open", - "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, - "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "other", "sha": "b" * 40}, - }, - ) - - assert wrong_repository.returncode == 1 - assert changed_ref.returncode == 1 - assert not wrong_repository_log.exists() - assert not changed_ref_log.exists() - - -def test_dispatch_settlement_rejects_successful_required_run(tmp_path: Path) -> None: - """A completed success cannot be mutated as though it were a failed attempt.""" - result, post_log = _run_settlement_step( - tmp_path, - run={ - "id": 42, - "event": "pull_request", - "path": ".github/workflows/codeql-pr.yml", - "head_sha": "b" * 40, - "status": "completed", - "conclusion": "success", - }, - ) - - assert result.returncode == 1 - assert not post_log.exists() - - -def test_dispatch_settlement_rejects_wrong_or_nonfailed_job_identity(tmp_path: Path) -> None: - wrong_jobs = [ - { +def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Path) -> None: + wrong_job_result, wrong_job_log = _run_wake_step( + tmp_path / "wrong-job", + job={ "id": 43, "run_id": 999, "head_sha": "b" * 40, @@ -1467,171 +695,42 @@ def test_dispatch_settlement_rejects_wrong_or_nonfailed_job_identity(tmp_path: P "status": "completed", "conclusion": "failure", }, - { - "id": 44, + ) + successful_job_result, successful_job_log = _run_wake_step( + tmp_path / "successful-job", + job={ + "id": 43, "run_id": 42, "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (actions)", + "name": "CodeQL compatibility analysis (python)", "status": "completed", - "conclusion": "failure", + "conclusion": "success", }, - ] - wrong_job_result, wrong_job_log = _run_settlement_step( - tmp_path / "wrong-job", - required_jobs=wrong_jobs, - ) - successful_jobs = [dict(job) for job in wrong_jobs] - successful_jobs[0].update(run_id=42, conclusion="success") - successful_job_result, successful_job_log = _run_settlement_step( - tmp_path / "successful-job", - required_jobs=successful_jobs, ) assert wrong_job_result.returncode == 1 assert successful_job_result.returncode == 1 - assert "missing or ambiguous exact job identity" in wrong_job_result.stdout + assert "missing or ambiguous exact run/job identity" in wrong_job_result.stdout assert not wrong_job_log.exists() assert not successful_job_log.exists() -def test_dispatch_settlement_all_mode_reruns_success_and_failure_jobs(tmp_path: Path) -> None: - all_jobs = [ - { - "id": 43, - "run_id": 42, - "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (python)", - "status": "completed", - "conclusion": "success", - }, - { - "id": 44, - "run_id": 42, +def test_dispatch_wake_allows_parallel_language_rerun_on_same_exact_run(tmp_path: Path) -> None: + """Another language may already have moved the shared run back to in_progress.""" + result, post_log = _run_wake_step( + tmp_path, + run={ + "id": 42, + "event": "pull_request", + "path": ".github/workflows/codeql-pr.yml", "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (actions)", - "status": "completed", - "conclusion": "failure", + "status": "in_progress", + "conclusion": None, }, - ] - result, post_log = _run_settlement_step( - tmp_path, - required_jobs=all_jobs, - extra_env={"RERUN_MODE": "all"}, ) assert result.returncode == 0, result.stderr - assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun" - ] - - -def test_dispatch_settlement_rejects_missing_handler_artifact(tmp_path: Path) -> None: - result, post_log = _run_settlement_step( - tmp_path, - handler_artifacts=[ - { - "name": "codeql-dispatch-python-100-1", - "expired": False, - "size_in_bytes": 10, - } - ], - ) - - assert result.returncode == 1 - assert "incomplete handler gate or SARIF evidence for actions" in result.stdout - assert not post_log.exists() - - -def test_dispatch_settlement_rejects_missing_handler_gate_steps(tmp_path: Path) -> None: - """A terminal scan name alone is not authenticated gate evidence.""" - result, post_log = _run_settlement_step( - tmp_path, - handler_jobs=[ - { - "name": "CodeQL dispatch scan (python)", - "status": "completed", - "conclusion": "success", - "run_attempt": 1, - "steps": [], - }, - { - "name": "CodeQL dispatch scan (actions)", - "status": "completed", - "conclusion": "success", - "run_attempt": 1, - "steps": [ - {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, - {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, - ], - }, - ], - ) - - assert result.returncode == 1 - assert "incomplete handler gate or SARIF evidence for python" in result.stdout - assert not post_log.exists() - - -def test_dispatch_settlement_rejects_unproven_matrix_subset(tmp_path: Path) -> None: - """Every required shard needs current handler gate and artifact evidence.""" - result, post_log = _run_settlement_step( - tmp_path, - handler_jobs=[ - { - "name": "CodeQL dispatch scan (actions)", - "status": "completed", - "conclusion": "success", - } - ], - handler_artifacts=[ - { - "name": "codeql-dispatch-actions-100-1", - "expired": False, - "size_in_bytes": 10, - } - ], - ) - - assert result.returncode == 1 - assert "incomplete handler gate or SARIF evidence for python" in result.stdout - assert not post_log.exists() - - -def test_dispatch_settlement_rejects_unrelated_failed_job(tmp_path: Path) -> None: - unrelated = { - "id": 45, - "run_id": 42, - "head_sha": "b" * 40, - "name": "unrelated required job", - "status": "completed", - "conclusion": "failure", - } - result, post_log = _run_settlement_step( - tmp_path, - required_jobs=[ - { - "id": 43, - "run_id": 42, - "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (python)", - "status": "completed", - "conclusion": "failure", - }, - { - "id": 44, - "run_id": 42, - "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (actions)", - "status": "completed", - "conclusion": "failure", - }, - unrelated, - ], - ) - - assert result.returncode == 1 - assert "unrelated failed jobs" in result.stdout - assert not post_log.exists() + assert post_log.exists() def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: @@ -1660,10 +759,6 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }}" in workflow ), "SUPPLIED_REQUIRED_JOBS must be serialised with toJSON(); a bare array breaks template validation" - assert ( - "SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }}" - in workflow - ), "The bounded nested rerun envelope must be serialized before shell validation" assert ( "SUPPLIED_REQUIRED_JOB_ID: ${{ github.event.client_payload.required_job_id || '' }}" in workflow @@ -1672,14 +767,3 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_LANGUAGE: ${{ github.event.client_payload.required_language || '' }}" in workflow ), "Queued pre-cutover payloads still supply required_language as a scalar" - assert "SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }}" in workflow - assert "SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}" in workflow - assert "conflicting nested and legacy pr_head identity" in workflow - - -def test_codeql_scan_dispatch_bridge_has_explicit_removal_condition() -> None: - """The legacy compatibility port cannot become permanent hidden policy.""" - workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - - assert "LEGACY_V1_REMOVAL_CONDITION" in workflow - assert "protected v2 producer" in workflow diff --git a/tests/test_contextual_orchestrator_review_policy.py b/tests/test_contextual_orchestrator_review_policy.py index e13e94107e..41c1ea40b2 100644 --- a/tests/test_contextual_orchestrator_review_policy.py +++ b/tests/test_contextual_orchestrator_review_policy.py @@ -116,12 +116,8 @@ def test_load_zdr_endpoints_skips_rows_without_provider_or_model(tmp_path) -> No json.dumps( { "data": [ - { - "model_id": "deepseek/deepseek-r1:free", - "model_name": "DeepSeek: R1 (free)", - "provider_name": "DeepSeek", - }, - {"model_id": "no-provider"}, + {"model_name": "deepseek/deepseek-r1:free", "provider_name": "DeepSeek"}, + {"model_name": "no-provider"}, {"provider_name": "NoModel"}, ] } @@ -144,91 +140,6 @@ def test_load_zdr_endpoints_respects_none_feed_path(tmp_path) -> None: assert policy._load_zdr_endpoints(str(empty_feed)) == frozenset() -def test_load_zdr_endpoints_keys_by_model_id_not_display_name(tmp_path) -> None: - """The live OpenRouter ZDR feed keys routes by ``model_id``, not ``model_name``. - - Confirmed by offline reproduction against the real - ``https://openrouter.ai/api/v1/endpoints/zdr`` feed: OpenRouter's - ``model_name`` is a human display string (e.g. "DeepSeek: DeepSeek V4.1 - Flash") while ``model_id`` is the slug contextual-orchestrator discovery - reports as ``model`` (e.g. "inclusionai/ling-3.0-flash-vl:free"). Keying - on ``model_name`` (introduced in 17052a7ca, #1360) meant no live-feed - route ever matched ``is_zdr_model(...)``, so with ``--require-zdr`` - (every private/internal consumer, per ADR-0003) the catalog was always - empty and the sidecar failed closed with "no attested ZDR model route is - available with the ZDR policy; orchestrator/free would fail closed". - This killed noema-review and strix on - ContextualWisdomLab/late-life-anxiety-reanalysis#10 (head - a1cd5bc6783c6510dfcf937f523c733366e82213, runs 34700409452/103571267389 - and 34700409446/103571829483) against central - fb17ef556f94f673234aa557254ae52779e9a7b0. See - ContextualWisdomLab/.github#2122. - """ - feed = tmp_path / "zdr.json" - feed.write_text( - json.dumps( - { - "data": [ - { - "name": "Novita | inclusionai/ling-3.0-flash-vl-20260910:free", - "model_id": "inclusionai/ling-3.0-flash-vl:free", - "model_name": "inclusionAI: Ling 3.0 Flash VL (free)", - "provider_name": "Novita", - }, - { - "name": "x", - "model_name": "Display Only", - "provider_name": "Novita", - }, - ] - } - ), - encoding="utf-8", - ) - - keys = policy._load_zdr_endpoints(str(feed)) - - assert keys == frozenset( - { - policy._route_key("Novita", "inclusionai/ling-3.0-flash-vl:free"), - policy._route_key("openrouter", "inclusionai/ling-3.0-flash-vl:free"), - } - ) - assert not any("Display Only" in key for key in keys) - assert not any("inclusionAI: Ling 3.0 Flash VL" in key for key in keys) - - report = { - "models": [ - { - "provider": "openrouter", - "model": "inclusionai/ling-3.0-flash-vl:free", - "agent_id": "or_ling_vl", - "is_free": True, - **FREE_PRICE, - }, - { - "provider": "openrouter", - "model": "other-vendor/not-covered:free", - "agent_id": "or_not_covered", - "is_free": True, - **FREE_PRICE, - }, - ] - } - result = policy.build_zdr_prioritized_catalog( - policy.parse_discovery_report(report), - limit=12, - account_cap=4, - zdr_endpoints=keys, - require_zdr=True, - pool="free", - ) - assert [agent["model"] for agent in result["agents"]] == [ - "inclusionai/ling-3.0-flash-vl:free" - ] - assert result["report"]["zdr_selected_count"] == 1 - - def test_route_key_prefixes_provider() -> None: """ZDR feed keys are matched with the provider prefix.""" assert policy._route_key("openrouter", "deepseek/deepseek-r1:free") == ( @@ -519,8 +430,7 @@ def test_load_zdr_endpoints_parses_feed(tmp_path) -> None: "data": [ { "name": "deepseek/deepseek-r1:free", - "model_id": "deepseek/deepseek-r1:free", - "model_name": "DeepSeek: R1 (free)", + "model_name": "deepseek/deepseek-r1:free", "provider_name": "DeepSeek", } ] @@ -543,15 +453,7 @@ def test_build_catalog_from_paths_writes_both_files(tmp_path) -> None: feed = tmp_path / "zdr.json" feed.write_text( json.dumps( - { - "data": [ - { - "model_id": "deepseek/deepseek-r1:free", - "model_name": "DeepSeek: R1 (free)", - "provider_name": "DeepSeek", - } - ] - } + {"data": [{"model_name": "deepseek/deepseek-r1:free", "provider_name": "DeepSeek"}]} ), encoding="utf-8", ) diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index b279d33a98..31af868d45 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -40,7 +40,7 @@ ) GATEWAY_MODEL = "contextual-orchestrator/orchestrator/free" -ORCH_PIN_SHA = "767e67fbc6b881a452761f32abb69b9971b9b03b" +ORCH_PIN_SHA = "414f22973658c4ddc3d4320fcf7acd9b4e8ba991" def _read(path: Path) -> str: diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py index 136b373539..571368677f 100644 --- a/tests/test_current_head_run_coalescer.py +++ b/tests/test_current_head_run_coalescer.py @@ -393,7 +393,6 @@ def test_run_json_uses_token_timeout_decodes_success_and_bounds_failure(monkeypa seen: dict[str, object] = {} def success(*args, **kwargs): - """Return bounded JSON while recording the subprocess timeout.""" seen.update(kwargs) return SimpleNamespace(returncode=0, stdout='{"ok":true}', stderr="") @@ -402,7 +401,6 @@ def success(*args, **kwargs): assert seen["timeout"] == module.API_TIMEOUT_SECONDS def timeout(*_args, **_kwargs): - """Raise the subprocess timeout sentinel for transport mapping.""" raise subprocess.TimeoutExpired(cmd="gh", timeout=30) monkeypatch.setattr(module.subprocess, "run", timeout) @@ -436,7 +434,6 @@ def test_fetch_helpers_fail_closed_and_paginate(monkeypatch) -> None: calls: list[list[str]] = [] def pages(args): - """Return two paginated workflow-run pages and then an empty page.""" calls.append(list(args)) status = next(item.split("=", 1)[1] for item in args if item.startswith("status=")) page = int(next(item.split("=", 1)[1] for item in args if item.startswith("page="))) @@ -476,191 +473,6 @@ def test_cancel_run_uses_explicit_transport_and_ordinary_endpoint(monkeypatch) - assert sleeps == [module.CANCELLATION_POLL_INTERVAL_SECONDS] -def test_cancel_run_preserves_started_run_after_cancel_409(monkeypatch) -> None: - """A run that started after the first POST is preserved without a second POST.""" - module = load_module() - cancel_calls = 0 - states = iter( - [ - {"status": "in_progress", "conclusion": None}, - ] - ) - - def run_json(args): - """Raise the queued-start race from the cancellation POST.""" - nonlocal cancel_calls - if args[-1].endswith("/cancel"): - cancel_calls += 1 - raise RuntimeError("gh: Cannot cancel a workflow run that has not been queued yet. (HTTP409)") - raise AssertionError(args) - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", lambda _repo, _run_id: next(states)) - with pytest.raises(module.CoalescingRefused, match="no longer queued"): - module._cancel_run("o/r", 123) - assert cancel_calls == 1 - - -def test_cancel_run_preserves_queued_run_after_cancel_409(monkeypatch) -> None: - """A queued run gets no compensating cancellation request after HTTP 409.""" - module = load_module() - cancel_calls = 0 - states = iter( - [ - {"status": "queued", "conclusion": None}, - {"status": "completed", "conclusion": "cancelled"}, - ] - ) - - def run_json(args): - """Raise the queued-start race while preserving the queued state.""" - nonlocal cancel_calls - if args[-1].endswith("/cancel"): - cancel_calls += 1 - raise RuntimeError("Cannot cancel a workflow run that has not been queued yet. (HTTP409)") - raise AssertionError(args) - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", lambda _repo, _run_id: next(states)) - with pytest.raises(module.CoalescingRefused, match="remained queued"): - module._cancel_run("o/r", 123) - assert cancel_calls == 1 - - -def test_coalesce_preserves_started_candidate_after_cancel_409(monkeypatch, capsys) -> None: - """The production coalesce path preserves a candidate that starts at POST time.""" - module = load_module() - candidate = run_record(100, 10) - sibling = run_record(101, 10) - candidate_fetches = 0 - cancel_calls = 0 - - monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) - monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) - - def fetch_run(_repo, run_id): - """Return the sibling or transition the candidate to in-progress.""" - nonlocal candidate_fetches - if run_id == 101: - return sibling - candidate_fetches += 1 - return candidate if candidate_fetches == 1 else run_record(100, 10, status="in_progress") - - def run_json(args): - """Raise the queued-start race without permitting unrelated commands.""" - nonlocal cancel_calls - if args[-1].endswith("/cancel"): - cancel_calls += 1 - raise RuntimeError("Cannot cancel a workflow run that has not been queued yet. (HTTP409)") - raise AssertionError(args) - - monkeypatch.setattr(module, "_fetch_run", fetch_run) - monkeypatch.setattr(module, "_run_json", run_json) - - assert module.coalesce( - "ContextualWisdomLab/.github", - 1, - "ContextualWisdomLab/.github", - "feature/current", - "a" * 40, - ) == [] - assert cancel_calls == 1 - assert "Preserving run 100" in capsys.readouterr().out - - -@pytest.mark.parametrize( - ("state", "error", "expected_posts", "expected_gets"), - [ - ({"status": "completed", "conclusion": "cancelled"}, None, 1, 1), - ({"status": "in_progress", "conclusion": None}, "no longer queued", 1, 1), - ({"status": "completed", "conclusion": "success"}, "no longer queued", 1, 1), - ({"status": "mystery", "conclusion": None}, "no longer queued", 1, 1), - ], -) -def test_cancel_run_409_state_gate_never_overclaims( - monkeypatch, state, error, expected_posts, expected_gets -) -> None: - """Only cancelled terminal evidence suppresses the preservation refusal.""" - module = load_module() - calls = {"post": 0, "get": 0} - - def run_json(args): - """Raise the cancellation race for each parameterized state.""" - if args[-1].endswith("/cancel"): - calls["post"] += 1 - raise RuntimeError("Cannot cancel a workflow run that has not been queued yet. (HTTP409)") - raise AssertionError(args) - - def fetch_run(_repo, _run_id): - """Return the parameterized authoritative post-409 state.""" - calls["get"] += 1 - return state - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", fetch_run) - if error: - with pytest.raises(module.CoalescingRefused, match=error): - module._cancel_run("o/r", 123) - else: - module._cancel_run("o/r", 123) - assert calls == {"post": expected_posts, "get": expected_gets} - - -def test_cancel_run_ignores_unrelated_error_without_recheck(monkeypatch) -> None: - """A non-409 cancellation error cannot trigger a compensating mutation.""" - module = load_module() - calls: list[str] = [] - - def run_json(args): - """Raise the unrelated cancellation failure without a second request.""" - calls.append("post") - raise RuntimeError("HTTP500 upstream failure") - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: calls.append("get")) - with pytest.raises(RuntimeError, match="HTTP500"): - module._cancel_run("o/r", 123) - assert calls == ["post"] - - -def test_cancel_run_fails_closed_when_queued_after_queue_start_race(monkeypatch) -> None: - """A queued run after a startup race is preserved without a second POST.""" - module = load_module() - calls = {"post": 0} - - def run_json(args): - """Raise the queue-start race while counting cancellation posts.""" - if args[-1].endswith("/cancel"): - calls["post"] += 1 - raise RuntimeError("Cannot cancel a workflow run that has not been queued yet. (HTTP409)") - raise AssertionError(args) - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: {"status": "queued", "conclusion": None}) - with pytest.raises(module.CoalescingRefused, match="remained queued"): - module._cancel_run("o/r", 123) - assert calls == {"post": 1} - - -def test_cancel_run_409_detection_does_not_depend_on_provider_english(monkeypatch) -> None: - """A bare HTTP 409 still preserves a queued run without a second POST.""" - module = load_module() - calls = {"post": 0} - - def run_json(args): - """Raise a bare HTTP 409 to test language-independent detection.""" - if args[-1].endswith("/cancel"): - calls["post"] += 1 - raise RuntimeError("HTTP 409 conflict") - raise AssertionError(args) - - monkeypatch.setattr(module, "_run_json", run_json) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: {"status": "queued"}) - with pytest.raises(module.CoalescingRefused, match="remained queued"): - module._cancel_run("o/r", 123) - assert calls == {"post": 1} - - def test_cancel_run_fails_when_terminal_cancellation_is_unproven(monkeypatch) -> None: """An accepted cancellation is not reported complete while GitHub stays active.""" module = load_module() @@ -765,7 +577,6 @@ def test_coalesce_refetches_candidate_last_and_preserves_started_run(monkeypatch monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) def fetch_run(_repo: str, run_id: int): - """Return the sibling while showing the candidate started meanwhile.""" return sibling if run_id == 101 else run_record(100, 10, status="in_progress") monkeypatch.setattr(module, "_fetch_run", fetch_run) @@ -862,7 +673,6 @@ def test_main_treats_coalescing_refused_as_a_safe_no_op(monkeypatch, capsys) -> ] def refuse(*_args: object) -> list[int]: - """Raise the safe coalescing refusal handled by the CLI entrypoint.""" raise module.CoalescingRefused("pull request head moved before duplicate classification") monkeypatch.setattr(module, "coalesce", refuse) diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index 674b984b63..49631d2a19 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -31,11 +31,9 @@ WORKFLOWS_DIR = REPO_ROOT / ".github/workflows" # The required workflows that keep the canonical `changed-scope` gate job. -# `sast-semgrep.yml` has only one consumer job, so it folds the classifier -# into that job as a step-level guard instead of a standalone job -- see -# GATED_JOBS below. GATE_WORKFLOWS = ( "security-scan.yml", + "sast-semgrep.yml", "strix.yml", ) @@ -54,6 +52,7 @@ # output, keyed by workflow filename. GATED_JOBS = { "security-scan.yml": ("osv-scan", "dependency-review", "trivy-fs", "scorecard"), + "sast-semgrep.yml": ("semgrep",), "strix.yml": ("strix",), } @@ -87,7 +86,7 @@ def _on_block(workflow: str) -> str: def test_gate_job_is_byte_identical_across_the_five_workflows_apart_from_if(): - """The `changed-scope` block must not drift between every gate copy.""" + """The `changed-scope` block must not drift between its five copies.""" normalized_blocks = set() for filename in GATE_WORKFLOWS: workflow = _read(filename) @@ -111,7 +110,7 @@ def test_gate_job_and_codeql_scope_step_share_one_doc_pattern_line(): `COPYING.txt`/`NOTICE`/`NOTICE.txt` names. """ doc_pattern_lines = set() - for filename in (*GATE_WORKFLOWS, "sast-semgrep.yml", "codeql-pr.yml"): + for filename in (*GATE_WORKFLOWS, "codeql-pr.yml"): workflow = _read(filename) matches = [ line for line in workflow.splitlines() if "*.md|*.markdown" in line @@ -209,8 +208,8 @@ def test_codeql_pr_gates_analyze_head_at_step_level_not_job_level(): def test_each_gate_workflow_keeps_an_always_admitted_job(): """A fully-skipped run must conclude `success`, never `skipped`. - Every gate workflow needs at least one job with no `needs:` and no - needs-output-dependent `if:` -- the `changed-scope` job itself + Every one of the five workflows needs at least one job with no `needs:` + and no needs-output-dependent `if:` -- the `changed-scope` job itself qualifies -- so a doc-only PR's run still has a job that runs and succeeds instead of every job skipping and the run itself reporting `skipped` (an undocumented conclusion for a required check). @@ -221,40 +220,3 @@ def test_each_gate_workflow_keeps_an_always_admitted_job(): job_if = re.search(r"(?m)^ if: (.*)$", block) assert job_if is not None, filename assert "needs." not in job_if.group(1), filename - - -def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): - """`sast-semgrep.yml` has one consumer, so the gate is a step, not a job. - - A standalone `changed-scope` job cost a second runner allocation per PR - purely to compute two booleans for one downstream job (measured in - docs/product-technical-gap-baseline.md, "Items 15/16/17 measurement"). - Folding it into `semgrep` keeps the load-bearing property -- the job - still runs and concludes `success` on a doc-only PR -- while the - expensive steps gate on the classifier step's output. The final gate - step must also carry that guard: a step-skipped `Run Semgrep` leaves - `steps.semgrep.outputs.rc` empty, which is `!= '0'`. - """ - workflow = _read("sast-semgrep.yml") - # The classifier's own log lines keep saying "changed-scope" (byte-for-byte - # verbatim across every copy, see test_gate_job_and_codeql_scope_step_share_ - # one_doc_pattern_line); what must be gone is the standalone JOB. - assert "changed-scope:" not in workflow - assert "needs: changed-scope" not in workflow - assert "needs.changed-scope" not in workflow - assert workflow.count("runs-on: ubuntu-24.04") == 1 - - semgrep = _top_level_job_block(workflow, "semgrep") - 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 "pull-requests: read" in semgrep - assert "id: scope" in semgrep - assert semgrep.count("steps.scope.outputs.code == 'true'") == 5 - assert ( - "if: always() && steps.scope.outputs.code == 'true' && " - "(steps.semgrep_sarif.outputs.finding_count != '0' || steps.semgrep.outputs.rc != '0')" - ) in semgrep - # Harden-runner audits egress and must precede the classifier's gh api call. - assert semgrep.index("Harden the runner") < semgrep.index("Classify changed paths") diff --git a/tests/test_github_api_url_boundary.py b/tests/test_github_api_url_boundary.py deleted file mode 100644 index a9050584fd..0000000000 --- a/tests/test_github_api_url_boundary.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Fail-closed GitHub REST authority contracts for central CI HTTP clients.""" - -from __future__ import annotations - -from email.message import Message -from io import BytesIO -from pathlib import Path -import re -import subprocess -from typing import Any -from urllib.request import Request -from urllib.response import addinfourl - -import pytest - -from scripts.ci import codeql_ghas_configuration_identity as identity -from scripts.ci import strix_evidence_binding as binding - - -UNTRUSTED_GITHUB_API_URLS = ( - "http://api.github.com/repos/ContextualWisdomLab/example", - "https://api.github.com.evil.example/repos/ContextualWisdomLab/example", - "https://api.github.com@evil.example/repos/ContextualWisdomLab/example", - "https://api.github.com:443/repos/ContextualWisdomLab/example", - "https://api.github.com/repos/ContextualWisdomLab/example#fragment", - "https://[api.github.com/repos/ContextualWisdomLab/example", - "file:///etc/passwd", -) -REDIRECT_TARGETS = ( - "https://api.github.com/repos/ContextualWisdomLab/redirected", - "https://api.github.com.evil.example/repos/ContextualWisdomLab/example", - "http://api.github.com/repos/ContextualWisdomLab/example", - "file:///etc/passwd", -) -CANONICAL_GITHUB_API_URL = "https://api.github.com/repos/ContextualWisdomLab/example" -G17_ROW_PREFIX = "| G-17 |" -FULL_COMMIT_SHA = re.compile(r"`([0-9a-f]{40})`") - - -class _SyntheticRedirectTransport: - """Return one synthetic 302 while recording every request reaching transport.""" - - def __init__(self, target: str) -> None: - """Store the redirect target and initialize the observed request ledger.""" - self.target = target - self.calls: list[tuple[str, str | None]] = [] - - def https_open(self, request: Request) -> Any: - """Return a synthetic redirect response without contacting a network target.""" - self.calls.append((request.full_url, request.get_header("Authorization"))) - headers = Message() - headers["Location"] = self.target - response = addinfourl(BytesIO(b""), headers, request.full_url, code=302) - response.msg = "Found" - return response - - -class _JsonResponse: - """Minimal context-managed JSON response for opener-boundary contracts.""" - - def __enter__(self) -> _JsonResponse: - """Enter the fake response context.""" - return self - - def __exit__(self, *_args: Any) -> None: - """Leave the fake response context without suppressing exceptions.""" - return None - - def read(self) -> bytes: - """Return an empty JSON array payload.""" - return b"[]" - - -def _unexpected_open(*_args: Any, **_kwargs: Any) -> Any: - """Fail if a rejected authority reaches the network/file opener boundary.""" - pytest.fail("rejected GitHub API authority reached opener") - - -def _assert_g17_evidence_is_published(baseline: str) -> None: - """Require every full G-17 evidence SHA to resolve in current published ancestry.""" - rows = [line for line in baseline.splitlines() if line.startswith(G17_ROW_PREFIX)] - assert len(rows) == 1, "G-17 must have exactly one gap-register row" - evidence_shas = FULL_COMMIT_SHA.findall(rows[0]) - assert evidence_shas, "G-17 must name full commit evidence" - - repository_root = Path(__file__).resolve().parents[1] - for evidence_sha in evidence_shas: - resolvable = subprocess.run( - ["git", "cat-file", "-e", f"{evidence_sha}^{{commit}}"], - cwd=repository_root, - check=False, - capture_output=True, - text=True, - ) - assert resolvable.returncode == 0, f"G-17 evidence {evidence_sha} is not published" - - ancestor = subprocess.run( - ["git", "merge-base", "--is-ancestor", evidence_sha, "HEAD"], - cwd=repository_root, - check=False, - capture_output=True, - text=True, - ) - assert ancestor.returncode == 0, ( - f"G-17 evidence {evidence_sha} is not published in current HEAD ancestry" - ) - - -@pytest.mark.parametrize("url", UNTRUSTED_GITHUB_API_URLS) -def test_codeql_identity_client_rejects_noncanonical_github_api_authority( - monkeypatch: pytest.MonkeyPatch, url: str -) -> None: - """CodeQL GHAS reads must reject non-HTTPS or non-api.github.com authorities.""" - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", _unexpected_open) - - with pytest.raises(identity.ConfigurationIdentityError, match="GitHub API URL"): - identity._request_json(url, token="test-token", timeout_seconds=1) - - -@pytest.mark.parametrize("url", UNTRUSTED_GITHUB_API_URLS) -def test_strix_evidence_client_rejects_noncanonical_github_api_authority( - monkeypatch: pytest.MonkeyPatch, url: str -) -> None: - """Strix evidence reads must reject non-HTTPS or non-api.github.com authorities.""" - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", _unexpected_open) - - with pytest.raises(binding.EvidenceBindingError, match="GitHub API URL"): - binding.default_github_opener(url, "test-token") - - -@pytest.mark.parametrize("target", REDIRECT_TARGETS) -@pytest.mark.parametrize("client", ("codeql", "strix")) -def test_production_openers_reject_redirect_without_forwarding_bearer( - monkeypatch: pytest.MonkeyPatch, - target: str, - client: str, -) -> None: - """Drive a synthetic 302 through each actual opener and forbid a second request.""" - if client == "codeql": - opener = identity._GITHUB_API_OPENER - call = lambda: identity._request_json( - CANONICAL_GITHUB_API_URL, - token="test-token", - timeout_seconds=1, - ) - error_type = identity.ConfigurationIdentityError - else: - opener = binding._GITHUB_API_OPENER - call = lambda: binding.default_github_opener( - CANONICAL_GITHUB_API_URL, - "test-token", - ) - error_type = binding.EvidenceBindingError - - transport = _SyntheticRedirectTransport(target) - monkeypatch.setitem( - opener.handle_open, - "https", - [transport, *opener.handle_open["https"]], - ) - - with pytest.raises(error_type, match="HTTP 302"): - call() - - assert transport.calls == [ - (CANONICAL_GITHUB_API_URL, "Bearer test-token"), - ] - - -@pytest.mark.parametrize("target", REDIRECT_TARGETS) -def test_codeql_identity_client_never_constructs_redirect_request_with_bearer_token( - target: str, -) -> None: - """A GitHub response must not redirect CodeQL credentials to another URL.""" - request = Request( - CANONICAL_GITHUB_API_URL, - headers={"Authorization": "Bearer test-token"}, - ) - handler = identity._RejectRedirects() - - redirected = handler.redirect_request(request, None, 302, "Found", {}, target) - - assert redirected is None - assert request.get_header("Authorization") == "Bearer test-token" - - -@pytest.mark.parametrize("target", REDIRECT_TARGETS) -def test_strix_evidence_client_never_constructs_redirect_request_with_bearer_token( - target: str, -) -> None: - """A GitHub response must not redirect Strix credentials to another URL.""" - request = Request( - CANONICAL_GITHUB_API_URL, - headers={"Authorization": "Bearer test-token"}, - ) - handler = binding._RejectRedirects() - - redirected = handler.redirect_request(request, None, 302, "Found", {}, target) - - assert redirected is None - assert request.get_header("Authorization") == "Bearer test-token" - - -def test_canonical_github_api_authority_reaches_both_openers( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """The exact HTTPS GitHub REST authority remains an allowed production control.""" - identity_calls: list[str] = [] - strix_calls: list[str] = [] - - def identity_open(request: Any, **_kwargs: Any) -> _JsonResponse: - """Record the CodeQL client's validated request URL.""" - identity_calls.append(request.full_url) - return _JsonResponse() - - def strix_open(request: Any, **_kwargs: Any) -> _JsonResponse: - """Record the Strix client's validated request URL.""" - strix_calls.append(request.full_url) - return _JsonResponse() - - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", identity_open) - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", strix_open) - - assert identity._request_json( - CANONICAL_GITHUB_API_URL, - token="test-token", - timeout_seconds=1, - ) == [] - assert binding.default_github_opener(CANONICAL_GITHUB_API_URL, "test-token") == [] - assert identity_calls == [CANONICAL_GITHUB_API_URL] - assert strix_calls == [CANONICAL_GITHUB_API_URL] - - -def test_documented_opener_lineage_references_published_commits() -> None: - """Owner evidence must name the published commits that carry each repair.""" - doctoring = Path( - "docs/doctoring/github-api-url-authority-2248.md" - ).read_text(encoding="utf-8") - baseline = Path("docs/product-technical-gap-baseline.md").read_text( - encoding="utf-8" - ) - evidence = doctoring + baseline - - assert "57477289ebec5631b0c48f0bc419f336dbe19deb" in doctoring - assert "663ffac390d27ab21daa58b91b624d3f00dce7de" in baseline - assert "9c19c6e00eafc028068719ab482282c1256f8893" in baseline - assert "b35410673ce60f9a693532daf74862c08971e9e3" not in evidence - assert "72e17608cac2d673b50b8380301649fb86d18096" not in evidence - _assert_g17_evidence_is_published(baseline) - - -def test_published_lineage_guard_rejects_unreachable_g17_evidence() -> None: - """A commit-shaped but unpublished G-17 evidence identifier must fail closed.""" - baseline = Path("docs/product-technical-gap-baseline.md").read_text( - encoding="utf-8" - ) - mutated = baseline.replace( - "57477289ebec5631b0c48f0bc419f336dbe19deb", - "0000000000000000000000000000000000000000", - 1, - ) - - with pytest.raises(AssertionError, match="not published"): - _assert_g17_evidence_is_published(mutated) - - -def test_doctoring_qualifies_foreign_semgrep_revision_owner() -> None: - """Foreign evidence must identify its repository instead of resembling a local SHA.""" - doctoring = Path( - "docs/doctoring/github-api-url-authority-2248.md" - ).read_text(encoding="utf-8") - revision = "40b8c63f75dc7c22c8a77482d73bfb864b146f7e" - expected_link = ( - f"[semgrep/semgrep-rules revision `{revision}`]" - f"(https://github.com/semgrep/semgrep-rules/commit/{revision})" - ) - - assert expected_link in doctoring diff --git a/tests/test_materialize_base_rust_dependencies.py b/tests/test_materialize_base_rust_dependencies.py deleted file mode 100644 index a44c1e7e06..0000000000 --- a/tests/test_materialize_base_rust_dependencies.py +++ /dev/null @@ -1,412 +0,0 @@ -from __future__ import annotations - -import json -import runpy -import shutil -import subprocess -from pathlib import Path - -import pytest - -from scripts.ci import materialize_base_rust_dependencies as materializer - -pytestmark = pytest.mark.skipif( - shutil.which("cargo") is None, reason="cargo is required to vendor a real dependency graph" -) - - -def git(repo: Path, *args: str) -> str: - """Run git in a temporary fixture repository.""" - return subprocess.run( - ["git", "-C", str(repo), *args], - check=True, - capture_output=True, - text=True, - ).stdout.strip() - - -def _init_repo(repo: Path) -> None: - repo.mkdir(parents=True, exist_ok=True) - git(repo, "init") - git(repo, "config", "user.name", "Test") - git(repo, "config", "user.email", "test@example.invalid") - - -def _commit_all(repo: Path) -> str: - git(repo, "add", "-A") - git(repo, "commit", "-m", "materialize fixture") - return git(repo, "rev-parse", "HEAD") - - -def _write_single_crate_workspace(repo: Path) -> None: - (repo / "Cargo.toml").write_text( - '[workspace]\nmembers = ["crates/foo"]\nresolver = "2"\n', encoding="utf-8" - ) - crate_dir = repo / "crates" / "foo" - crate_dir.mkdir(parents=True) - (crate_dir / "Cargo.toml").write_text( - '[package]\nname = "foo"\nversion = "0.1.0"\nedition = "2021"\n\n' - '[dependencies]\nitoa = "1"\n', - encoding="utf-8", - ) - src_dir = crate_dir / "src" - src_dir.mkdir() - (src_dir / "lib.rs").write_text("pub fn x() {}\n", encoding="utf-8") - subprocess.run( - ["cargo", "generate-lockfile"], cwd=repo, check=True, capture_output=True - ) - - -def test_no_tracked_cargo_lock_skips_gracefully(tmp_path: Path) -> None: - """Repositories with no Rust code produce an empty manifest, not an error.""" - repo = tmp_path / "repo" - _init_repo(repo) - (repo / "README.md").write_text("hi\n", encoding="utf-8") - base_sha = _commit_all(repo) - - output_dir = tmp_path / "out" - manifest = materializer.materialize(repo, base_sha, output_dir) - - assert manifest == [] - assert json.loads((output_dir / "manifest.json").read_text()) == [] - assert not (output_dir / "vendor").exists() - - -def test_vendors_a_single_workspace_offline_afterward(tmp_path: Path) -> None: - """A workspace's locked dependency closure vendors, and cargo then builds offline from it.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - output_dir = tmp_path / "out" - manifest = materializer.materialize( - repo, base_sha, output_dir, vendor_dir_for_config=str(output_dir / "vendor") - ) - - assert manifest == ["Cargo.lock"] - vendored_crates = {p.name.rsplit("-", 1)[0] for p in (output_dir / "vendor").iterdir()} - assert "itoa" in vendored_crates - config_text = (output_dir / "cargo-config.toml").read_text() - assert str(output_dir / "vendor") in config_text - - cargo_home = tmp_path / "cargo-home" - cargo_home.mkdir() - (cargo_home / "config.toml").write_text(config_text, encoding="utf-8") - build = subprocess.run( - ["cargo", "build", "--offline"], - cwd=repo, - env={**__import__("os").environ, "CARGO_HOME": str(cargo_home), "CARGO_NET_OFFLINE": "true"}, - capture_output=True, - text=True, - ) - assert build.returncode == 0, build.stderr - - -def test_pr_added_dependency_not_in_base_lock_is_not_materialized(tmp_path: Path) -> None: - """Vendoring reads only the validated base commit, never a later PR-controlled lock.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - crate_toml = repo / "crates" / "foo" / "Cargo.toml" - crate_toml.write_text( - crate_toml.read_text().replace('itoa = "1"', 'itoa = "1"\nryu = "1"'), encoding="utf-8" - ) - subprocess.run(["cargo", "generate-lockfile"], cwd=repo, check=True, capture_output=True) - _commit_all(repo) - - output_dir = tmp_path / "out" - manifest = materializer.materialize(repo, base_sha, output_dir) - - assert manifest == ["Cargo.lock"] - vendored_crates = {p.name.rsplit("-", 1)[0] for p in (output_dir / "vendor").iterdir()} - assert "ryu" not in vendored_crates - - -def test_multiple_workspace_roots_fail_closed(tmp_path: Path) -> None: - """An ambiguous multi-root layout refuses to guess which lock is authoritative.""" - repo = tmp_path / "repo" - _init_repo(repo) - for name in ("a", "b"): - crate_dir = repo / name - (crate_dir).mkdir() - (crate_dir / "Cargo.toml").write_text( - f'[workspace]\nmembers = ["{name}-crate"]\n', encoding="utf-8" - ) - (crate_dir / "Cargo.lock").write_text("# empty lock\n", encoding="utf-8") - base_sha = _commit_all(repo) - - with pytest.raises(RuntimeError, match="more than one Cargo workspace root"): - materializer.materialize(repo, base_sha, tmp_path / "out") - - -def test_main_reports_error_and_exits_nonzero_on_failure( - tmp_path: Path, capsys: pytest.CaptureFixture[str] -) -> None: - """The CLI surfaces a materialization failure as ``::error::`` and exit code 1.""" - repo = tmp_path / "not-a-git-repo" - repo.mkdir() - - exit_code = materializer.main( - [ - "--repo-root", - str(repo), - "--base-sha", - "a" * 40, - "--output-dir", - str(tmp_path / "out"), - ] - ) - - assert exit_code == 1 - assert "::error::Could not materialize base Rust dependencies" in capsys.readouterr().err - - -def test_main_reports_success_with_no_rust_project( - tmp_path: Path, capsys: pytest.CaptureFixture[str] -) -> None: - """The CLI reports a clean skip for a repository with no Rust code.""" - repo = tmp_path / "repo" - _init_repo(repo) - (repo / "README.md").write_text("hi\n", encoding="utf-8") - base_sha = _commit_all(repo) - - exit_code = materializer.main( - [ - "--repo-root", - str(repo), - "--base-sha", - base_sha, - "--output-dir", - str(tmp_path / "out"), - ] - ) - - assert exit_code == 0 - assert "Rust vendoring skipped" in capsys.readouterr().out - - -def test_main_reports_success_with_a_vendored_workspace( - tmp_path: Path, capsys: pytest.CaptureFixture[str] -) -> None: - """The CLI names the vendored base lock file on a successful run.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - exit_code = materializer.main( - [ - "--repo-root", - str(repo), - "--base-sha", - base_sha, - "--output-dir", - str(tmp_path / "out"), - ] - ) - - assert exit_code == 0 - assert "Materialized trusted base Cargo vendor directory from Cargo.lock." in ( - capsys.readouterr().out - ) - - -def test_module_entry_point_runs_main(monkeypatch: pytest.MonkeyPatch) -> None: - """``python -m`` execution reaches ``main`` and propagates its exit code.""" - monkeypatch.setattr("sys.argv", ["materialize_base_rust_dependencies.py"]) - with pytest.raises(SystemExit) as excinfo: - runpy.run_path( - str(Path(materializer.__file__)), run_name="__main__" - ) - assert excinfo.value.code == 2 # argparse: missing required arguments - - -def test_malformed_ls_tree_entry_without_tab_raises(monkeypatch: pytest.MonkeyPatch) -> None: - """A git ls-tree entry with no ```` separator is a git-format integrity failure.""" - monkeypatch.setattr(materializer, "_git", lambda *_a, **_k: b"bogus-entry-with-no-tab") - with pytest.raises(RuntimeError, match="malformed entry"): - materializer._regular_cargo_blob_paths(Path("/unused"), "a" * 40) - - -def test_malformed_ls_tree_metadata_raises(monkeypatch: pytest.MonkeyPatch) -> None: - """A git ls-tree entry with the wrong metadata field count is rejected.""" - monkeypatch.setattr(materializer, "_git", lambda *_a, **_k: b"100644 blob\tCargo.toml") - with pytest.raises(RuntimeError, match="malformed metadata"): - materializer._regular_cargo_blob_paths(Path("/unused"), "a" * 40) - - -def test_symlinked_cargo_toml_is_excluded(tmp_path: Path) -> None: - """A tracked symlink named ``Cargo.toml`` is never treated as a candidate manifest.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - (repo / "linked-crate").symlink_to("crates/foo") - base_sha = _commit_all(repo) - - paths = materializer._regular_cargo_blob_paths(repo, base_sha) - - assert "linked-crate/Cargo.toml" not in paths - assert "Cargo.toml" in paths - - -def test_is_workspace_manifest_rejects_invalid_toml() -> None: - """An unparseable base ``Cargo.toml`` fails closed instead of being treated as non-workspace.""" - with pytest.raises(RuntimeError, match="could not parse"): - materializer._is_workspace_manifest(b"not = [valid toml") - - -def test_select_vendor_root_workspace_without_sibling_lock_raises( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """A workspace root manifest with no ``Cargo.lock`` next to it fails closed.""" - monkeypatch.setattr( - materializer, "_git", lambda *_a, **_k: b'[workspace]\nmembers = ["crates/foo"]\n' - ) - with pytest.raises(RuntimeError, match="no sibling Cargo.lock"): - materializer._select_vendor_root(Path("/unused"), "a" * 40, ["Cargo.toml"]) - - -def test_select_vendor_root_returns_single_standalone_crate( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """A single crate with no ``[workspace]`` table is its own vendor root.""" - monkeypatch.setattr(materializer, "_git", lambda *_a, **_k: b'[package]\nname = "foo"\n') - root = materializer._select_vendor_root( - Path("/unused"), "a" * 40, ["crate-a/Cargo.toml", "crate-a/Cargo.lock"] - ) - assert root == "crate-a" - - -def test_select_vendor_root_single_lock_without_manifest_raises( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """A standalone ``Cargo.lock`` with no sibling ``Cargo.toml`` fails closed.""" - monkeypatch.setattr(materializer, "_git", lambda *_a, **_k: b"") - with pytest.raises(RuntimeError, match="no sibling Cargo.toml"): - materializer._select_vendor_root(Path("/unused"), "a" * 40, ["crate-a/Cargo.lock"]) - - -def test_select_vendor_root_multiple_locks_without_workspace_raises( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Two independent standalone crates with no shared workspace root fail closed.""" - monkeypatch.setattr(materializer, "_git", lambda *_a, **_k: b'[package]\nname = "x"\n') - with pytest.raises(RuntimeError, match="more than one Cargo.lock"): - materializer._select_vendor_root( - Path("/unused"), - "a" * 40, - ["crate-a/Cargo.toml", "crate-a/Cargo.lock", "crate-b/Cargo.toml", "crate-b/Cargo.lock"], - ) - - -def test_placeholder_target_paths_covers_explicit_lib_and_bin_entries() -> None: - """Explicitly declared ``[lib]``/``[[bin]]`` paths are added alongside the conventions.""" - manifest = ( - b'[package]\nname = "foo"\nversion = "0.1.0"\n\n' - b'[lib]\npath = "src/custom_lib.rs"\n\n' - b'[[bin]]\nname = "cli"\npath = "src/bin/cli.rs"\n' - b'[[bin]]\nname = "nameless"\n' - ) - paths = materializer._placeholder_target_paths(manifest) - assert paths == sorted( - {"src/lib.rs", "src/main.rs", "src/custom_lib.rs", "src/bin/cli.rs"} - ) - - -def test_placeholder_target_paths_returns_empty_for_invalid_or_workspace_only_toml() -> None: - """Invalid TOML and manifests with no ``[package]`` table need no placeholder targets.""" - assert materializer._placeholder_target_paths(b"not = [valid") == [] - assert materializer._placeholder_target_paths(b'[workspace]\nmembers = ["a"]\n') == [] - - -def test_reconstruct_base_tree_does_not_overwrite_an_existing_placeholder( - tmp_path: Path, -) -> None: - """Running placeholder synthesis twice for the same manifest is a no-op the second time.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - cargo_paths = materializer._regular_cargo_blob_paths(repo, base_sha) - - work_dir = tmp_path / "work" - materializer._reconstruct_base_tree(repo, base_sha, cargo_paths, work_dir) - marker = (work_dir / "crates" / "foo" / "src" / "lib.rs").read_text() - (work_dir / "crates" / "foo" / "src" / "lib.rs").write_text("not-overwritten") - materializer._reconstruct_base_tree(repo, base_sha, cargo_paths, work_dir) - - assert (work_dir / "crates" / "foo" / "src" / "lib.rs").read_text() == "not-overwritten" - assert marker == "" - - -def test_run_cargo_vendor_propagates_missing_binary(tmp_path: Path) -> None: - """A missing ``cargo`` executable surfaces as a materialize() ``RuntimeError``.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - with pytest.MonkeyPatch.context() as monkeypatch: - monkeypatch.setattr( - materializer, - "_run_cargo_vendor", - lambda *_a, **_k: (_ for _ in ()).throw(FileNotFoundError("cargo")), - ) - with pytest.raises(RuntimeError, match="could not run trusted cargo vendor"): - materializer.materialize(repo, base_sha, tmp_path / "out") - - -def test_materialize_surfaces_cargo_vendor_failure_detail( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A non-zero ``cargo vendor`` exit is reported with its captured stderr detail.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - monkeypatch.setattr( - materializer, - "_run_cargo_vendor", - lambda *_a, **_k: subprocess.CompletedProcess( - args=["cargo", "vendor"], returncode=101, stdout=b"", stderr=b"boom\n" - ), - ) - with pytest.raises(RuntimeError, match="cargo vendor failed for base lock Cargo.lock: boom"): - materializer.materialize(repo, base_sha, tmp_path / "out") - - -def test_materialize_surfaces_cargo_vendor_failure_with_no_stderr( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """A non-zero ``cargo vendor`` exit with empty stderr still names the exit status.""" - repo = tmp_path / "repo" - _init_repo(repo) - _write_single_crate_workspace(repo) - base_sha = _commit_all(repo) - - monkeypatch.setattr( - materializer, - "_run_cargo_vendor", - lambda *_a, **_k: subprocess.CompletedProcess( - args=["cargo", "vendor"], returncode=101, stdout=b"", stderr=b"" - ), - ) - with pytest.raises(RuntimeError, match="exit status 101"): - materializer.materialize(repo, base_sha, tmp_path / "out") - - -def test_materialize_rejects_bad_sha_and_symlinked_output_dir(tmp_path: Path) -> None: - """Both input-validation guards fail closed before any git or cargo command runs.""" - with pytest.raises(ValueError, match="40 hexadecimal"): - materializer.materialize(Path("/unused"), "not-a-sha", tmp_path / "out") - - real_dir = tmp_path / "real" - real_dir.mkdir() - linked_output = tmp_path / "linked-out" - linked_output.symlink_to(real_dir) - with pytest.raises(ValueError, match="must not be a symlink"): - materializer.materialize(Path("/unused"), "a" * 40, linked_output) diff --git a/tests/test_maturin_offline_build_contract.py b/tests/test_maturin_offline_build_contract.py deleted file mode 100644 index 65e483bc72..0000000000 --- a/tests/test_maturin_offline_build_contract.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Contract: the coverage sandbox builds a PyO3/maturin extension fully offline. - -Reproduces the sandbox shape fast-mlsirm#1907 hit: `python3 -m coverage run -m pytest` failed -collection with `ImportError: cannot import name '_core'` because nothing in the -`--network=none` coverage container ever built the compiled extension. This exercises the same -two steps the workflow's `build_maturin_extension_if_needed` helper -(.github/workflows/opencode-review-dispatch.yml) performs -- vendor the *base* commit's Cargo -dependencies with materialize_base_rust_dependencies.py, then run -`maturin build --offline` against that vendor directory -- and proves both that the import -succeeds afterward and that a dependency only a pull request added is never fetched. -""" - -from __future__ import annotations - -import shutil -import subprocess -import sys -from pathlib import Path - -import pytest - -from scripts.ci import materialize_base_rust_dependencies as materializer - -def _maturin_importable() -> bool: - """Return whether ``sys.executable`` (the interpreter these tests run under) has maturin.""" - return ( - subprocess.run( - [sys.executable, "-c", "import maturin"], capture_output=True - ).returncode - == 0 - ) - - -pytestmark = pytest.mark.skipif( - shutil.which("cargo") is None or shutil.which("rustc") is None or not _maturin_importable(), - reason="cargo, rustc, and an importable maturin module are required to build a real PyO3 extension", -) - -_PYPROJECT_TOML = """\ -[build-system] -requires = ["maturin>=1,<2"] -build-backend = "maturin" - -[project] -name = "fixture_core" -version = "0.1.0" -requires-python = ">=3.10" - -[tool.maturin] -module-name = "fixture_core._core" -""" - -_CARGO_TOML = """\ -[package] -name = "fixture_core" -version = "0.1.0" -edition = "2021" - -[lib] -name = "_core" -crate-type = ["cdylib"] - -[dependencies] -pyo3 = {{ version = "0.22", features = ["extension-module", "abi3-py310"] }} -{extra_dependency} -""" - -_LIB_RS = """\ -use pyo3::prelude::*; - -#[pyfunction] -fn ping() -> i64 {{ 42 }} - -#[pymodule] -fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {{ - m.add_function(wrap_pyfunction!(ping, m)?)?; - Ok(()) -}} -""" - - -def _git(repo: Path, *args: str) -> str: - return subprocess.run( - ["git", "-C", str(repo), *args], - check=True, - capture_output=True, - text=True, - ).stdout.strip() - - -def _init_repo(repo: Path) -> None: - repo.mkdir(parents=True, exist_ok=True) - _git(repo, "init") - _git(repo, "config", "user.name", "Test") - _git(repo, "config", "user.email", "test@example.invalid") - - -def _write_fixture_project(repo: Path, *, extra_dependency: str = "") -> None: - (repo / "pyproject.toml").write_text(_PYPROJECT_TOML, encoding="utf-8") - (repo / "Cargo.toml").write_text( - _CARGO_TOML.format(extra_dependency=extra_dependency), encoding="utf-8" - ) - src_dir = repo / "src" - src_dir.mkdir(exist_ok=True) - (src_dir / "lib.rs").write_text(_LIB_RS, encoding="utf-8") - package_dir = repo / "fixture_core" - package_dir.mkdir(exist_ok=True) - (package_dir / "__init__.py").touch() - subprocess.run( - ["cargo", "generate-lockfile"], cwd=repo, check=True, capture_output=True - ) - - -def _commit_all(repo: Path, message: str) -> str: - _git(repo, "add", "-A") - _git(repo, "commit", "-m", message) - return _git(repo, "rev-parse", "HEAD") - - -def _build_offline( - repo: Path, cargo_home: Path, vendor_output: Path, final_vendor_dir: Path, dist_dir: Path -) -> subprocess.CompletedProcess[str]: - """Run the exact offline build the sandbox's coverage step performs. - - Mirrors the workflow: materialization runs on the runner at ``vendor_output``, then the - ``base-rust-dependencies`` directory is copied into the trusted image at the fixed path - the baked ``cargo-config.toml`` names (``final_vendor_dir`` here). - """ - cargo_home.mkdir(parents=True, exist_ok=True) - shutil.copyfile(vendor_output / "cargo-config.toml", cargo_home / "config.toml") - shutil.copytree(vendor_output / "vendor", final_vendor_dir) - return subprocess.run( - [sys.executable, "-m", "maturin", "build", "--offline", "--release", "-o", str(dist_dir)], - cwd=repo, - env={ - "PATH": __import__("os").environ["PATH"], - "HOME": __import__("os").environ.get("HOME", "/tmp"), - "CARGO_HOME": str(cargo_home), - "CARGO_NET_OFFLINE": "true", - "CARGO_BUILD_JOBS": "1", - }, - capture_output=True, - text=True, - timeout=600, - ) - - -def test_offline_build_and_import_of_pyo3_extension_succeeds(tmp_path: Path) -> None: - """The vendored-offline build produces an importable `_core` extension module.""" - repo = tmp_path / "fixture-repo" - _init_repo(repo) - _write_fixture_project(repo) - base_sha = _commit_all(repo, "base commit") - - vendor_output = tmp_path / "vendor-output" - final_vendor_dir = tmp_path / "final-vendor-location" - materializer.materialize( - repo, base_sha, vendor_output, vendor_dir_for_config=str(final_vendor_dir) - ) - assert (vendor_output / "vendor").is_dir() - assert final_vendor_dir.as_posix() in (vendor_output / "cargo-config.toml").read_text( - "utf-8" - ) - - cargo_home = tmp_path / "cargo-home" - dist_dir = tmp_path / "dist" - result = _build_offline(repo, cargo_home, vendor_output, final_vendor_dir, dist_dir) - assert result.returncode == 0, result.stderr - - wheels = list(dist_dir.glob("*.whl")) - assert len(wheels) == 1 - - install_root = tmp_path / "install-root" - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--no-index", - "--no-deps", - "--target", - str(install_root), - str(wheels[0]), - ], - check=True, - capture_output=True, - text=True, - ) - check = subprocess.run( - [sys.executable, "-c", "from fixture_core import _core; print(_core.ping())"], - cwd=tmp_path, - env={"PYTHONPATH": str(install_root)}, - capture_output=True, - text=True, - ) - assert check.returncode == 0, check.stderr - assert check.stdout.strip() == "42" - - -def test_pull_request_added_dependency_is_never_fetched_offline(tmp_path: Path) -> None: - """A dependency only the PR head added must not be silently fetched offline.""" - repo = tmp_path / "fixture-repo" - _init_repo(repo) - _write_fixture_project(repo) - base_sha = _commit_all(repo, "base commit") - - # Simulate a pull request that adds a new Cargo dependency the trusted base - # materializer never saw and therefore never vendored. - _write_fixture_project(repo, extra_dependency='itoa = "1"') - (repo / "src" / "lib.rs").write_text( - _LIB_RS.replace( - "fn ping() -> i64 {{ 42 }}", - 'fn ping() -> i64 {{ itoa::Buffer::new().format(42i64).len() as i64 }}', - ), - encoding="utf-8", - ) - subprocess.run(["cargo", "generate-lockfile"], cwd=repo, check=True, capture_output=True) - _commit_all(repo, "pull request adds a new Cargo dependency") - - vendor_output = tmp_path / "vendor-output" - final_vendor_dir = tmp_path / "final-vendor-location" - materializer.materialize( - repo, base_sha, vendor_output, vendor_dir_for_config=str(final_vendor_dir) - ) - - vendor_crate_names = { - entry.name.rsplit("-", 1)[0] for entry in (vendor_output / "vendor").iterdir() - } - assert "itoa" not in vendor_crate_names - - cargo_home = tmp_path / "cargo-home" - dist_dir = tmp_path / "dist" - result = _build_offline(repo, cargo_home, vendor_output, final_vendor_dir, dist_dir) - - assert result.returncode != 0 - combined_output = result.stdout + result.stderr - assert "itoa" in combined_output - assert not list(dist_dir.glob("*.whl")) diff --git a/tests/test_noema_document_review_context.py b/tests/test_noema_document_review_context.py deleted file mode 100644 index e6ec2e6d70..0000000000 --- a/tests/test_noema_document_review_context.py +++ /dev/null @@ -1,277 +0,0 @@ -"""Regression tests for binary document input on the canonical Noema path.""" - -from __future__ import annotations - -import base64 -import io -import json -import os -import zipfile -from pathlib import Path - -import pytest - -from scripts.ci import noema_review_document as document -from scripts.ci import noema_review_gate as noema - - -def _docx_bytes(*, malformed: bool = False) -> bytes: - """Build a synthetic DOCX containing body, table, and Office Math text.""" - if malformed: - return b"not a zip archive" - xml = """ - - - DOCX-REVIEW-MARKERx+y - table-cell-a - table-cell-b - -""" - output = io.BytesIO() - with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: - archive.writestr("word/document.xml", xml) - return output.getvalue() - - -def _docx_entity_bytes() -> bytes: - """Build a DOCX whose entity declaration must be rejected safely.""" - xml = """ -]> - - &expansion; -""" - output = io.BytesIO() - with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: - archive.writestr("word/document.xml", xml) - return output.getvalue() - - -def _pr() -> dict[str, object]: - return { - "headRefOid": "head", - "baseRefOid": "base", - "title": "document review input", - "reviewThreads": {"nodes": []}, - } - - -def test_hosted_reader_bundle_is_pinned_and_local(): - """The hosted workflow must install only the reviewed local reader bundle.""" - repository_root = Path(__file__).resolve().parents[1] - workflow = (repository_root / ".github/workflows/noema-review.yml").read_text( - encoding="utf-8" - ) - quality_workflow = ( - repository_root - / ".github/workflows/agent-review-runtime-quality-ci.yml" - ).read_text(encoding="utf-8") - package = json.loads( - (repository_root / "scripts/ci/noema-document-reader/package.json").read_text( - encoding="utf-8" - ) - ) - lock = json.loads( - ( - repository_root / "scripts/ci/noema-document-reader/package-lock.json" - ).read_text(encoding="utf-8") - ) - - assert "Provision local reviewed HWP document reader" in workflow - assert 'NPM_CONFIG_IGNORE_SCRIPTS: "true"' in workflow - assert "npm ci --ignore-scripts --omit=dev --no-audit --no-fund" in workflow - assert "NOEMA_HWP_MCP_SOURCE=$reader_root/node_modules/hwp-mcp" in workflow - 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 "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 - assert "Install exact Noema document dependencies" in quality_workflow - for path in ( - "scripts/ci/noema_review_document.py", - "scripts/ci/noema_hwp_mcp_reader.mjs", - "scripts/ci/noema-document-reader/package.json", - "scripts/ci/noema-document-reader/package-lock.json", - "tests/test_noema_document_review_context.py", - ): - assert path in quality_workflow - assert "tests/test_noema_document_review_context.py" in quality_workflow - - -def test_docx_text_reaches_the_actual_reviewer_payload(monkeypatch): - """The extracted document context must be inside the model request body.""" - raw = _docx_bytes() - encoded = base64.b64encode(raw).decode("ascii") - - def fake_run(args, stdin=None): - assert "contents/docs/review.docx?ref=head" in args[2] - return encoded - - monkeypatch.setattr(noema, "run", fake_run) - context = noema.build_review_context( - "owner/repo", 7, _pr(), [("docs/review.docx", "modified")] - ) - assert "DOCX-REVIEW-MARKER" in context - assert "x+y" in context - assert "table-cell-a" in context - assert "table-cell-b" in context - - monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") - monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") - monkeypatch.setattr(noema, "validate_substantive_verdict", lambda *_args: None) - captured: dict[str, object] = {} - - class Response: - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def read(self): - verdict = {"decision": "comment", "summary": "checked", "findings": []} - return json.dumps( - {"choices": [{"message": {"content": json.dumps(verdict)}}]} - ).encode() - - class Opener: - def open(self, request): - captured.update(json.loads(request.data.decode())) - return Response() - - monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) - noema.call_llm( - "owner/repo", - 7, - _pr(), - "diff --git a/docs/review.docx b/docs/review.docx\n+binary\n", - False, - "head", - context, - ("docs/review.docx",), - ) - prompt = captured["messages"][1]["content"] - assert "DOCX-REVIEW-MARKER" in prompt - assert "table-cell-a" in prompt - - -def test_malformed_docx_is_explicit_in_review_context(monkeypatch): - """Malformed document bytes are reported instead of UTF-8 replacement text.""" - encoded = base64.b64encode(_docx_bytes(malformed=True)).decode("ascii") - monkeypatch.setattr(noema, "run", lambda _args, stdin=None: encoded) - - context = noema.changed_file_context( - "owner/repo", 7, "head", changed_files=[("docs/broken.docx", "modified")] - ) - - assert "### docs/broken.docx" in context - assert "document extraction failed: DOCX archive is malformed" in context - assert "not a zip archive" not in context - - -def test_forbidden_docx_entities_are_explicitly_rejected(): - """Defused XML entity failures become the same bounded reader error.""" - with pytest.raises(document.DocumentReadError, match="DOCX document.xml is malformed"): - document.extract_review_document("docs/entity.docx", _docx_entity_bytes()) - - -def test_hwp_reader_contract_is_local_and_fail_closed(monkeypatch): - """HWP/HWPX use the configured local adapter and reject failed readers.""" - monkeypatch.setenv(document.HWP_READER_ENV, "/trusted/hwp-mcp-source") - completed = document.subprocess.CompletedProcess( - ["node"], 0, stdout=b"HWP-REVIEW-MARKER\n", stderr=b"" - ) - monkeypatch.setattr(document.subprocess, "run", lambda *args, **kwargs: completed) - assert ( - document.extract_review_document("docs/review.hwpx", b"binary") - == "HWP-REVIEW-MARKER" - ) - - failed = document.subprocess.CompletedProcess( - ["node"], 1, stdout=b"", stderr=b"private parser details" - ) - monkeypatch.setattr(document.subprocess, "run", lambda *args, **kwargs: failed) - try: - document.extract_review_document("docs/broken.hwp", b"binary") - except document.DocumentReadError as exc: - assert str(exc) == "reviewed hwp-mcp/rhwp reader failed (exit 1)" - else: - raise AssertionError("expected failed local HWP reader to fail closed") - - def timed_out(*args, **kwargs): - raise document.subprocess.TimeoutExpired(args[0], kwargs["timeout"]) - - monkeypatch.setattr(document.subprocess, "run", timed_out) - try: - document.extract_review_document("docs/slow.hwpx", b"binary") - except document.DocumentReadError as exc: - assert "timed out after" in str(exc) - else: - raise AssertionError("expected hung local HWP reader to fail closed") - - -@pytest.mark.parametrize( - ("fixture_name", "expected_text"), - [("simple.hwp", "안녕하세요 hwp-mcp."), ("text_only.hwpx", "hwpx 텍스트.")], -) -def test_real_hwp_mcp_fixture_text_reaches_reviewer_payload( - monkeypatch, fixture_name, expected_text -): - """The reviewed local hwp-mcp/rhwp fixture reaches the Noema request.""" - source = Path(os.environ.get(document.HWP_READER_ENV, "")) - fixture = source / "test" / "fixtures" / fixture_name - if not fixture.is_file(): - pytest.skip( - "NOEMA_HWP_MCP_SOURCE is not configured with local reviewed fixtures" - ) - - monkeypatch.setenv(document.HWP_READER_ENV, str(source)) - encoded = base64.b64encode(fixture.read_bytes()).decode("ascii") - monkeypatch.setattr(noema, "run", lambda _args, stdin=None: encoded) - context = noema.build_review_context( - "owner/repo", 7, _pr(), [(f"docs/{fixture_name}", "modified")] - ) - assert expected_text in context - if fixture_name == "simple.hwp": - assert "| 이름 | 회사 |" in context - assert "| 남대현 | 포텐랩 |" in context - - monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") - monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") - monkeypatch.setattr(noema, "validate_substantive_verdict", lambda *_args: None) - captured: dict[str, object] = {} - - class Response: - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def read(self): - verdict = {"decision": "comment", "summary": "checked", "findings": []} - return json.dumps( - {"choices": [{"message": {"content": json.dumps(verdict)}}]} - ).encode() - - class Opener: - def open(self, request): - captured.update(json.loads(request.data.decode())) - return Response() - - monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) - noema.call_llm( - "owner/repo", - 7, - _pr(), - f"diff --git a/docs/{fixture_name} b/docs/{fixture_name}\n+binary\n", - False, - "head", - context, - (f"docs/{fixture_name}",), - ) - prompt = captured["messages"][1]["content"] - assert expected_text in prompt - if fixture_name == "simple.hwp": - assert "| 이름 | 회사 |" in prompt diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 490286aacc..628fa3cbc1 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -199,14 +199,8 @@ def test_noema_review_credentials_and_llm_use_orchestrator_free() -> None: publish = workflow_step(workflow, "Publish prepared Noema verdict on the exact live head") assert '.github/actions/noema-review/two_phase.py' in prepare assert '--prepare-verdict-file "$verdict_file"' in prepare - assert "NOEMA_TRANSPORT_RETRY_ATTEMPT" in prepare assert '.github/actions/noema-review/two_phase.py' in publish assert '--publish-verdict-file "$verdict_file"' in publish - redispatch = workflow_step(workflow, "Schedule bounded Noema transport re-dispatch") - assert 'transport_capacity_unavailable == \'true\'' in redispatch - assert 'transport_retry_eligible == \'true\'' in redispatch - assert 'event_type: "noema-review"' in redispatch - assert "transport_retry_attempt" in redispatch assert "python3 -m scripts.ci.noema_review_gate" not in workflow assert ( "contextual-orchestrator review sidecar must be provisioned before Noema LLM review." diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 5053645802..e8a0dd6f59 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1638,220 +1638,11 @@ def open(self, request): assert "upstream_phase=connecting" in output assert "attempt_number=2" in output assert "upstream_status=503" in output - assert "provider_attempt_count=1" in output assert "terminal_reason=eligible_candidates_exhausted" in output - assert "outcome=provider_capacity_unavailable" in output - assert "outcome=provider_capacity_unavailable" in diagnostic - assert exc_info.value.capacity_unavailable is True - assert exc_info.value.http_status == 502 - assert exc_info.value.provider_attempt_count == 1 assert secret not in output assert secret not in diagnostic -def test_is_provider_capacity_http_status_covers_only_capacity_class(): - """429/5xx are capacity; other statuses stay ordinary transport failures.""" - assert noema.is_provider_capacity_http_status(429) is True - assert noema.is_provider_capacity_http_status(502) is True - assert noema.is_provider_capacity_http_status(400) is False - assert noema.is_provider_capacity_http_status(None) is False - - -def test_transport_redispatch_delay_honors_retry_after_and_bound(): - """Retry-After wins when bounded; exhausted attempts refuse another delay.""" - head = "a" * 40 - assert ( - noema.transport_redispatch_delay_seconds( - transport_retry_attempt=0, - head_sha=head, - retry_after_seconds=90, - ) - == 90 - ) - assert ( - noema.transport_redispatch_delay_seconds( - transport_retry_attempt=0, - head_sha=head, - retry_after_seconds=999, - ) - is None - ) - delay = noema.transport_redispatch_delay_seconds( - transport_retry_attempt=0, - head_sha=head, - ) - assert delay is not None - assert ( - noema.TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS - <= delay - <= noema.TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS - ) - assert ( - noema.transport_redispatch_delay_seconds( - transport_retry_attempt=noema.MAX_TRANSPORT_REDISPATCH_ATTEMPTS, - head_sha=head, - ) - is None - ) - # Deterministic for the same head/attempt pair. - assert delay == noema.transport_redispatch_delay_seconds( - transport_retry_attempt=0, - head_sha=head, - ) - - -def test_parse_http_retry_after_seconds_rejects_hostile_values(): - """Only whole-seconds Retry-After values inside the ADR cap are accepted.""" - assert noema.parse_http_retry_after_seconds({"Retry-After": "120"}) == 120 - assert noema.parse_http_retry_after_seconds({"Retry-After": "0"}) is None - assert noema.parse_http_retry_after_seconds({"Retry-After": "301"}) is None - assert noema.parse_http_retry_after_seconds({"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"}) is None - assert noema.parse_http_retry_after_seconds({"Retry-After": "²"}) is None - assert noema.parse_http_retry_after_seconds(None) is None - assert noema.parse_http_retry_after_seconds(object()) is None - - class HostileHeaders: - def get(self, _name: str) -> str: - raise RuntimeError("hostile") - - assert noema.parse_http_retry_after_seconds(HostileHeaders()) is None - - -def test_append_github_output_noop_without_path_or_values(monkeypatch): - """Missing Actions output path or empty maps must not raise.""" - monkeypatch.delenv("GITHUB_OUTPUT", raising=False) - noema.append_github_output({"transport_retry_eligible": "true"}) - monkeypatch.setenv("GITHUB_OUTPUT", "/tmp/unused-noema-output") - noema.append_github_output({}) - - -def test_current_transport_retry_attempt_rejects_oversized_counter(monkeypatch): - """Counters above the hard ceiling fail closed to zero.""" - monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", "65") - assert noema.current_transport_retry_attempt() == 0 - - -def test_transport_redispatch_delay_rejects_negative_attempt_and_non_int_retry_after(): - """Negative attempts and non-int Retry-After values refuse a schedule.""" - head = "b" * 40 - assert ( - noema.transport_redispatch_delay_seconds( - transport_retry_attempt=-1, - head_sha=head, - ) - is None - ) - assert ( - noema.transport_redispatch_delay_seconds( - transport_retry_attempt=0, - head_sha=head, - retry_after_seconds="90", # type: ignore[arg-type] - ) - is None - ) - - -def test_call_llm_http_400_is_transport_but_not_capacity(monkeypatch, capsys): - """A non-transient 400 stays typed transport without authorizing re-dispatch.""" - monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") - monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") - - class Opener: - def open(self, request): - raise noema.urllib.error.HTTPError( - request.full_url, 400, "Bad Request", {}, io.BytesIO(b"{}") - ) - - monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) - - with pytest.raises(noema.NoemaTransportError) as exc_info: - noema.call_llm("owner/repo", 1, make_pr(), "diff", False, "head") - - assert exc_info.value.capacity_unavailable is False - assert exc_info.value.http_status == 400 - assert "outcome=provider_capacity_unavailable" not in capsys.readouterr().out - - -def test_call_llm_http_429_with_retry_after_is_capacity(monkeypatch, capsys): - """429 after gateway failover is capacity-class and preserves Retry-After.""" - monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") - monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") - body = json.dumps( - { - "error": { - "detail": { - "model": "provider/model-a", - "attempts": [ - {"provider_name": "openrouter", "attempt_number": 1, "provider_status": 429}, - {"provider_name": "nvidia_nim", "attempt_number": 2, "provider_status": 429}, - ], - } - } - } - ).encode() - - class Opener: - def open(self, request): - raise noema.urllib.error.HTTPError( - request.full_url, - 429, - "Too Many Requests", - {"Retry-After": "75"}, - io.BytesIO(body), - ) - - monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) - - with pytest.raises(noema.NoemaTransportError) as exc_info: - noema.call_llm("owner/repo", 1, make_pr(), "diff", False, "head") - - output = capsys.readouterr().out - assert exc_info.value.capacity_unavailable is True - assert exc_info.value.http_status == 429 - assert exc_info.value.retry_after_seconds == 75 - assert exc_info.value.provider_attempt_count == 2 - assert "provider_attempt_count=2" in output - assert "outcome=provider_capacity_unavailable" in output - - -def test_append_github_output_writes_allowlisted_keys(tmp_path, monkeypatch): - """GitHub Actions outputs accept only safe keys and single-line values.""" - output_path = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) - noema.append_github_output( - { - "transport_retry_eligible": "true", - "bad key": "nope", - "multiline": "a\nb", - } - ) - written = output_path.read_text(encoding="utf-8") - assert "transport_retry_eligible=true\n" in written - assert "bad key" not in written - assert "multiline" not in written - - -def test_current_transport_retry_attempt_parses_decimal_env(monkeypatch): - """Malformed counters fail closed to zero rather than inventing a budget.""" - monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", "1") - assert noema.current_transport_retry_attempt() == 1 - monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", "nope") - assert noema.current_transport_retry_attempt() == 0 - monkeypatch.delenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", raising=False) - assert noema.current_transport_retry_attempt() == 0 - - -def test_adr_0031_records_capacity_redispatch_decision(): - """Issue #2165's ADR decision must stay durable in-repo, not only in chat.""" - adr = Path("docs/adr/0031-noema-transport-capacity-redispatch.md").read_text( - encoding="utf-8" - ) - assert "provider_capacity_unavailable" in adr - assert "MAX_TRANSPORT_REDISPATCH_ATTEMPTS = 2" in adr - assert "does not gain a caller-side retry loop" in adr - assert "#2165" in adr - - @pytest.mark.parametrize( "body", [ @@ -1945,7 +1736,6 @@ def open(self, request): output = capsys.readouterr().out assert "served_model=github_models/deepseek-v3" in output - assert "provider_attempt_count=1" in output assert "provider_name=" not in output assert "upstream_phase=" not in output assert "attempt_number=" not in output diff --git a/tests/test_noema_two_phase_handoff.py b/tests/test_noema_two_phase_handoff.py index fd905fa343..992522be7b 100644 --- a/tests/test_noema_two_phase_handoff.py +++ b/tests/test_noema_two_phase_handoff.py @@ -191,77 +191,3 @@ def test_reader_rejects_hardlinked_aliases(tmp_path: Path) -> None: finally: envelope.unlink(missing_ok=True) alias.unlink(missing_ok=True) - - -def test_prepare_emits_capacity_outputs_before_failing_closed( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Transport capacity failures publish re-dispatch outputs without sealing a verdict.""" - module = _load_module() - _patch_live_gate(monkeypatch, module) - monkeypatch.setattr(module.gate, "fetch_diff", lambda _repo, _number: ("diff", False)) - monkeypatch.setattr(module.gate, "fetch_changed_files", lambda _repo, _number: [("src/a.py", "MODIFIED")]) - monkeypatch.setattr(module.gate, "build_review_context", lambda *_args: "context") - monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", "0") - output_path = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) - - def raise_capacity(*_args: object, **_kwargs: object) -> None: - raise module.gate.NoemaTransportError( - "Noema gateway transport failed: capacity", - capacity_unavailable=True, - http_status=429, - provider_attempt_count=3, - retry_after_seconds=90, - ) - - monkeypatch.setattr(module.gate, "call_llm", raise_capacity) - envelope = tmp_path / "verdict.json" - - with pytest.raises(module.gate.NoemaTransportError): - module.prepare_verdict("ContextualWisdomLab/example", 7, HEAD, envelope) - - assert not envelope.exists() - written = output_path.read_text(encoding="utf-8") - assert "transport_capacity_unavailable=true" in written - assert "transport_retry_eligible=true" in written - assert "transport_retry_delay_seconds=90" in written - assert "transport_retry_next_attempt=1" in written - assert "provider_attempt_count=3" in written - assert "transport_http_status=429" in written - - -def test_prepare_marks_exhausted_capacity_budget_ineligible( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - """When automatic re-dispatch attempts are spent, capacity stays failed closed.""" - module = _load_module() - _patch_live_gate(monkeypatch, module) - monkeypatch.setattr(module.gate, "fetch_diff", lambda _repo, _number: ("diff", False)) - monkeypatch.setattr(module.gate, "fetch_changed_files", lambda _repo, _number: [("src/a.py", "MODIFIED")]) - monkeypatch.setattr(module.gate, "build_review_context", lambda *_args: "context") - monkeypatch.setenv( - "NOEMA_TRANSPORT_RETRY_ATTEMPT", - str(module.gate.MAX_TRANSPORT_REDISPATCH_ATTEMPTS), - ) - output_path = tmp_path / "github_output" - monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) - - def raise_capacity(*_args: object, **_kwargs: object) -> None: - raise module.gate.NoemaTransportError( - "Noema gateway transport failed: capacity", - capacity_unavailable=True, - http_status=502, - provider_attempt_count=4, - ) - - monkeypatch.setattr(module.gate, "call_llm", raise_capacity) - - with pytest.raises(module.gate.NoemaTransportError): - module.prepare_verdict("ContextualWisdomLab/example", 7, HEAD, tmp_path / "verdict.json") - - written = output_path.read_text(encoding="utf-8") - assert "transport_capacity_unavailable=true" in written - assert "transport_retry_eligible=false" in written - assert "transport_retry_delay_seconds=" not in written - assert "automatic re-dispatch budget is exhausted" in capsys.readouterr().out diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 5a41cb7cdc..321d25bd57 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -479,20 +479,11 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): "github.event.pull_request.head.repo.full_name == github.repository" not in workflow ) - # coverage-source-tree was folded into validate-pr-metadata (2026-09-17): - # both only ever exchanged the OpenCode app token for READ-scoped data and - # neither executes untrusted PR-head content, so they sit on the same side - # of the trust boundary that keeps coverage-evidence (untrusted test/build - # execution, `actions: read` only) and opencode-review-target (privileged - # write-capable publication) isolated. Folding them removes one of the - # three needs:-chained job-to-job runner-queue re-entries this workflow - # paid under saturation; see - # docs/doctoring/actions-capacity-root-cause-20260917.md. - assert " coverage-source-tree:\n" not in workflow + assert " coverage-source-tree:\n" in workflow assert " coverage-evidence:\n" in workflow metadata_start = workflow.index(" validate-pr-metadata:\n") - metadata_end = workflow.index("\n coverage-evidence:", metadata_start) + metadata_end = workflow.index("\n coverage-source-tree:", metadata_start) metadata_job = workflow[metadata_start:metadata_end] assert "id-token: write" in metadata_job assert ( @@ -507,18 +498,22 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): "github.event.client_payload.target_repository != github.repository" in metadata_job ) - assert "github.event_name == 'repository_dispatch'" in metadata_job - assert "github.event_name == 'pull_request_target'" not in metadata_job + + source_start = workflow.index(" coverage-source-tree:\n") + source_end = workflow.index("\n coverage-evidence:", source_start) + source_job = workflow[source_start:source_end] + assert "github.event_name == 'repository_dispatch'" in source_job + assert "github.event_name == 'pull_request_target'" not in source_job + assert "id-token: write" in source_job assert ( - "Exchange OpenCode app token for target repository coverage reads" - in metadata_job + "Exchange OpenCode app token for target repository coverage reads" in source_job ) assert ( "GH_TOKEN: ${{ steps.coverage_read_app_token.outputs.token || " "secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}" - ) in metadata_job + ) in source_job assert ( - "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in metadata_job + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in source_job ) coverage_start = workflow.index(" coverage-evidence:\n") @@ -527,7 +522,7 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "github.event_name == 'repository_dispatch'" in coverage_job assert "github.event_name == 'pull_request_target'" not in coverage_job assert "id-token: write" not in coverage_job - assert "Report coverage source materialization failure" not in coverage_job + assert "Report coverage source materialization failure" in coverage_job assert ( "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" in coverage_job @@ -753,20 +748,17 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "opencode-base-vcs-dependencies.pth" in measure_step assert 'vcs-manifest.json >"$dependency_list"' in measure_step assert 'done <"$dependency_list"' in measure_step - # Import-root admission (including immutable ``python/`` layouts for - # fast-mlsirm) lives in scripts/ci/resolve_opencode_base_vcs_import_root.sh; - # the Dockerfile COPYs that helper rather than inlining candidate discovery. - assert "resolve_opencode_base_vcs_import_root.sh" in measure_step - assert ( - "COPY resolve-opencode-base-vcs-import-root.sh" - " /usr/local/libexec/resolve-opencode-base-vcs-import-root.sh" - ) in measure_step - assert 'install -m 0755 "$trusted_vcs_import_root_resolver"' in measure_step - assert ( - 'python_root="$("$resolver" "$destination" "$import_name" "$repository")"' - in measure_step - ) - assert 'candidate_count=$((candidate_count + 1))' not in measure_step + assert 'candidate_count=$((candidate_count + 1))' in measure_step + assert '[ "$candidate_count" -ne 1 ]' in measure_step + assert "has a missing or ambiguous import root" in measure_step + assert '[ ! -f "$import_root/__init__.py" ]' in measure_step + assert "has a namespace or linked import root" in measure_step + assert 'find "$destination" -type l -print -quit' in measure_step + assert "contains a symbolic-link layout" in measure_step + assert "-name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.dylib'" in measure_step + assert "contains a compiled extension" in measure_step + assert "-name '*.dist-info' -o -name '*.egg-info'" in measure_step + assert "contains installed distribution metadata" in measure_step assert 'printf \'%s\\n\' "$python_root" >>"$path_file"' in measure_step assert 'chmod -R a+rX /opt/base-vcs-dependencies "$path_file"' in measure_step assert "docker build --pull --no-cache --network=default" in measure_step @@ -2379,11 +2371,6 @@ def test_merge_scheduler_uses_escalating_mutation_credentials(): encoding="utf-8" ) - scan_job = workflow.split(" scan-pr-queue:\n", 1)[1] - permission_block = scan_job.split(" permissions:\n", 1)[1].split(" env:\n", 1)[0] - status_permissions = re.findall(r"^ statuses: (\w+)\s*$", permission_block, re.MULTILINE) - assert status_permissions == ["read"], "same-repository status evidence needs read-only permission" - assert "id-token: write" in workflow assert "Exchange OpenCode app token for scheduler mutations" in workflow assert "secrets.PR_REVIEW_MERGE_TOKEN" in workflow diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index c764ad0ad2..5c5325d1aa 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -49,7 +49,7 @@ def admission_script() -> str: """Extract the exact-head admission shell that precedes concurrency.""" workflow = WORKFLOW.read_text(encoding="utf-8") step = workflow.split(" - name: Admit only the exact live OpenCode head\n", 1)[1] - return textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n\n changed-scope:", 1)[0]) + return textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n\n coverage-source-tree:", 1)[0]) def test_stale_opencode_event_never_reaches_review_concurrency(tmp_path: Path) -> None: @@ -643,9 +643,9 @@ def test_opencode_review_concurrency_group_is_workflow_level_repo_and_pr() -> No assert "github.event.pull_request.number || github.run_id" in concurrency_block assert workflow_level_cancels_in_progress(workflow) assert " concurrency:" not in target_job.split(" permissions:", 1)[0] - admission = workflow.split( - " - name: Admit only the exact live OpenCode head\n", 1 - )[1].split("\n changed-scope:", 1)[0] + admission = workflow.split("\n admit-current-head:\n", 1)[1].split( + "\n coverage-source-tree:", 1 + )[0] assert "live_head" in admission assert "live_state" in admission assert 'echo "admitted=false"' in admission diff --git a/tests/test_opencode_review_coalesce_tick.py b/tests/test_opencode_review_coalesce_tick.py deleted file mode 100644 index cc44534640..0000000000 --- a/tests/test_opencode_review_coalesce_tick.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Contract for the push-burst coalescing tick. - -See docs/doctoring/actions-capacity-root-cause-20260917.md for the -measurement this window is derived from, and -scripts/ci/pr_review_merge_scheduler_core.py's coalesce_enabled()/ -head_stable_for_seconds() for the gate this tick's own dispatches pass -through -- the same gate used by every other scheduler invocation, so it -stays inert everywhere else unless this workflow's own env explicitly -turns it on. -""" - -from __future__ import annotations - -from pathlib import Path - -WORKFLOW_PATH = Path(".github/workflows/opencode-review-coalesce-tick.yml") - - -def _workflow_text() -> str: - return WORKFLOW_PATH.read_text(encoding="utf-8") - - -def _job_block() -> str: - workflow = _workflow_text() - return workflow.split("\njobs:\n", 1)[1] - - -def test_tick_is_inert_by_default(): - """Job-level gate skips before runner admission when coalescing is off. - - Step-scoped gating (#2232) forced inert ticks onto the org runner queue - (run 35219385415 queued 3h+). The flag must sit on the job, ahead of - runs-on, so GitHub can complete the schedule run as skipped without a - runner. See docs/doctoring/coalesce-tick-inert-runner-queue-20260917.md. - """ - job = _job_block() - header = job.split("runs-on:", 1)[0] - assert "if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true'" in header - assert "if: vars.OPENCODE_REVIEW_COALESCE_ENABLED != 'true'" not in job - - -def test_tick_runs_every_five_minutes_and_never_carries_manual_dispatch(): - """workflow_dispatch: is a branch-selectable manual entrypoint; central - workflows must not carry it (test_no_central_workflow_exposes_branch_selected_manual_dispatch).""" - workflow = _workflow_text() - on_block = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0] - assert 'cron: "*/5 * * * *"' in on_block - assert "workflow_dispatch:" not in workflow - - -def test_tick_does_not_stack(): - """At most one tick runs; a slow tick is never cut off mid-dispatch.""" - workflow = _workflow_text() - concurrency_block = workflow.split("\nconcurrency:\n", 1)[1].split("\npermissions:\n", 1)[0] - assert "group: opencode-review-coalesce-tick" in concurrency_block - assert "cancel-in-progress: false" in concurrency_block - - -def test_tick_bounds_its_own_wall_clock(): - job = _job_block() - assert "timeout-minutes: 4" in job - - -def test_tick_enables_coalescing_for_its_own_invocations_only(): - """Only this workflow's env sets the flag; nothing else should.""" - job = _job_block() - assert 'OPENCODE_REVIEW_COALESCE_ENABLED: "true"' in job - - -def test_tick_scopes_each_repository_pass_to_review_dispatch_only(): - """This tick coalesces reviews; it must not merge or update branches.""" - dispatch_step = _workflow_text().split( - " - name: Dispatch a coalesced OpenCode review for each stabilized head\n", - 1, - )[1] - assert "--no-enable-auto-merge" in dispatch_step - assert "--no-update-branches" in dispatch_step - assert "--branch-update-limit 0" in dispatch_step - assert "--trigger-reviews" in dispatch_step - assert '--review-workflow "Required OpenCode Review"' in dispatch_step - - -def test_tick_reuses_the_existing_scheduler_cli_unmodified(): - """No parallel dispatch/dedup logic -- reuse the one, already-tested path.""" - dispatch_step = _workflow_text().split( - " - name: Dispatch a coalesced OpenCode review for each stabilized head\n", - 1, - )[1] - assert "python3 scripts/ci/pr_review_merge_scheduler.py" in dispatch_step - assert "/dispatches" not in dispatch_step - - -def test_tick_searches_the_whole_organization_not_one_repository(): - workflow = _workflow_text() - assert "org:ContextualWisdomLab is:pr is:open draft:false" in workflow - assert "search(query:" in workflow - - -def test_tick_permissions_match_the_existing_scheduler_scan_job(): - """Same permission shape scan-pr-queue already carries for this same call path.""" - job = _job_block() - job_permissions = job.split(" permissions:\n", 1)[1].split("\n env:", 1)[0] - for line in ( - "contents: write", - "actions: write", - "pull-requests: write", - "id-token: write", - ): - assert line in job_permissions diff --git a/tests/test_opencode_review_surfaces.py b/tests/test_opencode_review_surfaces.py index 6089a634f9..858ca513b0 100644 --- a/tests/test_opencode_review_surfaces.py +++ b/tests/test_opencode_review_surfaces.py @@ -8,7 +8,6 @@ import pytest -from scripts.ci import opencode_review_receipt_gate as receipt_gate from scripts.ci import opencode_review_surfaces as surfaces ROOT = Path(__file__).resolve().parents[1] @@ -800,46 +799,3 @@ def test_publisher_workflow_cannot_replace_review_with_coverage_finding( model_skip = workflow.split("if [ \"$opencode_review_outcome\" != \"success\" ]; then", 1)[1] model_skip = model_skip.split("selected_review_output_file=", 1)[0] assert "publish_fallback_diff_review" in model_skip - - -def test_coverage_fallback_review_is_formal_not_comment() -> None: - """#1907: a COMMENT-only fallback can never satisfy the receipt gate, so the - required workflow's rerun-on-verdict path (opencode-review-dispatch.yml's - "Wake exact-head required OpenCode workflow" step) never fires and the - required opencode-review check fails closed forever. The fallback event - must be a formal state (REQUEST_CHANGES), matching the surrounding intent - comment: "so a miss never looks finished; next action stays 'fix coverage - evidence, then rerun'". - """ - workflow = (ROOT / ".github/workflows/opencode-review-dispatch.yml").read_text( - encoding="utf-8" - ) - fallback_fn = workflow.split("publish_fallback_diff_review() {", 1)[1] - fallback_fn = fallback_fn.split("\n }\n", 1)[0] - assert 'event="COMMENT"' not in fallback_fn - assert 'event="REQUEST_CHANGES"' in fallback_fn - - -def test_coverage_fallback_review_body_is_a_formal_receipt() -> None: - """The fallback body actually produced by build-fallback-review must be - accepted by the receipt gate once it is published as a formal event, so - the required workflow can observe a current-head verdict and stop - fail-closing indefinitely. - """ - body = surfaces.build_fallback_review( - changed_files=["python/fast_mlsirm/estimators/marginal.py"], - head_sha=HEAD, - run_id="1", - run_attempt="1", - coverage_result="failure", - ) - body += "\n## Review outcome\n\nCoverage is a gate, not the review. This body reviews the changed product files.\n" - review = { - "id": 1, - "user": {"login": "opencode-agent"}, - "commit_id": HEAD, - "state": "CHANGES_REQUESTED", - "body": body, - } - receipt, reason = receipt_gate.evaluate_receipts([review], HEAD, is_draft=False) - assert receipt is not None, reason diff --git a/tests/test_opencode_vcs_python_source_root_contract.py b/tests/test_opencode_vcs_python_source_root_contract.py deleted file mode 100644 index 86c6c9d8f3..0000000000 --- a/tests/test_opencode_vcs_python_source_root_contract.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Contract: trusted coverage image VCS import-root resolution admits python/. - -#2157: the coverage tool image aborted at docker step #17 because the inline -materializer only accepted root/`src/` layouts, while immutable -`fast-mlsirm@09f762ded` exposes `fast_mlsirm` under `python/`. #2123 admitted -those candidates on `main`; this contract keeps the resolver as an executable -helper the Dockerfile COPYs, and proves the image-path algorithm offline -against fixtures (including the live fast-mlsirm layout shape). -""" - -from __future__ import annotations - -import subprocess -from pathlib import Path - -import pytest - -_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] -_HELPER = ( - _REPOSITORY_ROOT / "scripts/ci/resolve_opencode_base_vcs_import_root.sh" -) -_DISPATCH = ( - _REPOSITORY_ROOT / ".github/workflows/opencode-review-dispatch.yml" -) - - -def _run_resolver( - destination: Path, import_name: str, repository: str = "fixture-pkg" -) -> subprocess.CompletedProcess[str]: - """Invoke the trusted VCS import-root resolver against a fixture tree.""" - - return subprocess.run( - [str(_HELPER), str(destination), import_name, repository], - capture_output=True, - text=True, - check=False, - ) - - -def test_helper_is_executable_and_wired_into_coverage_image_build() -> None: - """The Dockerfile must COPY and execute the reviewed helper, not inline drift.""" - - assert _HELPER.is_file() - assert _HELPER.stat().st_mode & 0o111 - workflow = _DISPATCH.read_text(encoding="utf-8") - assert "resolve_opencode_base_vcs_import_root.sh" in workflow - assert ( - "COPY resolve-opencode-base-vcs-import-root.sh" - " /usr/local/libexec/resolve-opencode-base-vcs-import-root.sh" - ) in workflow - assert 'python_root="$("$resolver" "$destination" "$import_name" "$repository")"' in workflow - assert 'install -m 0755 "$trusted_vcs_import_root_resolver"' in workflow - # Candidate layouts live in the helper; the Dockerfile must not re-inline them. - assert 'candidate_count=$((candidate_count + 1))' not in workflow - helper = _HELPER.read_text(encoding="utf-8") - assert '"$destination/python/$import_name"' in helper - assert '"$destination/python/$import_name.py"' in helper - assert "has a missing or ambiguous import root" in helper - - -def test_python_source_root_package_layout_resolves(tmp_path: Path) -> None: - """A maturin-style ``python//__init__.py`` tree maps to ``python/``.""" - - package = tmp_path / "python" / "fast_mlsirm" - package.mkdir(parents=True) - (package / "__init__.py").write_text('"""fixture"""\n', encoding="utf-8") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 0, result.stderr - assert result.stdout.strip() == str(tmp_path / "python") - - -def test_python_source_root_single_module_resolves(tmp_path: Path) -> None: - """A single-module ``python/.py`` is also a valid conventional root.""" - - python_root = tmp_path / "python" - python_root.mkdir() - (python_root / "fast_mlsirm.py").write_text("VALUE = 1\n", encoding="utf-8") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 0, result.stderr - assert result.stdout.strip() == str(python_root) - - -def test_src_layout_still_resolves(tmp_path: Path) -> None: - """Pre-existing ``src/`` layouts remain admitted.""" - - package = tmp_path / "src" / "demo_pkg" - package.mkdir(parents=True) - (package / "__init__.py").write_text('"""fixture"""\n', encoding="utf-8") - result = _run_resolver(tmp_path, "demo_pkg", "demo-pkg") - assert result.returncode == 0, result.stderr - assert result.stdout.strip() == str(tmp_path / "src") - - -def test_missing_import_root_fails_closed(tmp_path: Path) -> None: - """No conventional candidate must keep failing the image build, not defer.""" - - (tmp_path / "README.md").write_text("empty\n", encoding="utf-8") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 1 - assert ( - "locked VCS source fast-mlsirm has a missing or ambiguous import root" - " for fast_mlsirm" in result.stderr - ) - - -def test_ambiguous_python_and_src_roots_fail_closed(tmp_path: Path) -> None: - """Exactly one candidate may exist; python/ + src/ is still fatal.""" - - for root in ("python", "src"): - package = tmp_path / root / "fast_mlsirm" - package.mkdir(parents=True) - (package / "__init__.py").write_text('"""fixture"""\n', encoding="utf-8") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 1 - assert "missing or ambiguous import root" in result.stderr - - -def test_namespace_package_without_init_fails_closed(tmp_path: Path) -> None: - """A directory without ``__init__.py`` is rejected as a namespace root.""" - - package = tmp_path / "python" / "fast_mlsirm" - package.mkdir(parents=True) - (package / "mod.py").write_text("VALUE = 1\n", encoding="utf-8") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 1 - assert "namespace or linked import root" in result.stderr - - -def test_compiled_extension_in_checkout_fails_closed(tmp_path: Path) -> None: - """Checked-in compiled artifacts must not enter the trusted .pth path.""" - - package = tmp_path / "python" / "fast_mlsirm" - package.mkdir(parents=True) - (package / "__init__.py").write_text('"""fixture"""\n', encoding="utf-8") - (package / "_core.so").write_bytes(b"\x00") - result = _run_resolver(tmp_path, "fast_mlsirm", "fast-mlsirm") - assert result.returncode == 1 - assert "contains a compiled extension" in result.stderr - - -@pytest.mark.parametrize( - ("layout", "import_file"), - [ - ("root-package", "pkg"), - ("root-module", "pkg.py"), - ], -) -def test_repository_root_layouts_still_resolve( - tmp_path: Path, layout: str, import_file: str -) -> None: - """Root-level package and module layouts remain valid one-candidate roots.""" - - del layout # parametrize label only - if import_file.endswith(".py"): - (tmp_path / import_file).write_text("VALUE = 1\n", encoding="utf-8") - import_name = import_file[: -len(".py")] - else: - package = tmp_path / import_file - package.mkdir() - (package / "__init__.py").write_text('"""fixture"""\n', encoding="utf-8") - import_name = import_file - result = _run_resolver(tmp_path, import_name, "root-layout") - assert result.returncode == 0, result.stderr - assert result.stdout.strip() == str(tmp_path) diff --git a/tests/test_organization_commercial_readiness_loop_receipt_contract.py b/tests/test_organization_commercial_readiness_loop_receipt_contract.py index 6ae9dfa595..ce0956bba5 100644 --- a/tests/test_organization_commercial_readiness_loop_receipt_contract.py +++ b/tests/test_organization_commercial_readiness_loop_receipt_contract.py @@ -1,4 +1,3 @@ -import re from pathlib import Path from organization_commercial_readiness_fixtures import manual_workflow, workflow @@ -15,21 +14,6 @@ ) -def _harden_runner_allowed_endpoints(source: str) -> set[str]: - """Return the harden-runner allowlist entries without substring URL heuristics.""" - match = re.search( - r"(?m)^(?P[ \t]+)allowed-endpoints:[ \t]*>-[ \t]*\n" - r"(?P(?:(?P=indent) \S[^\n]*(?:\n|$))*)", - source, - ) - assert match is not None, "expected harden-runner allowed-endpoints block" - return { - line.strip() - for line in match.group("endpoints").splitlines() - if line.strip() - } - - def test_product_entrypoint_rejects_missing_model_key_or_manual_trigger() -> None: """Both the NVIDIA model boundary and manual opt-in trigger are mandatory.""" safe = manual_workflow() @@ -56,8 +40,6 @@ def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None assert "path: ${{ runner.temp }}/organization-commercial-readiness-loop.json" in source assert "if-no-files-found: error" in source assert "retention-days: 3" in source - endpoints = _harden_runner_allowed_endpoints(source) - assert "results-receiver.actions.githubusercontent.com:443" in endpoints - assert "*.actions.githubusercontent.com:443" in endpoints - assert "*.blob.core.windows.net:443" in endpoints - assert "- name: Checkout exact trusted coordinator source" not in endpoints + assert "results-receiver.actions.githubusercontent.com:443" in source + assert "*.actions.githubusercontent.com:443" in source + assert "*.blob.core.windows.net:443" in source diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index c393972cd5..c5d4e9d7a3 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -460,241 +460,6 @@ def opener(url: str, _token: str) -> object: ) -def _declaration_url_fragment(base_ref: str) -> str: - """Return the substring identifying the declaration-fetch request URL.""" - return f"/contents/{policy.ARTIFACT_PATH_DECLARATION_PATH}?ref={base_ref}" - - -def test_declared_prefix_from_base_ref_admits_a_real_binary_artifact(capsys: pytest.CaptureFixture[str]) -> None: - """A base-ref-declared prefix admits a genuine non-UTF-8 research artifact. - - ``local/model.npz`` has no ``BINARY_DOCUMENT_MAGIC`` entry, so admission - depends entirely on the declared prefix plus the "no patch + not valid - UTF-8" evidence -- the option (a) suffix decision from issue #2193. - """ - - artifact_bytes = b"\x93NUMPY\x01\x00\xff\xfe\x00\x01\x02\x80\x81\x82\xf0\x0f" - with pytest.raises(UnicodeDecodeError): - artifact_bytes.decode("utf-8") - - def opener(url: str, _token: str) -> object: - if "/pulls/2193/files" in url: - return [{"filename": "local/model.npz", "status": "added"}] - if _declaration_url_fragment("main") in url: - return encoded_file("\nlocal\n\n") - assert "/contents/local/model.npz" in url - return { - "type": "file", "encoding": "base64", "size": len(artifact_bytes), - "content": base64.b64encode(artifact_bytes).decode("ascii"), - } - - result = policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2193, - head_sha="a" * 40, - event_action="opened", - token="token", - base_ref="main", - opener=opener, - ) - assert result == () - notice = capsys.readouterr().out - assert "declared prefix 'local'" in notice - assert "base ref 'main'" in notice - assert "local/model.npz" in notice - - -def test_same_pr_self_authorization_is_refused() -> None: - """A declaration added only at the PR head grants no admission. - - The declaration is resolved *only* from ``base_ref``; when it is absent - there (the same PR adds the declaration and the binary together), the - artifact is scanned exactly as if no declaration existed anywhere, and a - genuinely non-UTF-8 file with no diff patch fails closed the same way - any other unrecognized binary format does. - """ - - artifact_bytes = b"\x93NUMPY\x01\x00\xff\xfe\x00\x01\x02\x80\x81\x82\xf0\x0f" - - def opener(url: str, _token: str) -> object: - if "/pulls/2194/files" in url: - return [{"filename": "local/model.npz", "status": "added"}] - if _declaration_url_fragment("main") in url: - raise policy.ArtifactDeclarationNotFoundError("no declaration at base ref") - assert "/contents/local/model.npz" in url - return { - "type": "file", "encoding": "base64", "size": len(artifact_bytes), - "content": base64.b64encode(artifact_bytes).decode("ascii"), - } - - with pytest.raises(policy.PolicyError, match="not valid UTF-8"): - policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2194, - head_sha="b" * 40, - event_action="opened", - token="token", - base_ref="main", - opener=opener, - ) - - -def test_runtime_form_under_declared_prefix_is_still_rejected() -> None: - """A declared prefix cannot launder an active Nginx runtime artifact.""" - - def opener(url: str, _token: str) -> object: - if "/pulls/2195/files" in url: - return [{"filename": "local/nginx.conf", "status": "added", "patch": "+listen 80;"}] - if _declaration_url_fragment("main") in url: - return encoded_file("local\n") - assert "/contents/local/nginx.conf" in url - return encoded_file("server { listen 80; }\n") - - result = policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2195, - head_sha="c" * 40, - event_action="opened", - token="token", - base_ref="main", - opener=opener, - ) - assert [item.rule for item in result] == ["nginx_runtime_artifact"] - - -def test_valid_utf8_file_under_declared_prefix_is_still_scanned() -> None: - """A declared prefix never admits a file that decodes as valid UTF-8. - - Without a diff patch, this would otherwise look like the exact binary - pre-filter shape (`patch_available=False`); the strict UTF-8 complement - in `_binary_documentation_evidence_confirms` refuses to trust it, so it - falls through to the ordinary scan and still gets flagged. - """ - - def opener(url: str, _token: str) -> object: - if "/pulls/2196/files" in url: - return [{"filename": "local/notes.dat", "status": "added"}] - if _declaration_url_fragment("main") in url: - return encoded_file("local\n") - assert "/contents/local/notes.dat" in url - return encoded_file("cat /etc/nginx/nginx.conf\n") - - result = policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2196, - head_sha="d" * 40, - event_action="opened", - token="token", - base_ref="main", - opener=opener, - ) - assert [item.rule for item in result] == ["nginx_runtime_path"] - - -@pytest.mark.parametrize( - ("declaration_text", "message"), - [ - ("/etc/passwd\n", "must be a relative path prefix"), - ("local/../etc\n", "malformed"), - ("..\n", "malformed"), - (".\n", "must be a relative path prefix"), - ("/\n", "must be a relative path prefix"), - ("data/*.npz\n", "glob"), - ("\n".join(f"path-{index}" for index in range(policy.MAX_DECLARED_ARTIFACT_PREFIXES + 1)), "exceeds 64 entries"), - ("a/" * (policy.MAX_DECLARED_ARTIFACT_PREFIX_DEPTH + 1) + "b\n", "exceeds depth 8"), - ], -) -def test_malformed_declaration_raises_naming_the_offending_entry(declaration_text: str, message: str) -> None: - """Every malformed declaration shape is a hard PolicyError, never silent.""" - - with pytest.raises(policy.PolicyError, match=message): - policy._parse_artifact_path_declaration(declaration_text) - - -def test_no_declaration_file_present_is_a_regression_guard() -> None: - """A repository with no declaration file behaves identically to today.""" - - def opener(url: str, _token: str) -> object: - if "/pulls/2197/files" in url: - return [{"filename": "docker-compose.yml", "status": "modified", "patch": "+image: nginx"}] - if _declaration_url_fragment("main") in url: - raise policy.ArtifactDeclarationNotFoundError("no declaration file in this repository") - return encoded_file("services:\n edge:\n image: nginx:1.27-alpine\n") - - result = policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2197, - head_sha="e" * 40, - event_action="opened", - token="token", - base_ref="main", - opener=opener, - ) - assert [item.rule for item in result] == ["nginx_container_image"] - - -def test_omitting_base_ref_never_fetches_a_declaration() -> None: - """The default (no ``base_ref``) reproduces this module's exact prior behavior.""" - - def opener(url: str, _token: str) -> object: - if "/pulls/2198/files" in url: - return [{"filename": "docker-compose.yml", "status": "modified", "patch": "+image: nginx"}] - assert "edge-policy-artifact-paths" not in url - return encoded_file("services:\n edge:\n image: nginx:1.27-alpine\n") - - result = policy.evaluate_pull_request( - api_url="https://api.github.test", - repository="ContextualWisdomLab/example", - pull_request=2198, - head_sha="f" * 40, - event_action="opened", - token="token", - opener=opener, - ) - assert [item.rule for item in result] == ["nginx_container_image"] - - -def test_evaluate_pull_request_rejects_malformed_base_ref() -> None: - """A malformed base ref fails before any network access.""" - - with pytest.raises(policy.PolicyError, match="base ref"): - policy.evaluate_pull_request( - api_url="x", - repository="a/b", - pull_request=1, - head_sha="a" * 40, - event_action="opened", - token="x", - base_ref="../etc/passwd", - opener=lambda _url, _token: pytest.fail("must not open"), - ) - - -def test_declared_prefix_for_path_matches_by_path_segment() -> None: - """A declared prefix matches whole path segments, not a raw string prefix.""" - - assert policy._declared_prefix_for_path("local/model.npz", ("local",)) == "local" - assert policy._declared_prefix_for_path("local-cache/model.npz", ("local",)) is None - assert policy._declared_prefix_for_path("evidence/raw/data.sav", ("evidence/raw",)) == "evidence/raw" - assert policy._declared_prefix_for_path("evidence/other.sav", ("evidence/raw",)) is None - - -def test_github_open_json_maps_not_found_to_artifact_declaration_error(monkeypatch: pytest.MonkeyPatch) -> None: - """A 404 from the GitHub API is distinguished from every other transport failure.""" - - monkeypatch.setattr( - policy.github_opener, "open", - lambda _request, timeout: (_ for _ in ()).throw(HTTPError("x", 404, "not found", {}, BytesIO())), - ) - with pytest.raises(policy.ArtifactDeclarationNotFoundError): - policy._github_open_json("https://api.github.com/repos/a/b", "token") - - def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: """Every malformed PNG boundary returns false without parsing past bounds.""" diff --git a/tests/test_pingora_edge_workflow_contract.py b/tests/test_pingora_edge_workflow_contract.py index 2267a45970..ad0667cc6b 100644 --- a/tests/test_pingora_edge_workflow_contract.py +++ b/tests/test_pingora_edge_workflow_contract.py @@ -45,9 +45,3 @@ def test_required_workflow_enforces_pingora_without_executing_pr_content() -> No assert text.index("Verify immutable central policy source") < text.index( "Enforce Cloudflare Pingora edge policy" ) - - # issue #2193: the research/data artifact path declaration must be - # resolved only from the base ref the pull_request_target event already - # carries, never from the untrusted PR head. - assert "PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}" in text - assert "--base-ref" in text diff --git a/tests/test_pingora_hwpx_evidence.py b/tests/test_pingora_hwpx_evidence.py deleted file mode 100644 index 5f7f65985f..0000000000 --- a/tests/test_pingora_hwpx_evidence.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Exercise HWPX admission through the production policy boundary offline.""" -import base64 -import io -import zipfile - -import pytest -from tests.test_pingora_edge_policy import policy - -# This repository's own pull requests are scanned by the policy under test, and -# only tests/test_pingora_edge_policy.py is path-exempt. Keep the denied runtime -# form split in source so the fixture exists at runtime, never in the diff. -RUNTIME_TEXT = "cat /etc/" + "nginx/nginx.conf\n" -RUNTIME_BYTES = RUNTIME_TEXT.encode("ascii") - - -def hwpx_archive(*, mime_value=b"application/hwp+zip", manifest_value=b"", compression_type=zipfile.ZIP_STORED): - """Create a deterministic, non-sensitive format-boundary fixture.""" - archive_buffer = io.BytesIO() - with zipfile.ZipFile(archive_buffer, "w") as archive_file: - mime_info = zipfile.ZipInfo("mimetype") - mime_info.compress_type = compression_type - archive_file.writestr(mime_info, mime_value) - if manifest_value is not None: - archive_file.writestr(zipfile.ZipInfo("Contents/content.hpf"), manifest_value) - return archive_buffer.getvalue() - - -def evaluate_bytes(file_path, file_bytes, *, patch_value=None, file_status="added"): - """Supply only in-memory GitHub metadata and exact-head content.""" - def open_evidence(request_url, request_token): - assert request_token == "offline-fixture" - if "/files?" in request_url: - file_entry = {"filename": file_path, "status": file_status} - if patch_value is not None: - file_entry["patch"] = patch_value - return [file_entry] - assert "?ref=" + "a" * 40 in request_url - return {"type": "file", "encoding": "base64", "size": len(file_bytes), - "content": base64.b64encode(file_bytes).decode("ascii")} - return policy.evaluate_pull_request(api_url="https://api.github.com", - repository="ContextualWisdomLab/example", pull_request=2116, head_sha="a" * 40, - event_action="opened", token="offline-fixture", opener=open_evidence) - - -@pytest.mark.parametrize("file_path", ["evidence/reviewer_response_draft.hwpx", "docs/paper.hwpx", "Evidence/PAPER.HWPX"]) -def test_valid_hwpx_is_admitted_at_document_and_consumer_paths(file_path): - """Recognize HWPX at the exact consumer directory, without renaming it.""" - assert evaluate_bytes(file_path, hwpx_archive()) == () - - -@pytest.mark.parametrize("file_bytes", [ - b"PK\x03\x04\xff", hwpx_archive()[:-10], - b"#!/bin/sh\n" + RUNTIME_BYTES + hwpx_archive(), - hwpx_archive() + b"\n" + RUNTIME_BYTES, - hwpx_archive(mime_value=b"application/zip"), - hwpx_archive(manifest_value=None), hwpx_archive(manifest_value=b""), - hwpx_archive(compression_type=zipfile.ZIP_DEFLATED), -]) -def test_malformed_or_disguised_archive_is_not_exempt(file_bytes): - """Unsupported format evidence fails closed instead of bypassing scanning.""" - with pytest.raises(policy.PolicyError): - evaluate_bytes("evidence/reviewer_response_draft.hwpx", file_bytes) - - -@pytest.mark.parametrize("file_path", ["evidence/paper.hwpx", "docs/paper.hwpx", "scripts/paper.hwpx"]) -@pytest.mark.parametrize("patch_value", [None, "+" + RUNTIME_TEXT.rstrip("\n")]) -def test_text_renamed_to_hwpx_preserves_runtime_scan(file_path, patch_value): - """Neither the suffix nor patch absence hides actual Nginx runtime text.""" - violations = evaluate_bytes(file_path, RUNTIME_BYTES, patch_value=patch_value) - assert [violation.rule for violation in violations] == ["nginx_runtime_path"] - - -def test_hwpx_does_not_expand_text_document_exemptions(): - """The consumer evidence directory does not exempt ordinary configuration.""" - violations = evaluate_bytes("evidence/config.txt", RUNTIME_BYTES) - assert [violation.rule for violation in violations] == ["nginx_runtime_path"] - - -def test_hwpx_in_runtime_path_remains_unavailable(): - """A format exception cannot exempt an active runtime location.""" - with pytest.raises(policy.PolicyError): - evaluate_bytes("docs/nginx/paper.hwpx", hwpx_archive()) - - -def test_removed_hwpx_does_not_load_deleted_content(): - """Deletion does not require unavailable final-head bytes.""" - assert evaluate_bytes("evidence/paper.hwpx", b"", file_status="removed") == () - - -def test_hwpx_rejects_inconsistent_comment_and_shifted_zip(): - """EOCD declarations and member offsets must bind to the actual bytes.""" - archive_bytes = hwpx_archive() - invalid_comment = archive_bytes[:-2] + b"\x01\x00" - for file_bytes in (invalid_comment, b"PK\x03\x04" + archive_bytes): - with pytest.raises(policy.PolicyError): - evaluate_bytes("evidence/paper.hwpx", file_bytes) - - -def test_hwpx_rejects_nonfirst_marker_and_encrypted_manifest(): - """A named marker alone cannot admit a reordered or encrypted package.""" - archive_buffer = io.BytesIO() - with zipfile.ZipFile(archive_buffer, "w") as archive_file: - archive_file.writestr(zipfile.ZipInfo("Contents/content.hpf"), b"") - archive_file.writestr(zipfile.ZipInfo("mimetype"), b"application/hwp+zip") - encrypted_bytes = bytearray(hwpx_archive()) - manifest_header = encrypted_bytes.rfind(b"PK\x01\x02") - encrypted_bytes[manifest_header + 8] |= 1 - for file_bytes in (archive_buffer.getvalue(), bytes(encrypted_bytes)): - with pytest.raises(policy.PolicyError): - evaluate_bytes("evidence/paper.hwpx", file_bytes) diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 8b7c55a4ef..2e733ac9e9 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 = "cbc8d214394c4b7acbe82ce7fba11fd073b91c98" +REVIEW_DISPATCH_BLOB_SHA = "d86497b3f43bebbabbb4f504eb5132cdf3b7b293" 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 4b8715d361..ba47b89c8d 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2302,254 +2302,6 @@ def fake_active_workflow_runs(repo, statuses, *, event=None, created=None, head_ assert sched.discover_opencode_required_run_id("owner/repo", head_sha) == 802 -def test_head_stable_for_seconds_reads_the_head_commit_timestamp(): - """The coalescing age check reads the fetched head commit, not wall time.""" - now = datetime(2026, 6, 25, 7, 5, 0, tzinfo=timezone.utc) - pr = make_pr( - commits={"nodes": [{"commit": {"oid": "head", "committedDate": "2026-06-25T07:00:00Z"}}]} - ) - assert sched.head_stable_for_seconds(pr, now=now) == 300.0 - - -def test_head_stable_for_seconds_fails_open_on_missing_data(): - """A missing or unparseable commit timestamp must never gate a dispatch.""" - assert sched.head_stable_for_seconds(make_pr(commits={"nodes": []})) is None - assert ( - sched.head_stable_for_seconds( - make_pr(commits={"nodes": [{"commit": {"oid": "head", "committedDate": None}}]}) - ) - is None - ) - - -def test_coalesce_enabled_defaults_off(monkeypatch): - """Every existing caller keeps immediate dispatch unless explicitly opted in.""" - monkeypatch.delenv("OPENCODE_REVIEW_COALESCE_ENABLED", raising=False) - assert sched.coalesce_enabled() is False - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_ENABLED", "false") - assert sched.coalesce_enabled() is False - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_ENABLED", "true") - assert sched.coalesce_enabled() is True - - -def test_coalesce_window_seconds_defaults_and_parses(monkeypatch): - monkeypatch.delenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", raising=False) - assert sched.coalesce_window_seconds() == 300 - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "120") - assert sched.coalesce_window_seconds() == 120 - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "not-a-number") - assert sched.coalesce_window_seconds() == 300 - - -def test_coalesce_tick_max_age_seconds_defaults_and_parses(monkeypatch): - monkeypatch.delenv("OPENCODE_REVIEW_COALESCE_TICK_MAX_AGE_SECONDS", raising=False) - assert sched.coalesce_tick_max_age_seconds() == 600 - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_TICK_MAX_AGE_SECONDS", "900") - assert sched.coalesce_tick_max_age_seconds() == 900 - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_TICK_MAX_AGE_SECONDS", "not-a-number") - assert sched.coalesce_tick_max_age_seconds() == 600 - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_TICK_MAX_AGE_SECONDS", "-1") - assert sched.coalesce_tick_max_age_seconds() == 600 - - -def test_recent_coalesce_tick_completed_matches_completed_schedule_runs(monkeypatch): - now = datetime(2026, 9, 17, 12, 0, tzinfo=timezone.utc) - monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "ContextualWisdomLab/.github") - - def fake_active_workflow_runs(repo, statuses, *, event=None, created=None, head_sha=None): - assert repo == "ContextualWisdomLab/.github" - assert statuses == ("completed",) - assert event == "schedule" - assert created == ">=2026-09-17T11:50:00Z" - return [ - { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", - "conclusion": "success", - "updated_at": "2026-09-17T11:55:00Z", - }, - { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", - "conclusion": "success", - "updated_at": "2026-09-17T11:40:00Z", - }, - { - "path": ".github/workflows/other.yml", - "conclusion": "success", - "updated_at": "2026-09-17T11:59:00Z", - }, - ] - - monkeypatch.setattr(sched, "active_workflow_runs", fake_active_workflow_runs) - assert sched.recent_coalesce_tick_completed( - "owner/repo", now=now, max_age_seconds=600 - ) - - -def test_recent_coalesce_tick_completed_ignores_skipped_and_cancelled_ticks(monkeypatch): - """Disabled-era skipped ticks must not count as healthy coalesce evidence.""" - now = datetime(2026, 9, 17, 12, 0, tzinfo=timezone.utc) - monkeypatch.setattr( - sched, - "active_workflow_runs", - lambda *a, **k: [ - { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", - "conclusion": "skipped", - "updated_at": "2026-09-17T11:55:00Z", - }, - { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", - "conclusion": "cancelled", - "updated_at": "2026-09-17T11:58:00Z", - }, - ], - ) - assert not sched.recent_coalesce_tick_completed( - "owner/repo", now=now, max_age_seconds=600 - ) - - -def test_recent_coalesce_tick_completed_returns_false_without_fresh_tick(monkeypatch): - now = datetime(2026, 9, 17, 12, 0, tzinfo=timezone.utc) - monkeypatch.setattr( - sched, - "active_workflow_runs", - lambda *a, **k: [ - { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", - "conclusion": "success", - "updated_at": "2026-09-17T11:00:00Z", - } - ], - ) - assert not sched.recent_coalesce_tick_completed( - "owner/repo", now=now, max_age_seconds=600 - ) - - -def test_recent_coalesce_tick_completed_treats_non_positive_max_age_as_stale(monkeypatch): - monkeypatch.setattr( - sched, - "active_workflow_runs", - lambda *a, **k: pytest.fail("must not query workflow runs when max age is zero"), - ) - assert not sched.recent_coalesce_tick_completed("owner/repo", max_age_seconds=0) - - -def _committed_seconds_ago(seconds: float) -> str: - """Return an ISO8601 timestamp `seconds` in the past, for coalescing tests.""" - from datetime import timedelta - - return (datetime.now(timezone.utc) - timedelta(seconds=seconds)).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - - -def test_dispatch_opencode_review_ignores_coalescing_when_disabled(monkeypatch): - """Flag-off path: a fresh head dispatches immediately, exactly as today.""" - monkeypatch.delenv("OPENCODE_REVIEW_COALESCE_ENABLED", raising=False) - monkeypatch.setenv("GITHUB_ACTIONS", "true") - monkeypatch.setenv("GH_TOKEN", "opencode-app-token") - monkeypatch.setattr(sched, "active_opencode_run_refs", lambda repo, workflow, pr: ([], [])) - monkeypatch.setattr(sched, "_cancel_revalidated_review_run_refs", lambda *a: ([], [])) - monkeypatch.setattr(sched, "review_dispatch_admitted", lambda *a: True) - monkeypatch.setattr(sched, "live_dispatch_head_matches", lambda *a: True) - monkeypatch.setattr(sched, "complete_paginated_pr_contexts", lambda *a: None) - monkeypatch.setattr(sched, "matching_actions_run_id", lambda *a: None) - monkeypatch.setattr(sched, "discover_opencode_required_run_id", lambda *a: None) - monkeypatch.setattr(sched, "reset_active_workflow_runs_cache", lambda: None) - monkeypatch.setattr(sched, "run_github_dispatch", lambda *a, **k: None) - - pr = make_pr( - headRefOid="a" * 40, - baseRefOid="b" * 40, - commits={"nodes": [{"commit": {"oid": "a" * 40, "committedDate": _committed_seconds_ago(5)}}]}, - ) - result = sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) - assert result == "dispatched" - - -def test_dispatch_opencode_review_coalesces_a_fresh_head_when_enabled(monkeypatch): - """Flag-on path: a head inside the settling window is deferred, not dispatched.""" - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_ENABLED", "true") - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "300") - monkeypatch.setenv("GITHUB_ACTIONS", "true") - monkeypatch.setenv("GH_TOKEN", "opencode-app-token") - called = [] - monkeypatch.setattr( - sched, "active_opencode_run_refs", lambda *a: called.append("active_opencode_run_refs") or ([], []) - ) - monkeypatch.setattr(sched, "recent_coalesce_tick_completed", lambda *a, **k: True) - - pr = make_pr( - headRefOid="a" * 40, - baseRefOid="b" * 40, - commits={"nodes": [{"commit": {"oid": "a" * 40, "committedDate": _committed_seconds_ago(60)}}]}, - ) - - result = sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) - # No live API call should happen once the coalescing gate defers -- the - # whole point is to avoid spending capacity on a head about to be - # superseded. - assert called == [] - assert result == "coalescing" - - -def test_dispatch_opencode_review_fail_opens_when_coalesce_tick_is_stale(monkeypatch): - """A fresh head still dispatches when the org tick has not completed recently.""" - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_ENABLED", "true") - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "300") - monkeypatch.setenv("GITHUB_ACTIONS", "true") - monkeypatch.setenv("GH_TOKEN", "opencode-app-token") - monkeypatch.setattr(sched, "recent_coalesce_tick_completed", lambda *a, **k: False) - monkeypatch.setattr(sched, "active_opencode_run_refs", lambda repo, workflow, pr: ([], [])) - monkeypatch.setattr(sched, "_cancel_revalidated_review_run_refs", lambda *a: ([], [])) - monkeypatch.setattr(sched, "review_dispatch_admitted", lambda *a: True) - monkeypatch.setattr(sched, "live_dispatch_head_matches", lambda *a: True) - monkeypatch.setattr(sched, "complete_paginated_pr_contexts", lambda *a: None) - monkeypatch.setattr(sched, "matching_actions_run_id", lambda *a: None) - monkeypatch.setattr(sched, "discover_opencode_required_run_id", lambda *a: None) - monkeypatch.setattr(sched, "reset_active_workflow_runs_cache", lambda: None) - monkeypatch.setattr(sched, "run_github_dispatch", lambda *a, **k: None) - - pr = make_pr( - headRefOid="a" * 40, - baseRefOid="b" * 40, - commits={"nodes": [{"commit": {"oid": "a" * 40, "committedDate": _committed_seconds_ago(60)}}]}, - ) - result = sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) - assert result == "dispatched" - - -def test_dispatch_opencode_review_dispatches_a_stable_head_when_enabled(monkeypatch): - """Flag-on path: a head past the settling window dispatches normally.""" - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_ENABLED", "true") - monkeypatch.setenv("OPENCODE_REVIEW_COALESCE_WINDOW_SECONDS", "300") - monkeypatch.setenv("GITHUB_ACTIONS", "true") - monkeypatch.setenv("GH_TOKEN", "opencode-app-token") - monkeypatch.setattr(sched, "active_opencode_run_refs", lambda repo, workflow, pr: ([], [])) - monkeypatch.setattr(sched, "_cancel_revalidated_review_run_refs", lambda *a: ([], [])) - monkeypatch.setattr(sched, "review_dispatch_admitted", lambda *a: True) - monkeypatch.setattr(sched, "live_dispatch_head_matches", lambda *a: True) - monkeypatch.setattr(sched, "complete_paginated_pr_contexts", lambda *a: None) - monkeypatch.setattr(sched, "matching_actions_run_id", lambda *a: None) - monkeypatch.setattr(sched, "discover_opencode_required_run_id", lambda *a: None) - monkeypatch.setattr(sched, "reset_active_workflow_runs_cache", lambda: None) - monkeypatch.setattr(sched, "run_github_dispatch", lambda *a, **k: None) - - pr = make_pr( - headRefOid="a" * 40, - baseRefOid="b" * 40, - commits={ - "nodes": [ - {"commit": {"oid": "a" * 40, "committedDate": "2020-01-01T00:00:00Z"}} - ] - }, - ) - result = sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) - assert result == "dispatched" - - def test_dispatch_opencode_review_falls_back_to_bounded_discovery(monkeypatch): """Scheduler dispatch uses the bounded fallback only when the rollup misses.""" monkeypatch.setenv("GITHUB_ACTIONS", "true") @@ -5631,22 +5383,6 @@ def test_stacked_pr_waits_when_opencode_dispatch_is_already_active(monkeypatch): assert stacked.reason == "stacked PR onto develop; same-head OpenCode workflow run is already active" -def test_stacked_pr_waits_when_opencode_dispatch_is_coalescing(monkeypatch): - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - - stacked = inspect(make_pr(baseRefName="develop")) - - assert stacked.action == "wait" - assert ( - stacked.reason - == "stacked PR onto develop; current head is within the push-burst coalescing window" - ) - - def test_stacked_pr_waits_on_bounded_admission_budget(monkeypatch): monkeypatch.setattr( sched, @@ -7340,17 +7076,6 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): assert coverage_active.reason == ( "current-head coverage evidence is complete, but a same-head OpenCode workflow run is already active" ) - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - coverage_coalescing = inspect(coverage_request) - assert coverage_coalescing.action == "wait" - assert coverage_coalescing.reason == ( - "current-head coverage evidence is complete, but the current head is within the " - "push-burst coalescing window" - ) monkeypatch.setattr( sched, "dispatch_opencode_review", @@ -8079,22 +7804,6 @@ def test_draft_pr_review_only_dispatch_strix_missing_then_opencode_chain(): ) -def test_draft_pr_review_only_dispatch_waits_while_opencode_coalesces(monkeypatch): - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - strix_complete_draft = make_pr( - isDraft=True, statusCheckRollup={"contexts": {"nodes": [strix_check()]}} - ) - coalescing_decision = inspect(strix_complete_draft, allow_draft_review_dispatch=True) - assert coalescing_decision.action == "wait" - assert coalescing_decision.reason == ( - "draft PR review-only dispatch; current head is within the push-burst coalescing window" - ) - - def test_draft_pr_review_only_dispatch_treats_failed_strix_like_missing(): """A terminal but non-passing Strix conclusion on a draft review-only request must fail closed the same as missing evidence: a fresh Strix @@ -8651,18 +8360,6 @@ def followup(updated_pr, **overrides): ) ) - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - assert "current head is within the push-burst coalescing window" in followup( - make_pr( - headRefOid="newest-head", - statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, - ) - ) - def test_post_update_branch_followup_treats_failed_strix_like_missing(monkeypatch): """A terminal but non-passing Strix conclusion after a branch update must @@ -9132,17 +8829,6 @@ def test_inspect_pr_handles_approved_reviews_and_dispatch(monkeypatch): stale_already_active.reason == "OpenCode review exceeded the status-check retry threshold, but a same-head workflow run is already active" ) - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - stale_coalescing = inspect(stale_opencode, stale_opencode_minutes=0) - assert stale_coalescing.action == "wait" - assert stale_coalescing.reason == ( - "OpenCode review exceeded the status-check retry threshold, but the current head is within " - "the push-burst coalescing window" - ) monkeypatch.setattr( sched, "dispatch_opencode_review", @@ -9182,19 +8868,6 @@ def test_inspect_pr_handles_approved_reviews_and_dispatch(monkeypatch): completed_strix_already_active.reason == "current head has completed Strix evidence; same-head OpenCode workflow run is already active" ) - monkeypatch.setattr( - sched, - "dispatch_opencode_review", - lambda repo, workflow, pr, dry_run: "coalescing", - ) - completed_strix_coalescing = inspect( - make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check()]}}), - ) - assert completed_strix_coalescing.action == "wait" - assert completed_strix_coalescing.reason == ( - "current head has completed Strix evidence, but the current head is within the " - "push-burst coalescing window" - ) monkeypatch.setattr( sched, "dispatch_opencode_review", diff --git a/tests/test_product_technical_gap_baseline_repository_identity_contract.py b/tests/test_product_technical_gap_baseline_repository_identity_contract.py deleted file mode 100644 index 2acdc2657e..0000000000 --- a/tests/test_product_technical_gap_baseline_repository_identity_contract.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Regression contract for owner-qualified cross-repository evidence identities.""" - -from pathlib import Path -import unittest - - -BASELINE_PATH = ( - Path(__file__).resolve().parents[1] / "docs" / "product-technical-gap-baseline.md" -) - - -class ProductTechnicalGapBaselineRepositoryIdentityContractTests(unittest.TestCase): - """Keep durable cross-repository evidence unambiguous outside its owner repo.""" - - def test_control_opencode_evidence_uses_owner_qualified_repository_identities(self) -> None: - """Reject the two legacy bare repository tokens and require their durable forms.""" - baseline = BASELINE_PATH.read_text(encoding="utf-8") - - legacy_tokens = ( - "`contextual-orchestrator#1149@684cf28f`", - "`fast-mlsirm@09f762d`", - ) - durable_tokens = ( - "`ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`", - "`ContextualWisdomLab/fast-mlsirm@09f762d`", - ) - - for token in legacy_tokens: - with self.subTest(token=token): - self.assertNotIn(token, baseline) - - for token in durable_tokens: - with self.subTest(token=token): - self.assertIn(token, baseline) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_python_security_pip_audit_failure_classification.py b/tests/test_python_security_pip_audit_failure_classification.py deleted file mode 100644 index 9cf131ef8d..0000000000 --- a/tests/test_python_security_pip_audit_failure_classification.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Behaviour contract for the `python-security.yml` pip-audit hard gate. - -Issue #2158: the step folded every non-zero pip-audit exit into one message -that asserted "known-vulnerable Python dependencies", so a PyPI transport -failure (`ConnectionResetError` from the advisory query, no findings at all) -read like a security finding and misdirected triage. The gate must stay -closed on every failure, but the printed evidence has to say which of the -two things happened. pip-audit 2.10.1 (the pinned version) prints -`Found N known vulnerabilit(y|ies) ... in N package(s)` to stderr when it has -findings (`pip_audit/_cli.py`), so that line is the discriminator. - -The tests execute the real step body with a fake `pip-audit` on PATH, the -same technique as `test_workflow_file_detection_pipefail_regression.py`. -""" - -from __future__ import annotations - -import os -from pathlib import Path -import re -import stat -import subprocess - -REPO_ROOT = Path(__file__).resolve().parents[1] -WORKFLOW = REPO_ROOT / ".github/workflows/python-security.yml" -STEP_MARKER = " - name: Run pip-audit (hard gate on any known vulnerability)\n" - - -def _extract_pip_audit_script(workflow_text: str) -> str: - """Return the step's `run:` body with the YAML block indentation removed.""" - start = workflow_text.index(STEP_MARKER) - run_start = workflow_text.index(" run: |\n", start) + len(" run: |\n") - rest = workflow_text[run_start:] - # The body ends at the next step (` - name:`) or the next top-level - # job key (two spaces then a non-space); blank lines inside the script are - # followed by ten-space indentation and must not terminate it. - boundary = re.search(r"\n(?: - name:|\n \S)", rest) - block = rest[: boundary.start()] if boundary else rest - return "\n".join(line[10:] for line in block.splitlines()) - - -def _fake_pip_audit(bin_dir: Path, body: str) -> None: - """Install a `pip-audit` shim whose behaviour is the given shell body.""" - shim = bin_dir / "pip-audit" - shim.write_text("#!/usr/bin/env bash\n" + body + "\n", encoding="utf-8") - shim.chmod(shim.stat().st_mode | stat.S_IXUSR) - - -def _run_step(tmp_path: Path, shim_body: str) -> subprocess.CompletedProcess[str]: - """Run the extracted step in a repo holding one requirements file.""" - repo = tmp_path / "repo" - repo.mkdir() - (repo / "requirements-demo-ci.txt").write_text("requests==2.32.0\n", encoding="utf-8") - bin_dir = tmp_path / "bin" - bin_dir.mkdir() - _fake_pip_audit(bin_dir, shim_body) - script = _extract_pip_audit_script(WORKFLOW.read_text(encoding="utf-8")) - return subprocess.run( - ["bash", "-c", script], - cwd=repo, - capture_output=True, - text=True, - timeout=60, - env={**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}"}, - ) - - -def test_genuine_findings_fail_closed_and_are_reported_as_findings(tmp_path): - """A real advisory hit still fails the job and names the vulnerable input.""" - result = _run_step( - tmp_path, - 'echo "Found 2 known vulnerabilities in 1 package" >&2; exit 1', - ) - assert result.returncode == 1 - assert "::error::pip-audit found known-vulnerable Python dependencies in -r ./requirements-demo-ci.txt" in result.stdout - assert "could not complete" not in result.stdout - - -def test_transport_failure_fails_closed_but_is_not_called_a_vulnerability(tmp_path): - """The #2158 shape: no findings, then an unhandled PyPI connection error.""" - result = _run_step( - tmp_path, - 'echo "No known vulnerabilities found" >&2; ' - 'echo "Traceback (most recent call last):" >&2; ' - 'echo "ConnectionResetError: [Errno 104] Connection reset by peer" >&2; exit 1', - ) - assert result.returncode == 1 - assert "known-vulnerable" not in result.stdout - assert ( - "::error::pip-audit could not complete for -r ./requirements-demo-ci.txt: " - "ConnectionResetError: [Errno 104] Connection reset by peer" - ) in result.stdout - assert "not a vulnerability finding" in result.stdout - - -def test_clean_audit_passes_without_error_annotations(tmp_path): - """A clean run exits 0 and prints no `::error::` line.""" - result = _run_step(tmp_path, 'echo "No known vulnerabilities found" >&2; exit 0') - assert result.returncode == 0, result.stderr - assert "::error::" not in result.stdout - - -def test_step_no_longer_asserts_a_finding_for_every_failure(): - """The single catch-all message must be gone from the workflow text.""" - workflow = WORKFLOW.read_text(encoding="utf-8") - assert "::error::pip-audit reported known-vulnerable Python dependencies." not in workflow - assert "Found [0-9]+ known vulnerabilit" in workflow diff --git a/tests/test_required_security_runner_image_contract.py b/tests/test_required_security_runner_image_contract.py index d20c0c3a98..2b48f66251 100644 --- a/tests/test_required_security_runner_image_contract.py +++ b/tests/test_required_security_runner_image_contract.py @@ -28,15 +28,13 @@ def test_sast_semgrep_uses_explicit_supported_image(self) -> None: `#1656` removed the sibling `cancel-closed-pr-runs` no-op job (it only duplicated PR-stable workflow concurrency), leaving one runner - job in this workflow instead of two. It was 2, not 1, again after the + job in this workflow instead of two. It is 2, not 1, again after the `changed-scope` gate job was added to skip doc-only PR scope (org - ruleset 18156473 ignores trigger-level path filters). The count - returned to 1 when that `changed-scope` job was folded into the - `semgrep` job as a step-level guard (one consumer, one runner). + ruleset 18156473 ignores trigger-level path filters). """ workflow = SAST_SEMGREP.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 1) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2) if __name__ == "__main__": diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 87277d45f5..19fe6b0f7f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1674,8 +1674,8 @@ def test_security_scan_preserves_base_output_across_cross_fork_checkout() -> Non assert workflow.count("--allow-no-lockfiles") == 4 assert workflow.count("path: source") == 2 - assert workflow.count("--output-file=old-results.json") == 2 - assert workflow.count("--output-file=new-results.json") == 2 + assert workflow.count("--output=old-results.json") == 2 + assert workflow.count("--output=new-results.json") == 2 assert workflow.count("source/") == 4 assert "clean: false" not in workflow assert "test -s old-results.json" in workflow @@ -1732,47 +1732,12 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai "Retry head OSV without transitive resolution\n if: steps.osv_head.outcome == 'failure'\n continue-on-error: true" in workflow ) - assert "--output-file=old-results.json" in workflow - assert "--output-file=new-results.json" in workflow + assert "--output=old-results.json" in workflow + assert "--output=new-results.json" in workflow assert "Print OSV findings being compared" in workflow assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow -def test_osv_scan_uses_current_output_flags_and_binds_sarif_checkout_path() -> None: - """Drop deprecated OSV output flags and bind upload-sarif to the real checkout. - - Live evidence (ContextualWisdomLab/.github#2132): the pinned - `ghcr.io/google/osv-scanner-action:v2.5.1` image warns - `--output has been deprecated in favor of --output-file` (scanner) and - `... in favor of --output-files` (reporter), and `upload-sarif` logged - twice that the workspace root "does not appear to be a git repository" - because the exact head is checked out into `source`. A bare - `--output-files=` defaults to the sarif format in v2.5.1, so the - reporter's output is unchanged. The checkout-path assertion is the - negative fixture: an absent or wrong `checkout_path` fails here instead - of silently relying on server-derived commit identity. - """ - workflow = workflow_text("security-scan.yml") - - # Check each named scanner/reporter step on its own, so a flag removed from - # one step cannot hide behind the same string appearing elsewhere. - for step_name, output_flag in ( - ("Scan base with OSV", "--output-file=old-results.json"), - ("Retry base OSV without transitive resolution", "--output-file=old-results.json"), - ("Scan head with OSV", "--output-file=new-results.json"), - ("Retry head OSV without transitive resolution", "--output-file=new-results.json"), - ("Report PR-introduced OSV findings", "--output-files=results.sarif"), - ): - step = workflow_step(workflow, step_name) - assert output_flag in step, step_name - assert "\n --output=" not in step, step_name - - head_checkout = workflow_step(workflow, "Checkout head") - checkout_dir = re.search(r"(?m)^\s+path: (\S+)$", head_checkout).group(1) - upload_step = workflow_step(workflow, "Upload OSV SARIF to code scanning") - assert f"checkout_path: ${{{{ github.workspace }}}}/{checkout_dir}" in upload_step - - def test_osv_sarif_upload_is_marked_comprehensive_after_clean_comparison( tmp_path: Path, ) -> None: diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..74ea718d44 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -1342,10 +1342,15 @@ def test_probe_isolation_capability_accepts_working_bwrap(monkeypatch): shell the host happens to have mounted. """ monkeypatch.setattr(sandboxed_web_e2e, "_probe_shell", lambda: "/bin/true") + + def mock_run(*args, **kwargs): + assert kwargs.get("shell") is False + return subprocess.CompletedProcess(args, 0, stdout="", stderr="") + monkeypatch.setattr( sandboxed_web_e2e.subprocess, "run", - lambda *args, **kwargs: subprocess.CompletedProcess(args, 0, stdout="", stderr=""), + mock_run, ) sandboxed_web_e2e._probe_isolation_capability("/usr/bin/bwrap") 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 3070e7ff21..ba0b2598a9 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -51,10 +51,10 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: - """Require validation, scan, and attempt wake jobs to pin Ubuntu 24.04.""" + """Require both CodeQL Scan Dispatch jobs to pin Ubuntu 24.04.""" workflow = CODEQL_SCAN_DISPATCH.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2) 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_strix_backend_unavailable_after_exempted_finding.py b/tests/test_strix_backend_unavailable_after_exempted_finding.py index 22c7d30f7d..029f43ec55 100644 --- a/tests/test_strix_backend_unavailable_after_exempted_finding.py +++ b/tests/test_strix_backend_unavailable_after_exempted_finding.py @@ -37,7 +37,7 @@ "Severity: CRITICAL\n" "Vulnerabilities 1\n" "CRITICAL: 1\n" - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); " + "Strix findings are limited to unchanged files in this pull request; " "allowing pipeline continuation.\n" ) @@ -247,7 +247,7 @@ def test_real_finding_after_continuation_never_retries(self) -> None: calls=$(( $(cat __COUNTER__) + 1 )) echo "$calls" > __COUNTER__ printf '%s\n' \ - "Strix findings are limited to unchanged files in this pull request (evidence_scope=repository_baseline); allowing pipeline continuation." \ + "Strix findings are limited to unchanged files in this pull request; allowing pipeline continuation." \ "LLM CONNECTION FAILED" \ "Vulnerability Report" "Severity: CRITICAL" "Vulnerabilities 1" exit 1 diff --git a/tests/test_strix_evidence_binding.py b/tests/test_strix_evidence_binding.py deleted file mode 100644 index 90a5454ecb..0000000000 --- a/tests/test_strix_evidence_binding.py +++ /dev/null @@ -1,971 +0,0 @@ -"""Contract tests for Strix evidence binding (#2159, #2168).""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import pytest - -from scripts.ci import strix_evidence_binding as binding - - -BASE = "a" * 40 -HEAD = "b" * 40 -OTHER = "c" * 40 - - -def _pr_binding(**overrides: Any) -> binding.PullRequestBinding: - """Build a live-open PR binding with optional field overrides.""" - - payload = { - "repository": "ContextualWisdomLab/example", - "pull_request": 2106, - "state": "open", - "base_ref": "main", - "base_sha": BASE, - "head_sha": HEAD, - } - payload.update(overrides) - return binding.PullRequestBinding(**payload) - - -def test_binding_requires_live_open_full_shas() -> None: - """Incomplete or closed PR tuples fail closed before attribution.""" - - with pytest.raises(binding.EvidenceBindingError, match="live and open"): - _pr_binding(state="closed").require_live_open() - with pytest.raises(binding.EvidenceBindingError, match="40-character"): - _pr_binding(base_sha="abc").require_live_open() - with pytest.raises(binding.EvidenceBindingError, match="must differ"): - _pr_binding(head_sha=BASE).require_live_open() - - -def test_changed_source_finding_is_pr_delta() -> None: - """A finding on an authenticated changed hunk is PR-delta evidence.""" - - changed = ( - binding.ChangedPath( - path="docs/codeql.md", - status="modified", - changed_lines=frozenset({12, 13}), - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("docs/codeql.md", 12, 12), - ) - assert verdict.scope is binding.EvidenceScope.PR_DELTA - assert binding.pr_delta_findings_block_merge([verdict]) - - -def test_completely_base_identical_source_finding_is_repository_baseline() -> None: - """#2106-style findings against unchanged protected-base source stay baseline.""" - - changed = ( - binding.ChangedPath( - path="docs/codeql.md", - status="modified", - changed_lines=frozenset({1}), - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("scripts/ci/pingora_edge_policy.py", 40, 45), - ) - assert verdict.scope is binding.EvidenceScope.REPOSITORY_BASELINE - assert "base-identical" in verdict.reason - assert binding.baseline_only_findings([verdict]) - assert not binding.pr_delta_findings_block_merge([verdict]) - - -def test_unchanged_dependency_context_is_context_dependency() -> None: - """Context closure findings are labeled separately from the PR delta.""" - - changed = ( - binding.ChangedPath( - path="backend/app/routes.py", - status="modified", - changed_lines=frozenset({8}), - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("backend/app/models.py", 3, 3), - context_dependency_paths=frozenset({"backend/app/models.py"}), - ) - assert verdict.scope is binding.EvidenceScope.CONTEXT_DEPENDENCY - assert binding.baseline_only_findings([verdict]) - - -def test_changed_path_nonintersecting_line_is_baseline_not_pr_delta() -> None: - """Same changed path but outside every hunk is not a PR-delta finding.""" - - changed = ( - binding.ChangedPath( - path="frontend/src/App.tsx", - status="modified", - changed_lines=frozenset({40, 41}), - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("frontend/src/App.tsx", 1, 1), - ) - assert verdict.scope is binding.EvidenceScope.REPOSITORY_BASELINE - - -def test_rename_maps_previous_and_current_paths_to_pr_delta() -> None: - """Renamed files attribute findings via previous_filename or filename.""" - - changed = ( - binding.ChangedPath( - path="src/new_name.py", - status="renamed", - previous_path="src/old_name.py", - changed_lines=frozenset({5}), - ), - ) - for path in ("src/new_name.py", "src/old_name.py"): - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation(path, 5, 5), - ) - assert verdict.scope is binding.EvidenceScope.PR_DELTA - assert verdict.path == "src/new_name.py" - - -def test_stale_head_and_stacked_base_reports_fail_closed() -> None: - """Reports bound to the wrong base or head cannot authorize attribution.""" - - changed = ( - binding.ChangedPath(path="a.py", status="modified", changed_lines=frozenset({1})), - ) - with pytest.raises(binding.EvidenceBindingError, match="stale-head"): - binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("a.py", 1, 1), - report_head_sha=OTHER, - ) - with pytest.raises(binding.EvidenceBindingError, match="stacked-base"): - binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("a.py", 1, 1), - report_base_sha=OTHER, - ) - - -def test_empty_changed_inventory_fails_closed() -> None: - """Attribution cannot proceed without an authenticated changed-file set.""" - - with pytest.raises(binding.EvidenceBindingError, match="changed-file inventory"): - binding.classify_finding_scope( - _pr_binding(), - (), - binding.FindingLocation("a.py", 1, 1), - ) - - -def test_unsafe_finding_path_is_unmapped() -> None: - """Traversal and absolute finding paths cannot become PR-delta evidence.""" - - changed = ( - binding.ChangedPath(path="safe.py", status="added", changed_lines=frozenset({1})), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("../secret.py", 1, 1), - ) - assert verdict.scope is binding.EvidenceScope.UNMAPPED - - -def test_parse_changed_lines_from_patch() -> None: - """Unified-diff hunks yield only added/context head-side line numbers.""" - - patch = ( - "@@ -10,3 +10,4 @@\n" - " keep\n" - "-old\n" - "+new\n" - "+extra\n" - " tail\n" - ) - assert binding.parse_changed_lines_from_patch(patch) == frozenset({11, 12}) - - -def test_load_changed_paths_from_github_paginates_and_keeps_renames() -> None: - """GitHub inventory loader pages to completion and preserves rename metadata.""" - - pages = [ - [ - { - "filename": f"f{index}.py", - "status": "modified", - "patch": "@@ -1 +1 @@\n-old\n+new\n", - } - for index in range(100) - ], - [ - { - "filename": "renamed.py", - "previous_filename": "legacy.py", - "status": "renamed", - "patch": "@@ -2 +2 @@\n-a\n+b\n", - } - ], - ] - calls: list[str] = [] - - def opener(url: str, token: str) -> list[dict[str, Any]]: - assert token == "token" - calls.append(url) - return pages[len(calls) - 1] - - rows = binding.load_changed_paths_from_github( - "https://api.github.com", - "ContextualWisdomLab/example", - 2106, - "token", - opener=opener, - ) - assert len(calls) == 2 - assert len(rows) == 101 - assert rows[-1].previous_path == "legacy.py" - assert 2 in rows[-1].changed_lines - - -def test_apply_patch_miss_rejects_already_applied_claim(tmp_path: Path) -> None: - """#2168: failed apply_patch cannot be summarized as already applied.""" - - log_text = ( - "WorkspaceReadNotFoundError: file not found: /workspace/backend/app/main.py\n" - "agents.sandbox.errors.ApplyPatchFileNotFoundError: " - "apply_patch missing file: backend/app/main.py\n" - ) - report_text = ( - "Medium CWE-862 finding. The immediate fix was already applied in " - "backend/app/main.py and syntax-verified.\n" - ) - events = binding.detect_apply_patch_failures(log_text) - assert events and events[0].success is False - - verdict = binding.classify_remediation( - finding_confirmed=True, - fix_proposed=True, - tool_events=events, - workspace_root=tmp_path, - relative_path="backend/app/main.py", - expected_snippet="def secure():", - source_commit_sha=None, - report_claims_already_applied=True, - ) - assert verdict.state is binding.RemediationState.REMEDIATION_FAILED - assert verdict.allows_already_applied_claim is False - - cleaned = binding.sanitize_remediation_report_text(report_text, log_text) - assert "already applied" not in cleaned.casefold() - assert "remediation NOT applied" in cleaned - assert binding.RemediationState.REMEDIATION_FAILED.value in cleaned - - -def test_workspace_byte_proof_allows_scan_workspace_applied(tmp_path: Path) -> None: - """A fix is applied-in-scan only after exact workspace bytes contain the diff.""" - - target = tmp_path / "backend" / "app" / "main.py" - target.parent.mkdir(parents=True) - target.write_text("def secure():\n return True\n", encoding="utf-8") - - verdict = binding.classify_remediation( - finding_confirmed=True, - fix_proposed=True, - tool_events=(), - workspace_root=tmp_path, - relative_path="backend/app/main.py", - expected_snippet="def secure():", - source_commit_sha=None, - report_claims_already_applied=True, - ) - assert verdict.state is binding.RemediationState.FIX_APPLIED_IN_SCAN_WORKSPACE - assert verdict.allows_already_applied_claim is True - - -def test_source_commit_receipt_is_required_for_committed_state() -> None: - """Isolated sandbox mutation is never described as source-repository mutation.""" - - verdict = binding.classify_remediation( - finding_confirmed=True, - fix_proposed=True, - tool_events=(), - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha=HEAD, - report_claims_already_applied=True, - ) - assert verdict.state is binding.RemediationState.FIX_COMMITTED_TO_SOURCE - - with pytest.raises(binding.EvidenceBindingError, match="full SHA"): - binding.classify_remediation( - finding_confirmed=True, - fix_proposed=False, - tool_events=(), - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha="short", - report_claims_already_applied=False, - ) - - -def test_cli_classify_finding_and_sanitize(tmp_path: Path) -> None: - """CLI surfaces JSON verdicts and sanitizes false remediation prose.""" - - binding_path = tmp_path / "binding.json" - changed_path = tmp_path / "changed.json" - binding_path.write_text( - json.dumps( - { - "repository": "ContextualWisdomLab/example", - "pull_request": 2106, - "state": "open", - "base_ref": "main", - "base_sha": BASE, - "head_sha": HEAD, - } - ), - encoding="utf-8", - ) - changed_path.write_text( - json.dumps( - [ - { - "path": "docs/a.md", - "status": "modified", - "changed_lines": [3], - "patch_available": True, - } - ] - ), - encoding="utf-8", - ) - assert ( - binding.main( - [ - "classify-finding", - "--binding-json", - str(binding_path), - "--changed-paths-json", - str(changed_path), - "--path", - "scripts/ci/pingora_edge_policy.py", - "--start-line", - "10", - ] - ) - == 0 - ) - - report = tmp_path / "report.md" - log = tmp_path / "strix.log" - out = tmp_path / "clean.md" - report.write_text("fix already applied in backend/app/main.py", encoding="utf-8") - log.write_text( - "ApplyPatchFileNotFoundError: apply_patch missing file: backend/app/main.py", - encoding="utf-8", - ) - assert ( - binding.main( - [ - "sanitize-report", - "--report-file", - str(report), - "--log-file", - str(log), - "--output-file", - str(out), - ] - ) - == 0 - ) - assert "NOT applied" in out.read_text(encoding="utf-8") - - -def test_cli_classify_remediation_fails_closed_on_patch_miss(tmp_path: Path) -> None: - """classify-remediation exits non-zero when already-applied claims are false.""" - - report = tmp_path / "report.md" - log = tmp_path / "strix.log" - report.write_text("already applied", encoding="utf-8") - log.write_text( - "WorkspaceReadNotFoundError: file not found: /workspace/backend/app/main.py", - encoding="utf-8", - ) - assert ( - binding.main( - [ - "classify-remediation", - "--report-file", - str(report), - "--log-file", - str(log), - "--finding-confirmed", - "--fix-proposed", - ] - ) - == 2 - ) - - -def test_load_changed_paths_rejects_malformed_payload() -> None: - """Malformed GitHub pages fail closed instead of truncating evidence.""" - - def opener(_url: str, _token: str) -> dict[str, str]: - return {"not": "a-list"} - - with pytest.raises(binding.EvidenceBindingError, match="JSON array"): - binding.load_changed_paths_from_github( - "https://api.github.com", - "ContextualWisdomLab/example", - 1, - "token", - opener=opener, - ) - - -def test_path_level_pr_delta_when_patch_truncated() -> None: - """Missing inline patches still prove the path changed at path scope.""" - - changed = ( - binding.ChangedPath( - path="large.bin", - status="modified", - changed_lines=frozenset(), - patch_available=False, - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("large.bin", 9, 9), - ) - assert verdict.scope is binding.EvidenceScope.PR_DELTA - - -def test_binding_rejects_malformed_identity_fields() -> None: - """Repository, PR number, base ref, and head SHA shape fail closed.""" - - with pytest.raises(binding.EvidenceBindingError, match="owner/name"): - _pr_binding(repository="noneslash").require_live_open() - with pytest.raises(binding.EvidenceBindingError, match="positive integer"): - _pr_binding(pull_request=0).require_live_open() - with pytest.raises(binding.EvidenceBindingError, match="base_ref"): - _pr_binding(base_ref=" ").require_live_open() - with pytest.raises(binding.EvidenceBindingError, match="head_sha"): - _pr_binding(head_sha="zzz").require_live_open() - - -def test_parse_patch_handles_deletions_escapes_and_zero_count_hunks() -> None: - """Deletion-only and escaped hunk lines do not invent head-side numbers.""" - - patch = "\n".join( - [ - " preamble before any hunk is ignored", - "@@ -5,0 +5,0 @@", - " dangling line while current hunk is inactive", - "@@ -10,2 +10,1 @@", - " keep", - "-gone", - "\\ No newline at end of file", - "+++ ignored", - "--- ignored", - ] - ) - assert binding.parse_changed_lines_from_patch(patch) == frozenset() - - -def test_path_only_changed_finding_is_pr_delta() -> None: - """A changed path without a claimed line is still PR-delta evidence.""" - - changed = ( - binding.ChangedPath( - path="docs/a.md", - status="modified", - changed_lines=frozenset({3}), - ), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("docs/a.md"), - ) - assert verdict.scope is binding.EvidenceScope.PR_DELTA - - -def test_nonpositive_and_inverted_line_ranges_are_unmapped() -> None: - """Line-level claims must use a positive, non-inverted range.""" - - changed = ( - binding.ChangedPath( - path="docs/a.md", - status="modified", - changed_lines=frozenset({3}), - ), - ) - assert ( - binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("docs/a.md", 0, 0), - ).scope - is binding.EvidenceScope.UNMAPPED - ) - assert ( - binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("docs/a.md", 5, 2), - ).scope - is binding.EvidenceScope.UNMAPPED - ) - - -def test_matching_report_head_and_base_accept_exact_tuple() -> None: - """Exact matching report SHAs authorize attribution.""" - - changed = ( - binding.ChangedPath(path="a.py", status="added", changed_lines=frozenset({1})), - ) - verdict = binding.classify_finding_scope( - _pr_binding(), - changed, - binding.FindingLocation("a.py", 1, 1), - report_head_sha=HEAD, - report_base_sha=BASE, - ) - assert verdict.scope is binding.EvidenceScope.PR_DELTA - - -def test_malformed_report_shas_fail_closed() -> None: - """Missing or short report SHAs cannot authorize attribution.""" - - changed = ( - binding.ChangedPath(path="a.py", status="added", changed_lines=frozenset({1})), - ) - with pytest.raises(binding.EvidenceBindingError, match="report head SHA"): - binding.require_matching_report_head(_pr_binding(), None) - with pytest.raises(binding.EvidenceBindingError, match="report head SHA"): - binding.require_matching_report_head(_pr_binding(), "abcd") - with pytest.raises(binding.EvidenceBindingError, match="report base SHA"): - binding.require_matching_report_base(_pr_binding(), None) - with pytest.raises(binding.EvidenceBindingError, match="report base SHA"): - binding.require_matching_report_base(_pr_binding(), "abcd") - - -def test_load_changed_paths_rejects_invalid_entries_and_cap() -> None: - """Malformed rows and oversized inventories fail closed.""" - - def bad_entry(_url: str, _token: str) -> list[object]: - return ["not-an-object"] - - with pytest.raises(binding.EvidenceBindingError, match="not an object"): - binding.load_changed_paths_from_github( - "https://api.github.com", "ContextualWisdomLab/example", 1, "t", opener=bad_entry - ) - - def bad_fields(_url: str, _token: str) -> list[dict[str, object]]: - return [{"filename": "", "status": "modified", "patch": None}] - - with pytest.raises(binding.EvidenceBindingError, match="invalid fields"): - binding.load_changed_paths_from_github( - "https://api.github.com", "ContextualWisdomLab/example", 1, "t", opener=bad_fields - ) - - def bad_previous(_url: str, _token: str) -> list[dict[str, object]]: - return [ - { - "filename": "a.py", - "status": "renamed", - "previous_filename": 1, - "patch": None, - } - ] - - with pytest.raises(binding.EvidenceBindingError, match="previous_filename"): - binding.load_changed_paths_from_github( - "https://api.github.com", "ContextualWisdomLab/example", 1, "t", opener=bad_previous - ) - - def bad_patch(_url: str, _token: str) -> list[dict[str, object]]: - return [{"filename": "a.py", "status": "modified", "patch": 12}] - - with pytest.raises(binding.EvidenceBindingError, match="patch must be a string"): - binding.load_changed_paths_from_github( - "https://api.github.com", "ContextualWisdomLab/example", 1, "t", opener=bad_patch - ) - - calls = {"n": 0} - - def oversized(_url: str, _token: str) -> list[dict[str, object]]: - calls["n"] += 1 - return [ - { - "filename": f"f{calls['n']}-{index}.py", - "status": "modified", - "patch": None, - } - for index in range(100) - ] - - # Force the in-loop cap by temporarily lowering MAX_CHANGED_FILES. - original = binding.MAX_CHANGED_FILES - try: - binding.MAX_CHANGED_FILES = 50 # type: ignore[misc] - with pytest.raises(binding.EvidenceBindingError, match="exceeded"): - binding.load_changed_paths_from_github( - "https://api.github.com", - "ContextualWisdomLab/example", - 1, - "t", - opener=oversized, - ) - finally: - binding.MAX_CHANGED_FILES = original # type: ignore[misc] - - -def test_default_github_opener_error_paths(monkeypatch: pytest.MonkeyPatch) -> None: - """Token, HTTP, network, and JSON failures fail closed.""" - - from io import BytesIO - - with pytest.raises(binding.EvidenceBindingError, match="token is required"): - binding.default_github_opener("https://api.github.com/x", "") - - def raise_http(*_args: object, **_kwargs: object) -> object: - raise binding.HTTPError( - "https://api.github.com/x", - 403, - "Forbidden", - hdrs=None, - fp=BytesIO(), - ) - - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_http) - with pytest.raises(binding.EvidenceBindingError, match="HTTP 403"): - binding.default_github_opener("https://api.github.com/x", "token") - - def raise_url(*_args: object, **_kwargs: object) -> object: - raise binding.URLError("down") - - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_url) - with pytest.raises(binding.EvidenceBindingError, match="URLError"): - binding.default_github_opener("https://api.github.com/x", "token") - - class Response: - """Fake successful HTTP response with invalid JSON bytes.""" - - def read(self) -> bytes: - """Return non-JSON payload bytes.""" - - return b"not-json" - - def __enter__(self) -> "Response": - """Enter the context manager.""" - - return self - - def __exit__(self, *_args: object) -> None: - """Exit the context manager.""" - - return None - - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) - with pytest.raises(binding.EvidenceBindingError, match="not JSON"): - binding.default_github_opener("https://api.github.com/x", "token") - - -def test_default_github_opener_success(monkeypatch: pytest.MonkeyPatch) -> None: - """A well-formed GitHub JSON body is returned decoded.""" - - class Response: - """Fake successful HTTP response.""" - - def read(self) -> bytes: - """Return a JSON array payload.""" - - return b'[{"filename":"a.py","status":"added","patch":null}]' - - def __enter__(self) -> "Response": - """Enter the context manager.""" - - return self - - def __exit__(self, *_args: object) -> None: - """Exit the context manager.""" - - return None - - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) - rows = binding.load_changed_paths_from_github( - "https://api.github.com", - "ContextualWisdomLab/example", - 1, - "token", - ) - assert rows[0].path == "a.py" - assert rows[0].patch_available is False - - -def test_apply_patch_failure_without_path_and_without_already_applied_claim() -> None: - """Tool failures without a path still fail closed; claims are optional.""" - - events = binding.detect_apply_patch_failures( - "ApplyPatchFileNotFoundError: something went wrong without a path marker\n" - ) - assert events[0].target_path == "unknown" - verdict = binding.classify_remediation( - finding_confirmed=True, - fix_proposed=False, - tool_events=events, - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha=None, - report_claims_already_applied=False, - ) - assert verdict.state is binding.RemediationState.REMEDIATION_FAILED - - -def test_workspace_diff_helpers_reject_unsafe_and_missing(tmp_path: Path) -> None: - """Workspace proof rejects empty snippets, unsafe paths, and missing files.""" - - assert binding.workspace_contains_expected_diff(tmp_path, "a.py", "") is False - assert binding.workspace_contains_expected_diff(tmp_path, "../x", "x") is False - assert binding.workspace_contains_expected_diff(tmp_path, "missing.py", "x") is False - link = tmp_path / "link.py" - target = tmp_path / "real.py" - target.write_text("body", encoding="utf-8") - link.symlink_to(target) - assert binding.workspace_contains_expected_diff(tmp_path, "link.py", "body") is False - - -def test_classify_remediation_proposed_confirmed_and_false_claim(tmp_path: Path) -> None: - """Proposed and confirmed states are distinct from false already-applied claims.""" - - assert ( - binding.classify_remediation( - finding_confirmed=True, - fix_proposed=True, - tool_events=(), - workspace_root=tmp_path, - relative_path="a.py", - expected_snippet="missing", - source_commit_sha=None, - report_claims_already_applied=False, - ).state - is binding.RemediationState.FIX_PROPOSED - ) - assert ( - binding.classify_remediation( - finding_confirmed=True, - fix_proposed=False, - tool_events=(), - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha=None, - report_claims_already_applied=False, - ).state - is binding.RemediationState.FINDING_CONFIRMED - ) - assert ( - binding.classify_remediation( - finding_confirmed=True, - fix_proposed=False, - tool_events=(), - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha=None, - report_claims_already_applied=True, - ).state - is binding.RemediationState.REMEDIATION_FAILED - ) - with pytest.raises(binding.EvidenceBindingError, match="incomplete"): - binding.classify_remediation( - finding_confirmed=False, - fix_proposed=False, - tool_events=(), - workspace_root=None, - relative_path=None, - expected_snippet=None, - source_commit_sha=None, - report_claims_already_applied=False, - ) - - -def test_sanitize_noop_and_baseline_helpers() -> None: - """Sanitize is a no-op without failures; helpers cover empty mapped sets.""" - - assert binding.sanitize_remediation_report_text("already applied", "clean log") == ( - "already applied" - ) - assert binding.baseline_only_findings([]) is False - unmapped = binding.FindingScopeVerdict( - scope=binding.EvidenceScope.UNMAPPED, - path="x", - reason="r", - ) - assert binding.baseline_only_findings([unmapped]) is False - - -def test_cli_stdout_sanitize_and_error_paths(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: - """CLI writes sanitized text to stdout and maps parse errors to exit 2.""" - - report = tmp_path / "report.md" - log = tmp_path / "strix.log" - report.write_text("no remediation claim", encoding="utf-8") - log.write_text("clean", encoding="utf-8") - assert ( - binding.main( - [ - "sanitize-report", - "--report-file", - str(report), - "--log-file", - str(log), - ] - ) - == 0 - ) - assert "no remediation claim" in capsys.readouterr().out - - bad_binding = tmp_path / "bad.json" - bad_binding.write_text("{", encoding="utf-8") - assert ( - binding.main( - [ - "classify-finding", - "--binding-json", - str(bad_binding), - "--changed-paths-json", - str(bad_binding), - "--path", - "a.py", - ] - ) - == 2 - ) - - # Force the terminal fallback return by calling main with no matched command - # after argparse would normally prevent it — cover via classify with bad shape. - changed = tmp_path / "changed.json" - changed.write_text( - json.dumps([{"path": "a.py", "status": "modified", "changed_lines": ["x"]}]), - encoding="utf-8", - ) - good_binding = tmp_path / "good.json" - good_binding.write_text( - json.dumps( - { - "repository": "ContextualWisdomLab/example", - "pull_request": 1, - "state": "open", - "base_ref": "main", - "base_sha": BASE, - "head_sha": HEAD, - } - ), - encoding="utf-8", - ) - assert ( - binding.main( - [ - "classify-finding", - "--binding-json", - str(good_binding), - "--changed-paths-json", - str(changed), - "--path", - "a.py", - ] - ) - == 2 - ) - - -def test_cli_unmapped_finding_exits_two(tmp_path: Path) -> None: - """classify-finding exits 2 when the finding path is unmapped.""" - - binding_path = tmp_path / "binding.json" - changed_path = tmp_path / "changed.json" - binding_path.write_text( - json.dumps( - { - "repository": "ContextualWisdomLab/example", - "pull_request": 2106, - "state": "open", - "base_ref": "main", - "base_sha": BASE, - "head_sha": HEAD, - } - ), - encoding="utf-8", - ) - changed_path.write_text( - json.dumps( - [ - { - "path": "docs/a.md", - "status": "modified", - "changed_lines": [3], - "patch_available": True, - } - ] - ), - encoding="utf-8", - ) - assert ( - binding.main( - [ - "classify-finding", - "--binding-json", - str(binding_path), - "--changed-paths-json", - str(changed_path), - "--path", - "../secret.py", - "--start-line", - "1", - ] - ) - == 2 - ) - - -def test_workspace_read_oserror_returns_false(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """An unreadable workspace file cannot prove a remediation diff.""" - - target = tmp_path / "a.py" - target.write_text("body", encoding="utf-8") - - def boom(self: Path, *_args: object, **_kwargs: object) -> str: - raise OSError("denied") - - monkeypatch.setattr(Path, "read_text", boom) - assert binding.workspace_contains_expected_diff(tmp_path, "a.py", "body") is False - - -def test_workspace_missing_root_returns_false(tmp_path: Path) -> None: - """A missing workspace root cannot authorize remediation byte proof.""" - - missing = tmp_path / "missing-root" - assert binding.workspace_contains_expected_diff(missing, "a.py", "body") is False