fix(ci): sync feed fetch retries with gc, HTTP/1.1, disk check; fail on TLS
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -390,11 +390,17 @@ git push origin v1.0.1
|
||||
|
||||
Если ошибка остаётся — на машине раннера нужно **освободить место** или увеличить диск / вынести workspace на больший том.
|
||||
|
||||
Перед `git clone` скрипт проверяет **свободное место на томе**, где лежит `DND_FEED_TMP_ROOT` (оценка: артефакты × 2.5 + ~1 GiB запас, минимум ~2 GiB). Если проверка мешает (редкий ложный срабатывание), можно задать **`DND_FEED_SKIP_DISK_CHECK=1`** (лучше всё же поправить диск).
|
||||
|
||||
Несколько **неудачных** `git fetch` подряд (обрыв TLS) могли раньше забивать диск частично распакованными pack-файлами; между попытками вызывается **`git gc --prune=now`**.
|
||||
|
||||
---
|
||||
|
||||
## Сбой fetch: `GnuTLS recv error` / `early EOF`
|
||||
|
||||
Сеть или TLS к серверу Gitea; скрипт делает **несколько повторов** `git fetch` (`DND_GIT_FETCH_RETRIES`, по умолчанию 4). При стабильных обрывах проверьте прокси/MTU/антивирус на раннере.
|
||||
Сеть или TLS к серверу Gitea. Скрипт делает **несколько повторов** `git fetch` (`DND_GIT_FETCH_RETRIES`, по умолчанию **6**), между попытками — **`git gc --prune=now`**, для clone/fetch задано **`http.version=HTTP/1.1`** (иногда стабильнее за прокси). Если ветки **`updates` на сервере ещё нет**, merge **намеренно** пропускается (первый push feed); при **любой другой** ошибке fetch job **завершится ошибкой** — не будет «тихого» продолжения с последующим `ENOSPC` на `git add`.
|
||||
|
||||
При стабильных обрывах проверьте прокси/MTU/антивирус на раннере и лимиты прокси к Gitea.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
* DND_GIT_PUSH_RETRIES — опционально, число попыток git push (1–5, по умолчанию 3)
|
||||
* DND_UPDATES_CLONE_DEPTH — опционально, глубина shallow clone (2–200, по умолчанию 40), чтобы merge с remote был надёжнее
|
||||
* DND_FEED_TMP_ROOT — каталог для временного клона feed (по умолчанию GITHUB_WORKSPACE / TMPDIR / os.tmpdir); не используйте узкий /tmp на раннере
|
||||
* DND_GIT_FETCH_RETRIES — число попыток git fetch (1–6, по умолчанию 6); между попытками — git gc --prune=now (освобождение после обрыва TLS/unpack)
|
||||
* DND_FEED_SKIP_DISK_CHECK — если "1", не проверять свободное место на томе DND_FEED_TMP_ROOT перед clone
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -27,6 +29,73 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg', '.appimage']);
|
||||
|
||||
/** Нет ветки updates на сервере — не путать с обрывом TLS. */
|
||||
const GIT_FETCH_MISSING_UPDATES_REF_RE = /couldn't find remote ref updates\b/i;
|
||||
|
||||
/** Одинаковые флаги для clone (до появления .git/config) и согласованы с configureGitHttp. */
|
||||
function gitHttpInlineFlags() {
|
||||
return [
|
||||
'-c',
|
||||
'http.version=HTTP/1.1',
|
||||
'-c',
|
||||
'http.postBuffer=2147483648',
|
||||
'-c',
|
||||
'http.lowSpeedLimit=0',
|
||||
'-c',
|
||||
'http.lowSpeedTime=0',
|
||||
];
|
||||
}
|
||||
|
||||
function estimateArtifactBytes(winDir, macDir, linuxDir) {
|
||||
const dirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
|
||||
let total = 0;
|
||||
for (const fromDir of dirs) {
|
||||
if (!fromDir || !fs.existsSync(fromDir)) continue;
|
||||
for (const name of fs.readdirSync(fromDir)) {
|
||||
const p = path.join(fromDir, name);
|
||||
if (!fs.statSync(p).isFile()) continue;
|
||||
const ext = path.extname(name).toLowerCase();
|
||||
if (!ALLOWED_EXT.has(ext)) continue;
|
||||
total += fs.statSync(p).size;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Свободные байты на томе, где лежит tmpRoot; при ошибке — «достаточно». */
|
||||
function freeBytesOnVolume(tmpRoot) {
|
||||
try {
|
||||
const s = fs.statfsSync(tmpRoot);
|
||||
const bavail = typeof s.bavail === 'bigint' ? Number(s.bavail) : s.bavail;
|
||||
const bsize = typeof s.bsize === 'bigint' ? Number(s.bsize) : s.bsize;
|
||||
if (!Number.isFinite(bavail) || !Number.isFinite(bsize)) return Number.MAX_SAFE_INTEGER;
|
||||
return bavail * bsize;
|
||||
} catch {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
}
|
||||
|
||||
function assertFeedDiskHeadroom(tmpRoot, artifactBytes) {
|
||||
if (process.env.DND_FEED_SKIP_DISK_CHECK?.trim() === '1') return;
|
||||
const avail = freeBytesOnVolume(tmpRoot);
|
||||
const minBytes = Math.max(2_000_000_000, Math.round(artifactBytes * 2.5) + 1_000_000_000);
|
||||
if (avail < minBytes) {
|
||||
throw new Error(
|
||||
`[sync-update-feed] мало места на томе «${tmpRoot}»: свободно ~${(avail / 1e9).toFixed(2)} GiB, ` +
|
||||
`нужно примерно ≥ ${(minBytes / 1e9).toFixed(2)} GiB (артефакты ~${(artifactBytes / 1e9).toFixed(2)} GiB + shallow clone/pack + git add). ` +
|
||||
`Освободите диск на раннере, увеличьте том или задайте DND_FEED_TMP_ROOT на другой диск. Либо DND_FEED_SKIP_DISK_CHECK=1 (не рекомендуется).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function tryGitGcPrune(work) {
|
||||
try {
|
||||
execFileSync('git', ['gc', '--prune=now'], { cwd: work, stdio: 'ignore' });
|
||||
} catch {
|
||||
/* после оборванного fetch часть pack/tmp остаётся — gc освобождает; игнорируем сбой gc */
|
||||
}
|
||||
}
|
||||
|
||||
function mustEnv(name) {
|
||||
const v = process.env[name]?.trim();
|
||||
if (!v) throw new Error(`Missing env ${name}`);
|
||||
@@ -89,6 +158,7 @@ function runGit(args, cwd) {
|
||||
/** Большие AppImage + exe в одном push: без этого Git по умолчанию может оборвать HTTPS (postBuffer / stall). */
|
||||
function configureGitHttpForLargePush(cwd) {
|
||||
// 2 GiB — достаточно для пачки артефактов; на старых Git при необходимости поднять на сервере лимиты nginx/Gitea.
|
||||
runGit(['config', 'http.version', 'HTTP/1.1'], cwd);
|
||||
runGit(['config', 'http.postBuffer', '2147483648'], cwd);
|
||||
runGit(['config', 'http.lowSpeedLimit', '0'], cwd);
|
||||
runGit(['config', 'http.lowSpeedTime', '0'], cwd);
|
||||
@@ -108,21 +178,34 @@ function sleepSyncSeconds(seconds) {
|
||||
function mergeOriginUpdates(work) {
|
||||
const fetchRetries = Math.max(
|
||||
1,
|
||||
Math.min(6, Number.parseInt(process.env.DND_GIT_FETCH_RETRIES || '4', 10) || 4),
|
||||
Math.min(8, Number.parseInt(process.env.DND_GIT_FETCH_RETRIES || '6', 10) || 6),
|
||||
);
|
||||
let fetchOk = false;
|
||||
/** @type {Error | undefined} */
|
||||
let lastFetchErr;
|
||||
for (let i = 0; i < fetchRetries; i += 1) {
|
||||
try {
|
||||
runGit(['fetch', 'origin', 'updates'], work);
|
||||
const r = spawnSync('git', ['fetch', 'origin', 'updates'], {
|
||||
cwd: work,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
if (r.status === 0) {
|
||||
fetchOk = true;
|
||||
break;
|
||||
} catch (err) {
|
||||
lastFetchErr = err;
|
||||
console.warn(`[sync-update-feed] git fetch failed (attempt ${i + 1}/${fetchRetries})`);
|
||||
if (i < fetchRetries - 1) {
|
||||
sleepSyncSeconds(15);
|
||||
}
|
||||
const errText = `${r.stderr || ''}\n${r.stdout || ''}`;
|
||||
if (GIT_FETCH_MISSING_UPDATES_REF_RE.test(errText)) {
|
||||
console.warn('[sync-update-feed] ветки updates нет на remote — пропуск merge (первый push feed)');
|
||||
return;
|
||||
}
|
||||
lastFetchErr = new Error(errText.trim() || `git fetch exited with status ${String(r.status)}`);
|
||||
console.warn(`[sync-update-feed] git fetch failed (attempt ${i + 1}/${fetchRetries})`);
|
||||
if (r.stderr?.trim()) {
|
||||
console.warn(r.stderr.trim());
|
||||
}
|
||||
tryGitGcPrune(work);
|
||||
if (i < fetchRetries - 1) {
|
||||
sleepSyncSeconds(18);
|
||||
}
|
||||
}
|
||||
if (!fetchOk) {
|
||||
@@ -165,6 +248,7 @@ function main() {
|
||||
|
||||
const tmpRoot = feedTempRoot();
|
||||
fs.mkdirSync(tmpRoot, { recursive: true });
|
||||
assertFeedDiskHeadroom(tmpRoot, estimateArtifactBytes(winDir, macDir, linuxDir));
|
||||
const tmp = fs.mkdtempSync(path.join(tmpRoot, 'dnd-feed-'));
|
||||
const work = path.join(tmp, 'repo');
|
||||
|
||||
@@ -173,12 +257,15 @@ function main() {
|
||||
Math.min(200, Number.parseInt(process.env.DND_UPDATES_CLONE_DEPTH || '40', 10) || 40),
|
||||
);
|
||||
|
||||
const gh = gitHttpInlineFlags();
|
||||
try {
|
||||
execFileSync('git', ['clone', `--depth=${String(cloneDepth)}`, '-b', 'updates', cloneUrl, work], {
|
||||
execFileSync('git', [...gh, 'clone', `--depth=${String(cloneDepth)}`, '-b', 'updates', cloneUrl, work], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
execFileSync('git', ['clone', `--depth=${String(cloneDepth)}`, cloneUrl, work], { stdio: 'inherit' });
|
||||
execFileSync('git', [...gh, 'clone', `--depth=${String(cloneDepth)}`, cloneUrl, work], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
runGit(['checkout', '-B', 'updates'], work);
|
||||
}
|
||||
|
||||
@@ -186,11 +273,7 @@ function main() {
|
||||
runGit(['config', 'user.name', 'gitea-actions'], work);
|
||||
configureGitHttpForLargePush(work);
|
||||
|
||||
try {
|
||||
mergeOriginUpdates(work);
|
||||
} catch {
|
||||
console.warn('[sync-update-feed] initial merge skipped (новая ветка или сеть/TLS — продолжаем)');
|
||||
}
|
||||
|
||||
const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
|
||||
let copied = 0;
|
||||
|
||||
Reference in New Issue
Block a user