fix(ci): more git push retries for feed; document HTTP 500 and temp branch

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-13 23:48:39 +08:00
parent 2dc7015f53
commit 26f8a81631
3 changed files with 27 additions and 10 deletions
+2
View File
@@ -136,6 +136,8 @@ jobs:
ARTIFACT_LINUX: ${{ github.workspace }}/release ARTIFACT_LINUX: ${{ github.workspace }}/release
DND_FEED_TMP_ROOT: ${{ github.workspace }}/.dnd-feed-tmp DND_FEED_TMP_ROOT: ${{ github.workspace }}/.dnd-feed-tmp
DND_UPDATES_SQUASH_HISTORY: '1' DND_UPDATES_SQUASH_HISTORY: '1'
DND_GIT_PUSH_RETRIES: '8'
DND_GIT_PUSH_RETRY_SLEEP_SEC: '35'
GIT_COMMIT_TAG: ${{ github.ref_name }} GIT_COMMIT_TAG: ${{ github.ref_name }}
run: | run: |
set -euo pipefail set -euo pipefail
+3 -3
View File
@@ -362,13 +362,13 @@ git push origin v1.0.1
## Если `git push` в `updates` падает: `remote end hung up` / таймаут ## Если `git push` в `updates` падает: `remote end hung up` / таймаут
Один коммит с **Windows + два AppImage** может быть **сотни МБ** — HTTPS-push иногда рвётся из‑за лимита буфера Git или таймаута **nginx / reverse proxy** перед Gitea. Один коммит с **Windows + два AppImage** может быть **сотни МБ** — HTTPS-push иногда рвётся из‑за лимита буфера Git или таймаута **nginx / reverse proxy** перед Gitea. Отдельно встречается **`HTTP 500`** от Gitea/прокси на большом pack (например один `*-win.zip` ~200 MiB) — это уже **ошибка сервера** при приёме тела запроса; повторы push иногда помогают, но при стабильном 500 нужно править лимиты/таймауты на стороне Gitea и reverse proxy.
**В репозитории с кодом** скрипт `scripts/sync-update-feed.mjs` уже выставляет в клоне feed-репо: **В репозитории с кодом** скрипт `scripts/sync-update-feed.mjs` уже выставляет в клоне feed-репо:
- `http.postBuffer` **2 GiB**; - `http.postBuffer` **2 GiB**;
- отключение «медленной передачи» (`http.lowSpeedLimit` / `http.lowSpeedTime`); - отключение «медленной передачи» (`http.lowSpeedLimit` / `http.lowSpeedTime`);
- до **3** повторов `git push` с паузой 20 с (переменная **`DND_GIT_PUSH_RETRIES`**, максимум 5). - повторы **`git push`**: переменная **`DND_GIT_PUSH_RETRIES`** (по умолчанию **5**, максимум **10**), пауза **`DND_GIT_PUSH_RETRY_SLEEP_SEC`** (по умолчанию **30** с, максимум **120**). В workflow для шага sync задано **`DND_GIT_PUSH_RETRIES=8`** и **`DND_GIT_PUSH_RETRY_SLEEP_SEC=35`**.
Если ошибка сохраняется — на **сервере** (nginx и т.п.) проверьте, например: Если ошибка сохраняется — на **сервере** (nginx и т.п.) проверьте, например:
@@ -382,7 +382,7 @@ git push origin v1.0.1
Переписывается **только** публичный репозиторий из секрета **`UPDATES_REPO`**, ветка **`updates`**. Репозиторий с исходниками игры (**DndGamePlayer**) и его теги **не меняются**. Переписывается **только** публичный репозиторий из секрета **`UPDATES_REPO`**, ветка **`updates`**. Репозиторий с исходниками игры (**DndGamePlayer**) и его теги **не меняются**.
В workflow задано **`DND_UPDATES_SQUASH_HISTORY=1`**: после merge и подкладки артефактов скрипт делает **`git checkout --orphan`** и собирает историю **только текущего релиза**. Чтобы не отправлять один огромный pack (~700+ MiB) и не ловить `curl 55 Broken pipe`, загрузка идёт через временную ветку **`updates-upload-*`**: сначала small files, затем каждый большой файл отдельным push (порог **`DND_FEED_LARGE_FILE_BYTES`**, по умолчанию 64 MiB). Ветка **`updates`** передвигается на готовый финальный коммит только в конце через **`--force-with-lease`** (если ветка уже была) или обычный первый push. Пользователи не видят «полурелиз», потому что `updates` меняется только после полной загрузки. В workflow задано **`DND_UPDATES_SQUASH_HISTORY=1`**: после merge и подкладки артефактов скрипт делает **`git checkout --orphan`** и собирает историю **только текущего релиза**. Чтобы не отправлять один огромный pack (~700+ MiB) и не ловить `curl 55 Broken pipe`, загрузка идёт через **временную** ветку **`updates-upload-*`**: сначала small files, затем каждый большой файл отдельным push (порог **`DND_FEED_LARGE_FILE_BYTES`**, по умолчанию 64 MiB). Эта ветка **не заменяет** публичный feed: URL для клиентов по-прежнему **`…/raw/branch/updates/`**. `updates-upload-*` — только черновик на время job; в конце ветка **`updates`** передвигается на готовый коммит (`--force-with-lease` или первый push), временная ветка **удаляется** с remote.
На **сервере Gitea** старые объекты коммитов остаются «висячими», пока не отработает **сборка мусора** репозитория (настройки сервера / ручной `git gc` в bare-репо). Для пользователей приложения важны только URL **`latest*.yml`** и установщиков — они не меняются по смыслу. На **сервере Gitea** старые объекты коммитов остаются «висячими», пока не отработает **сборка мусора** репозитория (настройки сервера / ручной `git gc` в bare-репо). Для пользователей приложения важны только URL **`latest*.yml`** и установщиков — они не меняются по смыслу.
+22 -7
View File
@@ -13,12 +13,13 @@
* ARTIFACT_MAC — каталог с файлами macOS * ARTIFACT_MAC — каталог с файлами macOS
* ARTIFACT_LINUX — каталог с файлами Linux (AppImage и т.д.) * ARTIFACT_LINUX — каталог с файлами Linux (AppImage и т.д.)
* GIT_COMMIT_TAG — опционально, для сообщения коммита * GIT_COMMIT_TAG — опционально, для сообщения коммита
* DND_GIT_PUSH_RETRIES — опционально, число попыток git push (15, по умолчанию 3) * DND_GIT_PUSH_RETRIES — число попыток git push (110, по умолчанию 5); при squash — для каждого push во временную ветку и финального push в updates
* DND_GIT_PUSH_RETRY_SLEEP_SEC — пауза между попытками push (5–120 с, по умолчанию 30)
* 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 на раннере * 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_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 * DND_FEED_SKIP_DISK_CHECK — если "1", не проверять свободное место на томе DND_FEED_TMP_ROOT перед clone
* DND_UPDATES_SQUASH_HISTORY — если "1", после каждого релиза ветка updates в UPDATES_REPO переписывается на историю только текущего релиза (orphan + временная ветка + force-with-lease) * DND_UPDATES_SQUASH_HISTORY — если "1", после каждого релиза ветка updates в UPDATES_REPO переписывается на историю только текущего релиза. Загрузка идёт во временную ветку updates-upload-* (маленькие pack'и), затем публикуется updates; временная ветка удаляется — пользователям feed по-прежнему нужна только ветка updates.
* DND_FEED_LARGE_FILE_BYTES — порог "большого" файла для дробления push в squash-режиме (по умолчанию 64 MiB) * DND_FEED_LARGE_FILE_BYTES — порог "большого" файла для дробления push в squash-режиме (по умолчанию 64 MiB)
*/ */
import { execFileSync, spawnSync } from 'node:child_process'; import { execFileSync, spawnSync } from 'node:child_process';
@@ -200,8 +201,20 @@ function sanitizeRefPart(s) {
.slice(0, 80); .slice(0, 80);
} }
function gitPushRetryCount() {
return Math.max(1, Math.min(10, Number.parseInt(process.env.DND_GIT_PUSH_RETRIES || '5', 10) || 5));
}
function gitPushRetrySleepSec() {
return Math.max(
5,
Math.min(120, Number.parseInt(process.env.DND_GIT_PUSH_RETRY_SLEEP_SEC || '30', 10) || 30),
);
}
function pushWithRetries(work, args, label) { function pushWithRetries(work, args, label) {
const retries = Math.max(1, Math.min(5, Number.parseInt(process.env.DND_GIT_PUSH_RETRIES || '3', 10) || 3)); const retries = gitPushRetryCount();
const sleepSec = gitPushRetrySleepSec();
let lastError; let lastError;
for (let attempt = 1; attempt <= retries; attempt += 1) { for (let attempt = 1; attempt <= retries; attempt += 1) {
try { try {
@@ -212,7 +225,7 @@ function pushWithRetries(work, args, label) {
lastError = err; lastError = err;
console.warn(`[sync-update-feed] ${label} failed (attempt ${attempt}/${retries})`); console.warn(`[sync-update-feed] ${label} failed (attempt ${attempt}/${retries})`);
if (attempt < retries) { if (attempt < retries) {
sleepSyncSeconds(20); sleepSyncSeconds(sleepSec);
} }
} }
} }
@@ -269,7 +282,8 @@ function publishSquashedUpdatesBranchStreamed(work, message, tag) {
console.warn( console.warn(
`[sync-update-feed] DND_UPDATES_SQUASH_HISTORY=1: переписываем только UPDATES_REPO/updates; ` + `[sync-update-feed] DND_UPDATES_SQUASH_HISTORY=1: переписываем только UPDATES_REPO/updates; ` +
`push дробится через временную ветку ${tempBranch} (${smallFiles.length} small, ${largeFiles.length} large)`, `push дробится через временную ветку ${tempBranch} (${smallFiles.length} small, ${largeFiles.length} large). ` +
`Ветка updates-upload-* служебная: после успеха на remote остаётся только updates; клиенты по-прежнему читают raw/.../updates/.`,
); );
runGit(['checkout', '--orphan', 'dnd-feed-upload-tmp'], work); runGit(['checkout', '--orphan', 'dnd-feed-upload-tmp'], work);
@@ -354,7 +368,8 @@ function mergeOriginUpdates(work) {
} }
function pushUpdatesBranch(work) { function pushUpdatesBranch(work) {
const retries = Math.max(1, Math.min(5, Number.parseInt(process.env.DND_GIT_PUSH_RETRIES || '3', 10) || 3)); const retries = gitPushRetryCount();
const sleepSec = gitPushRetrySleepSec();
let lastError; let lastError;
for (let attempt = 1; attempt <= retries; attempt += 1) { for (let attempt = 1; attempt <= retries; attempt += 1) {
try { try {
@@ -366,7 +381,7 @@ function pushUpdatesBranch(work) {
lastError = err; lastError = err;
console.warn(`[sync-update-feed] merge/push failed (attempt ${attempt}/${retries})`); console.warn(`[sync-update-feed] merge/push failed (attempt ${attempt}/${retries})`);
if (attempt < retries) { if (attempt < retries) {
sleepSyncSeconds(20); sleepSyncSeconds(sleepSec);
} }
} }
} }