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
87 changes: 82 additions & 5 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ concurrency:
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 == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != '' && format('target-{0}-pr-{1}', github.event.client_payload.target_repository, github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.run_id ||
github.ref }}
Expand Down Expand Up @@ -227,6 +228,80 @@ jobs:
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"

- name: Validate targeted repository dispatch
id: targeted_dispatch
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
TARGET_REPOSITORY_INPUT: ${{ github.event.client_payload.target_repository || '' }}
TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number || '' }}
TARGET_BASE_BRANCH_INPUT: ${{ github.event.client_payload.base_branch || '' }}
ALLOWED_TARGET_REPOSITORIES: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }}
run: |
set -euo pipefail

if [ -z "$TARGET_REPOSITORY_INPUT" ]; then
{
printf 'repository=%s\n' "$GITHUB_REPOSITORY"
printf 'base_branch=%s\n' "$DEFAULT_BRANCH"
} >>"$GITHUB_OUTPUT"
exit 0
fi

if [ "$GITHUB_EVENT_NAME" != "repository_dispatch" ] ||
[ "$GITHUB_REPOSITORY" != "ContextualWisdomLab/.github" ]; then
printf '::error::Targeted scheduler dispatch is restricted to repository_dispatch in ContextualWisdomLab/.github. event=%s execution_repository=%s\n' "$GITHUB_EVENT_NAME" "$GITHUB_REPOSITORY"
exit 1
fi
if ! [[ "$TARGET_REPOSITORY_INPUT" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
printf '::error::Targeted scheduler dispatch rejected an invalid repository or pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY_INPUT:-<empty>}" "${TARGET_PR_NUMBER:-<empty>}"
exit 1
fi

target_allowed=0
IFS=',' read -r -a allowed_targets <<<"$ALLOWED_TARGET_REPOSITORIES"
for allowed_target in "${allowed_targets[@]}"; do
allowed_target="${allowed_target//[[:space:]]/}"
if [ -n "$allowed_target" ] &&
[ "$TARGET_REPOSITORY_INPUT" = "$allowed_target" ]; then
target_allowed=1
break
fi
done
if [ "$target_allowed" -ne 1 ]; then
printf '::error::Targeted scheduler dispatch rejected repository %s because it is absent from the configured exact allowlist.\n' "$TARGET_REPOSITORY_INPUT"
exit 1
fi

pull_json="$(gh api "repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}")"
live_number="$(jq -r '.number // 0' <<<"$pull_json")"
live_state="$(jq -r '.state // empty' <<<"$pull_json")"
live_base_repository="$(jq -r '.base.repo.full_name // empty' <<<"$pull_json")"
live_head_repository="$(jq -r '.head.repo.full_name // empty' <<<"$pull_json")"
live_base_branch="$(jq -r '.base.ref // empty' <<<"$pull_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_json")"
if [ "$live_number" != "$TARGET_PR_NUMBER" ] ||
[ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ] ||
[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ] ||
[ -z "$live_base_branch" ] ||
! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::error::Targeted scheduler dispatch rejected closed, cross-repository, or malformed live PR metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_branch:-<missing>}" "${live_head_sha:-<missing>}"
exit 1
fi
if [ -n "$TARGET_BASE_BRANCH_INPUT" ] &&
[ "$TARGET_BASE_BRANCH_INPUT" != "$live_base_branch" ]; then
printf '::error::Targeted scheduler dispatch base branch does not match the live PR. supplied=%s live=%s\n' "$TARGET_BASE_BRANCH_INPUT" "$live_base_branch"
exit 1
fi

{
printf 'repository=%s\n' "$TARGET_REPOSITORY_INPUT"
printf 'base_branch=%s\n' "$live_base_branch"
printf 'head_sha=%s\n' "$live_head_sha"
} >>"$GITHUB_OUTPUT"
printf 'Validated exact targeted scheduler dispatch for %s#%s at %s on base %s.\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "$live_head_sha" "$live_base_branch"

- name: Resolve trusted scheduler source ref
id: trusted_source
env:
Expand Down Expand Up @@ -405,22 +480,24 @@ jobs:
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 }}
TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }}
TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }}
SCHEDULER_ACTIONS_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }}
# Same-repository dispatch credential: when this scheduler runs inside
# ContextualWisdomLab/.github (the repository the required workflows are
# dispatched on), the runner token can dispatch them without any
# cross-repository PAT. The scheduler only uses it when
# GITHUB_REPOSITORY equals the dispatch repository.
SCHEDULER_DISPATCH_TOKEN: ${{ github.token }}
SCHEDULER_READ_TOKEN: ${{ github.token }}
SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }}
SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github
SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }}
run: |
set -euo pipefail
project_flow="$PROJECT_FLOW_INPUT"
if [ -z "$project_flow" ]; then
case "$DEFAULT_BRANCH" in
case "$TARGET_DEFAULT_BRANCH" in
main|master) project_flow="github-flow" ;;
develop) project_flow="git-flow" ;;
*) project_flow="github-flow" ;;
Expand All @@ -435,8 +512,8 @@ jobs:
branch_update_limit="1"
fi
args=(
--repo "$GITHUB_REPOSITORY"
--base-branch "$DEFAULT_BRANCH"
--repo "$TARGET_REPOSITORY"
--base-branch "$TARGET_DEFAULT_BRANCH"
--max-prs "$MAX_PRS"
--project-flow "$project_flow"
--review-workflow "Required OpenCode Review"
Expand Down
28 changes: 28 additions & 0 deletions docs/org-required-workflow-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ The central `.github/workflows/opencode-review.yml` is now part of the active or
- Model-exhaustion posture: command exit codes and deterministic checks cannot synthesize an approval. Exhaustion remains `MODEL_OUTPUT_UNAVAILABLE`; only a prior real-model approval bound to the exact current head can satisfy the review gate after all checks, alerts, and threads are revalidated.
- Adversarial-evidence posture: every probe must cite its exact changed path and positive in-range line in the materialized current-head source tree. Unrelated paths, nonexistent lines, circular claims, and missing observed results fail closed with a concrete rejection reason.

For a bounded current-head retry in one repository, dispatch `merge-scheduler`
to the central repository with `target_repository`, `pr_number`, and the live
`base_branch`. The target must exactly match
`OPENCODE_REPOSITORY_DISPATCH_TARGETS`; the scheduler then re-reads the open PR
and rejects a noncanonical repository name, fork head, base mismatch, malformed
head SHA, or changed/closed PR before using cross-repository credentials:

```bash
jq -n '{
event_type: "merge-scheduler",
client_payload: {
target_repository: "ContextualWisdomLab/naruon",
pr_number: 1179,
base_branch: "develop",
trigger_reviews: true,
review_dispatch_limit: "1",
enable_auto_merge: false,
update_branches: false,
merge_mode: "disabled"
}
}' | gh api --method POST \
repos/ContextualWisdomLab/.github/dispatches --input -
```

Use the canonical `full_name` returned by the GitHub repository API. Keep
mutation options disabled for an evidence-only retry; enabling branch updates
or merge behavior is a separate operational decision.

Keep the OpenCode required workflow active only while the central workflow keeps proving current-head coverage, CodeGraph initialization, bounded evidence, model review output, and approval-gate publication on the current head.

## Code scanning required workflow posture
Expand Down
9 changes: 8 additions & 1 deletion tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,7 +1464,14 @@ def test_merge_scheduler_uses_escalating_mutation_credentials():
assert "secrets.PR_REVIEW_MERGE_TOKEN" in workflow
assert "secrets.OPENCODE_APPROVE_TOKEN" in workflow
assert "steps.scheduler_app_token.outputs.token" in workflow
assert "SCHEDULER_READ_TOKEN: ${{ github.token }}" in workflow
assert (
"SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' "
"&& github.event.client_payload.target_repository != '' && "
"(secrets.PR_REVIEW_MERGE_TOKEN || "
"secrets.OPENCODE_APPROVE_TOKEN || "
"steps.scheduler_app_token.outputs.token) || github.token }}"
in workflow
)
assert "SCHEDULER_MUTATION_TOKEN_SOURCE" in workflow
assert 'default: "1"' in workflow
assert 'review_dispatch_limit="-1"' in workflow
Expand Down
146 changes: 146 additions & 0 deletions tests/test_opencode_workflow_shell_syntax.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json
import os
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -117,3 +119,147 @@ def test_merge_scheduler_review_followup_run_block_is_valid_bash():
)

assert result.returncode == 0, result.stderr


def test_merge_scheduler_targeted_dispatch_run_block_is_valid_bash():
"""The exact-target allowlist and live-PR validation stays valid Bash."""
if sys.platform == "win32":
return
bash = shutil.which("bash")
if bash is None:
return

workflow_text = (
REPO_ROOT / ".github/workflows/pr-review-merge-scheduler.yml"
).read_text(encoding="utf-8")
script = _extract_run_block(
workflow_text,
"Validate targeted repository dispatch",
)
result = subprocess.run(
[bash, "-n"],
input=script,
text=True,
capture_output=True,
check=False,
)

assert result.returncode == 0, result.stderr


def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path):
"""Only an allowlisted same-repository open PR reaches scheduler outputs."""
if sys.platform == "win32":
return
bash = shutil.which("bash")
jq = shutil.which("jq")
if bash is None or jq is None:
return

workflow_text = (
REPO_ROOT / ".github/workflows/pr-review-merge-scheduler.yml"
).read_text(encoding="utf-8")
script = _extract_run_block(
workflow_text,
"Validate targeted repository dispatch",
)
fake_bin = tmp_path / "bin"
fake_bin.mkdir()
fake_gh = fake_bin / "gh"
fake_gh.write_text(
"""#!/usr/bin/env bash
set -euo pipefail
test "$1" = api
test "$2" = repos/ContextualWisdomLab/naruon/pulls/1179
printf '%s\\n' "$FAKE_PULL_JSON"
""",
encoding="utf-8",
)
fake_gh.chmod(0o755)
pull = {
"number": 1179,
"state": "open",
"base": {
"ref": "develop",
"repo": {"full_name": "ContextualWisdomLab/naruon"},
},
"head": {
"sha": "4afd4af7ad343660356791873d940aa2846f40c2",
"repo": {"full_name": "ContextualWisdomLab/naruon"},
},
}
output = tmp_path / "github-output"
env = {
**os.environ,
"PATH": f"{fake_bin}:{os.environ['PATH']}",
"FAKE_PULL_JSON": json.dumps(pull),
"GITHUB_EVENT_NAME": "repository_dispatch",
"GITHUB_REPOSITORY": "ContextualWisdomLab/.github",
"GITHUB_OUTPUT": str(output),
"DEFAULT_BRANCH": "main",
"TARGET_REPOSITORY_INPUT": "ContextualWisdomLab/naruon",
"TARGET_PR_NUMBER": "1179",
"TARGET_BASE_BRANCH_INPUT": "develop",
"ALLOWED_TARGET_REPOSITORIES": (
"ContextualWisdomLab/.github, ContextualWisdomLab/naruon"
),
}

accepted = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=env,
)

assert accepted.returncode == 0, accepted.stderr
assert output.read_text(encoding="utf-8").splitlines() == [
"repository=ContextualWisdomLab/naruon",
"base_branch=develop",
"head_sha=4afd4af7ad343660356791873d940aa2846f40c2",
]

output.unlink()
rejected_env = {
**env,
"ALLOWED_TARGET_REPOSITORIES": "ContextualWisdomLab/.github",
}
rejected = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=rejected_env,
)

assert rejected.returncode == 1
assert "absent from the configured exact allowlist" in rejected.stdout
assert not output.exists()

output.unlink(missing_ok=True)
cross_repo_pull = {
**pull,
"head": {
**pull["head"],
"repo": {"full_name": "outside/fork"},
},
}
cross_repo_env = {
**env,
"FAKE_PULL_JSON": json.dumps(cross_repo_pull),
}
cross_repo = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=cross_repo_env,
)

assert cross_repo.returncode == 1
assert "cross-repository" in cross_repo.stdout
assert not output.exists()
41 changes: 41 additions & 0 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,47 @@ def test_merge_scheduler_provides_same_repository_dispatch_credential() -> None:
assert workflow.count("SCHEDULER_DISPATCH_TOKEN: ${{ github.token }}") == 2


def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> None:
"""Central single-PR dispatch must validate live metadata before cross-repo use."""
workflow = workflow_text("pr-review-merge-scheduler.yml")
validation = workflow_step(workflow, "Validate targeted repository dispatch")
inspect = workflow_step(workflow, "Inspect PR review and merge queue")

assert "TARGET_REPOSITORY_INPUT:" in validation
assert "TARGET_PR_NUMBER:" in validation
assert "TARGET_BASE_BRANCH_INPUT:" in validation
assert (
"ALLOWED_TARGET_REPOSITORIES: ${{ "
"vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }}"
) in validation
assert 'GITHUB_REPOSITORY" != "ContextualWisdomLab/.github"' in validation
assert "target_allowed=0" in validation
assert '"repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}"' in validation
assert '[ "$live_state" != "open" ]' in validation
assert '[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation
assert '[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation
assert "Targeted scheduler dispatch base branch does not match the live PR" in validation
assert "TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }}" in inspect
assert (
"TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }}"
in inspect
)
assert '--repo "$TARGET_REPOSITORY"' in inspect
assert '--base-branch "$TARGET_DEFAULT_BRANCH"' in inspect
assert 'args+=(--pr-number "$PULL_REQUEST_NUMBER")' in inspect
assert (
"github.event_name == 'repository_dispatch' && "
"github.event.client_payload.target_repository != '' && "
"(secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || "
"steps.scheduler_app_token.outputs.token) || github.token"
) in inspect
assert (
"format('target-{0}-pr-{1}', "
"github.event.client_payload.target_repository, "
"github.event.client_payload.pr_number)"
) in workflow


def test_privileged_review_retries_use_default_branch_repository_dispatch() -> None:
"""Privileged retries must never load workflow code from a selected ref."""
expected_types = {
Expand Down
Loading