From 1c6f06b2786a4c31db0e558cbce6970104717e4a Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Tue, 12 May 2026 13:10:37 +0800 Subject: [PATCH] fix(ci): sync feed fetch retries with gc, HTTP/1.1, disk check; fail on TLS Co-authored-by: Cursor --- docs/GITEA_AUTO_UPDATE.md | 8 ++- scripts/sync-update-feed.mjs | 117 ++++++++++++++++++++++++++++++----- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/docs/GITEA_AUTO_UPDATE.md b/docs/GITEA_AUTO_UPDATE.md index e5d391a..7c788a9 100644 --- a/docs/GITEA_AUTO_UPDATE.md +++ b/docs/GITEA_AUTO_UPDATE.md @@ -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. --- diff --git a/scripts/sync-update-feed.mjs b/scripts/sync-update-feed.mjs index 604bb2d..aaf3170 100644 --- a/scripts/sync-update-feed.mjs +++ b/scripts/sync-update-feed.mjs @@ -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 — продолжаем)'); - } + mergeOriginUpdates(work); const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))]; let copied = 0;