Strix Security Scan ContextualWisdomLab/fast-mlsirm#1779@d336fd398cbe35bc68976353f7a124b71765a653 #7626
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Strix Security Scan | |
| run-name: >- | |
| Strix Security Scan ${{ github.event.client_payload.target_repository || | |
| github.event.pull_request.base.repo.full_name || github.repository }}#${{ | |
| github.event.client_payload.pr_number || github.event.pull_request.number || 'event' }}@${{ | |
| github.event.client_payload.pr_head_sha || github.event.pull_request.head.sha || github.sha }} | |
| on: | |
| push: | |
| branches: [main, develop, master] | |
| # Skip scans for changes that touch ONLY non-executable documentation and | |
| # image assets. A change whose entire diff is these paths has no source, | |
| # build, config, or workflow logic for a code security scanner to analyze, | |
| # so skipping it loses no coverage while freeing shared runner capacity. | |
| # Conservative by design: only file EXTENSIONS/paths that can never contain | |
| # executable logic are listed (no source, no *.txt, no *.svg, no CODEOWNERS, | |
| # no build scripts). A diff touching even one non-listed file still scans. | |
| # The weekly full-tree schedule below re-scans protected branches with no | |
| # path filter, backstopping every path. | |
| # This filter is only evaluated for natively-triggered runs. Repositories | |
| # covered by org ruleset 18156473 have every 'on:' filter ignored; the | |
| # job-level gate below is what skips them. | |
| paths-ignore: | |
| - '**/*.md' | |
| - '**/*.markdown' | |
| - '**/*.rst' | |
| - '**/*.png' | |
| - '**/*.jpg' | |
| - '**/*.jpeg' | |
| - '**/*.gif' | |
| - '**/*.webp' | |
| - '**/*.bmp' | |
| - '**/*.ico' | |
| - 'LICENSE' | |
| - 'LICENSE.*' | |
| - 'COPYING' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| pull_request_target: | |
| types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] | |
| # Same conservative doc/image-only skip for PR scans. GitHub evaluates these | |
| # path filters only for natively-triggered runs -- i.e. in the three | |
| # repositories ruleset 18156473 excludes (.github, noema, | |
| # IRT-bibliography-set). In every other repository the ruleset ignores | |
| # them, so the same doc/image-only decision is enforced by the | |
| # changed-scope job below. The run-name | |
| # includes the PR number and head SHA for status grouping, while the | |
| # concurrency group is scoped per repository and event class to prevent | |
| # shared-provider key rate-limit storms. Strix runs intentionally do not | |
| # cancel in progress because a pre-job cancellation leaves no scanner log to | |
| # review. GitHub keeps one active and one pending run per group; the merge | |
| # scheduler re-dispatches exact-head evidence when a pending run is | |
| # superseded. For PRs the merge scheduler manages, same-head Strix evidence | |
| # is still forced at merge time via repository_dispatch (which paths-ignore | |
| # does not affect), so merged code never loses evidence. | |
| paths-ignore: | |
| - '**/*.md' | |
| - '**/*.markdown' | |
| - '**/*.rst' | |
| - '**/*.png' | |
| - '**/*.jpg' | |
| - '**/*.jpeg' | |
| - '**/*.gif' | |
| - '**/*.webp' | |
| - '**/*.bmp' | |
| - '**/*.ico' | |
| - 'LICENSE' | |
| - 'LICENSE.*' | |
| - 'COPYING' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| schedule: | |
| # Weekly scan on protected branches (Mondays at 03:00 UTC). | |
| - cron: '0 3 * * 1' | |
| # Default-branch-only retry entrypoint; no caller-selected workflow ref. | |
| repository_dispatch: | |
| types: [strix-scan] | |
| concurrency: | |
| # Workflow-level admission is required: job-level groups are never evaluated | |
| # while the whole run is queued behind the organization job ceiling. | |
| group: >- | |
| strix-security-scan-${{ | |
| github.event.pull_request.base.repo.full_name || | |
| github.event.client_payload.target_repository || github.repository }}-${{ | |
| github.event.pull_request.number || | |
| github.event.client_payload.pr_number || github.run_id }} | |
| cancel-in-progress: true | |
| # Scorecard Token-Permissions (alert #43): keep the workflow-level token | |
| # read-only and scope same-repo status publication to the Strix scan job. | |
| permissions: | |
| actions: read | |
| contents: read | |
| models: read | |
| jobs: | |
| changed-scope: | |
| name: Detect changed scope | |
| # The org ruleset IGNORES every `on:` filter (paths, branches, types) when it | |
| # runs this workflow in another repository, and a trigger-level skip would | |
| # leave `.github`'s classic required contexts Pending forever. Both | |
| # mechanisms honour a JOB-level skip, so the doc/image-only decision is made | |
| # here and consumed through `needs`. See | |
| # docs/doctoring/required-workflow-path-filter-boundary.md. | |
| # Fails OPEN: an unreadable, empty, or truncated file list scans everything. | |
| if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| code: ${{ steps.scope.outputs.code }} | |
| deps: ${{ steps.scope.outputs.deps }} | |
| steps: | |
| - name: Classify changed paths | |
| id: scope | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| PR: ${{ github.event.pull_request.number }} | |
| EXPECTED_FILES: ${{ github.event.pull_request.changed_files }} | |
| shell: bash | |
| run: | | |
| set -uo pipefail | |
| code=true | |
| deps=true | |
| if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then | |
| changed="" | |
| for attempt in 1 2 3; do | |
| if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then | |
| break | |
| fi | |
| changed="" | |
| sleep $((attempt * 3)) | |
| done | |
| # GitHub caps /pulls/N/files at 3000 entries; a short list would hide | |
| # source files behind a doc-only verdict, so require an exact count. | |
| if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then | |
| code=false | |
| deps=false | |
| while IFS= read -r changed_path; do | |
| case "$changed_path" in | |
| *.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;; | |
| *) code=true ;; | |
| esac | |
| case "$changed_path" in | |
| requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;; | |
| esac | |
| done <<<"$changed" | |
| else | |
| echo "::notice::changed-scope could not read a complete PR file list; scanning everything." | |
| fi | |
| fi | |
| echo "code=${code}" >> "$GITHUB_OUTPUT" | |
| echo "deps=${deps}" >> "$GITHUB_OUTPUT" | |
| echo "changed-scope code=${code} deps=${deps}" | |
| admit-current-head: | |
| name: Admit current pull request head | |
| if: >- | |
| github.event_name != 'pull_request_target' || | |
| (github.event.action != 'closed' && github.event.action != 'converted_to_draft') | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| admitted: ${{ steps.admission.outputs.admitted }} | |
| target_repository: ${{ steps.admission.outputs.target_repository }} | |
| pr_number: ${{ steps.admission.outputs.pr_number }} | |
| steps: | |
| - name: Verify event metadata against the live pull request | |
| id: admission | |
| env: | |
| GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }} | |
| TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number || github.event.pull_request.number }} | |
| EXPECTED_BASE_REF: ${{ github.event.client_payload.pr_base_ref || github.event.pull_request.base.ref }} | |
| EXPECTED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || github.event.pull_request.base.sha }} | |
| EXPECTED_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.event.client_payload.target_repository }} | |
| EXPECTED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || github.event.pull_request.head.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| printf 'admitted=false\n' >> "$GITHUB_OUTPUT" | |
| if [ "$EVENT_NAME" != "pull_request_target" ] && [ "$EVENT_NAME" != "repository_dispatch" ]; then | |
| { | |
| echo "admitted=true" | |
| echo "target_repository=${TARGET_REPOSITORY}" | |
| echo "pr_number=${GITHUB_RUN_ID}" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ ! "$TARGET_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || | |
| [[ ! "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || | |
| [[ ! "$EXPECTED_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || | |
| [[ ! "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::Strix event metadata is incomplete or malformed." | |
| exit 1 | |
| fi | |
| pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")" | |
| live_tuple="$(jq -r '[.state // "", .base.repo.full_name // "", .base.ref // "", .base.sha // "", .head.repo.full_name // "", .head.sha // ""] | @tsv' <<<"$pull_request_json")" | |
| expected_tuple="$(printf 'open\t%s\t%s\t%s\t%s\t%s' "$TARGET_REPOSITORY" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "$EXPECTED_HEAD_REPOSITORY" "$EXPECTED_HEAD_SHA")" | |
| if [ "$live_tuple" != "$expected_tuple" ]; then | |
| echo "::notice::Strix event does not match the live pull request head; skipping stale evidence." | |
| exit 0 | |
| fi | |
| { | |
| echo "admitted=true" | |
| echo "target_repository=${TARGET_REPOSITORY}" | |
| echo "pr_number=${TARGET_PR_NUMBER}" | |
| } >> "$GITHUB_OUTPUT" | |
| cancel-superseded-pr-runs: | |
| if: >- | |
| github.event_name == 'pull_request_target' && | |
| (github.event.action == 'synchronize' || github.event.action == 'converted_to_draft' || github.event.action == 'closed') | |
| # Idempotent per PR: a fresh sweep re-verifies live state (live_target_matches | |
| # below) before selecting or cancelling anything, so it fully subsumes | |
| # whatever an older, not-yet-run instance would have done. cancel-in-progress | |
| # true is the right shape here (the merge scheduler's integrated exact-head | |
| # coalescer instead uses its own admission-order queueing, since each instance | |
| # carries a DIFFERENT specific expected-head only it can act on): it caps | |
| # this job to one running + one queued per PR instead of letting a push | |
| # burst pile up N independent, mutually-non-deduped sweeps that each cost a | |
| # full admission slot under the shared 60-job ceiling. Matches | |
| # codeql-pr.yml's established group-key style (PR-number scoped). | |
| concurrency: | |
| group: >- | |
| cancel-superseded-pr-runs-${{ | |
| github.event.pull_request.base.repo.full_name || github.repository }}-${{ | |
| github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| runs-on: ubuntu-24.04 | |
| # Bound this gh-api-only cleanup job so a stuck call (rate limit, hung | |
| # `gh api --paginate`) cannot silently occupy a runner for GitHub's | |
| # 360-minute platform default -- exactly the window when a busy PR is | |
| # producing the superseded runs this job exists to retire. Matches | |
| # the merge scheduler's bounded run-cleanup shape (gh-api-only, no provider | |
| # inference). | |
| timeout-minutes: 10 | |
| # Prefer the established scheduler credential, but let the close event use | |
| # its job-scoped token so abandoned scans are cancelled even when that | |
| # optional secret is unavailable. This job never checks out PR code. | |
| permissions: | |
| actions: write | |
| contents: read | |
| pull-requests: read | |
| env: | |
| GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| TARGET_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_ACTION: ${{ github.event.action }} | |
| CURRENT_RUN_ID: ${{ github.run_id }} | |
| steps: | |
| - name: Cancel queued and running scans for superseded or inactive pull requests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| live_target_matches() { | |
| local live_pr_json live_state live_draft live_head | |
| if ! live_pr_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}" 2>/tmp/strix-cleanup-gh-error)"; then | |
| echo "::warning::Strix cleanup could not verify the live pull request; leaving runs unchanged." | |
| sed 's/^/ /' /tmp/strix-cleanup-gh-error >&2 || true | |
| return 1 | |
| fi | |
| live_state="$(jq -r '.state // ""' <<<"$live_pr_json")" | |
| live_draft="$(jq -r '.draft // false' <<<"$live_pr_json")" | |
| live_head="$(jq -r '.head.sha // ""' <<<"$live_pr_json")" | |
| [ "$live_head" = "$TARGET_PR_HEAD_SHA" ] && { | |
| { [ "$PR_ACTION" = "closed" ] && [ "$live_state" = "closed" ]; } || | |
| { [ "$PR_ACTION" = "converted_to_draft" ] && [ "$live_state" = "open" ] && [ "$live_draft" = "true" ]; } || | |
| { [ "$PR_ACTION" = "synchronize" ] && [ "$live_state" = "open" ]; } | |
| } | |
| } | |
| cancel_runs() { | |
| local status="$1" | |
| if ! live_target_matches; then | |
| echo "::notice::Strix cleanup target changed before run selection; leaving runs unchanged." | |
| return 0 | |
| fi | |
| local runs_url="repos/${TARGET_REPOSITORY}/actions/runs?status=${status}&per_page=100" | |
| local runs_json | |
| if ! runs_json="$(gh api --paginate "$runs_url" 2>/tmp/strix-cleanup-gh-error)"; then | |
| echo "::warning::Strix cleanup could not inspect ${TARGET_REPOSITORY}; leaving runs unchanged." | |
| sed 's/^/ /' /tmp/strix-cleanup-gh-error >&2 || true | |
| return 0 | |
| fi | |
| local run_ids | |
| if ! run_ids="$(jq -r --arg pr "$TARGET_PR_NUMBER" --arg head_sha "$TARGET_PR_HEAD_SHA" \ | |
| --arg action "$PR_ACTION" --arg repo "$TARGET_REPOSITORY" --arg current "$CURRENT_RUN_ID" ' | |
| .workflow_runs[] | |
| | select((.id | tostring) != $current) | |
| | select(.name == "Strix Security Scan") | |
| | select(.event == "pull_request_target") | |
| | ((.display_title // "") | startswith("Strix Security Scan " + $repo + "#" + $pr + "@")) as $title_matches | |
| | ((.pull_requests // []) | any((.number | tostring) == $pr)) as $metadata_matches | |
| | select($title_matches or $metadata_matches) | |
| | ((.display_title // "") | endswith("@" + $head_sha)) as $title_is_current | |
| | ((.pull_requests // []) | any( | |
| ((.number | tostring) == $pr) | |
| and ((.head.sha // "") | ascii_downcase) == ($head_sha | ascii_downcase) | |
| )) as $metadata_is_current | |
| | ((.pull_requests // []) | any( | |
| ((.number | tostring) == $pr) and ((.head.sha // "") != "") | |
| )) as $metadata_has_head | |
| | select( | |
| $action == "closed" | |
| or $action == "converted_to_draft" | |
| or (($title_matches or $metadata_has_head) and (($title_is_current or $metadata_is_current) | not)) | |
| ) | |
| | .id | |
| ' <<<"$runs_json")"; then | |
| echo "::warning::Strix cleanup received invalid run data for ${TARGET_REPOSITORY}; leaving runs unchanged." | |
| return 0 | |
| fi | |
| while IFS= read -r run_id; do | |
| [ -n "$run_id" ] || continue | |
| if ! live_target_matches; then | |
| echo "::notice::Strix cleanup target changed before cancellation; leaving runs unchanged." | |
| return 0 | |
| fi | |
| if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>/tmp/strix-cleanup-cancel-error || | |
| gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" >/dev/null 2>>/tmp/strix-cleanup-cancel-error; then | |
| echo "Cancelled obsolete Strix run ${run_id} in ${TARGET_REPOSITORY} for PR #${TARGET_PR_NUMBER}." | |
| else | |
| echo "::warning::Strix cleanup could not cancel run ${run_id} in ${TARGET_REPOSITORY}; it may have finished or the credential lacks Actions write access." | |
| sed 's/^/ /' /tmp/strix-cleanup-cancel-error >&2 || true | |
| fi | |
| done <<<"$run_ids" | |
| } | |
| for active_status in queued in_progress requested waiting pending; do | |
| cancel_runs "$active_status" | |
| done | |
| strix: | |
| needs: [changed-scope, admit-current-head] | |
| if: needs.changed-scope.outputs.code == 'true' && needs.admit-current-head.outputs.admitted == 'true' | |
| # Large, actively-growing repositories (e.g. contextual-orchestrator) can | |
| # legitimately require well over two hours to scan -- this org's own | |
| # standing operating directive accepts that central OpenCode/Strix/Noema | |
| # scans may take more than two hours per model (docs/product-goal-directive.md). | |
| # Inference has no wall-clock deadline; cancellation is reserved for an | |
| # explicit operator action or a superseded head. | |
| runs-on: ubuntu-24.04 | |
| # Least-privilege token scoped to this job (Scorecard alert #43): the scan | |
| # exchanges an OIDC token (id-token) and publishes same-repo status evidence | |
| # from the scan job only. | |
| permissions: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| models: read | |
| statuses: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| steps: | |
| - name: Harden runner | |
| uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 | |
| with: | |
| egress-policy: audit | |
| disable-file-monitoring: true | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Resolve trusted Strix source ref | |
| id: trusted_source | |
| env: | |
| JOB_CONTEXT_JSON: ${{ toJSON(job) }} | |
| GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' >>"$GITHUB_OUTPUT" | |
| import json | |
| import os | |
| import re | |
| import sys | |
| try: | |
| job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") | |
| github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") | |
| except json.JSONDecodeError as exc: | |
| print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) | |
| raise SystemExit(1) | |
| trusted_repository = str( | |
| job_context.get("workflow_repository") or "ContextualWisdomLab/.github" | |
| ).strip() | |
| trusted_ref = str( | |
| job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" | |
| ).strip() | |
| workflow_ref = str( | |
| job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" | |
| ).strip() | |
| if not trusted_ref: | |
| trusted_ref = "main" | |
| prefix = "ContextualWisdomLab/.github/.github/workflows/strix.yml@" | |
| if workflow_ref.startswith(prefix): | |
| trusted_ref = workflow_ref.split("@", 1)[1] | |
| if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", trusted_repository): | |
| print("::error::Trusted workflow repository resolved to an invalid name.", file=sys.stderr) | |
| raise SystemExit(1) | |
| if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): | |
| print("::error::Trusted workflow ref resolved to an invalid value.", file=sys.stderr) | |
| raise SystemExit(1) | |
| print(f"repository={trusted_repository}") | |
| print(f"ref={trusted_ref}") | |
| PY | |
| - name: Checkout trusted Strix source | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: ${{ steps.trusted_source.outputs.repository }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| ref: ${{ steps.trusted_source.outputs.ref }} | |
| path: trusted-strix-source | |
| - name: Export trusted Strix source paths | |
| run: | | |
| set -euo pipefail | |
| trusted_strix_source="$GITHUB_WORKSPACE/trusted-strix-source" | |
| test -f "$trusted_strix_source/scripts/ci/strix_quick_gate.sh" | |
| test -f "$trusted_strix_source/scripts/ci/test_strix_quick_gate.sh" | |
| test -f "$trusted_strix_source/scripts/ci/strix_required_workflow_smoke.sh" | |
| { | |
| echo "TRUSTED_STRIX_SOURCE=$trusted_strix_source" | |
| echo "TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh" | |
| echo "TRUSTED_STRIX_GATE_TEST=$trusted_strix_source/scripts/ci/test_strix_quick_gate.sh" | |
| echo "TRUSTED_STRIX_REQUIRED_SMOKE=$trusted_strix_source/scripts/ci/strix_required_workflow_smoke.sh" | |
| } >> "$GITHUB_ENV" | |
| - name: Exchange OpenCode app token for target repository reads | |
| id: target_app_token | |
| env: | |
| OIDC_AUDIENCE: opencode-github-action | |
| OPENCODE_API_BASE_URL: https://api.opencode.ai | |
| run: | | |
| set -euo pipefail | |
| mark_unavailable() { | |
| echo "available=false" >>"$GITHUB_OUTPUT" | |
| } | |
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | |
| echo "OpenCode app token exchange unavailable: OIDC request environment is missing." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" | |
| separator="&" | |
| case "$request_url" in | |
| *\?*) ;; | |
| *) separator="?" ;; | |
| esac | |
| if ! oidc_response="$( | |
| curl -fsS \ | |
| -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${request_url}${separator}audience=${OIDC_AUDIENCE}" | |
| )"; then | |
| echo "OpenCode app token exchange unavailable: OIDC token request did not complete." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" | |
| if [ -z "$oidc_token" ]; then | |
| echo "OpenCode app token exchange unavailable: OIDC token response was empty." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| if ! token_response="$( | |
| curl -fsS \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${oidc_token}" \ | |
| "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | |
| )"; then | |
| echo "OpenCode app token exchange unavailable: app token request did not complete." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| app_token="$(jq -r '.token // empty' <<<"$token_response")" | |
| if [ -z "$app_token" ]; then | |
| echo "OpenCode app token exchange unavailable: app token response was empty." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| echo "::add-mask::$app_token" | |
| { | |
| echo "available=true" | |
| echo "token=$app_token" | |
| } >>"$GITHUB_OUTPUT" | |
| - name: Resolve target repository visibility | |
| id: target_visibility | |
| env: | |
| GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }} | |
| EVENT_REPOSITORY_VISIBILITY: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.visibility || github.event_name != 'repository_dispatch' && github.event.repository.visibility || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]]; then | |
| echo "::error::Strix target repository must belong to ContextualWisdomLab." | |
| exit 1 | |
| fi | |
| case "$EVENT_REPOSITORY_VISIBILITY" in | |
| PUBLIC | public) is_private=false ;; | |
| PRIVATE | private | INTERNAL | internal) is_private=true ;; | |
| "") | |
| is_private="" | |
| for target_visibility_attempt in 1 2 3 4 5 6; do | |
| # The single-quoted jq program intentionally expands jq's | |
| # `$visibility`, not a shell variable (ShellCheck SC2016). | |
| # shellcheck disable=SC2016 | |
| if is_private="$( | |
| gh api "repos/${TARGET_REPOSITORY}" --jq ' | |
| (.visibility // "" | ascii_downcase) as $visibility | |
| | if $visibility == "public" then "false" | |
| elif $visibility == "private" or $visibility == "internal" then "true" | |
| else empty | |
| end | |
| ' | |
| )"; then | |
| break | |
| fi | |
| is_private="" | |
| if [ "$target_visibility_attempt" -lt 6 ]; then | |
| echo "Repository visibility lookup failed (attempt ${target_visibility_attempt}/6), possibly a transient GitHub API rate limit; retrying after backoff." >&2 | |
| sleep "$(( target_visibility_attempt * 5 ))" | |
| fi | |
| done | |
| ;; | |
| *) | |
| echo "::error::Target repository event visibility was not public, private, or internal." | |
| exit 1 | |
| ;; | |
| esac | |
| case "$is_private" in | |
| true | false) ;; | |
| *) | |
| echo "::error::Target repository visibility did not resolve to true or false after retries." | |
| exit 1 | |
| ;; | |
| esac | |
| echo "is_private=$is_private" >>"$GITHUB_OUTPUT" | |
| - name: Materialize target workspace | |
| if: github.event_name != 'repository_dispatch' | |
| env: | |
| GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| TARGET_WORKSPACE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }} | |
| run: | | |
| set -euo pipefail | |
| trusted_workspace="$RUNNER_TEMP/trusted-workspace" | |
| mkdir -p "$trusted_workspace" | |
| git init -q "$trusted_workspace" | |
| gh auth setup-git | |
| git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git" | |
| git -C "$trusted_workspace" fetch --no-tags --depth=1 origin "$TARGET_WORKSPACE_SHA" | |
| git -C "$trusted_workspace" checkout --detach --quiet "$TARGET_WORKSPACE_SHA" | |
| git -C "$trusted_workspace" cat-file -e "$TARGET_WORKSPACE_SHA^{commit}" | |
| echo "TRUSTED_WORKSPACE=$trusted_workspace" >> "$GITHUB_ENV" | |
| - name: Validate repository dispatch against live pull request metadata | |
| if: github.event_name == 'repository_dispatch' | |
| env: | |
| GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| REPOSITORY: ${{ github.event.client_payload.target_repository }} | |
| PR_NUMBER: ${{ github.event.client_payload.pr_number }} | |
| SUPPLIED_BASE_REF: ${{ github.event.client_payload.pr_base_ref }} | |
| SUPPLIED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha }} | |
| SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || | |
| ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || | |
| ! [[ "$SUPPLIED_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || | |
| ! [[ "$SUPPLIED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || | |
| [ -z "$SUPPLIED_BASE_REF" ]; then | |
| echo "::error::repository_dispatch Strix metadata is incomplete or malformed." | |
| exit 1 | |
| fi | |
| pull_request_json="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" | |
| live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" | |
| live_base_repository="$(jq -r '.base.repo.full_name // empty' <<<"$pull_request_json")" | |
| live_head_repository="$(jq -r '.head.repo.full_name // empty' <<<"$pull_request_json")" | |
| live_base_ref="$(jq -r '.base.ref // empty' <<<"$pull_request_json")" | |
| live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")" | |
| live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")" | |
| if [ "$live_state" != "open" ] || | |
| [ "$live_base_repository" != "$REPOSITORY" ] || | |
| [ "$live_head_repository" != "$REPOSITORY" ] || | |
| [ "$live_base_ref" != "$SUPPLIED_BASE_REF" ] || | |
| [ "$live_base_sha" != "$SUPPLIED_BASE_SHA" ] || | |
| [ "$live_head_sha" != "$SUPPLIED_HEAD_SHA" ]; then | |
| printf '::error::repository_dispatch Strix metadata does not match live PR %s#%s. supplied base=%s/%s head=%s; live state=%s base_repo=%s base=%s/%s head_repo=%s head=%s.\n' \ | |
| "$REPOSITORY" "$PR_NUMBER" "$SUPPLIED_BASE_REF" "$SUPPLIED_BASE_SHA" "$SUPPLIED_HEAD_SHA" \ | |
| "${live_state:-missing}" "${live_base_repository:-missing}" "${live_base_ref:-missing}" "${live_base_sha:-missing}" \ | |
| "${live_head_repository:-missing}" "${live_head_sha:-missing}" | |
| exit 1 | |
| fi | |
| trusted_workspace="$RUNNER_TEMP/trusted-workspace" | |
| mkdir -p "$trusted_workspace" | |
| git init -q "$trusted_workspace" | |
| gh auth setup-git | |
| git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git" | |
| git -C "$trusted_workspace" fetch --no-tags --depth=1 origin "$live_base_sha" | |
| git -C "$trusted_workspace" checkout --detach --quiet "$live_base_sha" | |
| git -C "$trusted_workspace" cat-file -e "$live_base_sha^{commit}" | |
| echo "TRUSTED_WORKSPACE=$trusted_workspace" >> "$GITHUB_ENV" | |
| - name: Fetch pull request head for trusted scan | |
| if: github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '' | |
| env: | |
| GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} | |
| PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.client_payload.pr_number }} | |
| PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha }} | |
| PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$PR_NUMBER" ] || [ -z "$PR_HEAD_SHA" ]; then | |
| echo "::error::PR number and head SHA are required for trusted PR-scope Strix evidence." | |
| exit 1 | |
| fi | |
| gh auth setup-git | |
| if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::PR head SHA must be a 40-character git SHA." | |
| exit 1 | |
| fi | |
| if [ -n "$PR_BASE_SHA" ] && ! [[ "$PR_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::PR base SHA must be a 40-character git SHA." | |
| exit 1 | |
| fi | |
| if [ -n "$PR_BASE_SHA" ]; then | |
| git -C "$TRUSTED_WORKSPACE" fetch --no-tags --depth=1 origin "$PR_BASE_SHA" | |
| git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_BASE_SHA^{commit}" | |
| fi | |
| # Fetching the expected head SHA directly avoids false failures when | |
| # refs/pull/<n>/head has already advanced before this queued run starts. | |
| if git -C "$TRUSTED_WORKSPACE" fetch --no-tags --depth=1 origin "$PR_HEAD_SHA"; then | |
| git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}" | |
| if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:.github/workflows/strix.yml" 2>/dev/null; then | |
| mkdir -p "$TRUSTED_WORKSPACE/.github/workflows" | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml" | |
| echo "Materialized PR-head Strix workflow for self-test." | |
| fi | |
| if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then | |
| mkdir -p "$TRUSTED_WORKSPACE/scripts/ci" | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py" | |
| fi | |
| git -C "$TRUSTED_WORKSPACE" update-ref "refs/remotes/pull/${PR_NUMBER}/head" "$PR_HEAD_SHA" | |
| exit 0 | |
| fi | |
| for pr_head_fetch_attempt in 1 2 3 4 5 6; do | |
| git -C "$TRUSTED_WORKSPACE" fetch --no-tags --prune origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head" | |
| fetched_head_sha="$(git -C "$TRUSTED_WORKSPACE" rev-parse "refs/remotes/pull/${PR_NUMBER}/head")" | |
| if [ "$fetched_head_sha" = "$PR_HEAD_SHA" ]; then | |
| git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}" | |
| if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:.github/workflows/strix.yml" 2>/dev/null; then | |
| mkdir -p "$TRUSTED_WORKSPACE/.github/workflows" | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml" | |
| echo "Materialized PR-head Strix workflow for self-test." | |
| fi | |
| if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then | |
| mkdir -p "$TRUSTED_WORKSPACE/scripts/ci" | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py" | |
| fi | |
| exit 0 | |
| fi | |
| if [ "$pr_head_fetch_attempt" -lt 6 ]; then | |
| echo "Fetched PR head $fetched_head_sha, expected $PR_HEAD_SHA; retrying after propagation delay." >&2 | |
| sleep 10 | |
| fi | |
| done | |
| echo "::error::PR head ref did not resolve to expected commit $PR_HEAD_SHA after retries." >&2 | |
| exit 1 | |
| - name: Self-test Strix required workflow contract | |
| timeout-minutes: 2 | |
| working-directory: trusted-strix-source | |
| run: | | |
| set -euo pipefail | |
| printf 'Running bounded Strix required-workflow smoke test.\n' | |
| bash "$TRUSTED_STRIX_REQUIRED_SMOKE" | |
| - name: Materialize central Strix dependency lock from PR head | |
| if: >- | |
| github.event_name == 'pull_request_target' | |
| && github.repository == 'ContextualWisdomLab/.github' | |
| && github.event.pull_request.base.repo.full_name == 'ContextualWisdomLab/.github' | |
| && github.event.pull_request.head.repo.full_name == 'ContextualWisdomLab/.github' | |
| env: | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::PR head SHA must be a 40-character git SHA." | |
| exit 1 | |
| fi | |
| if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:requirements-strix-ci-hashes.txt" 2>/dev/null; then | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:requirements-strix-ci-hashes.txt" > "$TRUSTED_STRIX_SOURCE/requirements-strix-ci-hashes.txt" | |
| printf 'Materialized central Strix dependency lock from same-repository PR head.\n' | |
| fi | |
| - name: Gate Strix secrets | |
| id: gate | |
| env: | |
| STRIX_MODEL: contextual-orchestrator/orchestrator/free | |
| STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }} | |
| run: | | |
| requested_model="$(printf '%s' "$STRIX_MODEL_REQUESTED" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| case "$requested_model" in | |
| ""|orchestrator/free|contextual-orchestrator/orchestrator/free) ;; | |
| *) | |
| echo '::error::Strix model overrides are limited to contextual-orchestrator/orchestrator/free.' | |
| exit 1 | |
| ;; | |
| esac | |
| strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| { | |
| echo "strix_model=$strix_model" | |
| echo 'enabled=true' | |
| echo 'provider_mode=contextual_orchestrator' | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Provision contextual-orchestrator Strix sidecar | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} | |
| NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} | |
| NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} | |
| OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.is_private }} | |
| CONTEXTUAL_ORCHESTRATOR_POOL: free | |
| run: | | |
| set -euo pipefail | |
| bash "$TRUSTED_STRIX_SOURCE/scripts/ci/contextual_orchestrator_review_sidecar.sh" | |
| - name: Set up Python | |
| if: steps.gate.outputs.enabled == 'true' | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Install Strix | |
| if: steps.gate.outputs.enabled == 'true' | |
| working-directory: trusted-strix-source | |
| run: | | |
| set -euo pipefail | |
| # GitHub-hosted runners may inherit a collaborative umask (0002), | |
| # which makes pip-generated console scripts group-writable. Pin a | |
| # private install umask before creating the credential-bearing Strix | |
| # entry point; the runtime gate still rejects any later relaxation. | |
| umask 022 | |
| # --no-deps: strix-agent declares cryptography<49, conflicting with this repo's | |
| # cryptography==50.0.0 pin (CVE-2026-39892 fix, see requirements-strix-ci-overrides.txt). | |
| # --require-hashes already pins every package (including transitive deps) to an exact, | |
| # hash-verified version, so skipping pip's redundant declared-range resolution here is | |
| # safe -- verified locally with --dry-run against this exact file before pushing. | |
| python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes --no-deps -r requirements-strix-ci-hashes.txt | |
| strix_executable="$(command -v strix || true)" | |
| if [ -z "$strix_executable" ] || [[ "$strix_executable" != /* ]] \ | |
| || [ ! -f "$strix_executable" ] || [ -L "$strix_executable" ] \ | |
| || [ ! -x "$strix_executable" ]; then | |
| echo "::error::Pinned Strix installation did not produce a trusted absolute executable path." | |
| exit 1 | |
| fi | |
| case "$strix_executable" in | |
| "$GITHUB_WORKSPACE"/*|"$RUNNER_TEMP"/*) | |
| echo "::error::Refusing a Strix executable from a workspace or runner-temp path." | |
| exit 1 | |
| ;; | |
| esac | |
| strix_scripts_root="$(python3 -c 'import sysconfig; print(sysconfig.get_path("scripts"))')" | |
| if [ -z "$strix_scripts_root" ] || [[ "$strix_scripts_root" != /* ]] \ | |
| || [ ! -d "$strix_scripts_root" ] || [ -L "$strix_scripts_root" ]; then | |
| echo "::error::Pinned Strix installation did not produce a trusted absolute scripts root." | |
| exit 1 | |
| fi | |
| case "$strix_executable" in | |
| "$strix_scripts_root"/*) ;; | |
| *) | |
| echo "::error::Pinned Strix executable is outside the trusted scripts root." | |
| exit 1 | |
| ;; | |
| esac | |
| # pip and the hosted tool cache can preserve collaborative write bits | |
| # even after a private install umask. Normalize both the containing | |
| # scripts root and resolved console script before pinning their | |
| # identity; the runtime gate still fails closed on later relaxation. | |
| chmod go-w -- "$strix_scripts_root" "$strix_executable" | |
| strix_executable_sha256="$(python3 - "$strix_executable" <<'PY' | |
| import hashlib | |
| from pathlib import Path | |
| import sys | |
| print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest()) | |
| PY | |
| )" | |
| { | |
| printf 'STRIX_EXECUTABLE_PATH=%s\n' "$strix_executable" | |
| printf 'STRIX_EXECUTABLE_ROOT=%s\n' "$strix_scripts_root" | |
| printf 'STRIX_EXECUTABLE_SHA256=%s\n' "$strix_executable_sha256" | |
| } >> "$GITHUB_ENV" | |
| - name: Mask LLM API key | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PROVIDER_MODE" != "contextual_orchestrator" ]; then | |
| echo '::error::Strix must use the contextual-orchestrator provider.' | |
| exit 1 | |
| fi | |
| source "$TRUSTED_STRIX_SOURCE/scripts/ci/load_contextual_orchestrator_token.sh" | |
| # Sanitize CR/LF before masking to prevent broken ::add-mask:: | |
| # commands and potential workflow command injection. | |
| sanitized="$(printf '%s' "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" | tr -d '\r\n')" | |
| if [ -n "$sanitized" ]; then | |
| echo "::add-mask::${sanitized}" | |
| trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -n "$trimmed" ] && [ "$trimmed" != "$sanitized" ]; then | |
| echo "::add-mask::${trimmed}" | |
| fi | |
| fi | |
| - name: Prepare LLM API key input file | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PROVIDER_MODE" != "contextual_orchestrator" ]; then | |
| echo '::error::Strix must use the contextual-orchestrator provider.' | |
| exit 1 | |
| fi | |
| source "$TRUSTED_STRIX_SOURCE/scripts/ci/load_contextual_orchestrator_token.sh" | |
| sanitized="$(printf '%s' "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" | tr -d '\r\n')" | |
| trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -z "$trimmed" ]; then | |
| echo '::error::CONTEXTUAL_ORCHESTRATOR_TOKEN is required for Strix scans.' | |
| exit 1 | |
| fi | |
| umask 077 | |
| llm_api_key_file="$RUNNER_TEMP/llm_api_key.txt" | |
| printf '%s' "$trimmed" > "$llm_api_key_file" | |
| echo "LLM_API_KEY_FILE=$llm_api_key_file" >> "$GITHUB_ENV" | |
| - name: Prepare contextual-orchestrator API base | |
| if: steps.gate.outputs.provider_mode == 'contextual_orchestrator' | |
| run: | | |
| set -euo pipefail | |
| sidecar_base="${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" | |
| if [ "$sidecar_base" != "http://127.0.0.1:18080" ]; then | |
| echo '::error::Strix sidecar base URL is not the pinned local gateway origin.' | |
| exit 1 | |
| fi | |
| umask 077 | |
| llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt" | |
| printf '%s/v1' "${sidecar_base%/}" > "$llm_api_base_file" | |
| echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" | |
| - name: Prepare Strix model input file | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| STRIX_MODEL: ${{ steps.gate.outputs.strix_model }} | |
| run: | | |
| umask 077 | |
| strix_llm_file="$RUNNER_TEMP/strix_llm.txt" | |
| strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| case "$strix_model" in | |
| orchestrator/free | contextual-orchestrator/orchestrator/free) | |
| printf '%s' 'orchestrator/free' > "$strix_llm_file" | |
| ;; | |
| *) | |
| echo '::error::STRIX_LLM must select contextual-orchestrator/orchestrator/free.' | |
| exit 1 | |
| ;; | |
| esac | |
| echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV" | |
| - name: Run Strix (quick) | |
| if: steps.gate.outputs.enabled == 'true' | |
| # Security invariant for pull_request_target: execute only from the | |
| # trusted base checkout. The gate copies PR-head blobs into an isolated | |
| # temporary scope with execute bits stripped, then scans that scope as | |
| # data. PR evidence uses the __PR_SCOPE__ sentinel so the scanner target | |
| # cannot accidentally remain the trusted base checkout. | |
| working-directory: ${{ runner.temp }}/trusted-workspace | |
| env: | |
| STRIX_LLM_FILE: ${{ env.STRIX_LLM_FILE }} | |
| STRIX_REPO_ROOT: ${{ runner.temp }}/trusted-workspace | |
| LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }} | |
| STRIX_LLM_DEFAULT_PROVIDER: contextual_orchestrator | |
| LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }} | |
| STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '__PR_SCOPE__' || './' }} | |
| STRIX_SOURCE_DIRS: ". backend frontend" | |
| # The gateway auto pool is provider-diverse. Strix function tools | |
| # must not send a provider-specific reasoning setting to every route. | |
| STRIX_REASONING_EFFORT: none | |
| # The gateway owns discovery and provider failover; Strix must not | |
| # bypass its ZDR/privacy policy with an external fallback model. | |
| STRIX_FALLBACK_MODELS: "" | |
| STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" | |
| NPM_CONFIG_IGNORE_SCRIPTS: "true" | |
| PNPM_CONFIG_IGNORE_SCRIPTS: "true" | |
| YARN_ENABLE_SCRIPTS: "false" | |
| BUN_CONFIG_IGNORE_SCRIPTS: "true" | |
| STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM | |
| STRIX_DISABLE_PR_SCOPING: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '0' || '1' }} | |
| # A repository_dispatch executes in this central repository, so its | |
| # github.token cannot read the target repository's PR. Reuse the | |
| # target-app token that already validated and fetched that exact PR; | |
| # preserve the target-repository token for pull_request_target runs. | |
| GH_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && (steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token) || github.event_name == 'pull_request_target' && github.token || '' }} | |
| PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.client_payload.pr_number }} | |
| PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha }} | |
| PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }} | |
| IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && 'true' || 'false' }} | |
| run: | | |
| export LLM_TIMEOUT=0 | |
| export STRIX_MEMORY_COMPRESSOR_TIMEOUT=0 | |
| export STRIX_PROCESS_TIMEOUT_SECONDS=0 | |
| export STRIX_TOTAL_TIMEOUT_SECONDS=0 | |
| # Recognized signals that the LLM backend was unavailable / starved. | |
| # Defined before the gate loop so the bounded retry decision below | |
| # can classify outcomes without duplicating the patterns later. | |
| backend_unavailable_signal='STRIX_PROVIDER_UNAVAILABLE|RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*500[^[:cntrl:]]*internal_error|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|Error during penetration test: loginAsGuest failed after [0-9]+ attempts: curl exit 7: curl: \(7\) Failed to connect to 127\.0\.0\.1 port 48080' | |
| model_behavior_error_signal='(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)' | |
| # Any evidence that a vulnerability was actually reported. Its presence | |
| # forces a hard failure so real findings are NEVER downgraded. Keep the | |
| # severity branch anchored away from identifiers so environment lines | |
| # such as STRIX_FAIL_ON_MIN_SEVERITY do not look like findings. | |
| reported_vulnerability_signal='Vulnerabilities[[:space:]]+[1-9]|(^|[^A-Za-z0-9_])severity[[:space:]]*:' | |
| # Capture the gate exit code plus its console output. The gate returns | |
| # exit 1 both for genuine blocking vulnerabilities AND for | |
| # LLM-backend-unavailable outcomes (GitHub Models "Too many requests" | |
| # rate limits, OpenAI quota starvation, 413 tokens_limit_reached, | |
| # connection/warm-up failures, and scanner ModelBehaviorError) that | |
| # could not complete a scan. Provider failure is typed infrastructure | |
| # evidence, but remains non-passing because no authoritative complete | |
| # vulnerability result exists. | |
| # | |
| # The gateway owns provider discovery, repair, and failover. Invoke | |
| # the trusted gate once so repository-side retries cannot multiply a | |
| # single PR scan into hours of shared-runner occupancy. | |
| strix_run_log="$RUNNER_TEMP/strix_gate_console.log" | |
| : > "$strix_run_log" | |
| strix_terminal_log="$strix_run_log" | |
| strix_rc=0 | |
| set +e | |
| bash "$TRUSTED_STRIX_GATE" 2>&1 | tee "$strix_terminal_log" | |
| strix_rc="${PIPESTATUS[0]}" | |
| set -e | |
| if [ "$strix_rc" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| # Preserve configuration failures (exit 2) and any unexpected exit | |
| # code as hard failures — only the scan-failure code (1) can be an | |
| # infrastructure/backend-unavailability outcome. | |
| if [ "$strix_rc" -ne 1 ]; then | |
| exit "$strix_rc" | |
| fi | |
| # An earlier out-of-scope/below-threshold finding may already have | |
| # been exempted by the trusted gate. Classify a later provider | |
| # outage from the tail after the last continuation marker, but keep | |
| # that incomplete later scan non-passing. | |
| strix_neutralization_scope_log="$strix_terminal_log" | |
| if grep -Fq 'allowing pipeline continuation' "$strix_terminal_log"; then | |
| strix_neutralization_scope_log="$RUNNER_TEMP/strix_gate_console_tail.log" | |
| awk '/allowing pipeline continuation/{buf=""; next} {buf=buf $0 "\n"} END{printf "%s", buf}' \ | |
| "$strix_terminal_log" > "$strix_neutralization_scope_log" | |
| fi | |
| # Classify provider/backend exhaustion only when no vulnerability | |
| # finding was emitted. Classification improves diagnosis; it never | |
| # converts an incomplete scan into passing security evidence. | |
| if ( grep -Eiq "$backend_unavailable_signal" "$strix_neutralization_scope_log" \ | |
| || grep -Eq "$model_behavior_error_signal" "$strix_neutralization_scope_log" ) \ | |
| && ! grep -Eiq "$reported_vulnerability_signal" "$strix_neutralization_scope_log"; then | |
| echo "::error title=STRIX_PROVIDER_UNAVAILABLE::Strix could not complete authoritative vulnerability analysis because its provider/backend was unavailable (rate limit, token cap, connection, warm-up, or model-behavior failure). See the strix-reports artifact and run log." | |
| exit "$strix_rc" | |
| fi | |
| echo "Strix reported security findings or failed for a non-backend reason; failing the required check (gate exit ${strix_rc})." >&2 | |
| exit "$strix_rc" | |
| - name: Collect Strix reports for artifact upload | |
| if: ${{ always() && steps.gate.outputs.enabled == 'true' }} | |
| env: | |
| PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$GITHUB_WORKSPACE/strix_runs" | |
| copied_reports=0 | |
| for candidate_dir in "$TRUSTED_WORKSPACE/strix_runs" "$RUNNER_TEMP/strix_runs"; do | |
| if [ -d "$candidate_dir" ] && [ -n "$(find "$candidate_dir" -mindepth 1 -print -quit)" ]; then | |
| cp -R "$candidate_dir"/. "$GITHUB_WORKSPACE/strix_runs"/ | |
| copied_reports=1 | |
| fi | |
| done | |
| if [ -f "$RUNNER_TEMP/strix_gate_console.log" ]; then | |
| cp "$RUNNER_TEMP/strix_gate_console.log" "$GITHUB_WORKSPACE/strix_runs/gate-console.log" | |
| copied_reports=1 | |
| fi | |
| if [ -n "$(find "$GITHUB_WORKSPACE/strix_runs" -mindepth 1 -print -quit)" ]; then | |
| copied_reports=1 | |
| fi | |
| if [ "$copied_reports" -eq 0 ]; then | |
| summary_head_sha="${PR_HEAD_SHA:-$GITHUB_SHA}" | |
| { | |
| echo "Strix scan completed without structured report files." | |
| echo "run_id=$GITHUB_RUN_ID" | |
| echo "head_sha=$summary_head_sha" | |
| } > "$GITHUB_WORKSPACE/strix_runs/scan-summary.txt" | |
| fi | |
| - name: Upload Strix reports artifact | |
| if: ${{ always() && steps.gate.outputs.enabled == 'true' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: strix-reports | |
| path: strix_runs/ | |
| if-no-files-found: error | |
| retention-days: 5 | |
| - name: Publish same-head manual Strix status | |
| if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} | |
| env: | |
| TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} | |
| GITHUB_STATUS_TOKEN: ${{ (github.event.client_payload.target_repository == '' || github.event.client_payload.target_repository == github.repository) && github.token || '' }} | |
| PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} | |
| OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} | |
| TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.repository }} | |
| PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }} | |
| STRIX_RESULT: ${{ job.status }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::PR head SHA must be a 40-character git SHA." | |
| exit 1 | |
| fi | |
| case "$STRIX_RESULT" in | |
| success) | |
| state="success" | |
| description="Default-branch repository_dispatch Strix evidence passed" | |
| ;; | |
| failure|cancelled|skipped) | |
| state="failure" | |
| description="Default-branch repository_dispatch Strix evidence failed" | |
| ;; | |
| *) | |
| state="error" | |
| description="Default-branch repository_dispatch Strix evidence inconclusive" | |
| ;; | |
| esac | |
| post_strix_status() { | |
| token_label="$1" | |
| token="$2" | |
| if [ -z "$token" ]; then | |
| return 1 | |
| fi | |
| status_response="$(mktemp)" | |
| status_error="$(mktemp)" | |
| if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}" \ | |
| -f state="$state" \ | |
| -f context="strix" \ | |
| -f description="$description" \ | |
| -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | |
| >"$status_response" 2>"$status_error"; then | |
| rm -f "$status_response" "$status_error" | |
| echo "Published manual Strix status to ${TARGET_REPOSITORY}@${PR_HEAD_SHA} using ${token_label}." | |
| return 0 | |
| fi | |
| error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" | |
| rm -f "$status_response" "$status_error" | |
| if [ -n "$error_summary" ]; then | |
| echo "::notice::Manual Strix status publish using ${token_label} did not succeed: ${error_summary}" | |
| else | |
| echo "::notice::Manual Strix status publish using ${token_label} did not succeed." | |
| fi | |
| return 1 | |
| } | |
| if post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| echo "::warning::Could not publish manual Strix status from scan job; keeping scan evidence result authoritative in the workflow run." | |
| publish-manual-pr-evidence-status: | |
| name: publish-manual-pr-evidence-status | |
| needs: strix | |
| if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} | |
| runs-on: ubuntu-24.04 | |
| # Single-shot OIDC exchange plus a handful of curl/gh api calls, no loop | |
| # or pagination -- same shape as the agent-mention-*-dispatch.yml | |
| # validate-and-forward jobs, which bound at timeout-minutes: 5. Without | |
| # this the job falls back to GitHub's 360-minute platform default on a | |
| # hung network call. | |
| timeout-minutes: 5 | |
| permissions: | |
| id-token: write | |
| statuses: write # Required for downscoped OIDC status publication. | |
| steps: | |
| - name: Exchange OpenCode app token for target repository status | |
| id: target_app_token | |
| env: | |
| OIDC_AUDIENCE: opencode-github-action | |
| OPENCODE_API_BASE_URL: https://api.opencode.ai | |
| run: | | |
| set -euo pipefail | |
| mark_unavailable() { | |
| echo "available=false" >>"$GITHUB_OUTPUT" | |
| } | |
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | |
| echo "OpenCode app token exchange unavailable: OIDC request environment is missing." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" | |
| separator="&" | |
| case "$request_url" in | |
| *\?*) ;; | |
| *) separator="?" ;; | |
| esac | |
| if ! oidc_response="$( | |
| curl -fsS \ | |
| -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${request_url}${separator}audience=${OIDC_AUDIENCE}" | |
| )"; then | |
| echo "OpenCode app token exchange unavailable: OIDC token request did not complete." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" | |
| if [ -z "$oidc_token" ]; then | |
| echo "OpenCode app token exchange unavailable: OIDC token response was empty." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| if ! token_response="$( | |
| curl -fsS \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${oidc_token}" \ | |
| "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | |
| )"; then | |
| echo "OpenCode app token exchange unavailable: app token request did not complete." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| app_token="$(jq -r '.token // empty' <<<"$token_response")" | |
| if [ -z "$app_token" ]; then | |
| echo "OpenCode app token exchange unavailable: app token response was empty." | |
| mark_unavailable | |
| exit 0 | |
| fi | |
| echo "::add-mask::$app_token" | |
| { | |
| echo "available=true" | |
| echo "token=$app_token" | |
| } >>"$GITHUB_OUTPUT" | |
| - name: Publish same-head manual Strix status | |
| env: | |
| TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} | |
| GITHUB_STATUS_READ_TOKEN: ${{ github.token }} | |
| PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} | |
| OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} | |
| TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.repository }} | |
| PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }} | |
| STRIX_RESULT: ${{ needs.strix.result }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::PR head SHA must be a 40-character git SHA." | |
| exit 1 | |
| fi | |
| case "$STRIX_RESULT" in | |
| success) | |
| state="success" | |
| description="Default-branch repository_dispatch Strix evidence passed" | |
| ;; | |
| failure|cancelled|skipped) | |
| state="failure" | |
| description="Default-branch repository_dispatch Strix evidence failed" | |
| ;; | |
| *) | |
| state="error" | |
| description="Default-branch repository_dispatch Strix evidence inconclusive" | |
| ;; | |
| esac | |
| post_strix_status() { | |
| token_label="$1" | |
| token="$2" | |
| if [ -z "$token" ]; then | |
| return 1 | |
| fi | |
| status_response="$(mktemp)" | |
| status_error="$(mktemp)" | |
| if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}" \ | |
| -f state="$state" \ | |
| -f context="strix" \ | |
| -f description="$description" \ | |
| -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | |
| >"$status_response" 2>"$status_error"; then | |
| rm -f "$status_response" "$status_error" | |
| echo "Published manual Strix status to ${TARGET_REPOSITORY}@${PR_HEAD_SHA} using ${token_label}." | |
| return 0 | |
| fi | |
| error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" | |
| rm -f "$status_response" "$status_error" | |
| if [ -n "$error_summary" ]; then | |
| echo "::notice::Manual Strix status publish using ${token_label} did not succeed: ${error_summary}" | |
| else | |
| echo "::notice::Manual Strix status publish using ${token_label} did not succeed." | |
| fi | |
| return 1 | |
| } | |
| existing_current_run_success_status() { | |
| if [ "$state" != "success" ]; then | |
| return 1 | |
| fi | |
| target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| check_existing_status() { | |
| token_label="$1" | |
| token="$2" | |
| if [ -z "$token" ]; then | |
| return 1 | |
| fi | |
| status_response="$(mktemp)" | |
| status_error="$(mktemp)" | |
| if GH_TOKEN="$token" gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses" \ | |
| >"$status_response" 2>"$status_error"; then | |
| if jq -e --arg target_url "$target_url" \ | |
| 'any(.[]; .context == "strix" and .state == "success" and ((.target_url // "") == $target_url))' \ | |
| "$status_response" >/dev/null; then | |
| rm -f "$status_response" "$status_error" | |
| echo "Existing current-run Strix success status is already present on ${TARGET_REPOSITORY}@${PR_HEAD_SHA}; follow-up status publication is complete." | |
| return 0 | |
| fi | |
| rm -f "$status_response" "$status_error" | |
| echo "::notice::No current-run Strix success status was visible using ${token_label}." | |
| return 1 | |
| fi | |
| error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" | |
| rm -f "$status_response" "$status_error" | |
| if [ -n "$error_summary" ]; then | |
| echo "::notice::Could not inspect existing Strix status using ${token_label}: ${error_summary}" | |
| else | |
| echo "::notice::Could not inspect existing Strix status using ${token_label}." | |
| fi | |
| return 1 | |
| } | |
| if check_existing_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then | |
| return 0 | |
| fi | |
| if check_existing_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then | |
| return 0 | |
| fi | |
| if check_existing_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then | |
| return 0 | |
| fi | |
| if check_existing_status "github-token" "$GITHUB_STATUS_READ_TOKEN"; then | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| if post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| if existing_current_run_success_status; then | |
| exit 0 | |
| fi | |
| # A successful scan remains authoritative evidence even when an | |
| # external target repository does not grant any configured token | |
| # permission to create commit statuses. Keep every credential- | |
| # specific failure visible above, but do not turn a clean security | |
| # scan into a failed workflow solely because of target settings. | |
| if [ "$STRIX_RESULT" = "success" ]; then | |
| echo "::warning title=Manual Strix status unavailable::Strix scan succeeded, but no configured credential could publish or read the target commit status. Preserving the successful scan result; the target repository's branch protection remains authoritative. See the preceding token-specific notices." | |
| exit 0 | |
| fi | |
| echo "::error::Could not publish manual Strix status from follow-up job after all configured credentials failed after a non-successful scan; the target PR head is missing required Strix status evidence. See the preceding notices for token-specific reasons." | |
| exit 1 |