Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,14 @@ jobs:
timeout-minutes: 45
runs-on: ubuntu-latest
# Least-privilege token scoped to this job (Scorecard alert #43): the scan
# exchanges an OIDC token (id-token); commit-status publication uses
# explicit app/secret tokens, so GITHUB_TOKEN statuses stays read-only.
# exchanges an OIDC token (id-token) and posts same-repository commit status
# fallback from this job; all other jobs keep commit-status read-only.
permissions:
actions: read
contents: read
id-token: write
models: read
statuses: read
statuses: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
Expand Down Expand Up @@ -334,6 +334,11 @@ jobs:
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:.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"
Expand All @@ -349,6 +354,11 @@ jobs:
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:.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"
Expand Down Expand Up @@ -729,6 +739,7 @@ jobs:
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 || '' }}
GITHUB_STATUS_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.inputs.target_repository || github.repository }}
Expand Down Expand Up @@ -793,6 +804,13 @@ jobs:
if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then
exit 0
fi
if [ "$TARGET_REPOSITORY" = "$GITHUB_REPOSITORY" ]; then
if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then
exit 0
fi
else
echo "::notice::Skipping github-token fallback for cross-repository Strix status publish."
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:
Expand Down
80 changes: 46 additions & 34 deletions scripts/ci/opencode_review_approve_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,76 +84,88 @@ import sys
from pathlib import Path


def fail() -> None:
def reject(reason: str) -> None:
print(f"Reason: {reason}", file=sys.stderr)
raise SystemExit(1)


control_path = Path(sys.argv[1])
try:
control = json.loads(control_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
reject(f"control JSON is invalid: {exc}")

if not isinstance(control, dict):
reject("control JSON must be an object")


def nonempty_string(value: object) -> bool:
return isinstance(value, str) and len(value) > 0


def valid_finding(value: object) -> bool:
if not isinstance(value, dict):
return False
path = value.get("path")
def required_string(field: str) -> str:
value = control.get(field)
if not nonempty_string(value):
reject(f"{field} must be a non-empty string")
return str(value)


def validate_finding(index: int, finding: object) -> None:
if not isinstance(finding, dict):
reject(f"finding {index} must be an object")
path = finding.get("path")
if not nonempty_string(path):
return False
reject(f"finding {index} path must be a non-empty string")
if str(path).casefold() in {"n/a", "unknown"}:
return False
line = value.get("line")
reject(f"finding {index} path must name a source file")
line = finding.get("line")
if (
isinstance(line, bool)
or not isinstance(line, (int, float))
or not math.isfinite(float(line))
or line <= 0
or math.floor(float(line)) != float(line)
):
return False
required_strings = (
reject(f"finding {index} line must be a positive integer")
for field in (
"severity",
"title",
"problem",
"root_cause",
"fix_direction",
"regression_test_direction",
"suggested_diff",
)
if not all(nonempty_string(value.get(field)) for field in required_strings):
return False
suggested_diff = str(value.get("suggested_diff", "")).casefold()
):
if not nonempty_string(finding.get(field)):
reject(f"finding {index} field {field} must be a non-empty string")
suggested_diff = str(finding.get("suggested_diff", "")).casefold()
if suggested_diff.startswith("n/a") or suggested_diff.startswith("cannot provide diff"):
return False
return True

reject(f"finding {index} suggested_diff must be concrete")

try:
control = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
fail()

if not isinstance(control, dict):
fail()

if not all(nonempty_string(control.get(field)) for field in ("head_sha", "run_id", "run_attempt", "reason", "summary")):
fail()
head_sha = required_string("head_sha")
run_id = required_string("run_id")
run_attempt = required_string("run_attempt")
required_string("reason")
required_string("summary")

result = control.get("result")
if result not in {"APPROVE", "REQUEST_CHANGES"}:
fail()
reject("result must be APPROVE or REQUEST_CHANGES")

findings = control.get("findings")
if result == "REQUEST_CHANGES":
if not isinstance(findings, list) or len(findings) == 0:
fail()
reject("REQUEST_CHANGES requires at least one finding")
elif findings is not None and (not isinstance(findings, list) or len(findings) != 0):
fail()
reject("APPROVE requires findings to be empty")

if not all(valid_finding(finding) for finding in (findings or [])):
fail()
for index, finding in enumerate(findings or [], start=1):
validate_finding(index, finding)

print(control["head_sha"])
print(control["run_id"])
print(control["run_attempt"])
print(head_sha)
print(run_id)
print(run_attempt)
print(result)
PY
then
Expand Down
21 changes: 10 additions & 11 deletions scripts/ci/strix_required_workflow_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ repo_root="$(
cd -P -- "$script_dir/../.."
pwd -P
)"
workflow_file="$repo_root/.github/workflows/strix.yml"
workflow_root="${TRUSTED_WORKSPACE:-$repo_root}"
if [ ! -f "$workflow_root/.github/workflows/strix.yml" ]; then
workflow_root="$repo_root"
fi
workflow_file="$workflow_root/.github/workflows/strix.yml"
gate_script="$repo_root/scripts/ci/strix_quick_gate.sh"
full_gate_test="$repo_root/scripts/ci/test_strix_quick_gate.sh"

Expand Down Expand Up @@ -103,17 +107,10 @@ for line in lines[jobs_index + 1 :]:
if line.strip():
inside_permissions = False

if status_write_jobs:
if status_write_jobs != ["strix"]:
print(
"Strix workflow GITHUB_TOKEN status permissions must stay read-only; found statuses: write in: "
+ ", ".join(status_write_jobs),
file=sys.stderr,
)
raise SystemExit(1)

if "strix" not in status_read_jobs:
print(
"Strix workflow scan job must retain statuses: read for existing status evidence.",
"Strix workflow must scope statuses: write only to the strix scan job; found: "
+ (", ".join(status_write_jobs) if status_write_jobs else "none"),
file=sys.stderr,
)
raise SystemExit(1)
Expand All @@ -127,6 +124,8 @@ if ! bash -n "$gate_script" "$full_gate_test"; then
record_failure "Strix gate scripts must pass bash syntax checks"
fi

echo "Checking Strix workflow contract in $workflow_file"

checkout_count="$(grep -Fc "uses: actions/checkout@" "$workflow_file" || true)"
if [ "$checkout_count" != "1" ]; then
record_failure "Strix workflow must use actions/checkout exactly once for central trusted source checkout"
Expand Down
10 changes: 6 additions & 4 deletions scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ assert_strix_workflow_pr_trigger_hardened() {
assert_file_contains "$workflow_file" "branches: [main, develop, master]" "strix workflow scans GitHub Flow and Git Flow protected branches"
assert_file_contains "$workflow_file" "pull_request_target:" "strix workflow uses trusted PR trigger"
assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "strix workflow scopes pull_request_target concurrency to the active pull request head"
assert_file_contains "$workflow_file" 'strix-${{ github.event.inputs.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}' "strix manual dispatch concurrency scopes to the target repository when provided"
assert_file_contains "$workflow_file" "github.event.inputs.target_repository ||" "strix manual dispatch concurrency scopes to the target repository when provided"
assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha)" "strix workflow scopes manual PR evidence concurrency to the requested pull request head"
assert_file_contains "$workflow_file" "github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number)" "strix workflow retains a manual PR fallback group when no head SHA is provided"
assert_file_contains "$workflow_file" "|| github.ref" "strix workflow scopes non-PR concurrency to the current ref"
assert_file_contains "$workflow_file" "github.ref }}" "strix workflow scopes non-PR concurrency to the current ref"
assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }}" "strix workflow cancels only closed-PR cleanup runs"
assert_file_contains "$workflow_file" "cancel-in-progress stays disabled for normal PR updates" "strix workflow documents normal PR security evidence preservation"
assert_file_contains "$workflow_file" "refs/pull/<n>/head has already advanced before this queued run starts" "strix workflow documents stale scan queue avoidance"
Expand Down Expand Up @@ -132,8 +132,10 @@ assert_strix_workflow_pr_trigger_hardened() {
assert_file_contains "$workflow_file" "git -C \"\$TRUSTED_WORKSPACE\"" "strix workflow runs git only inside trusted workspace"
assert_file_contains "$workflow_file" 'working-directory: ${{ runner.temp }}/trusted-workspace' "strix workflow executes privileged steps from the trusted workspace"
assert_file_contains "$workflow_file" 'mkdir -p "$TRUSTED_WORKSPACE/scripts/ci"' "strix workflow creates the scheduler policy directory before materializing PR-head scheduler policy"
assert_file_contains "$workflow_file" 'git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml"' "strix workflow materializes the PR-head workflow for required-path self-test"
assert_file_contains "$workflow_file" "STRIX_REPO_ROOT:" "strix workflow passes target repository root to the central Strix gate"
assert_file_contains "$workflow_file" "bash \"\$TRUSTED_STRIX_REQUIRED_SMOKE\"" "strix workflow self-test executes bounded trusted smoke script"
assert_file_contains "$REPO_ROOT/scripts/ci/strix_required_workflow_smoke.sh" 'TRUSTED_WORKSPACE' "strix required-workflow smoke validates the fetched PR head workflow when available"
assert_file_not_contains "$workflow_file" "bash \"\$TRUSTED_STRIX_GATE_TEST\"" "strix required path does not execute the full long-form gate harness"
assert_file_contains "$workflow_file" "bash \"\$TRUSTED_STRIX_GATE\"" "strix workflow executes trusted temp gate script"
assert_file_contains "$workflow_file" "Collect Strix reports for artifact upload" "strix workflow preserves reports from trusted workspace"
Expand Down Expand Up @@ -885,7 +887,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" 'branch protection cannot observe an approval gate without a matching GitHub pull review' "opencode approval logs why approval publication failure is blocking"
assert_file_contains "$workflow_file" 'OpenCode approve review publication failed for head %s; branch protection still lacks the required GitHub review.' "opencode approval explains failed-closed review publication"
assert_file_not_contains "$workflow_file" 'OpenCode approve review publication skipped after successful gate; keeping the successful approval gate result' "opencode approval no longer soft-passes rejected APPROVE review writes"
assert_file_contains "$workflow_file" 'Branch protection remains authoritative for required reviews and peer checks.' "opencode approval logs that branch protection remains authoritative after review write failure"
assert_file_contains "$workflow_file" 'Branch protection: remains authoritative for required reviews and peer checks.' "opencode approval logs that branch protection remains authoritative after review write failure"
assert_file_not_contains "$workflow_file" 'gh_error_is_rate_limited()' "opencode approval soft-pass is event-scoped rather than rate-limit-specific"
assert_file_contains "$workflow_file" 'warn_gh_publication_failure "review overview comment"' "opencode approval soft-fails permission-denied overview publication"
assert_file_not_contains "$workflow_file" 'gh api -X DELETE "repos/${GH_REPOSITORY}/issues/comments/${comment_id}"' "opencode review must not delete Review Overview gate evidence"
Expand All @@ -907,7 +909,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" "collect_failed_check_evidence.sh" "opencode review workflow collects failed check logs and annotations"
assert_file_contains "$workflow_file" 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }}' "opencode evidence step passes HEAD_SHA to failed-check evidence collection"
assert_file_contains "$workflow_file" "FAILED_CHECK_EVIDENCE_ATTEMPTS" "opencode review workflow bounds waiting for peer check failures before model review"
assert_file_contains "$workflow_file" 'timeout-minutes: 40' "opencode evidence preparation has a bounded peer-check wait timeout"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode evidence preparation has a bounded peer-check wait timeout"
assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_ATTEMPTS: "20"' "opencode review workflow keeps pre-model peer-check waiting bounded for required workflow DX"
assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "15"' "opencode review workflow retries peer-check evidence without stalling the model stage for Strix-scale durations"
assert_file_contains "$workflow_file" "found completed failed peer-check evidence while other peer checks are still running" "opencode evidence preparation retries stale failed checks while peer checks are pending"
Expand Down
97 changes: 58 additions & 39 deletions scripts/ci/validate_opencode_failed_check_review.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,34 @@ if [ ! -s "$FAILED_CHECKS_FILE" ]; then
fi

review_text="$(
jq -r '
[
(.summary // ""),
(.reason // ""),
(
.findings[]?
| [
(.path // ""),
((.line // "") | tostring),
(.severity // ""),
(.title // ""),
(.problem // ""),
(.root_cause // ""),
(.fix_direction // ""),
(.regression_test_direction // ""),
(.suggested_diff // "")
]
| join("\n")
)
]
| join("\n")
' "$CONTROL_JSON_FILE"
python3 - "$CONTROL_JSON_FILE" <<'PY'
from __future__ import annotations

import json
import sys
from pathlib import Path

control = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
parts = [str(control.get("summary") or ""), str(control.get("reason") or "")]
for finding in control.get("findings") or []:
parts.append(
"\n".join(
str(finding.get(field) or "")
for field in (
"path",
"line",
"severity",
"title",
"problem",
"root_cause",
"fix_direction",
"regression_test_direction",
"suggested_diff",
)
)
)
print("\n".join(parts))
PY
)"

contains_review_text() {
Expand Down Expand Up @@ -168,23 +174,36 @@ extract_strix_report_model_markers() {
}

count_strix_review_findings() {
jq -r '
[
(.findings // [])[]
| [
.title,
.problem,
.root_cause,
.fix_direction,
.regression_test_direction,
.suggested_diff
]
| map(. // "")
| join("\n")
| select(test("strix|github[-_]models/|deepseek/|openai/gpt-|vertex_ai/|Vulnerability Report"; "i"))
]
| length
' "$CONTROL_JSON_FILE"
python3 - "$CONTROL_JSON_FILE" <<'PY'
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

control = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
pattern = re.compile(
r"strix|github[-_]models/|deepseek/|openai/gpt-|vertex_ai/|Vulnerability Report",
re.IGNORECASE,
)
count = 0
for finding in control.get("findings") or []:
text = "\n".join(
str(finding.get(field) or "")
for field in (
"title",
"problem",
"root_cause",
"fix_direction",
"regression_test_direction",
"suggested_diff",
)
)
if pattern.search(text):
count += 1
print(count)
PY
}

validate_distinct_strix_report_findings() {
Expand Down
Loading