diff --git a/.github/MAINTAINER.md b/.github/MAINTAINER.md index 24a830fb5..30237d737 100644 --- a/.github/MAINTAINER.md +++ b/.github/MAINTAINER.md @@ -36,7 +36,9 @@ __Note__: Difference between Merge Oncall and Codeowner If you meet any issues during the merge, you can discuss in [slack channels](https://slack.sglang.ai/): #dev, #pull-request, and #ci-cd-build-release. ## The List of Merge Oncalls and Reviewers -TODO + +Now we have many Merge Oncalls mainly because the CI is flaky and the CODEOWNERS is too coarse-grained. +In the future, we hope the CI can be improved and we only need bypass rarely. After that, most Merge Oncalls can be converted back to Write and CODEOWNERS. This list is based on the current situation. If you or someone you know would like to take on more responsibility and are qualified, please ping @Lianmin Zheng and @Ying Sheng in the Slack channel. They will start a nomination and internal review process. diff --git a/.github/workflows/slash_command_handler.yml b/.github/workflows/slash_command_handler.yml new file mode 100644 index 000000000..ed76e8921 --- /dev/null +++ b/.github/workflows/slash_command_handler.yml @@ -0,0 +1,46 @@ +name: Slash Command Handler + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write # Required to add labels and reactions + actions: write # Required to rerun workflows + issues: write # Required for comment reactions in some contexts + +jobs: + slash_command: + # Only run if it is a PR and the comment starts with a recognized command + if: > + github.event.issue.pull_request && + (startsWith(github.event.comment.body, '/tag-run-ci-label') || + startsWith(github.event.comment.body, '/rerun-failed-ci')) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + # We checkout the current context to get the script, + # but the script will fetch permissions from main as requested. + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip install requests PyGithub + + - name: Handle Slash Command + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_FULL_NAME: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + COMMENT_ID: ${{ github.event.comment.id }} + COMMENT_BODY: ${{ github.event.comment.body }} + USER_LOGIN: ${{ github.event.comment.user.login }} + run: | + python scripts/ci/slash_command_handler.py diff --git a/scripts/ci/slash_command_handler.py b/scripts/ci/slash_command_handler.py new file mode 100644 index 000000000..3ce982373 --- /dev/null +++ b/scripts/ci/slash_command_handler.py @@ -0,0 +1,129 @@ +import json +import os +import sys + +import requests +from github import Github + +# Configuration +PERMISSIONS_FILE_URL = "https://raw.githubusercontent.com/sgl-project/sglang/main/.github/CI_PERMISSIONS.json" + + +def get_env_var(name): + val = os.getenv(name) + if not val: + print(f"Error: Environment variable {name} not set.") + sys.exit(1) + return val + + +def load_permissions(user_login): + """ + Downloads the permissions JSON from the main branch and returns + the permissions dict for the specific user. + """ + try: + print(f"Fetching permissions from {PERMISSIONS_FILE_URL}...") + response = requests.get(PERMISSIONS_FILE_URL) + response.raise_for_status() + + data = response.json() + user_perms = data.get(user_login) + + if not user_perms: + print(f"User '{user_login}' not found in permissions file.") + return None + + return user_perms + + except Exception as e: + print(f"Failed to load or parse permissions file: {e}") + sys.exit(1) + + +def handle_tag_run_ci(gh_repo, pr, comment, user_perms): + """ + Handles the /tag-run-ci-label command. + """ + if not user_perms.get("can_tag_run_ci_label", False): + print("Permission denied: can_tag_run_ci_label is false.") + return + + print("Permission granted. Adding 'run-ci' label.") + pr.add_to_labels("run-ci") + + # React to the comment with +1 + comment.create_reaction("+1") + print("Label added and comment reacted.") + + +def handle_rerun_failed_ci(gh_repo, pr, comment, user_perms): + """ + Handles the /rerun-failed-ci command. + """ + if not user_perms.get("can_rerun_failed_ci", False): + print("Permission denied: can_rerun_failed_ci is false.") + return + + print("Permission granted. Triggering rerun of failed workflows.") + + # Get the SHA of the latest commit in the PR + head_sha = pr.head.sha + print(f"Checking workflows for commit: {head_sha}") + + # List all workflow runs for this commit + runs = gh_repo.get_workflow_runs(head_sha=head_sha) + + rerun_count = 0 + for run in runs: + # We only care about completed runs that failed + if run.status == "completed" and run.conclusion == "failure": + print(f"Rerunning workflow: {run.name} (ID: {run.id})") + run.rerun_failed() + rerun_count += 1 + + if rerun_count > 0: + comment.create_reaction("+1") + print(f"Triggered rerun for {rerun_count} failed workflows.") + else: + print("No failed workflows found to rerun.") + + +def main(): + # 1. Load Environment Variables + token = get_env_var("GITHUB_TOKEN") + repo_name = get_env_var("REPO_FULL_NAME") + pr_number = int(get_env_var("PR_NUMBER")) + comment_id = int(get_env_var("COMMENT_ID")) + comment_body = get_env_var("COMMENT_BODY").strip() + user_login = get_env_var("USER_LOGIN") + + # 2. Initialize GitHub API + g = Github(token) + repo = g.get_repo(repo_name) + pr = repo.get_pull(pr_number) + comment = repo.get_issue(pr_number).get_comment(comment_id) + + # 3. Load Permissions (Remote Check) + user_perms = load_permissions(user_login) + + if not user_perms: + print(f"User {user_login} does not have any configured permissions. Exiting.") + return + + # 4. Parse Command and Execute + # split lines to handle cases where there might be text after the command + first_line = comment_body.split("\n")[0].strip() + + if first_line.startswith("/tag-run-ci-label"): + handle_tag_run_ci(repo, pr, comment, user_perms) + + elif first_line.startswith("/rerun-failed-ci"): + handle_rerun_failed_ci(repo, pr, comment, user_perms) + + else: + print(f"Unknown or ignored command: {first_line}") + + +if __name__ == "__main__": + main()