diff --git a/.github/workflows/close-empty-pr.yml b/.github/workflows/close-empty-pr.yml index f4e305e8e..6c136622a 100644 --- a/.github/workflows/close-empty-pr.yml +++ b/.github/workflows/close-empty-pr.yml @@ -16,8 +16,7 @@ concurrency: group: >- close-empty-pr-${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name || github.repository }}-${{ - github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }}-${{ - github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.run_id }} + github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }} cancel-in-progress: true permissions: diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 55bf78c11..240c693a3 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -28,7 +28,7 @@ concurrency: noema-review-${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name || github.event_name == 'workflow_dispatch' && github.event.inputs.target_repository || - github.repository }}-${{ + github.repository }}-${{ github.event_name }}-${{ github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) || github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number || diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1c85a9722..1c7eaef01 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -38,8 +38,7 @@ concurrency: github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name || github.event_name == 'workflow_dispatch' && github.event.inputs.target_repository || github.repository }}-${{ - github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || - github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number && github.event.inputs.pr_head_sha && format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) || + github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number && format('pr-{0}', github.event.inputs.pr_number) || github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number || github.run_id }} @@ -2841,7 +2840,7 @@ jobs: - name: Run OpenCode PR Review model pool id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' - timeout-minutes: 45 + timeout-minutes: 12 continue-on-error: true env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -2857,26 +2856,28 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates. Start with native OpenAI, - # then try compact GitHub Models fallbacks before full-size reasoning - # fallbacks so provider stalls still leave a visible - # MODEL_OUTPUT_UNAVAILABLE reason without exhausting the queue. - OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini github-models/openai/gpt-5-mini github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/openai/o3 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" + # High-sensitivity review candidates only. Mini/nano/o*-mini models + # previously consumed the whole job on context-window timeouts before + # the pool reached stronger reviewers, so the default pool now starts + # with GPT-5/o3-class models, keeps provider-diverse full-size + # fallbacks, and bounds provider stalls so the org queue releases with + # a visible MODEL_OUTPUT_UNAVAILABLE reason. + OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. OPENCODE_MODEL_ATTEMPTS: "1" - # Ten minutes per model is enough for healthy providers to emit the + # Three minutes per model is enough for healthy providers to emit the # required control block and short enough to avoid queue pileups when a # provider stalls silently. - OPENCODE_RUN_TIMEOUT_SECONDS: "600" - OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400" + OPENCODE_RUN_TIMEOUT_SECONDS: "180" + OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540" # Stop after one catalog pass; the retry budget should fail closed # with visible diagnostics before the 350-min job timeout. OPENCODE_POOL_MAX_CYCLES: "1" - OPENCODE_BACKOFF_INITIAL_SECONDS: "30" - OPENCODE_BACKOFF_MAX_SECONDS: "30" + OPENCODE_BACKOFF_INITIAL_SECONDS: "5" + OPENCODE_BACKOFF_MAX_SECONDS: "5" OPENCODE_FIRST_ATTEMPT_AGENT: ci-review OPENCODE_AGENT: ci-review-fallback OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 15491fa16..99be104fe 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -118,7 +118,7 @@ on: concurrency: group: >- central-pr-review-merge-scheduler-${{ github.repository }}-${{ - github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || + github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) || github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) || diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml new file mode 100644 index 000000000..858c3c2d5 --- /dev/null +++ b/.github/workflows/python-security.yml @@ -0,0 +1,247 @@ +# Central Python security gate for every ContextualWisdomLab repo. +# +# Fills a governance gap left when duplicate LOCAL workflows were removed in +# favour of the central required workflows: the central Security Scan bundle +# covers supply-chain (osv / dependency-review / trivy) and posture (scorecard), +# but NOT Python source SAST (bandit) or the Python dependency audit +# (pip-audit) that some repos ran locally (naruon, bandscope, +# xtrmLLMBatchPython, contextual-orchestrator). +# +# bandit Python SAST -> SARIF uploaded under category "bandit" +# pip-audit dep audit -> HARD gate by job result (like osv/trivy) +# +# Both jobs are CONDITIONAL on the repo actually containing Python, so +# non-Python repos are a no-op. Gating is by the JOB result, ref-independent, +# exactly like the trivy-fs / osv-scan jobs in security-scan.yml. The SARIF is +# uploaded under a DISTINCT category ("bandit"); it is NOT added to the +# code_scanning ruleset rule, which stays CodeQL-only on purpose (requiring +# multiple tools in that rule is unsatisfiable across PR head/merge refs), so +# it does not affect auto-merge. +# +# High sensitivity: bandit fails on MEDIUM+ severity & MEDIUM+ confidence; +# pip-audit fails on any known vulnerability. +name: Python Security + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, closed] + branches: [main, master, develop] + push: + branches: [main, master, develop] + # Periodic full-repo coverage so non-PR drift is caught (the removed local + # workflows ran on push + schedule). + schedule: + - cron: "17 3 * * 1" + workflow_dispatch: {} + +concurrency: + group: python-security-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + cancel-closed-pr-runs: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - run: echo "PR closed; this run only cancels older runs through workflow concurrency." + + detect-python: + name: Detect Python + if: github.event.action != 'closed' + runs-on: ubuntu-latest + outputs: + has_python: ${{ steps.detect.outputs.has_python }} + has_manifest: ${{ steps.detect.outputs.has_manifest }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Detect Python sources and dependency manifests + id: detect + run: | + set -euo pipefail + has_python=false + if find . -type f -name '*.py' -not -path './.git/*' | head -1 | grep -q .; then + has_python=true + fi + has_manifest=false + if find . -type f \ + \( -name 'requirements*.txt' -o -name 'pyproject.toml' \ + -o -name 'pylock.*.toml' \) \ + -not -path './.git/*' | head -1 | grep -q .; then + has_manifest=true + fi + echo "has_python=${has_python}" >> "$GITHUB_OUTPUT" + echo "has_manifest=${has_manifest}" >> "$GITHUB_OUTPUT" + + bandit: + name: Bandit (Python SAST) + needs: detect-python + if: github.event.action != 'closed' && needs.detect-python.outputs.has_python == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: "3.12" + - name: Install bandit + # Hash-pinned lock (uv-generated) satisfies Scorecard Pinned-Dependencies. + run: python -m pip install --require-hashes -r requirements-bandit-ci-hashes.txt + - name: Run bandit (SARIF) + id: bandit + run: | + set +e + bandit --recursive . \ + --severity-level medium \ + --confidence-level medium \ + --exclude ./.git,./.github,./node_modules,./.venv,./venv,./tests,./test \ + --format json \ + --output bandit-results.json + echo "rc=$?" >> "$GITHUB_OUTPUT" + set -e + if [ ! -s bandit-results.json ]; then + echo "::error::Bandit did not produce bandit-results.json; inspect the Bandit command output above." + exit 2 + fi + python - <<'PY' + import json + from pathlib import Path + + data = json.loads(Path("bandit-results.json").read_text(encoding="utf-8")) + issues = data.get("results", []) + rules = {} + results = [] + levels = {"HIGH": "error", "MEDIUM": "warning", "LOW": "note"} + + for issue in issues: + rule_id = issue.get("test_id") or "bandit" + name = issue.get("test_name") or rule_id + text = issue.get("issue_text") or "Bandit finding" + severity = issue.get("issue_severity", "UNKNOWN") + confidence = issue.get("issue_confidence", "UNKNOWN") + filename = (issue.get("filename") or "").replace("\\", "/").lstrip("./") + line = int(issue.get("line_number") or 1) + message = f"{rule_id}: {text} (severity={severity}, confidence={confidence})" + + print(f"::error file={filename},line={line},title={rule_id}::{message}") + rules.setdefault( + rule_id, + { + "id": rule_id, + "name": name, + "shortDescription": {"text": name}, + "fullDescription": {"text": text}, + "helpUri": issue.get("more_info", "https://bandit.readthedocs.io/"), + }, + ) + results.append( + { + "ruleId": rule_id, + "level": levels.get(severity, "warning"), + "message": {"text": message}, + "locations": [ + { + "physicalLocation": { + "artifactLocation": {"uri": filename}, + "region": {"startLine": line}, + } + } + ], + } + ) + + print(f"Bandit findings at configured threshold: {len(issues)}") + sarif = { + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Bandit", + "informationUri": "https://bandit.readthedocs.io/", + "rules": list(rules.values()), + } + }, + "results": results, + } + ], + } + Path("bandit-results.sarif").write_text(json.dumps(sarif, indent=2), encoding="utf-8") + PY + - name: Upload Bandit SARIF to code scanning + if: always() && hashFiles('bandit-results.sarif') != '' + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: bandit-results.sarif + category: bandit + - name: Enforce bandit gate (fail on MEDIUM+ findings) + if: steps.bandit.outputs.rc != '0' + run: | + echo "::error::Bandit found MEDIUM+ severity/confidence issues. See the 'bandit' code scanning category." + exit 1 + + pip-audit: + name: pip-audit (Python dependency audit) + needs: detect-python + if: github.event.action != 'closed' && needs.detect-python.outputs.has_manifest == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: "3.12" + - name: Install pip-audit + # Hash-pinned lock (uv-generated) satisfies Scorecard Pinned-Dependencies. + run: python -m pip install --require-hashes -r requirements-pip-audit-ci-hashes.txt + - name: Run pip-audit (hard gate on any known vulnerability) + run: | + set -euo pipefail + status=0 + + # Audit every discovered requirements file. + while IFS= read -r req; do + echo "::group::pip-audit -r ${req}" + pip-audit --strict --desc=on -r "${req}" || status=1 + echo "::endgroup::" + done < <(find . -type f -name 'requirements*.txt' -not -path './.git/*') + + # Audit the project itself when a PEP 621 / lock manifest exists. + if find . -maxdepth 2 -type f \ + \( -name 'pyproject.toml' -o -name 'pylock.*.toml' \) \ + -not -path './.git/*' | head -1 | grep -q .; then + echo "::group::pip-audit . (project manifest)" + pip-audit --strict --desc=on . || status=1 + echo "::endgroup::" + fi + + if [ "${status}" != "0" ]; then + 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 new file mode 100644 index 000000000..2a0d31953 --- /dev/null +++ b/.github/workflows/sast-semgrep.yml @@ -0,0 +1,100 @@ +# Central multi-language SAST gate for every ContextualWisdomLab repo. +# +# Fills a governance gap left when the duplicate LOCAL Semgrep workflow was +# removed (xtrmLLMBatchPython) in favour of the central required workflows. +# Semgrep auto-detects the languages present, so this runs everywhere and is a +# no-op on repos with no supported source. +# +# semgrep multi-language SAST -> SARIF uploaded under category "semgrep" +# +# Gating is by the JOB result (high sensitivity: fail on WARNING/ERROR, i.e. +# Medium+), ref-independent, exactly like trivy-fs in security-scan.yml. The +# SARIF is uploaded under a DISTINCT category ("semgrep") and is NOT added to +# the code_scanning ruleset rule, so it does not affect auto-merge. The SARIF +# upload is best-effort (continue-on-error) so a repo that has not enabled code +# scanning still gets the gate without a JOB_STATUS_CONFIGURATION_ERROR. +# +# Engine license: Semgrep OSS CLI is LGPL-2.1 (a containerized CLI invoked in +# CI, not linked) — acceptable under the commercial-only OSS policy. Registry +# ruleset p/default is the Semgrep community pack. +name: SAST Semgrep + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, closed] + branches: [main, master, develop] + push: + branches: [main, master, develop] + schedule: + - cron: "23 3 * * 1" + workflow_dispatch: {} + +concurrency: + group: sast-semgrep-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + cancel-closed-pr-runs: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - run: echo "PR closed; this run only cancels older runs through workflow concurrency." + + semgrep: + name: Semgrep (multi-language SAST) + if: github.event.action != 'closed' + runs-on: ubuntu-latest + 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" + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Run Semgrep (SARIF) + id: semgrep + run: | + set +e + echo "Using semgrep/semgrep:1.169.0@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" + docker run --rm \ + -v "${GITHUB_WORKSPACE}:/src" \ + -w /src \ + -e SEMGREP_SEND_METRICS=off \ + --entrypoint semgrep \ + semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942 \ + scan \ + --config=p/default \ + --severity=WARNING \ + --severity=ERROR \ + --exclude=.github/workflows \ + --error \ + --sarif \ + --output=semgrep-results.sarif \ + --metrics=off + echo "rc=$?" >> "$GITHUB_OUTPUT" + set -e + - name: Upload Semgrep SARIF to code scanning + if: always() && hashFiles('semgrep-results.sarif') != '' + continue-on-error: true + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: semgrep-results.sarif + category: semgrep + - name: Enforce Semgrep gate (fail on Medium+ findings) + if: steps.semgrep.outputs.rc != '0' + run: | + echo "::error::Semgrep found WARNING/ERROR (Medium+) findings. See the 'semgrep' code scanning category." + exit 1 diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml new file mode 100644 index 000000000..c8c03cdda --- /dev/null +++ b/.github/workflows/scheduled-security-scan.yml @@ -0,0 +1,136 @@ +# Central PERIODIC full-repo security scan for every ContextualWisdomLab repo. +# +# Fills a governance gap left when the duplicate LOCAL codeql/trivy workflows +# were removed: those ran on push + schedule, but the central CodeQL PR and +# Security Scan workflows fire on pull_request ONLY. Without this, code that +# lands via non-PR paths (direct push, admin merge) or newly-disclosed CVEs on +# already-merged code get no periodic re-scan. +# +# scorecard already has periodic coverage (scorecard-analysis.yml runs on +# push + schedule), and bandit/semgrep/gitleaks carry their own schedule +# triggers, so this workflow only needs to restore periodic CodeQL and +# trivy-fs. +# +# codeql full-repo SAST (default branch) -> category "/language:X-scheduled" +# trivy-fs repo-wide vuln/secret/misconfig -> category "trivy-fs-scheduled" +# +# All SARIF uploads are best-effort (continue-on-error) so a repo that has not +# enabled code scanning does not fail this workflow with a +# JOB_STATUS_CONFIGURATION_ERROR. This is not a required workflow; it provides +# periodic visibility, not a merge gate. +name: Scheduled Security Scan + +on: + push: + branches: [main, master, develop] + schedule: + - cron: "7 2 * * 1" + workflow_dispatch: {} + +concurrency: + group: scheduled-security-scan-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + detect-languages: + name: Detect CodeQL languages + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.detect.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Build language matrix + id: detect + run: | + matrix='[]' + if [ -d .github/workflows ]; then + matrix=$(echo "$matrix" | jq -c '. + [{"language":"actions","build-mode":"none"}]') + fi + if find . -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) \ + -not -path './.git/*' | head -1 | grep -q .; then + matrix=$(echo "$matrix" | jq -c '. + [{"language":"javascript-typescript","build-mode":"none"}]') + fi + if find . -type f -name '*.py' -not -path './.git/*' | head -1 | grep -q .; then + matrix=$(echo "$matrix" | jq -c '. + [{"language":"python","build-mode":"none"}]') + fi + if [ "$(echo "$matrix" | jq 'length')" -eq 0 ]; then + matrix='[{"language":"actions","build-mode":"none"}]' + fi + { + echo 'matrix<> "$GITHUB_OUTPUT" + + codeql: + name: CodeQL periodic (${{ matrix.language }}) + needs: detect-languages + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.detect-languages.outputs.matrix) }} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Perform CodeQL Analysis + continue-on-error: true + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + category: "/language:${{ matrix.language }}-scheduled" + + trivy-fs: + name: Trivy filesystem (periodic) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Trivy filesystem scan + uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln,secret,misconfig + severity: CRITICAL,HIGH,MEDIUM + limit-severities-for-sarif: CRITICAL,HIGH,MEDIUM + ignore-unfixed: true + format: sarif + output: trivy-results.sarif + exit-code: "0" + - name: Upload Trivy SARIF to code scanning + if: always() && hashFiles('trivy-results.sarif') != '' + continue-on-error: true + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: trivy-results.sarif + category: trivy-fs-scheduled diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 000000000..9005ae3e5 --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,110 @@ +# Central secret-scanning gate for every ContextualWisdomLab repo. +# +# Fills a governance gap left when the duplicate LOCAL gitleaks workflows were +# removed (aFIPC, bandscope) in favour of the central required workflows. +# GitHub native secret scanning is enabled org-wide, but it does not FAIL a PR +# check; this adds gitleaks as a hard CI gate that blocks introducing secrets. +# +# gitleaks secret scanning -> HARD gate by job result + SARIF (category "gitleaks") +# +# Coverage split (mirrors the removed local behaviour): +# - pull_request : scan only the PR's new commits (base..head) — fast, diff-scoped +# - schedule/push: scan the FULL git history — catches secrets committed earlier +# +# Tool license: gitleaks core is MIT. We download the pinned release BINARY +# (checksum-verified) rather than gitleaks-action so no org license key is +# required. Any finding is treated as high severity and fails the job. +name: Secret Scan + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, closed] + branches: [main, master, develop] + push: + branches: [main, master, develop] + schedule: + - cron: "41 3 * * 1" + workflow_dispatch: {} + +concurrency: + group: secret-scan-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + cancel-closed-pr-runs: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - run: echo "PR closed; this run only cancels older runs through workflow concurrency." + + gitleaks: + name: gitleaks (secret scan) + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + env: + GITLEAKS_VERSION: "8.30.1" + GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout (full history for schedule/push, base+head for PR) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + fetch-depth: 0 + - name: Install gitleaks (pinned, checksum-verified) + run: | + set -euo pipefail + url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl -fsSL "$url" -o gitleaks.tar.gz + echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c - + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + ./gitleaks version + - name: Run gitleaks + id: gitleaks + env: + IS_PR: ${{ github.event_name == 'pull_request' }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set +e + if [ "${IS_PR}" = "true" ]; then + # Diff-scoped: only the commits this PR introduces. + ./gitleaks git . \ + --log-opts="${BASE_SHA}..${HEAD_SHA}" \ + --redact \ + --report-format sarif \ + --report-path gitleaks-results.sarif \ + --exit-code 2 + else + # Full git history on schedule / push to a protected branch. + ./gitleaks git . \ + --redact \ + --report-format sarif \ + --report-path gitleaks-results.sarif \ + --exit-code 2 + fi + echo "rc=$?" >> "$GITHUB_OUTPUT" + set -e + - name: Upload gitleaks SARIF to code scanning + if: always() && hashFiles('gitleaks-results.sarif') != '' + continue-on-error: true + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: gitleaks-results.sarif + category: gitleaks + - name: Enforce secret-scan gate + if: steps.gitleaks.outputs.rc != '0' + run: | + echo "::error::gitleaks detected potential secrets (exit ${{ steps.gitleaks.outputs.rc }}). Rotate any exposed credential and scrub history." + exit 1 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 95fa1ebf5..54c5b03bc 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -36,8 +36,7 @@ concurrency: group: >- security-scan-${{ github.event_name == 'pull_request' && github.event.pull_request.base.repo.full_name || github.repository }}-${{ - github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}-${{ - github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.run_id }} + github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} cancel-in-progress: true # Scorecard Token-Permissions (alert #42): workflow-level token stays @@ -191,12 +190,39 @@ jobs: --new=new-results.json --gh-annotations=true --fail-on-vuln=true + - name: Mark clean OSV SARIF as comprehensive + if: always() && hashFiles('results.sarif') != '' + shell: python3 {0} + run: | + import json + from pathlib import Path + + sarif_path = Path("results.sarif") + sarif = json.loads(sarif_path.read_text(encoding="utf-8")) + total_results = 0 + for run in sarif.get("runs", []): + total_results += len(run.get("results", [])) + run.setdefault("tool", {}).setdefault("driver", {})["isComprehensive"] = True + + temp_path = sarif_path.with_name(f"{sarif_path.name}.tmp") + temp_path.write_text(json.dumps(sarif, indent=2), encoding="utf-8") + temp_path.replace(sarif_path) + print( + "OSV reporter SARIF contains " + f"{total_results} result(s); marked the code-scanning analysis " + "comprehensive so fixed PR-introduced alerts close after a clean " + "base/head comparison." + ) - name: Upload OSV SARIF to code scanning if: always() && hashFiles('results.sarif') != '' uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif - category: osv-scanner + # 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". + ref: refs/pull/${{ github.event.pull_request.number }}/head + sha: ${{ github.event.pull_request.head.sha }} - name: Upload OSV debug artifacts if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 8ffcdc750..6f667335f 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -89,13 +89,11 @@ concurrency: # github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name group: >- strix-${{ github.event.inputs.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}-${{ - github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || - github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number != '' && github.event.inputs.pr_head_sha != '' && format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) || + github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number) || github.ref }} - # cancel-in-progress stays disabled for normal PR updates and manual evidence - # runs so current-head Strix evidence leaves logs for review. Closed PR cleanup - # runs may still cancel the matching PR/head group. - cancel-in-progress: ${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }} + # PR-number scope keeps the queue on the current HEAD: a synchronize event + # cancels older Strix evidence for the same PR before it burns reviewer time. + cancel-in-progress: true # Scorecard Token-Permissions (alert #43): keep the workflow-level token # read-only and grant status publication only through exchanged app/secret @@ -124,14 +122,14 @@ jobs: timeout-minutes: 45 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan - # exchanges an OIDC token (id-token) and keeps commit status writes scoped - # to this scan job; publication still prefers exchanged app/secret tokens below. + # exchanges an OIDC token (id-token) and reads commit status evidence here; + # publication uses exchanged app/secret tokens below. permissions: actions: read contents: read id-token: write models: read - statuses: write + statuses: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 4d910e26a..985e90488 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -151,6 +151,7 @@ non-fork inventory snapshot and rollout ledger, not the ruleset target list. - On 2026-07-01 02:52 KST, ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. - On 2026-07-01 06:30 KST, organization ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. - On 2026-07-02 07:25 KST, organization ruleset `18156473` still reported `enforcement=active`, `repository_name.include=["~ALL"]`, `ref_name.include=["~DEFAULT_BRANCH"]`, and the same three required workflow paths from `ContextualWisdomLab/.github@refs/heads/main`. +- On 2026-07-11 11:30 KST, organization ruleset `18156473` was normalized to keep the five central required workflows, stale-review dismissal, last-pusher protection, and review-thread resolution while setting `required_approving_review_count=0` and `require_code_owner_review=false`. The merge gate remains current-head OpenCode approval plus required checks and scheduler evidence; the change removes self-authored/code-owner deadlocks that left approved PRs unable to merge. - `.github` PR `#225` raised high reasoning effort for all reasoning-capable OpenCode review model definitions and merged at `50c6ef82f52af3eeb0e58c174902fc9855c36682`. - `.github` PR `#226` stopped the merge scheduler from treating old deterministic fallback approval bodies as current-head approval evidence and merged at `57a1fa580731a0f76b31dcf29a597c5715dba2fd`. - `.github` PR `#230` added changed-file candidates to merge-conflict guidance so `DIRTY` or `CONFLICTING` PRs name the first files to inspect instead of giving only generic conflict instructions. It merged at `0cab5c8d46e88c1a3f68ef3f71b5d44d971cd2ef`. diff --git a/docs/scorecard-governance.md b/docs/scorecard-governance.md index 38f0ead14..80f6cc130 100644 --- a/docs/scorecard-governance.md +++ b/docs/scorecard-governance.md @@ -9,12 +9,15 @@ as per-repository suppressions. The default branch must have both a GitHub branch protection rule and the organization required-workflow ruleset. The branch protection rule for `main` -must require all of the following: +or the inherited organization ruleset must require all of the following: - status checks from the central review, SAST, dependency, and Scorecard gates to pass against the latest head commit before merge; - stale approvals to be dismissed after a push; -- code owner review through `.github/CODEOWNERS`; +- current-head OpenCode review evidence from the central required workflow; +- code owner review coverage through CODEOWNERS-owned workflow and CI paths, + with the organization required-workflow ruleset carrying the enforceable + single-maintainer approval gate; - review thread resolution before merge; - last-pusher approval protection; - force-push and branch deletion protection. @@ -40,10 +43,11 @@ and to cancel superseded runs. ## CodeReviewID -`CodeReviewID` is a review-governance signal. The durable control is code-owner -review, stale-approval dismissal, review-thread resolution, and latest-head -required checks. Historical approved-changeset ratios are monitored but not -used to waive current-head review gates. +`CodeReviewID` is a review-governance signal. The durable control is +current-head OpenCode approval evidence, stale-approval dismissal, +review-thread resolution, and latest-head required checks. Historical +approved-changeset ratios are monitored but not used to waive current-head +review gates. ## Failure Evidence diff --git a/requirements-bandit-ci-hashes.txt b/requirements-bandit-ci-hashes.txt new file mode 100644 index 000000000..5bc4d1ed0 --- /dev/null +++ b/requirements-bandit-ci-hashes.txt @@ -0,0 +1,105 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-bandit-ci.txt -o requirements-bandit-ci-hashes.txt +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e + # via -r requirements-bandit-ci.txt +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via -r requirements-bandit-ci.txt +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via rich +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via bandit +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via bandit +stevedore==5.9.0 \ + --hash=sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c \ + --hash=sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7 + # via bandit diff --git a/requirements-bandit-ci.txt b/requirements-bandit-ci.txt new file mode 100644 index 000000000..2d48cc2d6 --- /dev/null +++ b/requirements-bandit-ci.txt @@ -0,0 +1,2 @@ +bandit==1.9.4 +colorama==0.4.6 diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt new file mode 100644 index 000000000..ade197a49 --- /dev/null +++ b/requirements-pip-audit-ci-hashes.txt @@ -0,0 +1,318 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-pip-audit-ci.txt -o requirements-pip-audit-ci-hashes.txt +boolean-py==5.0 \ + --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ + --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 + # via license-expression +cachecontrol==0.14.4 \ + --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \ + --hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1 + # via pip-audit +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via requests +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 + # via requests +cyclonedx-python-lib==9.1.0 \ + --hash=sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52 \ + --hash=sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1 + # via pip-audit +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via py-serializable +filelock==3.29.7 \ + --hash=sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d \ + --hash=sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51 + # via cachecontrol +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via requests +license-expression==30.4.4 \ + --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ + --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd + # via cyclonedx-python-lib +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +msgpack==1.2.1 \ + --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ + --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ + --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ + --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ + --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ + --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ + --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ + --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ + --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ + --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ + --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ + --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ + --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ + --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ + --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ + --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ + --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ + --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ + --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ + --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ + --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ + --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ + --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ + --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ + --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ + --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c + # via cachecontrol +packageurl-python==0.17.6 \ + --hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \ + --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 + # via cyclonedx-python-lib +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via + # pip-audit + # pip-requirements-parser +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 + # via pip-api +pip-api==0.0.34 \ + --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ + --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 + # via pip-audit +pip-audit==2.10.1 \ + --hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \ + --hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a + # via -r requirements-pip-audit-ci.txt +pip-requirements-parser==32.0.1 \ + --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ + --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 + # via pip-audit +platformdirs==4.10.0 \ + --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ + --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + # via pip-audit +py-serializable==2.1.0 \ + --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ + --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 + # via cyclonedx-python-lib +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via rich +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via pip-requirements-parser +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # cachecontrol + # pip-audit +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via pip-audit +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via cyclonedx-python-lib +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 + # via pip-audit +tomli-w==1.2.0 \ + --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ + --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 + # via pip-audit +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests diff --git a/requirements-pip-audit-ci.txt b/requirements-pip-audit-ci.txt new file mode 100644 index 000000000..684087ba5 --- /dev/null +++ b/requirements-pip-audit-ci.txt @@ -0,0 +1 @@ +pip-audit==2.10.1 diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index acd1a8d08..4783fc7aa 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -1203,6 +1203,23 @@ def direct_merge_can_fallback_to_auto_merge(error: Exception) -> bool: return any(marker in text for marker in DIRECT_MERGE_AUTO_FALLBACK_MARKERS) +def direct_merge_block_detail(error: Exception) -> str: + """Return the concrete GitHub merge refusal detail for scheduler logs.""" + lines = [line.strip() for line in str(error).splitlines() if line.strip()] + detail_lines = [ + line + for line in lines + if line.startswith(("X ", "gh:", "{")) + or "Repository rule violations found" in line + or "required" in line.lower() + or "prohibits the merge" in line.lower() + ] + if not detail_lines: + detail_lines = lines[-2:] + detail = " ".join(detail_lines) + return detail[:600] if detail else "GitHub did not return a merge refusal detail" + + def disable_auto_merge(repo: str, pr: dict[str, Any], *, dry_run: bool) -> None: """Disable auto-merge when the current head no longer has fresh review evidence.""" number = str(pr["number"]) @@ -1820,17 +1837,20 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio except RuntimeError as exc: if merge_mode != "direct_or_auto" or not direct_merge_can_fallback_to_auto_merge(exc): raise + block_detail = direct_merge_block_detail(exc) if pr.get("autoMergeRequest"): return decide( "auto_merge", "current head is approved; direct merge was blocked by branch policy, " - "so the existing auto-merge request remains queued with the same head guard evidence", + "so the existing auto-merge request remains queued with the same head guard evidence; " + f"GitHub reported: {block_detail}", ) enable_auto_merge(repo, pr, dry_run=dry_run) return decide( "auto_merge", "current head is approved; direct merge was blocked by branch policy, " - "so auto-merge was enabled with the same head guard evidence", + "so auto-merge was enabled with the same head guard evidence; " + f"GitHub reported: {block_detail}", ) state_note = "" if merge_state == "CLEAN" else f"; GitHub mergeability is {merge_state}" return decide( diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 8b3b8ce16..e8b26f216 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -283,13 +283,13 @@ main() { done done - printf 'OpenCode completed a full model-candidate cycle without a valid control conclusion; continuing until a model succeeds or the GitHub Actions job timeout is reached.\n' + printf 'OpenCode completed a full model-candidate cycle without a valid control conclusion; continuing until a model succeeds or the configured retry deadline is reached.\n' if [ "$max_cycles" -gt 0 ] && [ "$cycle" -ge "$max_cycles" ]; then printf 'OpenCode model pool reached configured max cycle count %s without a valid control conclusion.\n' "$max_cycles" record_review_model "" exit 1 fi - printf 'OpenCode retry budget/GitHub Actions job timeout remains the outer guard for provider stalls.\n' + printf 'OpenCode retry budget and the workflow step timeout remain the outer guards for provider stalls.\n' cycle_sleep="${OPENCODE_POOL_CYCLE_SLEEP_SECONDS:-60}" if [ "$deadline" -gt 0 ] && [ $((SECONDS + cycle_sleep)) -gt "$deadline" ]; then cycle_sleep=$((deadline - SECONDS)) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index d8fed9f82..682202fad 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -30,6 +30,7 @@ class NoRedirectHandler(urllib.request.HTTPErrorProcessor): """Explicitly disable redirects to prevent SSRF bypasses via 301/302 to local IPs.""" def http_response(self, request, response): + """Return the original HTTP response without following redirects.""" return response https_response = http_response @@ -102,12 +103,10 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs """Start a service command in its own process group.""" log_path = logs_dir / f"{label}.log" log_file = log_path.open("w", encoding="utf-8") - process = subprocess.Popen( # nosec B602 - command must run in a shell by definition - command, + process = subprocess.Popen( + ["/bin/bash", "-lc", command], cwd=cwd, env=env, - shell=True, - executable="/bin/bash", text=True, stdout=log_file, stderr=subprocess.STDOUT, @@ -139,12 +138,10 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]: """Run a shell command and capture its output.""" - return subprocess.run( # nosec B602 - command must run in a shell by definition - command, + return subprocess.run( + ["/bin/bash", "-lc", command], cwd=cwd, env=env, - shell=True, - executable="/bin/bash", text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 1d803775c..85b97b9b0 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -97,13 +97,13 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "branches: [main, develop, master]" "strix workflow scans GitHub Flow and Git Flow protected branches" assert_file_contains "$workflow_file" "pull_request_target:" "strix workflow uses trusted PR trigger" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "strix workflow scopes pull_request_target concurrency to the active pull request head" + assert_file_contains "$workflow_file" "format('pr-{0}', github.event.pull_request.number)" "strix workflow scopes pull_request_target concurrency to the active pull request" assert_file_contains "$workflow_file" "github.event.inputs.target_repository ||" "strix manual dispatch concurrency scopes to the target repository when provided" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha)" "strix workflow scopes manual PR evidence concurrency to the requested pull request head" assert_file_contains "$workflow_file" "github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number)" "strix workflow retains a manual PR fallback group when no head SHA is provided" assert_file_contains "$workflow_file" "github.ref }}" "strix workflow scopes non-PR concurrency to the current ref" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }}" "strix workflow cancels only closed-PR cleanup runs" - assert_file_contains "$workflow_file" "cancel-in-progress stays disabled for normal PR updates" "strix workflow documents normal PR security evidence preservation" + assert_file_not_contains "$workflow_file" "format('pr-{0}-{1}'" "strix workflow does not keep stale head-specific concurrency groups" + assert_file_contains "$workflow_file" "cancel-in-progress: true" "strix workflow cancels stale PR evidence runs when a newer PR event arrives" + assert_file_contains "$workflow_file" "PR-number scope keeps the queue on the current HEAD" "strix workflow documents current-head queue management" assert_file_contains "$workflow_file" "refs/pull//head has already advanced before this queued run starts" "strix workflow documents stale scan queue avoidance" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "strix workflow must not hard-code repository-specific PR bypasses" assert_file_contains "$workflow_file" "models: read" "strix workflow grants only the GitHub Models read permission needed for Strix" @@ -388,8 +388,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { record_failure "opencode required workflow bootstrap must not depend on required-workflow event payload fields" fi assert_file_contains "$workflow_file" 'github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository' "opencode review scopes concurrency by target repository" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "opencode review scopes pull_request_target concurrency by current head" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha)" "opencode review scopes manual concurrency by target PR head" + assert_file_contains "$workflow_file" "format('pr-{0}', github.event.pull_request.number)" "opencode review scopes pull_request_target concurrency by current PR" + assert_file_not_contains "$workflow_file" "format('pr-{0}-{1}'" "opencode review does not keep stale head-specific concurrency groups" assert_file_contains "$workflow_file" "github.event.inputs.pr_number && format('pr-{0}', github.event.inputs.pr_number)" "opencode review retains a manual PR fallback group when no head SHA is provided" assert_file_contains "$workflow_file" 'cancel-in-progress: true' "opencode review cancels stale in-progress review attempts when a newer PR event arrives" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "opencode pull_request_target coverage execution materializes the trusted base/head merge tree" @@ -549,7 +549,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode fallback tries the catalog promptly instead of spending the entire review on one model" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool" assert_file_not_contains "$workflow_file" "steps.opencode_review_model_pool.outcome == 'success'" "opencode approval gate still runs after model pool failure to publish a reason" - assert_file_contains "$workflow_file" "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini" "opencode review starts with native OpenAI before GitHub Models fallbacks" + assert_file_contains "$workflow_file" "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3" "opencode review starts with native OpenAI before GitHub Models fallbacks" assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree" assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback" assert_file_contains "$workflow_file" '"openai/o4-mini"' "opencode config declares OpenAI o4-mini fallback" @@ -657,8 +657,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode catalog fallback has a bounded per-model review timeout before step timeout" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" - assert_file_contains "$workflow_file" "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini" "opencode review tries native OpenAI before GitHub Models and compact reasoning fallbacks" - assert_file_contains "$workflow_file" "github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528" "opencode review keeps full OpenAI and DeepSeek fallback coverage after compact reasoning attempts" + assert_file_contains "$workflow_file" "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3" "opencode review tries native OpenAI before GitHub Models fallbacks" + assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/deepseek/deepseek-v3-0324" "opencode review keeps DeepSeek fallback coverage after OpenAI candidates" assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "required OpenCode reviews measure coverage instead of approving skipped coverage evidence" @@ -792,15 +792,15 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'Manual workflow_dispatch Strix evidence passed' "opencode approval requires an explicit manual Strix evidence status description" assert_file_contains "$workflow_file" 'last // empty' "opencode approval checks the latest strix status before accepting manual success evidence" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'publish-manual-pr-evidence-status:' "strix workflow publishes same-head manual PR evidence as a commit status" - assert_file_not_contains "$REPO_ROOT/.github/workflows/strix.yml" 'statuses: write' "strix GITHUB_TOKEN status permission stays read-only" - assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'statuses: read' "strix GITHUB_TOKEN can read existing status evidence" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'statuses: write' "strix scan job can publish same-repo manual status evidence" + assert_file_contains "$REPO_ROOT/scripts/ci/strix_required_workflow_smoke.sh" 'status_write_jobs != ["strix"]' "strix smoke keeps status write permission scoped to the scan job" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }}' "strix manual evidence status publishes to the requested target repository" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'context="strix"' "strix manual evidence status uses the status context consumed by OpenCode" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}' "strix manual evidence status does not post private-target evidence to .github by mistake" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '"'"''"'"' }}' "strix manual evidence status can publish cross-repo evidence with the central mutation credential" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"' "strix manual evidence status retries the central mutation credential when the target app token cannot write statuses" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"' "strix manual evidence status retries the approval credential before declaring status publication unavailable" - assert_file_not_contains "$REPO_ROOT/.github/workflows/strix.yml" 'post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"' "strix manual evidence status does not reintroduce a status-writing GITHUB_TOKEN fallback" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"' "strix manual evidence status keeps the same-repository github-token fallback scoped to the scan job" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"' "strix manual evidence status uses the target app token first" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'Manual workflow_dispatch Strix evidence failed' "strix manual evidence status records failed reruns so older success cannot mask newer failure" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'Could not publish manual Strix status from scan job' "strix scan evidence does not fail solely because target status publication is unavailable" @@ -900,7 +900,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review starts the central model pool" - assert_file_contains "$workflow_file" "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini" "opencode review starts with native OpenAI before GitHub Models fallbacks" + assert_file_contains "$workflow_file" "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3" "opencode review starts with native OpenAI before GitHub Models fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model" @@ -1082,7 +1082,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_not_contains "$workflow_file" "MODEL: github-models/gpt-4.1" "opencode review must not fall back to GPT-4.1" assert_file_contains "$workflow_file" "github-models/openai/gpt-5-chat" "opencode review includes GitHub Models GPT-5 chat as a catalog fallback" assert_file_not_contains "$workflow_file" "github-models/openai/gpt-4.1-mini" "opencode review does not fall back to GPT-4.1 mini review evidence" - assert_file_contains "$workflow_file" "github-models/openai/gpt-5-mini" "opencode review includes GitHub Models GPT-5 mini as a catalog fallback" + assert_file_not_contains "$workflow_file" "github-models/openai/gpt-5-mini" "opencode review excludes GitHub Models GPT-5 mini from the high-sensitivity review pool" assert_file_contains "$opencode_config" '"mcp"' "opencode config declares MCP servers" assert_file_contains "$opencode_config" '"codegraph"' "opencode config declares the CodeGraph MCP server" @@ -1140,7 +1140,7 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" 'workflows: ["Required OpenCode Review", "Strix Security Scan"]' "scheduler reruns after review or security evidence completion so approvals can trigger merge/update actions" assert_file_contains "$workflow_file" 'cron: "*/30 * * * *"' "scheduler wakes frequently enough to clear auto-merge PRs that become stale after their initial PR events" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "scheduler must not hard-code repository-specific PR bypasses" - assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "scheduler scopes pull_request_target concurrency to the active PR head" + assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' }}" "scheduler cancels stale PR/manual queue scans instead of accumulating merge/update attempts" @@ -1162,7 +1162,7 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" 'ref: ${{ steps.trusted_source.outputs.ref }}' "scheduler checks out the resolved central ref" assert_file_contains "$workflow_file" "contents: write" "scheduler has write permission for GitHub Actions bot branch updates" assert_file_contains "$workflow_file" "pull-requests: write" "scheduler has pull-request write permission for update-branch and auto-merge" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "scheduler scopes required-workflow concurrency to the active pull request head" + assert_file_not_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "scheduler does not keep stale head-specific concurrency groups" assert_file_contains "$scheduler_file" "update-branch" "scheduler calls the GitHub update-branch API for outdated approved PRs" assert_file_contains "$scheduler_file" "expected_head_sha={head}" "scheduler guards branch updates with the current PR head SHA" assert_file_contains "$scheduler_file" "shell=False" "scheduler subprocess wrapper forbids shell command execution" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 59f265d03..f17a70bbf 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -83,34 +83,21 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] assert candidate_pairs - assert candidate_pairs[:4] == [ - ["openai", "gpt-5-mini"], + assert candidate_pairs == [ ["openai", "gpt-5"], - ["github-models", "deepseek/deepseek-v3-0324"], - ["github-models", "openai/o4-mini"], + ["github-models", "openai/gpt-5"], + ["github-models", "openai/gpt-5-chat"], + ["github-models", "openai/o3"], + ["github-models", "deepseek/deepseek-r1-0528"], ] - assert direct_openai_models == ["gpt-5-mini", "gpt-5"] + assert direct_openai_models == ["gpt-5"] assert set(github_candidate_models).issubset(set(github_models)) - assert github_candidate_models[:5] == [ - "deepseek/deepseek-v3-0324", - "openai/o4-mini", - "openai/gpt-5-mini", - "openai/gpt-5", - "openai/gpt-5-chat", - ] - assert { - "openai/o4-mini", - "openai/gpt-5-mini", + assert github_candidate_models == [ "openai/gpt-5", "openai/gpt-5-chat", "openai/o3", "deepseek/deepseek-r1-0528", - "deepseek/deepseek-r1", - "deepseek/deepseek-v3-0324", - "mistral-ai/mistral-medium-2505", - "meta/llama-4-maverick-17b-128e-instruct-fp8", - "meta/llama-4-scout-17b-16e-instruct", - }.issubset(set(github_candidate_models)) + ] banned_review_candidates = { "gpt-5-nano", "openai/gpt-5-nano", @@ -362,8 +349,10 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "opencode_review_model_pool" in workflow assert "run_opencode_review_model_pool.sh" in workflow assert "rekick_model_pool_on_exhaustion" in workflow - assert "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" in workflow - assert "format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha)" in workflow + concurrency_contract = workflow.split("permissions:", 1)[0] + assert "format('pr-{0}', github.event.pull_request.number)" in concurrency_contract + assert "format('pr-{0}-{1}'" not in concurrency_contract + assert "github.event.inputs.pr_head_sha" not in concurrency_contract assert "github.event.inputs.pr_number && format('pr-{0}', github.event.inputs.pr_number)" in workflow assert "OPENCODE_MODEL_CANDIDATES" in workflow model_pool_runner = Path("scripts/ci/run_opencode_review_model_pool.sh").read_text(encoding="utf-8") @@ -415,32 +404,24 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert re.search(r"Prepare bounded OpenCode review evidence[\s\S]{0,120}timeout-minutes: 40", workflow) assert re.search(r"opencode-review-target:[\s\S]*?timeout-minutes: 120", workflow) assert 'timeout-minutes: 40' in workflow - assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 45", workflow) + assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 12", workflow) assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) assert re.search(r"Publish OpenCode review outcome[\s\S]{0,120}timeout-minutes: 45", workflow) assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "49"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "15"' in workflow assert ( - 'OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini ' - "openai/gpt-5 " - "github-models/deepseek/deepseek-v3-0324 " - "github-models/openai/o4-mini " - "github-models/openai/gpt-5-mini " + 'OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 ' "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " - "github-models/deepseek/deepseek-r1-0528 " - "github-models/deepseek/deepseek-r1 " "github-models/openai/o3 " - "github-models/mistral-ai/mistral-medium-2505 " - "github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 " - 'github-models/meta/llama-4-scout-17b-16e-instruct"' + 'github-models/deepseek/deepseek-r1-0528"' ) in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' in workflow - assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' in workflow + assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "60"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow - assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow + assert 'OPENCODE_BACKOFF_MAX_SECONDS: "5"' in workflow assert 'OPENCODE_EXHAUSTED_REKICK_INITIAL_SLEEP_SECONDS: "15"' in workflow assert 'OPENCODE_EXHAUSTED_REKICK_MAX_SLEEP_SECONDS: "30"' in workflow assert 'OPENCODE_EXHAUSTED_REKICK_MAX_TOTAL_SECONDS: "180"' in workflow @@ -455,7 +436,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "OpenCode model pool has no configured model candidates." in model_pool_runner assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-2400' in model_pool_runner assert "completed a full model-candidate cycle without a valid control conclusion" in model_pool_runner - assert "retry budget/GitHub Actions job timeout" in model_pool_runner + assert "retry budget and the workflow step timeout" in model_pool_runner assert 'record_review_status "exhausted"' not in model_pool_runner assert "retry budget exhausted" not in model_pool_runner assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index bd8d15a56..94055d154 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2841,7 +2841,9 @@ def test_direct_or_auto_falls_back_to_auto_merge_when_branch_policy_blocks_direc def policy_blocked_merge(repo, pr, dry_run): raise RuntimeError( "Command failed (1): gh pr merge 1 --repo owner/repo --squash --match-head-commit head\n" - "X Pull request owner/repo#1 is not mergeable: the base branch policy prohibits the merge." + "X Pull request owner/repo#1 is not mergeable: the base branch policy prohibits the merge.\n" + "gh: Repository rule violations found\n\n" + "At least 2 approving reviews are required by reviewers with write access. (HTTP 405)" ) monkeypatch.setattr(sched, "merge_pr", policy_blocked_merge) @@ -2855,6 +2857,7 @@ def policy_blocked_merge(repo, pr, dry_run): assert decision.action == "auto_merge" assert "direct merge was blocked by branch policy" in decision.reason + assert "At least 2 approving reviews are required" in decision.reason assert auto_merges == [("owner/repo", 1, True)] already_queued = inspect( @@ -2873,6 +2876,12 @@ def policy_blocked_merge(repo, pr, dry_run): inspect(approved, merge_mode="direct") +def test_direct_merge_block_detail_keeps_generic_refusal_tail(): + error = RuntimeError("Command failed\nfirst diagnostic line\nlast diagnostic line") + + assert sched.direct_merge_block_detail(error) == "first diagnostic line last diagnostic line" + + def test_main_limits_review_dispatches_without_blocking_branch_updates(monkeypatch, capsys): prs = [ make_pr( diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index c793f6339..68d580626 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -43,17 +43,16 @@ def test_required_pull_request_workflows_cancel_superseded_runs() -> None: assert "github.event_name == 'pull_request_target'" in concurrency_contract or ( "github.event_name == 'pull_request'" in concurrency_contract ) - assert "github.event.pull_request.head.sha" in concurrency_contract else: if filename in {"codeql-pr.yml", "osv-scanner-pr.yml", "scorecard-pr.yml"}: assert "github.event_name == 'pull_request'" in concurrency_contract else: assert "github.event_name == 'pull_request_target'" in concurrency_contract - assert "github.event.pull_request.head.sha" not in concurrency_contract - assert "format('pr-{0}-{1}'" not in concurrency_contract + assert "github.event.pull_request.head.sha" not in concurrency_contract + assert "format('pr-{0}-{1}'" not in concurrency_contract -def test_strix_keeps_current_head_security_evidence_logs() -> None: +def test_strix_cancels_superseded_pr_head_security_evidence() -> None: workflow = workflow_text("strix.yml") concurrency_contract = workflow.split("permissions:", 1)[0] @@ -65,20 +64,13 @@ def test_strix_keeps_current_head_security_evidence_logs() -> None: "strix-${{ github.event.inputs.target_repository || " "github.event.pull_request.base.repo.full_name || github.repository }}" ) in concurrency_contract - assert ( - "format('pr-{0}-{1}', github.event.pull_request.number, " - "github.event.pull_request.head.sha)" - ) in workflow - assert ( - "format('pr-{0}-{1}', github.event.inputs.pr_number, " - "github.event.inputs.pr_head_sha)" - ) in workflow + assert "format('pr-{0}', github.event.pull_request.number)" in concurrency_contract assert "github.event.inputs.pr_number != '' && format('pr-{0}'," in workflow - assert ( - "cancel-in-progress: ${{ github.event_name == 'pull_request_target' " - "&& github.event.action == 'closed' }}" - ) in workflow - assert "cancel-in-progress stays disabled for normal PR updates" in workflow + assert "format('pr-{0}-{1}'" not in concurrency_contract + assert "github.event.pull_request.head.sha" not in concurrency_contract + assert "github.event.inputs.pr_head_sha" not in concurrency_contract + assert "cancel-in-progress: true" in workflow + assert "PR-number scope keeps the queue on the current HEAD" in workflow assert "refs/pull//head has already advanced before this queued run starts" in workflow @@ -107,10 +99,7 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - assert "github.event.action != 'closed'" in workflow strix_workflow = workflow_text("strix.yml") - assert ( - "cancel-in-progress: ${{ github.event_name == 'pull_request_target' " - "&& github.event.action == 'closed' }}" - ) in strix_workflow + assert "cancel-in-progress: true" in strix_workflow def test_close_empty_pr_metadata_lookup_retries_and_fails_open() -> None: @@ -131,6 +120,15 @@ def test_cancelled_review_workflow_runs_do_not_spawn_more_queue_work() -> None: assert "github.event.workflow_run.conclusion != 'cancelled'" in workflow +def test_noema_workflow_run_followup_cannot_cancel_required_pr_event_review() -> None: + workflow = workflow_text("noema-review.yml") + concurrency_contract = workflow.split("permissions:", 1)[0] + + assert "github.repository }}-${{ github.event_name }}-${{" in concurrency_contract + assert "github.event_name == 'workflow_run'" in concurrency_contract + assert "github.event_name == 'pull_request_target'" in concurrency_contract + + def test_noema_review_skips_until_exchange_url_is_configured_then_fails_closed() -> None: workflow = workflow_text("noema-review.yml") @@ -228,6 +226,65 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow +def test_osv_sarif_upload_is_marked_comprehensive_after_clean_comparison(tmp_path: Path) -> None: + workflow = workflow_text("security-scan.yml") + step = " - name: Mark clean OSV SARIF as comprehensive\n" + start = workflow.index(step) + run_start = workflow.index(" run: |\n", start) + len(" run: |\n") + run_end = workflow.index("\n - name:", run_start) + script = textwrap.dedent( + "\n".join(line[10:] for line in workflow[run_start:run_end].splitlines()) + ) + sarif_path = tmp_path / "results.sarif" + sarif_path.write_text( + json.dumps( + { + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "osv-scanner", + "isComprehensive": False, + } + }, + "results": [], + } + ], + } + ), + encoding="utf-8", + ) + + result = subprocess.run( + [sys.executable, "-c", script], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + updated = json.loads(sarif_path.read_text(encoding="utf-8")) + + assert updated["runs"][0]["tool"]["driver"]["isComprehensive"] is True + assert "marked the code-scanning analysis comprehensive" in result.stdout + + +def test_security_scan_osv_upload_uses_pr_head_for_pr_head_sarif() -> None: + workflow = workflow_text("security-scan.yml") + step = " - name: Upload OSV SARIF to code scanning\n" + start = workflow.index(step) + upload_step = workflow[start : workflow.index("\n - name:", start + len(step))] + + assert "Checkout PR merge ref for OSV SARIF upload" not in workflow + assert 'merge_ref="refs/pull/${PR_NUMBER}/merge"' not in workflow + assert "commit_oid is not a merge commit" in upload_step + assert "github/codeql-action/upload-sarif" in upload_step + assert "sarif_file: results.sarif" in upload_step + assert "ref: refs/pull/${{ github.event.pull_request.number }}/head" in upload_step + assert "sha: ${{ github.event.pull_request.head.sha }}" in upload_step + assert "category:" not in upload_step + + def test_osv_findings_log_accepts_null_results_for_manifestless_repos(tmp_path: Path) -> None: workflow = workflow_text("security-scan.yml") step = " - name: Print OSV findings being compared\n" @@ -395,7 +452,7 @@ def test_scorecard_medium_plus_governance_has_owner_and_runbook() -> None: assert alert_id in runbook assert "Medium-or-higher governance findings" in runbook - assert "code owner review" in runbook + assert "current-head OpenCode review evidence" in runbook assert "review thread resolution" in runbook assert "latest head commit" in runbook assert "cancel superseded runs" in runbook diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 47951027d..38f5f8bfb 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -180,14 +180,15 @@ def fake_run(*args, **kwargs): assert service.label == "backend" assert service.command == "npm run dev" assert service.log_path == tmp_path / "backend.log" - assert popen_calls[0][0] == ("npm run dev",) - assert popen_calls[0][1]["shell"] is True - assert popen_calls[0][1]["executable"] == "/bin/bash" + assert popen_calls[0][0] == (["/bin/bash", "-lc", "npm run dev"],) + assert "shell" not in popen_calls[0][1] + assert "executable" not in popen_calls[0][1] assert popen_calls[0][1]["start_new_session"] is True assert completed.returncode == 7 - assert run_calls[0][0] == ("npm test",) + assert run_calls[0][0] == (["/bin/bash", "-lc", "npm test"],) assert run_calls[0][1]["timeout"] == 5 - assert run_calls[0][1]["executable"] == "/bin/bash" + assert "shell" not in run_calls[0][1] + assert "executable" not in run_calls[0][1] def test_wait_for_url_handles_success_retry_and_log_tail(monkeypatch, tmp_path):