fix(ci): reduce disk use for update feed sync (rename, tmp root, fetch retries)
Release / release (push) Failing after 8m46s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-12 11:27:02 +08:00
parent e923de350d
commit 064592d4d4
5 changed files with 88 additions and 44 deletions
+9 -31
View File
@@ -94,20 +94,6 @@ jobs:
--config.publish.provider=generic \ --config.publish.provider=generic \
--config.publish.url="${DND_UPDATE_FEED_URL}" --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-пакета. # Не используем `npm install`: на Linux npm падает с EBADPLATFORM для win32-пакета.
- name: sharp (@img/sharp-win32-x64) для Windows-артефакта при сборке на Linux - name: sharp (@img/sharp-win32-x64) для Windows-артефакта при сборке на Linux
shell: bash shell: bash
@@ -135,33 +121,25 @@ jobs:
--config.publish.provider=generic \ --config.publish.provider=generic \
--config.publish.url="${DND_UPDATE_FEED_URL}" --config.publish.url="${DND_UPDATE_FEED_URL}"
- name: Каталог артефактов для feed (_win) # Артефакты читает sync напрямую из release/ (без дублирующих _win/_linux — меньше ENOSPC).
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
- name: Пустой каталог mac (пока нет сборки mac в CI) - name: Пустой каталог mac (пока нет сборки mac в CI)
run: mkdir -p _mac run: mkdir -p _mac
- name: Push в публичный репозиторий updates - name: Push в публичный репозиторий updates
shell: bash
env: env:
DND_UPDATES_SERVER: ${{ secrets.DND_UPDATES_SERVER }} DND_UPDATES_SERVER: ${{ secrets.DND_UPDATES_SERVER }}
UPDATES_REPO: ${{ secrets.UPDATES_REPO }} UPDATES_REPO: ${{ secrets.UPDATES_REPO }}
DND_UPDATES_PUSH_TOKEN: ${{ secrets.DND_UPDATES_PUSH_TOKEN }} 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_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 }} 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 # Когда появится macOS-раннер: отдельный job build-macos, копирование eb-mac в _mac перед sync
# или расширить шаг «Каталог артефактов»; сейчас всё в одном job `release`. # или расширить шаг «Каталог артефактов»; сейчас всё в одном job `release`.
+14
View File
@@ -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`). - Проверка только в **собранной** установке (`app.isPackaged`).
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "DndGamePlayer", "name": "DndGamePlayer",
"version": "1.0.9", "version": "1.0.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "DndGamePlayer", "name": "DndGamePlayer",
"version": "1.0.9", "version": "1.0.10",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "DndGamePlayer", "name": "DndGamePlayer",
"version": "1.0.9", "version": "1.0.10",
"description": "DNDGamePlayer — редактор и проигрыватель игр", "description": "DNDGamePlayer — редактор и проигрыватель игр",
"main": "dist/main/index.cjs", "main": "dist/main/index.cjs",
"scripts": { "scripts": {
+62 -10
View File
@@ -15,6 +15,7 @@
* GIT_COMMIT_TAG — опционально, для сообщения коммита * GIT_COMMIT_TAG — опционально, для сообщения коммита
* DND_GIT_PUSH_RETRIES — опционально, число попыток git push (1–5, по умолчанию 3) * DND_GIT_PUSH_RETRIES — опционально, число попыток git push (1–5, по умолчанию 3)
* DND_UPDATES_CLONE_DEPTH — опционально, глубина shallow clone (2200, по умолчанию 40), чтобы merge с remote был надёжнее * DND_UPDATES_CLONE_DEPTH — опционально, глубина shallow clone (2200, по умолчанию 40), чтобы merge с remote был надёжнее
* DND_FEED_TMP_ROOT — каталог для временного клона feed (по умолчанию GITHUB_WORKSPACE / TMPDIR / os.tmpdir); не используйте узкий /tmp на раннере
*/ */
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
@@ -37,7 +38,34 @@ function optionalDir(name) {
return v && v.length > 0 ? v : ''; 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)) { if (!fromDir || !fs.existsSync(fromDir)) {
console.warn(`[sync-update-feed] skip missing dir: ${fromDir || '(empty)'}`); console.warn(`[sync-update-feed] skip missing dir: ${fromDir || '(empty)'}`);
return 0; return 0;
@@ -48,7 +76,7 @@ function copyFlatReleaseFiles(fromDir, toDir) {
if (!fs.statSync(src).isFile()) continue; if (!fs.statSync(src).isFile()) continue;
const ext = path.extname(name).toLowerCase(); const ext = path.extname(name).toLowerCase();
if (!ALLOWED_EXT.has(ext)) continue; if (!ALLOWED_EXT.has(ext)) continue;
fs.copyFileSync(src, path.join(toDir, name)); moveOrCopyArtifactFile(src, path.join(toDir, name));
n += 1; n += 1;
} }
return n; return n;
@@ -78,7 +106,28 @@ function sleepSyncSeconds(seconds) {
} }
function mergeOriginUpdates(work) { 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); runGit(['merge', '--no-edit', 'origin/updates'], work);
} }
@@ -114,7 +163,9 @@ function main() {
const host = u.host; const host = u.host;
const cloneUrl = `https://oauth2:${encodeURIComponent(token)}@${host}/${updatesRepo}.git`; 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 work = path.join(tmp, 'repo');
const cloneDepth = Math.max( const cloneDepth = Math.max(
@@ -138,13 +189,14 @@ function main() {
try { try {
mergeOriginUpdates(work); mergeOriginUpdates(work);
} catch { } catch {
console.warn('[sync-update-feed] initial merge skipped (новая ветка или пустой remote)'); console.warn('[sync-update-feed] initial merge skipped (новая ветка или сеть/TLS — продолжаем)');
} }
const copied = const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
copyFlatReleaseFiles(winDir, work) + let copied = 0;
copyFlatReleaseFiles(macDir, work) + for (const d of artifactDirs) {
copyFlatReleaseFiles(linuxDir, work); copied += moveOrCopyFlatReleaseFiles(d, work);
}
if (copied === 0) { if (copied === 0) {
throw new Error( throw new Error(
'[sync-update-feed] no release files copied (check ARTIFACT_WIN / ARTIFACT_MAC / ARTIFACT_LINUX)', '[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 }); 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(); main();