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.
6.2 kB · 200 lines
JavaScript
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201import { readdirSync, readFileSync } from "node:fs";import { join } from "node:path";
const { DISCORD_WEBHOOK_URL, WORKFLOW_STATUS, REPO_COUNT, BACKUP_RESULT, RESULTS_DIR, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_NUMBER, GITHUB_SERVER_URL,} = process.env;
if (!DISCORD_WEBHOOK_URL) { console.error("Missing DISCORD_WEBHOOK_URL"); process.exit(1);}
const FIELD_VALUE_LIMIT = 1024;const EMBED_TOTAL_LIMIT = 6000;const MAX_FIELDS = 25;
const runUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;const allSucceeded = WORKFLOW_STATUS === "success";const backupStatus = BACKUP_RESULT || "unknown";const repoCount = parseInt(REPO_COUNT, 10) || 0;
const discoverOk = WORKFLOW_STATUS === "success" || backupStatus !== "unknown";const backupOk = backupStatus === "success";
function readMirrorResults() { const counts = { updated: 0, unchanged: 0, failed: 0 }; const names = { updated: [], failed: [] };
if (!RESULTS_DIR) return { counts, names };
try { const files = readdirSync(RESULTS_DIR); for (const file of files) { if (!file.endsWith(".txt")) continue; const repoName = file.replace(/\.txt$/, ""); const status = readFileSync(join(RESULTS_DIR, file), "utf8").trim(); if (status === "updated") { counts.updated++; names.updated.push(repoName); } else if (status === "unchanged") { counts.unchanged++; } else { counts.failed++; names.failed.push(repoName); } } } catch {}
names.updated.sort(); names.failed.sort();
return { counts, names };}
const ghOwner = GITHUB_REPOSITORY?.split("/")[0] || "";const repoLink = (r) => `[${r}](${GITHUB_SERVER_URL}/${ghOwner}/${r})`;const moreMarker = (n) => ` \u2026 and **${n}** more`;
function embedLength(embed) { let total = (embed.title?.length || 0) + (embed.description?.length || 0); total += embed.footer?.text?.length || 0; for (const field of embed.fields) total += field.name.length + field.value.length; return total;}
function repoListFields(label, repos, { charBudget, fieldBudget }) { if (repos.length === 0 || fieldBudget < 1 || charBudget < 1) return [];
const name = `${label} (${repos.length})`; const linked = repos.map((r) => `- ${repoLink(r)}`).join("\n"); if (linked.length <= FIELD_VALUE_LIMIT && name.length + linked.length <= charBudget) { return [{ name, value: linked, inline: false }]; }
const chunks = []; let current = ""; let used = name.length; let shown = 0;
for (const repo of repos) { if (current && current.length + 2 + repo.length > FIELD_VALUE_LIMIT) { chunks.push(current); current = ""; if (chunks.length >= fieldBudget) break; used += 1; } const sep = current ? ", " : ""; if (used + sep.length + repo.length > charBudget) break; current += sep + repo; used += sep.length + repo.length; shown++; } if (current) chunks.push(current);
if (shown < repos.length) { let hidden = repos.length - shown; let last = chunks.pop() ?? ""; let marker = moreMarker(hidden); while (last && last.length + marker.length > FIELD_VALUE_LIMIT) { const cut = last.lastIndexOf(", "); if (cut < 0) { last = ""; break; } last = last.slice(0, cut); hidden++; marker = moreMarker(hidden); } chunks.push(last + marker); }
return chunks.map((value, i) => ({ name: i === 0 ? name : "\u200b", value, inline: false, }));}
(async () => { const { counts, names } = readMirrorResults(); const hasResults = counts.updated + counts.unchanged + counts.failed > 0; const ok = allSucceeded && counts.failed === 0;
const description = ok ? `Mirrored **${repoCount}** repositories from GitHub to GitLab.\nAll jobs completed successfully.\n\u200b` : `Mirror run completed with failures — **${repoCount}** repositories were processed.\n[View workflow logs](${runUrl}) for details.\n\u200b`;
const fields = [];
fields.push( { name: "Run", value: `[#${GITHUB_RUN_NUMBER}](${runUrl})`, inline: true }, { name: "Discover", value: discoverOk ? "Passed" : "Failed", inline: true }, { name: "Backup", value: backupOk ? "All passed" : backupStatus === "failure" ? "Some failed" : "Unknown", inline: true }, );
if (hasResults) { fields.push( { name: "Updated", value: `**${counts.updated}**`, inline: true }, { name: "Unchanged", value: `${counts.unchanged}`, inline: true }, { name: "Failed", value: counts.failed > 0 ? `**${counts.failed}**` : `${counts.failed}`, inline: true }, ); } else { fields.push( { name: "Repos processed", value: `**${repoCount}**`, inline: true }, ); }
const embed = { title: ok ? "Mirror \u2014 Success" : "Mirror \u2014 Failed", url: runUrl, color: ok ? 0x2ecc71 : 0xe74c3c, description, fields, footer: { text: GITHUB_REPOSITORY, icon_url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", }, timestamp: new Date().toISOString(), };
const charBudget = EMBED_TOTAL_LIMIT - embedLength(embed) - 64; const fieldBudget = MAX_FIELDS - fields.length;
const failedFields = repoListFields("Failed repos", names.failed, { charBudget, fieldBudget }); const failedCost = failedFields.reduce((n, f) => n + f.name.length + f.value.length, 0); const updatedFields = repoListFields("Updated repos", names.updated, { charBudget: charBudget - failedCost, fieldBudget: fieldBudget - failedFields.length, });
fields.push(...updatedFields, ...failedFields);
const payload = { username: "GitHub Mirror Bot", avatar_url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", embeds: [embed], };
const res = await fetch(DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), });
if (!res.ok) { const body = await res.text(); console.error(`Discord webhook failed: ${res.status} ${res.statusText}\n${body}`); process.exit(1); }
console.log("Discord notification sent");})();