From 935125a83ed2b4dd65e4afe93a18d26f5d5ab123 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Thu, 5 Mar 2026 00:49:30 +0100 Subject: [PATCH] chore: switch updater and release flow to gitea --- package.json | 5 +- scripts/release_gitea.mjs | 159 ++++++++++++++++++++++++++++++++++++++ src/main.ts | 23 ++++-- 3 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 scripts/release_gitea.mjs diff --git a/package.json b/package.json index 621f0cc..7e97978 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "test:e2e:stress": "npm run test:e2e:release && npm run test:e2e:release && npm run test:e2e:release", "pack": "npm run build && electron-builder --dir", "dist": "npm run build && electron-builder", - "dist:win": "npm run test:e2e:release && electron-builder --win" + "dist:win": "npm run test:e2e:release && electron-builder --win", + "release:gitea": "node scripts/release_gitea.mjs" }, "dependencies": { "axios": "^1.6.0", @@ -50,7 +51,7 @@ }, "publish": { "provider": "generic", - "url": "https://codeberg.org/Sucukdeluxe/Twitch-VOD-Manager/releases/download/latest/" + "url": "https://git.24-music.de/Administrator/Twitch-VOD-Manager/releases/latest/download/" } } } diff --git a/scripts/release_gitea.mjs b/scripts/release_gitea.mjs new file mode 100644 index 0000000..edb3b2b --- /dev/null +++ b/scripts/release_gitea.mjs @@ -0,0 +1,159 @@ +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const NPM_EXECUTABLE = process.platform === "win32" ? "npm.cmd" : "npm"; +const BASE_URL = String(process.env.GITEA_BASE_URL || "https://git.24-music.de").replace(/\/+$/, ""); +const OWNER = String(process.env.GITEA_REPO_OWNER || "Administrator").trim(); +const REPO = String(process.env.GITEA_REPO_NAME || "Twitch-VOD-Manager").trim(); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: process.cwd(), + encoding: "utf8", + input: options.input, + stdio: options.capture ? ["pipe", "pipe", "pipe"] : "inherit" + }); + if (result.status !== 0) { + const stderr = String(result.stderr || "").trim(); + const stdout = String(result.stdout || "").trim(); + const details = [stderr, stdout].filter(Boolean).join("\n"); + throw new Error(`Command failed: ${command} ${args.join(" ")}${details ? `\n${details}` : ""}`); + } + return String(result.stdout || ""); +} + +function parseArgs(argv) { + const args = argv.slice(2); + if (args.includes("--help") || args.includes("-h")) { + return { help: true }; + } + const dryRun = args.includes("--dry-run"); + const version = args.find((arg) => arg !== "--dry-run") || ""; + const notes = args.filter((arg) => arg !== "--dry-run").slice(1).join(" ").trim(); + return { help: false, dryRun, version, notes }; +} + +function ensureVersion(version) { + if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version || "").trim())) { + throw new Error("Invalid version format. Expected e.g. 4.2.0"); + } + return String(version).trim(); +} + +function getAuthHeader() { + const token = String(process.env.GITEA_TOKEN || process.env.FORGEJO_TOKEN || "").trim(); + if (token) { + return `token ${token}`; + } + + const output = run("git", ["credential", "fill"], { + capture: true, + input: `protocol=https\nhost=${new URL(BASE_URL).host}\n\n` + }); + const map = new Map(); + for (const line of output.split(/\r?\n/)) { + if (!line.includes("=")) continue; + const [key, value] = line.split("=", 2); + map.set(key, value); + } + const username = map.get("username"); + const password = map.get("password"); + if (!username || !password) { + throw new Error("Missing Gitea credentials. Set GITEA_TOKEN or configure git credential helper."); + } + return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`; +} + +async function apiRequest(method, url, authHeader, body, contentType = "application/json") { + const headers = { Accept: "application/json", Authorization: authHeader }; + if (body !== undefined) headers["Content-Type"] = contentType; + const response = await fetch(url, { method, headers, body }); + const text = await response.text(); + let parsed = null; + try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; } + return { ok: response.ok, status: response.status, body: parsed }; +} + +function ensureAssets(version) { + const releaseDir = path.join(process.cwd(), "release"); + const files = [ + `Twitch-VOD-Manager-Setup-${version}.exe`, + `Twitch-VOD-Manager-Setup-${version}.exe.blockmap`, + "latest.yml" + ]; + for (const file of files) { + const fullPath = path.join(releaseDir, file); + if (!fs.existsSync(fullPath)) { + throw new Error(`Missing release artifact: ${fullPath}`); + } + } + return { releaseDir, files }; +} + +async function createOrGetRelease(baseApi, tag, authHeader, notes) { + const existing = await apiRequest("GET", `${baseApi}/releases/tags/${encodeURIComponent(tag)}`, authHeader); + if (existing.ok) return existing.body; + const payload = { + tag_name: tag, + target_commitish: "main", + name: tag, + body: notes || `Release ${tag}`, + draft: false, + prerelease: false + }; + const created = await apiRequest("POST", `${baseApi}/releases`, authHeader, JSON.stringify(payload)); + if (!created.ok) { + throw new Error(`Failed to create release (${created.status}): ${JSON.stringify(created.body)}`); + } + return created.body; +} + +async function uploadAssets(baseApi, releaseId, authHeader, releaseDir, files) { + for (const fileName of files) { + const filePath = path.join(releaseDir, fileName); + const fileData = fs.readFileSync(filePath); + const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`; + const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream"); + if (response.ok || response.status === 409 || response.status === 422) { + continue; + } + throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`); + } +} + +async function main() { + const args = parseArgs(process.argv); + if (args.help) { + process.stdout.write("Usage: npm run release:gitea -- [release notes] [--dry-run]\n"); + process.stdout.write("Env: GITEA_BASE_URL, GITEA_REPO_OWNER, GITEA_REPO_NAME, GITEA_TOKEN\n"); + return; + } + + const version = ensureVersion(args.version); + const tag = `v${version}`; + const authHeader = getAuthHeader(); + const baseApi = `${BASE_URL}/api/v1/repos/${OWNER}/${REPO}`; + + run("git", ["fetch", "--tags"]); + if (!args.dryRun) { + run("git", ["push", "origin", "main"]); + run("git", ["push", "origin", tag]); + } + + run(NPM_EXECUTABLE, ["run", "dist:win"]); + const assets = ensureAssets(version); + if (args.dryRun) { + process.stdout.write(`Dry run complete for ${tag}\n`); + return; + } + + const release = await createOrGetRelease(baseApi, tag, authHeader, args.notes); + await uploadAssets(baseApi, release.id, authHeader, assets.releaseDir, assets.files); + process.stdout.write(`Release published: ${release.html_url || `${BASE_URL}/${OWNER}/${REPO}/releases/tag/${tag}`}\n`); +} + +main().catch((error) => { + process.stderr.write(`${String(error?.message || error)}\n`); + process.exit(1); +}); diff --git a/src/main.ts b/src/main.ts index 4221669..fa81d3d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,8 +9,13 @@ import { compareUpdateVersions, isNewerUpdateVersion, normalizeUpdateVersion } f // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '4.1.13'; +const APP_VERSION = app.getVersion(); const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; +const GITEA_BASE_URL = (process.env.GITEA_BASE_URL || 'https://git.24-music.de').replace(/\/+$/, ''); +const GITEA_REPO_OWNER = process.env.GITEA_REPO_OWNER || 'Administrator'; +const GITEA_REPO_NAME = process.env.GITEA_REPO_NAME || 'Twitch-VOD-Manager'; +const GITEA_RELEASES_API_LATEST_URL = `${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/releases/latest`; +const GITEA_RELEASES_DOWNLOAD_BASE_URL = `${GITEA_BASE_URL}/${GITEA_REPO_OWNER}/${GITEA_REPO_NAME}/releases/download`; // Paths const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager'); @@ -2969,19 +2974,23 @@ async function requestUpdateCheck(source: UpdateCheckSource, force = false): Pro try { try { - const codebergRes = await axios.get('https://codeberg.org/api/v1/repos/Sucukdeluxe/Twitch-VOD-Manager/releases/latest', { - timeout: 5000 + const giteaRes = await axios.get(GITEA_RELEASES_API_LATEST_URL, { + timeout: 5000, + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Twitch-VOD-Manager' + } }); - const tagName = codebergRes.data?.tag_name; + const tagName = giteaRes.data?.tag_name; if (tagName) { autoUpdater.setFeedURL({ provider: 'generic', - url: `https://codeberg.org/Sucukdeluxe/Twitch-VOD-Manager/releases/download/${tagName}` + url: `${GITEA_RELEASES_DOWNLOAD_BASE_URL}/${tagName}` }); - appendDebugLog('codeberg-feed-url-set', tagName); + appendDebugLog('gitea-feed-url-set', { tagName, owner: GITEA_REPO_OWNER, repo: GITEA_REPO_NAME }); } } catch (apiErr) { - appendDebugLog('codeberg-api-failed', String(apiErr)); + appendDebugLog('gitea-api-failed', String(apiErr)); } let timeoutHandle: NodeJS.Timeout | null = null;