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
74 changes: 73 additions & 1 deletion .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
permissions:
contents: read
pull-requests: read
id-token: write
outputs:
target_repository: ${{ steps.validate.outputs.target_repository }}
pr_number: ${{ steps.validate.outputs.pr_number }}
Expand All @@ -46,10 +47,81 @@ jobs:
head_sha: ${{ steps.validate.outputs.head_sha }}
is_private: ${{ steps.validate.outputs.is_private }}
steps:
- name: Exchange OpenCode app token for target repository metadata reads
id: metadata_read_app_token
if: >-
github.event_name == 'repository_dispatch'
&& github.event.client_payload.target_repository != ''
&& github.event.client_payload.target_repository != github.repository
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
run: |
set -euo pipefail

mark_unavailable() {
echo "available=false" >>"$GITHUB_OUTPUT"
}

if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] ||
[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "OpenCode app token exchange unavailable: OIDC request environment is missing."
mark_unavailable
exit 0
fi

request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}"
separator="&"
case "$request_url" in
*\?*) ;;
*) separator="?" ;;
esac

if ! oidc_response="$(
curl -fsS \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${request_url}${separator}audience=${OIDC_AUDIENCE}"
)"; then
echo "OpenCode app token exchange unavailable: OIDC token request did not complete."
mark_unavailable
exit 0
fi

oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")"
if [ -z "$oidc_token" ]; then
echo "OpenCode app token exchange unavailable: OIDC token response was empty."
mark_unavailable
exit 0
fi

if ! token_response="$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
"${OPENCODE_API_BASE_URL}/exchange_github_app_token"
)"; then
echo "OpenCode app token exchange unavailable: app token request did not complete."
mark_unavailable
exit 0
fi

app_token="$(jq -r '.token // empty' <<<"$token_response")"
if [ -z "$app_token" ]; then
echo "OpenCode app token exchange unavailable: app token response was empty."
mark_unavailable
exit 0
fi

echo "::add-mask::$app_token"
{
echo "available=true"
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"

- name: Bind workflow inputs to live organization pull request metadata
id: validate
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
GH_TOKEN: ${{ steps.metadata_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
EVENT_NAME: ${{ github.event_name }}
# A rerun retains github.actor from the original dispatch; authorize
# the identity that initiated the current run or rerun instead.
Expand Down
13 changes: 9 additions & 4 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 == 'schedule' && format('schedule-{0}', github.event.schedule) ||
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 ||
Expand Down Expand Up @@ -563,7 +564,11 @@ jobs:
(github.event_name == 'repository_dispatch' && github.event.client_payload.org_sweep == true)
)
runs-on: ubuntu-latest
timeout-minutes: 30
# The complete organization walk exceeded the legacy 30-minute boundary in
# production. Keep one running and one latest pending */15 sweep through the
# schedule-specific concurrency key above, while allowing the current walk
# enough time to finish instead of cancelling before later repositories.
timeout-minutes: 60
permissions:
actions: write
checks: read
Expand All @@ -583,10 +588,10 @@ jobs:
ORG_SWEEP_MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || vars.ORG_SWEEP_MAX_PRS || '1000' }}
ORG_SWEEP_REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || inputs.review_dispatch_limit || vars.ORG_SWEEP_REVIEW_DISPATCH_LIMIT || '1' }}
ORG_SWEEP_BRANCH_UPDATE_LIMIT: ${{ github.event.client_payload.branch_update_limit || inputs.branch_update_limit || vars.ORG_SWEEP_BRANCH_UPDATE_LIMIT || '1' }}
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}
ORG_SWEEP_MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || 'direct_or_auto' }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}
ORG_SWEEP_STALE_QUEUE_HOURS: ${{ vars.ORG_SWEEP_STALE_QUEUE_HOURS || '24' }}
# A repository the sweep credential structurally cannot read (the OpenCode
# app is not installed there / the PR_REVIEW_MERGE_TOKEN lacks it) returns
Expand Down
5 changes: 5 additions & 0 deletions scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1445,8 +1445,13 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() {
assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "scheduler must not hard-code repository-specific PR bypasses"
assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR"
assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR"
assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan"
assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run"
assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts"
assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk"
assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews"
assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads"
assert_file_contains "$workflow_file" "ORG_SWEEP_UPDATE_BRANCHES: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps refresh eligible stale branches"
assert_file_contains "$workflow_file" 'github.event.workflow_run.pull_requests[0].number' "scheduler scopes OpenCode workflow_run events to the completed review PR"
assert_file_contains "$workflow_file" "github.event.client_payload.trigger_reviews != false" "scheduler enables review dispatch by default for default-branch dispatch events"
assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' || github.event_name == 'push'" "scheduler can dispatch a bounded follow-up OpenCode review after review workflow completion"
Expand Down
17 changes: 17 additions & 0 deletions tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,23 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch():
assert " coverage-source-tree:\n" in workflow
assert " coverage-evidence:\n" in workflow

metadata_start = workflow.index(" validate-pr-metadata:\n")
metadata_end = workflow.index("\n coverage-source-tree:", metadata_start)
metadata_job = workflow[metadata_start:metadata_end]
assert "id-token: write" in metadata_job
assert (
"Exchange OpenCode app token for target repository metadata reads"
in metadata_job
)
assert (
"GH_TOKEN: ${{ steps.metadata_read_app_token.outputs.token || "
"secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}"
) in metadata_job
assert (
"github.event.client_payload.target_repository != github.repository"
in metadata_job
)

source_start = workflow.index(" coverage-source-tree:\n")
source_end = workflow.index("\n coverage-evidence:", source_start)
source_job = workflow[source_start:source_end]
Expand Down
24 changes: 20 additions & 4 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,9 @@ def test_org_queue_sweep_covers_target_repositories_on_a_heartbeat() -> None:
github.token silently), skip the central repository itself, and fail with a
visible reason when it cannot mutate sibling repositories. The sweep runs
every 15 minutes so an approval that lands after a PR's last event is
auto-updated/merged within ~15 minutes instead of idling for up to an hour.
auto-updated/merged promptly instead of idling indefinitely. Its cron has a
distinct concurrency key from the separate 30-minute scan, and the job has
enough runtime headroom to finish a complete organization walk.
"""
workflow = workflow_text("pr-review-merge-scheduler.yml")

Expand All @@ -544,6 +546,20 @@ def test_org_queue_sweep_covers_target_repositories_on_a_heartbeat() -> None:
assert "github.repository == 'ContextualWisdomLab/.github'" in workflow
assert "github.event.schedule == '*/15 * * * *'" in workflow
assert "github.event.client_payload.org_sweep == true" in workflow
assert (
"github.event_name == 'schedule' && format('schedule-{0}', "
"github.event.schedule)"
) in workflow
org_sweep_header = workflow.split(" org-queue-sweep:", 1)[1].split(
" permissions:", 1
)[0]
assert "timeout-minutes: 60" in org_sweep_header
for setting in (
"ORG_SWEEP_TRIGGER_REVIEWS",
"ORG_SWEEP_ENABLE_AUTO_MERGE",
"ORG_SWEEP_UPDATE_BRANCHES",
):
assert f"{setting}: ${{{{ github.event_name == 'schedule' ||" in workflow
# The single-repository scan must not double-run on the sweep cron.
assert "github.event.schedule != '*/15 * * * *'" in workflow
assert "github.event.client_payload.org_sweep != true" in workflow
Expand Down Expand Up @@ -636,18 +652,18 @@ def test_org_queue_sweep_manual_cadence_inputs_reach_the_sweep_job() -> None:
"ORG_SWEEP_MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || vars.ORG_SWEEP_MAX_PRS || '1000' }}"
) in workflow
assert (
"ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}"
"ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}"
in workflow
)
assert (
"ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}"
"ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}"
) in workflow
assert (
"ORG_SWEEP_MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || 'direct_or_auto' }}"
in workflow
)
assert (
"ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}"
"ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}"
in workflow
)
assert 'if [ "$ORG_SWEEP_TRIGGER_REVIEWS" = "true" ]; then' in workflow
Expand Down
Loading