From 3117415c9bcd00ec06835eaae06690d53d18a334 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Fri, 15 May 2026 03:20:00 -0700 Subject: [PATCH] ci: standalone PR awareness workflow (decouple from pr-test reusable chain) (#25387) --- .github/workflows/_pr-awareness-comment.yml | 123 -------------------- .github/workflows/pr-states.yml | 105 +++++++++++++++++ .github/workflows/pr-test-extra.yml | 13 +-- .github/workflows/pr-test.yml | 12 +- 4 files changed, 107 insertions(+), 146 deletions(-) delete mode 100644 .github/workflows/_pr-awareness-comment.yml create mode 100644 .github/workflows/pr-states.yml diff --git a/.github/workflows/_pr-awareness-comment.yml b/.github/workflows/_pr-awareness-comment.yml deleted file mode 100644 index a0a4d817c..000000000 --- a/.github/workflows/_pr-awareness-comment.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: PR Awareness Comment -# Reusable workflow maintaining an "awareness block" at the bottom of the PR -# body: extra-CI opt-in state + latest PR Test / PR Test Extra run URLs. -# Called from pr-test.yml + pr-test-extra.yml after their check-changes job so -# the block appears ~30s in, not after downstream stages. Each caller updates -# only its own slot; the other slot is preserved verbatim. -# -# pulls.update fires `pull_request: edited`, which is NOT in the default -# trigger types, so this does not loop back into the CI workflows. - -on: - workflow_call: - inputs: - workflow_kind: - description: "Which workflow is calling: 'pr-test' or 'pr-test-extra'" - required: true - type: string - -permissions: - pull-requests: write - -jobs: - update-pr-body: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - # Serialize concurrent writers from pr-test + pr-test-extra: both race on - # get/modify/update of the PR body, and the later `pulls.update` clobbers - # the earlier writer's slot. Group is per-PR so unrelated PRs don't block. - # cancel-in-progress MUST be false — cancelling would drop the earlier update. - concurrency: - group: pr-awareness-${{ github.event.pull_request.number }} - cancel-in-progress: false - steps: - - name: Update awareness block in PR body - uses: actions/github-script@v7 - env: - # Pass input via env (process.env) instead of `${{ }}` interpolation - # into the JS body, so future user-controlled values can't inject JS. - KIND: ${{ inputs.workflow_kind }} - with: - script: | - const kind = process.env.KIND; - const labels = context.payload.pull_request.labels.map(l => l.name); - const hasExtra = labels.includes('run-ci-extra'); - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - - const outerStart = ''; - const outerEnd = ''; - const ptStart = ''; - const ptEnd = ''; - const peStart = ''; - const peEnd = ''; - - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }); - const body = pr.body || ''; - - // Extract existing block + slots so we preserve the other workflow's URL - const blockRe = new RegExp(`${outerStart}[\\s\\S]*?${outerEnd}`); - const existingBlock = (body.match(blockRe) || [''])[0]; - - function extractSlot(text, start, end) { - const re = new RegExp(`${start}([\\s\\S]*?)${end}`); - const m = text.match(re); - return m ? m[1] : null; - } - const prevPrTest = extractSlot(existingBlock, ptStart, ptEnd); - const prevPrExtra = extractSlot(existingBlock, peStart, peEnd); - - // Each caller updates only its own slot; the other slot is preserved. - // For pr-test-extra without the label: the rest of its graph is - // label-gate-skipped, so linking to that run would mislead — inline - // a warning notice in the slot instead. - // When kind=pr-test and hasExtra=false, force-reset the extra slot - // rather than carrying prevPrExtra forward — otherwise an old Run - // URL from a previous opt-in lingers after the label is removed, - // until pr-test-extra runs again and rewrites the slot. - const notEnabledText = ':warning: **Not enabled** — add `run-ci-extra` label to opt in.'; - const newPrTest = (kind === 'pr-test') - ? `[Run #${context.runId}](${runUrl})` - : (prevPrTest || '_Not run yet_'); - const newPrExtra = (kind === 'pr-test-extra') - ? (hasExtra - ? `[Run #${context.runId}](${runUrl})` - : notEnabledText) - : (hasExtra - ? (prevPrExtra || '_Not run yet_') - : notEnabledText); - - const newBlock = [ - outerStart, - '### CI Awareness', - '', - `Latest PR Test: ${ptStart}${newPrTest}${ptEnd}`, - `Latest PR Test (Extra): ${peStart}${newPrExtra}${peEnd}`, - outerEnd, - ].join('\n'); - - // Replace existing block in place, or append at body end. - let newBody; - if (blockRe.test(body)) { - newBody = body.replace(blockRe, newBlock); - } else { - const sep = body.length === 0 - ? '' - : (body.endsWith('\n') ? '\n---\n\n' : '\n\n---\n\n'); - newBody = `${body}${sep}${newBlock}`; - } - - if (newBody !== body) { - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - body: newBody, - }); - core.info(`Updated PR body awareness block (caller=${kind}).`); - } else { - core.info(`PR body awareness block already up-to-date (caller=${kind}).`); - } diff --git a/.github/workflows/pr-states.yml b/.github/workflows/pr-states.yml new file mode 100644 index 000000000..6c6b72328 --- /dev/null +++ b/.github/workflows/pr-states.yml @@ -0,0 +1,105 @@ +name: PR States +# Maintains a CI-states block at the bottom of the PR body. + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + pull-requests: write + actions: read + +concurrency: + group: pr-states-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + update-pr-body: + runs-on: ubuntu-latest + steps: + - name: Update CI states block in PR body + uses: actions/github-script@v7 + with: + script: | + const sha = context.payload.pull_request.head.sha; + const labels = context.payload.pull_request.labels.map(l => l.name); + const hasExtra = labels.includes('run-ci-extra'); + + // Retry briefly: pr-test* may not be API-visible yet when we start. + async function findRunBySha(workflowFile) { + const { data } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowFile, + head_sha: sha, + per_page: 1, + }); + return data.workflow_runs[0] || null; + } + async function findRunWithRetry(workflowFile, maxAttempts = 6, delayMs = 5000) { + for (let i = 0; i < maxAttempts; i++) { + const run = await findRunBySha(workflowFile); + if (run) return run; + if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, delayMs)); + } + return null; + } + + const ptRun = await findRunWithRetry('pr-test.yml'); + const peRun = hasExtra ? await findRunWithRetry('pr-test-extra.yml') : null; + + // Treat a fully-skipped run as "no real run" — happens when the PR + // was opened without the label and label was added later (GHA does + // not retrigger on `labeled`). + const isReal = (run) => run && run.conclusion !== 'skipped'; + + const notEnabledText = ':warning: **Not enabled** — add `run-ci-extra` label to opt in.'; + const stalePushText = ':warning: **Not run on latest push** — push again or use `/rerun-failed-ci` to dispatch.'; + const ptText = isReal(ptRun) ? `[Run #${ptRun.id}](${ptRun.html_url})` : '_Not run yet_'; + const peText = !hasExtra + ? notEnabledText + : (isReal(peRun) ? `[Run #${peRun.id}](${peRun.html_url})` : stalePushText); + + const outerStart = ''; + const outerEnd = ''; + const ptStart = ''; + const ptEnd = ''; + const peStart = ''; + const peEnd = ''; + + const newBlock = [ + outerStart, + '---', + '### CI States', + '', + `Latest PR Test: ${ptStart}${ptText}${ptEnd}`, + `Latest PR Test (Extra): ${peStart}${peText}${peEnd}`, + outerEnd, + ].join('\n'); + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const body = pr.body || ''; + + const blockRe = new RegExp(`(?:\\n+---\\n+)?${outerStart}[\\s\\S]*?${outerEnd}`); + let newBody; + if (blockRe.test(body)) { + newBody = body.replace(blockRe, `\n\n${newBlock}`); + } else { + const sep = body.length === 0 + ? '' + : (body.endsWith('\n') ? '\n' : '\n\n'); + newBody = `${body}${sep}${newBlock}`; + } + + if (newBody !== body) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: newBody, + }); + } diff --git a/.github/workflows/pr-test-extra.yml b/.github/workflows/pr-test-extra.yml index cc7041cf7..4039c7e6c 100644 --- a/.github/workflows/pr-test-extra.yml +++ b/.github/workflows/pr-test-extra.yml @@ -59,7 +59,7 @@ permissions: actions: write contents: read issues: read - pull-requests: write + pull-requests: read jobs: # =============================================== check changes ==================================================== @@ -85,17 +85,6 @@ jobs: pr_test_yml: '.github/workflows/pr-test-extra.yml' secrets: inherit - # always(): on a PR without `run-ci-extra` the rest of this workflow's graph - # is skipped, but we still want the awareness block updated so the author - # sees this run was a no-op. - call-pr-awareness: - needs: check-changes - if: always() && github.event_name == 'pull_request' - uses: ./.github/workflows/_pr-awareness-comment.yml - with: - workflow_kind: pr-test-extra - secrets: inherit - # =============================================== sgl-kernel ==================================================== sgl-kernel-build-wheels: needs: check-changes diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index dbd41d4ef..a11db0c3a 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -63,7 +63,7 @@ permissions: actions: write contents: read issues: read - pull-requests: write + pull-requests: read jobs: # =============================================== check changes ==================================================== @@ -75,16 +75,6 @@ jobs: force_continue_on_error: ${{ inputs.force_continue_on_error == true }} secrets: inherit - # Runs right after check-changes so the awareness block is injected ~30s in - # rather than waiting for downstream stages. - call-pr-awareness: - needs: check-changes - if: always() && github.event_name == 'pull_request' - uses: ./.github/workflows/_pr-awareness-comment.yml - with: - workflow_kind: pr-test - secrets: inherit - call-pr-test-extra: if: github.event_name == 'schedule' || inputs.test_parallel_dispatch == true uses: ./.github/workflows/pr-test-extra.yml