From 9cb3c79dbe375d49b33429ce84f7b6ebbbf02e56 Mon Sep 17 00:00:00 2001 From: Docker Build Date: Sun, 10 May 2026 15:38:07 -0500 Subject: [PATCH] fix(launcher): opt-in GitHub auto-update; clarify Gitea for from_release - Gate electron-updater GitHub provider on launcher_updates_from_github (default false) so GITHUB_TOKEN no longer targets the source repo without latest.yml. - Improve GitHub releases 404 hint when assets are on Gitea. - Document in README and default-launcher.json. Co-authored-by: Cursor --- tools/fractured-launcher-electron/README.md | 3 +- .../default-launcher.json | 15 +++- .../lib/auto-update.js | 69 ++++++++++++++----- .../fractured-launcher-electron/lib/github.js | 9 ++- tools/fractured-launcher-electron/main.js | 24 +++++-- 5 files changed, 96 insertions(+), 24 deletions(-) diff --git a/tools/fractured-launcher-electron/README.md b/tools/fractured-launcher-electron/README.md index 08916fd..6a424e4 100644 --- a/tools/fractured-launcher-electron/README.md +++ b/tools/fractured-launcher-electron/README.md @@ -142,7 +142,8 @@ Schema is defined by **`default-launcher.json`** (shipped in the app; first run - **`game_dir`**: WoW 3.3.5a root (contains `Wow.exe`). - **`update_feed_url`**: optional generic HTTPS base for launcher auto-update. -- **`gitea`**: **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**, **`token_env`** — when **`base_url`** is set (and owner/repo set), **`from_release`** downloads use the **Gitea** API. +- **`launcher_updates_from_github`**: default **`false`**. Only when **`true`** will a **`GITHUB_TOKEN`** (or **`github.token_env`**) enable **electron-updater**’s GitHub provider against **`github.owner` / `github.repo`**. Leave **`false`** when launcher binaries and **`latest.yml`** live on **Gitea** (use **`gitea`** + token instead) so a stray GitHub token does not produce “No published versions on GitHub”. +- **`gitea`**: **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**, **`token_env`** — when **`base_url`** is set (and owner/repo set), **`from_release`** downloads and (with token if needed) the **generic** updater feed use **Gitea**. **Required** for players if your CI mirrors patches/launchers to Gitea only. - **`github`**: used for **non-release** repo paths (`from_release`: false) and for **GitHub** **`from_release`** when **`gitea.base_url`** is empty. - **`patch_manifest`**: **`enabled`**, **`source`** (default `patch-manifest.json`), **`from_release`** — checksum-based skip + verify (see above). - **`files`**, **`realmlist`**, **`auth`**, **`launch`**. diff --git a/tools/fractured-launcher-electron/default-launcher.json b/tools/fractured-launcher-electron/default-launcher.json index d68e06b..0f40c9f 100644 --- a/tools/fractured-launcher-electron/default-launcher.json +++ b/tools/fractured-launcher-electron/default-launcher.json @@ -1,13 +1,26 @@ { "game_dir": "", "update_feed_url": "", + "launcher_updates_from_github": false, + "gitea": { + "base_url": "", + "owner": "", + "repo": "", + "release_tag": "latest", + "token_env": "GITEA_TOKEN" + }, "github": { "owner": "Dawnforger", - "repo": "Fractured-Distro", + "repo": "Fractured", "ref": "main", "release_tag": "latest", "token_env": "GITHUB_TOKEN" }, + "patch_manifest": { + "enabled": true, + "source": "patch-manifest.json", + "from_release": true + }, "files": [ { "source": "patch-Z.MPQ", diff --git a/tools/fractured-launcher-electron/lib/auto-update.js b/tools/fractured-launcher-electron/lib/auto-update.js index 735c085..c75e7a3 100644 --- a/tools/fractured-launcher-electron/lib/auto-update.js +++ b/tools/fractured-launcher-electron/lib/auto-update.js @@ -2,28 +2,51 @@ const { dialog } = require('electron'); const { autoUpdater } = require('electron-updater'); +const { useGiteaReleases, getGiteaUpdaterFeedBase } = require('./gitea-release'); /** * @param {import('electron').App} app * @param {() => import('electron').BrowserWindow | null} getMainWindow - * @param {{ updateFeedUrl?: string, githubOwner?: string, githubRepo?: string, token?: string }} opts + * @param {{ + * updateFeedUrl?: string, + * githubOwner?: string, + * githubRepo?: string, + * githubToken?: string, + * giteaToken?: string, + * allowGithubLauncherUpdates?: boolean, + * config?: object, + * }} opts */ -function setupAutoUpdater(app, getMainWindow, opts = {}) { +async function setupAutoUpdater(app, getMainWindow, opts = {}) { if (!app.isPackaged) { return { checkNow: async () => ({ skipped: true, reason: 'development build' }), }; } - autoUpdater.autoDownload = true; - autoUpdater.autoInstallOnAppQuit = true; - - const token = String(opts.token || '').trim(); + const ghToken = String(opts.githubToken || '').trim(); + const giteaTok = String(opts.giteaToken || '').trim(); const envGeneric = String(process.env.LAUNCHER_UPDATE_URL || '').trim(); const configGeneric = String(opts.updateFeedUrl || '').trim(); - const genericUrl = envGeneric || configGeneric; - const owner = String(opts.githubOwner || 'Dawnforger').trim(); - const repo = String(opts.githubRepo || 'Fractured').trim(); + let genericUrl = envGeneric || configGeneric; + let genericAuthHeader = ''; + + if (!genericUrl && opts.config && useGiteaReleases(opts.config)) { + const gfb = await getGiteaUpdaterFeedBase(opts.config); + if (gfb && gfb.url) { + genericUrl = gfb.url; + const t = String(gfb.token || giteaTok || '').trim(); + if (t) genericAuthHeader = `token ${t}`; + } + } else if (genericUrl) { + if (giteaTok) genericAuthHeader = `token ${giteaTok}`; + else if (ghToken) genericAuthHeader = `Bearer ${ghToken}`; + } + + const owner = String(opts.githubOwner || '').trim(); + const repo = String(opts.githubRepo || '').trim(); + + let feedConfigured = false; if (genericUrl) { const base = genericUrl.replace(/\/?$/, '/'); @@ -31,22 +54,37 @@ function setupAutoUpdater(app, getMainWindow, opts = {}) { provider: 'generic', url: base, }); - if (token) { + if (genericAuthHeader) { autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, - Authorization: `Bearer ${token}`, + Authorization: genericAuthHeader, }; } - } else if (token && owner && repo) { + feedConfigured = true; + } else if (opts.allowGithubLauncherUpdates && ghToken && owner && repo) { autoUpdater.setFeedURL({ provider: 'github', owner, repo, private: true, - token, + token: ghToken, }); + feedConfigured = true; } + if (!feedConfigured) { + const reason = + 'No update channel configured. Set launcher.json → update_feed_url (HTTPS folder with latest.yml), ' + + 'or fill gitea.base_url/owner/repo (+ GITEA_TOKEN for private), ' + + 'or set launcher_updates_from_github to true with GITHUB_TOKEN for private GitHub release feeds.'; + return { + checkNow: async () => ({ skipped: true, reason }), + }; + } + + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + const send = (msg) => { const w = getMainWindow(); if (w && !w.isDestroyed()) { @@ -63,9 +101,8 @@ function setupAutoUpdater(app, getMainWindow, opts = {}) { const m = (err && (err.message || String(err))) || ''; if (/404|releases\.atom|HttpError:\s*404/i.test(m)) { send( - 'Launcher update: could not read GitHub releases (404). ' + - 'If the repo is private, set GITHUB_TOKEN (or your token_env) so the launcher can authenticate, ' + - 'or set update_feed_url in launcher.json to a public HTTPS folder that contains latest.yml.' + 'Launcher update: 404 (no latest.yml or wrong URL). For Gitea use gitea.* + token, or set update_feed_url. ' + + 'For private GitHub set GITHUB_TOKEN.' ); return; } diff --git a/tools/fractured-launcher-electron/lib/github.js b/tools/fractured-launcher-electron/lib/github.js index 3734aaf..cc05077 100644 --- a/tools/fractured-launcher-electron/lib/github.js +++ b/tools/fractured-launcher-electron/lib/github.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs').promises; const { githubToken } = require('./github-token'); +const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release'); const { fetchToFile, downloadBodyToFile } = require('./http-download'); function encodeRepoPath(repoPath) { @@ -65,6 +66,9 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) { } async function downloadReleaseAsset(cfg, assetName, destPath) { + if (useGiteaReleases(cfg)) { + return downloadGiteaReleaseAsset(cfg, assetName, destPath); + } const token = githubToken(cfg); const tag = (cfg.github.release_tag || 'latest').trim() || 'latest'; const { owner, repo } = cfg.github; @@ -78,7 +82,10 @@ async function downloadReleaseAsset(cfg, assetName, destPath) { const text = await res.text(); if (!res.ok) { let hint = ''; - if (res.status === 404) hint = ' (wrong tag or private repo without token?)'; + if (res.status === 404) { + hint = + ' (wrong tag, private repo without token, or releases live on Gitea — set gitea.base_url, gitea.owner, gitea.repo in launcher.json)'; + } if (res.status === 401 || res.status === 403) hint = ' (set GITHUB_TOKEN or token_env PAT)'; throw new Error(`releases list ${res.status}${hint}: ${text.slice(0, 600)}`); } diff --git a/tools/fractured-launcher-electron/main.js b/tools/fractured-launcher-electron/main.js index a0f1df1..3473bf6 100644 --- a/tools/fractured-launcher-electron/main.js +++ b/tools/fractured-launcher-electron/main.js @@ -5,6 +5,7 @@ const path = require('path'); const { spawn } = require('child_process'); const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store'); const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch'); +const { readPatchState } = require('./lib/patch-manifest'); const { setupAutoUpdater } = require('./lib/auto-update'); let mainWindow; @@ -46,16 +47,23 @@ async function readMergedConfig() { app.whenReady().then(async () => { createWindow(); const { config } = await loadConfig(app); - const tokenEnv = config.github && config.github.token_env; - const token = - (tokenEnv && String(process.env[tokenEnv] || '').trim()) || + const ghEnv = config.github && config.github.token_env; + const githubToken = + (ghEnv && String(process.env[ghEnv] || '').trim()) || String(process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '').trim(); + const giteaEnv = config.gitea && config.gitea.token_env; + const giteaToken = + (giteaEnv && String(process.env[giteaEnv] || '').trim()) || + String(process.env.GITEA_TOKEN || '').trim(); const updateFeedUrl = String(process.env.LAUNCHER_UPDATE_URL || config.update_feed_url || '').trim(); - autoUpdateApi = setupAutoUpdater(app, () => mainWindow, { + autoUpdateApi = await setupAutoUpdater(app, () => mainWindow, { updateFeedUrl, + config, githubOwner: config.github && config.github.owner, githubRepo: config.github && config.github.repo, - token, + githubToken, + giteaToken, + allowGithubLauncherUpdates: config.launcher_updates_from_github === true, }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); @@ -68,12 +76,18 @@ app.on('window-all-closed', () => { ipcMain.handle('launcher:load', async () => { const { configPath, config } = await readMergedConfig(); + let clientBuild = ''; + if (wowInstallValid(config)) { + const st = await readPatchState(config.game_dir); + if (st && st.client_build) clientBuild = String(st.client_build); + } return { configPath, gameDir: config.game_dir || '', authEnabled: !!(config.auth && config.auth.enabled), wowExe: (config.launch && config.launch.exe) || 'Wow.exe', wowOk: wowInstallValid(config), + clientBuild, }; });