From 109f27ba3a3c3db4cade92859341415794f95992 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Wed, 19 Nov 2025 00:49:13 +0800 Subject: [PATCH] [CI] update pr-gate to be compatible with new slash triggering mananer. (#13522) --- .github/workflows/pr-gate.yml | 80 +++++++++++++++++++--- docs/developer_guide/contribution_guide.md | 16 +++++ scripts/ci/slash_command_handler.py | 1 + 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index aeb2a8686..49eda05d1 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -14,9 +14,15 @@ jobs: pr-gate: runs-on: ubuntu-latest steps: + # 1. for commits on main: no gating needed + # 2. for workflow_dispatch: this can only be triggered by users with write access + - name: Skip PR-Gate for non-PR events + if: github.event_name != 'pull_request' + run: | + echo "Not a pull_request event; skipping PR-Gate checks." + exit 0 - name: Fetch latest PR info id: pr - if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -42,13 +48,13 @@ jobs: echo "===================" - name: Block draft PR - if: github.event_name == 'pull_request' && fromJson(steps.pr.outputs.draft) + if: fromJson(steps.pr.outputs.draft) run: | echo "PR is draft. Blocking CI." exit 1 - name: Require run-ci label (optional) - if: github.event_name == 'pull_request' && inputs.require-run-ci == true + if: inputs.require-run-ci == true run: | labels='${{ steps.pr.outputs.labels }}' if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci') }}" == "false" ]]; then @@ -57,19 +63,24 @@ jobs: fi - name: Enforce rate limit for low-permission actors (optional) - if: (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && inputs.rate-limit-hours != 0 uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const HOURS = Number("${{ inputs.rate-limit-hours }}"); + const DEFAULT_HOURS = Number("${{ inputs.rate-limit-hours }}"); 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; + let triggeringActor = curRun.data.triggering_actor?.login || context.actor; + if (triggeringActor === "github-actions[bot]") { + triggeringActor = `${{ steps.pr.outputs.user }}`; + core.info( + `triggering_actor is github-actions[bot]; substituting PR author '${triggeringActor}'.` + ); + } async function hasHighPermission(username) { try { @@ -87,8 +98,55 @@ jobs: 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}'.`); + let effectiveCooldownMinutes = DEFAULT_HOURS * 60; + let perUserCooldownMinutes = null; + + try { + const contentResp = await github.rest.repos.getContent({ + owner, + repo, + path: ".github/CI_PERMISSIONS.json", + ref: "main", + }); + + if (!Array.isArray(contentResp.data) && contentResp.data && "content" in contentResp.data) { + const raw = Buffer.from( + contentResp.data.content, + contentResp.data.encoding || "base64" + ).toString(); + const ciPermissions = JSON.parse(raw); + + const userPerm = ciPermissions[triggeringActor]; + if (userPerm && typeof userPerm.cooldown_interval_minutes === "number") { + perUserCooldownMinutes = userPerm.cooldown_interval_minutes; + core.info( + `Per-user cooldown for '${triggeringActor}' from CI_PERMISSIONS.json: ${perUserCooldownMinutes} minutes.` + ); + } else { + core.info(`No per-user cooldown found for '${triggeringActor}' in CI_PERMISSIONS.json.`); + } + } else { + core.info("CI_PERMISSIONS.json content response is not a file; skipping per-user cooldown."); + } + } catch (e) { + core.info(`CI_PERMISSIONS.json not found or unreadable: ${e.message}. Using default rate limit only.`); + } + + if (perUserCooldownMinutes !== null) { + effectiveCooldownMinutes = Math.min(effectiveCooldownMinutes, perUserCooldownMinutes); + } + + if (effectiveCooldownMinutes <= 0) { + core.info( + `Effective cooldown for '${triggeringActor}' is 0 minutes; no rate limit enforced for this user.` + ); + return; + } + + const cutoff = new Date(Date.now() - effectiveCooldownMinutes * 60 * 1000); + core.info( + `Checking for workflow runs since ${cutoff.toISOString()} (last ${effectiveCooldownMinutes} minutes) for event '${eventName}'.` + ); const { data } = await github.rest.actions.listWorkflowRuns({ owner, @@ -108,8 +166,10 @@ jobs: if (recentFound) { core.setFailed( `User '${triggeringActor}' already triggered '${context.workflow}' via '${eventName}' at ${recentFound.created_at}. ` + - `Please wait ${HOURS} hours before triggering again.` + `Please wait ${effectiveCooldownMinutes} minutes before triggering again.` ); } else { - core.info(`No recent runs detected within the last ${HOURS} hours; proceeding.`); + core.info( + `No recent runs detected for '${triggeringActor}' within the last ${effectiveCooldownMinutes} minutes; proceeding.` + ); } diff --git a/docs/developer_guide/contribution_guide.md b/docs/developer_guide/contribution_guide.md index 8593d35c2..eb1f391f7 100644 --- a/docs/developer_guide/contribution_guide.md +++ b/docs/developer_guide/contribution_guide.md @@ -85,6 +85,22 @@ To avoid spamming a PR with too many `/rerun-failed-ci` comments, you can also t If you don’t have permission, please ask maintainers to trigger CI for you. +### CI rate limits + +We apply CI rate limits to prevent abuse and ensure fair usage of our CI resources. + +Each CI workflow has a default limit defined in its workflow configuration file. For example, in [pr-gate.yml](https://github.com/sgl-project/sglang/blob/main/.github/workflows/pr-gate.yml), the default rate limit window is 2 hours, and each workflow can override it via the `rate-limit-hours` input parameter: + +```yaml +rate-limit-hours: + description: "Rate limit window size in hours; 0 disables rate limiting" + type: number + default: 2 +``` + +Users listed in [CI_PERMISSIONS.json](https://github.com/sgl-project/sglang/blob/main/.github/CI_PERMISSIONS.json) may have a per-user cooldown interval. In practice, we use the minimum of the workflow’s default window and the user-specific interval. + + ## Code style guidance - Avoid code duplication. If the same code snippet (more than five lines) appears multiple times, extract it into a shared function. - Minimize device synchronization. Reduce expensive CPU-GPU synchronization operations, such as `tensor.item()` or `tensor.cpu()`, whenever possible. Use vectorized code. diff --git a/scripts/ci/slash_command_handler.py b/scripts/ci/slash_command_handler.py index 1c7c4ac08..9bb55ec05 100644 --- a/scripts/ci/slash_command_handler.py +++ b/scripts/ci/slash_command_handler.py @@ -89,6 +89,7 @@ def handle_rerun_failed_ci(gh_repo, pr, comment, user_perms, react_on_success=Tr continue if run.conclusion == "failure": + # DEBUG print(f"Rerunning failed workflow: {run.name} (ID: {run.id})") try: # Use rerun_failed_jobs for efficiency on failures