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