1 | import * as wmill from "windmill-client"; |
2 | import { basename } from "node:path" |
3 | const util = require('util'); |
4 | const exec = util.promisify(require('child_process').exec); |
5 |
|
6 |
|
7 | export async function main(repo_url_resource_path: string) { |
8 | const cwd = process.cwd(); |
9 | process.env["HOME"] = "."; |
10 | console.log(`Cloning repo from resource`); |
11 | const repo_name = await git_clone(repo_url_resource_path); |
12 | process.chdir(`${cwd}/${repo_name}`); |
13 | console.log(`Attempting an empty push to repository ${repo_name}`); |
14 | await git_push(); |
15 | console.log("Finished"); |
16 | process.chdir(`${cwd}`); |
17 | } |
18 |
|
19 | async function git_clone(repo_resource_path: string): Promise<string> { |
20 | |
21 |
|
22 | const repo_resource = await wmill.getResource(repo_resource_path); |
23 |
|
24 | let repo_url = repo_resource.url |
25 |
|
26 | if (repo_resource.is_github_app) { |
27 | const token = await get_gh_app_token() |
28 | const authRepoUrl = prependTokenToGitHubUrl(repo_resource.url, token); |
29 | repo_url = authRepoUrl; |
30 | } |
31 |
|
32 | const azureMatch = repo_url.match(/AZURE_DEVOPS_TOKEN\((?<url>.+)\)/); |
33 | if (azureMatch) { |
34 | console.log( |
35 | "Requires Azure DevOps service account access token, requesting..." |
36 | ); |
37 | const azureResource = await wmill.getResource(azureMatch.groups.url); |
38 | const response = await fetch( |
39 | `https://login.microsoftonline.com/${azureResource.azureTenantId}/oauth2/token`, |
40 | { |
41 | method: "POST", |
42 | body: new URLSearchParams({ |
43 | client_id: azureResource.azureClientId, |
44 | client_secret: azureResource.azureClientSecret, |
45 | grant_type: "client_credentials", |
46 | resource: "499b84ac-1321-427f-aa17-267ca6975798/.default", |
47 | }), |
48 | } |
49 | ); |
50 | const { access_token } = await response.json(); |
51 | repo_url = repo_url.replace(azureMatch[0], access_token); |
52 | } |
53 | const repo_name = basename(repo_url, ".git"); |
54 | await sh_run(4, "git", "clone", "--quiet", "--depth", "1", repo_url, repo_name); |
55 | return repo_name; |
56 | } |
57 | async function git_push() { |
58 | await sh_run(undefined, "git", "config", "user.email", process.env["WM_EMAIL"]) |
59 | await sh_run(undefined, "git", "config", "user.name", process.env["WM_USERNAME"]) |
60 | await sh_run(undefined, "git", "push"); |
61 | } |
62 |
|
63 | async function sh_run( |
64 | secret_position: number | undefined, |
65 | cmd: string, |
66 | ...args: string[] |
67 | ) { |
68 | const nargs = secret_position != undefined ? args.slice() : args; |
69 | if (secret_position && secret_position < 0) { |
70 | secret_position = nargs.length - 1 + secret_position; |
71 | } |
72 | let secret: string | undefined = undefined; |
73 | if (secret_position != undefined) { |
74 | nargs[secret_position] = "***"; |
75 | secret = args[secret_position]; |
76 | } |
77 |
|
78 | console.log(`Running '${cmd} ${nargs.join(" ")} ...'`); |
79 | const command = exec(`${cmd} ${args.join(" ")}`) |
80 | try { |
81 | const { stdout, stderr } = await command |
82 | if (stdout.length > 0) { |
83 | console.log(stdout); |
84 | } |
85 | if (stderr.length > 0) { |
86 | console.log(stderr); |
87 | } |
88 | console.log("Command successfully executed"); |
89 |
|
90 | } catch (error) { |
91 | let errorString = error.toString(); |
92 | if (secret) { |
93 | errorString = errorString.replace(secret, "***"); |
94 | } |
95 | const err = `SH command '${cmd} ${nargs.join( |
96 | " " |
97 | )}' returned with error ${errorString}`; |
98 | throw Error(err); |
99 | } |
100 | } |
101 |
|
102 | async function get_gh_app_token() { |
103 | const workspace = process.env["WM_WORKSPACE"]; |
104 | const jobToken = process.env["WM_TOKEN"]; |
105 |
|
106 | const baseUrl = |
107 | process.env["BASE_INTERNAL_URL"] ?? |
108 | process.env["BASE_URL"] ?? |
109 | "http://localhost:8000"; |
110 |
|
111 | const url = `${baseUrl}/api/w/${workspace}/github_app/token`; |
112 |
|
113 | const response = await fetch(url, { |
114 | method: 'POST', |
115 | headers: { |
116 | 'Content-Type': 'application/json', |
117 | 'Authorization': `Bearer ${jobToken}`, |
118 | }, |
119 | body: JSON.stringify({ |
120 | job_token: jobToken, |
121 | }), |
122 | }); |
123 |
|
124 | if (!response.ok) { |
125 | throw new Error(`Error: ${response.statusText}`); |
126 | } |
127 |
|
128 | const data = await response.json(); |
129 |
|
130 | return data.token; |
131 | } |
132 |
|
133 | function prependTokenToGitHubUrl(gitHubUrl: string, installationToken: string) { |
134 | if (!gitHubUrl || !installationToken) { |
135 | throw new Error("Both GitHub URL and Installation Token are required."); |
136 | } |
137 |
|
138 | try { |
139 | const url = new URL(gitHubUrl); |
140 |
|
141 | |
142 | if (url.hostname !== "github.com") { |
143 | throw new Error("Invalid GitHub URL. Must be in the format 'https://github.com/owner/repo.git'."); |
144 | } |
145 |
|
146 | |
147 | return `https://x-access-token:${installationToken}@github.com${url.pathname}`; |
148 | } catch (e) { |
149 | const error = e as Error |
150 | throw new Error(`Invalid URL: ${error.message}`) |
151 | } |
152 | } |