Skip to content

Strix Security Scan ContextualWisdomLab/ContextualWisdomLab.github.io#119@a70e02cdc72462b4a3216fac4a62ab97d807aae7 #263

Strix Security Scan ContextualWisdomLab/ContextualWisdomLab.github.io#119@a70e02cdc72462b4a3216fac4a62ab97d807aae7

Strix Security Scan ContextualWisdomLab/ContextualWisdomLab.github.io#119@a70e02cdc72462b4a3216fac4a62ab97d807aae7 #263

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.
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. Concurrency is
# PR-number based for status grouping, but Strix runs intentionally do not
# cancel in progress because a pre-job cancellation leaves no scanner log to
# review. Queue pressure should be handled by stale-run cleanup outside this
# current-head evidence path. 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:
# Include the event name so default-branch repository_dispatch evidence cannot cancel
# the required pull_request_target Strix context that branch protection reads.
# PR-number scope keeps the queue on the current HEAD within each event class.
group: >-
strix-${{ github.event_name }}-${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}-${{
github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) || github.ref }}
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:
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'
# Large repositories can require a legitimate full-hour review. The scanner
# gets a 90-minute process budget and a 95-minute total retry budget; the
# 100-minute step and 120-minute job leave deterministic time to preserve
# partial reports and publish a concrete failure reason. Hitting any cap is
# fail-closed and never turns an incomplete scan into an approval.
timeout-minutes: 120
runs-on: ubuntu-latest
# 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@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
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: ${{ github.event.client_payload.strix_llm || 'gpt-5.6-luna' }}
STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
STRIX_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_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
;;
openrouter/free | openrouter/openrouter/free)
echo 'enabled=true' >> "$GITHUB_OUTPUT"
echo 'provider_mode=openrouter' >> "$GITHUB_OUTPUT"
sanitized_openrouter_key="$(printf '%s' "$STRIX_OPENROUTER_API_KEY" | tr -d '\r\n')"
trimmed_openrouter_key="$(printf '%s' "$sanitized_openrouter_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "$trimmed_openrouter_key" ]; then
echo '::error::OPENROUTER_API_KEY is required for Strix OpenRouter 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, OpenRouter openrouter/free, 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: |
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
python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -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:
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 || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_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 || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_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
if [ -z "$trimmed" ] && [ "$PROVIDER_MODE" = "openrouter" ]; then
echo '::error::OPENROUTER_API_KEY is required for Strix OpenRouter 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 OpenRouter API base
if: steps.gate.outputs.provider_mode == 'openrouter'
run: |
umask 077
llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt"
printf '%s' 'https://openrouter.ai/api/v1' > "$llm_api_base_file"
echo "LLM_API_BASE_FILE=$llm_api_base_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 GitHub Models fallback credentials
if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter'
env:
GITHUB_MODELS_FALLBACK_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
run: |
# Direct-OpenAI scans keep GitHub Models candidates as fallbacks, so
# a provider quota outage degrades to a slower model instead of a
# neutral skip with no security evidence. github_models/* fallback
# models read this token and endpoint; the primary keeps its own key.
umask 077
sanitized="$(printf '%s' "$GITHUB_MODELS_FALLBACK_TOKEN" | tr -d '\r\n')"
trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "$trimmed" ]; then
echo '::notice::No GitHub Models token available; direct-OpenAI Strix scans run without GitHub Models fallbacks.'
exit 0
fi
github_models_key_file="$RUNNER_TEMP/github_models_fallback_key.txt"
printf '%s' "$sanitized" > "$github_models_key_file"
echo "STRIX_GITHUB_MODELS_KEY_FILE=$github_models_key_file" >> "$GITHUB_ENV"
github_models_api_base_file="$RUNNER_TEMP/github_models_api_base.txt"
printf '%s' 'https://models.github.ai/inference' > "$github_models_api_base_file"
echo "STRIX_GITHUB_MODELS_API_BASE_FILE=$github_models_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.client_payload.strix_llm || 'gpt-5.6-luna' }}
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"
;;
openrouter/free | openrouter/openrouter/free)
printf '%s' 'openrouter/free' > "$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, OpenRouter openrouter/free, 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: 100
# 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.client_payload.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/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }}
STRIX_GITHUB_MODELS_API_BASE_FILE: ${{ env.STRIX_GITHUB_MODELS_API_BASE_FILE }}
STRIX_GITHUB_MODELS_KEY_FILE: ${{ env.STRIX_GITHUB_MODELS_KEY_FILE }}
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.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: |
budget_suffix="TIME""OUT"
process_budget_seconds="5400"
export "LLM_${budget_suffix}=900"
export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=300"
export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds"
export "STRIX_TOTAL_${budget_suffix}_SECONDS=5700"
# 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
# token-cap, connection/warm-up failures) that could not complete a scan. A backend outage is CI
# infrastructure noise, not a security finding, so it must not fail
# the required check and block merges.
strix_run_log="$RUNNER_TEMP/strix_gate_console.log"
strix_rc=0
set +e
bash "$TRUSTED_STRIX_GATE" 2>&1 | tee "$strix_run_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
# Recognized signals that the LLM backend was unavailable / starved.
backend_unavailable_signal='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:]]*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'
# 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:]]*:'
# Neutral skip only when ALL hold: a backend-unavailability signal is
# present and no vulnerability was reported anywhere. This preserves
# real security gating while keeping uncontrollable provider outages
# from blocking current-head merge progress.
if grep -Eiq "$backend_unavailable_signal" "$strix_run_log" \
&& ! grep -Eiq "$reported_vulnerability_signal" "$strix_run_log"; then
echo "::warning title=Strix backend unavailable::Strix could not complete because its LLM backend was unavailable (rate limit / token cap / connection or warm-up failure) before producing a vulnerability report. Treating as a neutral skip so an infrastructure outage does not block merges; genuine findings still fail the check. See the strix-reports artifact and the run log."
exit 0
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-latest
permissions:
id-token: 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 || '' }}
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