Files
gitea-github-cron/mirror.js
T

118 lines
4.4 KiB
JavaScript

const syncOrgAvatar = require('./sync-org-avatar');
const { GH_PAT, GITEA_TOKEN, GITEA_URL } = process.env;
const G_API = `https://${GITEA_URL.replace(/^https?:\/\//, '')}/api/v1`;
const headers = {
GH: { Authorization: `token ${GH_PAT}`, Accept: 'application/vnd.github+json' },
GT: { Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }
};
const req = async (url, h, m = 'GET', b = null, allow404 = false) => {
const res = await fetch(url, { method: m, headers: h, body: b ? JSON.stringify(b) : null });
if (res.status === 404 && allow404) return null;
if (!res.ok) {
const remaining = res.headers.get('x-ratelimit-remaining');
const reset = res.headers.get('x-ratelimit-reset');
const limit = remaining === null ? '' : ` (GitHub rate limit remaining: ${remaining}, reset: ${reset})`;
throw new Error(`${res.status} ${res.statusText}${limit}: ${(await res.text()).slice(0, 500)}`);
}
return m === 'GET' ? res.json() : res;
};
const getPages = async (url) => {
let p = 1, all = [], d;
do {
d = await req(`${url}${url.includes('?') ? '&' : '?'}per_page=100&page=${p++}`, headers.GH);
if (!Array.isArray(d)) throw new Error(`Expected a list from ${url}`);
all.push(...d);
} while (d?.length === 100);
return all;
};
(async () => {
console.log('Starting Mirror Sync...');
try {
const gUser = await req(`${G_API}/user`, headers.GT);
if (!gUser) throw new Error('Cannot auth with Gitea');
const [ghUserRepos, ghOrgs] = await Promise.all([
getPages('https://api.github.com/user/repos?affiliation=owner&visibility=all'),
getPages('https://api.github.com/user/orgs')
]);
let allRepos = [...ghUserRepos];
let avatarFailures = 0, repoFailures = 0;
for (const org of ghOrgs) {
console.log(`Checking Org: ${org.login}`);
try {
const gOrg = await req(`${G_API}/orgs/${org.login}`, headers.GT, 'GET', null, true);
if (!gOrg) {
console.log(`Creating Org: ${org.login}`);
await req(`${G_API}/orgs`, headers.GT, 'POST', { username: org.login, visibility: 'public' });
} else if (gOrg.visibility !== 'public') {
console.log(`Updating Org Visibility: ${org.login}`);
await req(`${G_API}/orgs/${org.login}`, headers.GT, 'PATCH', { visibility: 'public' });
}
try {
if (await syncOrgAvatar(org, G_API, headers.GT)) console.log(`Updated Org Avatar: ${org.login}`);
} catch (e) {
avatarFailures++;
console.error(`Failed to sync avatar for ${org.login}:`, e.message);
}
const orgRepos = await getPages(`https://api.github.com/orgs/${org.login}/repos?type=all`);
allRepos.push(...orgRepos);
} catch (e) {
throw new Error(`Failed to process org ${org.login}: ${e.message}`);
}
}
console.log(`Processing ${allRepos.length} repositories...`);
for (const r of allRepos) {
try {
const owner = r.owner.login;
const exists = await req(`${G_API}/repos/${owner}/${r.name}`, headers.GT, 'GET', null, true);
if (!exists) {
console.log(`Mirroring: ${owner}/${r.name}`);
const payload = {
clone_addr: r.clone_url,
auth_token: GH_PAT,
mirror: true,
repo_name: r.name,
repo_owner: owner,
service: 'github',
description: r.description || '',
private: r.private,
wiki: true,
lfs: true,
releases: true,
issues: true,
pull_requests: true,
labels: true,
milestones: true,
mirror_prune: true
};
await req(`${G_API}/repos/migrate`, headers.GT, 'POST', payload);
} else if (exists.private !== r.private) {
console.log(`Updating visibility: ${owner}/${r.name} → ${r.private ? 'private' : 'public'}`);
await req(`${G_API}/repos/${owner}/${r.name}`, headers.GT, 'PATCH', { private: r.private });
}
} catch (e) {
repoFailures++;
console.error(`Failed to mirror ${r.owner.login}/${r.name}:`, e.message);
}
}
if (avatarFailures || repoFailures) {
console.error(`Sync finished with ${avatarFailures} avatar failure(s) and ${repoFailures} repository failure(s).`);
process.exitCode = 1;
}
} catch (err) {
console.error('Fatal Error:', err);
process.exit(1);
}
if (!process.exitCode) console.log('Sync Complete.');
})();