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