Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af4c2616f2 | |||
| 3877a6f2a6 | |||
| 1c6f06b278 | |||
| 064592d4d4 |
@@ -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,26 @@ 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
|
||||||
|
DND_UPDATES_SQUASH_HISTORY: '1'
|
||||||
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`.
|
||||||
|
|||||||
@@ -378,9 +378,41 @@ 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`
|
## Если 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 на больший том.
|
||||||
|
|
||||||
|
Перед `git clone` скрипт проверяет **свободное место на томе**, где лежит `DND_FEED_TMP_ROOT` (оценка: артефакты × 2.5 + ~1 GiB запас, минимум ~2 GiB). Если проверка мешает (редкий ложный срабатывание), можно задать **`DND_FEED_SKIP_DISK_CHECK=1`** (лучше всё же поправить диск).
|
||||||
|
|
||||||
|
Несколько **неудачных** `git fetch` подряд (обрыв TLS) могли раньше забивать диск частично распакованными pack-файлами; между попытками вызывается **`git gc --prune=now`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сбой fetch: `GnuTLS recv error` / `early EOF`
|
||||||
|
|
||||||
|
Сеть или TLS к серверу Gitea. Скрипт делает **несколько повторов** `git fetch` (`DND_GIT_FETCH_RETRIES`, по умолчанию **6**), между попытками — **`git gc --prune=now`**, для clone/fetch задано **`http.version=HTTP/1.1`** (иногда стабильнее за прокси). Если ветки **`updates` на сервере ещё нет**, merge **намеренно** пропускается (первый push feed); при **любой другой** ошибке fetch job **завершится ошибкой** — не будет «тихого» продолжения с последующим `ENOSPC` на `git add`.
|
||||||
|
|
||||||
|
При стабильных обрывах проверьте прокси/MTU/антивирус на раннере и лимиты прокси к Gitea.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.9",
|
"version": "1.0.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.9",
|
"version": "1.0.11",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "DndGamePlayer",
|
"name": "DndGamePlayer",
|
||||||
"version": "1.0.9",
|
"version": "1.0.11",
|
||||||
"description": "DNDGamePlayer — редактор и проигрыватель игр",
|
"description": "DNDGamePlayer — редактор и проигрыватель игр",
|
||||||
"main": "dist/main/index.cjs",
|
"main": "dist/main/index.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+217
-23
@@ -15,8 +15,12 @@
|
|||||||
* 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 (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_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 } from 'node:child_process';
|
import { execFileSync, spawnSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -26,6 +30,73 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
|
|
||||||
const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg', '.appimage']);
|
const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg', '.appimage']);
|
||||||
|
|
||||||
|
/** Нет ветки updates на сервере — не путать с обрывом TLS. */
|
||||||
|
const GIT_FETCH_MISSING_UPDATES_REF_RE = /couldn't find remote ref updates\b/i;
|
||||||
|
|
||||||
|
/** Одинаковые флаги для clone (до появления .git/config) и согласованы с configureGitHttp. */
|
||||||
|
function gitHttpInlineFlags() {
|
||||||
|
return [
|
||||||
|
'-c',
|
||||||
|
'http.version=HTTP/1.1',
|
||||||
|
'-c',
|
||||||
|
'http.postBuffer=2147483648',
|
||||||
|
'-c',
|
||||||
|
'http.lowSpeedLimit=0',
|
||||||
|
'-c',
|
||||||
|
'http.lowSpeedTime=0',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateArtifactBytes(winDir, macDir, linuxDir) {
|
||||||
|
const dirs = [...new Set([winDir, macDir, linuxDir].filter(Boolean))];
|
||||||
|
let total = 0;
|
||||||
|
for (const fromDir of dirs) {
|
||||||
|
if (!fromDir || !fs.existsSync(fromDir)) continue;
|
||||||
|
for (const name of fs.readdirSync(fromDir)) {
|
||||||
|
const p = path.join(fromDir, name);
|
||||||
|
if (!fs.statSync(p).isFile()) continue;
|
||||||
|
const ext = path.extname(name).toLowerCase();
|
||||||
|
if (!ALLOWED_EXT.has(ext)) continue;
|
||||||
|
total += fs.statSync(p).size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Свободные байты на томе, где лежит tmpRoot; при ошибке — «достаточно». */
|
||||||
|
function freeBytesOnVolume(tmpRoot) {
|
||||||
|
try {
|
||||||
|
const s = fs.statfsSync(tmpRoot);
|
||||||
|
const bavail = typeof s.bavail === 'bigint' ? Number(s.bavail) : s.bavail;
|
||||||
|
const bsize = typeof s.bsize === 'bigint' ? Number(s.bsize) : s.bsize;
|
||||||
|
if (!Number.isFinite(bavail) || !Number.isFinite(bsize)) return Number.MAX_SAFE_INTEGER;
|
||||||
|
return bavail * bsize;
|
||||||
|
} catch {
|
||||||
|
return Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFeedDiskHeadroom(tmpRoot, artifactBytes) {
|
||||||
|
if (process.env.DND_FEED_SKIP_DISK_CHECK?.trim() === '1') return;
|
||||||
|
const avail = freeBytesOnVolume(tmpRoot);
|
||||||
|
const minBytes = Math.max(2_000_000_000, Math.round(artifactBytes * 2.5) + 1_000_000_000);
|
||||||
|
if (avail < minBytes) {
|
||||||
|
throw new Error(
|
||||||
|
`[sync-update-feed] мало места на томе «${tmpRoot}»: свободно ~${(avail / 1e9).toFixed(2)} GiB, ` +
|
||||||
|
`нужно примерно ≥ ${(minBytes / 1e9).toFixed(2)} GiB (артефакты ~${(artifactBytes / 1e9).toFixed(2)} GiB + shallow clone/pack + git add). ` +
|
||||||
|
`Освободите диск на раннере, увеличьте том или задайте DND_FEED_TMP_ROOT на другой диск. Либо DND_FEED_SKIP_DISK_CHECK=1 (не рекомендуется).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryGitGcPrune(work) {
|
||||||
|
try {
|
||||||
|
execFileSync('git', ['gc', '--prune=now'], { cwd: work, stdio: 'ignore' });
|
||||||
|
} catch {
|
||||||
|
/* после оборванного fetch часть pack/tmp остаётся — gc освобождает; игнорируем сбой gc */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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}`);
|
||||||
@@ -37,7 +108,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 +146,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;
|
||||||
@@ -61,6 +159,7 @@ function runGit(args, cwd) {
|
|||||||
/** Большие AppImage + exe в одном push: без этого Git по умолчанию может оборвать HTTPS (postBuffer / stall). */
|
/** Большие AppImage + exe в одном push: без этого Git по умолчанию может оборвать HTTPS (postBuffer / stall). */
|
||||||
function configureGitHttpForLargePush(cwd) {
|
function configureGitHttpForLargePush(cwd) {
|
||||||
// 2 GiB — достаточно для пачки артефактов; на старых Git при необходимости поднять на сервере лимиты nginx/Gitea.
|
// 2 GiB — достаточно для пачки артефактов; на старых Git при необходимости поднять на сервере лимиты nginx/Gitea.
|
||||||
|
runGit(['config', 'http.version', 'HTTP/1.1'], cwd);
|
||||||
runGit(['config', 'http.postBuffer', '2147483648'], cwd);
|
runGit(['config', 'http.postBuffer', '2147483648'], cwd);
|
||||||
runGit(['config', 'http.lowSpeedLimit', '0'], cwd);
|
runGit(['config', 'http.lowSpeedLimit', '0'], cwd);
|
||||||
runGit(['config', 'http.lowSpeedTime', '0'], cwd);
|
runGit(['config', 'http.lowSpeedTime', '0'], cwd);
|
||||||
@@ -77,8 +176,91 @@ 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) {
|
function mergeOriginUpdates(work) {
|
||||||
runGit(['fetch', 'origin', 'updates'], work);
|
const fetchRetries = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(8, Number.parseInt(process.env.DND_GIT_FETCH_RETRIES || '6', 10) || 6),
|
||||||
|
);
|
||||||
|
let fetchOk = false;
|
||||||
|
/** @type {Error | undefined} */
|
||||||
|
let lastFetchErr;
|
||||||
|
for (let i = 0; i < fetchRetries; i += 1) {
|
||||||
|
const r = spawnSync('git', ['fetch', 'origin', 'updates'], {
|
||||||
|
cwd: work,
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 64 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (r.status === 0) {
|
||||||
|
fetchOk = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const errText = `${r.stderr || ''}\n${r.stdout || ''}`;
|
||||||
|
if (GIT_FETCH_MISSING_UPDATES_REF_RE.test(errText)) {
|
||||||
|
console.warn('[sync-update-feed] ветки updates нет на remote — пропуск merge (первый push feed)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastFetchErr = new Error(errText.trim() || `git fetch exited with status ${String(r.status)}`);
|
||||||
|
console.warn(`[sync-update-feed] git fetch failed (attempt ${i + 1}/${fetchRetries})`);
|
||||||
|
if (r.stderr?.trim()) {
|
||||||
|
console.warn(r.stderr.trim());
|
||||||
|
}
|
||||||
|
tryGitGcPrune(work);
|
||||||
|
if (i < fetchRetries - 1) {
|
||||||
|
sleepSyncSeconds(18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,20 +296,26 @@ 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 });
|
||||||
|
assertFeedDiskHeadroom(tmpRoot, estimateArtifactBytes(winDir, macDir, linuxDir));
|
||||||
|
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 squash = feedSquashSingleCommitEnabled();
|
||||||
2,
|
const cloneDepth = squash
|
||||||
Math.min(200, Number.parseInt(process.env.DND_UPDATES_CLONE_DEPTH || '40', 10) || 40),
|
? 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 {
|
try {
|
||||||
execFileSync('git', ['clone', `--depth=${String(cloneDepth)}`, '-b', 'updates', cloneUrl, work], {
|
execFileSync('git', [...gh, 'clone', `--depth=${String(cloneDepth)}`, '-b', 'updates', cloneUrl, work], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
execFileSync('git', ['clone', `--depth=${String(cloneDepth)}`, cloneUrl, work], { stdio: 'inherit' });
|
execFileSync('git', [...gh, 'clone', `--depth=${String(cloneDepth)}`, cloneUrl, work], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
runGit(['checkout', '-B', 'updates'], work);
|
runGit(['checkout', '-B', 'updates'], work);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,16 +323,13 @@ function main() {
|
|||||||
runGit(['config', 'user.name', 'gitea-actions'], work);
|
runGit(['config', 'user.name', 'gitea-actions'], work);
|
||||||
configureGitHttpForLargePush(work);
|
configureGitHttpForLargePush(work);
|
||||||
|
|
||||||
try {
|
mergeOriginUpdates(work);
|
||||||
mergeOriginUpdates(work);
|
|
||||||
} catch {
|
|
||||||
console.warn('[sync-update-feed] initial merge skipped (новая ветка или пустой remote)');
|
|
||||||
}
|
|
||||||
|
|
||||||
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)',
|
||||||
@@ -155,14 +340,23 @@ function main() {
|
|||||||
runGit(['add', '-A'], work);
|
runGit(['add', '-A'], work);
|
||||||
const st = execFileSync('git', ['status', '--porcelain'], { cwd: work }).toString().trim();
|
const st = execFileSync('git', ['status', '--porcelain'], { cwd: work }).toString().trim();
|
||||||
if (st) {
|
if (st) {
|
||||||
runGit(['commit', '-m', `update feed ${tag}`], work);
|
const msg = `update feed ${tag}`;
|
||||||
pushUpdatesBranch(work);
|
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 {
|
} else {
|
||||||
console.warn('[sync-update-feed] nothing to commit (identical artifacts?)');
|
console.warn('[sync-update-feed] nothing to commit (identical artifacts?)');
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user