From 064592d4d4a104b1f82f52806d1409045f78ea36 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Tue, 12 May 2026 11:27:02 +0800 Subject: [PATCH] fix(ci): reduce disk use for update feed sync (rename, tmp root, fetch retries) Co-authored-by: Cursor --- .gitea/workflows/release.yml | 40 +++++--------------- docs/GITEA_AUTO_UPDATE.md | 14 +++++++ package-lock.json | 4 +- package.json | 2 +- scripts/sync-update-feed.mjs | 72 +++++++++++++++++++++++++++++++----- 5 files changed, 88 insertions(+), 44 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e2535d9..00e6861 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -94,20 +94,6 @@ jobs: --config.publish.provider=generic \ --config.publish.url="${DND_UPDATE_FEED_URL}" - - name: Каталог артефактов для feed (_linux) - shell: bash - run: | - mkdir -p _linux - shopt -s nullglob || true - for f in release/*; do - [[ -f "$f" ]] || continue - base=$(basename "$f") - case "$base" in - latest-linux.yml|*.yaml|*.AppImage|*.appimage|*.blockmap) cp -v "$f" _linux/ ;; - esac - done - ls -la _linux - # Не используем `npm install`: на Linux npm падает с EBADPLATFORM для win32-пакета. - name: sharp (@img/sharp-win32-x64) для Windows-артефакта при сборке на Linux shell: bash @@ -135,33 +121,25 @@ jobs: --config.publish.provider=generic \ --config.publish.url="${DND_UPDATE_FEED_URL}" - - name: Каталог артефактов для feed (_win) - shell: bash - run: | - mkdir -p _win - shopt -s nullglob || true - for f in release/*; do - [[ -f "$f" ]] || continue - base=$(basename "$f") - case "$base" in - *.yml|*.yaml|*.exe|*.blockmap|*.zip) cp -v "$f" _win/ ;; - esac - done - ls -la _win - + # Артефакты читает sync напрямую из release/ (без дублирующих _win/_linux — меньше ENOSPC). - name: Пустой каталог mac (пока нет сборки mac в CI) run: mkdir -p _mac - name: Push в публичный репозиторий updates + shell: bash env: DND_UPDATES_SERVER: ${{ secrets.DND_UPDATES_SERVER }} UPDATES_REPO: ${{ secrets.UPDATES_REPO }} DND_UPDATES_PUSH_TOKEN: ${{ secrets.DND_UPDATES_PUSH_TOKEN }} - ARTIFACT_WIN: ${{ github.workspace }}/_win + ARTIFACT_WIN: ${{ github.workspace }}/release ARTIFACT_MAC: ${{ github.workspace }}/_mac - ARTIFACT_LINUX: ${{ github.workspace }}/_linux + ARTIFACT_LINUX: ${{ github.workspace }}/release + DND_FEED_TMP_ROOT: ${{ github.workspace }}/.dnd-feed-tmp GIT_COMMIT_TAG: ${{ github.ref_name }} - run: node scripts/sync-update-feed.mjs + run: | + set -euo pipefail + mkdir -p "${{ github.workspace }}/.dnd-feed-tmp" "${{ github.workspace }}/_mac" + node scripts/sync-update-feed.mjs # Когда появится macOS-раннер: отдельный job build-macos, копирование eb-mac в _mac перед sync # или расширить шаг «Каталог артефактов»; сейчас всё в одном job `release`. diff --git a/docs/GITEA_AUTO_UPDATE.md b/docs/GITEA_AUTO_UPDATE.md index 184c866..e5d391a 100644 --- a/docs/GITEA_AUTO_UPDATE.md +++ b/docs/GITEA_AUTO_UPDATE.md @@ -384,6 +384,20 @@ git push origin v1.0.1 --- +## ENOSPC / «no space left on device» при sync + +Частая причина: **дублирование** артефактов (`release/` → `_linux`/`_win` → временный клон в **`/tmp`**) на раннере с маленьким root. Сейчас CI **не копирует** в `_win`/`_linux`: `ARTIFACT_WIN` и `ARTIFACT_LINUX` указывают на **`release/`**, а временный клон feed создаётся под **`DND_FEED_TMP_ROOT`** (в workflow — `${{ github.workspace }}/.dnd-feed-tmp`). Скрипт по возможности делает **`rename`** файлов в клон (без второй полной копии на том же диске). + +Если ошибка остаётся — на машине раннера нужно **освободить место** или увеличить диск / вынести workspace на больший том. + +--- + +## Сбой fetch: `GnuTLS recv error` / `early EOF` + +Сеть или TLS к серверу Gitea; скрипт делает **несколько повторов** `git fetch` (`DND_GIT_FETCH_RETRIES`, по умолчанию 4). При стабильных обрывах проверьте прокси/MTU/антивирус на раннере. + +--- + ## Поведение приложения - Проверка только в **собранной** установке (`app.isPackaged`). diff --git a/package-lock.json b/package-lock.json index cebe7b3..9c9be02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "DndGamePlayer", - "version": "1.0.9", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "DndGamePlayer", - "version": "1.0.9", + "version": "1.0.10", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 5c2b71f..49b8309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "DndGamePlayer", - "version": "1.0.9", + "version": "1.0.10", "description": "DNDGamePlayer — редактор и проигрыватель игр", "main": "dist/main/index.cjs", "scripts": { diff --git a/scripts/sync-update-feed.mjs b/scripts/sync-update-feed.mjs index b6c7ec8..604bb2d 100644 --- a/scripts/sync-update-feed.mjs +++ b/scripts/sync-update-feed.mjs @@ -15,6 +15,7 @@ * GIT_COMMIT_TAG — опционально, для сообщения коммита * 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 на раннере */ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; @@ -37,7 +38,34 @@ function optionalDir(name) { return v && v.length > 0 ? v : ''; } -function copyFlatReleaseFiles(fromDir, toDir) { +/** Куда класть временный клон: workspace раннера предпочтительнее, чем /tmp (место под AppImage). */ +function feedTempRoot() { + const a = + process.env.DND_FEED_TMP_ROOT?.trim() || + process.env.GITHUB_WORKSPACE?.trim() || + process.env.GITEA_WORKSPACE?.trim() || + process.env.TMPDIR?.trim() || + os.tmpdir(); + return a; +} + +/** Перенос (rename) без дублирования байтов на одном томе; иначе copy + unlink исходника (освобождение места). */ +function moveOrCopyArtifactFile(src, dest) { + if (fs.existsSync(dest)) { + fs.unlinkSync(dest); + } + try { + fs.renameSync(src, dest); + return; + } catch (e) { + const code = /** @type {NodeJS.ErrnoException} */ (e).code; + if (code !== 'EXDEV' && code !== 'EINVAL') throw e; + } + fs.copyFileSync(src, dest); + fs.unlinkSync(src); +} + +function moveOrCopyFlatReleaseFiles(fromDir, toDir) { if (!fromDir || !fs.existsSync(fromDir)) { console.warn(`[sync-update-feed] skip missing dir: ${fromDir || '(empty)'}`); return 0; @@ -48,7 +76,7 @@ function copyFlatReleaseFiles(fromDir, toDir) { 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)); + moveOrCopyArtifactFile(src, path.join(toDir, name)); n += 1; } return n; @@ -78,7 +106,28 @@ function sleepSyncSeconds(seconds) { } function mergeOriginUpdates(work) { - runGit(['fetch', 'origin', 'updates'], work); + const fetchRetries = Math.max( + 1, + Math.min(6, Number.parseInt(process.env.DND_GIT_FETCH_RETRIES || '4', 10) || 4), + ); + let fetchOk = false; + let lastFetchErr; + for (let i = 0; i < fetchRetries; i += 1) { + try { + runGit(['fetch', 'origin', 'updates'], work); + 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); + } + } + } + if (!fetchOk) { + throw lastFetchErr ?? new Error('git fetch origin updates failed'); + } runGit(['merge', '--no-edit', 'origin/updates'], work); } @@ -114,7 +163,9 @@ function main() { const host = u.host; const cloneUrl = `https://oauth2:${encodeURIComponent(token)}@${host}/${updatesRepo}.git`; - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-feed-')); + const tmpRoot = feedTempRoot(); + fs.mkdirSync(tmpRoot, { recursive: true }); + const tmp = fs.mkdtempSync(path.join(tmpRoot, 'dnd-feed-')); const work = path.join(tmp, 'repo'); const cloneDepth = Math.max( @@ -138,13 +189,14 @@ function main() { try { mergeOriginUpdates(work); } catch { - console.warn('[sync-update-feed] initial merge skipped (новая ветка или пустой remote)'); + console.warn('[sync-update-feed] initial merge skipped (новая ветка или сеть/TLS — продолжаем)'); } - const copied = - copyFlatReleaseFiles(winDir, work) + - copyFlatReleaseFiles(macDir, work) + - copyFlatReleaseFiles(linuxDir, work); + const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))]; + let copied = 0; + for (const d of artifactDirs) { + copied += moveOrCopyFlatReleaseFiles(d, work); + } if (copied === 0) { throw new Error( '[sync-update-feed] no release files copied (check ARTIFACT_WIN / ARTIFACT_MAC / ARTIFACT_LINUX)', @@ -162,7 +214,7 @@ function main() { } fs.rmSync(tmp, { recursive: true, force: true }); - console.log(`[sync-update-feed] done (${String(copied)} file(s) copied)`); + console.log(`[sync-update-feed] done (${String(copied)} file(s) into feed repo)`); } main();