diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 0c13c56d1..a6e6b8d98 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -4334,6 +4334,7 @@ jobs: if ! gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/strix.yml" \ --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Strix workflow is not installed on %s; skipping optional current-head Strix workflow-run lookup.\n' "$GH_REPOSITORY" >&2 : >"$output_file" rm -f "$runs_json" "$workflow_lookup_err" return 0 @@ -4540,7 +4541,22 @@ jobs: latest_current_head_manual_strix_run() { local runs_json + local workflow_lookup_err runs_json="$(mktemp)" + workflow_lookup_err="$(mktemp)" + + if ! gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/strix.yml" \ + --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then + if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Strix workflow is not installed on %s; skipping optional manual Strix run lookup.\n' "$GH_REPOSITORY" >&2 + rm -f "$runs_json" "$workflow_lookup_err" + return 0 + fi + cat "$workflow_lookup_err" >&2 + rm -f "$runs_json" "$workflow_lookup_err" + return 1 + fi + rm -f "$workflow_lookup_err" if ! gh run list \ --repo "$GH_REPOSITORY" \ diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 5605124b5..5bdbd0bf7 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -142,7 +142,7 @@ jobs: def iter_findings(path): data = json.loads(Path(path).read_text(encoding="utf-8")) - for result in data.get("results", []): + for result in data.get("results") or []: source = result.get("source", {}) source_name = source.get("path") or source.get("name") or "unknown" for package in result.get("packages", []): diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index c0926d4ee..6a69d240c 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -396,6 +396,27 @@ cleanup() { } trap cleanup EXIT +target_workflow_available() { + local workflow_file="$1" + local workflow_lookup_err + + workflow_lookup_err="$(mktemp)" + tmp_files+=("$workflow_lookup_err") + + if gh api -X GET "repos/${GH_REPOSITORY}/actions/workflows/${workflow_file}" \ + --jq '.id' >/dev/null 2>"$workflow_lookup_err"; then + return 0 + fi + + if grep -Fq "HTTP 404" "$workflow_lookup_err"; then + printf 'Optional workflow %s is not installed on %s; skipping current-head workflow-run lookup.\n' "$workflow_file" "$GH_REPOSITORY" >&2 + return 1 + fi + + cat "$workflow_lookup_err" >&2 + return 1 +} + manual_success_for_label() { local label="$1" local failed_run_id="${2:-}" @@ -580,26 +601,28 @@ gh api graphql \ | @tsv ' >"$manual_success_check_runs" -env HEAD_SHA="$HEAD_SHA" gh run list \ - --repo "$GH_REPOSITORY" \ - --workflow strix.yml \ - --commit "$HEAD_SHA" \ - --limit 200 \ - --json databaseId,workflowName,status,conclusion,url,event,headSha \ - --jq ' - .[] - | select((.event // "") == "workflow_dispatch") - | select((.headSha // "") == env.HEAD_SHA) - | select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix") - | select((.status // "") == "completed") - | select((.conclusion // "" | ascii_downcase) == "success") - | [ - "strix", - (.url // ""), - "Manual workflow_dispatch Strix evidence passed" - ] - | @tsv - ' >>"$manual_success_check_runs" || true +if target_workflow_available "strix.yml"; then + env HEAD_SHA="$HEAD_SHA" gh run list \ + --repo "$GH_REPOSITORY" \ + --workflow strix.yml \ + --commit "$HEAD_SHA" \ + --limit 200 \ + --json databaseId,workflowName,status,conclusion,url,event,headSha \ + --jq ' + .[] + | select((.event // "") == "workflow_dispatch") + | select((.headSha // "") == env.HEAD_SHA) + | select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix") + | select((.status // "") == "completed") + | select((.conclusion // "" | ascii_downcase) == "success") + | [ + "strix", + (.url // ""), + "Manual workflow_dispatch Strix evidence passed" + ] + | @tsv + ' >>"$manual_success_check_runs" || true +fi env HEAD_SHA="$HEAD_SHA" gh run list \ --repo "$GH_REPOSITORY" \ diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index b39052c5a..71ad28ca7 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1,6 +1,7 @@ import json import subprocess import sys +import textwrap from pathlib import Path @@ -128,6 +129,43 @@ 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_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" + 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()) + ) + + for filename in ("old-results.json", "new-results.json"): + (tmp_path / filename).write_text('{"results": null}\n', encoding="utf-8") + + result = subprocess.run( + [sys.executable, "-c", script], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ) + + assert "OSV base scan produced 0 finding(s) in old-results.json." in result.stdout + assert "OSV head scan produced 0 finding(s) in new-results.json." in result.stdout + + +def test_optional_strix_workflow_absence_is_logged_without_failing_lookup() -> None: + workflow = workflow_text("opencode-review.yml") + failed_check_evidence = (REPO_ROOT / "scripts/ci/collect_failed_check_evidence.sh").read_text( + encoding="utf-8" + ) + + assert "skipping optional current-head Strix workflow-run lookup" in workflow + assert "skipping optional manual Strix run lookup" in workflow + assert "Optional workflow %s is not installed" in failed_check_evidence + assert 'if target_workflow_available "strix.yml"; then' in failed_check_evidence + + def test_pr_scorecard_sarif_delegates_sast_and_vulnerability_posture_to_hard_gates() -> None: """PR Scorecard SARIF should not duplicate CodeQL/OSV/Trivy hard gates.""" for filename in ("scorecard-pr.yml", "security-scan.yml"):