@decaf-ts/utils
Version:
module management utils for decaf-ts
250 lines • 10.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MirrorRepoCommand = void 0;
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const command_js_1 = require("./../command.cjs");
const help_command_js_1 = require("./help.command.cjs");
function makeAuthUrl(baseUrl, token) {
const [scheme, rest] = baseUrl.split("://", 2);
return `${scheme}://x-access-token:${encodeURIComponent(token)}@${rest}`;
}
function parseGitUrl(raw) {
let host, cleanPath;
if (raw.startsWith("git@")) {
const rest = raw.slice(4);
const c = rest.indexOf(":");
if (c === -1)
throw new Error(`invalid SSH URL: ${raw}`);
host = rest.slice(0, c);
cleanPath = rest.slice(c + 1);
}
else {
const u = new URL(raw);
host = u.hostname;
cleanPath = u.pathname.slice(1);
}
cleanPath = cleanPath.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
const parts = cleanPath.split("/").filter(Boolean);
if (parts.length < 2)
throw new Error(`URL missing owner/repo: ${raw}`);
const owner = parts[0];
const repoName = parts[parts.length - 1];
const httpsUrl = `https://${host}/${owner}/${repoName}.git`;
const mirrorSlug = `mirror-${host}-${owner}-${repoName}`
.replace(/[^a-zA-Z0-9]+/g, "-")
.replace(/-{2,}/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
return { host, owner, repoName, httpsUrl, mirrorSlug };
}
function spawnExec(prog, args, env, cwd) {
const result = (0, node_child_process_1.spawnSync)(prog, args, {
stdio: "inherit",
env: env ?? process.env,
cwd,
});
if (result.error)
throw result.error;
if (result.status !== 0)
throw new Error(`${prog} exited with code ${result.status}`);
}
function spawnCapture(prog, args, env) {
const result = (0, node_child_process_1.spawnSync)(prog, args, {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
env: env ?? process.env,
});
if (result.error)
throw result.error;
if (result.status !== 0)
throw new Error(`${prog} exited with code ${result.status}: ${result.stderr}`);
return result.stdout.trim();
}
function spawnTry(prog, args, env) {
const result = (0, node_child_process_1.spawnSync)(prog, args, {
stdio: "pipe",
env: env ?? process.env,
});
return result.status === 0 && !result.error;
}
function spawnTryCapture(prog, args, env) {
try {
return spawnCapture(prog, args, env);
}
catch {
return "";
}
}
function mirrorRepoRecursive(srcUrl, tgtOwner, tgtHost, srcToken, tgtToken, globalWorkDir, recursive, log, depth = 0) {
if (depth > 5) {
log.warn(`Max recursion depth reached; skipping ${srcUrl}`);
return;
}
let parsed;
try {
parsed = parseGitUrl(srcUrl);
}
catch (err) {
log.warn(`Cannot parse ${srcUrl}: ${err.message}`);
return;
}
const { httpsUrl, owner: srcOwner, repoName, mirrorSlug } = parsed;
const display = `${srcOwner}/${repoName}`;
const tgtFull = `${tgtOwner}/${mirrorSlug}`;
const repoParent = (0, node_path_1.join)(globalWorkDir, `d${depth}-${mirrorSlug}`);
const repoDir = (0, node_path_1.join)(repoParent, "mirror.git");
(0, node_fs_1.mkdirSync)(repoParent, { recursive: true });
const srcAuth = makeAuthUrl(httpsUrl, srcToken);
const tgtAuth = makeAuthUrl(`https://${tgtHost}/${tgtOwner}/${mirrorSlug}.git`, tgtToken);
const ghEnv = { ...process.env, GH_TOKEN: tgtToken, GH_HOST: tgtHost };
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
log.warn(`[depth=${depth}] Mirroring ${display} -> ${tgtFull}`);
if (spawnTry("gh", ["repo", "view", tgtFull], ghEnv)) {
log.warn(`Repository ${tgtFull} exists; cloning existing mirror.`);
spawnExec("git", ["clone", "--mirror", tgtAuth, repoDir], gitEnv);
spawnTry("git", ["-C", repoDir, "remote", "rename", "origin", "target"]);
}
else {
log.warn(`Creating private repository ${tgtFull}`);
spawnExec("gh", [
"repo",
"create",
tgtFull,
"--private",
"--disable-issues",
"--disable-wiki",
"--description",
`Automated mirror of ${display}`,
], ghEnv);
log.warn(`Cloning ${display}`);
spawnExec("git", ["clone", "--mirror", srcAuth, repoDir], gitEnv);
spawnTry("git", ["-C", repoDir, "remote", "rename", "origin", "source"]);
}
if (spawnTry("git", ["-C", repoDir, "remote", "get-url", "target"])) {
spawnExec("git", ["-C", repoDir, "remote", "set-url", "target", tgtAuth]);
}
else {
spawnExec("git", ["-C", repoDir, "remote", "add", "target", tgtAuth]);
}
if (spawnTry("git", ["-C", repoDir, "remote", "get-url", "source"])) {
spawnExec("git", ["-C", repoDir, "remote", "set-url", "source", srcAuth]);
}
else {
spawnExec("git", ["-C", repoDir, "remote", "add", "source", srcAuth]);
}
log.warn(`Fetching latest refs from ${display}`);
spawnExec("git", ["-C", repoDir, "fetch", "--prune", "source", "+refs/*:refs/*"], gitEnv);
// git clone --mirror sets remote.<name>.mirror=true, which forbids explicit refspecs.
// Unset it so we can pass a negative refspec to exclude refs/pull/*.
spawnTry("git", ["-C", repoDir, "config", "--unset", "remote.target.mirror"]);
log.warn(`Pushing mirror to ${tgtFull}`);
// --prune removes stale refs on the target (equivalent to --mirror's deletion behaviour).
// ^refs/pull/* excludes GitHub-internal hidden refs that reject pushes (git 2.29+).
spawnExec("git", ["-C", repoDir, "push", "--prune", "target", "+refs/*:refs/*", "^refs/pull/*"], gitEnv);
log.warn(`Completed mirror for ${display}`);
if (recursive && depth < 5) {
const gitmodules = spawnTryCapture("git", [
"-C",
repoDir,
"show",
"HEAD:.gitmodules",
]);
if (gitmodules) {
log.warn(`Found .gitmodules in ${display}; recursing into submodules (depth=${depth + 1})...`);
for (const line of gitmodules.split("\n")) {
const match = /^\s*url\s*=\s*(.+)$/.exec(line);
if (match) {
const subUrl = match[1].trim();
log.warn(`Submodule: ${subUrl}`);
try {
mirrorRepoRecursive(subUrl, tgtOwner, tgtHost, srcToken, tgtToken, globalWorkDir, true, log, depth + 1);
}
catch (err) {
log.warn(`Failed to mirror submodule ${subUrl}: ${err.message}`);
}
}
}
}
}
}
const options = {};
class MirrorRepoCommand extends command_js_1.Command {
constructor() {
super("MirrorRepoCommand", options);
}
help() {
(0, help_command_js_1.printCommandHelp)(this.log, "mirror-repo", "Mirror a single Git repository to a target host using bare clone/push.", "mirror-repo", [
{ flag: "-h, --help", description: "Show this help text and exit" },
{
flag: "-v, --version",
description: "Print package version and exit",
},
], [
"All configuration is read from environment variables set by the GitHub Actions matrix job.",
"SOURCE_HTTPS_URL: normalized HTTPS URL of the source repository.",
"TARGET_OWNER: owner/org on the target host.",
"TARGET_HOST: target host (default: github.com).",
"SOURCE_HOST_TOKEN / SOURCE_OWNER_TOKEN: PAT for reading the source.",
"TARGET_OWNER_TOKEN / TARGET_OWNER_FALLBACK_TOKEN / TARGET_HOST_TOKEN: PAT for writing to the target.",
"RECURSIVE: set to 'true' to also mirror all git submodules recursively.",
], ["mirror-repo"]);
}
async run(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_answers) {
const log = this.log.for(this.run);
const sourceHttpsUrl = process.env["SOURCE_HTTPS_URL"] ?? "";
const sourceDisplayName = process.env["SOURCE_DISPLAY_NAME"] ??
process.env["SOURCE_URL"] ??
"unknown source";
const sourceHostSecretName = process.env["SOURCE_HOST_SECRET_NAME"] ?? "";
const sourceOwnerSecretName = process.env["SOURCE_OWNER_SECRET_NAME"] ?? "";
const targetOwner = process.env["TARGET_OWNER"] ?? "";
const targetHost = process.env["TARGET_HOST"] ?? "github.com";
const targetOwnerSecretName = process.env["TARGET_OWNER_SECRET_NAME"] ?? "";
const targetOwnerFallbackSecretName = process.env["TARGET_OWNER_FALLBACK_SECRET_NAME"] ?? "";
const targetHostSecretName = process.env["TARGET_HOST_SECRET_NAME"] ?? "";
const recursive = process.env["RECURSIVE"] === "true";
if (!sourceHttpsUrl) {
log.warn(`Skipping ${sourceDisplayName}: missing normalized HTTPS URL.`);
return;
}
const sourceToken = process.env["SOURCE_HOST_TOKEN"] ||
process.env["SOURCE_OWNER_TOKEN"] ||
"";
if (!sourceToken) {
log.warn(`Skipping ${sourceDisplayName}: missing source access token` +
` (expected secrets ${sourceHostSecretName} or ${sourceOwnerSecretName}).`);
return;
}
const targetToken = process.env["TARGET_OWNER_TOKEN"] ||
process.env["TARGET_OWNER_FALLBACK_TOKEN"] ||
process.env["TARGET_HOST_TOKEN"] ||
"";
if (!targetToken) {
const missing = [
targetOwnerSecretName,
targetOwnerFallbackSecretName,
targetHostSecretName,
].filter(Boolean);
throw new Error(`Missing target access token (expected secrets ${missing.join(", ")})` +
` for ${sourceDisplayName}.`);
}
const globalWorkDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "git-mirror-"));
process.on("exit", () => {
try {
(0, node_fs_1.rmSync)(globalWorkDir, { recursive: true, force: true });
}
catch {
// best-effort cleanup
}
});
mirrorRepoRecursive(sourceHttpsUrl, targetOwner, targetHost, sourceToken, targetToken, globalWorkDir, recursive, log, 0);
}
}
exports.MirrorRepoCommand = MirrorRepoCommand;
//# sourceMappingURL=mirror-repo.command.js.map
//# sourceMappingURL=mirror-repo.command.cjs.map