Edits history of script submission #23861 for ' Git-sync: review pull request (windmill)'

  • bun
    One script reply has been approved by the moderators
    Ap­pro­ved
    import * as wmillclient from "windmill-client";
    import wmill from "[email protected]";
    import { execFile } from "node:child_process";
    import { mkdtempSync } from "node:fs";
    import { tmpdir } from "node:os";
    import { join } from "node:path";
    import { promisify } from "node:util";
    import process from "process";
    
    const run = promisify(execFile);
    
    type GitRepository = {
      url: string;
      branch: string;
      folder?: string;
      is_github_app: boolean;
    };
    
    /**
     * Reviews a pull request on a git-synced repository with the workspace's AI, for the git sync
     * setting "Review pull requests with AI". Enqueued by the server on a pull request event; the
     * server posts what this returns. Runs `wmill review`, which reads the review instructions
     * from the base branch only and never reads the checkout's wmill.yaml.
     */
    export async function main(
      workspace_id: string,
      repo_url_resource_path: string,
      base_ref: string, // the tracked branch the pull request targets
      pr_ref: string, // the host's head ref: pull/<n>/head on GitHub, merge-requests/<n>/head on GitLab
      head_sha: string // the head the event was for; a newer push gets its own review
    ) {
      if (!/^(pull|merge-requests)\/\d+\/head$/.test(pr_ref)) {
        throw new Error(`Not a pull request head ref: ${pr_ref}`);
      }
      if (!/^[0-9a-f]{40}$/.test(head_sha)) {
        throw new Error(`Not a commit sha: ${head_sha}`);
      }
    
      const repo: GitRepository = await wmillclient.getResource(repo_url_resource_path);
      process.env.GIT_TERMINAL_PROMPT = "0";
      process.env["HOME"] = ".";
    
      let url = repo.url;
      if (repo.is_github_app) {
        url = prependTokenToGitHubUrl(url, await get_gh_app_token());
      } else if (!urlCarriesCredential(url)) {
        const token = await get_stored_git_token();
        if (token) url = prependTokenToGitUrl(url, token);
      }
    
      // A bare clone of the base branch with its history, so the review finds the merge base;
      // the pull request's head is fetched from the host's own ref, present even for a
      // pull request from another repository.
      // The job's result is written to its working directory, so it is restored before returning.
      const cwd = process.cwd();
      const dir = join(cwd, "repo.git");
      await git(url, "clone", "--quiet", "--bare", "--single-branch", "--branch", base_ref, url, dir);
      process.chdir(dir);
      try {
        return await review(url, pr_ref, head_sha, workspace_id, repo.folder);
      } finally {
        process.chdir(cwd);
      }
    }
    
    async function review(
      url: string,
      pr_ref: string,
      head_sha: string,
      workspace_id: string,
      folder: string | undefined
    ) {
      await git(url, "fetch", "--quiet", "--no-tags", url, pr_ref);
      const fetched = (await git(url, "rev-parse", "FETCH_HEAD")).trim();
      if (fetched !== head_sha) {
        return { skipped: `The pull request moved from ${head_sha} to ${fetched}` };
      }
      const base = (await git(url, "rev-parse", "HEAD")).trim();
    
      return await wmill_json([
        "review",
        "--base",
        base,
        "--head",
        head_sha,
        ...(folder ? ["--folder", folder] : []),
        "--json",
        "--base-url",
        (process.env["BASE_INTERNAL_URL"] ?? process.env["BASE_URL"]) + "/",
        "--workspace",
        workspace_id,
        "--token",
        process.env["WM_TOKEN"] ?? "",
        "--config-dir",
        mkdtempSync(join(tmpdir(), "wmill-config-")),
      ]);
    }
    
    /** Runs git without a shell, keeping the credential-bearing remote out of the log. */
    async function git(url: string, ...args: string[]): Promise<string> {
      try {
        const { stdout } = await run("git", args, { maxBuffer: 512 * 1024 * 1024 });
        return stdout;
      } catch (error: any) {
        throw new Error(`git ${args[0]} failed: ${String(error?.stderr ?? error).replaceAll(url, "***")}`);
      }
    }
    
    /** The JSON `wmill <cmd> --json` prints. */
    async function wmill_json(cmd: string[]) {
      const originalLog = console.log;
      let output = "";
      console.log = (msg: string) => {
        output += msg + "\n";
      };
      try {
        await wmill.parse(cmd);
      } finally {
        console.log = originalLog;
      }
      const start = output.indexOf("{");
      if (start === -1) throw new Error(`wmill review printed no result: ${output}`);
      return JSON.parse(output.slice(start));
    }
    
    function urlCarriesCredential(url: string | undefined): boolean {
      return /:\/\/[^/@]+@/.test(url ?? "");
    }
    
    // The credential Windmill stores for this repository, or undefined when it holds
    // none. Absence is normal (a public remote), so it must not fail the review.
    async function get_stored_git_token() {
      try {
        return await get_gh_app_token();
      } catch (error) {
        console.log("no stored git credential for this repository:", error?.message ?? String(error));
        return undefined;
      }
    }
    
    async function get_gh_app_token() {
      const workspace = process.env["WM_WORKSPACE"];
      const jobToken = process.env["WM_TOKEN"];
      const baseUrl = process.env["BASE_INTERNAL_URL"] ?? process.env["BASE_URL"] ?? "http://localhost:8000";
      const response = await fetch(`${baseUrl}/api/w/${workspace}/github_app/token`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${jobToken}` },
        body: JSON.stringify({ job_token: jobToken }),
      });
      if (!response.ok) {
        const errorBody = await response.text().catch(() => "");
        throw new Error(`GitHub App token error (${response.status}): ${errorBody || response.statusText}`);
      }
      return (await response.json()).token;
    }
    
    // Rewrite the git remote formats `new URL` can't parse into an https URL:
    // scp-like ssh ([user@]host:owner/repo) and scheme-less (host/owner/repo).
    // A host:8080/... port form is kept as authority rather than read as scp.
    function normalizeGitHubUrl(raw: string): string {
      const cleaned = raw.trim();
      if (cleaned.includes("://")) {
        return cleaned;
      }
      const colonIdx = cleaned.indexOf(":");
      if (colonIdx !== -1) {
        const before = cleaned.slice(0, colonIdx);
        const after = cleaned.slice(colonIdx + 1);
        const firstSeg = after.split("/")[0];
        const isPort = !before.includes("@") && firstSeg !== "" && /^\d+$/.test(firstSeg);
        const isUserinfo = firstSeg.includes("@");
        if (before !== "" && !before.includes("/") && after !== "" && !isPort && !isUserinfo) {
          const host = before.split("@").pop();
          return `https://${host}/${after.replace(/^\/+/, "")}`;
        }
      }
      return `https://${cleaned}`;
    }
    
    function prependTokenToGitHubUrl(gitHubUrl: string, installationToken: string) {
      const url = new URL(normalizeGitHubUrl(gitHubUrl));
      return `https://x-access-token:${installationToken}@${url.host}${url.pathname}`;
    }
    
    // `oauth2` is the username GitLab documents for token auth; the scheme the resource
    // chose is kept, so a self-managed server reached over http is not dialled as https.
    function prependTokenToGitUrl(gitUrl: string, token: string) {
      const url = new URL(normalizeGitHubUrl(gitUrl));
      url.username = "oauth2";
      url.password = token;
      return url.toString();
    }
    

    Submitted by hugo989 4 hours ago