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 88 days ago Viewed 2266 times
0
Submitted by hugo697 Deno
Verified 88 days ago
1
import * as wmillclient from "npm:windmill-client@1.258.3";
2
import wmill from "https://deno.land/x/wmill@v1.258.3/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: "script" | "flow" | "app" | "folder" | "resource" | "variable" | "resourcetype" | "schedule",
9
  skip_secret = true,
10
  path: string | undefined,
11
  parent_path: string | undefined,
12
  commit_msg: string,
13
  use_individual_branch = false,
14
) {
15
  const repo_resource = await wmillclient.getResource(repo_url_resource_path);
16

17
  const cwd = Deno.cwd();
18
  Deno.env.set("HOME", ".");
19
  console.log(
20
    `Syncing script/flow/app ${path ?? ""} with parent ${parent_path ?? ""}`,
21
  );
22

23
  const repo_name = await git_clone(cwd, repo_resource, use_individual_branch);
24
  await move_to_git_branch(
25
    workspace_id,
26
    path_type,
27
    path,
28
    parent_path,
29
    use_individual_branch,
30
  );
31

32
  const subfolder = repo_resource.folder ?? "";
33
  const branch_or_default = repo_resource.branch ?? "<DEFAULT>";
34
  console.log(
35
    `Pushing to repository ${repo_name} in subfolder ${subfolder} on branch ${branch_or_default}`,
36
  );
37

38
  await wmill_sync_pull(path_type, workspace_id, path, parent_path, skip_secret);
39

40
  await git_push(path, parent_path, commit_msg);
41

42
  console.log("Finished syncing");
43
  Deno.chdir(`${cwd}`);
44
}
45

46
async function git_clone(
47
  cwd: string,
48
  repo_resource: any,
49
  use_individual_branch: boolean,
50
): Promise<string> {
51
  // TODO: handle private SSH keys as well
52
  const repo_url = repo_resource.url;
53
  const subfolder = repo_resource.folder ?? "";
54
  const branch = repo_resource.branch ?? "";
55
  const repo_name = basename(repo_url, ".git");
56

57
  const args = [
58
    "clone",
59
    "--quiet",
60
    "--depth",
61
    "1",
62
  ];
63
  if (use_individual_branch) {
64
    args.push("--no-single-branch"); // needed in case the asset branch already exists in the repo
65
  }
66
  if (subfolder !== "") {
67
    args.push("--sparse");
68
  }
69
  if (branch !== "") {
70
    args.push("--branch");
71
    args.push(branch);
72
  }
73
  args.push(repo_url);
74
  args.push(repo_name);
75
  await sh_run(
76
    -2,
77
    "git",
78
    ...args,
79
  );
80

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

108
  return repo_name;
109
}
110

111
async function move_to_git_branch(
112
  workspace_id: string,
113
  path_type: "script" | "flow" | "app" | "folder" | "resource" | "variable" | "resourcetype" | "schedule",
114
  path: string | undefined,
115
  parent_path: string | undefined,
116
  use_individual_branch: boolean,
117
) {
118
  if (!use_individual_branch) {
119
    return;
120
  }
121
  const branchName = path !== undefined
122
    ? `wm_deploy/${workspace_id}/${path_type}/${path.replaceAll("/", "__")}`
123
    : `wm_deploy/${workspace_id}/${path_type}/${
124
      parent_path.replaceAll("/", "__")
125
    }`;
126

127
  try {
128
    await sh_run(
129
      undefined,
130
      "git",
131
      "checkout",
132
      branchName,
133
    );
134
  } catch (err) {
135
    console.log(
136
      `Error checking out branch ${branchName}. It is possible it doesn't exist yet, tentatively creating it... Error was:\n${err}`,
137
    );
138
    try {
139
      await sh_run(
140
        undefined,
141
        "git",
142
        "checkout",
143
        "-b",
144
        branchName,
145
      );
146
      await sh_run(
147
        undefined,
148
        "git",
149
        "config",
150
        "--add",
151
        "--bool",
152
        "push.autoSetupRemote",
153
        "true",
154
      );
155
    } catch (err) {
156
      console.log(
157
        `Error checking out branch '${branchName}'. Error was:\n${err}`,
158
      );
159
      throw err;
160
    }
161
  }
162
  console.log(`Successfully switched to branch ${branchName}`);
163
}
164

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

236
async function sh_run(
237
  secret_position: number | undefined,
238
  cmd: string,
239
  ...args: string[]
240
) {
241
  const nargs = secret_position != undefined ? args.slice() : args;
242
  if (secret_position != undefined) {
243
    nargs[secret_position] = "***";
244
  }
245
  console.log(`Running '${cmd} ${nargs.join(" ")} ...'`);
246
  const command = new Deno.Command(cmd, {
247
    args: args,
248
  });
249

250
  const { code, stdout, stderr } = await command.output();
251
  if (stdout.length > 0) {
252
    console.log(new TextDecoder().decode(stdout));
253
  }
254
  if (stderr.length > 0) {
255
    console.log(new TextDecoder().decode(stderr));
256
  }
257
  if (code !== 0) {
258
    const err = `SH command '${cmd} ${
259
      args.join(" ")
260
    }' returned with a non-zero status ${code}.`;
261
    throw err;
262
  }
263
  console.log("Command successfully executed");
264
}
265

266
function regexFromPath(
267
  path_type: "script" | "flow" | "app" | "folder" | "resource" | "variable" | "resourcetype" | "schedule",
268
  path: string,
269
) {
270
  if (path_type == "flow") {
271
    return `${path}.flow/*`;
272
  } else if (path_type == "folder") {
273
    return `${path}/folder.meta.*`;
274
  } else if (path_type == "resourcetype") {
275
    return `${path}.resource-type.*`;
276
  } else if (path_type == "resource") {
277
    return `${path}.resource.*`;
278
  } else if (path_type == "variable") {
279
    return `${path}.variable.*`;
280
  } else if (path_type == "schedule") {
281
    return `${path}.schedule.*`;
282
  } else {
283
    return `${path}.*`;
284
  }
285
}
286
async function wmill_sync_pull(
287
  path_type: "script" | "flow" | "app" | "folder" | "resource" | "variable" | "resourcetype" | "schedule",
288
  workspace_id: string,
289
  path: string | undefined,
290
  parent_path: string | undefined,
291
  skip_secret: boolean,
292
) {
293
  const includes = [];
294
  if (path !== undefined && path !== null && path !== "") {
295
    includes.push(regexFromPath(path_type, path));
296
  }
297
  if (parent_path !== undefined && parent_path !== null && parent_path !== "") {
298
    includes.push(regexFromPath(path_type, parent_path));
299
  }
300

301
  await wmill_run(
302
    6,
303
    "workspace",
304
    "add",
305
    workspace_id,
306
    workspace_id,
307
    Deno.env.get("BASE_INTERNAL_URL") + "/",
308
    "--token",
309
    Deno.env.get("WM_TOKEN") ?? "",
310
  );
311
  console.log("Pulling workspace into git repo");
312
  await wmill_run(
313
    3,
314
    "sync",
315
    "pull",
316
    "--token",
317
    Deno.env.get("WM_TOKEN") ?? "",
318
    "--workspace",
319
    workspace_id,
320
    "--yes",
321
    "--raw",
322
    skip_secret ? "--skip-secrets" : "",
323
    "--include-schedules",
324
    "--includes",
325
    includes.join(","),
326
  );
327
}
328

329
async function wmill_run(secret_position: number, ...cmd: string[]) {
330
  cmd = cmd.filter((elt) => elt !== "")
331
  const cmd2 = cmd.slice();
332
  cmd2[secret_position] = "***";
333
  console.log(`Running 'wmill ${cmd2.join(" ")} ...'`);
334
  await wmill.parse(cmd);
335
  console.log("Command successfully executed");
336
}
337