Skip to content
Closed
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
103 changes: 24 additions & 79 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ jobs:
export R_LIBS_USER="${RUNNER_TEMP}/R-library"
mkdir -p "$R_LIBS_USER"
run_and_capture "R coverage tooling (covr/testthat)" \
bash -c 'timeout 780 Rscript -e '\''user_repo <- "https://cloud.r-project.org"; codename <- tryCatch(sub("[[:space:]]+$", "", system2("lsb_release", "-cs", stdout = TRUE, stderr = FALSE)[1]), error = function(e) ""); if (length(codename) == 1 && !is.na(codename) && nzchar(codename)) { options(HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os))); repos <- c(sprintf("https://p3m.dev/cran/__linux__/%s/latest", codename), user_repo) } else { repos <- user_repo }; lib <- Sys.getenv("R_LIBS_USER"); install_deps <- c("Depends", "Imports", "LinkingTo"); dir.create(lib, recursive = TRUE, showWarnings = FALSE); .libPaths(c(lib, .libPaths())); required <- c("covr", "testthat"); if (file.exists("DESCRIPTION")) { desc <- read.dcf("DESCRIPTION")[1, , drop = FALSE]; fields <- intersect(c("Depends", "Imports", "LinkingTo"), colnames(desc)); values <- as.character(desc[, fields, drop = TRUE]); values <- values[!is.na(values)]; package_deps <- trimws(gsub("\\s*\\([^)]*\\)", "", unlist(strsplit(paste(values, collapse = ","), ","), use.names = FALSE))); package_deps <- setdiff(package_deps[nzchar(package_deps)], "R"); required <- unique(c(required, package_deps)); }; for (pkg in required) if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg, repos = repos, lib = lib, dependencies = install_deps); missing <- required[!vapply(required, requireNamespace, logical(1), quietly = TRUE)]; if (length(missing)) stop("R coverage tooling packages unavailable after install: ", paste(missing, collapse = ", "))'\'' || { echo "R coverage tooling install unavailable or exceeded the runner time budget; deferring to required peer R CMD check evidence."; exit 0; }'
bash -c 'Rscript -e '\''repos <- "https://cloud.r-project.org"; lib <- Sys.getenv("R_LIBS_USER"); install_deps <- c("Depends", "Imports", "LinkingTo"); dir.create(lib, recursive = TRUE, showWarnings = FALSE); .libPaths(c(lib, .libPaths())); required <- c("covr", "testthat"); if (file.exists("DESCRIPTION")) { desc <- read.dcf("DESCRIPTION")[1, , drop = FALSE]; fields <- intersect(c("Depends", "Imports", "LinkingTo", "Suggests"), colnames(desc)); values <- as.character(desc[, fields, drop = TRUE]); values <- values[!is.na(values)]; package_deps <- trimws(gsub("\\s*\\([^)]*\\)", "", unlist(strsplit(paste(values, collapse = ","), ","), use.names = FALSE))); package_deps <- setdiff(package_deps[nzchar(package_deps)], "R"); required <- unique(c(required, package_deps)); }; for (pkg in required) if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg, repos = repos, lib = lib, dependencies = install_deps); missing <- required[!vapply(required, requireNamespace, logical(1), quietly = TRUE)]; if (length(missing)) stop("R coverage tooling packages unavailable after install: ", paste(missing, collapse = ", "))'\'' || { echo "R coverage tooling install unavailable in coverage runner; deferring to required peer R CMD check evidence."; exit 0; }'
if [ -f DESCRIPTION ]; then
if [ -d tests/testthat ]; then
run_and_capture "R package testthat suite" \
Expand Down Expand Up @@ -834,7 +834,6 @@ jobs:
needs: [coverage-evidence]
if: >-
always()
&& needs.coverage-evidence.result != 'cancelled'
&& (
github.event_name == 'workflow_dispatch'
|| (
Expand Down Expand Up @@ -2007,7 +2006,7 @@ jobs:
"$schema": "https://opencode.ai/config.json",
"model": "github-models/deepseek/deepseek-r1-0528",
"small_model": "github-models/deepseek/deepseek-v3-0324",
"enabled_providers": ["openai", "github-models"],
"enabled_providers": ["github-models"],
"lsp": true,
"mcp": {
"codegraph": {
Expand Down Expand Up @@ -2133,50 +2132,6 @@ jobs:
}
},
"provider": {
"openai": {
"npm": "@ai-sdk/openai",
"name": "OpenAI (direct)",
"options": {
"baseURL": "https://api.openai.com/v1",
"apiKey": "{env:OPENAI_API_KEY}"
},
"models": {
"gpt-5": {
"name": "OpenAI GPT-5 (direct)",
"tool_call": true,
"reasoning": true,
"options": {
"reasoningEffort": "high"
},
"variants": {
"high": {
"reasoningEffort": "high"
}
},
"limit": {
"context": 400000,
"output": 128000
}
},
"gpt-5-mini": {
"name": "OpenAI GPT-5 Mini (direct)",
"tool_call": true,
"reasoning": true,
"options": {
"reasoningEffort": "high"
},
"variants": {
"high": {
"reasoningEffort": "high"
}
},
"limit": {
"context": 400000,
"output": 128000
}
}
}
},
"github-models": {
"npm": "@ai-sdk/openai-compatible",
"name": "GitHub Models",
Expand Down Expand Up @@ -2384,44 +2339,31 @@ jobs:
env:
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
GITHUB_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
# Native OpenAI backend for the lead review model. GitHub Models
# rate-limits every request and caps bodies at ~4000 tokens, so the
# rate-starved shared pool never returned a verdict; hitting
# api.openai.com directly with the org OPENAI_API_KEY gives the lead
# model a working, un-throttled backend. Resolves {env:OPENAI_API_KEY}
# in the opencode.jsonc "openai" provider block.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
USE_GITHUB_TOKEN: "true"
SHARE: "false"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
NO_COLOR: "1"
# Lead with the NATIVE OpenAI backend (openai/gpt-5-mini, openai/gpt-5
# via api.openai.com with the org OPENAI_API_KEY). GitHub Models
# rate-limited ("Too many requests") and 4000-token-capped
# (413 tokens_limit_reached) EVERY model in the shared pool, so the
# reviewer never produced a verdict and every run hung to the 350-min
# timeout — a 100% org-wide failure. The native provider is not subject
# to those limits, so it can actually complete and approve. The
# existing github-models entries stay as fallbacks (tried only if the
# native key is missing or the direct call fails).
# github-models ordering rationale (unchanged): contract-reliable mini
# reasoning models first, high-quota non-reasoning models next, and the
# rate-starved github-models flagships (gpt-5/o3, 8-12 req/day) last so
# a throttled/hung leader always falls back instead of eating the step.
OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini openai/gpt-5 github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/gpt-5-mini github-models/openai/gpt-5-nano github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/deepseek/deepseek-v3-0324 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct github-models/openai/o3 github-models/openai/gpt-5"
# Ordered contract-reliability first, then quota, with the rate-starved
# flagships last. The mini reasoning models (o4-mini, o3-mini, gpt-5-
# mini/nano/chat) reliably emit the strict review contract — every
# required label and only source-backed findings — so they lead. The
# high-quota non-reasoning models (deepseek-v3, mistral, llama-4) emit
# bare or hallucinated reviews the publish/approve gates reject, so
# they are fallbacks only. gpt-5/o3 ("Reasoning" tier, 8-12 req/day)
# stay last: first-placing them stalled every review until timeout
# because a rate-limited/hung flagship never fell back.
OPENCODE_MODEL_CANDIDATES: "github-models/openai/o4-mini github-models/openai/o3-mini github-models/openai/gpt-5-mini github-models/openai/gpt-5-nano github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/deepseek/deepseek-v3-0324 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct github-models/openai/o3 github-models/openai/gpt-5"
# One attempt per model, then fall through to the next model. Retrying
# the SAME model 5x let a rate-limited/hung leader consume the whole
# step, so the pool never reached a healthy fallback model.
OPENCODE_MODEL_ATTEMPTS: "1"
# 15 min per model — enough for a bounded review attempt, but short
# enough that a hung provider yields to the next candidate before it
# freezes the review queue.
OPENCODE_RUN_TIMEOUT_SECONDS: "900"
# 90 min per model — generous for a deep tool-using review, but bounded
# so a rate-limited model yields to the next one instead of eating the
# 350-min step. (20400s = 340min gave one model the entire budget with
# no fallback; 600s was too short for a proper review.)
OPENCODE_RUN_TIMEOUT_SECONDS: "5400"
OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"
# Bound provider/model-pool outages before the 350-min job timeout. A
# zero budget disables the script deadline and caused org-wide hangs.
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2700"
OPENCODE_POOL_MAX_CYCLES: "1"
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "0"
OPENCODE_BACKOFF_INITIAL_SECONDS: "30"
OPENCODE_BACKOFF_MAX_SECONDS: "30"
OPENCODE_FIRST_ATTEMPT_AGENT: ci-review
Expand Down Expand Up @@ -2790,9 +2732,6 @@ jobs:
CHECK_LOOKUP_GH_TOKEN: ${{ github.token }}
GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
# Exposed so the "openai" provider in opencode.jsonc resolves during the
# failed-check diagnosis opencode run that shares this config.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }}
OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md
OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md
Expand Down Expand Up @@ -2869,6 +2808,12 @@ jobs:
fi
}

gh_error_is_rate_limited() {
local error_file="$1"
[ -s "$error_file" ] || return 1
grep -Eiq '(API rate limit exceeded|rate limit exceeded|secondary rate limit)' "$error_file"
}

emit_change_flow_mermaid_graph() {
local merge_state="${1:-UNKNOWN}"
local changed_files_file surfaces_file idx next_node
Expand Down
17 changes: 1 addition & 16 deletions .github/workflows/osv-scanner-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ jobs:
osv-scan:
if: github.event.action != 'closed'
# ponytail: use upstream reusable PR workflow, don't hand-roll the diff scan
# Pinned to v2.3.8 + 1 commit (3a7550f) which gates the JSON job outputs
# behind the new `export-results` input (default false). v2.3.8 dumped the
# full old/new osv-scanner JSON into job outputs unconditionally, tripping
# GitHub's 1,048,576-byte job-outputs cap and failing the run. Same nested
# action pins as v2.3.8; only the Export step is now conditional.
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@3a7550f43ba5b58905a821ce3a0ed24c4858b3f4 # v2.3.8 + export-results gate
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8
permissions:
actions: read
contents: read
Expand All @@ -44,13 +39,3 @@ jobs:
# (medium_or_higher), not by failing this check. Keep the check green
# so it only supplies the analysis; the ruleset decides blocking.
fail-on-vuln: false
# RELIABILITY: resolve Maven parent POMs through Google's byte-identical
# Maven Central mirror instead of repo.maven.apache.org, which
# intermittently 429s during transitive resolution (e.g.
# spring-boot-starter-parent). Same maven2 layout, same bytes -> no
# coverage loss; transitive scanning stays fully enabled. See
# security-scan.yml for the full rationale.
scan-args: |-
--maven-registry=https://maven-central.storage-download.googleapis.com/maven2
-r
./
21 changes: 0 additions & 21 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,6 @@ jobs:
security-events: write
with:
fail-on-vuln: true
# RELIABILITY: point Maven transitive (parent-POM) resolution at Google's
# byte-identical Maven Central mirror instead of repo.maven.apache.org.
# osv-scanner resolves parent POMs (e.g. spring-boot-starter-parent) over
# its own HTTP client (osv-scalibr pomxmlnet -> defaultRegistry.URL); the
# canonical Central host intermittently returns HTTP 429, which failed this
# required gate on APPROVED Maven PRs with "No issues found" (a network
# flake, not a real vuln). The mirror serves the same maven2 layout and the
# same bytes, so coverage is unchanged -- this ONLY swaps the default
# registry host. Repos' own pom.xml <repositories> are still added on top
# (scalibr AddRegistry), and transitive scanning stays fully enabled (no
# --no-resolve). Caching ~/.m2 or a settings.xml <mirror> would NOT help:
# osv-scanner never reads ~/.m2/repository and parses settings.xml only for
# <servers> auth, not <mirrors>. --maven-registry is the only effective lever.
scan-args: |-
--maven-registry=https://maven-central.storage-download.googleapis.com/maven2
-r
./

dependency-review:
if: github.event.action != 'closed'
Expand Down Expand Up @@ -151,10 +134,6 @@ jobs:
format: sarif
output: trivy-results.sarif
exit-code: "1"
# Without this, trivy-action rebuilds the SARIF scan with ALL
# severities and exit-code applies to any LOW/MEDIUM finding,
# contradicting the documented CRITICAL/HIGH-only gate above.
limit-severities-for-sarif: true
- name: Upload Trivy SARIF to code scanning
if: always() && hashFiles('trivy-results.sarif') != ''
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
Expand Down
7 changes: 6 additions & 1 deletion scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def call_llm(repo: str, number: int, pr: dict[str, Any], diff: str, truncated: b
return None
parsed = urllib.parse.urlparse(api_url)
if parsed.scheme.lower() not in {"http", "https"}:
raise ValueError("URL scheme must be http or https")
raise ValueError("URL must start with http:// or https://")
hostname = (parsed.hostname or "").lower()
if not hostname:
raise ValueError("URL must have a valid hostname")
Expand All @@ -299,6 +299,9 @@ def call_llm(repo: str, number: int, pr: dict[str, Any], diff: str, truncated: b
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified:
raise ValueError("URL cannot target internal IP addresses")

if not (api_url.lower().startswith("http://") or api_url.lower().startswith("https://")):
raise ValueError(f"NOEMA_LLM_API_URL must start with http:// or https:// to prevent SSRF vulnerabilities, got: {api_url}")

prompt = {
"role": "user",
"content": "\n".join(
Expand Down Expand Up @@ -335,6 +338,8 @@ def call_llm(repo: str, number: int, pr: dict[str, Any], diff: str, truncated: b
},
method="POST",
)
if parsed.scheme.lower() not in {"http", "https"}:
raise ValueError("URL scheme http or https required")
with urllib.request.urlopen(request, timeout=120) as response: # nosec B310
raw = response.read().decode("utf-8")
data = json.loads(raw)
Expand Down
4 changes: 3 additions & 1 deletion scripts/ci/opencode_review_normalize_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@
PREFERRED_REVIEW_LANGUAGE_RE = re.compile(
r"Preferred review language:\s*`?([A-Za-z]+)`?", re.IGNORECASE
)
# Performance optimization: Pre-compile regex at module level to avoid redundant parsing inside changed_files_from_evidence loops.
LIST_MARKER_RE = re.compile(r"^[-*+]\s+")


def admits_missing_structural_review(reason: str, summary: str) -> bool:
Expand Down Expand Up @@ -530,7 +532,7 @@ def changed_files_from_evidence(text: str) -> list[str]:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
line = re.sub(r"^[-*+]\s+", "", line)
line = LIST_MARKER_RE.sub("", line)
parts = line.split("\t")
path = parts[-1].strip()
if not path or path.startswith("["):
Expand Down
28 changes: 1 addition & 27 deletions scripts/ci/run_opencode_review_model_pool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,6 @@ is_context_overflow_failure() {
grep -Eiq 'ContextOverflowError|tokens_limit_reached|Request body too large|context window' "$opencode_json_file"
}

is_direct_openai_candidate() {
case "$1" in
openai/*) return 0 ;;
*) return 1 ;;
esac
}

should_skip_model_candidate() {
local model_candidate="$1"

if is_direct_openai_candidate "$model_candidate" && [ -z "${OPENAI_API_KEY:-}" ]; then
printf 'Skipping OpenCode %s because OPENAI_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate"
return 0
fi
return 1
}

run_one_model_attempt() {
local model_candidate="$1"
local attempt="$2"
Expand Down Expand Up @@ -186,13 +169,12 @@ run_one_model_attempt() {

main() {
local attempts budget_seconds deadline now remaining model_candidate attempt safe_model prompt_file candidate_output_file
local opencode_json_file opencode_export_file agent retry_sleep original_run_timeout run_status cycle_sleep cycle max_cycles
local opencode_json_file opencode_export_file agent retry_sleep original_run_timeout run_status cycle_sleep cycle
local -a model_candidates

attempts="${OPENCODE_MODEL_ATTEMPTS:-3}"
original_run_timeout="${OPENCODE_RUN_TIMEOUT_SECONDS:-900}"
budget_seconds="${OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-18000}"
max_cycles="${OPENCODE_POOL_MAX_CYCLES:-0}"
deadline=0
if [ "$budget_seconds" -gt 0 ]; then
deadline=$((SECONDS + budget_seconds))
Expand All @@ -210,9 +192,6 @@ main() {
while :; do
printf 'Starting OpenCode model pool cycle %s.\n' "$cycle"
for model_candidate in "${model_candidates[@]}"; do
if should_skip_model_candidate "$model_candidate"; then
continue
fi
assert_reasoning_effort_for_candidate "$model_candidate"
safe_model="${model_candidate//\//-}"
prompt_file="${RUNNER_TEMP}/opencode-review-${safe_model}-prompt.md"
Expand Down Expand Up @@ -264,11 +243,6 @@ main() {
done

printf 'OpenCode completed a full model-candidate cycle without a valid control conclusion; continuing until a model succeeds or the GitHub Actions job timeout is reached.\n'
if [ "$max_cycles" -gt 0 ] && [ "$cycle" -ge "$max_cycles" ]; then
printf 'OpenCode model pool reached configured max cycle count %s without a valid control conclusion.\n' "$max_cycles"
record_review_model ""
exit 1
fi
cycle_sleep="${OPENCODE_POOL_CYCLE_SLEEP_SECONDS:-60}"
if [ "$deadline" -gt 0 ] && [ $((SECONDS + cycle_sleep)) -gt "$deadline" ]; then
cycle_sleep=$((deadline - SECONDS))
Expand Down
Loading
Loading