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
56 changes: 53 additions & 3 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5782,7 +5782,7 @@ jobs:
local successful_names_file="$2"
local output_file="$3"

awk '
awk -v successful_names_file="$successful_names_file" '
BEGIN {
while ((getline name < successful_names_file) > 0) {
successful[name] = 1
Expand All @@ -5803,7 +5803,7 @@ jobs:
}
print
}
' successful_names_file="$successful_names_file" "$input_file" >"$output_file"
' "$input_file" >"$output_file"
}

collect_current_head_commit_check_runs() {
Expand Down Expand Up @@ -6648,7 +6648,9 @@ jobs:
rm -f "$lookup_error_file"

printf '%s\n' "$reviews_json" |
python3 scripts/ci/opencode_existing_approval_gate.py --head "$HEAD_SHA"
python3 scripts/ci/opencode_existing_approval_gate.py \
--head "$HEAD_SHA" \
--require-opencode-app
}

request_changes_for_merge_conflict_if_present() {
Expand Down Expand Up @@ -7172,13 +7174,61 @@ jobs:
GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref || github.event.inputs.pr_base_ref || '' }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha || '' }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::warning::Merge scheduler follow-up skipped after approval because no mutation credential was available. Required-workflow PR events and schedules remain authoritative."
exit 0
fi

if [ -z "${PR_NUMBER:-}" ] || [[ ! "${PR_HEAD_SHA:-}" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::warning::Merge scheduler follow-up skipped because the exact pull request number or 40-character head SHA was unavailable. Repository=%s PR=%s head=%s.\n' "$GH_REPOSITORY" "${PR_NUMBER:-missing}" "${PR_HEAD_SHA:-missing}"
exit 0
fi

approval_read_token="${SCHEDULER_READ_TOKEN:-${GH_TOKEN:-}}"
approval_visible=0
approval_reason="current-head OpenCode App approval is not visible"
for approval_attempt in 1 2 3 4 5 6; do
approval_error_file="$(mktemp)"
gate_error_file="$(mktemp)"
if reviews_json="$(
GH_TOKEN="$approval_read_token" timeout 30s \
gh api --paginate --slurp \
"repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews" \
2>"$approval_error_file"
)"; then
if printf '%s\n' "$reviews_json" |
python3 scripts/ci/opencode_existing_approval_gate.py \
--head "$PR_HEAD_SHA" \
--require-opencode-app \
2>"$gate_error_file"; then
approval_visible=1
printf 'Current-head OpenCode App approval is visible for %s#%s at %s after publication attempt %s.\n' "$GH_REPOSITORY" "$PR_NUMBER" "$PR_HEAD_SHA" "$approval_attempt"
rm -f "$approval_error_file" "$gate_error_file"
break
fi
approval_reason="$(tail -n 1 "$gate_error_file" 2>/dev/null || true)"
[ -n "$approval_reason" ] || approval_reason="current-head OpenCode App approval failed validation"
else
approval_reason="$(tail -n 1 "$approval_error_file" 2>/dev/null || true)"
[ -n "$approval_reason" ] || approval_reason="GitHub review API lookup failed without an error body"
fi
rm -f "$approval_error_file" "$gate_error_file"

if [ "$approval_attempt" -lt 6 ]; then
approval_delay="$((approval_attempt * 2))"
printf 'Current-head OpenCode App approval for %s#%s at %s is not ready after publication attempt %s: %s. Retrying in %ss.\n' "$GH_REPOSITORY" "$PR_NUMBER" "$PR_HEAD_SHA" "$approval_attempt" "$approval_reason" "$approval_delay"
sleep "$approval_delay"
fi
done

if [ "$approval_visible" -ne 1 ]; then
printf '::warning::Merge scheduler follow-up skipped because current-head OpenCode App approval did not become visible after publication. Repository=%s PR=%s head=%s reason=%s. The review-event and scheduled scheduler paths remain authoritative.\n' "$GH_REPOSITORY" "$PR_NUMBER" "$PR_HEAD_SHA" "$approval_reason"
exit 0
fi

default_branch="$(
gh api "repos/${GH_REPOSITORY}" --jq '.default_branch // empty' 2>/dev/null || true
)"
Expand Down
110 changes: 109 additions & 1 deletion .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches: [main, develop, master]
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, auto_merge_enabled, closed]
pull_request_review:
types: [submitted, dismissed]
workflow_run:
workflows: ["Required OpenCode Review", "Strix Security Scan"]
types: [completed]
Expand Down Expand Up @@ -137,13 +139,14 @@ concurrency:
group: >-
central-pr-review-merge-scheduler-${{ github.repository }}-${{
github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'pull_request_review' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) ||
github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) ||
github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) ||
github.event_name == 'workflow_dispatch' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) ||
github.event_name == 'workflow_dispatch' && github.run_id ||
github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'workflow_dispatch' }}

# Scorecard Token-Permissions (alert #9): declare a least-privilege default at
# the workflow level. The scan-pr-queue job that actually needs write access
Expand Down Expand Up @@ -343,7 +346,112 @@ jobs:
- name: Self-test scheduler
run: python3 scripts/ci/pr_review_merge_scheduler.py --self-test

- name: Wait for approved OpenCode publication run to finish
id: review_followup
if: >-
github.event_name == 'pull_request_review'
&& github.event.action == 'submitted'
&& github.event.review.state == 'approved'
&& (
github.event.review.user.login == 'opencode-agent'
|| github.event.review.user.login == 'opencode-agent[bot]'
)
env:
GH_TOKEN: ${{ github.token }}
REVIEW_HEAD_SHA: ${{ github.event.review.commit_id }}
REVIEW_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
echo "proceed=true" >>"$GITHUB_OUTPUT"

if [[ ! "${REVIEW_HEAD_SHA:-}" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::warning::Post-approval direct-merge follow-up skipped because the OpenCode App review did not carry a 40-character commit SHA. value=%s.\n' "${REVIEW_HEAD_SHA:-missing}"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi

pull_error_file="$(mktemp)"
if ! pull_json="$(
gh api "repos/${GITHUB_REPOSITORY}/pulls/${REVIEW_PR_NUMBER}" \
2>"$pull_error_file"
)"; then
pull_reason="$(tail -n 1 "$pull_error_file" 2>/dev/null || true)"
[ -n "$pull_reason" ] || pull_reason="GitHub pull-request lookup failed without an error body"
rm -f "$pull_error_file"
printf '::warning::Post-approval direct-merge follow-up skipped because the live pull request snapshot could not be read. PR=%s review_head=%s reason=%s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$pull_reason"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi
rm -f "$pull_error_file"
live_state="$(jq -r '.state // "unknown"' <<<"$pull_json")"
live_head="$(jq -r '.head.sha // empty' <<<"$pull_json")"
if [ "$live_state" != "open" ] || [ "$live_head" != "$REVIEW_HEAD_SHA" ]; then
printf '::notice::Post-approval direct-merge follow-up skipped because the pull request snapshot changed. PR=%s review_head=%s live_head=%s state=%s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "${live_head:-missing}" "$live_state"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi

opencode_state="missing"
opencode_reason="no opencode-review check run was visible for the approved head"
for check_attempt in 1 2 3 4 5 6 7 8; do
check_error_file="$(mktemp)"
if checks_json="$(
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/commits/${REVIEW_HEAD_SHA}/check-runs?per_page=100" \
2>"$check_error_file"
)"; then
opencode_state="$(
jq -r '
[.[].check_runs[]
| select(.name == "opencode-review")
| select(.app.slug == "github-actions")] as $runs
| if ($runs | length) == 0 then "missing"
elif any($runs[]; .status != "completed") then "running"
elif any($runs[]; .conclusion != "success") then
"failed:" + ([$runs[] | (.conclusion // "missing")] | unique | join(","))
else "success"
end
' <<<"$checks_json"
)"
case "$opencode_state" in
success)
printf 'Approved OpenCode publication run completed successfully for PR %s at %s after check attempt %s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$check_attempt"
rm -f "$check_error_file"
break
;;
failed:*)
opencode_reason="opencode-review completed without success (${opencode_state#failed:})"
rm -f "$check_error_file"
break
;;
running)
opencode_reason="opencode-review is still running for the approved head"
;;
*)
opencode_reason="no opencode-review check run was visible for the approved head"
;;
esac
else
opencode_state="api-error"
opencode_reason="$(tail -n 1 "$check_error_file" 2>/dev/null || true)"
[ -n "$opencode_reason" ] || opencode_reason="GitHub check-runs lookup failed without an error body"
fi
rm -f "$check_error_file"

if [ "$check_attempt" -lt 8 ]; then
check_delay="$((check_attempt * 2))"
printf 'Approved OpenCode publication run is not complete for PR %s at %s after check attempt %s: %s. Retrying in %ss.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$check_attempt" "$opencode_reason" "$check_delay"
sleep "$check_delay"
fi
done

if [ "$opencode_state" != "success" ]; then
printf '::warning::Post-approval direct-merge follow-up skipped because the approved OpenCode publication run did not complete successfully. PR=%s head=%s state=%s reason=%s. The scheduled organization sweep remains authoritative.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$opencode_state" "$opencode_reason"
echo "proceed=false" >>"$GITHUB_OUTPUT"
fi

- name: Inspect PR review and merge queue
if: steps.review_followup.outputs.proceed != 'false'
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
SCHEDULER_ACTIONS_TOKEN: ${{ github.token }}
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ its own `pull_request_target` job token to repository-write permission; its
immediate post-approval scheduler follow-up uses only an explicit merge token or
the OpenCode app token, otherwise it leaves the separate scheduler required
workflow and schedule authoritative.
Post-approval reuse and follow-up accept only an exact-head review authored by
the OpenCode GitHub App; a GitHub Actions-authored review is not OpenCode
approval evidence. The separate scheduler also listens for that App review,
waits for the publishing OpenCode check to finish, and then retries direct merge
outside the review job when repository auto-merge is unavailable. Every merge
keeps `--match-head-commit`; it prefers squash and retries with a merge commit
only when the target repository explicitly reports that squash is disabled.
Superseded queued or running workflow cleanup remains mandatory, but a GitHub
cancel or force-cancel API failure cannot make an old head authoritative or
block a policy-clean current head. The scheduler logs the exact run id and
bounded API error as an Actions warning, then continues the current-head
decision.
That `update_branch` path is deliberately not used for `DIRTY` or
`CONFLICTING` PRs: GitHub cannot synthesize a safe conflict resolution for the
author, so the merge scheduler must give the author a repair path instead of pretending
Expand Down Expand Up @@ -78,7 +90,11 @@ PoC/execution result. It must also split `Developer experience:` from
product, documentation, review-comment, or status-check reader outcomes. The PoC
can be a temporary scratch repro, focused test, lint, security check,
performance probe, or UI verification command, but it must be actually run and
cited. Execution evidence must be sandboxed in the CI workspace or an isolated
cited. Every adversarial probe must also state an observed result such as an
exit code, passed or failed test/assertion, rejected input, log value, or source
trace outcome. Generic `source inspection` or `test coverage verifies` prose
without that observation is not reusable approval evidence. Execution evidence
must be sandboxed in the CI workspace or an isolated
temporary directory, with a credential-scrubbed environment by default and no
persistent mutation outside test caches or scratch files. When repo-native
verification legitimately needs network access or GitHub Secrets, pass only the
Expand Down
49 changes: 49 additions & 0 deletions scripts/ci/adversarial_evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Validate that an adversarial probe cites independent proof."""

from __future__ import annotations

import re


CIRCULAR_EVIDENCE_PHRASES = (
"handles this case",
"properly handles all cases",
"works as expected",
"is correct",
"is safe",
"no issues found",
)
INDEPENDENT_PROOF_RE = re.compile(
r"\b(?:assert(?:ion|ed|s)?|check|codegraph|command|coverage|diff|exit code|"
r"gate|log|run|sarif|source|test(?:ed|ing|s)?|trace)\b|\bline\s+[1-9][0-9]*\b",
re.IGNORECASE,
)
OBSERVED_RESULT_RE = re.compile(
r"\b(?:blocked|confirmed|contains?|disproved|exit code\s+[0-9]+|failed|matched|"
r"observed|pass(?:ed)?|raised|rejected|rejects|reported|returned|showed)\b",
re.IGNORECASE,
)


def adversarial_evidence_rejection_reason(evidence: str, path: str) -> str | None:
"""Return why probe evidence is circular or lacks a concrete proof anchor."""
cleaned = evidence.strip()
lowered = cleaned.casefold()
if any(phrase in lowered for phrase in CIRCULAR_EVIDENCE_PHRASES):
return "repeats the implementation claim instead of citing independent proof"
if path and path.casefold() in lowered:
has_proof_anchor = True
else:
has_proof_anchor = INDEPENDENT_PROOF_RE.search(cleaned) is not None
if not has_proof_anchor:
return (
"must cite an executed command, test/assertion, log/check/SARIF receipt, "
"source trace, diff, CodeGraph path, or exact changed file"
)
if not OBSERVED_RESULT_RE.search(cleaned):
return (
"must state the observed proof result, such as an exit code, passed or failed "
"test/assertion, rejected input, log value, or source-trace outcome"
)
return None
1 change: 0 additions & 1 deletion scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
PRIMARY_REVIEW_AUTHORS = {
"opencode-agent[bot]",
"opencode-agent",
"github-actions[bot]",
}
PRIMARY_REVIEW_MARKERS = (
"OpenCode reviewed the current-head bounded evidence and found no blocking issues.",
Expand Down
Loading
Loading