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