[CI] update pr-gate to be compatible with new slash triggering mananer. (#13522)

This commit is contained in:
Liangsheng Yin
2025-11-19 00:49:13 +08:00
committed by GitHub
parent c1a30aa765
commit 109f27ba3a
3 changed files with 87 additions and 10 deletions
+70 -10
View File
@@ -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.`
);
}
@@ -85,6 +85,22 @@ To avoid spamming a PR with too many `/rerun-failed-ci` comments, you can also t
If you dont 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 workflows 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.
+1
View File
@@ -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