copy_from_oss.py 9.14 KB
Newer Older
Lianmin Zheng's avatar
Lianmin Zheng committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""
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 tempfile

# --- Configuration Begin ---
# List of folders and files to copy from the OSS repo.
# Changes outside these paths will be ignored.
folder_names = [
    "3rdparty",
    "assets",
    "benchmark",
    "docker",
    "docs",
    "examples",
    "sgl-kernel",
    "README.md",
    "python/sglang/lang",
    "python/sglang/srt",
    "python/sglang/test",
    "test/lang",
    "test/srt",
]

private_repo = "your-org/sglang-private-repo"
# --- Configuration End ---


def write_github_step_summary(content):
    if not os.environ.get("GITHUB_STEP_SUMMARY"):
        return

    with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
        f.write(content)


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, folder_names, dry_run):
    """Sync specified directories from oss_root to current working directory."""
    rsync_commands = []
    for folder_name in folder_names:
        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."""
    # This command exits with 1 if there are changes, 0 otherwise.
    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
        sync_directories(oss_root, folder_names, args.dry_run)

        # Check for changes and create PR if necessary
        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:
        # Remove temporary directory if it was created
        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()