-
Notifications
You must be signed in to change notification settings - Fork 5k
Use slash command to trigger CI #13512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catching the broad
Suggested change
|
||||||||||||||||||||||
| print(f"Failed to load or parse permissions file: {e}") | ||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def handle_tag_run_ci(gh_repo, pr, comment, user_perms): | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The parameter
Suggested change
|
||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| 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) | ||||||||||||||||||||||
|
Comment on lines
+118
to
+122
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve maintainability and avoid "magic strings," it's good practice to define command strings as constants. You can define them in the configuration section at the top of the file and then reference them here. For example, you could add: # In configuration section
CMD_TAG_RUN_CI = "/tag-run-ci-label"
CMD_RERUN_FAILED_CI = "/rerun-failed-ci"Then, this block can be updated to use these constants.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| print(f"Unknown or ignored command: {first_line}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||
| main() | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling
sys.exit(1)directly from this utility function makes it less reusable and harder to test. It's better practice for utility functions to signal errors by raising an exception. The calling function (main) can then catch this exception, print an appropriate error message, and exit.