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