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:
@@ -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)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user