Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
42 changes: 39 additions & 3 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1828,9 +1828,10 @@ jobs:
# Coverage and current-head evidence are prepared before the model pool.
# A single legitimate review may need a full hour. The enclosing job must
# contain the 12-minute evidence step, 205-minute provider-pool step, the
# 36-minute publication gate, and setup/cleanup overhead without truncating
# a late current-head verdict or its bounded failure reason.
timeout-minutes: 300
# 36-minute publication gate, the 18-minute Noema handoff, and setup/cleanup
# overhead without truncating a late current-head verdict, handoff, merge
# scheduler follow-up, or bounded failure reason.
timeout-minutes: 325
permissions:
actions: read
checks: read
Expand Down Expand Up @@ -7390,6 +7391,37 @@ jobs:
-f target_url="$RUN_URL" \
-f description="$description" >/dev/null

- name: Dispatch Noema after current-head OpenCode approval
if: >-
always()
&& github.event_name == 'repository_dispatch'
&& needs.validate-pr-metadata.outputs.target_repository != ''
&& needs.validate-pr-metadata.outputs.pr_number != ''
&& needs.validate-pr-metadata.outputs.head_sha != ''
continue-on-error: true
timeout-minutes: 18
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }}
GH_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }}
PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }}
PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }}
OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt
OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }}
OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head
OPENCODE_REQUIRE_ADVERSARIAL_VALIDATION: "true"
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::warning::Noema handoff skipped because no target-repository dispatch credential was available."
exit 1
fi
python3 scripts/ci/noema_review_handoff.py \
--repo "$GH_REPOSITORY" \
--pr-number "$PR_NUMBER" \
--head-sha "$PR_HEAD_SHA" \
--attempts 90 \
--interval-seconds 10

- name: Run merge scheduler after approval
continue-on-error: true
env:
Expand All @@ -7401,6 +7433,10 @@ jobs:
PR_BASE_REF: ${{ needs.validate-pr-metadata.outputs.base_ref }}
PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }}
PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }}
OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt
OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }}
OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head
OPENCODE_REQUIRE_ADVERSARIAL_VALIDATION: "true"
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
Expand Down
6 changes: 5 additions & 1 deletion scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,11 @@ def existing_noema_review(pr: dict[str, Any], actor: str) -> bool:
continue
if str(review.get("state") or "").upper() not in {"APPROVED", "CHANGES_REQUESTED", "COMMENTED"}:
continue
if review_author(review) == actor or marker in str(review.get("body") or ""):
if (
actor
and review_author(review) == actor
and marker in str(review.get("body") or "")
):
return True
return False

Expand Down
293 changes: 293 additions & 0 deletions scripts/ci/noema_review_handoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""Dispatch Noema after a current-head OpenCode approval and await its verdict."""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
import time
from collections.abc import Callable, Sequence
from typing import Any, TextIO

from scripts.ci.opencode_existing_approval_gate import (
flatten_reviews,
has_reusable_real_model_approval,
)
from scripts.ci.redact_sensitive_log import redact_text


REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$")
SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
GH_COMMAND_TIMEOUT_SECONDS = 60.0
MAX_TRANSIENT_BACKOFF_MULTIPLIER = 4
NOEMA_REVIEW_AUTHOR = "cwl-noema-review[bot]"
NOEMA_REVIEW_MARKER = "<!-- noema-review-gate "
TERMINAL_NOEMA_STATES = {"APPROVED", "CHANGES_REQUESTED", "COMMENTED"}


GhRunner = Callable[[Sequence[str], str | None], str]
ApprovalChecker = Callable[..., bool]


def run_gh(args: Sequence[str], stdin: str | None = None) -> str:
"""Run a GitHub CLI command without invoking a shell."""
try:
completed = subprocess.run(
["gh", *args],
check=False,
input=stdin,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=GH_COMMAND_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
raise RuntimeError(
f"gh command timed out after {GH_COMMAND_TIMEOUT_SECONDS:g} seconds"
) from None
if completed.returncode != 0:
detail = redact_text(completed.stderr or completed.stdout).strip()
raise RuntimeError(
detail or f"gh command failed with exit code {completed.returncode}"
)
return completed.stdout
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def fetch_head(repo: str, number: int, *, runner: GhRunner = run_gh) -> str:
"""Return the live pull request head SHA."""
return runner(
["api", f"repos/{repo}/pulls/{number}", "--jq", ".head.sha // empty"],
None,
).strip()


def fetch_reviews(
repo: str,
number: int,
*,
runner: GhRunner = run_gh,
) -> list[dict[str, Any]]:
"""Return the complete flattened pull request review history."""
document = json.loads(
runner(
[
"api",
"--paginate",
"--slurp",
f"repos/{repo}/pulls/{number}/reviews",
],
None,
)
or "[]"
)
return flatten_reviews(document)


def noema_review_state(reviews: list[dict[str, Any]], head_sha: str) -> str | None:
"""Return Noema's latest terminal verdict for the exact current head."""
for review in reversed(reviews):
if str(review.get("commit_id") or "").lower() != head_sha.lower():
continue
author = str((review.get("user") or {}).get("login") or "").lower()
if author != NOEMA_REVIEW_AUTHOR:
continue
if NOEMA_REVIEW_MARKER not in str(review.get("body") or ""):
continue
state = str(review.get("state") or "").upper()
if state in TERMINAL_NOEMA_STATES:
return state
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def dispatch_noema(
repo: str,
number: int,
head_sha: str,
*,
runner: GhRunner = run_gh,
) -> None:
"""Dispatch the target repository's default-branch Noema workflow."""
payload = {
"event_type": "noema-review",
"client_payload": {
"target_repository": repo,
"pr_number": number,
"pr_head_sha": head_sha,
},
}
runner(
["api", "-X", "POST", f"repos/{repo}/dispatches", "--input", "-"],
json.dumps(payload),
)


def transient_backoff_seconds(consecutive_failures: int, interval_seconds: float) -> float:
"""Return bounded exponential backoff based on the configured poll interval."""
exponent = max(consecutive_failures - 1, 0)
multiplier = min(2**exponent, MAX_TRANSIENT_BACKOFF_MULTIPLIER)
return interval_seconds * multiplier


def run_handoff(
repo: str,
number: int,
head_sha: str,
*,
attempts: int,
interval_seconds: float,
runner: GhRunner = run_gh,
sleeper: Callable[[float], None] = time.sleep,
approval_checker: ApprovalChecker = has_reusable_real_model_approval,
log: TextIO | None = None,
) -> int:
"""Verify OpenCode, dispatch Noema, and wait for an exact-head verdict."""
log = log or sys.stderr
dispatched = False
consecutive_failures = 0
for attempt in range(1, attempts + 1):
try:
live_head = fetch_head(repo, number, runner=runner)
reviews = fetch_reviews(repo, number, runner=runner)
except RuntimeError as exc:
consecutive_failures += 1
detail = redact_text(str(exc)).strip() or "GitHub API call failed"
if attempt < attempts:
delay = transient_backoff_seconds(
consecutive_failures,
interval_seconds,
)
print(
"Transient GitHub API failure during Noema handoff "
f"poll {attempt}/{attempts}: {detail}; retrying in {delay:g}s.",
file=log,
)
sleeper(delay)
else:
print(
"Noema handoff exhausted its bounded polls after a transient "
f"GitHub API failure: {detail}.",
file=log,
)
continue

if live_head.lower() != head_sha.lower():
action = (
"stopped because the pull request head changed"
if dispatched
else "refused stale input because the pull request head changed"
)
print(
f"Noema handoff {action}: "
f"expected {head_sha}, observed {live_head or '<missing>'}.",
file=log,
)
return 2

if not dispatched and not approval_checker(reviews, head_sha, log=log):
print(
"Noema handoff skipped because the exact head has no reusable "
"OpenCode App real-model approval.",
file=log,
)
return 1

state = noema_review_state(reviews, head_sha)
if state is not None:
timing = "already published" if not dispatched else "published"
print(
f"Noema {timing} {state} for {repo}#{number} at {head_sha} "
f"after poll {attempt}/{attempts}.",
file=log,
)
return 0 if state == "APPROVED" else 1

if not dispatched:
try:
dispatch_noema(repo, number, head_sha, runner=runner)
except RuntimeError as exc:
consecutive_failures += 1
detail = redact_text(str(exc)).strip() or "GitHub API call failed"
if attempt < attempts:
delay = transient_backoff_seconds(
consecutive_failures,
interval_seconds,
)
print(
"Transient GitHub API failure while dispatching Noema "
f"on poll {attempt}/{attempts}: {detail}; "
f"retrying in {delay:g}s.",
file=log,
)
sleeper(delay)
else:
print(
"Noema dispatch exhausted its bounded polls after a "
f"transient GitHub API failure: {detail}.",
file=log,
)
continue
dispatched = True
print(
f"Dispatched default-branch Noema review for {repo}#{number} "
f"at {head_sha}.",
file=log,
)

consecutive_failures = 0
if attempt < attempts:
sleeper(interval_seconds)

if dispatched:
print(
f"Noema did not publish an exact-head verdict after {attempts} polls; "
"the merge scheduler will retain the two-reviewer policy.",
file=log,
)
else:
print(
f"Noema was not dispatched after {attempts} bounded polls; "
"the merge scheduler will retain the two-reviewer policy.",
file=log,
)
return 1


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
"""Parse and validate handoff arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", required=True)
parser.add_argument("--pr-number", required=True, type=int)
parser.add_argument("--head-sha", required=True)
parser.add_argument("--attempts", type=int, default=90)
parser.add_argument("--interval-seconds", type=float, default=10.0)
args = parser.parse_args(argv)
if not REPOSITORY_RE.fullmatch(args.repo):
parser.error("--repo must name a ContextualWisdomLab repository")
if args.pr_number < 1:
parser.error("--pr-number must be positive")
if not SHA_RE.fullmatch(args.head_sha):
parser.error("--head-sha must be a 40-character Git SHA")
if args.attempts < 1:
parser.error("--attempts must be positive")
if args.interval_seconds < 0:
parser.error("--interval-seconds must be non-negative")
return args


def main(argv: Sequence[str] | None = None) -> int:
"""Run the Noema handoff command."""
args = parse_args(argv)
return run_handoff(
args.repo,
args.pr_number,
args.head_sha,
attempts=args.attempts,
interval_seconds=args.interval_seconds,
)


if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
2 changes: 1 addition & 1 deletion scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "skipping remaining attempts for this model" "opencode review skips same-model retries after context-window overflow"
assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" "exceeded your current quota" "strix wrapper neutralizes quota-only provider failures without vulnerability reports"
assert_file_contains "$REPO_ROOT/scripts/ci/strix_quick_gate.sh" "billing details" "strix quick gate classifies provider quota starvation as infrastructure"
assert_file_contains "$workflow_file" 'timeout-minutes: 300' "opencode review target contains evidence, the bounded long-review pool, publication, and cleanup overhead"
assert_file_contains "$workflow_file" 'timeout-minutes: 325' "opencode review target contains evidence, the bounded long-review pool, publication, Noema handoff, and cleanup overhead"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode evidence preparation fails closed before it ties up the review queue"
assert_file_contains "$workflow_file" 'timeout-minutes: 205' "opencode model pool preserves full-hour candidates within a bounded provider-pool window"
assert_file_contains "$workflow_file" 'timeout-minutes: 34' "opencode fast approval publication is bounded around the dynamic image and package/GPU check wait"
Expand Down
Loading
Loading