Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3116aba96 | ||
|
|
b7518aa450 | ||
|
|
eb8031b126 | ||
|
|
e4b841a8d8 |
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
# Rust-часть десктопа: прокси и проверки URL. Полная сборка инсталлятора — локально
|
# Rust-часть десктопа: прокси и проверки URL. Установщики — в release.yml
|
||||||
# (`pnpm --filter @pipu/desktop bundle`): на раннере нет Windows и WebKitGTK.
|
# (Linux AppImage/deb и Windows NSIS через cargo-xwin на ubuntu-latest).
|
||||||
name: ci / desktop
|
name: ci / desktop
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N]: Gitea Release с заметками → APK.
|
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N]: Gitea Release с заметками → APK, Linux и Windows.
|
||||||
# Релиз создаётся сразу (статус Android — «собирается»); сборка APK докладывает в него файлы
|
# Релиз создаётся сразу (статусы Android/десктоп — «собирается»); сборки докладывают файлы
|
||||||
# и переписывает статус: собран / не удалась (scripts/release.mjs).
|
# и переписывают статус: собран / не удалась (scripts/release.mjs).
|
||||||
|
# Один Ubuntu-раннер: сначала APK, потом десктоп (Linux native + Windows через cargo-xwin).
|
||||||
# Docker-образы не публикуются: сервер собирает их сам из клонированного репозитория (deploy/update.sh).
|
# Docker-образы не публикуются: сервер собирает их сам из клонированного репозитория (deploy/update.sh).
|
||||||
#
|
#
|
||||||
# Секреты (Settings → Actions → Secrets):
|
# Секреты (Settings → Actions → Secrets):
|
||||||
@@ -197,3 +198,133 @@ jobs:
|
|||||||
- name: Статус в релизе — сборка отменена
|
- name: Статус в релизе — сборка отменена
|
||||||
if: cancelled()
|
if: cancelled()
|
||||||
run: node scripts/release.mjs android cancelled
|
run: node scripts/release.mjs android cancelled
|
||||||
|
|
||||||
|
# После Android: на одном хосте параллельный Tauri + Gradle валит раннер.
|
||||||
|
# Windows NSIS — cargo-xwin + makensis на этом же Ubuntu (виндового раннера нет).
|
||||||
|
desktop:
|
||||||
|
name: desktop (Linux и Windows в релиз)
|
||||||
|
needs: [release, android]
|
||||||
|
if: always() && needs.release.result == 'success'
|
||||||
|
timeout-minutes: 90
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PIPU_VERSION: ${{ needs.release.outputs.version }}
|
||||||
|
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
VERSION: ${{ needs.release.outputs.version }}
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||||||
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||||
|
CARGO_BUILD_JOBS: "2"
|
||||||
|
XWIN_CACHE_DIR: /opt/ci-cache/xwin
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Статус в релизе — собирается
|
||||||
|
run: |
|
||||||
|
node scripts/release.mjs desktop linux building
|
||||||
|
node scripts/release.mjs desktop windows building
|
||||||
|
- name: Rust
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||||
|
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \
|
||||||
|
libdbus-1-dev libsecret-1-dev patchelf libfuse2 \
|
||||||
|
nsis lld llvm clang
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index
|
||||||
|
~/.cargo/registry/cache
|
||||||
|
~/.cargo/git/db
|
||||||
|
~/.cargo/bin/cargo-xwin
|
||||||
|
key: cargo-desktop-${{ hashFiles('apps/desktop/src-tauri/Cargo.toml') }}
|
||||||
|
restore-keys: cargo-desktop-
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- name: Сборка AppImage и deb
|
||||||
|
id: linux
|
||||||
|
working-directory: apps/desktop
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
pnpm bundle:linux
|
||||||
|
bundle=src-tauri/target/release/bundle
|
||||||
|
shopt -s nullglob
|
||||||
|
for f in "$bundle"/appimage/*.AppImage; do
|
||||||
|
cp "$f" "$GITHUB_WORKSPACE/pipu-music-${PIPU_VERSION}-linux.AppImage"
|
||||||
|
done
|
||||||
|
for f in "$bundle"/deb/*.deb; do
|
||||||
|
cp "$f" "$GITHUB_WORKSPACE/pipu-music-${PIPU_VERSION}-linux.deb"
|
||||||
|
done
|
||||||
|
ls -lh "$GITHUB_WORKSPACE"/pipu-music-${PIPU_VERSION}-linux.*
|
||||||
|
rm -rf src-tauri/target/release
|
||||||
|
- name: Linux в релиз
|
||||||
|
id: linux-upload
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
files=()
|
||||||
|
for f in pipu-music-${PIPU_VERSION}-linux.AppImage pipu-music-${PIPU_VERSION}-linux.deb; do
|
||||||
|
[ -f "$f" ] && files+=("$f")
|
||||||
|
done
|
||||||
|
if [ ${#files[@]} -eq 0 ]; then
|
||||||
|
echo "::error::нет AppImage и deb после сборки"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
node scripts/release.mjs desktop linux success "${files[@]}"
|
||||||
|
- name: Windows toolchain
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
if ! command -v cargo-xwin >/dev/null; then
|
||||||
|
cargo install --locked cargo-xwin
|
||||||
|
fi
|
||||||
|
if [ ! -d /opt/ci-cache ]; then
|
||||||
|
echo "XWIN_CACHE_DIR=$HOME/.cache/xwin" >> "$GITHUB_ENV"
|
||||||
|
mkdir -p "$HOME/.cache/xwin"
|
||||||
|
else
|
||||||
|
mkdir -p /opt/ci-cache/xwin
|
||||||
|
fi
|
||||||
|
- name: Сборка NSIS
|
||||||
|
id: win
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
working-directory: apps/desktop
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
pnpm bundle:windows:cross
|
||||||
|
bundle=src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis
|
||||||
|
shopt -s nullglob
|
||||||
|
copied=
|
||||||
|
for f in "$bundle"/*-setup.exe; do
|
||||||
|
cp "$f" "$GITHUB_WORKSPACE/pipu-music-${PIPU_VERSION}-windows-setup.exe"
|
||||||
|
copied=1
|
||||||
|
done
|
||||||
|
if [ -z "$copied" ]; then
|
||||||
|
echo "::error::нет NSIS setup.exe после сборки в $bundle"
|
||||||
|
ls -la "$bundle" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ls -lh "$GITHUB_WORKSPACE/pipu-music-${PIPU_VERSION}-windows-setup.exe"
|
||||||
|
- name: Windows в релиз
|
||||||
|
id: win-upload
|
||||||
|
if: ${{ !cancelled() && steps.win.outcome == 'success' }}
|
||||||
|
run: node scripts/release.mjs desktop windows success "pipu-music-${PIPU_VERSION}-windows-setup.exe"
|
||||||
|
- name: Статус Linux — сборка не удалась
|
||||||
|
if: failure() && steps.linux-upload.outcome != 'success'
|
||||||
|
run: node scripts/release.mjs desktop linux failure
|
||||||
|
- name: Статус Windows — сборка не удалась
|
||||||
|
if: failure() && steps.win-upload.outcome != 'success'
|
||||||
|
run: node scripts/release.mjs desktop windows failure
|
||||||
|
- name: Статус в релизе — сборка отменена
|
||||||
|
if: cancelled()
|
||||||
|
run: |
|
||||||
|
node scripts/release.mjs desktop linux cancelled
|
||||||
|
node scripts/release.mjs desktop windows cancelled
|
||||||
@@ -5,12 +5,14 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const root = dirname(fileURLToPath(import.meta.url));
|
const root = dirname(fileURLToPath(import.meta.url));
|
||||||
const conf = JSON.parse(readFileSync(resolve(root, 'src-tauri/tauri.conf.json'), 'utf8'));
|
const conf = JSON.parse(readFileSync(resolve(root, 'src-tauri/tauri.conf.json'), 'utf8'));
|
||||||
let version = '0.0.0';
|
let version = process.env.PIPU_VERSION ?? '0.0.0';
|
||||||
try {
|
if (!process.env.PIPU_VERSION) {
|
||||||
const tag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], { encoding: 'utf8' }).trim();
|
try {
|
||||||
version = tag.replace(/^v/, '').replace(/-rc\.\d+$/, '');
|
const tag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], { encoding: 'utf8' }).trim();
|
||||||
} catch {
|
version = tag.replace(/^v/, '');
|
||||||
// local untagged build
|
} catch {
|
||||||
|
// local untagged build
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conf.version = version;
|
conf.version = version;
|
||||||
writeFileSync(resolve(root, 'src-tauri/tauri.release.json'), JSON.stringify(conf, null, 2));
|
writeFileSync(resolve(root, 'src-tauri/tauri.release.json'), JSON.stringify(conf, null, 2));
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"bundle": "node inject-version.mjs && tauri build --config src-tauri/tauri.release.json",
|
"bundle": "node inject-version.mjs && tauri build --config src-tauri/tauri.release.json",
|
||||||
|
"bundle:linux": "node inject-version.mjs && tauri build --config src-tauri/tauri.release.json --bundles appimage,deb",
|
||||||
|
"bundle:windows": "node inject-version.mjs && tauri build --config src-tauri/tauri.release.json --bundles nsis",
|
||||||
|
"bundle:windows:cross": "node inject-version.mjs && tauri build --config src-tauri/tauri.release.json --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"icons": "tauri icon ../web/public/favicon.svg"
|
"icons": "tauri icon ../web/public/favicon.svg"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,5 +38,7 @@ keyring = { version = "3.6.3", features = ["apple-native"] }
|
|||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
lto = "thin"
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
opt-level = "s"
|
||||||
|
panic = "abort"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.ico", "icons/icon.icns"],
|
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.ico", "icons/icon.icns"],
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": { "depends": ["libwebkit2gtk-4.1-0", "libayatana-appindicator3-1", "libsecret-1-0", "gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad", "gstreamer1.0-libav"] },
|
"deb": { "depends": ["libwebkit2gtk-4.1-0", "libayatana-appindicator3-1", "libsecret-1-0", "gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad", "gstreamer1.0-libav"] },
|
||||||
"appimage": { "bundleMediaFramework": true }
|
"appimage": { "bundleMediaFramework": false }
|
||||||
},
|
},
|
||||||
"windows": { "nsis": { "installMode": "currentUser", "languages": ["English", "Russian"], "displayLanguageSelector": true } }
|
"windows": { "nsis": { "installMode": "currentUser", "languages": ["English", "Russian"], "displayLanguageSelector": true } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,8 @@ a.source:hover {
|
|||||||
|
|
||||||
@media (max-width: 359px) {
|
@media (max-width: 359px) {
|
||||||
.now > :first-child { display: none; }
|
.now > :first-child { display: none; }
|
||||||
|
/* share + очередь + три кнопки управления не оставляют месту название трека (e2e 320px). */
|
||||||
|
.shareButton { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.shareStatus {
|
.shareStatus {
|
||||||
|
|||||||
+3
-2
@@ -103,8 +103,9 @@
|
|||||||
|
|
||||||
Android-приложение обновляется само: при запуске оно сверяется с сервером и предлагает новый APK.
|
Android-приложение обновляется само: при запуске оно сверяется с сервером и предлагает новый APK.
|
||||||
Раздать приложение — ссылка `https://<ваш-домен>/download` (без входа; `/android` перенаправляет сюда):
|
Раздать приложение — ссылка `https://<ваш-домен>/download` (без входа; `/android` перенаправляет сюда):
|
||||||
сборка под систему посетителя (APK, Windows, Linux, macOS), остальные платформы ниже, адрес сервера
|
сборка под систему посетителя, остальные платформы ниже, адрес сервера для копирования и короткая инструкция.
|
||||||
для копирования и короткая инструкция. Та же страница — внизу экрана входа и в меню профиля.
|
APK, Linux (AppImage, deb) и Windows (NSIS) прикладываются к релизу Gitea автоматически. Та же страница — внизу экрана входа
|
||||||
|
и в меню профиля.
|
||||||
|
|
||||||
## 7. Наблюдение
|
## 7. Наблюдение
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -12,6 +12,6 @@ pnpm --filter @pipu/desktop dev
|
|||||||
pnpm --filter @pipu/desktop bundle
|
pnpm --filter @pipu/desktop bundle
|
||||||
```
|
```
|
||||||
|
|
||||||
На Windows получается NSIS-установщик, на Linux — AppImage, deb и rpm. Зависимости Linux: WebKitGTK 4.1, AppIndicator, libsecret, GStreamer.
|
На Windows получается NSIS-установщик, на Linux — AppImage, deb и rpm. Зависимости Linux: WebKitGTK 4.1, AppIndicator, libsecret, GStreamer (плеер идёт через WebKit, в AppImage GStreamer не упаковываем — иначе файл раздувается на десятки мегабайт).
|
||||||
|
|
||||||
Готовые файлы с последнего релиза Gitea сервер отдаёт на публичной странице `/download`: посетителю сразу предлагается сборка его ОС, остальные платформы и Android — ниже.
|
Релиз по тегу собирает всё на Linux-раннере: AppImage/`.deb` и Windows NSIS через `cargo-xwin` (`pipu-music-<версия>-windows-setup.exe`). Подпись Windows-установщика на Linux нет. Страница `/download` на сервере отдаёт те файлы, что есть в последнем релизе Gitea.
|
||||||
+100
-15
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { basename, resolve } from 'node:path';
|
import { basename, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@@ -29,6 +29,12 @@ const COMMIT_RE = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
|
|||||||
|
|
||||||
export const STATUS_START = '<!-- android-status -->';
|
export const STATUS_START = '<!-- android-status -->';
|
||||||
export const STATUS_END = '<!-- /android-status -->';
|
export const STATUS_END = '<!-- /android-status -->';
|
||||||
|
export const DESKTOP_PLATFORMS = ['linux', 'windows'];
|
||||||
|
|
||||||
|
export function desktopStatusMarks(platform) {
|
||||||
|
if (!DESKTOP_PLATFORMS.includes(platform)) throw new Error(`unknown desktop platform: ${platform}`);
|
||||||
|
return { start: `<!-- desktop-${platform}-status -->`, end: `<!-- /desktop-${platform}-status -->` };
|
||||||
|
}
|
||||||
|
|
||||||
/** Раскладывает коммиты (Conventional Commits) по разделам заметок. */
|
/** Раскладывает коммиты (Conventional Commits) по разделам заметок. */
|
||||||
export function groupCommits(commits) {
|
export function groupCommits(commits) {
|
||||||
@@ -76,16 +82,62 @@ export function renderAndroidStatus({ status, runUrl, versionCode, apk, signing
|
|||||||
return [STATUS_START, ...lines.map((l) => `> ${l}`), STATUS_END].join('\n');
|
return [STATUS_START, ...lines.map((l) => `> ${l}`), STATUS_END].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заменяет блок статуса в тексте релиза (или дописывает его в начало, если блока нет). */
|
/** Блок статуса сборки десктопа в тексте релиза (отдельный для linux и windows). */
|
||||||
export function replaceAndroidStatus(body, block) {
|
export function renderDesktopStatus({ platform, status, runUrl, files }) {
|
||||||
const start = body.indexOf(STATUS_START);
|
const label = platform === 'windows' ? 'Windows' : 'Linux';
|
||||||
const end = body.indexOf(STATUS_END);
|
const pending = platform === 'windows' ? 'NSIS-установщик' : 'AppImage и .deb';
|
||||||
if (start === -1 || end === -1 || end < start) return `${block}\n\n${body}`;
|
const lines =
|
||||||
return body.slice(0, start) + block + body.slice(end + STATUS_END.length);
|
status === 'success'
|
||||||
|
? [
|
||||||
|
`✅ **Десктоп (${label}):** собрано — ${files.map((f) => `\`${f.name}\` (${formatSize(f.size)})`).join(', ')}.`,
|
||||||
|
]
|
||||||
|
: status === 'cancelled'
|
||||||
|
? [
|
||||||
|
`⏹ **Десктоп (${label}):** сборка отменена — [лог сборки](${runUrl}).`,
|
||||||
|
'Файлы появятся здесь после перезапуска сборки.',
|
||||||
|
]
|
||||||
|
: status === 'failure'
|
||||||
|
? [
|
||||||
|
`❌ **Десктоп (${label}):** сборка не удалась — [лог сборки](${runUrl}).`,
|
||||||
|
'Остальные клиенты от этого не зависят; установщик появится после перезапуска сборки.',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`⏳ **Десктоп (${label}):** собирается — [ход сборки](${runUrl}).`,
|
||||||
|
`${pending} появится в этом релизе автоматически.`,
|
||||||
|
];
|
||||||
|
const { start, end } = desktopStatusMarks(platform);
|
||||||
|
return [start, ...lines.map((l) => `> ${l}`), end].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Текст релиза: статус Android, изменения, обновление сервера, установка APK. */
|
function replaceBlock(body, startMark, endMark, block) {
|
||||||
export function renderNotes({ version, tag, prevTag, prerelease, repoUrl, groups, androidStatus }) {
|
const start = body.indexOf(startMark);
|
||||||
|
const end = body.indexOf(endMark);
|
||||||
|
if (start === -1 || end === -1 || end < start) return `${block}\n\n${body}`;
|
||||||
|
return body.slice(0, start) + block + body.slice(end + endMark.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Заменяет блок статуса Android в тексте релиза (или дописывает его в начало, если блока нет). */
|
||||||
|
export function replaceAndroidStatus(body, block) {
|
||||||
|
return replaceBlock(body, STATUS_START, STATUS_END, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceDesktopStatus(body, platform, block) {
|
||||||
|
const { start, end } = desktopStatusMarks(platform);
|
||||||
|
return replaceBlock(body, start, end, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Текст релиза: статусы сборок, изменения, обновление сервера, установка клиентов. */
|
||||||
|
export function renderNotes({
|
||||||
|
version,
|
||||||
|
tag,
|
||||||
|
prevTag,
|
||||||
|
prerelease,
|
||||||
|
repoUrl,
|
||||||
|
groups,
|
||||||
|
androidStatus,
|
||||||
|
desktopLinuxStatus,
|
||||||
|
desktopWindowsStatus,
|
||||||
|
}) {
|
||||||
const link = (it) => `([${it.sha.slice(0, 7)}](${repoUrl}/commit/${it.sha}))`;
|
const link = (it) => `([${it.sha.slice(0, 7)}](${repoUrl}/commit/${it.sha}))`;
|
||||||
const item = (it) => `- ${it.scope ? `**${it.scope}:** ` : ''}${it.text} ${link(it)}`;
|
const item = (it) => `- ${it.scope ? `**${it.scope}:** ` : ''}${it.text} ${link(it)}`;
|
||||||
const out = [];
|
const out = [];
|
||||||
@@ -93,7 +145,10 @@ export function renderNotes({ version, tag, prevTag, prerelease, repoUrl, groups
|
|||||||
if (prerelease) {
|
if (prerelease) {
|
||||||
out.push('> 🧪 **Предварительная версия** — для проверки перед релизом, не для основной установки.', '');
|
out.push('> 🧪 **Предварительная версия** — для проверки перед релизом, не для основной установки.', '');
|
||||||
}
|
}
|
||||||
out.push(androidStatus, '', '## Что изменилось', '');
|
out.push(androidStatus, '');
|
||||||
|
if (desktopLinuxStatus) out.push(desktopLinuxStatus, '');
|
||||||
|
if (desktopWindowsStatus) out.push(desktopWindowsStatus, '');
|
||||||
|
out.push('## Что изменилось', '');
|
||||||
if (groups.breaking.length) {
|
if (groups.breaking.length) {
|
||||||
out.push('### ⚠️ Несовместимые изменения', '', ...groups.breaking.map(item), '');
|
out.push('### ⚠️ Несовместимые изменения', '', ...groups.breaking.map(item), '');
|
||||||
}
|
}
|
||||||
@@ -124,11 +179,14 @@ export function renderNotes({ version, tag, prevTag, prerelease, repoUrl, groups
|
|||||||
'',
|
'',
|
||||||
`Ожидается \`"version":"${version}"\`.`,
|
`Ожидается \`"version":"${version}"\`.`,
|
||||||
'',
|
'',
|
||||||
'## Android',
|
'## Клиенты',
|
||||||
'',
|
'',
|
||||||
`- **Установка или обновление:** скачайте \`pipu-music-${version}.apk\` ниже и откройте его на телефоне;`,
|
'Скачайте с этой страницы или с `/download` на сервере (подскажет сборку под вашу систему).',
|
||||||
' Android попросит разрешить установку из этого источника. Обновление ставится поверх, данные сохраняются.',
|
'',
|
||||||
`- **Проверка файла:** \`sha256sum pipu-music-${version}.apk\` должен совпасть с \`pipu-music-${version}.apk.sha256\`.`,
|
`- **Android:** \`pipu-music-${version}.apk\` — откройте на телефоне; Android попросит разрешить установку из этого источника.`,
|
||||||
|
`- **Windows:** \`pipu-music-${version}-windows-setup.exe\`.`,
|
||||||
|
`- **Linux:** \`pipu-music-${version}-linux.AppImage\` (сделайте исполняемым) или \`.deb\`.`,
|
||||||
|
`- **Проверка APK:** \`sha256sum pipu-music-${version}.apk\` должен совпасть с \`pipu-music-${version}.apk.sha256\`.`,
|
||||||
'',
|
'',
|
||||||
`Первая установка сервера — [инструкция](${repoUrl}/src/tag/${tag}/docs/install-guide.md).`,
|
`Первая установка сервера — [инструкция](${repoUrl}/src/tag/${tag}/docs/install-guide.md).`,
|
||||||
'',
|
'',
|
||||||
@@ -202,6 +260,8 @@ async function create() {
|
|||||||
repoUrl,
|
repoUrl,
|
||||||
groups: groupCommits(commitsBetween(prevTag, tag)),
|
groups: groupCommits(commitsBetween(prevTag, tag)),
|
||||||
androidStatus: renderAndroidStatus({ status: 'building', runUrl }),
|
androidStatus: renderAndroidStatus({ status: 'building', runUrl }),
|
||||||
|
desktopLinuxStatus: renderDesktopStatus({ platform: 'linux', status: 'building', runUrl }),
|
||||||
|
desktopWindowsStatus: renderDesktopStatus({ platform: 'windows', status: 'building', runUrl }),
|
||||||
});
|
});
|
||||||
const fields = { name: `Pipu Music ${env('VERSION')}`, body, prerelease, draft: false };
|
const fields = { name: `Pipu Music ${env('VERSION')}`, body, prerelease, draft: false };
|
||||||
const saved = release
|
const saved = release
|
||||||
@@ -242,13 +302,38 @@ async function android(status, apkPath) {
|
|||||||
console.log(`статус Android: ${status}`);
|
console.log(`статус Android: ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function desktop(platform, status, paths) {
|
||||||
|
const { release, runUrl } = await context();
|
||||||
|
if (!release) throw new Error(`релиз для ${env('TAG')} не найден — сначала node scripts/release.mjs create`);
|
||||||
|
const files = [];
|
||||||
|
if (status === 'success') {
|
||||||
|
if (!paths.length) throw new Error('desktop success: укажите файлы');
|
||||||
|
for (const file of paths) {
|
||||||
|
const data = readFileSync(file);
|
||||||
|
files.push({ name: basename(file), size: data.length });
|
||||||
|
const sum = `${file}.sha256`;
|
||||||
|
if (!existsSync(sum)) writeFileSync(sum, `${createHash('sha256').update(data).digest('hex')}\n`);
|
||||||
|
await upload(release, file);
|
||||||
|
if (existsSync(sum)) await upload(release, sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const block = renderDesktopStatus({ platform, status, runUrl, files });
|
||||||
|
await api('PATCH', `/releases/${release.id}`, { json: { body: replaceDesktopStatus(release.body ?? '', platform, block) } });
|
||||||
|
console.log(`статус десктопа (${platform}): ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const [cmd, ...args] = process.argv.slice(2);
|
const [cmd, ...args] = process.argv.slice(2);
|
||||||
if (cmd === 'create') return create();
|
if (cmd === 'create') return create();
|
||||||
if (cmd === 'android' && ['building', 'success', 'failure', 'cancelled'].includes(args[0])) {
|
if (cmd === 'android' && ['building', 'success', 'failure', 'cancelled'].includes(args[0])) {
|
||||||
return android(args[0], args[1]);
|
return android(args[0], args[1]);
|
||||||
}
|
}
|
||||||
throw new Error('использование: release.mjs create | android building|failure|cancelled | android success <file.apk>');
|
if (cmd === 'desktop' && DESKTOP_PLATFORMS.includes(args[0]) && ['building', 'success', 'failure', 'cancelled'].includes(args[1])) {
|
||||||
|
return desktop(args[0], args[1], args.slice(2));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'использование: release.mjs create | android building|failure|cancelled | android success <file.apk> | desktop linux|windows building|failure|cancelled | desktop linux|windows success <file...>',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { test } from 'node:test';
|
|||||||
import {
|
import {
|
||||||
groupCommits,
|
groupCommits,
|
||||||
renderAndroidStatus,
|
renderAndroidStatus,
|
||||||
|
renderDesktopStatus,
|
||||||
renderNotes,
|
renderNotes,
|
||||||
replaceAndroidStatus,
|
replaceAndroidStatus,
|
||||||
|
replaceDesktopStatus,
|
||||||
|
desktopStatusMarks,
|
||||||
STATUS_END,
|
STATUS_END,
|
||||||
STATUS_START,
|
STATUS_START,
|
||||||
} from './release.mjs';
|
} from './release.mjs';
|
||||||
@@ -77,6 +80,34 @@ test('adds a status block when the body has none', () => {
|
|||||||
assert.ok(replaceAndroidStatus('hand-written notes', block).startsWith(block));
|
assert.ok(replaceAndroidStatus('hand-written notes', block).startsWith(block));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('desktop status blocks', () => {
|
||||||
|
const linuxMarks = desktopStatusMarks('linux');
|
||||||
|
const winMarks = desktopStatusMarks('windows');
|
||||||
|
const building = renderDesktopStatus({ platform: 'linux', status: 'building', runUrl: 'https://run' });
|
||||||
|
assert.ok(building.startsWith(linuxMarks.start) && building.endsWith(linuxMarks.end));
|
||||||
|
assert.match(building, /Десктоп \(Linux\):.*собирается — \[ход сборки\]\(https:\/\/run\)/);
|
||||||
|
const ok = renderDesktopStatus({
|
||||||
|
platform: 'linux',
|
||||||
|
status: 'success',
|
||||||
|
files: [{ name: 'pipu-music-1.4.0-linux.AppImage', size: 40 * 1024 * 1024 }],
|
||||||
|
});
|
||||||
|
assert.match(ok, /`pipu-music-1\.4\.0-linux\.AppImage` \(40,0 МБ\)/);
|
||||||
|
assert.equal(replaceDesktopStatus(`a\n\n${building}\n\nb`, 'linux', ok).includes(building), false);
|
||||||
|
|
||||||
|
const winBuilding = renderDesktopStatus({ platform: 'windows', status: 'building', runUrl: 'https://run' });
|
||||||
|
assert.ok(winBuilding.startsWith(winMarks.start) && winBuilding.endsWith(winMarks.end));
|
||||||
|
const winOk = renderDesktopStatus({
|
||||||
|
platform: 'windows',
|
||||||
|
status: 'success',
|
||||||
|
files: [{ name: 'pipu-music-1.4.0-windows-setup.exe', size: 12 * 1024 * 1024 }],
|
||||||
|
});
|
||||||
|
const both = `${building}\n\n${winBuilding}`;
|
||||||
|
const patched = replaceDesktopStatus(both, 'windows', winOk);
|
||||||
|
assert.match(patched, /Linux/);
|
||||||
|
assert.match(patched, /windows-setup\.exe/);
|
||||||
|
assert.doesNotMatch(patched, /Десктоп \(Windows\):.*собирается/);
|
||||||
|
});
|
||||||
|
|
||||||
test('release notes', () => {
|
test('release notes', () => {
|
||||||
const notes = renderNotes({
|
const notes = renderNotes({
|
||||||
version: '0.2.0',
|
version: '0.2.0',
|
||||||
@@ -86,12 +117,16 @@ test('release notes', () => {
|
|||||||
repoUrl,
|
repoUrl,
|
||||||
groups: groupCommits([{ sha: sha(1), subject: 'feat(web): library screen' }]),
|
groups: groupCommits([{ sha: sha(1), subject: 'feat(web): library screen' }]),
|
||||||
androidStatus: renderAndroidStatus({ status: 'building', runUrl: 'https://run' }),
|
androidStatus: renderAndroidStatus({ status: 'building', runUrl: 'https://run' }),
|
||||||
|
desktopLinuxStatus: renderDesktopStatus({ platform: 'linux', status: 'building', runUrl: 'https://run' }),
|
||||||
|
desktopWindowsStatus: renderDesktopStatus({ platform: 'windows', status: 'building', runUrl: 'https://run' }),
|
||||||
});
|
});
|
||||||
assert.match(notes, /### Новое\n\n- \*\*web:\*\* library screen \(\[1111111\]\(https:\/\/git\.example\/owner\/pipu-music\/commit\/1{40}\)\)/);
|
assert.match(notes, /### Новое\n\n- \*\*web:\*\* library screen \(\[1111111\]\(https:\/\/git\.example\/owner\/pipu-music\/commit\/1{40}\)\)/);
|
||||||
assert.match(notes, /\[v0\.1\.7\.\.\.v0\.2\.0\]\(https:\/\/git\.example\/owner\/pipu-music\/compare\/v0\.1\.7\.\.\.v0\.2\.0\)/);
|
assert.match(notes, /\[v0\.1\.7\.\.\.v0\.2\.0\]\(https:\/\/git\.example\/owner\/pipu-music\/compare\/v0\.1\.7\.\.\.v0\.2\.0\)/);
|
||||||
assert.match(notes, /\.\/update\.sh v0\.2\.0/);
|
assert.match(notes, /\.\/update\.sh v0\.2\.0/);
|
||||||
assert.match(notes, /"version":"0\.2\.0"/);
|
assert.match(notes, /"version":"0\.2\.0"/);
|
||||||
assert.match(notes, /pipu-music-0\.2\.0\.apk/);
|
assert.match(notes, /pipu-music-0\.2\.0\.apk/);
|
||||||
|
assert.match(notes, /pipu-music-0\.2\.0-windows-setup\.exe/);
|
||||||
|
assert.match(notes, /pipu-music-0\.2\.0-linux\.AppImage/);
|
||||||
assert.match(notes, /src\/tag\/v0\.2\.0\/docs\/install-guide\.md/);
|
assert.match(notes, /src\/tag\/v0\.2\.0\/docs\/install-guide\.md/);
|
||||||
assert.doesNotMatch(notes, /Предварительная версия/);
|
assert.doesNotMatch(notes, /Предварительная версия/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user