Compare commits
3 Commits
v1.0.14
...
0e14180044
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e14180044 | |||
| cd3ba5fe07 | |||
| 26f8a81631 |
@@ -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
|
||||||
|
|||||||
@@ -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,12 +382,14 @@ 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`** и установщиков — они не меняются по смыслу.
|
||||||
|
|
||||||
Не запускайте **два релиза**, которые одновременно пушат feed, — возможна гонка и отказ **`--force-with-lease`**.
|
Не запускайте **два релиза**, которые одновременно пушат 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`
|
## Если push отклонён: `(fetch first)` / `rejected`
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.14",
|
"version": "1.0.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.14",
|
"version": "1.0.15",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.14",
|
"version": "1.0.15",
|
||||||
"description": "DNDGamePlayer — редактор и проигрыватель игр",
|
"description": "DNDGamePlayer — редактор и проигрыватель игр",
|
||||||
"main": "dist/main/index.cjs",
|
"main": "dist/main/index.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* ветка `updates`, чтобы generic URL …/raw/branch/updates/ указывал на актуальные latest*.yml и установщики.
|
* ветка `updates`, чтобы generic URL …/raw/branch/updates/ указывал на актуальные latest*.yml и установщики.
|
||||||
*
|
*
|
||||||
* Копирование **merge**: существующие файлы в ветке (другие ОС) не удаляются — обновляются только
|
* Копирование **merge**: существующие файлы в ветке (другие ОС) не удаляются — обновляются только
|
||||||
* те имена, которые пришли из переданных каталогов артефактов.
|
* те имена, которые пришли из переданных каталогов артефактов. Перед копированием удаляются **устаревшие**
|
||||||
|
* установщики другой semver в имени (`DNDGamePlayer-1.0.6-…` при релизе `v1.0.14`), чтобы в feed не копились старые версии.
|
||||||
*
|
*
|
||||||
* Переменные окружения:
|
* Переменные окружения:
|
||||||
* DND_UPDATES_SERVER — https://git.example.com (без слэша в конце)
|
* DND_UPDATES_SERVER — https://git.example.com (без слэша в конце)
|
||||||
@@ -13,13 +14,15 @@
|
|||||||
* 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 (1–5, по умолчанию 3)
|
* DND_GIT_PUSH_RETRIES — число попыток git push (1–10, по умолчанию 5); при squash — для каждого push во временную ветку и финального push в updates
|
||||||
|
* DND_GIT_PUSH_RETRY_SLEEP_SEC — пауза между попытками push (5–120 с, по умолчанию 30)
|
||||||
* DND_UPDATES_CLONE_DEPTH — опционально, глубина shallow clone (2–200, по умолчанию 40), чтобы merge с remote был надёжнее
|
* DND_UPDATES_CLONE_DEPTH — опционально, глубина shallow clone (2–200, по умолчанию 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)
|
||||||
|
* DND_FEED_PRUNE_OLD_VERSIONS — если "0"/"false"/"no", не удалять старые DNDGamePlayer-* другой версии перед копированием
|
||||||
*/
|
*/
|
||||||
import { execFileSync, spawnSync } from 'node:child_process';
|
import { execFileSync, spawnSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -98,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) {
|
function mustEnv(name) {
|
||||||
const v = process.env[name]?.trim();
|
const v = process.env[name]?.trim();
|
||||||
if (!v) throw new Error(`Missing env ${name}`);
|
if (!v) throw new Error(`Missing env ${name}`);
|
||||||
@@ -200,8 +277,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 +301,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 +358,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 +444,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 +457,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,6 +505,9 @@ function main() {
|
|||||||
|
|
||||||
mergeOriginUpdates(work);
|
mergeOriginUpdates(work);
|
||||||
|
|
||||||
|
const currentSemver = parseReleaseSemverFromGitTag();
|
||||||
|
pruneObsoleteDndReleaseArtifacts(work, currentSemver, macDir);
|
||||||
|
|
||||||
const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
|
const artifactDirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
|
||||||
let copied = 0;
|
let copied = 0;
|
||||||
for (const d of artifactDirs) {
|
for (const d of artifactDirs) {
|
||||||
|
|||||||
Reference in New Issue
Block a user