mirror of
https://github.com/Heretek-AI/GDevelop-Unlocked.git
synced 2026-11-07 02:45:58 -10:00
Release: the update feed survived in the built artifact — set publish: null
The artifact from the green Linux run still contains resources/app-update.yml (provider github, owner 4ian, repo GDevelop). Deleting the `publish` key is not enough: with no publish config, electron-builder 26 (PublishManager.getPublishConfigsForUpdateInfo) derives the feed from package.json `repository` and writes the file anyway. Only an explicit `publish: null` disables that. On the Linux zip it happened to be inert (electron-updater needs an AppImage there). On Windows NSIS and macOS it would be a live feed that replaces the patched app with stock GDevelop on the next upstream release — the opposite of the release notes' claim. The script now replaces the block with `publish: null`, evaluates the config to prove it, and refuses a config with no publish key at all (previously accepted as 'already neutered', which is exactly the auto-detect case). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fcacf7142e
commit
9ceb42e62d
3 files changed
+117
-55
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Patches, launcher files and the hash-pinned Tier-2 runtime must stay
|
||||
# byte-exact on every platform: a CRLF checkout breaks every hunk and
|
||||
# every sha256 pin. No end-of-line conversion, anywhere.
|
||||
* -text
|
||||
@@ -2,21 +2,30 @@
|
||||
//
|
||||
// Upstream ships `publish: [{ provider: 'github' }]` in
|
||||
// newIDE/electron-app/electron-builder-config.js, which points
|
||||
// electron-updater at 4ian/GDevelop releases. A unlocked build must never
|
||||
// phone that feed (it would download stock code over the patched app), so
|
||||
// the release workflow strips the `publish` key before building. With no
|
||||
// feed, checkForUpdatesAndNotify resolves empty and the explicit
|
||||
// updates-check IPC becomes a graceful no-op.
|
||||
// electron-updater at 4ian/GDevelop releases. An unlocked build must never
|
||||
// follow that feed (it would download stock code over the patched app).
|
||||
//
|
||||
// The key is REPLACED with `publish: null`, not deleted. Deleting it is
|
||||
// not enough: with no publish config, electron-builder (26.x,
|
||||
// PublishManager.getPublishConfigsForUpdateInfo) falls back to the
|
||||
// `repository` field of package.json and still writes
|
||||
// resources/app-update.yml pointing at 4ian/GDevelop — found in the first
|
||||
// CI-built artifact. Only an explicit `null` disables that fallback.
|
||||
// Without app-update.yml the app's update check has nothing to read.
|
||||
//
|
||||
// Usage: node scripts/strip-updater-feed.mjs <upstream-checkout-root>
|
||||
// Exits non-zero if no publish block is found (loud is better than
|
||||
// shipping an updatable build by accident).
|
||||
// Exits non-zero unless the evaluated config ends with `publish === null`
|
||||
// (loud is better than shipping an updatable build by accident). A config
|
||||
// with no `publish` key at all is NOT treated as neutered, for the reason
|
||||
// above.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const CONFIG_REL = 'newIDE/electron-app/electron-builder-config.js';
|
||||
const NEUTERED = /\bpublish:\s*null\b/;
|
||||
|
||||
function stripPublishBlock(text) {
|
||||
function neuterPublishBlock(text) {
|
||||
const keyIdx = text.indexOf('publish:');
|
||||
if (keyIdx === -1) throw new Error('no `publish:` key found');
|
||||
const bracketIdx = text.indexOf('[', keyIdx);
|
||||
@@ -34,10 +43,14 @@ function stripPublishBlock(text) {
|
||||
}
|
||||
}
|
||||
if (end === -1) throw new Error('unterminated `publish:` array');
|
||||
// Extend to swallow a trailing comma and the preceding separator comma.
|
||||
let after = end + 1;
|
||||
while (after < text.length && /[\s,]/.test(text[after])) after++;
|
||||
return text.slice(0, keyIdx) + text.slice(after);
|
||||
return text.slice(0, keyIdx) + 'publish: null' + text.slice(end + 1);
|
||||
}
|
||||
|
||||
/** Evaluate the build config the way electron-builder will. */
|
||||
function loadedPublishValue(abs) {
|
||||
const require = createRequire(import.meta.url);
|
||||
delete require.cache[abs];
|
||||
return require(abs).publish;
|
||||
}
|
||||
|
||||
const root = path.resolve(process.argv[2] || '.');
|
||||
@@ -47,12 +60,23 @@ if (!fs.existsSync(abs)) {
|
||||
process.exit(2);
|
||||
}
|
||||
const before = fs.readFileSync(abs, 'utf8');
|
||||
if (!before.includes('publish:')) {
|
||||
console.log('[strip-updater-feed] no publish block — already neutered.');
|
||||
process.exit(0);
|
||||
if (NEUTERED.test(before)) {
|
||||
console.log('[strip-updater-feed] publish already null — neutered.');
|
||||
} else if (!before.includes('publish:')) {
|
||||
console.error(
|
||||
'[strip-updater-feed] no `publish:` key in the build config. ' +
|
||||
'electron-builder would then derive the update feed from ' +
|
||||
'package.json `repository` — refusing to call that neutered.'
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
fs.writeFileSync(abs, neuterPublishBlock(before));
|
||||
console.log(`[strip-updater-feed] set publish: null in ${CONFIG_REL}`);
|
||||
}
|
||||
const publish = loadedPublishValue(abs);
|
||||
if (publish !== null) {
|
||||
console.error(
|
||||
`[strip-updater-feed] evaluated config still has publish = ${JSON.stringify(publish)}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const afterText = stripPublishBlock(before);
|
||||
if (afterText.includes('publish:'))
|
||||
throw new Error('publish block survived stripping');
|
||||
fs.writeFileSync(abs, afterText);
|
||||
console.log(`[strip-updater-feed] removed update feed from ${CONFIG_REL}`);
|
||||
@@ -1,11 +1,20 @@
|
||||
// Updater-feed strip tests: the release build must never ship an
|
||||
// electron-updater feed pointing at upstream releases.
|
||||
//
|
||||
// What "neutered" means is load-bearing: the config must evaluate to
|
||||
// `publish === null`. A config with the key merely deleted still yields
|
||||
// resources/app-update.yml (electron-builder derives the feed from
|
||||
// package.json `repository`), which is how the first CI artifact shipped
|
||||
// one. Limit: electron-builder is not installed here, so these tests pin
|
||||
// the config value it reads, not the packaged output — release.yml
|
||||
// asserts the built artifact has no app-update.yml.
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -13,64 +22,89 @@ const ROOT = path.resolve(here, '..', '..');
|
||||
const UPSTREAM =
|
||||
process.env.UPSTREAM_DIR || path.join(ROOT, 'review', 'GDevelop');
|
||||
const CONFIG = 'newIDE/electron-app/electron-builder-config.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function stripOnCopy() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'updater-strip-'));
|
||||
const src = path.join(UPSTREAM, CONFIG);
|
||||
assert.ok(fs.existsSync(src), `missing fixture ${CONFIG}`);
|
||||
const destDir = path.join(dir, 'newIDE/electron-app');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
fs.copyFileSync(src, path.join(destDir, 'electron-builder-config.js'));
|
||||
const r = spawnSync('node', ['scripts/strip-updater-feed.mjs', dir], {
|
||||
function strip(dir) {
|
||||
return spawnSync('node', ['scripts/strip-updater-feed.mjs', dir], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(r.status, 0, `strip failed:\n${r.stdout}\n${r.stderr}`);
|
||||
return { dir, text: fs.readFileSync(path.join(destDir, 'electron-builder-config.js'), 'utf8') };
|
||||
}
|
||||
|
||||
function makeTree(configText) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'updater-strip-'));
|
||||
const abs = path.join(dir, CONFIG);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
if (configText === undefined) {
|
||||
const src = path.join(UPSTREAM, CONFIG);
|
||||
assert.ok(fs.existsSync(src), `missing fixture ${CONFIG}`);
|
||||
fs.copyFileSync(src, abs);
|
||||
} else {
|
||||
fs.writeFileSync(abs, configText);
|
||||
}
|
||||
return { dir, abs };
|
||||
}
|
||||
|
||||
describe('release: updater feed is neutered', () => {
|
||||
it('removes the publish block from the real upstream config', () => {
|
||||
const { dir, text } = stripOnCopy();
|
||||
it('real upstream config evaluates to publish === null, rest intact', () => {
|
||||
const { dir, abs } = makeTree();
|
||||
try {
|
||||
assert.ok(!text.includes('publish:'), 'publish block survived');
|
||||
assert.deepEqual(require(abs).publish, [{ provider: 'github' }]);
|
||||
delete require.cache[abs];
|
||||
const r = strip(dir);
|
||||
assert.equal(r.status, 0, `strip failed:\n${r.stdout}\n${r.stderr}`);
|
||||
const config = require(abs);
|
||||
assert.equal(config.publish, null);
|
||||
assert.ok('publish' in config, 'key must stay: absent means auto-detect');
|
||||
// Everything else (appId, targets, signing hook) must be intact.
|
||||
assert.equal(config.appId, 'com.gdevelop-app.ide');
|
||||
assert.equal(config.win.target.length, 2);
|
||||
assert.equal(config.afterSign, 'scripts/electron-builder-after-sign.js');
|
||||
const text = fs.readFileSync(abs, 'utf8');
|
||||
assert.ok(!text.includes("provider: 'github'"), 'github provider survived');
|
||||
// Everything else (appId, targets, signing branches) must be intact.
|
||||
assert.ok(text.includes("appId: 'com.gdevelop-app.ide'"), 'appId lost');
|
||||
assert.ok(text.includes('GD_PORTABLE_BUILD'), 'portable branch lost');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('is idempotent (second run is a no-op, exit 0)', () => {
|
||||
const { dir } = stripOnCopy();
|
||||
it('is idempotent (second run is a verified no-op, exit 0)', () => {
|
||||
const { dir, abs } = makeTree();
|
||||
try {
|
||||
const r = spawnSync('node', ['scripts/strip-updater-feed.mjs', dir], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(strip(dir).status, 0);
|
||||
const once = fs.readFileSync(abs, 'utf8');
|
||||
const r = strip(dir);
|
||||
assert.equal(r.status, 0, `re-run failed:\n${r.stdout}\n${r.stderr}`);
|
||||
assert.equal(fs.readFileSync(abs, 'utf8'), once);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('treats a config without publish as already neutered', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'updater-strip-neg-'));
|
||||
it('refuses a config with no publish key (feed would be auto-detected)', () => {
|
||||
const { dir } = makeTree(
|
||||
'const config = { appId: 1 };\nmodule.exports = config;\n'
|
||||
);
|
||||
try {
|
||||
const destDir = path.join(dir, 'newIDE/electron-app');
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(destDir, 'electron-builder-config.js'),
|
||||
'const config = { appId: 1 };\nmodule.exports = config;\n'
|
||||
);
|
||||
const r = spawnSync('node', ['scripts/strip-updater-feed.mjs', dir], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
// No publish key at all: treated as already-neutered, exit 0.
|
||||
assert.equal(r.status, 0);
|
||||
const r = strip(dir);
|
||||
assert.equal(r.status, 1, 'absent key was accepted as neutered');
|
||||
assert.ok(r.stderr.includes('no `publish:` key'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fails when publish is re-assigned after the object literal', () => {
|
||||
// The text edit alone would pass; evaluating the config catches it.
|
||||
const { dir } = makeTree(
|
||||
"const config = { appId: 1, publish: [{ provider: 'github' }] };\n" +
|
||||
"config.publish = [{ provider: 'generic', url: 'https://example.invalid' }];\n" +
|
||||
'module.exports = config;\n'
|
||||
);
|
||||
try {
|
||||
const r = strip(dir);
|
||||
assert.equal(r.status, 1);
|
||||
assert.ok(r.stderr.includes('evaluated config still has publish'));
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user