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