diff --git a/.github/workflows/pr-states.yml b/.github/workflows/pr-states.yml index e2df22a9b..3ec893a8c 100644 --- a/.github/workflows/pr-states.yml +++ b/.github/workflows/pr-states.yml @@ -2,26 +2,38 @@ name: PR States # Maintains the CI-states block at the bottom of the PR body. Uses # pull_request_target (not pull_request) for fork-PR write access; safe # because we never check out PR head code, only API-read and PATCH the body. +# +# Three triggers, each covering a gap: +# - pull_request_target: push / open / label change (all PRs incl forks). +# - workflow_run: initial pr-test* completion (incl fork PRs; the +# notify-job below can't fire on forks — their pull_request +# GITHUB_TOKEN is forced read-only). Does NOT re-fire for rerun +# attempts, which is why workflow_dispatch is needed too. +# - workflow_dispatch: rerun completions for non-fork PRs, dispatched by +# pr-test*'s notify-pr-states job. workflow_dispatch is the only event +# GITHUB_TOKEN can cascade-create. on: pull_request_target: types: [opened, synchronize, reopened, labeled, unlabeled] workflow_run: - # Listen to pr-test* lifecycle so reruns initiated by slash commands - # (run.rerun / run.rerun_failed_jobs via GITHUB_TOKEN) -- which don't - # refire pull_request_target -- still refresh the CI-states block. workflows: ["PR Test Base", "PR Test Extra"] types: [requested, completed] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number whose CI-states block should be refreshed.' + required: true + type: string permissions: pull-requests: write actions: read concurrency: - # Dedupe per commit SHA across both event sources so simultaneous - # synchronize + workflow_run firings on the same push queue instead - # of racing on the body PATCH. - group: pr-states-${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }} + # Per-SHA dedupe (PR number for workflow_dispatch). Queues concurrent + # fires across the three triggers so they don't race on the body PATCH. + group: pr-states-${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha || inputs.pr_number }} cancel-in-progress: false jobs: @@ -40,14 +52,16 @@ jobs: // Event-agnostic PR lookup. // - pull_request_target: PR is in the payload. - // - workflow_run: pull_requests[] is populated for same-repo PRs; - // fork PRs need a head-ref reverse lookup ("forkOwner:branch") - // because listPullRequestsAssociatedWithCommit requires the commit - // to be in the default branch. Bail when no open PR matches. + // - workflow_run: pull_requests[] is populated for same-repo + // PRs; fork PRs need a head-ref reverse lookup + // ("forkOwner:branch") because + // listPullRequestsAssociatedWithCommit requires the commit to + // be in the default branch. Bail when no open PR matches. + // - workflow_dispatch: pr_number is an explicit input. let prNumber; if (context.payload.pull_request) { prNumber = context.payload.pull_request.number; - } else { + } else if (context.eventName === 'workflow_run') { const wr = context.payload.workflow_run; if (wr.pull_requests && wr.pull_requests.length > 0) { prNumber = wr.pull_requests[0].number; @@ -71,6 +85,18 @@ jobs: core.info(`workflow_run has no PR linkage; skipping.`); return; } + } else if (context.eventName === 'workflow_dispatch') { + // Reject non-digit pr_number; parseInt('123abc', 10) returns + // 123 and would silently truncate trailing garbage. + const raw = context.payload.inputs.pr_number; + if (!/^\d+$/.test(raw)) { + core.setFailed(`workflow_dispatch: invalid pr_number input '${raw}' (must be digits only)`); + return; + } + prNumber = parseInt(raw, 10); + } else { + core.info(`Unsupported event '${context.eventName}'; skipping.`); + return; } const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, diff --git a/.github/workflows/pr-test-extra.yml b/.github/workflows/pr-test-extra.yml index a6dccd393..a52271b1e 100644 --- a/.github/workflows/pr-test-extra.yml +++ b/.github/workflows/pr-test-extra.yml @@ -221,3 +221,67 @@ jobs: partitions: ${{ needs.check-changes.outputs.partitions }} run_timeout_minutes: '60' secrets: inherit + + # =============================================== aggregator ==================================================== + # Mirrors pr-test.yml's `pr-test-finish` so notify-pr-states below only + # depends on one job rather than re-listing every stage. Fails if any + # dependent failed/cancelled. + pr-test-extra-finish: + needs: + [ + check-changes, + call-gate, + sgl-kernel-build-wheels, + extra-a-test-1-gpu-small, + extra-a-test-1-gpu-large, + extra-a-test-2-gpu-large, + extra-b-test-4-gpu-h100, + extra-b-test-4-gpu-b200, + extra-b-test-8-gpu-h200, + extra-b-test-deepep-8-gpu-h200, + ] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all dependent job statuses + run: | + json_needs='${{ toJson(needs) }}' + job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]') + for job in $job_names; do + result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result') + echo "$job: $result" + if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then + echo "The above jobs failed." + exit 1 + fi + done + echo "All jobs completed successfully" + exit 0 + + # =============================================== notify pr-states ==================================================== + # Dispatches pr-states.yml after every attempt (initial + every rerun). + # workflow_run.completed only fires on the initial completion (one + # workflow_run record across attempts), so we explicitly dispatch — + # workflow_dispatch is the only event GITHUB_TOKEN can cascade-create. + # + # Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced + # read-only regardless of declared `actions: write`, so dispatch 403s. + # Fork PRs get pr-states updates via the workflow_run subscription + # there (initial completion) and pull_request_target (push / label). + notify-pr-states: + needs: [pr-test-extra-finish] + if: | + always() && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Dispatch pr-states refresh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh workflow run pr-states.yml \ + --repo ${{ github.repository }} \ + --ref main \ + -f pr_number="$PR_NUMBER" diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index d4e938168..792055dc6 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -563,3 +563,31 @@ jobs: # If the loop completes, all jobs were successful echo "All jobs completed successfully" exit 0 + + # =============================================== notify pr-states ==================================================== + # Dispatches pr-states.yml after every attempt (initial + every rerun). + # workflow_run.completed only fires on the initial completion (one + # workflow_run record across attempts), so we explicitly dispatch — + # workflow_dispatch is the only event GITHUB_TOKEN can cascade-create. + # + # Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced + # read-only regardless of declared `actions: write`, so dispatch 403s. + # Fork PRs get pr-states updates via the workflow_run subscription + # there (initial completion) and pull_request_target (push / label). + notify-pr-states: + needs: [pr-test-finish] + if: | + always() && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Dispatch pr-states refresh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh workflow run pr-states.yml \ + --repo ${{ github.repository }} \ + --ref main \ + -f pr_number="$PR_NUMBER"