diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7d7f325c1..e45283778 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -96,7 +96,7 @@ jobs: - name: Materialize pull request merge tree for coverage measurement env: - GH_TOKEN: ${{ github.event_name == 'workflow_dispatch' && (secrets.OPENCODE_APPROVE_TOKEN || github.token) || github.token }} + GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} @@ -105,11 +105,16 @@ jobs: set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" rm -rf "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::Coverage merge tree materialization requires a GitHub token." + exit 1 + fi + auth_header="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" git init "$fetch_dir" git -C "$fetch_dir" remote add origin "${GITHUB_SERVER_URL}/${TARGET_REPOSITORY}.git" - auth_header="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" if ! git -C "$fetch_dir" \ - -c "http.${GITHUB_SERVER_URL}/.extraheader=AUTHORIZATION: basic ${auth_header}" \ + -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"; then echo "::error::Coverage fetch could not authenticate to ${TARGET_REPOSITORY} or read base/head SHAs ${PR_BASE_SHA}/${PR_HEAD_SHA}; check token permissions, target repository access, and SHA visibility." exit 1 @@ -2879,6 +2884,12 @@ jobs: fi } + gh_error_is_rate_limited() { + local error_file="$1" + [ -s "$error_file" ] || return 1 + grep -Eiq '(API rate limit exceeded|rate limit exceeded|secondary rate limit)' "$error_file" + } + emit_change_flow_mermaid_graph() { local merge_state="${1:-UNKNOWN}" local changed_files_file surfaces_file idx next_node @@ -4225,7 +4236,7 @@ jobs: | group_by(.name // "") | map(last) | .[]? - | select((.name // "") != "opencode-review") + | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence"] | index($n)) | not) | select((.status // "") == "completed") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) @@ -4238,7 +4249,7 @@ jobs: | group_by(.name // "") | map(last) | .[]? - | select((.name // "") != "opencode-review") + | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence"] | index($n)) | not) | select((.status // "") != "completed") | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.status // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) ' diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c2a1948c4..ce2dcc811 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -74,6 +74,7 @@ jobs: --format=json --output=old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 + --allow-no-lockfiles -r ./ - name: Explain base OSV resolver fallback @@ -88,6 +89,7 @@ jobs: --format=json --output=old-results.json --no-resolve + --allow-no-lockfiles -r ./ - name: Checkout head @@ -107,6 +109,7 @@ jobs: --format=json --output=new-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 + --allow-no-lockfiles -r ./ - name: Explain head OSV resolver fallback @@ -121,6 +124,7 @@ jobs: --format=json --output=new-results.json --no-resolve + --allow-no-lockfiles -r ./ - name: Require OSV scan output diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b29772934..3aa14d57c 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -650,6 +650,17 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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" assert_file_not_contains "$workflow_file" "Exchange OpenCode app token for target repository coverage reads" "coverage evidence must not expose OIDC to PR-head test execution" + local coverage_merge_tree_step + coverage_merge_tree_step="$( + awk ' + /^[[:space:]]*- name: Materialize pull request merge tree for coverage measurement/ { in_step = 1 } + in_step { print } + in_step && /^[[:space:]]*- name:/ && $0 !~ /Materialize pull request merge tree for coverage measurement/ { exit } + ' "$workflow_file" + )" + if [[ "$coverage_merge_tree_step" != *'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}'* ]]; then + record_failure "opencode coverage merge-tree fetch must use OPENCODE_APPROVE_TOKEN before github.token for target repository reads" + fi assert_file_contains "$workflow_file" 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' "coverage evidence fetches exact base and head commits as data" assert_file_contains "$workflow_file" 'merge --no-ff --no-edit "$PR_HEAD_SHA"' "coverage evidence materializes the current pull request merge tree without action checkout" assert_file_contains "$workflow_file" "Coverage merge tree could not be materialized" "coverage evidence logs an actionable merge-tree failure reason" @@ -828,6 +839,21 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'warn_gh_publication_failure "pull review with fallback review token"' "opencode approval explains fallback review publication failures" assert_file_contains "$workflow_file" 'gh_error_is_rate_limited()' "opencode approval detects rate-limited publication failures" assert_file_contains "$workflow_file" '[ "$event" = "APPROVE" ] && gh_error_is_rate_limited "$gh_error_file"' "opencode approval only soft-fails rate-limited approve publication failures" + local approval_step + local helper_line + local approve_call_line + approval_step="$( + awk ' + /^[[:space:]]*- name: Approve PR if OpenCode review passed/ { in_step = 1 } + in_step { print } + in_step && /^[[:space:]]*- name: Run merge scheduler after approval/ { exit } + ' "$workflow_file" + )" + helper_line="$(grep -nF 'gh_error_is_rate_limited()' <<<"$approval_step" | head -n 1 | cut -d: -f1 || true)" + approve_call_line="$(grep -nF '[ "$event" = "APPROVE" ] && gh_error_is_rate_limited "$gh_error_file"' <<<"$approval_step" | head -n 1 | cut -d: -f1 || true)" + if [ -z "$helper_line" ] || [ -z "$approve_call_line" ] || [ "$helper_line" -ge "$approve_call_line" ]; then + record_failure "opencode approval step must define gh_error_is_rate_limited before create_pull_review uses it" + fi assert_file_contains "$workflow_file" 'OpenCode could not publish the APPROVE pull review for head %s because the GitHub API rate limit was exceeded' "opencode approval keeps successful gate results for rate-limited approval review publication" assert_file_contains "$workflow_file" 'OpenCode could not publish the pull review for head %s, so the review state was not changed.' "opencode approval fails when review publication fails" assert_file_contains "$workflow_file" 'warn_gh_publication_failure "review overview comment"' "opencode approval soft-fails permission-denied overview publication" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 7e11baccd..23479c6e7 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -158,10 +158,12 @@ def test_opencode_target_coverage_materializes_merge_tree_without_checkout_actio assert "uses: actions/checkout" not in step assert "refs/pull/${{ github.event.pull_request.number }}/merge" not in step assert "TARGET_REPOSITORY:" in step - assert "x-access-token:%s" in step - assert "AUTHORIZATION: basic ${auth_header}" in step - assert "AUTHORIZATION: bearer" not in step + assert 'printf \'x-access-token:%s\' "$GH_TOKEN" | base64 | tr -d \'\\n\'' in step + assert "echo \"::add-mask::$auth_header\"" in step + assert '-c http.extraheader="AUTHORIZATION: basic ${auth_header}"' in step assert 'http."${GITHUB_SERVER_URL}/".extraheader' not in step + assert 'AUTHORIZATION: bearer ${GH_TOKEN}' not in step + assert "AUTHORIZATION: bearer" not in step assert 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' in step assert "Coverage fetch could not authenticate" in step assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step @@ -372,6 +374,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) + assert workflow.count('["opencode-review","coverage-evidence"]') >= 2 assert "falling back to current-head REST check-runs" in workflow strix_workflow = Path(".github/workflows/strix.yml").read_text(encoding="utf-8") diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 71495cde0..e132e25bc 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -98,6 +98,16 @@ def test_security_scan_skips_dependency_review_when_dependency_graph_is_unavaila assert "steps.dependency_review_support.outputs.supported == 'true'" in workflow +def test_security_scan_allows_repositories_without_supported_lockfiles() -> None: + workflow = workflow_text("security-scan.yml") + + assert workflow.count("--allow-no-lockfiles") == 4 + assert "--output=old-results.json" in workflow + assert "--output=new-results.json" in workflow + assert "test -s old-results.json" in workflow + assert "test -s new-results.json" in workflow + + def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_failure() -> None: workflow = workflow_text("security-scan.yml")