948f15d44a
tauri-apps/tauri-action жёстко ходит в api.github.com для создания релиза и загрузки апдейтер-артефактов — не настраиваемый эндпоинт, для Gitea не годится ни при какой замене секретов. Вместо неё: - scripts/gitea-publish-release.mjs — публикует релиз через прямые вызовы Gitea Release API: собирает latest.json (формат манифеста Tauri v2 updater) из .sig-файлов подписанных бандлов, создаёт версионный релиз (тег = версия, для истории) и пересоздаёт релиз "latest" — у Gitea нет built-in алиаса /releases/latest/download/<file> как у GitHub, поэтому тег "latest" удаляется и создаётся заново на каждом релизе с теми же ассетами, чтобы URL апдейтера был стабильным. Оба релиза удаляются-и-пересоздаются — идемпотентно к повторному прогону тега. - .gitea/workflows/release.yml — job "build" (та же матрица платформ) вызывает `tauri build` напрямую (подпись через TAURI_SIGNING_PRIVATE_KEY читает сам Tauri CLI, без разницы кто его вызывает), собирает пары <файл>+.sig в dist-bundle/ и грузит артефактом bundle-<updater_key>; job "publish" скачивает все артефакты и вызывает скрипт выше. - tauri.conf.json: добавлен второй updater-эндпоинт (Gitea, плейсхолдер CHANGE_ME_GITEA_SERVER — заполнить реальным адресом) рядом с существующим GitHub — апдейтер пробует оба по порядку. GitHub-версия пайплайна не тронута, продолжает работать через tauri-action как раньше. Не проверено вживую на реальном Gitea/Windows/macOS раннере — нет доступа в этой среде. Нужные секреты в Gitea: GITEA_SERVER_URL, GITEA_TOKEN (личный токен с правом записи в репозиторий), плюс уже существующие TAURI_SIGNING_PRIVATE_KEY(_PASSWORD).
168 lines
6.8 KiB
JavaScript
168 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
||
// Публикует Tauri-релиз в Gitea напрямую через Release API — вместо
|
||
// tauri-apps/tauri-action, который жёстко ходит в api.github.com (не
|
||
// настраиваемый эндпоинт, для Gitea не годится). Используется только в
|
||
// .gitea/workflows/release.yml, job "publish".
|
||
//
|
||
// Схема:
|
||
// 1. Job "build" (матрица платформ) собирает бандлы через `tauri build`
|
||
// напрямую (не через tauri-action) и складывает каждую подписанную пару
|
||
// <файл>+<файл>.sig в dist-bundle/, которая грузится как build-артефакт
|
||
// с именем bundle-<updater_key> (updater_key — "darwin-aarch64" и т.п.,
|
||
// см. matrix в workflow).
|
||
// 2. Этот скрипт (job "publish") скачивает все артефакты, находит пары
|
||
// <файл>+.sig, собирает latest.json (формат манифеста апдейтера Tauri v2,
|
||
// см. https://v2.tauri.app/plugin/updater/) и публикует:
|
||
// - версионный релиз (тег = версия, для истории/changelog в Gitea UI);
|
||
// - релиз "latest" — Gitea, в отличие от GitHub, не имеет built-in алиаса
|
||
// /releases/latest/download/<file>, поэтому эмулируем его вручную:
|
||
// тег "latest" удаляется и пересоздаётся на каждом релизе с теми же
|
||
// ассетами, что и версионный — так у апдейтера в tauri.conf.json
|
||
// стабильный URL, не привязанный к конкретной версии.
|
||
//
|
||
// Оба релиза удаляются-и-пересоздаются при каждом запуске (idempotent —
|
||
// повторный прогон того же тега не падает на "уже существует").
|
||
|
||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||
import { join, basename } from "node:path";
|
||
|
||
const GITEA_SERVER_URL = requireEnv("GITEA_SERVER_URL").replace(/\/$/, "");
|
||
const GITEA_TOKEN = requireEnv("GITEA_TOKEN");
|
||
const REPO = requireEnv("GITEA_REPO"); // "owner/name"
|
||
const VERSION = requireEnv("RELEASE_VERSION"); // напр. "v1.2.3"
|
||
const ARTIFACTS_DIR = process.argv[2] || "artifacts";
|
||
|
||
function requireEnv(name) {
|
||
const v = process.env[name];
|
||
if (!v) throw new Error(`Переменная окружения ${name} обязательна`);
|
||
return v;
|
||
}
|
||
|
||
function apiUrl(path) {
|
||
return `${GITEA_SERVER_URL}/api/v1/repos/${REPO}${path}`;
|
||
}
|
||
|
||
async function giteaFetch(path, options = {}) {
|
||
const res = await fetch(apiUrl(path), {
|
||
...options,
|
||
headers: {
|
||
Authorization: `token ${GITEA_TOKEN}`,
|
||
...(options.headers || {}),
|
||
},
|
||
});
|
||
if (!res.ok && res.status !== 404) {
|
||
const text = await res.text().catch(() => "");
|
||
throw new Error(`Gitea API ${path} -> ${res.status}: ${text}`);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
/** Рекурсивно находит все пары <файл>+<файл>.sig под dir. */
|
||
function collectSigPairs(dir) {
|
||
const pairs = [];
|
||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||
const full = join(dir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
pairs.push(...collectSigPairs(full));
|
||
continue;
|
||
}
|
||
if (entry.name.endsWith(".sig")) {
|
||
pairs.push({ sigPath: full, artifactPath: full.slice(0, -4), dir });
|
||
}
|
||
}
|
||
return pairs;
|
||
}
|
||
|
||
/** Удаляет (если есть) и заново создаёт релиз на теге — гарантирует idempotent-повтор. */
|
||
async function ensureRelease(tag) {
|
||
const existing = await giteaFetch(`/releases/tags/${tag}`);
|
||
if (existing.status === 200) {
|
||
const { id } = await existing.json();
|
||
await giteaFetch(`/releases/${id}`, { method: "DELETE" });
|
||
}
|
||
// Релиз и git-тег в Gitea — разные сущности; удаление релиза не удаляет тег.
|
||
await giteaFetch(`/tags/${tag}`, { method: "DELETE" });
|
||
|
||
const created = await giteaFetch(`/releases`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
tag_name: tag,
|
||
name: tag,
|
||
draft: false,
|
||
prerelease: false,
|
||
target_commitish: process.env.GITHUB_SHA || "main",
|
||
}),
|
||
});
|
||
if (!created.ok) {
|
||
throw new Error(`Не удалось создать релиз ${tag}: HTTP ${created.status}`);
|
||
}
|
||
return created.json();
|
||
}
|
||
|
||
async function uploadAsset(releaseId, filePath, fileName) {
|
||
const data = readFileSync(filePath);
|
||
const form = new FormData();
|
||
form.append("attachment", new Blob([data]), fileName);
|
||
const res = await giteaFetch(
|
||
`/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`,
|
||
{ method: "POST", body: form },
|
||
);
|
||
if (!res.ok) {
|
||
throw new Error(`Не удалось загрузить ${fileName}: HTTP ${res.status}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
async function main() {
|
||
const pairs = collectSigPairs(ARTIFACTS_DIR);
|
||
if (pairs.length === 0) {
|
||
throw new Error(
|
||
`Не найдено ни одной пары <файл>+.sig в ${ARTIFACTS_DIR} — сборка не создала подписанные updater-бандлы?`,
|
||
);
|
||
}
|
||
|
||
const versionedRelease = await ensureRelease(VERSION);
|
||
const latestRelease = await ensureRelease("latest");
|
||
|
||
const platforms = {};
|
||
|
||
for (const { sigPath, artifactPath, dir } of pairs) {
|
||
const signature = readFileSync(sigPath, "utf-8").trim();
|
||
const fileName = basename(artifactPath);
|
||
// Папка артефакта matrix-job'а называется bundle-<updater_key>, см. workflow.
|
||
const updaterKey = basename(dir).replace(/^bundle-/, "");
|
||
|
||
console.log(`→ ${updaterKey}: ${fileName}`);
|
||
|
||
await uploadAsset(versionedRelease.id, artifactPath, fileName);
|
||
await uploadAsset(latestRelease.id, artifactPath, fileName);
|
||
|
||
platforms[updaterKey] = {
|
||
signature,
|
||
// Всегда через тег "latest" — стабильный URL независимо от версии.
|
||
url: `${GITEA_SERVER_URL}/${REPO}/releases/download/latest/${fileName}`,
|
||
};
|
||
}
|
||
|
||
const manifest = {
|
||
version: VERSION.replace(/^v/, ""),
|
||
notes: `Netrunner VPN ${VERSION}`,
|
||
pub_date: new Date().toISOString(),
|
||
platforms,
|
||
};
|
||
|
||
const manifestPath = join(ARTIFACTS_DIR, "latest.json");
|
||
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||
|
||
await uploadAsset(versionedRelease.id, manifestPath, "latest.json");
|
||
await uploadAsset(latestRelease.id, manifestPath, "latest.json");
|
||
|
||
console.log("✅ Опубликовано:\n" + JSON.stringify(manifest, null, 2));
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|