diff --git a/docs/GITEA_AUTO_UPDATE.md b/docs/GITEA_AUTO_UPDATE.md index c877f9c..551b48e 100644 --- a/docs/GITEA_AUTO_UPDATE.md +++ b/docs/GITEA_AUTO_UPDATE.md @@ -388,6 +388,8 @@ git push origin v1.0.1 Не запускайте **два релиза**, которые одновременно пушат feed, — возможна гонка и отказ **`--force-with-lease`**. +Перед подкладкой артефактов скрипт **удаляет** из корня ветки файлы вида **`DNDGamePlayer-<чужая версия>-*`** / **`DNDGamePlayer-Setup-<чужая версия>.exe`** (и связанные `.blockmap`), чтобы после `merge` не копились старые установщики (1.0.6 рядом с 1.0.14). Версия текущего релиза берётся из **`GIT_COMMIT_TAG`** (`v1.0.14` → `1.0.14`). Файлы **`latest*.yml`** перезаписываются копированием. Если в **`ARTIFACT_MAC`** нет ни одного файла сборки, **`.dmg` / `.pkg`** не удаляются — сохраняется вручную залитый mac в feed. Отключить очистку: **`DND_FEED_PRUNE_OLD_VERSIONS=0`**. + --- ## Если push отклонён: `(fetch first)` / `rejected` diff --git a/scripts/sync-update-feed.mjs b/scripts/sync-update-feed.mjs index 48dc5a4..bfeee77 100644 --- a/scripts/sync-update-feed.mjs +++ b/scripts/sync-update-feed.mjs @@ -3,7 +3,8 @@ * ветка `updates`, чтобы generic URL …/raw/branch/updates/ указывал на актуальные latest*.yml и установщики. * * Копирование **merge**: существующие файлы в ветке (другие ОС) не удаляются — обновляются только - * те имена, которые пришли из переданных каталогов артефактов. + * те имена, которые пришли из переданных каталогов артефактов. Перед копированием удаляются **устаревшие** + * установщики другой semver в имени (`DNDGamePlayer-1.0.6-…` при релизе `v1.0.14`), чтобы в feed не копились старые версии. * * Переменные окружения: * DND_UPDATES_SERVER — https://git.example.com (без слэша в конце) @@ -21,6 +22,7 @@ * DND_FEED_SKIP_DISK_CHECK — если "1", не проверять свободное место на томе DND_FEED_TMP_ROOT перед clone * 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_PRUNE_OLD_VERSIONS — если "0"/"false"/"no", не удалять старые DNDGamePlayer-* другой версии перед копированием */ import { execFileSync, spawnSync } from 'node:child_process'; import fs from 'node:fs'; @@ -99,6 +101,80 @@ function tryGitGcPrune(work) { } } +/** Тег релиза `v1.0.14` → `1.0.14` (с опциональным prerelease). */ +function parseReleaseSemverFromGitTag() { + const t = (process.env.GIT_COMMIT_TAG || '').trim(); + const m = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.+]+)?)$/.exec(t); + return m ? m[1] : null; +} + +function macArtifactLooksLike(fileName) { + const lo = fileName.toLowerCase(); + if (lo.endsWith('.dmg') || lo.endsWith('.pkg')) return true; + if (lo.endsWith('.zip') && /(?:^|-)mac(?:-|$)|darwin/i.test(fileName)) return true; + return false; +} + +/** + * Версия из имени артефакта electron-builder (DNDGamePlayer + semver в имени). + * `latest*.yml` и прочее без semver — null. + */ +function extractVersionedDndArtifactSemver(fileName) { + const n = fileName; + let m = /^DNDGamePlayer-Setup-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.+]+)?)\.exe(?:\.blockmap)?$/i.exec(n); + if (m) return m[1]; + m = /^DNDGamePlayer-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.+]+)?)-/i.exec(n); + if (m) return m[1]; + m = /^DNDGamePlayer-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.+]+)?)\.(dmg|pkg)$/i.exec(n); + if (m) return m[1]; + return null; +} + +function artifactDirHasIncomingFiles(dir) { + if (!dir || !fs.existsSync(dir)) return false; + for (const name of fs.readdirSync(dir)) { + const p = path.join(dir, name); + if (!fs.statSync(p).isFile()) continue; + const ext = path.extname(name).toLowerCase(); + if (ALLOWED_EXT.has(ext)) return true; + } + return false; +} + +/** + * Удаляет из корня клона старые установщики с другим semver (после merge они иначе остаются навсегда). + * Если в ARTIFACT_MAC нет файлов — .dmg/.pkg не трогаем (ручная заливка mac в feed). + */ +function pruneObsoleteDndReleaseArtifacts(work, currentSemver, macDir) { + const off = process.env.DND_FEED_PRUNE_OLD_VERSIONS?.trim().toLowerCase(); + if (off === '0' || off === 'false' || off === 'no') return 0; + if (!currentSemver) { + console.warn('[sync-update-feed] prune: пропуск — GIT_COMMIT_TAG не похож на v1.2.3'); + return 0; + } + const pruneMac = artifactDirHasIncomingFiles(macDir); + let removed = 0; + for (const name of fs.readdirSync(work)) { + if (name === '.git') continue; + const p = path.join(work, name); + if (!fs.statSync(p).isFile()) continue; + const ext = path.extname(name).toLowerCase(); + if (!ALLOWED_EXT.has(ext)) continue; + const fileVer = extractVersionedDndArtifactSemver(name); + if (!fileVer) continue; + if (fileVer === currentSemver) continue; + if (!pruneMac && macArtifactLooksLike(name)) continue; + fs.unlinkSync(p); + removed += 1; + } + if (removed > 0) { + console.warn( + `[sync-update-feed] prune: удалено устаревших артефактов (другая версия в имени): ${String(removed)}`, + ); + } + return removed; +} + function mustEnv(name) { const v = process.env[name]?.trim(); if (!v) throw new Error(`Missing env ${name}`); @@ -429,6 +505,9 @@ function main() { mergeOriginUpdates(work); + const currentSemver = parseReleaseSemverFromGitTag(); + pruneObsoleteDndReleaseArtifacts(work, currentSemver, macDir); + const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))]; let copied = 0; for (const d of artifactDirs) {