0
Git repo test read write
One script reply has been approved by the moderators Verified

This script will test the connection to the Git repo passed stored in the resources passed as an argument. It first clones the repo (checking read) and then attemps an empty push (checking write)

Created by hugo697 639 days ago Viewed 54725 times
0
Submitted by rubenfiszel Bun
Verified 363 days ago
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
    // Add safe.directory to handle dubious ownership in cloned repo
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
    // Cleanup: remove safe.directory config
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
  // TODO: handle private SSH keys as well
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
    // Re-throw other errors
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
    // GitHub repository URL should be in the format: https://github.com/owner/repo.git
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
    // Convert URL to include the installation token
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

Other submissions