From 8bc2e5bd4950117b3b664a82501d1ac0db041730 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Tue, 12 May 2026 10:49:57 +0800 Subject: [PATCH] Release 1.0.7: Linux AppImage (x64/arm64) CI, merge update feed, docs - electron-builder linux + qemu cross arm64 on ubuntu-22.04 job - sync-update-feed: merge copy, ARTIFACT_LINUX, .appimage - GITEA_AUTO_UPDATE + MANUAL_MAC_UPDATE_UPLOAD Co-authored-by: Cursor --- .gitea/workflows/release.yml | 46 ++++++++- app/shared/package.build.test.ts | 9 +- docs/GITEA_AUTO_UPDATE.md | 17 +++- docs/MANUAL_MAC_UPDATE_UPLOAD.md | 159 +++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 23 ++++- scripts/sync-update-feed.mjs | 50 +++++----- 7 files changed, 274 insertions(+), 34 deletions(-) create mode 100644 docs/MANUAL_MAC_UPDATE_UPLOAD.md diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ad4b6f9..e2535d9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -2,7 +2,7 @@ # # Метки runs-on = labels твоих act_runner (Админка Gitea → Действия → Раннеры). # Не используем windows-latest/macos-latest — это только GitHub-hosted. -# По умолчанию: одна Linux-сборка Win+NSIS (Wine). macOS — когда будет раннер (см. комментарий в build-macos). +# По умолчанию: один job на ubuntu-22.04 — Win (Wine+NSIS), Linux AppImage (x64+arm64), sync в updates без затирания других ОС. # # Один job без actions/upload-artifact: официальный upload-artifact@v4 с GitHub на Gitea # падает (GHESNotSupportedError). Сборка и sync-update-feed идут в одном окружении. @@ -67,6 +67,47 @@ jobs: - run: npm ci + - run: npm run build + + # Linux AppImage (x64 + arm64) до подмешивания win32-sharp в node_modules. + - name: Зависимости Linux AppImage и кросс-сборка arm64 на amd64 + shell: bash + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-user-static \ + binfmt-support \ + desktop-file-utils \ + squashfs-tools + + - name: electron-builder (linux AppImage x64, arm64) + shell: bash + env: + DND_UPDATE_FEED_URL: ${{ secrets.DND_UPDATE_FEED_URL }} + run: | + set -euo pipefail + if [[ -z "${DND_UPDATE_FEED_URL:-}" ]]; then + echo "Secret DND_UPDATE_FEED_URL is not set (URL со слэшем в конце)" >&2 + exit 1 + fi + npx electron-builder --linux AppImage --x64 --arm64 --publish never \ + --config.publish.provider=generic \ + --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-пакета. - name: sharp (@img/sharp-win32-x64) для Windows-артефакта при сборке на Linux shell: bash @@ -80,8 +121,6 @@ jobs: test -f node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64.node rm -rf "$tmp" - - run: npm run build - - name: electron-builder (win) shell: bash env: @@ -120,6 +159,7 @@ jobs: DND_UPDATES_PUSH_TOKEN: ${{ secrets.DND_UPDATES_PUSH_TOKEN }} ARTIFACT_WIN: ${{ github.workspace }}/_win ARTIFACT_MAC: ${{ github.workspace }}/_mac + ARTIFACT_LINUX: ${{ github.workspace }}/_linux GIT_COMMIT_TAG: ${{ github.ref_name }} run: node scripts/sync-update-feed.mjs diff --git a/app/shared/package.build.test.ts b/app/shared/package.build.test.ts index 119f40c..68dd106 100644 --- a/app/shared/package.build.test.ts +++ b/app/shared/package.build.test.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); -void test('package.json: конфиг electron-builder (mac/win)', () => { +void test('package.json: конфиг electron-builder (mac/win/linux)', () => { const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) as { build: { appId: string; @@ -14,6 +14,8 @@ void test('package.json: конфиг electron-builder (mac/win)', () => { asarUnpack: string[]; extraResources: { from: string; to: string }[]; mac: { target: unknown }; + linux: { target: unknown }; + appImage?: { artifactName?: string }; files: string[]; }; }; @@ -34,5 +36,10 @@ void test('package.json: конфиг electron-builder (mac/win)', () => { ), ); assert.ok(Array.isArray(pkg.build.mac.target)); + assert.ok(Array.isArray(pkg.build.linux.target)); + const linuxTargets = pkg.build.linux.target as { target: string; arch: string[] }[]; + assert.ok(linuxTargets.some((t) => t.target === 'AppImage')); + assert.ok(linuxTargets.some((t) => t.arch.includes('x64') && t.arch.includes('arm64'))); + assert.ok(pkg.build.appImage?.artifactName?.includes('${arch}')); assert.ok(pkg.build.files.includes('dist/**/*')); }); diff --git a/docs/GITEA_AUTO_UPDATE.md b/docs/GITEA_AUTO_UPDATE.md index 8a1a496..d75186e 100644 --- a/docs/GITEA_AUTO_UPDATE.md +++ b/docs/GITEA_AUTO_UPDATE.md @@ -315,7 +315,16 @@ sudo journalctl -u gitea-act-runner -f - В **`.gitea/workflows/release.yml`** в `runs-on:` должна совпадать **именно метка `ubuntu-22.04`** (Gitea сопоставляет её и с **`ubuntu-22.04:host`**, и с **`ubuntu-22.04:docker://...`** — см. [Labels](https://docs.gitea.com/usage/actions/act-runner#labels) в документации act_runner). - Если при регистрации указал только **`self-hosted`** — добавь **`ubuntu-22.04:host`** (или поменяй `runs-on` в workflow на твои метки и закоммить). -Сборка **Windows (NSIS)** в CI идёт **на Linux**: нативный **`nsis`** (`makensis`) + **`wine64`** и обёртка **`wine`→`wine64`** (без **wine32**/i386 — см. `release.yml`). Отдельная **macOS**-сборка в workflow отключена, пока нет Mac-раннера (см. комментарии в `release.yml`). +Сборка **Windows (NSIS)** и **Linux (AppImage x64 + arm64)** в CI идёт **на одном** `ubuntu-22.04`: NSIS + `wine64` для Win, `qemu-user-static` для кросс-сборки arm64 AppImage на amd64 (см. `release.yml`). **macOS** в этом job не собирается (ручная выкладка или отдельный раннер — см. `docs/MANUAL_MAC_UPDATE_UPLOAD.md`). + +--- + +## Linux: AppImage и автообновление + +- В ветке **`updates`** рядом с Windows лежат **`latest-linux.yml`** и файлы **`*.AppImage`** (x64 и arm64). Скрипт **`scripts/sync-update-feed.mjs`** делает **merge-копирование**: файлы других ОС в репозитории **не удаляются**, обновляются только имена, пришедшие из текущего CI-прогона. +- **Базовый дистрибутив для бинарников:** сборка на **Ubuntu 22.04 (glibc 2.35)** — совместимость с «максимумом» настольных дистрибутивов с glibc не старее целевого; **Alpine/musl** без отдельной сборки не гарантируется. +- **Запуск AppImage:** на части систем нужен **FUSE** (например `libfuse2` для старых форматов / документация дистрибутива). Подпись пакетов в первом варианте **не** используется (как договорённость по проекту). +- Правила **electron-updater** в приложении те же: упакованная сборка и **активная лицензия** (`installAutoUpdater.ts`). --- @@ -346,7 +355,7 @@ git push origin v1.0.1 2. В **DndGamePlayerUpdates** есть хотя бы один коммит (не пустой репо). 3. В приватном репо заданы **все четыре** секрета из таблицы шага 2 (имена **не** начинаются с `GITEA_`). 4. В репо с кодом есть **`.gitea/workflows/release.yml`**. -5. Релиз: пуш тега `v*` → в Actions job **`release`** (сборка Win + push feed в одном job, без GitHub `upload-artifact`); в публичном репо появляется ветка **`updates`** с `latest.yml` и установщиками (нужен online-раннер, см. раздел про act_runner). +5. Релиз: пуш тега `v*` → в Actions job **`release`**: сборка **Windows** + **Linux AppImage** и push feed; в публичном репо ветка **`updates`** содержит `latest.yml`, `latest-linux.yml`, установщики Windows и **`.AppImage`** для Linux (нужен online-раннер `ubuntu-22.04`, см. раздел про act_runner). Скрипт sync **не затирает** артефакты других платформ при обновлении. 6. В приложении: обновления только **`app.isPackaged`** и при **активной лицензии** (см. `app/main/update/installAutoUpdater.ts`). --- @@ -356,9 +365,9 @@ git push origin v1.0.1 - Проверка только в **собранной** установке (`app.isPackaged`). - Только если **лицензия активна**. - Первый запрос примерно через **12 с** после старта; при смене лицензии — снова (не чаще **30 с**). -- Для отладки можно задать переменную окружения **`DND_UPDATE_FEED_URL`** при запуске `.exe` — переопределит feed. +- Для отладки можно задать переменную окружения **`DND_UPDATE_FEED_URL`** при запуске приложения (Windows / Linux / macOS) — переопределит feed. -Подпись кода в CI отключена: `CSC_IDENTITY_AUTO_DISCOVERY=false`. +Подпись кода в CI отключена: `CSC_IDENTITY_AUTO_DISCOVERY=false` (в т.ч. Linux AppImage без репозитория подписи). --- diff --git a/docs/MANUAL_MAC_UPDATE_UPLOAD.md b/docs/MANUAL_MAC_UPDATE_UPLOAD.md new file mode 100644 index 0000000..161bafe --- /dev/null +++ b/docs/MANUAL_MAC_UPDATE_UPLOAD.md @@ -0,0 +1,159 @@ +# Ручная выкладка macOS-обновлений в публичный feed + +Инструкция для случая, когда **сборка делается на вашем Mac вручную**, а файлы для `electron-updater` нужно **вручную** положить в публичный репозиторий обновлений. + +Общая схема проекта: приватный репозиторий с кодом, публичный **`DndGamePlayerUpdates`**, ветка **`updates`**, URL feed как в `package.json`: + +`https://git.mailib.ru/ifontosh/DndGamePlayerUpdates/raw/branch/updates/` + +--- + +## 1. Что собрать на Mac + +В каталоге проекта `dnd_player`: + +```bash +cd /путь/к/dnd_player +npm ci +npm run build +``` + +Сборка установщиков только под macOS (без публикации в сеть, только файлы в `release/`): + +```bash +npx electron-builder --mac --publish never \ + --config.publish.provider=generic \ + --config.publish.url="https://git.mailib.ru/ifontosh/DndGamePlayerUpdates/raw/branch/updates/" +``` + +**Важно:** значение `--config.publish.url=...` должно совпадать с `package.json` → `build.publish.url` (**со слэшем в конце**). Так внутри приложения и в `latest-mac.yml` будет корректный базовый URL для скачивания. + +После сборки откройте папку **`release/`** в корне проекта. Там должны быть, среди прочего: + +- **`latest-mac.yml`** — обязателен для `electron-updater` на Mac; +- **`.dmg`** и/или **`.zip`** — то, на что ссылается `latest-mac.yml`; +- при необходимости **`.blockmap`** (если electron-builder их создал) — заливайте с теми же именами, что указаны в `latest-mac.yml`. + +Имена файлов зависят от версии и архитектур (в конфиге dmg и zip для **x64** и **arm64**). На Apple Silicon без дополнительных шагов часто получается только **arm64**; для отдельного Intel-сборщика нужна своя машина или параметры arch — ориентируйтесь на фактический список файлов в `release/`. + +--- + +## 2. Куда заливать + +Файлы должны оказаться в **публичном** репозитории: + +| Параметр | Значение | +| ------------ | ------------------------------------------------------------------------- | +| Репозиторий | `ifontosh/DndGamePlayerUpdates` (подставьте свой owner/repo, если другой) | +| Ветка | `updates` | +| Расположение | **корень ветки** (не подпапка) | + +Проверка: в браузере должен открываться, например: + +`https://git.mailib.ru/ifontosh/DndGamePlayerUpdates/raw/branch/updates/latest-mac.yml` + +Файлы Windows (`latest.yml`, `.exe`, …), которые кладёт CI, должны **остаться** в том же корне. Вы **добавляете или обновляете** только macOS-артефакты и **`latest-mac.yml`**, не удаляя артефакты Windows (если не делаете осознанную зачистку старых версий). + +--- + +## 3. Способ A — через `git` (удобно для больших dmg) + +### 3.1. Клон и ветка `updates` + +```bash +cd ~/где-удобно +git clone https://git.mailib.ru/ifontosh/DndGamePlayerUpdates.git +cd DndGamePlayerUpdates +git fetch origin +git checkout updates +``` + +Если ветки `updates` ещё нет: + +```bash +git checkout main +git pull +git checkout -b updates +git push -u origin updates +``` + +Дальше для каждой выкладки работайте в ветке **`updates`**. + +### 3.2. Актуализировать локальную копию + +```bash +git checkout updates +git pull origin updates +``` + +### 3.3. Скопировать файлы из `release/` в корень клона + +```bash +cp /путь/к/dnd_player/release/latest-mac.yml . +cp /путь/к/dnd_player/release/*.dmg . +cp /путь/к/dnd_player/release/*.zip . +# blockmap, если есть: +cp /путь/к/dnd_player/release/*.blockmap . 2>/dev/null || true +``` + +Проверка, что Windows-файлы на месте: + +```bash +ls -la +``` + +### 3.4. Коммит и push + +```bash +git add latest-mac.yml *.dmg *.zip +git add *.blockmap 2>/dev/null || true +git status +git commit -m "mac: DNDGamePlayer vX.Y.Z (ручная выкладка)" +git push origin updates +``` + +Для HTTPS обычно используют **персональный токен (PAT)** Gitea вместо пароля, либо настроенный **SSH** (`git@git.mailib.ru:ifontosh/DndGamePlayerUpdates.git`). + +### 3.5. Проверка + +- Откройте `latest-mac.yml` по raw-URL (см. выше). +- Откройте в браузере прямую ссылку на один из `.dmg` из этого YAML — не должно быть 404. + +--- + +## 4. Способ B — через веб-интерфейс Gitea + +Подходит для редких правок; для больших **dmg** удобнее git. + +1. Репозиторий `DndGamePlayerUpdates` → ветка **`updates`**. +2. Загрузить или изменить **`latest-mac.yml`** и бинарники (**имена как в `release/`**). +3. Не удалять при этом файлы Windows в корне, если они нужны для PC-обновлений. + +--- + +## 5. Версии и Windows + +- В **`latest-mac.yml`** и в именах файлов должна быть та **версия приложения**, которую вы отдаёте пользователям Mac (как в `package.json` на момент сборки). +- **`latest.yml`** (Windows) и **`latest-mac.yml`** (Mac) — разные файлы; версии на платформах могут совпадать или нет. Каждая ОС читает свой YAML. + +--- + +## 6. Подпись кода (кратко) + +Без **Developer ID** и при необходимости **нотаризации** macOS может ограничивать запуск после скачивания. На процедуру «залить файлы в репо» это не влияет, но влияет на UX после автообновления. Для продакшена имеет смысл позже настроить `CSC_LINK`, `CSC_KEY_PASSWORD` и нотаризацию по [документации electron-builder](https://www.electron.build/). + +--- + +## 7. Чеклист + +1. Локально собрали Mac с `--publish never` и верным `publish.url`. +2. В `release/` есть **`latest-mac.yml`** и все объекты, на которые он ссылается. +3. В ветке **`updates`** в корне репозитория обновили/добавили эти файлы, не снеся Windows-артефакты без необходимости. +4. Проверили raw-URL **`latest-mac.yml`** и ссылку на dmg. +5. В **упакованном** приложении с **активной лицензией** сработает существующий `electron-updater` (задержка первой проверки и cooldown — см. `app/main/update/installAutoUpdater.ts`). + +--- + +## Связанные документы + +- Общая схема Gitea, секреты, раннер: [GITEA_AUTO_UPDATE.md](./GITEA_AUTO_UPDATE.md). diff --git a/package-lock.json b/package-lock.json index 4a78682..937c151 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "DndGamePlayer", - "version": "1.0.5", + "version": "1.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "DndGamePlayer", - "version": "1.0.5", + "version": "1.0.7", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 51b7d75..3e79374 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "DndGamePlayer", - "version": "1.0.6", + "version": "1.0.7", "description": "DNDGamePlayer — редактор и проигрыватель игр", "main": "dist/main/index.cjs", "scripts": { @@ -18,7 +18,8 @@ "pack": "npm run build && node scripts/release-win-prep.mjs && electron-builder", "pack:dir": "npm run build && node scripts/release-win-prep.mjs && electron-builder --dir", "pack:mac": "npm run build && electron-builder --mac", - "pack:win": "npm run build && node scripts/release-win-prep.mjs && electron-builder --win" + "pack:win": "npm run build && node scripts/release-win-prep.mjs && electron-builder --win", + "pack:linux": "npm run build && electron-builder --linux" }, "keywords": [], "author": "", @@ -132,6 +133,24 @@ ], "icon": "build/icon.ico" }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64", + "arm64" + ] + } + ], + "category": "Game", + "maintainer": "DNDGamePlayer", + "synopsis": "DNDGamePlayer — редактор и проигрыватель игр", + "icon": "build/icon.png" + }, + "appImage": { + "artifactName": "${productName}-${version}-${arch}.${ext}" + }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, diff --git a/scripts/sync-update-feed.mjs b/scripts/sync-update-feed.mjs index 6285a70..ac3634a 100644 --- a/scripts/sync-update-feed.mjs +++ b/scripts/sync-update-feed.mjs @@ -1,13 +1,17 @@ /** - * Складывает артефакты electron-builder (win + mac) в публичный репозиторий, + * Складывает артефакты electron-builder (win + mac + linux) в публичный репозиторий, * ветка `updates`, чтобы generic URL …/raw/branch/updates/ указывал на актуальные latest*.yml и установщики. * - * Переменные окружения (имена без префикса GITEA_ — в Gitea секреты GITEA_* зарезервированы): + * Копирование **merge**: существующие файлы в ветке (другие ОС) не удаляются — обновляются только + * те имена, которые пришли из переданных каталогов артефактов. + * + * Переменные окружения: * DND_UPDATES_SERVER — https://git.example.com (без слэша в конце) - * UPDATES_REPO — owner/repo (публичный репозиторий «только релизы») + * UPDATES_REPO — owner/repo (публичный репозиторий) * DND_UPDATES_PUSH_TOKEN — PAT с правом push в UPDATES_REPO - * ARTIFACT_WIN — каталог с файлами сборки Windows - * ARTIFACT_MAC — каталог с файлами сборки macOS + * ARTIFACT_WIN — каталог с файлами Windows (можно пустой / отсутствует — пропуск) + * ARTIFACT_MAC — каталог с файлами macOS + * ARTIFACT_LINUX — каталог с файлами Linux (AppImage и т.д.) * GIT_COMMIT_TAG — опционально, для сообщения коммита */ import { execFileSync } from 'node:child_process'; @@ -18,7 +22,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg']); +const ALLOWED_EXT = new Set(['.yml', '.yaml', '.exe', '.blockmap', '.zip', '.dmg', '.pkg', '.appimage']); function mustEnv(name) { const v = process.env[name]?.trim(); @@ -26,9 +30,14 @@ function mustEnv(name) { return v; } +function optionalDir(name) { + const v = process.env[name]?.trim(); + return v && v.length > 0 ? v : ''; +} + function copyFlatReleaseFiles(fromDir, toDir) { - if (!fs.existsSync(fromDir)) { - console.warn(`[sync-update-feed] skip missing dir: ${fromDir}`); + if (!fromDir || !fs.existsSync(fromDir)) { + console.warn(`[sync-update-feed] skip missing dir: ${fromDir || '(empty)'}`); return 0; } let n = 0; @@ -47,19 +56,13 @@ function runGit(args, cwd) { execFileSync('git', args, { cwd, stdio: 'inherit' }); } -function emptyWorkingTreeExceptGit(cwd) { - for (const ent of fs.readdirSync(cwd)) { - if (ent === '.git') continue; - fs.rmSync(path.join(cwd, ent), { recursive: true, force: true }); - } -} - function main() { const server = mustEnv('DND_UPDATES_SERVER').replace(/\/+$/u, ''); const updatesRepo = mustEnv('UPDATES_REPO'); const token = mustEnv('DND_UPDATES_PUSH_TOKEN'); - const winDir = mustEnv('ARTIFACT_WIN'); - const macDir = mustEnv('ARTIFACT_MAC'); + const winDir = optionalDir('ARTIFACT_WIN'); + const macDir = optionalDir('ARTIFACT_MAC'); + const linuxDir = optionalDir('ARTIFACT_LINUX'); const u = new URL(server); const host = u.host; @@ -78,11 +81,14 @@ function main() { runGit(['config', 'user.email', 'ci@gitea-actions.local'], work); runGit(['config', 'user.name', 'gitea-actions'], work); - emptyWorkingTreeExceptGit(work); - - const copied = copyFlatReleaseFiles(winDir, work) + copyFlatReleaseFiles(macDir, work); + const copied = + copyFlatReleaseFiles(winDir, work) + + copyFlatReleaseFiles(macDir, work) + + copyFlatReleaseFiles(linuxDir, work); if (copied === 0) { - throw new Error('[sync-update-feed] no release files copied (check ARTIFACT_WIN / ARTIFACT_MAC)'); + throw new Error( + '[sync-update-feed] no release files copied (check ARTIFACT_WIN / ARTIFACT_MAC / ARTIFACT_LINUX)', + ); } const tag = process.env.GIT_COMMIT_TAG?.trim() || 'ci'; @@ -96,7 +102,7 @@ function main() { } fs.rmSync(tmp, { recursive: true, force: true }); - console.log(`[sync-update-feed] done (${String(copied)} file(s) staged)`); + console.log(`[sync-update-feed] done (${String(copied)} file(s) copied)`); } main();