0
Sync script to Git repo
One script reply has been approved by the moderators Verified

This script will pull the script from the current workspace in a temporary folder, then commit it to the remote Git repository and push it. Only the script-related file will be pushed, nothing else. It takes as input the git_repository resource containing the repository URL, the script path, and the commit message to use for the commit. All params are mandatory.

Created by hugo697 141 days ago Viewed 8688 times
0
Submitted by hugo697 Deno
Verified 141 days ago
1
import * as wmillclient from "npm:windmill-client@1.293.1";
2
import wmill from "https://deno.land/x/wmill@v1.293.1/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 script/flow/app ${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
  // TODO: handle private SSH keys as well
70
  const 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 args = ["clone", "--quiet", "--depth", "1"];
76
  if (use_individual_branch) {
77
    args.push("--no-single-branch"); // needed in case the asset branch already exists in the repo
78
  }
79
  if (subfolder !== "") {
80
    args.push("--sparse");
81
  }
82
  if (branch !== "") {
83
    args.push("--branch");
84
    args.push(branch);
85
  }
86
  args.push(repo_url);
87
  args.push(repo_name);
88
  await sh_run(-1, "git", ...args);
89

90
  try {
91
    Deno.chdir(`${cwd}/${repo_name}`);
92
  } catch (err) {
93
    console.log(
94
      `Error changing directory to '${cwd}/${repo_name}'. Error was:\n${err}`,
95
    );
96
    throw err;
97
  }
98
  Deno.chdir(`${cwd}/${repo_name}`);
99
  if (subfolder !== "") {
100
    await sh_run(undefined, "git", "sparse-checkout", "add", subfolder);
101
  }
102
  try {
103
    Deno.chdir(`${cwd}/${repo_name}/${subfolder}`);
104
  } catch (err) {
105
    console.log(
106
      `Error changing directory to '${cwd}/${repo_name}/${subfolder}'. Error was:\n${err}`,
107
    );
108
    throw err;
109
  }
110

111
  return repo_name;
112
}
113

114
async function move_to_git_branch(
115
  workspace_id: string,
116
  path_type:
117
    | "script"
118
    | "flow"
119
    | "app"
120
    | "folder"
121
    | "resource"
122
    | "variable"
123
    | "resourcetype"
124
    | "schedule"
125
    | "user"
126
    | "group",
127
  path: string | undefined,
128
  parent_path: string | undefined,
129
  use_individual_branch: boolean,
130
  group_by_folder: boolean,
131
) {
132
  if (!use_individual_branch || path_type === "user" || path_type === "group") {
133
    return;
134
  }
135

136
  const branchName = group_by_folder
137
    ? `wm_deploy/${workspace_id}/${
138
      (path ?? parent_path)?.split("/").slice(0, 2).join("__")
139
    }`
140
    : `wm_deploy/${workspace_id}/${path_type}/${
141
      (path ?? parent_path)?.replaceAll(
142
        "/",
143
        "__",
144
      )
145
    }`;
146

147
  try {
148
    await sh_run(undefined, "git", "checkout", branchName);
149
  } catch (err) {
150
    console.log(
151
      `Error checking out branch ${branchName}. It is possible it doesn't exist yet, tentatively creating it... Error was:\n${err}`,
152
    );
153
    try {
154
      await sh_run(undefined, "git", "checkout", "-b", branchName);
155
      await sh_run(
156
        undefined,
157
        "git",
158
        "config",
159
        "--add",
160
        "--bool",
161
        "push.autoSetupRemote",
162
        "true",
163
      );
164
    } catch (err) {
165
      console.log(
166
        `Error checking out branch '${branchName}'. Error was:\n${err}`,
167
      );
168
      throw err;
169
    }
170
  }
171
  console.log(`Successfully switched to branch ${branchName}`);
172
}
173

174
async function git_push(
175
  path: string | undefined,
176
  parent_path: string | undefined,
177
  commit_msg: string,
178
) {
179
  await sh_run(
180
    undefined,
181
    "git",
182
    "config",
183
    "user.email",
184
    Deno.env.get("WM_EMAIL") ?? "",
185
  );
186
  await sh_run(
187
    undefined,
188
    "git",
189
    "config",
190
    "user.name",
191
    Deno.env.get("WM_USERNAME") ?? "",
192
  );
193
  if (path !== undefined && path !== null && path !== "") {
194
    try {
195
      await sh_run(undefined, "git", "add", `${path}**`);
196
    } catch (e) {
197
      console.log(`Unable to stage files matching ${path}**, ${e}`);
198
    }
199
  }
200
  if (parent_path !== undefined && parent_path !== null && parent_path !== "") {
201
    try {
202
      await sh_run(undefined, "git", "add", `${parent_path}**`);
203
    } catch (e) {
204
      console.log(`Unable to stage files matching ${parent_path}, ${e}`);
205
    }
206
  }
207
  try {
208
    await sh_run(undefined, "git", "diff", "--cached", "--quiet");
209
  } catch {
210
    // git diff returns exit-code = 1 when there's at least on staged changes
211
    await sh_run(undefined, "git", "commit", "-m", commit_msg);
212
    await sh_run(undefined, "git", "push", "--porcelain");
213
    return;
214
  }
215
  console.log("No changes detected, nothing to commit. Returning...");
216
}
217

218
async function sh_run(
219
  secret_position: number | undefined,
220
  cmd: string,
221
  ...args: string[]
222
) {
223
  const nargs = secret_position != undefined ? args.slice() : args;
224
  if (secret_position < 0) {
225
    secret_position = nargs.length - 1 + secret_position;
226
  }
227
  if (secret_position != undefined) {
228
    nargs[secret_position] = "***";
229
  }
230
  console.log(`Running '${cmd} ${nargs.join(" ")} ...'`);
231
  const command = new Deno.Command(cmd, {
232
    args: args,
233
  });
234

235
  const { code, stdout, stderr } = await command.output();
236
  if (stdout.length > 0) {
237
    console.log(new TextDecoder().decode(stdout));
238
  }
239
  if (stderr.length > 0) {
240
    console.log(new TextDecoder().decode(stderr));
241
  }
242
  if (code !== 0) {
243
    const err = `SH command '${cmd} ${
244
      args.join(
245
        " ",
246
      )
247
    }' returned with a non-zero status ${code}.`;
248
    throw err;
249
  }
250
  console.log("Command successfully executed");
251
}
252

253
function regexFromPath(
254
  path_type:
255
    | "script"
256
    | "flow"
257
    | "app"
258
    | "folder"
259
    | "resource"
260
    | "variable"
261
    | "resourcetype"
262
    | "schedule"
263
    | "user"
264
    | "group",
265
  path: string,
266
) {
267
  if (path_type == "flow") {
268
    return `${path}.flow/*`;
269
  } else if (path_type == "folder") {
270
    return `${path}/folder.meta.*`;
271
  } else if (path_type == "resourcetype") {
272
    return `${path}.resource-type.*`;
273
  } else if (path_type == "resource") {
274
    return `${path}.resource.*`;
275
  } else if (path_type == "variable") {
276
    return `${path}.variable.*`;
277
  } else if (path_type == "schedule") {
278
    return `${path}.schedule.*`;
279
  } else if (path_type == "user") {
280
    return `${path}.user.*`;
281
  } else if (path_type == "group") {
282
    return `${path}.group.*`;
283
  } else {
284
    return `${path}.*`;
285
  }
286
}
287
async function wmill_sync_pull(
288
  path_type:
289
    | "script"
290
    | "flow"
291
    | "app"
292
    | "folder"
293
    | "resource"
294
    | "variable"
295
    | "resourcetype"
296
    | "schedule"
297
    | "user"
298
    | "group",
299
  workspace_id: string,
300
  path: string | undefined,
301
  parent_path: string | undefined,
302
  skip_secret: boolean,
303
) {
304
  const includes = [];
305
  if (path !== undefined && path !== null && path !== "") {
306
    includes.push(regexFromPath(path_type, path));
307
  }
308
  if (parent_path !== undefined && parent_path !== null && parent_path !== "") {
309
    includes.push(regexFromPath(path_type, parent_path));
310
  }
311

312
  await wmill_run(
313
    6,
314
    "workspace",
315
    "add",
316
    workspace_id,
317
    workspace_id,
318
    Deno.env.get("BASE_INTERNAL_URL") + "/",
319
    "--token",
320
    Deno.env.get("WM_TOKEN") ?? "",
321
  );
322
  console.log("Pulling workspace into git repo");
323
  await wmill_run(
324
    3,
325
    "sync",
326
    "pull",
327
    "--token",
328
    Deno.env.get("WM_TOKEN") ?? "",
329
    "--workspace",
330
    workspace_id,
331
    "--yes",
332
    "--raw",
333
    skip_secret ? "--skip-secrets" : "",
334
    "--include-schedules",
335
    "--include-users",
336
    "--include-groups",
337
    "--includes",
338
    includes.join(","),
339
  );
340
}
341

342
async function wmill_run(secret_position: number, ...cmd: string[]) {
343
  cmd = cmd.filter((elt) => elt !== "");
344
  const cmd2 = cmd.slice();
345
  cmd2[secret_position] = "***";
346
  console.log(`Running 'wmill ${cmd2.join(" ")} ...'`);
347
  await wmill.parse(cmd);
348
  console.log("Command successfully executed");
349
}
350