Skip to content

๐Ÿ›ก๏ธ Sentinel: [๋ณด์•ˆ ๊ฐœ์„ ] ์–ธ์–ด ์„ค์ • ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ(Input Validation) ์ถ”๊ฐ€ #138

๐Ÿ›ก๏ธ Sentinel: [๋ณด์•ˆ ๊ฐœ์„ ] ์–ธ์–ด ์„ค์ • ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ(Input Validation) ์ถ”๊ฐ€

๐Ÿ›ก๏ธ Sentinel: [๋ณด์•ˆ ๊ฐœ์„ ] ์–ธ์–ด ์„ค์ • ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ(Input Validation) ์ถ”๊ฐ€ #138

name: Required PR Review Merge Scheduler
on:
push:
branches: [main, develop, master]
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, auto_merge_enabled, closed]
workflow_run:
workflows: ["Required OpenCode Review", "Strix Security Scan"]
types: [completed]
workflow_call:
inputs:
dry_run:
description: Print planned actions without mutating PRs
required: false
default: false
type: boolean
max_prs:
description: Maximum open PRs to inspect
required: false
default: "100"
type: string
pr_number:
description: Optional single pull request number to inspect immediately
required: false
default: ""
type: string
trigger_reviews:
description: Dispatch OpenCode Review for PR heads without current approval
required: false
default: true
type: boolean
review_dispatch_limit:
description: Maximum OpenCode/Strix review dispatch actions per scheduler run
required: false
default: "1"
type: string
enable_auto_merge:
description: Enable auto-merge for current-head approved PRs
required: false
default: true
type: boolean
merge_mode:
description: "Merge behavior for current-head approved PRs: direct_or_auto, auto, direct, or disabled"
required: false
default: direct_or_auto
type: string
update_branches:
description: Update outdated PR branches after OpenCode approval
required: false
default: true
type: boolean
stale_opencode_minutes:
description: Redispatch OpenCode Review when an in-progress OpenCode check is older than this many minutes
required: false
default: "420"
type: string
project_flow:
description: Project flow, usually github-flow or git-flow
required: false
default: ""
type: string
base_branch:
description: Base branch to scan; defaults to the caller repository default branch
required: false
default: ""
type: string
canonical_ref:
description: Ref of ContextualWisdomLab/.github to use for scheduler code
required: false
default: "main"
type: string
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
inputs:
dry_run:
description: Print planned actions without mutating PRs
required: false
default: false
type: boolean
max_prs:
description: Maximum open PRs to inspect
required: false
default: "100"
pr_number:
description: Optional single pull request number to inspect immediately
required: false
default: ""
trigger_reviews:
description: Dispatch OpenCode Review for PR heads without current approval
required: false
default: true
type: boolean
review_dispatch_limit:
description: Maximum OpenCode/Strix review dispatch actions per scheduler run
required: false
default: "1"
enable_auto_merge:
description: Enable auto-merge for current-head approved PRs
required: false
default: true
type: boolean
merge_mode:
description: "Merge behavior for current-head approved PRs: direct_or_auto, auto, direct, or disabled"
required: false
default: direct_or_auto
update_branches:
description: Update outdated PR branches after OpenCode approval
required: false
default: true
type: boolean
stale_opencode_minutes:
description: Redispatch OpenCode Review when an in-progress OpenCode check is older than this many minutes
required: false
default: "420"
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 == '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' }}
jobs:
cancel-closed-pr-runs:
if: github.event_name == 'pull_request_target' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: echo "PR closed; this run only cancels older runs through workflow concurrency."
scan-pr-queue:
# workflow_dispatch review runs do not reliably carry pull_requests metadata.
# Without this guard, one completed central review can wake a repo-wide scan.
if: >-
(
github.event_name != 'pull_request_target' ||
github.event.action != 'closed'
) &&
(
github.event_name != 'workflow_run' ||
(
github.event.workflow_run.conclusion != 'cancelled' &&
github.event.workflow_run.pull_requests[0].number
)
)
runs-on: ubuntu-latest
permissions:
actions: write
checks: read
contents: write
id-token: write
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ inputs.base_branch || github.event.repository.default_branch }}
DRY_RUN: ${{ inputs.dry_run == true }}
MAX_PRS: ${{ inputs.max_prs || '100' }}
PROJECT_FLOW_INPUT: ${{ inputs.project_flow || vars.PROJECT_FLOW || '' }}
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || inputs.pr_number || '' }}
TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || github.event_name == 'push' || github.event_name == 'pull_request_target' || inputs.trigger_reviews == true }}
REVIEW_DISPATCH_LIMIT_INPUT: ${{ inputs.review_dispatch_limit || vars.REVIEW_DISPATCH_LIMIT || '1' }}
ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || inputs.enable_auto_merge == true }}
MERGE_MODE: ${{ inputs.merge_mode || vars.PR_MERGE_MODE || 'direct_or_auto' }}
UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || inputs.update_branches == true }}
STALE_OPENCODE_MINUTES: ${{ inputs.stale_opencode_minutes || vars.STALE_OPENCODE_MINUTES || '420' }}
steps:
- name: Exchange OpenCode app token for scheduler mutations
id: scheduler_app_token
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: Resolve trusted scheduler source ref
id: trusted_source
env:
INPUT_CANONICAL_REF: ${{ inputs.canonical_ref || '' }}
WORKFLOW_REF: ${{ github.workflow_ref }}
run: |
set -euo pipefail
trusted_ref="${INPUT_CANONICAL_REF:-main}"
case "$WORKFLOW_REF" in
ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@*)
trusted_ref="${WORKFLOW_REF##*@}"
;;
esac
printf 'ref=%s\n' "$trusted_ref" >>"$GITHUB_OUTPUT"
- name: Checkout trusted scheduler
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ContextualWisdomLab/.github
ref: ${{ steps.trusted_source.outputs.ref }}
fetch-depth: 1
- name: Self-test scheduler
run: python3 scripts/ci/pr_review_merge_scheduler.py --self-test
- name: Inspect PR review and merge queue
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 }}
SCHEDULER_READ_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_REQUIRED_WORKFLOW_REF: ${{ steps.trusted_source.outputs.ref }}
run: |
set -euo pipefail
project_flow="$PROJECT_FLOW_INPUT"
if [ -z "$project_flow" ]; then
case "$DEFAULT_BRANCH" in
main|master) project_flow="github-flow" ;;
develop) project_flow="git-flow" ;;
*) project_flow="github-flow" ;;
esac
fi
review_dispatch_limit="$REVIEW_DISPATCH_LIMIT_INPUT"
if [ -z "$review_dispatch_limit" ]; then
review_dispatch_limit="-1"
fi
args=(
--repo "$GITHUB_REPOSITORY"
--base-branch "$DEFAULT_BRANCH"
--max-prs "$MAX_PRS"
--project-flow "$project_flow"
--review-workflow "Required OpenCode Review"
--review-dispatch-limit "$review_dispatch_limit"
--stale-opencode-minutes "$STALE_OPENCODE_MINUTES"
)
if [ -n "$PULL_REQUEST_NUMBER" ]; then
args+=(--pr-number "$PULL_REQUEST_NUMBER")
fi
if [ "$DRY_RUN" = "true" ]; then
args+=(--dry-run)
fi
if [ "$TRIGGER_REVIEWS" = "true" ]; then
args+=(--trigger-reviews)
else
args+=(--no-trigger-reviews)
fi
if [ "$ENABLE_AUTO_MERGE" = "true" ]; then
args+=(--enable-auto-merge)
else
args+=(--no-enable-auto-merge)
fi
args+=(--merge-mode "$MERGE_MODE")
if [ "$UPDATE_BRANCHES" = "true" ]; then
args+=(--update-branches)
else
args+=(--no-update-branches)
fi
python3 scripts/ci/pr_review_merge_scheduler.py "${args[@]}"