refactor: separate plugin repositories and release-based catalog

This commit is contained in:
2026-09-11 10:06:30 +08:00
parent 07174e35ab
commit 07cf38b1a4
148 changed files with 377 additions and 19048 deletions
+82
View File
@@ -0,0 +1,82 @@
import { execFileSync } from 'node:child_process';
const origin = new URL(process.env.GITEA_SERVER_URL || 'https://git.render.ink');
if (origin.protocol !== 'https:' || origin.username || origin.password) throw new Error('Gitea requires an HTTPS origin without credentials');
export const server = origin.origin;
export const catalogRepository = process.env.CATALOG_REPOSITORY || 'trove/curios';
export function repositoryPath(repository) {
if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repository)) throw new Error('Invalid repository name');
return repository.split('/').map(encodeURIComponent).join('/');
}
export async function api(route, { token = process.env.GITEA_TOKEN, method = 'GET', body, missing = false } = {}) {
const auth = authorization(token);
const response = await fetch(`${server}/api/v1${route}`, {
method, redirect: 'error', signal: AbortSignal.timeout(60_000),
headers: { Authorization: auth, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) },
body: body instanceof FormData ? body : body === undefined ? undefined : JSON.stringify(body),
});
if (missing && response.status === 404) return null;
if (!response.ok) {
const error = new Error(`Gitea ${method} ${route.split('?')[0]} returned HTTP ${response.status}`);
error.status = response.status;
throw error;
}
if (response.status === 204) return null;
return response.json();
}
// Keep credentials on this Gitea origin; release storage may redirect to a CDN.
export async function download(url, token = process.env.GITEA_TOKEN, limit = 512 * 1024 * 1024) {
let current = new URL(url);
for (let redirects = 0; redirects <= 5; redirects++) {
if (current.protocol !== 'https:' || current.username || current.password) throw new Error('Invalid release download URL');
const response = await fetch(current, {
redirect: 'manual', signal: AbortSignal.timeout(120_000),
headers: current.origin === server ? { Authorization: authorization(token) } : {},
});
if ([301, 302, 303, 307, 308].includes(response.status)) {
const location = response.headers.get('location');
await response.body?.cancel();
if (!location) throw new Error('Release redirect has no location');
current = new URL(location, current);
continue;
}
if (!response.ok) throw new Error(`Release download returned HTTP ${response.status}`);
const chunks = []; let size = 0;
for await (const chunk of response.body) {
size += chunk.length;
if (size > limit) throw new Error('Release download exceeds declared size limit');
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
throw new Error('Too many release download redirects');
}
let localAuthorization;
function authorization(token) {
if (token) return `token ${token}`;
if (process.env.GITEA_ACTIONS === 'true') throw new Error('Workflow token is missing');
if (!localAuthorization) {
const output = execFileSync('git', ['-c', 'credential.interactive=false', 'credential', 'fill'], {
input: `url=${server}\n\n`, encoding: 'utf8', timeout: 10_000,
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, stdio: ['pipe', 'pipe', 'ignore'],
});
const fields = Object.fromEntries(output.trim().split('\n').map(line => {
const separator = line.indexOf('='); return [line.slice(0, separator), line.slice(separator + 1)];
}));
if (!fields.username || !fields.password) throw new Error('Set GITEA_TOKEN or configure Git HTTPS credentials');
localAuthorization = `Basic ${Buffer.from(`${fields.username}:${fields.password}`).toString('base64')}`;
}
return localAuthorization;
}
export async function notifyCatalog(repository, tag) {
await api(`/repos/${repositoryPath(catalogRepository)}/actions/workflows/sync-release.yml/dispatches`, {
token: process.env.CATALOG_TOKEN,
method: 'POST', body: { ref: 'main', inputs: { repository, tag } },
});
console.log(`Requested catalog registration: ${repository}@${tag}`);
}