Bump github/codeql-action/analyze from 4.36.2 to 4.37.0 #144
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 | |
| 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. | |
| 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, closed] | |
| # Same conservative doc/image-only skip for PR scans. GitHub evaluates these | |
| # path filters against the PR's full base..head diff, so a PR is skipped only | |
| # when EVERY changed file is a non-executable doc/image asset; any code, | |
| # config, build, or workflow change still triggers the scan. The head.sha | |
| # concurrency design (below) is unchanged. For PRs the merge scheduler | |
| # manages, same-head Strix evidence is still forced at merge time via | |
| # workflow_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' | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: Optional pull request number for trusted PR-scope evidence | |
| required: false | |
| type: string | |
| target_repository: | |
| description: Optional repository that owns the pull request, in owner/name form | |
| required: false | |
| default: "" | |
| type: string | |
| pr_base_sha: | |
| description: Optional pull request base SHA for trusted PR-scope evidence | |
| required: false | |
| type: string | |
| pr_head_sha: | |
| description: Optional pull request head SHA for trusted PR-scope evidence | |
| required: false | |
| type: string | |
| strix_llm: | |
| description: Optional Strix model override for manual evidence runs | |
| required: false | |
| default: openai/gpt-5 | |
| type: string | |
| concurrency: | |
| group: >- | |
| strix-${{ github.event.inputs.target_repository || github.repository }}-${{ github.event_name == 'pull_request_target' && | |
| format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || | |
| github.event.inputs.pr_number != '' && github.event.inputs.pr_head_sha != '' && | |
| format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) || | |
| github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number) || github.ref }} | |
| # cancel-in-progress stays disabled for normal PR updates: an attacker could | |
| # force-push a benign commit to cancel an in-progress scan of a malicious | |
| # commit. Closed PR events only cancel older runs for the same PR/head group. | |
| cancel-in-progress: ${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| models: read | |
| statuses: write | |
| jobs: | |
| cancel-closed-pr-runs: | |
| if: github.event_name == 'pull_request_target' && github.event.action == 'closed' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "PR closed; this run only cancels older runs through workflow concurrency." | |
| strix: | |
| if: github.event_name != 'pull_request_target' || github.event.action != 'closed' | |
| # The scan itself is hard-bounded to 30 min (the "Run Strix (quick)" step | |
| # has timeout-minutes: 30 and exports STRIX_TOTAL_TIMEOUT_SECONDS=1800), and | |
| # every other step is quick or self-bounded (self-test is 2 min). A healthy | |
| # run finishes well under 50 min. The 120 cap only ever bit HUNG runs (e.g. | |
| # a network stall in pip/git with no per-step timeout); 60 min still clears | |
| # the realistic worst case with margin while freeing a stuck runner in half | |
| # the time. Fail-closed: hitting the cap fails the run, never passes it. | |
| timeout-minutes: 60 | |
| runs-on: ubuntu-latest | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| steps: | |
| - name: Harden runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| disable-file-monitoring: true | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| 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: Materialize target workspace | |
| 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.event.inputs.target_repository || github.repository }} | |
| TARGET_WORKSPACE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.inputs.pr_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: Fetch pull request head for trusted scan | |
| if: github.event_name == 'pull_request_target' || github.event.inputs.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.inputs.pr_number }} | |
| PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} | |
| PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.inputs.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:opencode.jsonc" 2>/dev/null; then | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:opencode.jsonc" > "$TRUSTED_WORKSPACE/opencode.jsonc" | |
| 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:opencode.jsonc" 2>/dev/null; then | |
| git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:opencode.jsonc" > "$TRUSTED_WORKSPACE/opencode.jsonc" | |
| 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: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }} | |
| STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY }} | |
| STRIX_VERTEX_CREDENTIALS: ${{ secrets.GCP_SA_KEY }} | |
| STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} | |
| run: | | |
| strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| case "$strix_model" in | |
| openai/gpt-5-mini* | openai/gpt-5-nano* | \ | |
| openai/openai/gpt-5-mini* | openai/openai/gpt-5-nano* | \ | |
| github_models/openai/gpt-5-mini* | github_models/openai/gpt-5-nano*) | |
| echo '::error::STRIX_LLM must not select mini or nano GPT-5 variants for security evidence.' | |
| exit 1 | |
| ;; | |
| openai/gpt-5* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]* | \ | |
| openai/openai/gpt-5* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]* | \ | |
| github_models/openai/gpt-5* | github_models/openai/gpt-[6-9]* | github_models/openai/gpt-[1-9][0-9]*) | |
| echo 'enabled=true' >> "$GITHUB_OUTPUT" | |
| echo 'provider_mode=github_models' >> "$GITHUB_OUTPUT" | |
| sanitized_github_models_token="$(printf '%s' "$STRIX_GITHUB_MODELS_TOKEN" | tr -d '\r\n')" | |
| trimmed_github_models_token="$(printf '%s' "$sanitized_github_models_token" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -z "$trimmed_github_models_token" ]; then | |
| echo '::error::STRIX_GITHUB_MODELS_TOKEN is required for GitHub Models Strix scans.' | |
| exit 1 | |
| fi | |
| ;; | |
| gpt-5.[4-9]* | gpt-5.[1-9][0-9]* | gpt-[6-9]* | gpt-[1-9][0-9]* | \ | |
| openai-direct/gpt-5.[4-9]* | openai-direct/gpt-5.[1-9][0-9]* | openai-direct/gpt-[6-9]* | openai-direct/gpt-[1-9][0-9]*) | |
| echo 'enabled=true' >> "$GITHUB_OUTPUT" | |
| echo 'provider_mode=openai_direct' >> "$GITHUB_OUTPUT" | |
| sanitized_openai_key="$(printf '%s' "$STRIX_OPENAI_API_KEY" | tr -d '\r\n')" | |
| trimmed_openai_key="$(printf '%s' "$sanitized_openai_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -z "$trimmed_openai_key" ]; then | |
| echo '::error::STRIX_OPENAI_API_KEY is required for Strix OpenAI Platform scans.' | |
| exit 1 | |
| fi | |
| ;; | |
| vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash) | |
| echo 'enabled=true' >> "$GITHUB_OUTPUT" | |
| echo 'provider_mode=vertex_ai' >> "$GITHUB_OUTPUT" | |
| sanitized_vertex_credentials="$(printf '%s' "$STRIX_VERTEX_CREDENTIALS" | tr -d '\r')" | |
| trimmed_vertex_credentials="$(printf '%s' "$sanitized_vertex_credentials" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -z "$trimmed_vertex_credentials" ]; then | |
| echo '::error::GCP_SA_KEY is required for Vertex AI Strix scans.' | |
| exit 1 | |
| fi | |
| ;; | |
| *) | |
| echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model.' | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Set up Python | |
| if: steps.gate.outputs.enabled == 'true' | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Install Strix | |
| if: steps.gate.outputs.enabled == 'true' | |
| working-directory: trusted-strix-source | |
| run: | | |
| python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-strix-ci-hashes.txt | |
| - name: Mask LLM API key | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| LLM_API_KEY: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && secrets.STRIX_OPENAI_API_KEY || '' }} | |
| run: | | |
| # Sanitize CR/LF before masking to prevent broken ::add-mask:: | |
| # commands and potential workflow command injection. | |
| sanitized="$(printf '%s' "$LLM_API_KEY" | 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: | |
| LLM_API_KEY_SECRET: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && secrets.STRIX_OPENAI_API_KEY || '' }} | |
| PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }} | |
| run: | | |
| sanitized="$(printf '%s' "$LLM_API_KEY_SECRET" | tr -d '\r\n')" | |
| trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" | |
| if [ -z "$trimmed" ] && [ "$PROVIDER_MODE" = "github_models" ]; then | |
| echo '::error::STRIX_GITHUB_MODELS_TOKEN is required for GitHub Models Strix scans.' | |
| exit 1 | |
| fi | |
| if [ -z "$trimmed" ] && [ "$PROVIDER_MODE" = "openai_direct" ]; then | |
| echo '::error::STRIX_OPENAI_API_KEY is required for Strix OpenAI Platform scans.' | |
| exit 1 | |
| fi | |
| umask 077 | |
| llm_api_key_file="$RUNNER_TEMP/llm_api_key.txt" | |
| printf '%s' "$sanitized" > "$llm_api_key_file" | |
| echo "LLM_API_KEY_FILE=$llm_api_key_file" >> "$GITHUB_ENV" | |
| - name: Prepare GitHub Models API base | |
| if: steps.gate.outputs.provider_mode == 'github_models' | |
| run: | | |
| umask 077 | |
| llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt" | |
| printf '%s' 'https://models.github.ai/inference' > "$llm_api_base_file" | |
| echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" | |
| - name: Prepare Vertex AI credentials | |
| if: steps.gate.outputs.provider_mode == 'vertex_ai' | |
| env: | |
| GCP_SA_KEY_JSON: ${{ secrets.GCP_SA_KEY }} | |
| run: | | |
| umask 077 | |
| credentials_file="$RUNNER_TEMP/gcp-sa-key.json" | |
| printf '%s' "$GCP_SA_KEY_JSON" > "$credentials_file" | |
| python3 - "$credentials_file" >> "$GITHUB_ENV" <<'PY' | |
| import json | |
| import pathlib | |
| import sys | |
| credentials_path = pathlib.Path(sys.argv[1]) | |
| def reject_duplicate_json_keys(pairs): | |
| parsed = {} | |
| for key, value in pairs: | |
| if key in parsed: | |
| raise ValueError("duplicate credential key") | |
| parsed[key] = value | |
| return parsed | |
| try: | |
| credentials_text = credentials_path.read_text(encoding="utf-8") | |
| credentials = json.loads( | |
| credentials_text, | |
| object_pairs_hook=reject_duplicate_json_keys, | |
| ) | |
| except (OSError, UnicodeDecodeError, json.JSONDecodeError, ValueError): | |
| raise SystemExit( | |
| "GCP_SA_KEY must be valid service account JSON for Vertex AI Strix scans." | |
| ) | |
| if not isinstance(credentials, dict): | |
| raise SystemExit( | |
| "GCP_SA_KEY must be a JSON object for Vertex AI Strix scans." | |
| ) | |
| project_id = str(credentials.get("project_id", "")).strip() | |
| if not project_id: | |
| raise SystemExit("GCP_SA_KEY must include project_id for Vertex AI Strix scans.") | |
| print(f"GOOGLE_APPLICATION_CREDENTIALS={credentials_path}") | |
| print(f"CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE={credentials_path}") | |
| print(f"VERTEXAI_PROJECT={project_id}") | |
| print(f"GOOGLE_CLOUD_PROJECT={project_id}") | |
| print(f"GCP_PROJECT={project_id}") | |
| print(f"GCLOUD_PROJECT={project_id}") | |
| print(f"CLOUDSDK_CORE_PROJECT={project_id}") | |
| print(f"CLOUDSDK_PROJECT={project_id}") | |
| PY | |
| - name: Prepare Strix model input file | |
| if: steps.gate.outputs.enabled == 'true' | |
| env: | |
| STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }} | |
| 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 | |
| openai/gpt-5-mini* | openai/gpt-5-nano* | \ | |
| openai/openai/gpt-5-mini* | openai/openai/gpt-5-nano* | \ | |
| github_models/openai/gpt-5-mini* | github_models/openai/gpt-5-nano*) | |
| echo '::error::STRIX_LLM must not select mini or nano GPT-5 variants for security evidence.' | |
| exit 1 | |
| ;; | |
| openai/gpt-5* | openai/gpt-[6-9]* | openai/gpt-[1-9][0-9]* | \ | |
| openai/openai/gpt-5* | openai/openai/gpt-[6-9]* | openai/openai/gpt-[1-9][0-9]* | \ | |
| github_models/openai/gpt-5* | github_models/openai/gpt-[6-9]* | github_models/openai/gpt-[1-9][0-9]*) | |
| printf '%s' "${strix_model#github_models/}" > "$strix_llm_file" | |
| ;; | |
| openai/*) | |
| printf '%s' "$strix_model" > "$strix_llm_file" | |
| ;; | |
| openai-direct/gpt-*) | |
| printf 'openai_direct/%s' "${strix_model#openai-direct/}" > "$strix_llm_file" | |
| ;; | |
| gpt-*) | |
| printf 'openai_direct/%s' "$strix_model" > "$strix_llm_file" | |
| ;; | |
| vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash) | |
| printf '%s' "$strix_model" > "$strix_llm_file" | |
| ;; | |
| *) | |
| echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model.' | |
| exit 1 | |
| ;; | |
| esac | |
| echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV" | |
| - name: Run Strix (quick) | |
| if: steps.gate.outputs.enabled == 'true' | |
| timeout-minutes: 30 | |
| # 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: ${{ steps.gate.outputs.provider_mode == 'vertex_ai' && 'vertex_ai' || 'openai' }} | |
| LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }} | |
| GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} | |
| CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: ${{ env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE }} | |
| VERTEXAI_PROJECT: ${{ env.VERTEXAI_PROJECT }} | |
| GOOGLE_CLOUD_PROJECT: ${{ env.GOOGLE_CLOUD_PROJECT }} | |
| GCP_PROJECT: ${{ env.GCP_PROJECT }} | |
| GCLOUD_PROJECT: ${{ env.GCLOUD_PROJECT }} | |
| CLOUDSDK_CORE_PROJECT: ${{ env.CLOUDSDK_CORE_PROJECT }} | |
| CLOUDSDK_PROJECT: ${{ env.CLOUDSDK_PROJECT }} | |
| VERTEXAI_LOCATION: ${{ secrets.VERTEX_LOCATION || 'us-central1' }} | |
| VERTEX_LOCATION: ${{ secrets.VERTEX_LOCATION || 'us-central1' }} | |
| STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && '__PR_SCOPE__' || './' }} | |
| STRIX_SOURCE_DIRS: ". backend frontend" | |
| STRIX_REASONING_EFFORT: high | |
| STRIX_LLM_MAX_RETRIES: 1 | |
| STRIX_TRANSIENT_RETRY_PER_MODEL: 2 | |
| STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60 | |
| STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/gpt-5-chat github_models/openai/o3 github_models/deepseek/deepseek-v3-0324 github_models/deepseek/deepseek-r1-0528 github_models/deepseek/deepseek-r1' || '' }} | |
| STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" | |
| STRIX_VERTEX_FALLBACK_MODELS: "" | |
| 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.inputs.pr_number != '') && '0' || '1' }} | |
| GH_TOKEN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && github.token || '' }} | |
| PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.inputs.pr_number }} | |
| PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} | |
| PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} | |
| IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && 'true' || 'false' }} | |
| run: | | |
| budget_suffix="TIME""OUT" | |
| process_budget_seconds="1500" | |
| export "LLM_${budget_suffix}=120" | |
| export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=10" | |
| export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds" | |
| export "STRIX_TOTAL_${budget_suffix}_SECONDS=1800" | |
| bash "$TRUSTED_STRIX_GATE" | |
| - 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.inputs.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 [ -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 == 'workflow_dispatch' && github.event.inputs.pr_head_sha != '' }} | |
| env: | |
| TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} | |
| PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} | |
| OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} | |
| GITHUB_STATUS_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }} | |
| PR_HEAD_SHA: ${{ github.event.inputs.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="Manual workflow_dispatch Strix evidence passed" | |
| ;; | |
| failure|cancelled|skipped) | |
| state="failure" | |
| description="Manual workflow_dispatch Strix evidence failed" | |
| ;; | |
| *) | |
| state="error" | |
| description="Manual workflow_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 "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 [ "$TARGET_REPOSITORY" = "$GITHUB_REPOSITORY" ] && 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 == 'workflow_dispatch' && github.event.inputs.pr_head_sha != '' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| statuses: write | |
| 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 || '' }} | |
| PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} | |
| OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} | |
| GITHUB_STATUS_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }} | |
| PR_HEAD_SHA: ${{ github.event.inputs.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="Manual workflow_dispatch Strix evidence passed" | |
| ;; | |
| failure|cancelled|skipped) | |
| state="failure" | |
| description="Manual workflow_dispatch Strix evidence failed" | |
| ;; | |
| *) | |
| state="error" | |
| description="Manual workflow_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 "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 [ "$TARGET_REPOSITORY" = "$GITHUB_REPOSITORY" ] && post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then | |
| exit 0 | |
| fi | |
| echo "::warning::Could not publish manual Strix status from follow-up job; scan job publishes the authoritative status when target credentials are available." |