1 | import * as wmillclient from "windmill-client"; |
2 | import wmill from "[email protected]"; |
3 | import { basename } from "node:path"; |
4 | const util = require("util"); |
5 | const exec = util.promisify(require("child_process").exec); |
6 | import process from "process"; |
7 |
|
8 | type GpgKey = { |
9 | email: string; |
10 | private_key: string; |
11 | passphrase: string; |
12 | }; |
13 |
|
14 | const FORKED_WORKSPACE_PREFIX = "wm-fork-"; |
15 | const FORKED_BRANCH_PREFIX = "wm-fork"; |
16 |
|
17 | type PathType = |
18 | | "script" |
19 | | "flow" |
20 | | "app" |
21 | | "folder" |
22 | | "resource" |
23 | | "variable" |
24 | | "resourcetype" |
25 | | "schedule" |
26 | | "user" |
27 | | "group" |
28 | | "httptrigger" |
29 | | "websockettrigger" |
30 | | "kafkatrigger" |
31 | | "natstrigger" |
32 | | "postgrestrigger" |
33 | | "mqtttrigger" |
34 | | "sqstrigger" |
35 | | "gcptrigger" |
36 | | "emailtrigger"; |
37 |
|
38 | let gpgFingerprint: string | undefined = undefined; |
39 |
|
40 | export async function main( |
41 | workspace_id: string, |
42 | repo_url_resource_path: string, |
43 | path_type: PathType, |
44 | skip_secret = true, |
45 | path: string | undefined, |
46 | parent_path: string | undefined, |
47 | commit_msg: string, |
48 | parent_workspace_id?: string, |
49 | use_individual_branch = false, |
50 | group_by_folder = false, |
51 | only_create_branch: boolean = false |
52 | ) { |
53 | let safeDirectoryPath: string | undefined; |
54 | const repo_resource = await wmillclient.getResource(repo_url_resource_path); |
55 | const cwd = process.cwd(); |
56 | process.env["HOME"] = "."; |
57 | if (!only_create_branch) { |
58 | console.log( |
59 | `Syncing ${path_type} ${path ?? ""} with parent ${parent_path ?? ""}` |
60 | ); |
61 | } |
62 |
|
63 | if (repo_resource.is_github_app) { |
64 | const token = await get_gh_app_token(); |
65 | const authRepoUrl = prependTokenToGitHubUrl(repo_resource.url, token); |
66 | repo_resource.url = authRepoUrl; |
67 | } |
68 |
|
69 | const { repo_name, safeDirectoryPath: cloneSafeDirectoryPath, clonedBranchName } = await git_clone(cwd, repo_resource, use_individual_branch || workspace_id.startsWith(FORKED_WORKSPACE_PREFIX)); |
70 | safeDirectoryPath = cloneSafeDirectoryPath; |
71 |
|
72 |
|
73 | |
74 | |
75 | |
76 |
|
77 | if (workspace_id.startsWith(FORKED_WORKSPACE_PREFIX)) { |
78 | if (use_individual_branch) { |
79 | console.log("Cannot have `use_individual_branch` in a forked workspace, disabling option`"); |
80 | use_individual_branch = false; |
81 | } |
82 | if (group_by_folder) { |
83 | console.log("Cannot have `group_by_folder` in a forked workspace, disabling option`"); |
84 | group_by_folder = false; |
85 | } |
86 | } |
87 |
|
88 | if (parent_workspace_id && parent_workspace_id.startsWith(FORKED_WORKSPACE_PREFIX)) { |
89 | const parentBranch = get_fork_branch_name(parent_workspace_id, clonedBranchName); |
90 | console.log(`This workspace's parent is also a fork, moving to branch ${parentBranch} in case a new branch needs to be created with the appropriate root`); |
91 | await move_to_git_branch( |
92 | parent_workspace_id, |
93 | path_type, |
94 | path, |
95 | parent_path, |
96 | use_individual_branch, |
97 | group_by_folder, |
98 | clonedBranchName |
99 | ); |
100 | } |
101 |
|
102 | await move_to_git_branch( |
103 | workspace_id, |
104 | path_type, |
105 | path, |
106 | parent_path, |
107 | use_individual_branch, |
108 | group_by_folder, |
109 | clonedBranchName |
110 | ); |
111 |
|
112 |
|
113 | const subfolder = repo_resource.folder ?? ""; |
114 | const branch_or_default = repo_resource.branch ?? "<DEFAULT>"; |
115 | console.log( |
116 | `Pushing to repository ${repo_name} in subfolder ${subfolder} on branch ${branch_or_default}` |
117 | ); |
118 |
|
119 | |
120 | if (!only_create_branch) { |
121 | await wmill_sync_pull( |
122 | path_type, |
123 | workspace_id, |
124 | path, |
125 | parent_path, |
126 | skip_secret, |
127 | repo_url_resource_path, |
128 | use_individual_branch, |
129 | repo_resource.branch |
130 | ); |
131 | } |
132 | try { |
133 | await git_push(path, parent_path, commit_msg, repo_resource, only_create_branch); |
134 | } catch (e) { |
135 | throw e; |
136 | } finally { |
137 | await delete_pgp_keys(); |
138 | |
139 | if (safeDirectoryPath) { |
140 | try { |
141 | await sh_run(undefined, "git", "config", "--global", "--unset", "safe.directory", safeDirectoryPath); |
142 | } catch (e) { |
143 | console.log(`Warning: Could not unset safe.directory config: ${e}`); |
144 | } |
145 | } |
146 | } |
147 | console.log("Finished syncing"); |
148 | process.chdir(`${cwd}`); |
149 | } |
150 |
|
151 | function get_fork_branch_name(w_id: string, originalBranch: string): string { |
152 | if (w_id.startsWith(FORKED_WORKSPACE_PREFIX)) { |
153 | return w_id.replace(FORKED_WORKSPACE_PREFIX, `${FORKED_BRANCH_PREFIX}/${originalBranch}/`); |
154 | } |
155 | return w_id; |
156 | } |
157 |
|
158 | async function git_clone( |
159 | cwd: string, |
160 | repo_resource: any, |
161 | no_single_branch: boolean, |
162 | ): Promise<{ repo_name: string; safeDirectoryPath: string; clonedBranchName: string }> { |
163 | |
164 | let repo_url = repo_resource.url; |
165 | const subfolder = repo_resource.folder ?? ""; |
166 | const branch = repo_resource.branch ?? ""; |
167 | const repo_name = basename(repo_url, ".git"); |
168 | const azureMatch = repo_url.match(/AZURE_DEVOPS_TOKEN\((?<url>.+)\)/); |
169 | if (azureMatch) { |
170 | console.log( |
171 | "Requires Azure DevOps service account access token, requesting..." |
172 | ); |
173 | const azureResource = await wmillclient.getResource(azureMatch.groups.url); |
174 | const response = await fetch( |
175 | `https://login.microsoftonline.com/${azureResource.azureTenantId}/oauth2/token`, |
176 | { |
177 | method: "POST", |
178 | body: new URLSearchParams({ |
179 | client_id: azureResource.azureClientId, |
180 | client_secret: azureResource.azureClientSecret, |
181 | grant_type: "client_credentials", |
182 | resource: "499b84ac-1321-427f-aa17-267ca6975798/.default", |
183 | }), |
184 | } |
185 | ); |
186 | const { access_token } = await response.json(); |
187 | repo_url = repo_url.replace(azureMatch[0], access_token); |
188 | } |
189 | const args = ["clone", "--quiet", "--depth", "1"]; |
190 | if (no_single_branch) { |
191 | args.push("--no-single-branch"); |
192 | } |
193 | if (subfolder !== "") { |
194 | args.push("--sparse"); |
195 | } |
196 | if (branch !== "") { |
197 | args.push("--branch"); |
198 | args.push(branch); |
199 | } |
200 | args.push(repo_url); |
201 | args.push(repo_name); |
202 | await sh_run(-1, "git", ...args); |
203 | try { |
204 | process.chdir(`${cwd}/${repo_name}`); |
205 | const safeDirectoryPath = process.cwd(); |
206 | |
207 | try { |
208 | await sh_run(undefined, "git", "config", "--global", "--add", "safe.directory", process.cwd()); |
209 | } catch (e) { |
210 | console.log(`Warning: Could not add safe.directory config: ${e}`); |
211 | } |
212 |
|
213 | if (subfolder !== "") { |
214 | await sh_run(undefined, "git", "sparse-checkout", "add", subfolder); |
215 | try { |
216 | process.chdir(`${cwd}/${repo_name}/${subfolder}`); |
217 | } catch (err) { |
218 | console.log( |
219 | `Error changing directory to '${cwd}/${repo_name}/${subfolder}'. Error was:\n${err}` |
220 | ); |
221 | throw err; |
222 | } |
223 | } |
224 | const clonedBranchName = (await sh_run(undefined, "git", "rev-parse", "--abbrev-ref", "HEAD")).trim(); |
225 | return { repo_name, safeDirectoryPath, clonedBranchName }; |
226 |
|
227 | } catch (err) { |
228 | console.log( |
229 | `Error changing directory to '${cwd}/${repo_name}'. Error was:\n${err}` |
230 | ); |
231 | throw err; |
232 | } |
233 | } |
234 | async function move_to_git_branch( |
235 | workspace_id: string, |
236 | path_type: PathType, |
237 | path: string | undefined, |
238 | parent_path: string | undefined, |
239 | use_individual_branch: boolean, |
240 | group_by_folder: boolean, |
241 | originalBranchName: string |
242 | ) { |
243 | let branchName; |
244 | if (workspace_id.startsWith(FORKED_WORKSPACE_PREFIX)) { |
245 | branchName = get_fork_branch_name(workspace_id, originalBranchName); |
246 | } else { |
247 | if (!use_individual_branch || path_type === "user" || path_type === "group") { |
248 | return; |
249 | } |
250 | branchName = group_by_folder |
251 | ? `wm_deploy/${workspace_id}/${(path ?? parent_path) |
252 | ?.split("/") |
253 | .slice(0, 2) |
254 | .join("__")}` |
255 | : `wm_deploy/${workspace_id}/${path_type}/${( |
256 | path ?? parent_path |
257 | )?.replaceAll("/", "__")}`; |
258 | } |
259 |
|
260 | try { |
261 | await sh_run(undefined, "git", "checkout", branchName); |
262 | } catch (err) { |
263 | console.log( |
264 | `Error checking out branch ${branchName}. It is possible it doesn't exist yet, tentatively creating it... Error was:\n${err}` |
265 | ); |
266 | try { |
267 | await sh_run(undefined, "git", "checkout", "-b", branchName); |
268 | await sh_run( |
269 | undefined, |
270 | "git", |
271 | "config", |
272 | "--add", |
273 | "--bool", |
274 | "push.autoSetupRemote", |
275 | "true" |
276 | ); |
277 | } catch (err) { |
278 | console.log( |
279 | `Error checking out branch '${branchName}'. Error was:\n${err}` |
280 | ); |
281 | throw err; |
282 | } |
283 | } |
284 | console.log(`Successfully switched to branch ${branchName}`); |
285 | } |
286 | async function git_push( |
287 | path: string | undefined, |
288 | parent_path: string | undefined, |
289 | commit_msg: string, |
290 | repo_resource: any, |
291 | only_create_branch: boolean, |
292 | ) { |
293 | let user_email = process.env["WM_EMAIL"] ?? ""; |
294 | let user_name = process.env["WM_USERNAME"] ?? ""; |
295 |
|
296 | if (repo_resource.gpg_key) { |
297 | await set_gpg_signing_secret(repo_resource.gpg_key); |
298 | |
299 | await sh_run( |
300 | undefined, |
301 | "git", |
302 | "config", |
303 | "user.email", |
304 | repo_resource.gpg_key.email |
305 | ); |
306 | await sh_run(undefined, "git", "config", "user.name", user_name); |
307 | } else { |
308 | await sh_run(undefined, "git", "config", "user.email", user_email); |
309 | await sh_run(undefined, "git", "config", "user.name", user_name); |
310 | } |
311 | if (only_create_branch) { |
312 | await sh_run(undefined, "git", "push", "--porcelain"); |
313 | } |
314 |
|
315 | if (path !== undefined && path !== null && path !== "") { |
316 | try { |
317 | await sh_run(undefined, "git", "add", "wmill-lock.yaml", `${path}**`); |
318 | } catch (e) { |
319 | console.log(`Unable to stage files matching ${path}**, ${e}`); |
320 | } |
321 | } |
322 | if (parent_path !== undefined && parent_path !== null && parent_path !== "") { |
323 | try { |
324 | await sh_run( |
325 | undefined, |
326 | "git", |
327 | "add", |
328 | "wmill-lock.yaml", |
329 | `${parent_path}**` |
330 | ); |
331 | } catch (e) { |
332 | console.log(`Unable to stage files matching ${parent_path}, ${e}`); |
333 | } |
334 | } |
335 | try { |
336 | await sh_run(undefined, "git", "diff", "--cached", "--quiet"); |
337 | } catch { |
338 | |
339 | const commitArgs = ["git", "commit"]; |
340 |
|
341 | |
342 | commitArgs.push("--author", `"${user_name} <${user_email}>"`); |
343 | commitArgs.push( |
344 | "-m", |
345 | `"${commit_msg == undefined || commit_msg == "" ? "no commit msg" : commit_msg}"` |
346 | ); |
347 |
|
348 | await sh_run(undefined, ...commitArgs); |
349 | try { |
350 | await sh_run(undefined, "git", "push", "--porcelain"); |
351 | } catch (e) { |
352 | console.log(`Could not push, trying to rebase first: ${e}`); |
353 | await sh_run(undefined, "git", "pull", "--rebase"); |
354 | await sh_run(undefined, "git", "push", "--porcelain"); |
355 | } |
356 | return; |
357 | } |
358 | console.log("No changes detected, nothing to commit. Returning..."); |
359 | } |
360 | async function sh_run( |
361 | secret_position: number | undefined, |
362 | cmd: string, |
363 | ...args: string[] |
364 | ) { |
365 | const nargs = secret_position != undefined ? args.slice() : args; |
366 | if (secret_position && secret_position < 0) { |
367 | secret_position = nargs.length - 1 + secret_position; |
368 | } |
369 | let secret: string | undefined = undefined; |
370 | if (secret_position != undefined) { |
371 | nargs[secret_position] = "***"; |
372 | secret = args[secret_position]; |
373 | } |
374 |
|
375 | console.log(`Running '${cmd} ${nargs.join(" ")} ...'`); |
376 | const command = exec(`${cmd} ${args.join(" ")}`); |
377 | |
378 | |
379 | |
380 | try { |
381 | const { stdout, stderr } = await command; |
382 | if (stdout.length > 0) { |
383 | console.log(stdout); |
384 | } |
385 | if (stderr.length > 0) { |
386 | console.log(stderr); |
387 | } |
388 | console.log("Command successfully executed"); |
389 | return stdout; |
390 | } catch (error) { |
391 | let errorString = error.toString(); |
392 | if (secret) { |
393 | errorString = errorString.replace(secret, "***"); |
394 | } |
395 | const err = `SH command '${cmd} ${nargs.join( |
396 | " " |
397 | )}' returned with error ${errorString}`; |
398 | throw Error(err); |
399 | } |
400 | } |
401 |
|
402 | function regexFromPath(path_type: PathType, path: string) { |
403 | if (path_type == "flow") { |
404 | return `${path}.flow/*`; |
405 | } |
406 | if (path_type == "app") { |
407 | return `${path}.app/*`; |
408 | } else if (path_type == "folder") { |
409 | return `${path}/folder.meta.*`; |
410 | } else if (path_type == "resourcetype") { |
411 | return `${path}.resource-type.*`; |
412 | } else if (path_type == "resource") { |
413 | return `${path}.resource.*`; |
414 | } else if (path_type == "variable") { |
415 | return `${path}.variable.*`; |
416 | } else if (path_type == "schedule") { |
417 | return `${path}.schedule.*`; |
418 | } else if (path_type == "user") { |
419 | return `${path}.user.*`; |
420 | } else if (path_type == "group") { |
421 | return `${path}.group.*`; |
422 | } else if (path_type == "httptrigger") { |
423 | return `${path}.http_trigger.*`; |
424 | } else if (path_type == "websockettrigger") { |
425 | return `${path}.websocket_trigger.*`; |
426 | } else if (path_type == "kafkatrigger") { |
427 | return `${path}.kafka_trigger.*`; |
428 | } else if (path_type == "natstrigger") { |
429 | return `${path}.nats_trigger.*`; |
430 | } else if (path_type == "postgrestrigger") { |
431 | return `${path}.postgres_trigger.*`; |
432 | } else if (path_type == "mqtttrigger") { |
433 | return `${path}.mqtt_trigger.*`; |
434 | } else if (path_type == "sqstrigger") { |
435 | return `${path}.sqs_trigger.*`; |
436 | } else if (path_type == "gcptrigger") { |
437 | return `${path}.gcp_trigger.*`; |
438 | } else if (path_type == "emailtrigger") { |
439 | return `${path}.email_trigger.*`; |
440 | } else { |
441 | return `${path}.*`; |
442 | } |
443 | } |
444 |
|
445 | async function wmill_sync_pull( |
446 | path_type: PathType, |
447 | workspace_id: string, |
448 | path: string | undefined, |
449 | parent_path: string | undefined, |
450 | skip_secret: boolean, |
451 | repo_url_resource_path: string, |
452 | use_individual_branch: boolean, |
453 | original_branch?: string |
454 | ) { |
455 | const includes = []; |
456 | if (path !== undefined && path !== null && path !== "") { |
457 | includes.push(regexFromPath(path_type, path)); |
458 | } |
459 | if (parent_path !== undefined && parent_path !== null && parent_path !== "") { |
460 | includes.push(regexFromPath(path_type, parent_path)); |
461 | } |
462 |
|
463 | console.log("Pulling workspace into git repo"); |
464 |
|
465 | const args = [ |
466 | "sync", |
467 | "pull", |
468 | "--token", |
469 | process.env["WM_TOKEN"] ?? "", |
470 | "--workspace", |
471 | workspace_id, |
472 | "--base-url", |
473 | process.env["BASE_URL"] + "/", |
474 | "--repository", |
475 | repo_url_resource_path, |
476 | "--yes", |
477 | skip_secret ? "--skip-secrets" : "", |
478 | ]; |
479 |
|
480 | if (path_type === "schedule" && !use_individual_branch) { |
481 | args.push("--include-schedules"); |
482 | } |
483 |
|
484 | if (path_type === "group" && !use_individual_branch) { |
485 | args.push("--include-groups"); |
486 | } |
487 |
|
488 | if (path_type === "user" && !use_individual_branch) { |
489 | args.push("--include-users"); |
490 | } |
491 |
|
492 | if (path_type.includes("trigger") && !use_individual_branch) { |
493 | args.push("--include-triggers"); |
494 | } |
495 | |
496 | if (path_type === "settings" && !use_individual_branch) { |
497 | args.push("--include-settings"); |
498 | } |
499 |
|
500 | |
501 | if (path_type === "key" && !use_individual_branch) { |
502 | args.push("--include-key"); |
503 | } |
504 |
|
505 | args.push("--extra-includes", includes.join(",")); |
506 |
|
507 | |
508 | if (use_individual_branch && original_branch) { |
509 | console.log(`Individual branch deployment detected - using promotion settings from '${original_branch}'`); |
510 | args.push("--promotion", original_branch); |
511 | } |
512 |
|
513 | await wmill_run(3, ...args); |
514 | } |
515 |
|
516 | async function wmill_run(secret_position: number, ...cmd: string[]) { |
517 | cmd = cmd.filter((elt) => elt !== ""); |
518 | const cmd2 = cmd.slice(); |
519 | cmd2[secret_position] = "***"; |
520 | console.log(`Running 'wmill ${cmd2.join(" ")} ...'`); |
521 | await wmill.parse(cmd); |
522 | console.log("Command successfully executed"); |
523 | } |
524 |
|
525 | |
526 | async function set_gpg_signing_secret(gpg_key: GpgKey) { |
527 | try { |
528 | console.log("Setting GPG private key for git commits"); |
529 |
|
530 | const formattedGpgContent = gpg_key.private_key.replace( |
531 | /(-----BEGIN PGP PRIVATE KEY BLOCK-----)([\s\S]*?)(-----END PGP PRIVATE KEY BLOCK-----)/, |
532 | (_: string, header: string, body: string, footer: string) => |
533 | header + |
534 | "\n" + |
535 | "\n" + |
536 | body.replace(/ ([^\s])/g, "\n$1").trim() + |
537 | "\n" + |
538 | footer |
539 | ); |
540 |
|
541 | const gpg_path = `/tmp/gpg`; |
542 | await sh_run(undefined, "mkdir", "-p", gpg_path); |
543 | await sh_run(undefined, "chmod", "700", gpg_path); |
544 | process.env.GNUPGHOME = gpg_path; |
545 | |
546 |
|
547 | try { |
548 | await sh_run( |
549 | 1, |
550 | "bash", |
551 | "-c", |
552 | `cat <<EOF | gpg --batch --import \n${formattedGpgContent}\nEOF` |
553 | ); |
554 | } catch (e) { |
555 | |
556 | throw new Error("Failed to import GPG key!"); |
557 | } |
558 |
|
559 | const listKeysOutput = await sh_run( |
560 | undefined, |
561 | "gpg", |
562 | "--list-secret-keys", |
563 | "--with-colons", |
564 | "--keyid-format=long" |
565 | ); |
566 |
|
567 | const keyInfoMatch = listKeysOutput.match( |
568 | /sec:[^:]*:[^:]*:[^:]*:([A-F0-9]+):.*\nfpr:::::::::([A-F0-9]{40}):/ |
569 | ); |
570 |
|
571 | if (!keyInfoMatch) { |
572 | throw new Error("Failed to extract GPG Key ID and Fingerprint"); |
573 | } |
574 |
|
575 | const keyId = keyInfoMatch[1]; |
576 | gpgFingerprint = keyInfoMatch[2]; |
577 |
|
578 | if (gpg_key.passphrase) { |
579 | |
580 | |
581 | await sh_run( |
582 | 1, |
583 | "bash", |
584 | "-c", |
585 | `echo "dummy" | gpg --batch --pinentry-mode loopback --passphrase '${gpg_key.passphrase}' --status-fd=2 -bsau ${keyId}` |
586 | ); |
587 | } |
588 |
|
589 | |
590 | await sh_run(undefined, "git", "config", "user.signingkey", keyId); |
591 | await sh_run(undefined, "git", "config", "commit.gpgsign", "true"); |
592 | console.log(`GPG signing configured with key ID: ${keyId} `); |
593 | } catch (e) { |
594 | console.error(`Failure while setting GPG key: ${e} `); |
595 | await delete_pgp_keys(); |
596 | } |
597 | } |
598 |
|
599 | async function delete_pgp_keys() { |
600 | console.log("deleting gpg keys"); |
601 | if (gpgFingerprint) { |
602 | await sh_run( |
603 | undefined, |
604 | "gpg", |
605 | "--batch", |
606 | "--yes", |
607 | "--pinentry-mode", |
608 | "loopback", |
609 | "--delete-secret-key", |
610 | gpgFingerprint |
611 | ); |
612 | await sh_run( |
613 | undefined, |
614 | "gpg", |
615 | "--batch", |
616 | "--yes", |
617 | "--delete-key", |
618 | "--pinentry-mode", |
619 | "loopback", |
620 | gpgFingerprint |
621 | ); |
622 | } |
623 | } |
624 |
|
625 | async function get_gh_app_token() { |
626 | const workspace = process.env["WM_WORKSPACE"]; |
627 | const jobToken = process.env["WM_TOKEN"]; |
628 |
|
629 | const baseUrl = |
630 | process.env["BASE_INTERNAL_URL"] ?? |
631 | process.env["BASE_URL"] ?? |
632 | "http://localhost:8000"; |
633 |
|
634 | const url = `${baseUrl}/api/w/${workspace}/github_app/token`; |
635 |
|
636 | const response = await fetch(url, { |
637 | method: "POST", |
638 | headers: { |
639 | "Content-Type": "application/json", |
640 | Authorization: `Bearer ${jobToken}`, |
641 | }, |
642 | body: JSON.stringify({ |
643 | job_token: jobToken, |
644 | }), |
645 | }); |
646 |
|
647 | if (!response.ok) { |
648 | throw new Error(`Error: ${response.statusText}`); |
649 | } |
650 |
|
651 | const data = await response.json(); |
652 |
|
653 | return data.token; |
654 | } |
655 |
|
656 | function prependTokenToGitHubUrl(gitHubUrl: string, installationToken: string) { |
657 | if (!gitHubUrl || !installationToken) { |
658 | throw new Error("Both GitHub URL and Installation Token are required."); |
659 | } |
660 |
|
661 | try { |
662 | const url = new URL(gitHubUrl); |
663 |
|
664 | |
665 | if (url.hostname !== "github.com") { |
666 | throw new Error( |
667 | "Invalid GitHub URL. Must be in the format 'https://github.com/owner/repo.git'." |
668 | ); |
669 | } |
670 |
|
671 | |
672 | return `https://x-access-token:${installationToken}@github.com${url.pathname}`; |
673 | } catch (e) { |
674 | const error = e as Error; |
675 | throw new Error(`Invalid URL: ${error.message}`); |
676 | } |
677 | } |
678 |
|