From 3877a6f2a6febbe2b6a72efbb3d09a9f1d31583d Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Tue, 12 May 2026 13:42:53 +0800 Subject: [PATCH] feat(ci): squash updates repo history each release (DND_UPDATES_SQUASH_HISTORY) Co-authored-by: Cursor --- .gitea/workflows/release.yml | 1 + docs/GITEA_AUTO_UPDATE.md | 14 ++++++- scripts/sync-update-feed.mjs | 71 +++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 00e6861..438c6f3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -135,6 +135,7 @@ jobs: ARTIFACT_MAC: ${{ github.workspace }}/_mac ARTIFACT_LINUX: ${{ github.workspace }}/release DND_FEED_TMP_ROOT: ${{ github.workspace }}/.dnd-feed-tmp + DND_UPDATES_SQUASH_HISTORY: '1' GIT_COMMIT_TAG: ${{ github.ref_name }} run: | set -euo pipefail diff --git a/docs/GITEA_AUTO_UPDATE.md b/docs/GITEA_AUTO_UPDATE.md index 7c788a9..b9c673f 100644 --- a/docs/GITEA_AUTO_UPDATE.md +++ b/docs/GITEA_AUTO_UPDATE.md @@ -378,9 +378,21 @@ git push origin v1.0.1 --- +## Линейная история в `UPDATES_REPO` (например DndGamePlayerUpdates) + +Переписывается **только** публичный репозиторий из секрета **`UPDATES_REPO`**, ветка **`updates`**. Репозиторий с исходниками игры (**DndGamePlayer**) и его теги **не меняются**. + +В workflow задано **`DND_UPDATES_SQUASH_HISTORY=1`**: после merge и подкладки артефактов скрипт делает **`git checkout --orphan`** → один корневой коммит → push с **`--force-with-lease`**, если локально уже есть **`refs/remotes/origin/updates`** (ветка на сервере была); иначе — обычный первый **`git push -u origin updates`**. Так на каждом релизе в истории остаётся **один** коммит с актуальным набором файлов — меньше трафик и диск при `clone`/`fetch` на раннере. + +На **сервере Gitea** старые объекты коммитов остаются «висячими», пока не отработает **сборка мусора** репозитория (настройки сервера / ручной `git gc` в bare-репо). Для пользователей приложения важны только URL **`latest*.yml`** и установщиков — они не меняются по смыслу. + +Не запускайте **два релиза**, которые одновременно пушат feed, — возможна гонка и отказ **`--force-with-lease`**. + +--- + ## Если push отклонён: `(fetch first)` / `rejected` -Пока job собирает артефакты, в **`updates`** мог успеть попасть **другой** коммит (второй релиз, ручная выкладка). Скрипт `sync-update-feed.mjs` перед push делает **`git fetch` + `git merge origin/updates`** (и после клона — то же в начале), плюс shallow **глубина по умолчанию 40** (`DND_UPDATES_CLONE_DEPTH`). Не запускайте **два релиза одного и того же репо одновременно** по двум тегам — возможны конфликты merge. +Пока job собирает артефакты, в **`updates`** мог успеть попасть **другой** коммит (второй релиз, ручная выкладка). Скрипт `sync-update-feed.mjs` перед push (в режиме **без** squash) делает **`git fetch` + `git merge origin/updates`** (и после клона — то же в начале), плюс shallow **глубина** (`DND_UPDATES_CLONE_DEPTH`: по умолчанию **40**, при **`DND_UPDATES_SQUASH_HISTORY=1`** в скрипте по умолчанию **8**). В режиме **squash** перед push merge **не** выполняется — уже смерженное дерево сжимается в один коммит и отправляется force-with-lease. Не запускайте **два релиза одного и того же репо одновременно** по двум тегам — возможны конфликты merge. --- diff --git a/scripts/sync-update-feed.mjs b/scripts/sync-update-feed.mjs index aaf3170..a4086ff 100644 --- a/scripts/sync-update-feed.mjs +++ b/scripts/sync-update-feed.mjs @@ -18,6 +18,7 @@ * 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 + * DND_UPDATES_SQUASH_HISTORY — если "1", после каждого релиза ветка updates в UPDATES_REPO — один корневой коммит (orphan + force-with-lease push), история не копится (меньше места на раннере и на сервере после GC) */ import { execFileSync, spawnSync } from 'node:child_process'; import fs from 'node:fs'; @@ -175,6 +176,55 @@ function sleepSyncSeconds(seconds) { } } +function feedSquashSingleCommitEnabled() { + return process.env.DND_UPDATES_SQUASH_HISTORY?.trim() === '1'; +} + +/** После merge и add: одна линия без истории (только репо UPDATES_REPO / ветка updates). */ +function squashUpdatesBranchToSingleCommit(work, message) { + const orphan = 'dnd-feed-squash-tmp'; + runGit(['checkout', '--orphan', orphan], work); + runGit(['add', '-A'], work); + runGit(['commit', '-m', message], work); + runGit(['branch', '-D', 'updates'], work); + runGit(['branch', '-m', 'updates'], work); +} + +function hadOriginUpdatesRemoteRef(work) { + try { + execFileSync('git', ['show-ref', '--verify', '--quiet', 'refs/remotes/origin/updates'], { + cwd: work, + stdio: 'ignore', + }); + return true; + } catch { + return false; + } +} + +/** После squash не делаем merge перед push — вернёт старую историю с remote. */ +function pushUpdatesBranchAfterSquash(work) { + const retries = Math.max(1, Math.min(5, Number.parseInt(process.env.DND_GIT_PUSH_RETRIES || '3', 10) || 3)); + const args = hadOriginUpdatesRemoteRef(work) + ? ['push', '--force-with-lease', '-u', 'origin', 'updates'] + : ['push', '-u', 'origin', 'updates']; + let lastError; + for (let attempt = 1; attempt <= retries; attempt += 1) { + try { + console.log(`[sync-update-feed] push updates (squash, attempt ${attempt}/${retries})`); + execFileSync('git', args, { cwd: work, stdio: 'inherit' }); + return; + } catch (err) { + lastError = err; + console.warn(`[sync-update-feed] push failed (attempt ${attempt}/${retries})`); + if (attempt < retries) { + sleepSyncSeconds(20); + } + } + } + throw lastError; +} + function mergeOriginUpdates(work) { const fetchRetries = Math.max( 1, @@ -252,10 +302,10 @@ function main() { const tmp = fs.mkdtempSync(path.join(tmpRoot, 'dnd-feed-')); const work = path.join(tmp, 'repo'); - const cloneDepth = Math.max( - 2, - Math.min(200, Number.parseInt(process.env.DND_UPDATES_CLONE_DEPTH || '40', 10) || 40), - ); + const squash = feedSquashSingleCommitEnabled(); + const cloneDepth = squash + ? Math.max(1, Math.min(200, Number.parseInt(process.env.DND_UPDATES_CLONE_DEPTH || '8', 10) || 8)) + : Math.max(2, Math.min(200, Number.parseInt(process.env.DND_UPDATES_CLONE_DEPTH || '40', 10) || 40)); const gh = gitHttpInlineFlags(); try { @@ -290,8 +340,17 @@ function main() { runGit(['add', '-A'], work); const st = execFileSync('git', ['status', '--porcelain'], { cwd: work }).toString().trim(); if (st) { - runGit(['commit', '-m', `update feed ${tag}`], work); - pushUpdatesBranch(work); + const msg = `update feed ${tag}`; + if (squash) { + console.warn( + '[sync-update-feed] DND_UPDATES_SQUASH_HISTORY=1: одна линия в UPDATES_REPO (orphan + force-with-lease при наличии origin/updates)', + ); + squashUpdatesBranchToSingleCommit(work, msg); + pushUpdatesBranchAfterSquash(work); + } else { + runGit(['commit', '-m', msg], work); + pushUpdatesBranch(work); + } } else { console.warn('[sync-update-feed] nothing to commit (identical artifacts?)'); }