1 | import * as wmillclient from "npm:windmill-client@1.321.2"; |
2 | import wmill from "https://deno.land/x/wmill@v1.321.2/main.ts"; |
3 | import { basename } from "https://deno.land/std@0.208.0/path/mod.ts"; |
4 |
|
5 | export async function main( |
6 | workspace_id: string, |
7 | repo_url_resource_path: string, |
8 | path_type: |
9 | | "script" |
10 | | "flow" |
11 | | "app" |
12 | | "folder" |
13 | | "resource" |
14 | | "variable" |
15 | | "resourcetype" |
16 | | "schedule" |
17 | | "user" |
18 | | "group", |
19 | skip_secret = true, |
20 | path: string | undefined, |
21 | parent_path: string | undefined, |
22 | commit_msg: string, |
23 | use_individual_branch = false, |
24 | group_by_folder = false |
25 | ) { |
26 | const repo_resource = await wmillclient.getResource(repo_url_resource_path); |
27 |
|
28 | const cwd = Deno.cwd(); |
29 | Deno.env.set("HOME", "."); |
30 | console.log( |
31 | `Syncing ${path_type} ${path ?? ""} with parent ${parent_path ?? ""}` |
32 | ); |
33 |
|
34 | const repo_name = await git_clone(cwd, repo_resource, use_individual_branch); |
35 | await move_to_git_branch( |
36 | workspace_id, |
37 | path_type, |
38 | path, |
39 | parent_path, |
40 | use_individual_branch, |
41 | group_by_folder |
42 | ); |
43 |
|
44 | const subfolder = repo_resource.folder ?? ""; |
45 | const branch_or_default = repo_resource.branch ?? "<DEFAULT>"; |
46 | console.log( |
47 | `Pushing to repository ${repo_name} in subfolder ${subfolder} on branch ${branch_or_default}` |
48 | ); |
49 |
|
50 | await wmill_sync_pull( |
51 | path_type, |
52 | workspace_id, |
53 | path, |
54 | parent_path, |
55 | skip_secret |
56 | ); |
57 |
|
58 | await git_push(path, parent_path, commit_msg); |
59 |
|
60 | console.log("Finished syncing"); |
61 | Deno.chdir(`${cwd}`); |
62 | } |
63 |
|
64 | async function git_clone( |
65 | cwd: string, |
66 | repo_resource: any, |
67 | use_individual_branch: boolean |
68 | ): Promise<string> { |
69 | |
70 | let repo_url = repo_resource.url; |
71 | const subfolder = repo_resource.folder ?? ""; |
72 | const branch = repo_resource.branch ?? ""; |
73 | const repo_name = basename(repo_url, ".git"); |
74 |
|
75 | const azureMatch = repo_url.match(/AZURE_DEVOPS_TOKEN\((?<url>.+)\)/); |
76 |
|
77 | if (azureMatch) { |
78 | console.log( |
79 | "Requires Azure DevOps service account access token, requesting..." |
80 | ); |
81 | const azureResource = await wmillclient.getResource(azureMatch.groups.url); |
82 |
|
83 | const response = await fetch( |
84 | `https://login.microsoftonline.com/${azureResource.azureTenantId}/oauth2/token`, |
85 | { |
86 | method: "POST", |
87 | body: new URLSearchParams({ |
88 | client_id: azureResource.azureClientId, |
89 | client_secret: azureResource.azureClientSecret, |
90 | grant_type: "client_credentials", |
91 | resource: "499b84ac-1321-427f-aa17-267ca6975798/.default", |
92 | }), |
93 | } |
94 | ); |
95 |
|
96 | const { access_token } = await response.json(); |
97 |
|
98 | repo_url = repo_url.replace(azureMatch[0], access_token); |
99 | } |
100 |
|
101 | const args = ["clone", "--quiet", "--depth", "1"]; |
102 | if (use_individual_branch) { |
103 | args.push("--no-single-branch"); |
104 | } |
105 | if (subfolder !== "") { |
106 | args.push("--sparse"); |
107 | } |
108 | if (branch !== "") { |
109 | args.push("--branch"); |
110 | args.push(branch); |
111 | } |
112 | args.push(repo_url); |
113 | args.push(repo_name); |
114 | await sh_run(-1, "git", ...args); |
115 |
|
116 | try { |
117 | Deno.chdir(`${cwd}/${repo_name}`); |
118 | } catch (err) { |
119 | console.log( |
120 | `Error changing directory to '${cwd}/${repo_name}'. Error was:\n${err}` |
121 | ); |
122 | throw err; |
123 | } |
124 | Deno.chdir(`${cwd}/${repo_name}`); |
125 | if (subfolder !== "") { |
126 | await sh_run(undefined, "git", "sparse-checkout", "add", subfolder); |
127 | } |
128 | try { |
129 | Deno.chdir(`${cwd}/${repo_name}/${subfolder}`); |
130 | } catch (err) { |
131 | console.log( |
132 | `Error changing directory to '${cwd}/${repo_name}/${subfolder}'. Error was:\n${err}` |
133 | ); |
134 | throw err; |
135 | } |
136 |
|
137 | return repo_name; |
138 | } |
139 |
|
140 | async function move_to_git_branch( |
141 | workspace_id: string, |
142 | path_type: |
143 | | "script" |
144 | | "flow" |
145 | | "app" |
146 | | "folder" |
147 | | "resource" |
148 | | "variable" |
149 | | "resourcetype" |
150 | | "schedule" |
151 | | "user" |
152 | | "group", |
153 | path: string | undefined, |
154 | parent_path: string | undefined, |
155 | use_individual_branch: boolean, |
156 | group_by_folder: boolean |
157 | ) { |
158 | if (!use_individual_branch || path_type === "user" || path_type === "group") { |
159 | return; |
160 | } |
161 |
|
162 | const branchName = group_by_folder |
163 | ? `wm_deploy/${workspace_id}/${(path ?? parent_path) |
164 | ?.split("/") |
165 | .slice(0, 2) |
166 | .join("__")}` |
167 | : `wm_deploy/${workspace_id}/${path_type}/${( |
168 | path ?? parent_path |
169 | )?.replaceAll("/", "__")}`; |
170 |
|
171 | try { |
172 | await sh_run(undefined, "git", "checkout", branchName); |
173 | } catch (err) { |
174 | console.log( |
175 | `Error checking out branch ${branchName}. It is possible it doesn't exist yet, tentatively creating it... Error was:\n${err}` |
176 | ); |
177 | try { |
178 | await sh_run(undefined, "git", "checkout", "-b", branchName); |
179 | await sh_run( |
180 | undefined, |
181 | "git", |
182 | "config", |
183 | "--add", |
184 | "--bool", |
185 | "push.autoSetupRemote", |
186 | "true" |
187 | ); |
188 | } catch (err) { |
189 | console.log( |
190 | `Error checking out branch '${branchName}'. Error was:\n${err}` |
191 | ); |
192 | throw err; |
193 | } |
194 | } |
195 | console.log(`Successfully switched to branch ${branchName}`); |
196 | } |
197 |
|
198 | async function git_push( |
199 | path: string | undefined, |
200 | parent_path: string | undefined, |
201 | commit_msg: string |
202 | ) { |
203 | await sh_run( |
204 | undefined, |
205 | "git", |
206 | "config", |
207 | "user.email", |
208 | Deno.env.get("WM_EMAIL") ?? "" |
209 | ); |
210 | await sh_run( |
211 | undefined, |
212 | "git", |
213 | "config", |
214 | "user.name", |
215 | Deno.env.get("WM_USERNAME") ?? "" |
216 | ); |
217 | if (path !== undefined && path !== null && path !== "") { |
218 | try { |
219 | await sh_run(undefined, "git", "add", `${path}**`); |
220 | } catch (e) { |
221 | console.log(`Unable to stage files matching ${path}**, ${e}`); |
222 | } |
223 | } |
224 | if (parent_path !== undefined && parent_path !== null && parent_path !== "") { |
225 | try { |
226 | await sh_run(undefined, "git", "add", `${parent_path}**`); |
227 | } catch (e) { |
228 | console.log(`Unable to stage files matching ${parent_path}, ${e}`); |
229 | } |
230 | } |
231 | try { |
232 | await sh_run(undefined, "git", "diff", "--cached", "--quiet"); |
233 | } catch { |
234 | |
235 | await sh_run(undefined, "git", "commit", "-m", commit_msg); |
236 | await sh_run(undefined, "git", "push", "--porcelain"); |
237 | return; |
238 | } |
239 | console.log("No changes detected, nothing to commit. Returning..."); |
240 | } |
241 |
|
242 | async function sh_run( |
243 | secret_position: number | undefined, |
244 | cmd: string, |
245 | ...args: string[] |
246 | ) { |
247 | const nargs = secret_position != undefined ? args.slice() : args; |
248 | if (secret_position < 0) { |
249 | secret_position = nargs.length - 1 + secret_position; |
250 | } |
251 | if (secret_position != undefined) { |
252 | nargs[secret_position] = "***"; |
253 | } |
254 | console.log(`Running '${cmd} ${nargs.join(" ")} ...'`); |
255 | const command = new Deno.Command(cmd, { |
256 | args: args, |
257 | }); |
258 |
|
259 | const { code, stdout, stderr } = await command.output(); |
260 | if (stdout.length > 0) { |
261 | console.log(new TextDecoder().decode(stdout)); |
262 | } |
263 | if (stderr.length > 0) { |
264 | console.log(new TextDecoder().decode(stderr)); |
265 | } |
266 | if (code !== 0) { |
267 | const err = `SH command '${cmd} ${args.join( |
268 | " " |
269 | )}' returned with a non-zero status ${code}.`; |
270 | throw err; |
271 | } |
272 | console.log("Command successfully executed"); |
273 | } |
274 |
|
275 | function regexFromPath( |
276 | path_type: |
277 | | "script" |
278 | | "flow" |
279 | | "app" |
280 | | "folder" |
281 | | "resource" |
282 | | "variable" |
283 | | "resourcetype" |
284 | | "schedule" |
285 | | "user" |
286 | | "group", |
287 | path: string |
288 | ) { |
289 | if (path_type == "flow") { |
290 | return `${path}.flow/*`; |
291 | } if (path_type == "app") { |
292 | return `${path}.app/*`; |
293 | } else if (path_type == "folder") { |
294 | return `${path}/folder.meta.*`; |
295 | } else if (path_type == "resourcetype") { |
296 | return `${path}.resource-type.*`; |
297 | } else if (path_type == "resource") { |
298 | return `${path}.resource.*`; |
299 | } else if (path_type == "variable") { |
300 | return `${path}.variable.*`; |
301 | } else if (path_type == "schedule") { |
302 | return `${path}.schedule.*`; |
303 | } else if (path_type == "user") { |
304 | return `${path}.user.*`; |
305 | } else if (path_type == "group") { |
306 | return `${path}.group.*`; |
307 | } else { |
308 | return `${path}.*`; |
309 | } |
310 | } |
311 | async function wmill_sync_pull( |
312 | path_type: |
313 | | "script" |
314 | | "flow" |
315 | | "app" |
316 | | "folder" |
317 | | "resource" |
318 | | "variable" |
319 | | "resourcetype" |
320 | | "schedule" |
321 | | "user" |
322 | | "group", |
323 | workspace_id: string, |
324 | path: string | undefined, |
325 | parent_path: string | undefined, |
326 | skip_secret: boolean |
327 | ) { |
328 | const includes = []; |
329 | if (path !== undefined && path !== null && path !== "") { |
330 | includes.push(regexFromPath(path_type, path)); |
331 | } |
332 | if (parent_path !== undefined && parent_path !== null && parent_path !== "") { |
333 | includes.push(regexFromPath(path_type, parent_path)); |
334 | } |
335 |
|
336 | await wmill_run( |
337 | 6, |
338 | "workspace", |
339 | "add", |
340 | workspace_id, |
341 | workspace_id, |
342 | Deno.env.get("BASE_INTERNAL_URL") + "/", |
343 | "--token", |
344 | Deno.env.get("WM_TOKEN") ?? "" |
345 | ); |
346 | console.log("Pulling workspace into git repo"); |
347 | await wmill_run( |
348 | 3, |
349 | "sync", |
350 | "pull", |
351 | "--token", |
352 | Deno.env.get("WM_TOKEN") ?? "", |
353 | "--workspace", |
354 | workspace_id, |
355 | "--yes", |
356 | "--raw", |
357 | skip_secret ? "--skip-secrets" : "", |
358 | "--include-schedules", |
359 | "--include-users", |
360 | "--include-groups", |
361 | "--includes", |
362 | includes.join(",") |
363 | ); |
364 | } |
365 |
|
366 | async function wmill_run(secret_position: number, ...cmd: string[]) { |
367 | cmd = cmd.filter((elt) => elt !== ""); |
368 | const cmd2 = cmd.slice(); |
369 | cmd2[secret_position] = "***"; |
370 | console.log(`Running 'wmill ${cmd2.join(" ")} ...'`); |
371 | await wmill.parse(cmd); |
372 | console.log("Command successfully executed"); |
373 | } |
374 |
|