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