Something went wrong. Try again.
Mirror your GitHub repos to GitLab & Tangled — staged Node.js GitHub Actions with parallel jobs, retries, and description sync.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108// Shared helpers. Node built-ins only — the workflow never installs anything.import { spawnSync } from "node:child_process";
const secrets = [];
export function registerSecrets(...values) { for (const value of values) if (value) secrets.push(value);}
export function redact(text) { return secrets.reduce((acc, value) => acc.split(value).join("***"), String(text ?? ""));}
// Node's fetch reports every network error as "fetch failed"; the cause names the real one.export function errorText(err) { const cause = err.cause?.code || err.cause?.message; return redact(cause ? `${err.message} (${cause})` : err.message);}
// GitLab.com caps bursts per user; withRetry waits these out instead of counting them as failures.const RATE_LIMITED = /\b429\b|rate limit exceeded/i;
export function rateLimitError(message, seconds = 60) { const err = new Error(message); err.retryAfterMs = seconds * 1000; return err;}
export function git(args, { allowFail = false } = {}) { const res = spawnSync("git", args, { encoding: "utf8", maxBuffer: 1 << 26 }); if (res.error) throw res.error; const out = redact(`${res.stdout || ""}${res.stderr || ""}`.trim()); if (res.status !== 0 && !allowFail) { throw RATE_LIMITED.test(out) ? rateLimitError(out) : new Error(out || `git ${args[0]} exited ${res.status}`); } return { ok: res.status === 0, out };}
// GitHub advertises refs/pull/*, GitLab adds refs/merge_requests/* — neither survives a mirror.const MIRRORED_REF = /^refs\/(heads|tags|notes)\//;
export function parseRefs(lsRemoteOutput) { const refs = new Map(); for (const line of lsRemoteOutput.split("\n")) { const [sha, ref] = line.split("\t"); if (!sha || !ref || !MIRRORED_REF.test(ref)) continue; refs.set(ref, sha); } return refs;}
// null means unreadable, never empty, so a failed lookup falls back to a full mirror.export function remoteRefs(url) { const { ok, out } = git(["ls-remote", url], { allowFail: true }); if (!ok && RATE_LIMITED.test(out)) throw rateLimitError(out); return ok ? parseRefs(out) : null;}
export function sameRefs(a, b) { if (!a || !b || a.size !== b.size) return false; for (const [ref, sha] of a) if (b.get(ref) !== sha) return false; return true;}
export function glSlug(name) { return name .replace(/[^A-Za-z0-9_.-]/g, "-") .replace(/^[-_.]+/, "") .replace(/(?:\.git|\.atom)$/i, "") .replace(/[-_.]+$/, "") || "repo";}
// Stride, not contiguous slices: alphabetical neighbours tend to be similar in size.export function selectBatch(items, index, count) { return items.filter((_, i) => i % count === index);}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function withRetry(fn, { attempts = 3, baseDelayMs = 1000, rateLimitWaits = 8, onRetry } = {}) { let failures = 0; for (let attempt = 1; ; attempt++) { try { return await fn(); } catch (err) { const limited = err.retryAfterMs !== undefined && rateLimitWaits-- > 0; if (!limited && ++failures >= attempts) throw err; onRetry?.(attempt, err); // Jitter so the parallel jobs don't all hit the next window together. await sleep(limited ? err.retryAfterMs + Math.random() * 30_000 : baseDelayMs * 2 ** (failures - 1)); } }}
export async function pool(items, limit, worker) { const iter = items[Symbol.iterator](); const size = Math.max(1, Math.min(limit, items.length)); await Promise.all( Array.from({ length: size }, async () => { for (;;) { const { value, done } = iter.next(); if (done) return; await worker(value); } }), );}