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 <cursoragent@cursor.com>
This commit is contained in:
Docker Build
2026-05-10 15:38:07 -05:00
parent 75e3b59442
commit 9cb3c79dbe
5 changed files with 96 additions and 24 deletions
+2 -1
View File
@@ -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`). - **`game_dir`**: WoW 3.3.5a root (contains `Wow.exe`).
- **`update_feed_url`**: optional generic HTTPS base for launcher auto-update. - **`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. - **`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). - **`patch_manifest`**: **`enabled`**, **`source`** (default `patch-manifest.json`), **`from_release`** — checksum-based skip + verify (see above).
- **`files`**, **`realmlist`**, **`auth`**, **`launch`**. - **`files`**, **`realmlist`**, **`auth`**, **`launch`**.
@@ -1,13 +1,26 @@
{ {
"game_dir": "", "game_dir": "",
"update_feed_url": "", "update_feed_url": "",
"launcher_updates_from_github": false,
"gitea": {
"base_url": "",
"owner": "",
"repo": "",
"release_tag": "latest",
"token_env": "GITEA_TOKEN"
},
"github": { "github": {
"owner": "Dawnforger", "owner": "Dawnforger",
"repo": "Fractured-Distro", "repo": "Fractured",
"ref": "main", "ref": "main",
"release_tag": "latest", "release_tag": "latest",
"token_env": "GITHUB_TOKEN" "token_env": "GITHUB_TOKEN"
}, },
"patch_manifest": {
"enabled": true,
"source": "patch-manifest.json",
"from_release": true
},
"files": [ "files": [
{ {
"source": "patch-Z.MPQ", "source": "patch-Z.MPQ",
@@ -2,28 +2,51 @@
const { dialog } = require('electron'); const { dialog } = require('electron');
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
const { useGiteaReleases, getGiteaUpdaterFeedBase } = require('./gitea-release');
/** /**
* @param {import('electron').App} app * @param {import('electron').App} app
* @param {() => import('electron').BrowserWindow | null} getMainWindow * @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) { if (!app.isPackaged) {
return { return {
checkNow: async () => ({ skipped: true, reason: 'development build' }), checkNow: async () => ({ skipped: true, reason: 'development build' }),
}; };
} }
autoUpdater.autoDownload = true; const ghToken = String(opts.githubToken || '').trim();
autoUpdater.autoInstallOnAppQuit = true; const giteaTok = String(opts.giteaToken || '').trim();
const token = String(opts.token || '').trim();
const envGeneric = String(process.env.LAUNCHER_UPDATE_URL || '').trim(); const envGeneric = String(process.env.LAUNCHER_UPDATE_URL || '').trim();
const configGeneric = String(opts.updateFeedUrl || '').trim(); const configGeneric = String(opts.updateFeedUrl || '').trim();
const genericUrl = envGeneric || configGeneric; let genericUrl = envGeneric || configGeneric;
const owner = String(opts.githubOwner || 'Dawnforger').trim(); let genericAuthHeader = '';
const repo = String(opts.githubRepo || 'Fractured').trim();
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) { if (genericUrl) {
const base = genericUrl.replace(/\/?$/, '/'); const base = genericUrl.replace(/\/?$/, '/');
@@ -31,22 +54,37 @@ function setupAutoUpdater(app, getMainWindow, opts = {}) {
provider: 'generic', provider: 'generic',
url: base, url: base,
}); });
if (token) { if (genericAuthHeader) {
autoUpdater.requestHeaders = { autoUpdater.requestHeaders = {
...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({ autoUpdater.setFeedURL({
provider: 'github', provider: 'github',
owner, owner,
repo, repo,
private: true, 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 send = (msg) => {
const w = getMainWindow(); const w = getMainWindow();
if (w && !w.isDestroyed()) { if (w && !w.isDestroyed()) {
@@ -63,9 +101,8 @@ function setupAutoUpdater(app, getMainWindow, opts = {}) {
const m = (err && (err.message || String(err))) || ''; const m = (err && (err.message || String(err))) || '';
if (/404|releases\.atom|HttpError:\s*404/i.test(m)) { if (/404|releases\.atom|HttpError:\s*404/i.test(m)) {
send( send(
'Launcher update: could not read GitHub releases (404). ' + 'Launcher update: 404 (no latest.yml or wrong URL). For Gitea use gitea.* + token, or set update_feed_url. ' +
'If the repo is private, set GITHUB_TOKEN (or your token_env) so the launcher can authenticate, ' + 'For private GitHub set GITHUB_TOKEN.'
'or set update_feed_url in launcher.json to a public HTTPS folder that contains latest.yml.'
); );
return; return;
} }
@@ -3,6 +3,7 @@
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const { githubToken } = require('./github-token'); const { githubToken } = require('./github-token');
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
const { fetchToFile, downloadBodyToFile } = require('./http-download'); const { fetchToFile, downloadBodyToFile } = require('./http-download');
function encodeRepoPath(repoPath) { function encodeRepoPath(repoPath) {
@@ -65,6 +66,9 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
} }
async function downloadReleaseAsset(cfg, assetName, destPath) { async function downloadReleaseAsset(cfg, assetName, destPath) {
if (useGiteaReleases(cfg)) {
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
}
const token = githubToken(cfg); const token = githubToken(cfg);
const tag = (cfg.github.release_tag || 'latest').trim() || 'latest'; const tag = (cfg.github.release_tag || 'latest').trim() || 'latest';
const { owner, repo } = cfg.github; const { owner, repo } = cfg.github;
@@ -78,7 +82,10 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
const text = await res.text(); const text = await res.text();
if (!res.ok) { if (!res.ok) {
let hint = ''; 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)'; 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)}`); throw new Error(`releases list ${res.status}${hint}: ${text.slice(0, 600)}`);
} }
+19 -5
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store'); const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store');
const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch'); const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch');
const { readPatchState } = require('./lib/patch-manifest');
const { setupAutoUpdater } = require('./lib/auto-update'); const { setupAutoUpdater } = require('./lib/auto-update');
let mainWindow; let mainWindow;
@@ -46,16 +47,23 @@ async function readMergedConfig() {
app.whenReady().then(async () => { app.whenReady().then(async () => {
createWindow(); createWindow();
const { config } = await loadConfig(app); const { config } = await loadConfig(app);
const tokenEnv = config.github && config.github.token_env; const ghEnv = config.github && config.github.token_env;
const token = const githubToken =
(tokenEnv && String(process.env[tokenEnv] || '').trim()) || (ghEnv && String(process.env[ghEnv] || '').trim()) ||
String(process.env.GH_TOKEN || process.env.GITHUB_TOKEN || '').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(); const updateFeedUrl = String(process.env.LAUNCHER_UPDATE_URL || config.update_feed_url || '').trim();
autoUpdateApi = setupAutoUpdater(app, () => mainWindow, { autoUpdateApi = await setupAutoUpdater(app, () => mainWindow, {
updateFeedUrl, updateFeedUrl,
config,
githubOwner: config.github && config.github.owner, githubOwner: config.github && config.github.owner,
githubRepo: config.github && config.github.repo, githubRepo: config.github && config.github.repo,
token, githubToken,
giteaToken,
allowGithubLauncherUpdates: config.launcher_updates_from_github === true,
}); });
app.on('activate', () => { app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow(); if (BrowserWindow.getAllWindows().length === 0) createWindow();
@@ -68,12 +76,18 @@ app.on('window-all-closed', () => {
ipcMain.handle('launcher:load', async () => { ipcMain.handle('launcher:load', async () => {
const { configPath, config } = await readMergedConfig(); 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 { return {
configPath, configPath,
gameDir: config.game_dir || '', gameDir: config.game_dir || '',
authEnabled: !!(config.auth && config.auth.enabled), authEnabled: !!(config.auth && config.auth.enabled),
wowExe: (config.launch && config.launch.exe) || 'Wow.exe', wowExe: (config.launch && config.launch.exe) || 'Wow.exe',
wowOk: wowInstallValid(config), wowOk: wowInstallValid(config),
clientBuild,
}; };
}); });