1 | import * as wmillclient from "windmill-client"; |
2 | import wmill from "[email protected]"; |
3 | import { basename, join } from "node:path"; |
4 | import { existsSync } from "fs"; |
5 | const util = require("util"); |
6 | const exec = util.promisify(require("child_process").exec); |
7 | import process from "process"; |
8 |
|
9 | type GpgKey = { |
10 | email: string; |
11 | private_key: string; |
12 | passphrase: string; |
13 | }; |
14 |
|
15 | type GitRepository = { |
16 | url: string; |
17 | branch: string; |
18 | folder: string; |
19 | gpg_key: any; |
20 | is_github_app: boolean; |
21 | }; |
22 |
|
23 | const FORKED_WORKSPACE_PREFIX = "wm-fork-"; |
24 | const FORKED_BRANCH_PREFIX = "wm-fork"; |
25 |
|
26 | let gpgFingerprint: string | undefined = undefined; |
27 |
|
28 | export async function main( |
29 | workspace_id: string, |
30 | repo_url_resource_path: string, |
31 | dry_run: boolean, |
32 | only_wmill_yaml: boolean = false, |
33 | pull: boolean = false, |
34 | settings_json?: string, |
35 | use_promotion_overrides?: boolean, |
36 | clone_ref?: string, |
37 | pr_head_sha?: string |
38 | ) { |
39 | let safeDirectoryPath: string | undefined; |
40 | console.log("DEBUG: Starting main function", { |
41 | workspace_id, |
42 | |
43 | dry_run, |
44 | only_wmill_yaml, |
45 | pull, |
46 | settings_json: settings_json ? "PROVIDED" : "NOT_PROVIDED", |
47 | }); |
48 |
|
49 | const repo_resource: GitRepository = await wmillclient.getResource( |
50 | repo_url_resource_path |
51 | ); |
52 |
|
53 | process.env.GIT_TERMINAL_PROMPT = "0"; |
54 | console.log("DEBUG: Set GIT_TERMINAL_PROMPT=0 to prevent interactive prompts"); |
55 | const safeUrl = repo_resource.url |
56 | ? repo_resource.url.replace(/\/\/[^@]+@/, '//***@') |
57 | : undefined; |
58 | console.log("DEBUG: Retrieved repo resource", { |
59 | url: safeUrl, |
60 | branch: repo_resource.branch, |
61 | folder: repo_resource.folder, |
62 | is_github_app: repo_resource.is_github_app, |
63 | has_gpg: !!repo_resource.gpg_key, |
64 | }); |
65 |
|
66 | |
67 | const repository_path = repo_url_resource_path.startsWith("$res:") |
68 | ? repo_url_resource_path.substring(5) |
69 | : repo_url_resource_path; |
70 | console.log("DEBUG: Repository path for CLI:", repository_path); |
71 |
|
72 | |
73 | const promotion_branch = use_promotion_overrides ? repo_resource.branch : undefined; |
74 | console.log("DEBUG: Promotion branch:", promotion_branch); |
75 |
|
76 | const cwd = process.cwd(); |
77 | console.log("DEBUG: Current working directory:", cwd); |
78 | process.env["HOME"] = "."; |
79 |
|
80 | if (repo_resource.is_github_app) { |
81 | console.log("DEBUG: Using GitHub App authentication"); |
82 | const token = await get_gh_app_token(); |
83 | console.log("DEBUG: Got GitHub App token:", token ? "SUCCESS" : "FAILED"); |
84 | const authRepoUrl = prependTokenToGitHubUrl(repo_resource.url, token); |
85 | console.log("DEBUG: URL conversion:", { |
86 | original: repo_resource.url, |
87 | authenticated: authRepoUrl, |
88 | }); |
89 | repo_resource.url = authRepoUrl; |
90 | } |
91 |
|
92 | console.log("DEBUG: Starting git clone..."); |
93 | let cloneOutcome; |
94 | try { |
95 | cloneOutcome = await git_clone(cwd, repo_resource, pull, workspace_id, clone_ref, pr_head_sha); |
96 | } catch (error) { |
97 | const msg = error?.message ?? String(error); |
98 | const sentinel = ["PR_MERGE_CONFLICTS", "PR_HEAD_REF_UNAVAILABLE"].find((s) => msg.includes(s)); |
99 | |
100 | |
101 | |
102 | if (sentinel) { |
103 | console.log(`DEBUG: PR diff aborted: ${msg}`); |
104 | |
105 | |
106 | process.chdir(cwd); |
107 | return { pr_check_error: sentinel, message: msg }; |
108 | } |
109 | throw error; |
110 | } |
111 | const { repo_name, safeDirectoryPath: cloneSafeDirectoryPath, clonedBranchName } = cloneOutcome; |
112 | safeDirectoryPath = cloneSafeDirectoryPath; |
113 | console.log("DEBUG: Git clone completed, repo name:", repo_name); |
114 |
|
115 | const subfolder = repo_resource.folder ?? ""; |
116 | const fullPath = join(cwd, repo_name, subfolder); |
117 | console.log("DEBUG: Full path:", fullPath); |
118 |
|
119 | process.chdir(fullPath); |
120 | console.log("DEBUG: Changed directory to:", process.cwd()); |
121 |
|
122 | |
123 | console.log("DEBUG: Setting up workspace..."); |
124 | await wmill_run( |
125 | 6, |
126 | "workspace", |
127 | "add", |
128 | workspace_id, |
129 | workspace_id, |
130 | process.env["BASE_URL"] + "/", |
131 | "--token", |
132 | process.env["WM_TOKEN"] ?? "" |
133 | ); |
134 | console.log("DEBUG: Workspace setup completed"); |
135 |
|
136 | let result; |
137 | try { |
138 | console.log("DEBUG: Entering main execution branch", { |
139 | only_wmill_yaml, |
140 | pull, |
141 | dry_run, |
142 | }); |
143 |
|
144 | if (only_wmill_yaml) { |
145 | |
146 | result = await executeSettingsOperation( |
147 | workspace_id, |
148 | repository_path, |
149 | settings_json, |
150 | fullPath, |
151 | pull, |
152 | dry_run, |
153 | clonedBranchName, |
154 | repo_resource, |
155 | promotion_branch |
156 | ); |
157 | } else { |
158 | |
159 | result = await executeSyncOperation( |
160 | workspace_id, |
161 | repository_path, |
162 | settings_json, |
163 | fullPath, |
164 | repo_resource, |
165 | pull, |
166 | dry_run, |
167 | clonedBranchName, |
168 | promotion_branch |
169 | ); |
170 | } |
171 |
|
172 | console.log("DEBUG: Main execution completed successfully", result); |
173 | } catch (error) { |
174 | console.log("DEBUG: Error in main execution:", error); |
175 | throw error; |
176 | } finally { |
177 | |
178 | if (safeDirectoryPath) { |
179 | try { |
180 | await sh_run(undefined, "git", "config", "--global", "--unset", "safe.directory", safeDirectoryPath); |
181 | } catch (e) { |
182 | console.log(`Warning: Could not unset safe.directory config: ${e}`); |
183 | } |
184 | } |
185 | console.log("DEBUG: Changing back to original directory:", cwd); |
186 | process.chdir(cwd); |
187 | } |
188 |
|
189 | |
190 | return result; |
191 | } |
192 |
|
193 | async function executeSettingsOperation( |
194 | workspace_id: string, |
195 | repository_path: string, |
196 | settings_json: string | undefined, |
197 | fullPath: string, |
198 | pull: boolean, |
199 | dry_run: boolean, |
200 | clonedBranchName: string, |
201 | repo_resource?: any, |
202 | promotion_branch?: string |
203 | ) { |
204 | if (pull) { |
205 | console.log("DEBUG: Executing pull branch (wmill.yaml only)"); |
206 | |
207 | if (dry_run) { |
208 | return await executeCliSettingsPushDryRun( |
209 | workspace_id, |
210 | repository_path, |
211 | settings_json, |
212 | fullPath, |
213 | promotion_branch |
214 | ); |
215 | } else { |
216 | |
217 | return await executeCliSettingsPushDryRun( |
218 | workspace_id, |
219 | repository_path, |
220 | settings_json, |
221 | fullPath, |
222 | promotion_branch |
223 | ); |
224 | } |
225 | } else { |
226 | console.log("DEBUG: Executing push branch (wmill.yaml only)"); |
227 | |
228 | if (dry_run) { |
229 | return await executeCliSettingsPullDryRun( |
230 | workspace_id, |
231 | repository_path, |
232 | settings_json, |
233 | fullPath, |
234 | promotion_branch |
235 | ); |
236 | } else { |
237 | if (!settings_json) throw Error("settings_json required in this mode"); |
238 | return await executeCliSettingsPull( |
239 | workspace_id, |
240 | repository_path, |
241 | fullPath, |
242 | settings_json, |
243 | repo_resource, |
244 | promotion_branch |
245 | ); |
246 | } |
247 | } |
248 | } |
249 |
|
250 | async function executeSyncOperation( |
251 | workspace_id: string, |
252 | repository_path: string, |
253 | settings_json: string | undefined, |
254 | fullPath: string, |
255 | repo_resource: any, |
256 | pull: boolean, |
257 | dry_run: boolean, |
258 | clonedBranchName: string, |
259 | promotion_branch?: string |
260 | ) { |
261 | if (pull) { |
262 | console.log("DEBUG: Executing sync pull", { dry_run }); |
263 | |
264 | if (dry_run) { |
265 | return await executeCliSyncPushDryRun( |
266 | workspace_id, |
267 | repository_path, |
268 | settings_json, |
269 | fullPath, |
270 | promotion_branch |
271 | ); |
272 | } else { |
273 | return await executeCliSyncPush( |
274 | workspace_id, |
275 | repository_path, |
276 | repo_resource, |
277 | settings_json |
278 | ); |
279 | } |
280 | } else { |
281 | console.log("DEBUG: Executing sync push", { dry_run }); |
282 | |
283 | if (dry_run) { |
284 | return await executeCliSyncPullDryRun( |
285 | workspace_id, |
286 | repository_path, |
287 | settings_json, |
288 | fullPath |
289 | ); |
290 | } else { |
291 | return await executeCliSyncPull( |
292 | workspace_id, |
293 | repository_path, |
294 | repo_resource, |
295 | clonedBranchName, |
296 | settings_json |
297 | ); |
298 | } |
299 | } |
300 | } |
301 |
|
302 | |
303 | async function executeCliSettingsPullDryRun( |
304 | workspace_id: string, |
305 | repository_path: string, |
306 | settings_json?: string, |
307 | repoPath?: string, |
308 | promotion_branch?: string |
309 | ) { |
310 | try { |
311 | |
312 | let wmillYamlExists = existsSync("wmill.yaml"); |
313 | if (!wmillYamlExists) { |
314 | console.log( |
315 | "DEBUG: No wmill.yaml found, will create with repository settings" |
316 | ); |
317 |
|
318 | |
319 | |
320 | return { |
321 | success: true, |
322 | hasChanges: true, |
323 | message: "wmill.yaml will be created with repository settings", |
324 | isInitialSetup: true, |
325 | repository: repository_path |
326 | }; |
327 | } |
328 |
|
329 | |
330 | |
331 | const args = [ |
332 | undefined, |
333 | "gitsync-settings", |
334 | "pull", |
335 | "--diff", |
336 | "--repository", |
337 | repository_path, |
338 | "--workspace", |
339 | workspace_id, |
340 | "--override", |
341 | ]; |
342 |
|
343 | if (settings_json) { |
344 | args.push("--with-backend-settings", settings_json); |
345 | } |
346 |
|
347 | if (promotion_branch) { |
348 | args.push("--promotion", promotion_branch); |
349 | } |
350 |
|
351 | args.push( |
352 | "--token", |
353 | process.env["WM_TOKEN"] ?? "", |
354 | "--base-url", |
355 | process.env["BASE_URL"] + "/", |
356 | "--json-output" |
357 | ); |
358 |
|
359 | return await wmill_run(...args); |
360 | } catch (error) { |
361 | const errorMessage = error.message || error.toString(); |
362 | |
363 | if ((errorMessage.includes("src refspec") && errorMessage.includes("does not match any")) || |
364 | (errorMessage.includes("Remote branch") && errorMessage.includes("not found"))) { |
365 | console.log("DEBUG: Empty repository detected - branch doesn't exist or no commits"); |
366 | return { |
367 | success: true, |
368 | hasChanges: true, |
369 | message: "Empty repository detected - requires initialization", |
370 | isInitialSetup: true, |
371 | repository: repository_path |
372 | }; |
373 | } |
374 | throw new Error("Settings pull dry run failed: " + errorMessage); |
375 | } |
376 | } |
377 |
|
378 | |
379 | async function executeCliSettingsPushDryRun( |
380 | workspace_id: string, |
381 | repository_path: string, |
382 | settings_json?: string, |
383 | repoPath?: string, |
384 | promotion_branch?: string |
385 | ) { |
386 | try { |
387 | console.log("DEBUG: Settings push dry run with JSON:", settings_json); |
388 |
|
389 | |
390 | if (!existsSync("wmill.yaml")) { |
391 | console.log("DEBUG: No wmill.yaml found in git repository"); |
392 | throw new Error( |
393 | "No wmill.yaml found in the git repository. Please initialize the repository first by pushing settings from Windmill to git." |
394 | ); |
395 | } |
396 |
|
397 | |
398 | const args = [ |
399 | undefined, |
400 | "gitsync-settings", |
401 | "push", |
402 | "--diff", |
403 | "--repository", |
404 | repository_path, |
405 | "--workspace", |
406 | workspace_id, |
407 | ]; |
408 |
|
409 | if (settings_json) { |
410 | args.push("--with-backend-settings", settings_json); |
411 | } |
412 |
|
413 | if (promotion_branch) { |
414 | args.push("--promotion", promotion_branch); |
415 | } |
416 |
|
417 | args.push( |
418 | "--token", |
419 | process.env["WM_TOKEN"] ?? "", |
420 | "--base-url", |
421 | process.env["BASE_URL"] + "/", |
422 | "--json-output" |
423 | ); |
424 |
|
425 | return await wmill_run(...args); |
426 | } catch (error) { |
427 | throw new Error("Settings push dry run failed: " + error.message); |
428 | } |
429 | } |
430 |
|
431 | |
432 | async function executeCliSettingsPull( |
433 | workspace_id: string, |
434 | repository_path: string, |
435 | repoPath: string, |
436 | settings_json: string, |
437 | clonedBranchName: string, |
438 | repo_resource?: any, |
439 | promotion_branch?: string |
440 | ) { |
441 | console.log("DEBUG: executeCliSettingsPull started", { |
442 | workspace_id, |
443 | repository_path, |
444 | repoPath, |
445 | settings_json: settings_json ? "PROVIDED" : "NOT_PROVIDED", |
446 | }); |
447 |
|
448 | try { |
449 | |
450 | let wmillYamlExists = existsSync("wmill.yaml"); |
451 | if (!wmillYamlExists) { |
452 | console.log( |
453 | "DEBUG: No wmill.yaml found, initializing with default settings" |
454 | ); |
455 |
|
456 | |
457 | await wmill_run( |
458 | null, |
459 | "init", |
460 | "--use-default", |
461 | "--token", |
462 | process.env["WM_TOKEN"] ?? "", |
463 | "--base-url", |
464 | process.env["BASE_URL"] + "/", |
465 | "--workspace", |
466 | workspace_id |
467 | ); |
468 |
|
469 | console.log("DEBUG: wmill.yaml initialized with defaults"); |
470 | } |
471 |
|
472 | console.log("DEBUG: Running CLI gitsync-settings pull command..."); |
473 | const args = [ |
474 | null, |
475 | "gitsync-settings", |
476 | "pull", |
477 | "--repository", |
478 | repository_path, |
479 | "--workspace", |
480 | workspace_id, |
481 | wmillYamlExists ? "--override" : "--replace", |
482 | ]; |
483 |
|
484 | if (settings_json) { |
485 | args.push("--with-backend-settings", settings_json); |
486 | } |
487 |
|
488 | if (promotion_branch) { |
489 | args.push("--promotion", promotion_branch); |
490 | } |
491 |
|
492 | args.push( |
493 | "--token", |
494 | process.env["WM_TOKEN"] ?? "", |
495 | "--base-url", |
496 | process.env["BASE_URL"] + "/" |
497 | ); |
498 |
|
499 | const res = await wmill_run(...args); |
500 | console.log("DEBUG: CLI settings pull result:", res); |
501 |
|
502 | console.log("DEBUG: Starting git push process..."); |
503 | const pushResult = await git_push( |
504 | "Update wmill.yaml via settings", |
505 | repo_resource || { gpg_key: null }, |
506 | clonedBranchName |
507 | ); |
508 | console.log("DEBUG: Git push completed:", pushResult); |
509 |
|
510 | return { success: true, message: "Settings pushed to git successfully" }; |
511 | } catch (error) { |
512 | console.log("DEBUG: Error in executeCliSettingsPull:", error); |
513 | const errorMessage = error.message || error.toString(); |
514 | throw new Error("Settings pull failed: " + errorMessage); |
515 | } |
516 | } |
517 |
|
518 | |
519 | async function executeCliSyncPullDryRun( |
520 | workspace_id: string, |
521 | repository_path: string, |
522 | settings_json?: string, |
523 | repoPath?: string |
524 | ) { |
525 | try { |
526 | console.log("DEBUG: executeCliSyncPullDryRun started", { |
527 | workspace_id, |
528 | repository_path, |
529 | settings_json: settings_json ? "PROVIDED" : "NOT_PROVIDED", |
530 | }); |
531 |
|
532 | |
533 | let wmillYamlExists = existsSync("wmill.yaml"); |
534 | let settingsDiffResult = {} |
535 | if (!wmillYamlExists) { |
536 | console.log( |
537 | "DEBUG: No wmill.yaml found, initializing with default settings" |
538 | ); |
539 |
|
540 | |
541 | await wmill_run( |
542 | null, |
543 | "init", |
544 | "--use-default", |
545 | "--token", |
546 | process.env["WM_TOKEN"] ?? "", |
547 | "--base-url", |
548 | process.env["BASE_URL"] + "/", |
549 | "--workspace", |
550 | workspace_id |
551 | ); |
552 |
|
553 | console.log("DEBUG: wmill.yaml initialized with defaults"); |
554 |
|
555 |
|
556 | |
557 | console.log("DEBUG: Checking wmill.yaml changes with gitsync-settings pull --diff"); |
558 | const settingsDiffArgs = [ |
559 | null, |
560 | "gitsync-settings", |
561 | "pull", |
562 | "--diff", |
563 | "--repository", |
564 | repository_path, |
565 | "--workspace", |
566 | workspace_id, |
567 | "--replace", |
568 | "--json-output" |
569 | ]; |
570 |
|
571 | if (settings_json) { |
572 | settingsDiffArgs.push("--with-backend-settings", settings_json); |
573 | } |
574 |
|
575 |
|
576 | settingsDiffArgs.push( |
577 | "--token", |
578 | process.env["WM_TOKEN"] ?? "", |
579 | "--base-url", |
580 | process.env["BASE_URL"] + "/" |
581 | ); |
582 |
|
583 | settingsDiffResult = await wmill_run(...settingsDiffArgs); |
584 | console.log("DEBUG: Settings diff result:", settingsDiffResult); |
585 |
|
586 | |
587 | console.log("DEBUG: Pulling git-sync settings from backend"); |
588 | const settingsArgs = [ |
589 | null, |
590 | "gitsync-settings", |
591 | "pull", |
592 | "--repository", |
593 | repository_path, |
594 | "--workspace", |
595 | workspace_id, |
596 | "--replace", |
597 | ]; |
598 |
|
599 | if (settings_json) { |
600 | settingsArgs.push("--with-backend-settings", settings_json); |
601 | } |
602 |
|
603 |
|
604 | settingsArgs.push( |
605 | "--token", |
606 | process.env["WM_TOKEN"] ?? "", |
607 | "--base-url", |
608 | process.env["BASE_URL"] + "/" |
609 | ); |
610 |
|
611 | await wmill_run(...settingsArgs); |
612 | console.log("DEBUG: Git-sync settings pulled successfully"); |
613 | } |
614 |
|
615 | const args = [ |
616 | "sync", |
617 | "pull", |
618 | "--dry-run", |
619 | "--json-output", |
620 | "--workspace", |
621 | workspace_id, |
622 | "--token", |
623 | process.env["WM_TOKEN"] ?? "", |
624 | "--base-url", |
625 | process.env["BASE_URL"] + "/", |
626 | "--repository", |
627 | repository_path, |
628 | ]; |
629 |
|
630 | const result = await wmill_run(null, ...args); |
631 |
|
632 | |
633 | if (!result.changes) { |
634 | result.changes = []; |
635 | } |
636 |
|
637 | const hasWmillYaml = result.changes.some(change => change.path === 'wmill.yaml'); |
638 | if (!hasWmillYaml) { |
639 | if (!wmillYamlExists) { |
640 | |
641 | result.total = result.total + 1 |
642 | result.changes.push({ type: 'added', path: 'wmill.yaml' }); |
643 | } else if (settingsDiffResult?.hasChanges) { |
644 | |
645 | console.log("DEBUG: Adding wmill.yaml as modified due to settings changes"); |
646 | result.total = result.total + 1 |
647 | result.changes.push({ type: 'edited', path: 'wmill.yaml' }); |
648 | } |
649 | } |
650 |
|
651 | return result; |
652 | } catch (error) { |
653 | throw new Error("Sync pull dry run failed: " + error.message); |
654 | } |
655 | } |
656 |
|
657 | |
658 | async function executeCliSyncPushDryRun( |
659 | workspace_id: string, |
660 | repository_path: string, |
661 | settings_json?: string, |
662 | repoPath?: string, |
663 | promotion_branch?: string |
664 | ) { |
665 | try { |
666 | |
667 | console.log("DEBUG: Checking wmill.yaml changes with gitsync-settings push --diff"); |
668 | const settingsArgs = [ |
669 | undefined, |
670 | "gitsync-settings", |
671 | "push", |
672 | "--diff", |
673 | "--repository", |
674 | repository_path, |
675 | "--workspace", |
676 | workspace_id, |
677 | "--json-output" |
678 | ]; |
679 |
|
680 | if (settings_json) { |
681 | settingsArgs.push("--with-backend-settings", settings_json); |
682 | } |
683 |
|
684 | if (promotion_branch) { |
685 | settingsArgs.push("--promotion", promotion_branch); |
686 | } |
687 |
|
688 | settingsArgs.push( |
689 | "--token", |
690 | process.env["WM_TOKEN"] ?? "", |
691 | "--base-url", |
692 | process.env["BASE_URL"] + "/" |
693 | ); |
694 |
|
695 | const settingsDiffResult = await wmill_run(...settingsArgs); |
696 | console.log("DEBUG: Settings diff result:", settingsDiffResult); |
697 |
|
698 | |
699 | console.log("DEBUG: Checking resource changes with sync push --dry-run"); |
700 | const syncArgs = [ |
701 | "sync", |
702 | "push", |
703 | "--dry-run", |
704 | "--json-output", |
705 | "--workspace", |
706 | workspace_id, |
707 | "--token", |
708 | process.env["WM_TOKEN"] ?? "", |
709 | "--base-url", |
710 | process.env["BASE_URL"] + "/", |
711 | "--repository", |
712 | repository_path, |
713 | ]; |
714 |
|
715 | const syncResult = await wmill_run(null, ...syncArgs); |
716 | console.log("DEBUG: Sync result:", syncResult); |
717 |
|
718 | |
719 | if (!syncResult.changes) { |
720 | syncResult.changes = []; |
721 | } |
722 |
|
723 | if (settingsDiffResult?.hasChanges) { |
724 | console.log("DEBUG: Adding wmill.yaml as modified due to settings changes"); |
725 | syncResult.settingsDiffResult = settingsDiffResult |
726 | } |
727 |
|
728 | return syncResult; |
729 | } catch (error) { |
730 | throw new Error("Sync push dry run failed: " + error.message); |
731 | } |
732 | } |
733 |
|
734 | |
735 | async function executeCliSyncPull( |
736 | workspace_id: string, |
737 | repository_path: string, |
738 | repo_resource: any, |
739 | clonedBranchName: string, |
740 | settings_json?: string |
741 | ) { |
742 | try { |
743 | |
744 | |
745 | console.log("DEBUG: Initializing with default settings"); |
746 |
|
747 | |
748 | let wmillYamlExists = existsSync("wmill.yaml"); |
749 | let settingsDiffResult = {} |
750 | if (!wmillYamlExists) { |
751 | console.log( |
752 | "DEBUG: No wmill.yaml found, initializing with default settings" |
753 | ); |
754 |
|
755 | |
756 | await wmill_run( |
757 | null, |
758 | "init", |
759 | "--use-default", |
760 | "--token", |
761 | process.env["WM_TOKEN"] ?? "", |
762 | "--base-url", |
763 | process.env["BASE_URL"] + "/", |
764 | "--workspace", |
765 | workspace_id |
766 | ); |
767 |
|
768 | console.log("DEBUG: wmill.yaml initialized with defaults"); |
769 |
|
770 |
|
771 | |
772 | console.log("DEBUG: Checking wmill.yaml changes with gitsync-settings pull --diff"); |
773 | const settingsDiffArgs = [ |
774 | null, |
775 | "gitsync-settings", |
776 | "pull", |
777 | "--diff", |
778 | "--repository", |
779 | repository_path, |
780 | "--workspace", |
781 | workspace_id, |
782 | "--replace", |
783 | "--json-output" |
784 | ]; |
785 |
|
786 | if (settings_json) { |
787 | settingsDiffArgs.push("--with-backend-settings", settings_json); |
788 | } |
789 |
|
790 | settingsDiffArgs.push( |
791 | "--token", |
792 | process.env["WM_TOKEN"] ?? "", |
793 | "--base-url", |
794 | process.env["BASE_URL"] + "/" |
795 | ); |
796 |
|
797 | settingsDiffResult = await wmill_run(...settingsDiffArgs); |
798 | console.log("DEBUG: Settings diff result:", settingsDiffResult); |
799 |
|
800 | |
801 | console.log("DEBUG: Pulling git-sync settings from backend"); |
802 | const settingsArgs = [ |
803 | null, |
804 | "gitsync-settings", |
805 | "pull", |
806 | "--repository", |
807 | repository_path, |
808 | "--workspace", |
809 | workspace_id, |
810 | "--replace", |
811 | ]; |
812 |
|
813 | if (settings_json) { |
814 | settingsArgs.push("--with-backend-settings", settings_json); |
815 | } |
816 |
|
817 | settingsArgs.push( |
818 | "--token", |
819 | process.env["WM_TOKEN"] ?? "", |
820 | "--base-url", |
821 | process.env["BASE_URL"] + "/" |
822 | ); |
823 |
|
824 | await wmill_run(...settingsArgs); |
825 | console.log("DEBUG: Git-sync settings pulled successfully"); |
826 | } |
827 |
|
828 | const args = [ |
829 | "sync", |
830 | "pull", |
831 | "--yes", |
832 | "--workspace", |
833 | workspace_id, |
834 | "--token", |
835 | process.env["WM_TOKEN"] ?? "", |
836 | "--base-url", |
837 | process.env["BASE_URL"] + "/", |
838 | "--repository", |
839 | repository_path, |
840 | ]; |
841 |
|
842 | await wmill_run(null, ...args); |
843 |
|
844 | |
845 | await git_push( |
846 | "Initialize windmill sync repo", |
847 | repo_resource, |
848 | clonedBranchName |
849 | ); |
850 | await delete_pgp_keys(); |
851 |
|
852 | return { success: true, message: "CLI sync pull completed" }; |
853 | } catch (error) { |
854 | const errorMessage = error.message || error.toString(); |
855 | throw new Error("Sync pull failed: " + errorMessage); |
856 | } |
857 | } |
858 |
|
859 | |
860 | async function executeCliSyncPush( |
861 | workspace_id: string, |
862 | repository_path: string, |
863 | repo_resource: any, |
864 | settings_json?: string |
865 | ) { |
866 | try { |
867 | |
868 | console.log("DEBUG: Getting git repo settings with gitsync-settings push --diff"); |
869 | const settingsArgs = [ |
870 | undefined, |
871 | "gitsync-settings", |
872 | "push", |
873 | "--diff", |
874 | "--repository", |
875 | repository_path, |
876 | "--workspace", |
877 | workspace_id, |
878 | "--json-output" |
879 | ]; |
880 |
|
881 | settingsArgs.push( |
882 | "--token", |
883 | process.env["WM_TOKEN"] ?? "", |
884 | "--base-url", |
885 | process.env["BASE_URL"] + "/" |
886 | ); |
887 |
|
888 | const settingsResult = await wmill_run(...settingsArgs); |
889 | console.log("DEBUG: Settings result:", settingsResult); |
890 |
|
891 | |
892 | console.log("DEBUG: Running sync push"); |
893 | const syncArgs = [ |
894 | "sync", |
895 | "push", |
896 | "--yes", |
897 | "--json-output", |
898 | "--workspace", |
899 | workspace_id, |
900 | "--token", |
901 | process.env["WM_TOKEN"] ?? "", |
902 | "--base-url", |
903 | process.env["BASE_URL"] + "/", |
904 | "--repository", |
905 | repository_path, |
906 | ]; |
907 |
|
908 | const syncResult = await wmill_run(null, ...syncArgs); |
909 | console.log("DEBUG: Sync result:", syncResult); |
910 |
|
911 | |
912 | const result = { |
913 | ...syncResult, |
914 | success: true, |
915 | message: "CLI sync push completed", |
916 | settings_json: settingsResult?.local |
917 | }; |
918 |
|
919 | console.log("DEBUG: Combined result with settings_json:", result); |
920 | return result; |
921 | } catch (error) { |
922 | throw new Error("Sync push failed: " + error.message); |
923 | } |
924 | } |
925 |
|
926 | function get_fork_branch_name(w_id: string, originalBranch: string): string { |
927 | if (w_id.startsWith(FORKED_WORKSPACE_PREFIX)) { |
928 | return w_id.replace(FORKED_WORKSPACE_PREFIX, `${FORKED_BRANCH_PREFIX}/${originalBranch}/`); |
929 | } |
930 | return w_id; |
931 | } |
932 |
|
933 | |
934 | async function git_clone( |
935 | cwd: string, |
936 | repo_resource: any, |
937 | isPull: boolean, |
938 | workspace_id: string, |
939 | cloneRefOverride?: string, |
940 | prHeadSha?: string |
941 | ): Promise<{ repo_name: string; safeDirectoryPath: string; clonedBranchName: string }> { |
942 | let repo_url = repo_resource.url; |
943 | const subfolder = repo_resource.folder ?? ""; |
944 | |
945 | |
946 | const prRefMatch = (cloneRefOverride ?? "").match(/^(?:refs\/)?(pull\/\d+\/head)$/); |
947 | |
948 | |
949 | let branch = (cloneRefOverride && cloneRefOverride !== "" && !prRefMatch) ? cloneRefOverride : (repo_resource.branch ?? ""); |
950 | const repo_name = basename(repo_url, ".git"); |
951 |
|
952 | const azureMatch = repo_url.match(/AZURE_DEVOPS_TOKEN\((?<url>.+)\)/); |
953 | if (azureMatch) { |
954 | console.log("Fetching Azure DevOps access token..."); |
955 | const azureResource = await wmillclient.getResource(azureMatch.groups.url); |
956 | const response = await fetch( |
957 | `https://login.microsoftonline.com/${azureResource.azureTenantId}/oauth2/token`, |
958 | { |
959 | method: "POST", |
960 | body: new URLSearchParams({ |
961 | client_id: azureResource.azureClientId, |
962 | client_secret: azureResource.azureClientSecret, |
963 | grant_type: "client_credentials", |
964 | resource: "499b84ac-1321-427f-aa17-267ca6975798/.default", |
965 | }), |
966 | } |
967 | ); |
968 | const { access_token } = await response.json(); |
969 | repo_url = repo_url.replace(azureMatch[0], access_token); |
970 | } |
971 |
|
972 | const args = ["clone", "--quiet", "--depth", "1"]; |
973 | if (workspace_id.startsWith(FORKED_WORKSPACE_PREFIX)) args.push("--no-single-branch"); |
974 | if (subfolder !== "") args.push("--sparse"); |
975 | if (branch !== "") args.push("--branch", branch); |
976 | args.push(repo_url, repo_name); |
977 |
|
978 | try { |
979 | await sh_run(-1, "git", ...args); |
980 | } catch (error) { |
981 | const errorString = error.toString(); |
982 | |
983 | if (branch !== "" && errorString.includes("Remote branch") && errorString.includes("not found")) { |
984 | console.log(`DEBUG: Branch ${branch} not found, cloning without branch specification for empty repo`); |
985 | |
986 | const fallbackArgs = ["clone", "--quiet", "--depth", "1"]; |
987 | if (subfolder !== "") fallbackArgs.push("--sparse"); |
988 | fallbackArgs.push(repo_url, repo_name); |
989 | await sh_run(-1, "git", ...fallbackArgs); |
990 | } else { |
991 | throw error; |
992 | } |
993 | } |
994 |
|
995 | const fullPath = join(cwd, repo_name); |
996 | process.chdir(fullPath); |
997 |
|
998 | const safeDirectoryPath = fullPath; |
999 | |
1000 | try { |
1001 | await sh_run(undefined, "git", "config", "--global", "--add", "safe.directory", process.cwd()); |
1002 | } catch (e) { |
1003 | console.log(`Warning: Could not add safe.directory config: ${e}`); |
1004 | } |
1005 |
|
1006 | if (prRefMatch) { |
1007 | const prRef = prRefMatch[1]; |
1008 | |
1009 | |
1010 | |
1011 | const maxAttempts = prHeadSha ? 5 : 1; |
1012 | let fetchError: any = undefined; |
1013 | let fetched = false; |
1014 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
1015 | if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 3000)); |
1016 | try { |
1017 | await sh_run(undefined, "git", "fetch", "--depth", "1", "origin", `+${prRef}:__wm_pr_ref`); |
1018 | if (prHeadSha) { |
1019 | const sha = (await sh_run(undefined, "git", "rev-parse", "__wm_pr_ref")).trim(); |
1020 | if (sha !== prHeadSha) { |
1021 | console.log(`DEBUG: ${prRef} is at ${sha}, expected ${prHeadSha} (attempt ${attempt}/${maxAttempts})`); |
1022 | continue; |
1023 | } |
1024 | } |
1025 | fetched = true; |
1026 | break; |
1027 | } catch (error) { |
1028 | fetchError = error; |
1029 | console.log(`DEBUG: Fetching ${prRef} failed (attempt ${attempt}/${maxAttempts})`); |
1030 | } |
1031 | } |
1032 | if (!fetched) { |
1033 | if (prHeadSha) { |
1034 | |
1035 | |
1036 | throw new Error( |
1037 | `PR_HEAD_REF_UNAVAILABLE: could not fetch ${prRef} at ${prHeadSha}` |
1038 | ); |
1039 | } |
1040 | throw fetchError; |
1041 | } |
1042 | if (prHeadSha) { |
1043 | |
1044 | |
1045 | |
1046 | |
1047 | const baseBranch = branch !== "" |
1048 | ? branch |
1049 | : (await sh_run(undefined, "git", "rev-parse", "--abbrev-ref", "HEAD")).trim(); |
1050 | |
1051 | |
1052 | for (const step of ["--deepen=64", "--deepen=1024", "--unshallow"]) { |
1053 | const found = await sh_run(undefined, "git", "merge-base", "HEAD", "__wm_pr_ref") |
1054 | .then(() => true) |
1055 | .catch(() => false); |
1056 | if (found) break; |
1057 | try { |
1058 | await sh_run(undefined, "git", "fetch", step, "origin", baseBranch, `+${prRef}:__wm_pr_ref`); |
1059 | } catch (error) { |
1060 | console.log(`DEBUG: fetch ${step} failed: ${error}`); |
1061 | } |
1062 | } |
1063 | const hasMergeBase = await sh_run(undefined, "git", "merge-base", "HEAD", "__wm_pr_ref") |
1064 | .then(() => true) |
1065 | .catch(() => false); |
1066 | if (!hasMergeBase) { |
1067 | |
1068 | |
1069 | |
1070 | const shallow = ( |
1071 | await sh_run(undefined, "git", "rev-parse", "--is-shallow-repository").catch(() => "true") |
1072 | ).toString().trim(); |
1073 | if (shallow === "false") { |
1074 | throw new Error( |
1075 | `PR_MERGE_CONFLICTS: ${prRef} shares no history with ${baseBranch}` |
1076 | ); |
1077 | } |
1078 | throw new Error( |
1079 | `PR_HEAD_REF_UNAVAILABLE: could not fetch enough history to find a merge base between ${baseBranch} and ${prRef}` |
1080 | ); |
1081 | } |
1082 | |
1083 | |
1084 | |
1085 | |
1086 | const refSha = (await sh_run(undefined, "git", "rev-parse", "__wm_pr_ref")).trim(); |
1087 | if (refSha !== prHeadSha) { |
1088 | throw new Error( |
1089 | `PR_HEAD_REF_UNAVAILABLE: ${prRef} moved to ${refSha} while computing the diff for ${prHeadSha}` |
1090 | ); |
1091 | } |
1092 | try { |
1093 | await sh_run( |
1094 | undefined, |
1095 | "git", |
1096 | "-c", "user.name=windmill", |
1097 | "-c", "[email protected]", |
1098 | "merge", "--no-ff", "--no-edit", "__wm_pr_ref" |
1099 | ); |
1100 | } catch (error) { |
1101 | |
1102 | |
1103 | const unmerged = await sh_run(undefined, "git", "ls-files", "-u") |
1104 | .then((out) => out.trim()) |
1105 | .catch(() => ""); |
1106 | if (unmerged !== "") { |
1107 | |
1108 | |
1109 | throw new Error( |
1110 | `PR_MERGE_CONFLICTS: merging ${prRef} into ${baseBranch} produced conflicts` |
1111 | ); |
1112 | } |
1113 | throw error; |
1114 | } |
1115 | } else { |
1116 | await sh_run(undefined, "git", "checkout", "__wm_pr_ref"); |
1117 | } |
1118 | } |
1119 |
|
1120 | if (subfolder !== "") { |
1121 | await sh_run(undefined, "git", "sparse-checkout", "add", subfolder); |
1122 | const subfolderPath = join(fullPath, subfolder); |
1123 |
|
1124 | if (!existsSync(subfolderPath)) { |
1125 | if (isPull) { |
1126 | |
1127 | throw new Error(`Subfolder ${subfolder} does not exist.`); |
1128 | } else { |
1129 | |
1130 | console.log( |
1131 | `DEBUG: Creating subfolder ${subfolder} for push operation` |
1132 | ); |
1133 | await sh_run(undefined, "mkdir", "-p", subfolderPath); |
1134 | } |
1135 | } |
1136 |
|
1137 | process.chdir(subfolderPath); |
1138 | } |
1139 |
|
1140 | let clonedBranchName: string; |
1141 | try { |
1142 | clonedBranchName = (await sh_run(undefined, "git", "rev-parse", "--abbrev-ref", "HEAD")).trim(); |
1143 | } catch (error) { |
1144 | |
1145 | console.log("DEBUG: No HEAD found (empty repository), using target branch:", branch || "main"); |
1146 | clonedBranchName = branch || "main"; |
1147 | } |
1148 | |
1149 | if ( |
1150 | workspace_id.startsWith(FORKED_WORKSPACE_PREFIX) && |
1151 | !clonedBranchName.startsWith(`${FORKED_BRANCH_PREFIX}/`) |
1152 | ) { |
1153 | clonedBranchName = get_fork_branch_name(workspace_id, clonedBranchName); |
1154 | try { |
1155 | |
1156 | |
1157 | await sh_run(undefined, "git", "checkout", "-b", clonedBranchName, `origin/${clonedBranchName}`); |
1158 | } catch { |
1159 | try { |
1160 | await sh_run(undefined, "git", "checkout", "-b", clonedBranchName); |
1161 | } catch { |
1162 | console.info("Could not create branch, trying to switch to existing branch"); |
1163 | await sh_run(undefined, "git", "checkout", clonedBranchName); |
1164 | } |
1165 | } |
1166 | } |
1167 |
|
1168 | return { repo_name, safeDirectoryPath, clonedBranchName }; |
1169 | } |
1170 |
|
1171 | |
1172 | async function sh_run( |
1173 | secret_position: number | undefined, |
1174 | cmd: string, |
1175 | ...args: string[] |
1176 | ) { |
1177 | const nargs = secret_position != undefined ? args.slice() : args; |
1178 | if (secret_position && secret_position < 0) |
1179 | secret_position = nargs.length - 1 + secret_position; |
1180 |
|
1181 | let secret: string | undefined = undefined; |
1182 | if (secret_position != undefined) { |
1183 | nargs[secret_position] = "***"; |
1184 | secret = args[secret_position]; |
1185 | } |
1186 |
|
1187 | console.log(`DEBUG: Running shell command: '${cmd} ${nargs.join(" ")} ...'`); |
1188 | try { |
1189 | const { stdout, stderr } = await exec(`${cmd} ${args.join(" ")}`); |
1190 | if (stdout.length > 0) { |
1191 | console.log("DEBUG: Shell stdout:", stdout); |
1192 | } |
1193 | if (stderr.length > 0) { |
1194 | console.log("DEBUG: Shell stderr:", stderr); |
1195 | } |
1196 | console.log(`DEBUG: Shell command completed successfully: ${cmd}`); |
1197 | return stdout; |
1198 | } catch (error: any) { |
1199 | let errorString = error.toString(); |
1200 | if (secret) errorString = errorString.replace(secret, "***"); |
1201 | console.log(`DEBUG: Shell command FAILED: ${cmd}`, errorString); |
1202 | throw new Error( |
1203 | `SH command '${cmd} ${nargs.join(" ")}' failed: ${errorString}` |
1204 | ); |
1205 | } |
1206 | } |
1207 |
|
1208 | async function wmill_run( |
1209 | secret_position: number | undefined | null, |
1210 | ...cmd: string[] |
1211 | ) { |
1212 | cmd = cmd.filter((elt) => elt !== ""); |
1213 | const cmd2 = cmd.slice(); |
1214 | if (secret_position) { |
1215 | cmd2[secret_position] = "***"; |
1216 | } |
1217 | console.log(`DEBUG: Running CLI command: 'wmill ${cmd2.join(" ")} ...'`); |
1218 |
|
1219 | |
1220 | const originalLog = console.log; |
1221 | let cliOutput = ""; |
1222 | console.log = (msg: string) => { |
1223 | cliOutput += msg + "\n"; |
1224 | originalLog(msg); |
1225 | }; |
1226 |
|
1227 | try { |
1228 | await wmill.parse(cmd); |
1229 | console.log = originalLog; |
1230 | console.log("DEBUG: CLI command executed successfully"); |
1231 | } catch (error) { |
1232 | console.log = originalLog; |
1233 | console.log("DEBUG: CLI command execution failed:", error); |
1234 | throw error; |
1235 | } |
1236 | |
1237 |
|
1238 | console.log("DEBUG: Captured CLI output length:", cliOutput.length); |
1239 | console.log("DEBUG: Raw CLI output:", cliOutput); |
1240 |
|
1241 | try { |
1242 | console.log("DEBUG: Attempting to parse CLI output as JSON..."); |
1243 |
|
1244 | |
1245 | const jsonStartIndex = cliOutput.indexOf('{'); |
1246 | if (jsonStartIndex === -1) { |
1247 | console.log("DEBUG: No JSON found in CLI output"); |
1248 | return {}; |
1249 | } |
1250 |
|
1251 | |
1252 | const jsonString = cliOutput.substring(jsonStartIndex).trim(); |
1253 | console.log("DEBUG: Extracted JSON string:", jsonString); |
1254 |
|
1255 | const res = JSON.parse(jsonString); |
1256 | console.log("DEBUG: Successfully parsed JSON result:", res); |
1257 | return res; |
1258 | } catch (e) { |
1259 | console.log("DEBUG: Failed to parse CLI output as JSON:", e); |
1260 | console.log("DEBUG: Returning empty object"); |
1261 | return {}; |
1262 | } |
1263 | } |
1264 |
|
1265 | async function git_push( |
1266 | commit_msg: string, |
1267 | repo_resource: any, |
1268 | target_branch: string |
1269 | ) { |
1270 | console.log("DEBUG: git_push started", { |
1271 | commit_msg, |
1272 | target_branch, |
1273 | has_gpg_key: !!repo_resource.gpg_key, |
1274 | }); |
1275 |
|
1276 | const user_email = process.env["WM_EMAIL"] ?? ""; |
1277 | const user_name = process.env["WM_USERNAME"] ?? ""; |
1278 |
|
1279 | if (repo_resource.gpg_key) { |
1280 | console.log("DEBUG: Setting up GPG signing..."); |
1281 | await set_gpg_signing_secret(repo_resource.gpg_key); |
1282 | |
1283 | console.log("DEBUG: Setting git user config with GPG key email..."); |
1284 | await sh_run( |
1285 | undefined, |
1286 | "git", |
1287 | "config", |
1288 | "user.email", |
1289 | repo_resource.gpg_key.email |
1290 | ); |
1291 | await sh_run(undefined, "git", "config", "user.name", user_name); |
1292 | } else { |
1293 | console.log("DEBUG: Setting git user config..."); |
1294 | await sh_run(undefined, "git", "config", "user.email", user_email); |
1295 | await sh_run(undefined, "git", "config", "user.name", user_name); |
1296 | } |
1297 |
|
1298 | try { |
1299 | console.log("DEBUG: Adding files to git..."); |
1300 | await sh_run(undefined, "git", "add", "-A", ":!./.config"); |
1301 | console.log("DEBUG: Files added successfully"); |
1302 | } catch (error) { |
1303 | console.log("DEBUG: Unable to stage files:", error); |
1304 | } |
1305 |
|
1306 | try { |
1307 | console.log("DEBUG: Checking for changes to commit..."); |
1308 | await sh_run(undefined, "git", "diff", "--cached", "--quiet"); |
1309 | console.log("DEBUG: No changes detected, returning no changes status"); |
1310 | return { status: "no changes pushed" }; |
1311 | } catch { |
1312 | console.log("DEBUG: Changes detected, proceeding with commit..."); |
1313 | |
1314 | await sh_run( |
1315 | undefined, |
1316 | "git", |
1317 | "commit", |
1318 | "--author", |
1319 | `"${user_name} <${user_email}>"`, |
1320 | "-m", |
1321 | `"${commit_msg}"` |
1322 | ); |
1323 | console.log("DEBUG: Commit completed successfully"); |
1324 |
|
1325 | try { |
1326 | console.log("DEBUG: Attempting first push..."); |
1327 | await sh_run(undefined, "git", "push", "--set-upstream", "origin", target_branch); |
1328 | console.log("DEBUG: First push succeeded"); |
1329 | return { status: "changes pushed" }; |
1330 | } catch (e) { |
1331 | const errorString = e.toString(); |
1332 |
|
1333 | |
1334 | if (errorString.includes("src refspec") && errorString.includes("does not match any")) { |
1335 | console.log("DEBUG: Empty repository detected - setting up initial branch and push"); |
1336 | try { |
1337 | |
1338 | |
1339 | await sh_run(undefined, "git", "branch", "-M", target_branch); |
1340 | console.log(`DEBUG: Set branch to ${target_branch}`); |
1341 |
|
1342 | |
1343 | await sh_run(undefined, "git", "push", "-u", "origin", target_branch); |
1344 | console.log(`DEBUG: Initial push to ${target_branch} branch succeeded`); |
1345 | return { status: "changes pushed" }; |
1346 | } catch (initialPushError) { |
1347 | console.log("DEBUG: Initial push setup failed:", initialPushError); |
1348 | throw initialPushError; |
1349 | } |
1350 | } |
1351 |
|
1352 | console.log("DEBUG: First push failed, attempting rebase and retry:", e); |
1353 | try { |
1354 | await sh_run(undefined, "git", "pull", "--rebase"); |
1355 | console.log("DEBUG: Rebase completed, attempting second push..."); |
1356 | await sh_run(undefined, "git", "push", "--set-upstream", "origin", target_branch); |
1357 | console.log("DEBUG: Second push succeeded"); |
1358 | return { status: "changes pushed" }; |
1359 | } catch (retryError) { |
1360 | const retryErrorString = retryError.toString(); |
1361 |
|
1362 | |
1363 | if (retryErrorString.includes("no such ref was fetched") || |
1364 | retryErrorString.includes("couldn't find remote ref")) { |
1365 | console.log("DEBUG: Retry failed due to empty repository - setting up initial branch and push"); |
1366 | try { |
1367 | |
1368 | await sh_run(undefined, "git", "branch", "-M", target_branch); |
1369 | console.log(`DEBUG: Set branch to ${target_branch}`); |
1370 |
|
1371 | |
1372 | await sh_run(undefined, "git", "push", "-u", "origin", target_branch); |
1373 | console.log(`DEBUG: Initial push to ${target_branch} branch after retry succeeded`); |
1374 | return { status: "changes pushed" }; |
1375 | } catch (finalPushError) { |
1376 | console.log("DEBUG: Final push attempt failed:", finalPushError); |
1377 | throw finalPushError; |
1378 | } |
1379 | } |
1380 |
|
1381 | console.log("DEBUG: Second push also failed:", retryError); |
1382 | throw retryError; |
1383 | } |
1384 | } |
1385 | } |
1386 | } |
1387 |
|
1388 | async function set_gpg_signing_secret(gpg_key: GpgKey) { |
1389 | const gpg_path = "/tmp/gpg"; |
1390 | await sh_run(undefined, "mkdir", "-p", gpg_path); |
1391 | await sh_run(undefined, "chmod", "700", gpg_path); |
1392 | process.env.GNUPGHOME = gpg_path; |
1393 |
|
1394 | const formatted = gpg_key.private_key.replace( |
1395 | /(-----BEGIN PGP PRIVATE KEY BLOCK-----)([\s\S]*?)(-----END PGP PRIVATE KEY BLOCK-----)/, |
1396 | (_, header, body, footer) => |
1397 | header + "\n\n" + body.replace(/ ([^\s])/g, "\n$1").trim() + "\n" + footer |
1398 | ); |
1399 |
|
1400 | try { |
1401 | await sh_run( |
1402 | 1, |
1403 | "bash", |
1404 | "-c", |
1405 | `cat <<EOF | gpg --batch --import \n${formatted}\nEOF` |
1406 | ); |
1407 | } catch { |
1408 | throw new Error("Failed to import GPG key!"); |
1409 | } |
1410 |
|
1411 | const keyList = await sh_run( |
1412 | undefined, |
1413 | "gpg", |
1414 | "--list-secret-keys", |
1415 | "--with-colons", |
1416 | "--keyid-format=long" |
1417 | ); |
1418 | const match = keyList.match( |
1419 | /sec:[^:]*:[^:]*:[^:]*:([A-F0-9]+):.*\nfpr:::::::::([A-F0-9]{40}):/ |
1420 | ); |
1421 | if (!match) throw new Error("Failed to extract GPG Key ID and Fingerprint"); |
1422 |
|
1423 | const keyId = match[1]; |
1424 | gpgFingerprint = match[2]; |
1425 |
|
1426 | if (gpg_key.passphrase) { |
1427 | await sh_run( |
1428 | 1, |
1429 | "bash", |
1430 | "-c", |
1431 | `echo dummy | gpg --batch --pinentry-mode loopback --passphrase '${gpg_key.passphrase}' --status-fd=2 -bsau ${keyId}` |
1432 | ); |
1433 | } |
1434 |
|
1435 | await sh_run(undefined, "git", "config", "user.signingkey", keyId); |
1436 | await sh_run(undefined, "git", "config", "commit.gpgsign", "true"); |
1437 | } |
1438 |
|
1439 | async function delete_pgp_keys() { |
1440 | if (gpgFingerprint) { |
1441 | await sh_run( |
1442 | undefined, |
1443 | "gpg", |
1444 | "--batch", |
1445 | "--yes", |
1446 | "--pinentry-mode", |
1447 | "loopback", |
1448 | "--delete-secret-key", |
1449 | gpgFingerprint |
1450 | ); |
1451 | await sh_run( |
1452 | undefined, |
1453 | "gpg", |
1454 | "--batch", |
1455 | "--yes", |
1456 | "--pinentry-mode", |
1457 | "loopback", |
1458 | "--delete-key", |
1459 | gpgFingerprint |
1460 | ); |
1461 | } |
1462 | } |
1463 |
|
1464 | async function get_gh_app_token() { |
1465 | const workspace = process.env["WM_WORKSPACE"]; |
1466 | const jobToken = process.env["WM_TOKEN"]; |
1467 | const baseUrl = |
1468 | process.env["BASE_INTERNAL_URL"] ?? |
1469 | process.env["BASE_URL"] ?? |
1470 | "http://localhost:8000"; |
1471 | const url = `${baseUrl}/api/w/${workspace}/github_app/token`; |
1472 |
|
1473 | const response = await fetch(url, { |
1474 | method: "POST", |
1475 | headers: { |
1476 | "Content-Type": "application/json", |
1477 | Authorization: `Bearer ${jobToken}`, |
1478 | }, |
1479 | body: JSON.stringify({ job_token: jobToken }), |
1480 | }); |
1481 |
|
1482 | if (!response.ok) { |
1483 | const errorBody = await response.text().catch(() => ""); |
1484 | throw new Error(`GitHub App token error (${response.status}): ${errorBody || response.statusText}`); |
1485 | } |
1486 | const data = await response.json(); |
1487 | return data.token; |
1488 | } |
1489 |
|
1490 | |
1491 | |
1492 | |
1493 | function normalizeGitHubUrl(raw: string): string { |
1494 | const cleaned = raw.trim(); |
1495 | if (cleaned.includes("://")) { |
1496 | return cleaned; |
1497 | } |
1498 | const colonIdx = cleaned.indexOf(":"); |
1499 | if (colonIdx !== -1) { |
1500 | const before = cleaned.slice(0, colonIdx); |
1501 | const after = cleaned.slice(colonIdx + 1); |
1502 | const firstSeg = after.split("/")[0]; |
1503 | |
1504 | |
1505 | const isPort = !before.includes("@") && firstSeg !== "" && /^\d+$/.test(firstSeg); |
1506 | |
1507 | |
1508 | const isUserinfo = firstSeg.includes("@"); |
1509 | if (before !== "" && !before.includes("/") && after !== "" && !isPort && !isUserinfo) { |
1510 | const host = before.split("@").pop(); |
1511 | return `https://${host}/${after.replace(/^\/+/, "")}`; |
1512 | } |
1513 | } |
1514 | return `https://${cleaned}`; |
1515 | } |
1516 |
|
1517 | function prependTokenToGitHubUrl(gitHubUrl: string, installationToken: string) { |
1518 | |
1519 | const url = new URL(normalizeGitHubUrl(gitHubUrl)); |
1520 | return `https://x-access-token:${installationToken}@${url.host}${url.pathname}`; |
1521 | } |
1522 |
|