Files
DndGamePlayer/scripts/sync-update-feed.mjs
T
Ivan Fontosh 07641be2d2
Release / release (push) Failing after 6m13s
fix(ci): resilient large git push to updates feed (1.0.8)
http.postBuffer 2GiB, disable low-speed abort, retry push; docs for nginx/Gitea limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:04:31 +08:00

148 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Складывает артефакты electron-builder (win + mac + linux) в публичный репозиторий,
* ветка `updates`, чтобы generic URL …/raw/branch/updates/ указывал на актуальные latest*.yml и установщики.
*
* Копирование **merge**: существующие файлы в ветке (другие ОС) не удаляются — обновляются только
* те имена, которые пришли из переданных каталогов артефактов.
*
* Переменные окружения:
* DND_UPDATES_SERVER — https://git.example.com (без слэша в конце)
* UPDATES_REPO — owner/repo (публичный репозиторий)
* DND_UPDATES_PUSH_TOKEN — PAT с правом push в UPDATES_REPO
* ARTIFACT_WIN — каталог с файлами Windows (можно пустой / отсутствует — пропуск)
* ARTIFACT_MAC — каталог с файлами macOS
* ARTIFACT_LINUX — каталог с файлами Linux (AppImage и т.д.)
* GIT_COMMIT_TAG — опционально, для сообщения коммита
* DND_GIT_PUSH_RETRIES — опционально, число попыток git push (1–5, по умолчанию 3)
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg', '.appimage']);
function mustEnv(name) {
const v = process.env[name]?.trim();
if (!v) throw new Error(`Missing env ${name}`);
return v;
}
function optionalDir(name) {
const v = process.env[name]?.trim();
return v && v.length > 0 ? v : '';
}
function copyFlatReleaseFiles(fromDir, toDir) {
if (!fromDir || !fs.existsSync(fromDir)) {
console.warn(`[sync-update-feed] skip missing dir: ${fromDir || '(empty)'}`);
return 0;
}
let n = 0;
for (const name of fs.readdirSync(fromDir)) {
const src = path.join(fromDir, name);
if (!fs.statSync(src).isFile()) continue;
const ext = path.extname(name).toLowerCase();
if (!ALLOWED_EXT.has(ext)) continue;
fs.copyFileSync(src, path.join(toDir, name));
n += 1;
}
return n;
}
function runGit(args, cwd) {
execFileSync('git', args, { cwd, stdio: 'inherit' });
}
/** Большие AppImage + exe в одном push: без этого Git по умолчанию может оборвать HTTPS (postBuffer / stall). */
function configureGitHttpForLargePush(cwd) {
// 2 GiB — достаточно для пачки артефактов; на старых Git при необходимости поднять на сервере лимиты nginx/Gitea.
runGit(['config', 'http.postBuffer', '2147483648'], cwd);
runGit(['config', 'http.lowSpeedLimit', '0'], cwd);
runGit(['config', 'http.lowSpeedTime', '0'], cwd);
}
function sleepSyncSeconds(seconds) {
try {
execFileSync('sleep', [String(seconds)], { stdio: 'ignore' });
} catch {
const end = Date.now() + seconds * 1000;
while (Date.now() < end) {
/* fallback без утилиты sleep */
}
}
}
function pushUpdatesBranch(work) {
const retries = Math.max(1, Math.min(5, Number.parseInt(process.env.DND_GIT_PUSH_RETRIES || '3', 10) || 3));
let lastError;
for (let attempt = 1; attempt <= retries; attempt += 1) {
try {
runGit(['push', '-u', 'origin', 'updates'], work);
return;
} catch (err) {
lastError = err;
console.warn(`[sync-update-feed] git push failed (attempt ${attempt}/${retries})`);
if (attempt < retries) {
sleepSyncSeconds(20);
}
}
}
throw lastError;
}
function main() {
const server = mustEnv('DND_UPDATES_SERVER').replace(/\/+$/u, '');
const updatesRepo = mustEnv('UPDATES_REPO');
const token = mustEnv('DND_UPDATES_PUSH_TOKEN');
const winDir = optionalDir('ARTIFACT_WIN');
const macDir = optionalDir('ARTIFACT_MAC');
const linuxDir = optionalDir('ARTIFACT_LINUX');
const u = new URL(server);
const host = u.host;
const cloneUrl = `https://oauth2:${encodeURIComponent(token)}@${host}/${updatesRepo}.git`;
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-feed-'));
const work = path.join(tmp, 'repo');
try {
execFileSync('git', ['clone', '--depth', '1', '-b', 'updates', cloneUrl, work], { stdio: 'inherit' });
} catch {
execFileSync('git', ['clone', '--depth', '1', cloneUrl, work], { stdio: 'inherit' });
runGit(['checkout', '-B', 'updates'], work);
}
runGit(['config', 'user.email', 'ci@gitea-actions.local'], work);
runGit(['config', 'user.name', 'gitea-actions'], work);
configureGitHttpForLargePush(work);
const copied =
copyFlatReleaseFiles(winDir, work) +
copyFlatReleaseFiles(macDir, work) +
copyFlatReleaseFiles(linuxDir, work);
if (copied === 0) {
throw new Error(
'[sync-update-feed] no release files copied (check ARTIFACT_WIN / ARTIFACT_MAC / ARTIFACT_LINUX)',
);
}
const tag = process.env.GIT_COMMIT_TAG?.trim() || 'ci';
runGit(['add', '-A'], work);
const st = execFileSync('git', ['status', '--porcelain'], { cwd: work }).toString().trim();
if (st) {
runGit(['commit', '-m', `update feed ${tag}`], work);
pushUpdatesBranch(work);
} else {
console.warn('[sync-update-feed] nothing to commit (identical artifacts?)');
}
fs.rmSync(tmp, { recursive: true, force: true });
console.log(`[sync-update-feed] done (${String(copied)} file(s) copied)`);
}
main();