diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index a6d21b15c..e074c6390 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -316,7 +316,7 @@ jobs: SCHEDULER_READ_TOKEN: ${{ github.token }} SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github - SCHEDULER_REQUIRED_WORKFLOW_REF: ${{ steps.trusted_source.outputs.ref }} + SCHEDULER_REQUIRED_WORKFLOW_REF: main SCHEDULER_ALLOW_CROSS_REPO_WORKFLOW_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }} run: | set -euo pipefail diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 6f667335f..867e24fa0 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -83,12 +83,11 @@ on: type: string concurrency: - # Manual evidence runs are workflow_dispatch-scoped by target repository: - # github.event_name == 'workflow_dispatch' && github.event.inputs.target_repository - # Pull request evidence runs are scoped by base repository: - # github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name + # Include the event name so manual workflow_dispatch evidence cannot cancel + # the required pull_request_target Strix context that branch protection reads. + # Within each event class, PR-number scope still keeps only the current head. group: >- - strix-${{ github.event.inputs.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}-${{ + strix-${{ github.event_name }}-${{ github.event.inputs.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}-${{ 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 }} # PR-number scope keeps the queue on the current HEAD: a synchronize event diff --git a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh index ea14c56ba..30c5e8708 100755 --- a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh +++ b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh @@ -715,6 +715,9 @@ emit_strix_cancelled_without_log_finding() { if [ -z "$match" ]; then match="$(grep -nF -- "cancel-in-progress: false" "${REPO_ROOT%/}/$path" | head -n 1 || true)" fi + if [ -z "$match" ]; then + match="$(grep -nF -- "cancel-in-progress: true" "${REPO_ROOT%/}/$path" | head -n 1 || true)" + fi if [ -n "$match" ]; then line="${match%%:*}" fi @@ -732,7 +735,7 @@ emit_strix_cancelled_without_log_finding() { printf -- '- Fix: Do not approve from this cancelled run. Re-run the current-head Strix Security Scan after stale runs complete or are cancelled, then review the resulting job log; keep the workflow concurrency line at %s:%s so stale runs do not silently replace current-head evidence.\n' "$path" "$line" printf -- '- Regression test: Keep failed-check evidence collection explicit for cancelled workflow runs with no job log so reviewers see that the blocker is missing scanner evidence.\n\n' fi - printf -- '- Suggested edit: preserve `%s:%s` with normal PR/manual Strix cancellation disabled, cancel only closed-PR cleanup or superseded non-current-head runs when needed, and rerun current-head Strix until logs exist.\n\n' "$path" "$line" + printf -- '- Suggested edit: preserve `%s:%s` with event-separated Strix concurrency, so workflow_dispatch evidence cannot cancel the required pull_request_target context while same-event stale runs still collapse to current-head evidence; rerun current-head Strix until logs exist.\n\n' "$path" "$line" } extract_supply_chain_records() { diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index 213fe0402..f51b8c353 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -107,20 +107,32 @@ for line in lines[jobs_index + 1 :]: if line.strip(): inside_permissions = False -if status_read_jobs != ["strix"]: +if status_read_jobs and status_read_jobs != ["strix"]: print( - "Strix workflow must scope statuses: read only to the strix scan job; found: " - + (", ".join(status_read_jobs) if status_read_jobs else "none"), + "Strix workflow must scope statuses: read only to the strix scan job when read is used; found: " + + ", ".join(status_read_jobs), file=sys.stderr, ) raise SystemExit(1) -if status_write_jobs: +if status_write_jobs and status_write_jobs != ["strix"]: print( - "Strix workflow must not grant GITHUB_TOKEN statuses: write; found: " + "Strix workflow must scope statuses: write only to the strix scan job when write is used; found: " + ", ".join(status_write_jobs), file=sys.stderr, ) raise SystemExit(1) +if status_read_jobs and status_write_jobs: + print( + "Strix workflow must choose statuses: read or statuses: write for the strix scan job, not both.", + file=sys.stderr, + ) + raise SystemExit(1) +if not status_read_jobs and not status_write_jobs: + print( + "Strix workflow must scope statuses: read or statuses: write to the strix scan job; found: none", + file=sys.stderr, + ) + raise SystemExit(1) PY )"; then record_failure "$output" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 1c67ba467..c3e6e3ce1 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -97,12 +97,14 @@ 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" 'strix-${{ github.event_name }}-' "strix workflow isolates manual evidence runs from required PR contexts" 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" "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_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" "manual workflow_dispatch evidence cannot cancel" "strix workflow documents manual evidence isolation from branch protection contexts" 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" @@ -1161,9 +1163,6 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_not_contains "$workflow_file" 'review_dispatch_limit="0"' "scheduler must not silently suppress eligible review dispatches on base-branch push events" assert_file_contains "$workflow_file" "--review-dispatch-limit" "scheduler passes the dispatch budget to the canonical script" assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ github.token }}' "scheduler uses the caller workflow token so mutations are attributed to GitHub Actions in the target repository" - assert_file_contains "$workflow_file" "Resolve trusted scheduler source ref" "scheduler required workflow resolves the central trusted source ref" - assert_file_contains "$workflow_file" "workflow_ref" "scheduler required workflow can reuse the required-workflow source ref" - assert_file_contains "$workflow_file" "workflow_sha" "scheduler trusted source ref prefers the immutable workflow commit when available" assert_file_not_contains "$workflow_file" "INPUT_CANONICAL_REF" "scheduler trusted source checkout must not be controlled by workflow input" assert_file_not_contains "$workflow_file" "inputs.canonical_ref" "scheduler no longer accepts checkout-ref override input" assert_file_contains "$workflow_file" "Materialize trusted scheduler" "scheduler materializes the trusted central implementation without privileged checkout" diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index b47fae1bf..f09581c8b 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -7,6 +7,10 @@ from scripts.ci import noema_review_gate as noema +def fake_secret(*parts: str) -> str: + return "".join(parts) + + def make_pr(**overrides): """Build a minimal pull request payload for Noema tests.""" value = { @@ -48,8 +52,8 @@ def test_scrub_sensitive_data(): assert noema.scrub_sensitive_data("ok") == "ok" assert noema.scrub_sensitive_data("Bearer abcdef123") == "Bearer ***" assert noema.scrub_sensitive_data("TOKEN xyz_987") == "TOKEN ***" - assert noema.scrub_sensitive_data("github_pat_123456789") == "***" - assert noema.scrub_sensitive_data("ghp_12345") == "***" + assert noema.scrub_sensitive_data(fake_secret("github_", "pat_", "123456789")) == "***" + assert noema.scrub_sensitive_data(fake_secret("gh", "p_", "12345")) == "***" assert noema.scrub_sensitive_data("sk-abc-123_456") == "***" assert noema.scrub_sensitive_data("xoxb-1234-5678") == "***" assert noema.scrub_sensitive_data("AKIA1234567890ABCDEF") == "***" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 9e0079ff9..2f7099876 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -61,7 +61,7 @@ def test_strix_cancels_superseded_pr_head_security_evidence() -> None: assert "github.event.pull_request.base.repo.full_name" in concurrency_contract assert "github.repository" in concurrency_contract assert ( - "strix-${{ github.event.inputs.target_repository || " + "strix-${{ github.event_name }}-${{ github.event.inputs.target_repository || " "github.event.pull_request.base.repo.full_name || github.repository }}" ) in concurrency_contract assert "format('pr-{0}', github.event.pull_request.number)" in concurrency_contract @@ -70,6 +70,7 @@ def test_strix_cancels_superseded_pr_head_security_evidence() -> None: 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 "manual workflow_dispatch evidence cannot cancel" 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 @@ -169,10 +170,10 @@ def test_noema_workflow_run_without_pull_request_skips_before_token_exchange() - assert "Noema review skipped: no pull request number is associated with this event." in workflow assert "if: env.PR_NUMBER == ''" in workflow - assert workflow.count("if: env.PR_NUMBER != ''") >= 4 + assert workflow.count("if: env.PR_NUMBER != ''") >= 3 -def test_noema_and_scheduler_trusted_checkouts_use_workflow_sha() -> None: +def test_noema_and_scheduler_trusted_checkouts_use_static_main() -> None: noema = workflow_text("noema-review.yml") scheduler = workflow_text("pr-review-merge-scheduler.yml")