Required OpenCode Review ContextualWisdomLab/kaefa#85@8b9a3351685b657cd4b8a78119620c9f6d4d2967 #162
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Required OpenCode Review | |
| run-name: >- | |
| Required OpenCode Review ${{ github.event.pull_request.base.repo.full_name || | |
| github.repository }}#${{ github.event.pull_request.number || 'event' }}@${{ | |
| github.event.pull_request.head.sha || github.sha }} | |
| on: | |
| # This required-workflow entrypoint never checks out or executes pull-request | |
| # content and never binds repository secrets. Privileged review execution is | |
| # isolated in opencode-review-dispatch.yml on repository_dispatch only. | |
| pull_request_target: | |
| # `converted_to_draft` is included so a PR going draft mid-poll fires a | |
| # fresh run of this same workflow: the head-scoped concurrency group below | |
| # (`cancel-in-progress: true`) cancels any in-flight non-draft | |
| # "Fail closed without a current-head OpenCode verdict" poll for that | |
| # exact same head. Every non-closed admission path revalidates the live | |
| # PR/head/state before dispatching, exempting, or polling so out-of-order | |
| # draft/ready/closed events cannot publish stale evidence or wait on an | |
| # impossible verdict. | |
| types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] | |
| concurrency: | |
| # Scoped by exact head SHA (not just PR number) so a delayed, out-of-order | |
| # run for an older head cannot cancel the authoritative run already active | |
| # for a newer head -- GitHub cancels whichever run is currently active in | |
| # the group when a new one starts, with no notion of "older"/"newer", so | |
| # sharing a group across different heads let a stale event retire the | |
| # current head's still-valid run before its own live-head check could ever | |
| # reject it (Devin Review on `#1568`). Same-head events (draft<->ready | |
| # transitions, a synchronize retry) still share one group, so | |
| # `converted_to_draft` still cancels an active same-head verdict poll. | |
| group: >- | |
| opencode-review-bootstrap-${{ | |
| github.event.pull_request.base.repo.full_name || github.repository }}-${{ | |
| github.event.pull_request.number || github.run_id }}-${{ | |
| github.event.pull_request.head.sha || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| id-token: write | |
| jobs: | |
| required-workflow-bootstrap: | |
| name: required-workflow-bootstrap | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Materialize the required review workflow | |
| run: >- | |
| echo "Required OpenCode workflow materialized without checking out or | |
| executing pull-request content." | |
| - name: Reject untrusted fork review resource consumption | |
| env: | |
| PR_ACTION: ${{ github.event.action }} | |
| BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} | |
| HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PR_ACTION" = "closed" ]; then | |
| echo "PR closed; fork-resource-consumption check is not required." | |
| exit 0 | |
| fi | |
| if [ -z "$BASE_REPOSITORY" ] || [ -z "$HEAD_REPOSITORY" ] || [ "$HEAD_REPOSITORY" != "$BASE_REPOSITORY" ]; then | |
| echo "::error::Long-running required review is restricted to branches in the base repository. A maintainer must materialize an external contribution on a trusted branch before review." | |
| exit 1 | |
| fi | |
| - name: Resolve immutable central policy source | |
| id: trusted_source | |
| env: | |
| JOB_CONTEXT_JSON: ${{ toJSON(job) }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| WORKFLOW_REF: ${{ github.workflow_ref }} | |
| 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 "{}") | |
| except json.JSONDecodeError as exc: | |
| print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) | |
| raise SystemExit(1) | |
| expected_repository = "ContextualWisdomLab/.github" | |
| expected_file = ".github/workflows/opencode-review.yml" | |
| workflow_sha = str( | |
| job_context.get("workflow_sha") or os.environ.get("WORKFLOW_SHA") or "" | |
| ).strip() | |
| workflow_ref = str( | |
| job_context.get("workflow_ref") or os.environ.get("WORKFLOW_REF") or "" | |
| ).strip() | |
| workflow_ref_head, separator, _ = workflow_ref.partition("@") | |
| if not separator: | |
| print("::error::Required workflow ref is missing its immutable ref separator.", file=sys.stderr) | |
| raise SystemExit(1) | |
| ref_parts = workflow_ref_head.split("/", 2) | |
| if len(ref_parts) < 2 or not ref_parts[0] or not ref_parts[1]: | |
| print("::error::Required workflow ref does not identify a repository.", file=sys.stderr) | |
| raise SystemExit(1) | |
| workflow_repository = "/".join(ref_parts[:2]) | |
| workflow_file_path = str(job_context.get("workflow_file_path") or "").strip() | |
| if not workflow_file_path: | |
| prefix = f"{expected_repository}/{expected_file}@" | |
| if workflow_ref.startswith(prefix): | |
| workflow_file_path = expected_file | |
| if workflow_repository != expected_repository: | |
| print( | |
| f"::error::Required workflow repository resolved to {workflow_repository}, expected {expected_repository}.", | |
| file=sys.stderr, | |
| ) | |
| raise SystemExit(1) | |
| if not re.fullmatch(r"[0-9a-fA-F]{40}", workflow_sha): | |
| print("::error::Required workflow SHA is missing or malformed.", file=sys.stderr) | |
| raise SystemExit(1) | |
| if workflow_file_path != expected_file: | |
| print("::error::Required workflow file path is missing or unexpected.", file=sys.stderr) | |
| raise SystemExit(1) | |
| expected_ref_prefix = f"{expected_repository}/{expected_file}@" | |
| if not workflow_ref.startswith(expected_ref_prefix): | |
| print("::error::Required workflow ref is missing or inconsistent.", file=sys.stderr) | |
| raise SystemExit(1) | |
| print(f"repository={workflow_repository}") | |
| print(f"sha={workflow_sha}") | |
| print(f"workflow_file_path={workflow_file_path}") | |
| PY | |
| - name: Materialize trusted central policy source | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$TRUSTED_SOURCE_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then | |
| echo "::error::Trusted central policy source ref must resolve to the immutable workflow commit SHA before archive materialization." | |
| exit 1 | |
| fi | |
| trusted_archive="${RUNNER_TEMP}/trusted-opencode-policy-source.tar.gz" | |
| trusted_source_dir="${GITHUB_WORKSPACE}/.cwl-required-source" | |
| api_url="${GITHUB_API_URL:-https://api.github.com}" | |
| mkdir -p "$trusted_source_dir" | |
| curl -fsSL \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -o "$trusted_archive" \ | |
| "${api_url}/repos/ContextualWisdomLab/.github/tarball/${TRUSTED_SOURCE_REF}" | |
| python3 - "$trusted_archive" "$trusted_source_dir" <<'PY' | |
| import shutil | |
| import sys | |
| import tarfile | |
| from pathlib import Path, PurePosixPath | |
| archive_path = Path(sys.argv[1]) | |
| root = Path(sys.argv[2]) | |
| try: | |
| if root.is_symlink(): | |
| raise ValueError("trusted source directory must not be a symlink") | |
| if root.exists(): | |
| shutil.rmtree(root) | |
| root.mkdir(parents=True) | |
| with tarfile.open(archive_path, "r:gz") as archive: | |
| members = archive.getmembers() | |
| top_levels: set[str] = set() | |
| targets: set[str] = set() | |
| directories: list[tuple[tarfile.TarInfo, tuple[str, ...]]] = [] | |
| files: list[tuple[tarfile.TarInfo, tuple[str, ...]]] = [] | |
| for member in members: | |
| name = member.name | |
| if not name or name.startswith("/") or "\x00" in name or "\\" in name: | |
| raise ValueError(f"unsafe archive member path: {name!r}") | |
| parts = PurePosixPath(name).parts | |
| if not parts or parts[0] in {".", ".."}: | |
| raise ValueError(f"unsafe archive member path: {name!r}") | |
| top_levels.add(parts[0]) | |
| relative_parts = parts[1:] | |
| if not relative_parts: | |
| if not member.isdir(): | |
| raise ValueError("archive root must be a directory") | |
| continue | |
| if any(part in {"", ".", ".."} for part in relative_parts): | |
| raise ValueError(f"unsafe archive member path: {name!r}") | |
| relative_key = "/".join(relative_parts) | |
| if relative_key in targets: | |
| raise ValueError(f"duplicate archive member path: {relative_key}") | |
| targets.add(relative_key) | |
| if member.isdir(): | |
| directories.append((member, relative_parts)) | |
| elif member.isfile(): | |
| files.append((member, relative_parts)) | |
| else: | |
| raise ValueError(f"unsupported archive member type: {name!r}") | |
| if len(top_levels) != 1: | |
| raise ValueError("archive must contain exactly one top-level directory") | |
| for _member, relative_parts in sorted( | |
| directories, key=lambda item: len(item[1]) | |
| ): | |
| (root / Path(*relative_parts)).mkdir(parents=True, exist_ok=True) | |
| for member, relative_parts in files: | |
| destination = root / Path(*relative_parts) | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| source = archive.extractfile(member) | |
| if source is None: | |
| raise ValueError(f"archive member is not readable: {member.name!r}") | |
| with source, destination.open("xb") as output: | |
| shutil.copyfileobj(source, output) | |
| except (OSError, tarfile.TarError, ValueError) as exc: | |
| raise SystemExit(f"trusted source archive failed closed: {exc}") from exc | |
| PY | |
| - name: Verify immutable central policy source | |
| env: | |
| EXPECTED_FILE: ${{ steps.trusted_source.outputs.workflow_file_path }} | |
| run: | | |
| set -euo pipefail | |
| trusted_source_dir="$GITHUB_WORKSPACE/.cwl-required-source" | |
| if [ ! -f "$trusted_source_dir/$EXPECTED_FILE" ] || [ -L "$trusted_source_dir/$EXPECTED_FILE" ]; then | |
| printf '::error::Required workflow source file is missing or symlinked: %s.\n' \ | |
| "$EXPECTED_FILE" | |
| exit 1 | |
| fi | |
| if [ ! -f "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ] || [ -L "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]; then | |
| echo "::error::Trusted Pingora edge policy helper is missing or symlinked." | |
| exit 1 | |
| fi | |
| - name: Enforce Cloudflare Pingora edge policy | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || 0 }} | |
| PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| EVENT_ACTION: ${{ github.event.action || 'unknown' }} | |
| run: | | |
| set -euo pipefail | |
| python3 .cwl-required-source/scripts/ci/pingora_edge_policy.py \ | |
| --repository "$TARGET_REPOSITORY" \ | |
| --pull-request "$PULL_REQUEST_NUMBER" \ | |
| --head-sha "$PULL_REQUEST_HEAD_SHA" \ | |
| --event-action "$EVENT_ACTION" \ | |
| --api-url "https://api.github.com" | |
| coverage-source-tree: | |
| name: coverage-source-tree | |
| needs: [required-workflow-bootstrap] | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - run: >- | |
| echo "PR-head source and coverage execution are delegated to the | |
| authenticated default-branch OpenCode review dispatch." | |
| coverage-evidence: | |
| name: coverage-evidence | |
| needs: [coverage-source-tree] | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - run: >- | |
| echo "This required-workflow job preserves the stable branch-protection | |
| context without executing pull-request content." | |
| opencode-review-target: | |
| name: opencode-review | |
| needs: [coverage-evidence] | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| id-token: write | |
| steps: | |
| - name: Request current-head OpenCode review execution | |
| if: github.event.action != 'closed' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| OIDC_AUDIENCE: opencode-github-action | |
| OPENCODE_API_BASE_URL: https://api.opencode.ai | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_DRAFT: ${{ github.event.pull_request.draft }} | |
| BASE_BRANCH: ${{ github.event.pull_request.base.ref }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| run: | | |
| set -euo pipefail | |
| live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" | |
| live_draft="$(printf '%s' "$live_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')" | |
| live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" | |
| if [ -z "$live_head" ] || [ -z "$live_draft" ] || [ -z "$live_state" ]; then | |
| echo "::error::Could not validate live pull request state before review dispatch." | |
| exit 1 | |
| fi | |
| if [ "$live_state" != "open" ] && [ "$live_state" != "closed" ]; then | |
| echo "::error::Could not validate live pull request state before review dispatch." | |
| exit 1 | |
| fi | |
| if [ "$live_state" = "closed" ]; then | |
| echo "PR is closed on the live exact head; a current-head OpenCode review is not requested." | |
| exit 0 | |
| fi | |
| if [ "$live_draft" = "true" ]; then | |
| echo "PR is still a draft on the live exact head; a current-head OpenCode review is not requested until it is marked ready for review." | |
| exit 0 | |
| fi | |
| if [ "${live_head,,}" != "${HEAD_SHA,,}" ]; then | |
| echo "Pull request head moved on the live open, ready-for-review PR; a fresh dispatch will fire for the current head." | |
| exit 0 | |
| fi | |
| if [ "$PR_DRAFT" = "true" ]; then | |
| echo "Event draft snapshot is stale; continuing current-head OpenCode review dispatch for the live ready PR." | |
| fi | |
| effective_pr_draft="$live_draft" | |
| helper="$(mktemp)" | |
| trap 'rm -f "$helper"' EXIT | |
| gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=${WORKFLOW_SHA}" \ | |
| --jq .content | base64 --decode >"$helper" | |
| receipt_state="$(python3 - "$helper" "$TARGET_REPOSITORY" "$PR_NUMBER" "$HEAD_SHA" "$effective_pr_draft" <<'PY' | |
| import importlib.machinery | |
| import importlib.util | |
| import sys | |
| helper_path, repository, number, head_sha, draft = sys.argv[1:] | |
| loader = importlib.machinery.SourceFileLoader( | |
| "trusted_opencode_receipt_gate", helper_path | |
| ) | |
| spec = importlib.util.spec_from_loader(loader.name, loader) | |
| if spec is None or spec.loader is None: | |
| raise RuntimeError("trusted OpenCode receipt helper could not be loaded") | |
| gate = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(gate) | |
| reviews = gate.fetch_reviews(repository, int(number)) | |
| receipt, _reason = gate.evaluate_receipts( | |
| reviews, head_sha, is_draft=draft.lower() == "true" | |
| ) | |
| print("present" if receipt is not None else "missing") | |
| PY | |
| )" | |
| if [ "$receipt_state" = "present" ]; then | |
| echo "Current-head substantive OpenCode verdict already exists; scheduler wake skipped." | |
| exit 0 | |
| fi | |
| if [ "$receipt_state" != "missing" ]; then | |
| echo "::error::Trusted OpenCode receipt helper returned an invalid state." | |
| exit 1 | |
| fi | |
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | |
| echo "::error::OpenCode review dispatch requires GitHub OIDC." | |
| exit 1 | |
| fi | |
| separator='&' | |
| [[ "$ACTIONS_ID_TOKEN_REQUEST_URL" == *\?* ]] || separator='?' | |
| oidc_token="$(curl -fsS -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}${separator}audience=${OIDC_AUDIENCE}" | jq -r '.value // empty')" | |
| if [ -z "$oidc_token" ]; then | |
| echo "::error::OpenCode review dispatch could not obtain its OIDC token." | |
| exit 1 | |
| fi | |
| app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -r '.token // empty')" | |
| if [ -z "$app_token" ]; then | |
| echo "::error::OpenCode review dispatch could not obtain its repository-scoped app token." | |
| exit 1 | |
| fi | |
| echo "::add-mask::$app_token" | |
| jq -cn \ | |
| --arg target_repository "$TARGET_REPOSITORY" \ | |
| --arg pr_number "$PR_NUMBER" \ | |
| --arg base_branch "$BASE_BRANCH" \ | |
| '{event_type:"merge-scheduler",client_payload:{target_repository:$target_repository,pr_number:$pr_number,base_branch:$base_branch,max_prs:"1",review_dispatch_limit:"1",trigger_reviews:true,enable_auto_merge:false,update_branches:false,dry_run:false}}' | | |
| GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input - | |
| - name: Fail closed without a current-head OpenCode verdict | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PR_ACTION: ${{ github.event.action }} | |
| PR_DRAFT: ${{ github.event.pull_request.draft }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PR_ACTION" = "closed" ]; then | |
| echo "PR closed; a current-head OpenCode verdict is not required." | |
| exit 0 | |
| fi | |
| if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then | |
| echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict." | |
| exit 1 | |
| fi | |
| live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" | |
| live_draft="$(printf '%s' "$live_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')" | |
| live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" | |
| if [ -z "$live_head" ] || [ -z "$live_draft" ] || [ -z "$live_state" ]; then | |
| echo "::error::Could not validate live pull request state before verdict admission." | |
| exit 1 | |
| fi | |
| if [ "$live_state" != "open" ] && [ "$live_state" != "closed" ]; then | |
| echo "::error::Could not validate live pull request state before verdict admission." | |
| exit 1 | |
| fi | |
| if [ "$live_state" = "closed" ]; then | |
| echo "PR is closed on the live exact head; a current-head OpenCode verdict is not required." | |
| exit 0 | |
| fi | |
| if [ "$live_draft" = "true" ]; then | |
| echo "PR is still a draft on the live exact head; a current-head OpenCode verdict is not required until it is marked ready for review." | |
| exit 0 | |
| fi | |
| if [ "${live_head,,}" != "${HEAD_SHA,,}" ]; then | |
| echo "Pull request head moved on the live open, ready-for-review PR; a fresh poll will start for the current head." | |
| exit 0 | |
| fi | |
| if [ "$PR_DRAFT" = "true" ]; then | |
| echo "Event draft snapshot is stale; continuing verdict polling for the live ready PR." | |
| fi | |
| verdict="" | |
| live_poll_failures=0 | |
| review_poll_failures=0 | |
| max_poll_transport_failures=3 | |
| poll_interval_seconds=60 | |
| # Wall-clock backstop, distinct from max_poll_transport_failures above: | |
| # that counter only bounds *consecutive transport failures*, so a | |
| # review dispatch that never produces a verdict -- while every | |
| # individual `gh api` call keeps succeeding -- previously polled | |
| # forever, holding a live runner for up to GitHub's 360-minute | |
| # platform default job timeout. 10800s (3h) is chosen to stay | |
| # comfortably above this org's own documented "accommodate over 2 | |
| # hours per model" allowance (docs/product-goal-directive.md §8) | |
| # while still releasing the runner well before the platform | |
| # default. This bounds how long the CI job waits for a verdict; it | |
| # does not cap the model's own reasoning/streaming time, which | |
| # remains governed entirely upstream by the dispatched review run | |
| # itself. | |
| poll_deadline_epoch=$(( $(date -u +%s) + 10800 )) | |
| while :; do | |
| if [ "$(date -u +%s)" -ge "$poll_deadline_epoch" ]; then | |
| echo "::error::No current-head OpenCode verdict after 180 minutes of polling; failing closed and releasing the runner." | |
| exit 1 | |
| fi | |
| if ! live_poll_pr="$(timeout 30s gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then | |
| live_poll_failures=$((live_poll_failures + 1)) | |
| if [ "$live_poll_failures" -ge "$max_poll_transport_failures" ]; then | |
| echo "::error::Live pull request read failed ${live_poll_failures} consecutive times while polling; failing closed and releasing the runner." | |
| exit 1 | |
| fi | |
| echo "::warning::Live pull request read failed while polling (${live_poll_failures}/${max_poll_transport_failures}); retrying after revalidation delay." | |
| sleep "$poll_interval_seconds" | |
| continue | |
| fi | |
| live_poll_failures=0 | |
| live_poll_head="$(printf '%s' "$live_poll_pr" | jq -r '.head.sha // empty')" | |
| live_poll_draft="$(printf '%s' "$live_poll_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')" | |
| live_poll_state="$(printf '%s' "$live_poll_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" | |
| if [ -z "$live_poll_head" ] || [ -z "$live_poll_draft" ] || [ -z "$live_poll_state" ]; then | |
| echo "::error::Could not validate live pull request state while polling for a current-head OpenCode verdict." | |
| exit 1 | |
| fi | |
| if [ "$live_poll_state" != "open" ] && [ "$live_poll_state" != "closed" ]; then | |
| echo "::error::Could not validate live pull request state while polling for a current-head OpenCode verdict." | |
| exit 1 | |
| fi | |
| if [ "${live_poll_head,,}" != "${HEAD_SHA,,}" ]; then | |
| echo "::notice::Pull request head moved while waiting for a current-head OpenCode verdict; retiring superseded Required OpenCode Review poll." | |
| exit 0 | |
| fi | |
| if [ "$live_poll_state" = "closed" ]; then | |
| echo "PR closed while waiting for the current-head OpenCode verdict; the poll is no longer required." | |
| exit 0 | |
| fi | |
| if [ "$live_poll_draft" = "true" ]; then | |
| echo "PR became draft while waiting for the current-head OpenCode verdict; the poll is no longer required until it is marked ready for review." | |
| exit 0 | |
| fi | |
| if ! reviews="$(timeout 30s gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")"; then | |
| review_poll_failures=$((review_poll_failures + 1)) | |
| if [ "$review_poll_failures" -ge "$max_poll_transport_failures" ]; then | |
| echo "::error::Reviews API read failed ${review_poll_failures} consecutive times while polling; failing closed and releasing the runner." | |
| exit 1 | |
| fi | |
| echo "::warning::Reviews API read failed while polling (${review_poll_failures}/${max_poll_transport_failures}); revalidating live PR state before retry." | |
| sleep "$poll_interval_seconds" | |
| continue | |
| fi | |
| review_poll_failures=0 | |
| verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" ' | |
| (add // []) | |
| | [ | |
| .[] | |
| | select( | |
| (.user.login // "" | ascii_downcase) as $user | |
| | $user == "opencode-agent" or $user == "opencode-agent[bot]" | |
| ) | |
| | select((.commit_id // "" | ascii_downcase) == ($sha | ascii_downcase)) | |
| | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED") | |
| ] | |
| | (last // {}) as $review | |
| | ($review.body // "" | ascii_downcase) as $body | |
| | if $review.state == "CHANGES_REQUESTED" then | |
| "CHANGES_REQUESTED" | |
| elif $review.state == "APPROVED" | |
| and ($body | contains("deterministic current-head evidence") | not) | |
| and ($body | contains("deterministic fallback approval") | not) | |
| and ($body | contains("model-unavailable evidence fallback") | not) | |
| and ($body | contains("did not emit a usable current-head control block") | not) | |
| and ($body | contains("scope: `unsupported`") | not) | |
| and ($body | contains("model-pool outcome: `unknown`") | not) | |
| then | |
| "APPROVED" | |
| else | |
| empty | |
| end | |
| ')" | |
| if [ -n "$verdict" ]; then | |
| break | |
| fi | |
| sleep "$poll_interval_seconds" | |
| done | |
| if [ -z "$verdict" ]; then | |
| echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. This required check is not a review and must not succeed until the authenticated dispatch posts a current-head verdict." | |
| exit 1 | |
| fi | |
| echo "Current-head OpenCode verdict: ${verdict}." | |
| cancel-superseded-opencode-review-runs: | |
| # Exact-head concurrency protects a newer authoritative run from delayed | |
| # old-head events, while the poll above now revalidates live PR identity on | |
| # every wait iteration so an already-running obsolete poll can self-retire | |
| # without consuming a second runner. This sibling job remains a defense in | |
| # depth for queued/requested old-head runs and for legacy runs created from | |
| # older workflow revisions that lack the in-loop self-retirement check. | |
| # Every cancellation candidate and every cancellation itself is re-verified | |
| # against the live PR head immediately beforehand, so a cleanup run that is | |
| # itself delayed/stale cannot cancel a still-authoritative run. | |
| if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| actions: write | |
| contents: read | |
| pull-requests: read | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} | |
| TARGET_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| CURRENT_RUN_ID: ${{ github.run_id }} | |
| steps: | |
| - name: Cancel queued and running OpenCode review runs for a superseded pull request head | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| live_head_matches() { | |
| local live_head | |
| if ! live_head="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}" --jq '.head.sha' 2>/tmp/opencode-cleanup-gh-error)"; then | |
| echo "::warning::OpenCode review cleanup could not verify the live pull request head; leaving runs unchanged." | |
| sed 's/^/ /' /tmp/opencode-cleanup-gh-error >&2 || true | |
| return 1 | |
| fi | |
| [ "${live_head,,}" = "${TARGET_PR_HEAD_SHA,,}" ] | |
| } | |
| cancel_runs() { | |
| local status="$1" | |
| if ! live_head_matches; then | |
| echo "::notice::OpenCode review cleanup target changed before run selection; leaving runs unchanged." | |
| return 0 | |
| fi | |
| local runs_url="repos/${TARGET_REPOSITORY}/actions/runs?status=${status}&per_page=100" | |
| local runs_json | |
| if ! runs_json="$(gh api --paginate "$runs_url" 2>/tmp/opencode-cleanup-gh-error)"; then | |
| echo "::warning::OpenCode review cleanup could not inspect ${TARGET_REPOSITORY}; leaving runs unchanged." | |
| sed 's/^/ /' /tmp/opencode-cleanup-gh-error >&2 || true | |
| return 0 | |
| fi | |
| local run_ids | |
| if ! run_ids="$(jq -r --arg pr "$TARGET_PR_NUMBER" --arg head_sha "$TARGET_PR_HEAD_SHA" \ | |
| --arg repo "$TARGET_REPOSITORY" --arg current "$CURRENT_RUN_ID" ' | |
| .workflow_runs[] | |
| | select((.id | tostring) != $current) | |
| | select(.name == "Required OpenCode Review") | |
| | select(.event == "pull_request_target") | |
| | ((.display_title // "") | startswith("Required OpenCode Review " + $repo + "#" + $pr + "@")) as $title_matches | |
| | ((.pull_requests // []) | any((.number | tostring) == $pr)) as $metadata_matches | |
| | select($title_matches or $metadata_matches) | |
| | ((.display_title // "") | endswith("@" + $head_sha)) as $title_is_current | |
| | ((.pull_requests // []) | any( | |
| ((.number | tostring) == $pr) | |
| and ((.head.sha // "") | ascii_downcase) == ($head_sha | ascii_downcase) | |
| )) as $metadata_is_current | |
| | select(($title_is_current or $metadata_is_current) | not) | |
| | .id | |
| ' <<<"$runs_json")"; then | |
| echo "::warning::OpenCode review cleanup received invalid run data for ${TARGET_REPOSITORY}; leaving runs unchanged." | |
| return 0 | |
| fi | |
| while IFS= read -r run_id; do | |
| [ -n "$run_id" ] || continue | |
| if ! live_head_matches; then | |
| echo "::notice::OpenCode review cleanup target changed before cancellation; leaving runs unchanged." | |
| return 0 | |
| fi | |
| if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>/tmp/opencode-cleanup-cancel-error || | |
| gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" >/dev/null 2>>/tmp/opencode-cleanup-cancel-error; then | |
| echo "Cancelled superseded Required OpenCode Review run ${run_id} in ${TARGET_REPOSITORY} for PR #${TARGET_PR_NUMBER}." | |
| else | |
| echo "::warning::OpenCode review cleanup could not cancel run ${run_id} in ${TARGET_REPOSITORY}; it may have finished or the credential lacks Actions write access." | |
| sed 's/^/ /' /tmp/opencode-cleanup-cancel-error >&2 || true | |
| fi | |
| done <<<"$run_ids" | |
| } | |
| for active_status in queued in_progress requested waiting pending; do | |
| cancel_runs "$active_status" | |
| done | |
| echo "Superseded OpenCode review run cleanup completed." |