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