| """ |
| Sync code from OSS repo to the local repo and open a PR if changes exist. |
| |
| NOTE: |
| 1. You need to execute this script in the git root folder. |
| 2. A GH_TOKEN environment variable is required to create the pull request. |
| - see also https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens |
| |
| This script will: |
| 1. Clone the sgl-project/sglang repository (or use a local copy). |
| 2. Sync specified files and directories using rsync. |
| 3. Check if the sync operation resulted in any changes. |
| 4. If there are changes: |
| a. Create a new branch. |
| b. Commit and push the changes. |
| c. Open a pull request using the GitHub CLI (gh). |
| |
| Usage: |
| # Run the full sync and PR creation process |
| python3 scripts/copy_from_oss.py |
| |
| # Perform a dry run without making any actual changes |
| python3 scripts/copy_from_oss.py --dry-run |
| |
| # Use a local directory as the source instead of cloning |
| python3 scripts/copy_from_oss.py --local-dir ~/projects/sglang |
| """ |
|
|
| import argparse |
| import datetime |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
|
|
| |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
|
|
| from utils import FOLDER_NAMES, write_github_step_summary |
|
|
| |
| private_repo = "your-org/sglang-private-repo" |
| |
|
|
|
|
| def check_dependencies(): |
| """Check for required command-line tools.""" |
| if not shutil.which("git"): |
| raise EnvironmentError("git is not installed or not in PATH.") |
| if not shutil.which("gh"): |
| raise EnvironmentError("GitHub CLI (gh) is not installed or not in PATH.") |
| print("✅ All dependencies (git, gh) are available.") |
|
|
|
|
| def checkout_main(dry_run): |
| """Checkout to the main branch.""" |
| commands = [ |
| "git checkout main", |
| "git reset --hard", |
| ] |
| for cmd in commands: |
| print(f"Run: {cmd}") |
| if not dry_run: |
| try: |
| subprocess.run(cmd, shell=True, check=True, capture_output=True) |
| except subprocess.CalledProcessError as e: |
| print(f"Git command failed: {e.stderr.decode()}") |
| raise |
| print("✅ Checkout the main branch.") |
|
|
|
|
| def get_source_folder(args): |
| """ |
| Prepare the source repository, either by cloning from GitHub or using a local directory. |
| Returns the path to the source repo root, a temporary directory path (if created), |
| and the short commit hash. |
| """ |
| temp_dir = None |
| if args.local_dir: |
| oss_root = os.path.expanduser(args.local_dir) |
| if not os.path.exists(oss_root): |
| raise FileNotFoundError( |
| f"Specified local directory {oss_root} does not exist." |
| ) |
| print(f"Using local directory as the source: {oss_root}") |
| else: |
| temp_dir = tempfile.mkdtemp() |
| oss_root = temp_dir |
| print(f"Created temporary directory: {oss_root}") |
|
|
| repo_url = "https://github.com/sgl-project/sglang.git" |
| try: |
| subprocess.run( |
| [ |
| "git", |
| "clone", |
| "--single-branch", |
| "--branch", |
| "main", |
| repo_url, |
| temp_dir, |
| ], |
| check=True, |
| capture_output=True, |
| ) |
| print(f"Successfully cloned repository to {temp_dir}") |
| except subprocess.CalledProcessError as e: |
| print(f"Error cloning repository: {e.stderr.decode()}") |
| raise |
|
|
| commit_hash = subprocess.run( |
| ["git", "-C", oss_root, "rev-parse", "HEAD"], |
| capture_output=True, |
| text=True, |
| check=True, |
| ).stdout.strip()[:8] |
| print(f"✅ Get source OSS code at commit: {commit_hash}") |
| return oss_root, temp_dir, commit_hash |
|
|
|
|
| def sync_directories(oss_root, sync_paths, dry_run): |
| """Sync specified directories from oss_root to current working directory.""" |
| rsync_commands = [] |
| for folder_name in sync_paths: |
| target_name = f"{oss_root}/{folder_name}" |
| src_name = "./" + "/".join(folder_name.split("/")[:-1]) |
| cmd = f"rsync -r --delete {target_name} {src_name}" |
| rsync_commands.append(cmd) |
|
|
| for cmd in rsync_commands: |
| try: |
| print(f"Run: {cmd}") |
| if not dry_run: |
| subprocess.run(cmd, shell=True, check=True) |
| except subprocess.CalledProcessError as e: |
| print(f"Error executing command '{cmd}': {e}") |
| raise |
| print(f"✅ Sync all folders.") |
|
|
|
|
| def check_for_changes(): |
| """Check if there are any uncommitted git changes.""" |
| |
| result = subprocess.run(["git", "diff", "--quiet"]) |
| return result.returncode != 0 |
|
|
|
|
| def create_and_push_branch(branch_name, commit_message, dry_run): |
| """Create a new branch, commit all changes, and push to origin.""" |
| commands = [ |
| f"git checkout -b {branch_name}", |
| "git config user.name 'github-actions[bot]'", |
| "git config user.email 'github-actions[bot]@users.noreply.github.com'", |
| "git add .", |
| f"git commit -m '{commit_message}'", |
| f"git push origin {branch_name} --force", |
| ] |
| print("\nCreating and pushing git branch...") |
| for cmd in commands: |
| print(f"Run: {cmd}") |
| if not dry_run: |
| try: |
| subprocess.run(cmd, shell=True, check=True, capture_output=True) |
| except subprocess.CalledProcessError as e: |
| print(f"Git command failed: {e.stderr.decode()}") |
| raise |
|
|
|
|
| def create_pull_request(branch_name, title, body, dry_run): |
| """Create a pull request using the GitHub CLI.""" |
| gh_token = os.getenv("GH_TOKEN") |
| if not gh_token: |
| print( |
| "\n⚠️ Warning: GH_TOKEN environment variable not set. Skipping PR creation." |
| ) |
| if not dry_run: |
| return |
|
|
| print("\nCreating pull request...") |
| command = [ |
| "gh", |
| "pr", |
| "create", |
| "--base", |
| "main", |
| "--head", |
| branch_name, |
| "--repo", |
| private_repo, |
| "--title", |
| title, |
| "--body", |
| body, |
| ] |
| print(f"Run: {' '.join(command)}") |
| if not dry_run: |
| env = os.environ.copy() |
| env["GH_TOKEN"] = gh_token |
| try: |
| result = subprocess.run( |
| command, check=True, capture_output=True, text=True, env=env |
| ) |
| pr_url = result.stdout.strip() |
| msg = f"✅ Successfully created pull request: {pr_url}" |
| print(msg) |
| write_github_step_summary(msg) |
| except subprocess.CalledProcessError as e: |
| print(f"Error creating pull request: {e.stderr}") |
| raise |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Copy code from OSS and open a PR if changes are detected." |
| ) |
| parser.add_argument( |
| "--local-dir", |
| type=str, |
| help="Path to local SGLang directory to use instead of cloning from GitHub.", |
| ) |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| help="Dry run the script without executing git, rsync, or gh commands.", |
| ) |
| args = parser.parse_args() |
|
|
| check_dependencies() |
| checkout_main(args.dry_run) |
|
|
| oss_root, temp_dir, oss_commit = get_source_folder(args) |
|
|
| try: |
| |
| sync_directories(oss_root, FOLDER_NAMES, args.dry_run) |
|
|
| |
| if not check_for_changes(): |
| msg = "😴 No changes detected. The code is already in sync." |
| print(msg) |
| write_github_step_summary(msg) |
| return |
|
|
| print("✅ Changes detected. Proceeding to create a PR.") |
|
|
| current_date = datetime.datetime.now().strftime("%Y%m%d") |
| branch_name = f"copy-from-oss-{oss_commit}-{current_date}" |
| commit_message = f"Copy OSS code from {oss_commit} on {current_date}" |
| pr_title = ( |
| f"[Automated PR] Copy OSS code from commit {oss_commit} on {current_date}" |
| ) |
| pr_body = ( |
| f"Copy OSS code from https://github.com/sgl-project/sglang/commit/{oss_commit} on {current_date}." |
| "\n\n---\n\n" |
| "*This is an automated PR created by scripts/copy_from_oss.py.*" |
| ) |
|
|
| create_and_push_branch(branch_name, commit_message, args.dry_run) |
| create_pull_request(branch_name, pr_title, pr_body, args.dry_run) |
|
|
| finally: |
| |
| if temp_dir: |
| try: |
| shutil.rmtree(temp_dir) |
| print(f"\nRemoved temporary directory: {temp_dir}") |
| except OSError as e: |
| print(f"Error removing temporary directory {temp_dir}: {e}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|