diff --git a/.github/workflows/execute-notebook.yml b/.github/workflows/execute-notebook.yml index aa516115046f..0452d04d2e98 100644 --- a/.github/workflows/execute-notebook.yml +++ b/.github/workflows/execute-notebook.yml @@ -6,7 +6,7 @@ on: paths: - "python/sglang/**" - "docs/**" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: diff --git a/.github/workflows/pr-benchmark-rust.yml b/.github/workflows/pr-benchmark-rust.yml index 5e389cfcee2e..e267141f9abc 100644 --- a/.github/workflows/pr-benchmark-rust.yml +++ b/.github/workflows/pr-benchmark-rust.yml @@ -9,7 +9,7 @@ on: branches: [ main ] paths: - "sgl-router/**" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml new file mode 100644 index 000000000000..17647d16ae50 --- /dev/null +++ b/.github/workflows/pr-gate.yml @@ -0,0 +1,96 @@ +on: + workflow_call: + +jobs: + pr-gate: + runs-on: ubuntu-latest + steps: + - name: Fetch latest PR info + id: pr + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + core.setOutput("labels", JSON.stringify(pr.data.labels.map(l => l.name))); + core.setOutput("draft", pr.data.draft); + core.setOutput("user", pr.data.user.login); + + - name: Block draft PR + if: github.event_name == 'pull_request' && fromJson(steps.pr.outputs.draft) + run: | + echo "PR is draft. Blocking CI." + exit 1 + + - name: Require run-ci label + if: github.event_name == 'pull_request' + run: | + labels='${{ steps.pr.outputs.labels }}' + echo "Labels: $labels" + if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci') }}" == "false" ]]; then + echo "Missing required label 'run-ci'." + exit 1 + fi + + - name: Enforce rate limit for low-permission actors + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const HOURS = 2; + const owner = context.repo.owner; + const repo = context.repo.repo; + const eventName = context.eventName; + const curRun = await github.rest.actions.getWorkflowRun({ + owner, repo, run_id: context.runId + }); + const triggeringActor = curRun.data.triggering_actor?.login || context.actor; + + async function hasHighPermission(username) { + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); + const perm = data.permission || 'none'; + return perm === 'write' || perm === 'maintain' || perm === 'admin'; + } catch (e) { + if (e.status === 404 || e.status === 403) return false; + throw e; + } + } + + if (await hasHighPermission(triggeringActor)) { + core.info(`Triggering user '${triggeringActor}' has high permission. No rate limit applied.`); + return; + } + + const cutoff = new Date(Date.now() - HOURS * 60 * 60 * 1000); + core.info(`Checking for workflow runs since ${cutoff.toISOString()} (last ${HOURS} hours) for event '${eventName}'.`); + + const { data } = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: 'pr-test.yml', + event: eventName, + per_page: 100, + }); + + const runs = data.workflow_runs || []; + const recentFound = runs.find((run) => { + if (String(run.id) === String(context.runId)) return false; + if (new Date(run.created_at) < cutoff) return false; + return (run.actor?.login === triggeringActor) || (run.triggering_actor?.login === triggeringActor); + }); + + if (recentFound) { + core.setFailed( + `User '${triggeringActor}' already triggered '${context.workflow}' via '${eventName}' at ${recentFound.created_at}. ` + + `Please wait ${HOURS} hours before triggering again.` + ); + } else { + core.info(`No recent runs detected within the last ${HOURS} hours; proceeding.`); + } diff --git a/.github/workflows/pr-test-amd.yml b/.github/workflows/pr-test-amd.yml index e722b24cdf82..2ac58c6dcd7d 100644 --- a/.github/workflows/pr-test-amd.yml +++ b/.github/workflows/pr-test-amd.yml @@ -19,7 +19,7 @@ on: - "test/**" - "sgl-kernel/**" - ".github/workflows/pr-test-amd.yml" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: @@ -27,7 +27,11 @@ concurrency: cancel-in-progress: true jobs: + call-gate: + uses: ./.github/workflows/pr-gate.yml + secrets: inherit check-changes: + needs: [call-gate] runs-on: ubuntu-latest outputs: main_package: ${{ steps.filter.outputs.main_package }} @@ -35,19 +39,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Fail if the PR does not have the 'run-ci' label - if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'run-ci') - run: | - echo "This pull request does not have the 'run-ci' label. Failing the workflow." - exit 1 - - - name: Fail if the PR is a draft - if: github.event_name == 'pull_request' && github.event.pull_request.draft == true - run: | - echo "This pull request is a draft. Failing the workflow." - exit 1 - - name: Detect file changes id: filter uses: dorny/paths-filter@v3 @@ -416,6 +407,7 @@ jobs: pr-test-amd-finish: needs: [ + call-gate, check-changes, sgl-kernel-unit-test-amd, diff --git a/.github/workflows/pr-test-npu.yml b/.github/workflows/pr-test-npu.yml index fd694f01caaf..246c786c14c6 100644 --- a/.github/workflows/pr-test-npu.yml +++ b/.github/workflows/pr-test-npu.yml @@ -17,7 +17,7 @@ on: - "scripts/ci/**" - "test/**" - ".github/workflows/pr-test-npu.yml" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-test-pd-router.yml b/.github/workflows/pr-test-pd-router.yml index 063503a36f5e..eaa2068fdc16 100644 --- a/.github/workflows/pr-test-pd-router.yml +++ b/.github/workflows/pr-test-pd-router.yml @@ -13,7 +13,7 @@ on: - 'python/sglang/srt/disaggregation/**' - 'scripts/ci/ci_start_disaggregation_servers.sh' - 'sgl-router/**' - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-test-rust.yml b/.github/workflows/pr-test-rust.yml index c834ceb1c704..c8a7a0086ef1 100644 --- a/.github/workflows/pr-test-rust.yml +++ b/.github/workflows/pr-test-rust.yml @@ -9,7 +9,7 @@ on: branches: [ main ] paths: - "sgl-router/**" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-test-xeon.yml b/.github/workflows/pr-test-xeon.yml index e15d3deef9b3..5d53238a511a 100644 --- a/.github/workflows/pr-test-xeon.yml +++ b/.github/workflows/pr-test-xeon.yml @@ -21,7 +21,7 @@ on: - "sgl-kernel/**" - ".github/workflows/pr-test-xeon.yml" - "docker/xeon.Dockerfile" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-test-xpu.yml b/.github/workflows/pr-test-xpu.yml index 60c4ddc6de57..61592937bed6 100644 --- a/.github/workflows/pr-test-xpu.yml +++ b/.github/workflows/pr-test-xpu.yml @@ -19,7 +19,7 @@ on: - "test/**" - "sgl-kernel/**" - ".github/workflows/pr-test-xpu.yml" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 9fc6cfd1aef4..dca07b17f1fd 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -5,7 +5,7 @@ on: branches: [main] pull_request: branches: [main] - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: inputs: version: @@ -22,8 +22,12 @@ concurrency: cancel-in-progress: true jobs: + call-gate: + uses: ./.github/workflows/pr-gate.yml + secrets: inherit # =============================================== check changes ==================================================== check-changes: + needs: [call-gate] runs-on: ubuntu-latest outputs: main_package: ${{ steps.filter.outputs.main_package }} @@ -33,76 +37,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Fail if the PR does not have the 'run-ci' label - if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'run-ci') - run: | - echo "This pull request does not have the 'run-ci' label. Failing the workflow." - exit 1 - - - name: Fail if the PR is a draft - if: github.event_name == 'pull_request' && github.event.pull_request.draft == true - run: | - echo "This pull request is a draft. Failing the workflow." - exit 1 - - - name: Enforce rate limit for low-permission actors - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const HOURS = 2; - const owner = context.repo.owner; - const repo = context.repo.repo; - const eventName = context.eventName; - const curRun = await github.rest.actions.getWorkflowRun({ - owner, repo, run_id: context.runId - }); - const triggeringActor = curRun.data.triggering_actor?.login || context.actor; - - async function hasHighPermission(username) { - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); - const perm = data.permission || 'none'; - return perm === 'write' || perm === 'maintain' || perm === 'admin'; - } catch (e) { - if (e.status === 404 || e.status === 403) return false; - throw e; - } - } - - if (await hasHighPermission(triggeringActor)) { - core.info(`Triggering user '${triggeringActor}' has high permission. No rate limit applied.`); - return; - } - - const cutoff = new Date(Date.now() - HOURS * 60 * 60 * 1000); - core.info(`Checking for workflow runs since ${cutoff.toISOString()} (last ${HOURS} hours) for event '${eventName}'.`); - - const { data } = await github.rest.actions.listWorkflowRuns({ - owner, - repo, - workflow_id: 'pr-test.yml', - event: eventName, - per_page: 100, - }); - - const runs = data.workflow_runs || []; - const recentFound = runs.find((run) => { - if (String(run.id) === String(context.runId)) return false; - if (new Date(run.created_at) < cutoff) return false; - return (run.actor?.login === triggeringActor) || (run.triggering_actor?.login === triggeringActor); - }); - - if (recentFound) { - core.setFailed( - `User '${triggeringActor}' already triggered '${context.workflow}' via '${eventName}' at ${recentFound.created_at}. ` + - `Please wait ${HOURS} hours before triggering again.` - ); - } else { - core.info(`No recent runs detected within the last ${HOURS} hours; proceeding.`); - } - - name: Detect file changes id: filter uses: dorny/paths-filter@v3 @@ -959,6 +893,7 @@ jobs: pr-test-finish: needs: [ + call-gate, check-changes, sgl-kernel-build-wheels, diff --git a/.github/workflows/quantization-test.yml b/.github/workflows/quantization-test.yml index 70357f68db8c..f34534c2db1a 100644 --- a/.github/workflows/quantization-test.yml +++ b/.github/workflows/quantization-test.yml @@ -17,7 +17,7 @@ on: - "scripts/ci/**" - "test/**" - ".github/workflows/quantization-test.yml" - types: [synchronize, labeled] + types: [synchronize] workflow_dispatch: concurrency: