Skip to content

Commit 710d618

Browse files
committed
Fix Windows auto-update manifest publishing
1 parent 8879b5a commit 710d618

File tree

4 files changed

+162
-13
lines changed

4 files changed

+162
-13
lines changed

electron/main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ log.catchErrors({
4646

4747
console.log(`Log file will be written to: ${log.transports.file.getFile().path}`);
4848

49+
if (process.platform === 'win32') {
50+
const arch = process.arch === 'ia32' ? 'ia32' : process.arch === 'arm64' ? 'arm64' : 'x64';
51+
const channel = `win32-${arch}`;
52+
if (autoUpdater.channel !== channel) {
53+
console.log(`Configuring auto-updater channel for Windows architecture: ${channel}`);
54+
autoUpdater.channel = channel;
55+
}
56+
}
57+
4958
let mainWindow: BrowserWindow | null;
5059
let autoCheckEnabled = true;
5160
let pendingAutoUpdateCheck: NodeJS.Timeout | null = null;

scripts/__tests__/release-workflow.test.mjs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,17 @@ test('release tooling rewrites metadata and keeps latest.yml published', async (
201201
const entries = readManifestEntries(manifest);
202202
const relativeInstaller = path.relative(repoPath(), renamedInstaller);
203203
const relativeLatest = path.relative(repoPath(), latestPath);
204+
const relativeWinChannel = path.relative(
205+
repoPath(),
206+
path.join(releaseDir, 'win32-x64.yml'),
207+
);
204208

205209
assert(entries.includes(relativeInstaller), 'Installer should be present in manifest after renaming');
206210
assert(entries.includes(relativeLatest), 'latest.yml must be uploaded as part of the release');
211+
assert(
212+
entries.includes(relativeWinChannel),
213+
'win32-x64.yml must be uploaded for architecture-specific Windows updates',
214+
);
207215

208216
const installerBuffer = await fs.readFile(renamedInstaller);
209217
const expectedSha = computeSha512Base64(installerBuffer);
@@ -352,15 +360,37 @@ test('release workflow verifies metadata directories across platforms and publis
352360
});
353361

354362
const manifestEntries = new Set(readManifestEntries(await fs.readFile(manifestPath, 'utf8')));
355-
const metadataFiles = [ia32.metadataPath, x64.metadataPath, linux.metadataPath, mac.metadataPath];
356-
for (const metadataPath of metadataFiles) {
357-
assert(metadataPath, 'metadataPath should be defined for all fixtures');
363+
const expectedMetadataUploads = [x64.metadataPath, linux.metadataPath, mac.metadataPath];
364+
for (const metadataPath of expectedMetadataUploads) {
365+
assert(metadataPath, 'metadataPath should be defined for uploaded manifests');
358366
const relativePath = path.relative(repoPath(), metadataPath);
359367
assert(manifestEntries.has(relativePath), `${relativePath} must be included in release manifest`);
360368
}
361369

370+
const ia32LatestRelative = path.relative(repoPath(), ia32.metadataPath);
371+
assert(
372+
!manifestEntries.has(ia32LatestRelative),
373+
'Windows ia32 latest.yml should be replaced by architecture-specific manifest to avoid duplicate assets',
374+
);
375+
376+
const windowsChannels = [
377+
path.relative(repoPath(), path.join(ia32.releaseDir, 'win32-ia32.yml')),
378+
path.relative(repoPath(), path.join(x64.releaseDir, 'win32-x64.yml')),
379+
];
380+
for (const channel of windowsChannels) {
381+
assert(manifestEntries.has(channel), `${channel} must be uploaded for Windows auto-update`);
382+
}
383+
362384
const metadataDirs = await listMetadataDirectories(path.join(workspace, 'release-artifacts'));
363-
assert(metadataDirs.length >= metadataFiles.length, 'expected to discover metadata directories');
385+
const expectedDirs = [ia32.metadataPath, x64.metadataPath, linux.metadataPath, mac.metadataPath]
386+
.filter(Boolean)
387+
.map((metadataPath) => path.dirname(metadataPath));
388+
for (const expectedDir of expectedDirs) {
389+
assert(
390+
metadataDirs.includes(expectedDir),
391+
`${expectedDir} should be discovered for metadata verification`,
392+
);
393+
}
364394

365395
for (const dir of metadataDirs) {
366396
await runLocalVerification(dir);
@@ -423,8 +453,8 @@ test('remote auto-update check fails when Windows release metadata is absent', a
423453
assert(error instanceof Error, 'Expected runRemoteCheck to reject with an Error instance');
424454
assert.match(
425455
error.message,
426-
/Missing required auto-update metadata asset[\s\S]*latest\.yml/,
427-
'Error message should mention latest.yml as the missing manifest',
456+
/Missing required auto-update metadata asset[\s\S]*latest\.yml[\s\S]*win32-<arch>\.yml/,
457+
'Error message should mention both legacy and architecture-specific manifests',
428458
);
429459
return true;
430460
},

scripts/generate-release-notes.mjs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,12 @@ async function collectAssets(artifactRoot) {
252252
const filePath = normalisedPath;
253253
const artifactDir = path.relative(artifactRoot, path.dirname(filePath));
254254
if (isAutoUpdateSupportFile(fileName)) {
255-
updateSupportFiles.push({ filePath, artifactDir });
255+
updateSupportFiles.push({
256+
filePath,
257+
artifactDir,
258+
assetName: path.basename(filePath),
259+
upload: true,
260+
});
256261
continue;
257262
}
258263

@@ -275,6 +280,7 @@ async function collectAssets(artifactRoot) {
275280
format: detectFormat(fileName),
276281
sha512,
277282
size,
283+
assetName: fileName,
278284
});
279285
}
280286
}
@@ -296,6 +302,74 @@ async function collectAssets(artifactRoot) {
296302
return { releaseAssets, updateSupportFiles };
297303
}
298304

305+
function normaliseWindowsChannelName(arch) {
306+
if (!arch) {
307+
return null;
308+
}
309+
const normalised = arch.toLowerCase();
310+
if (['x64', 'ia32', 'arm64'].includes(normalised)) {
311+
return `win32-${normalised}`;
312+
}
313+
return `win32-${normalised.replace(/[^a-z0-9]+/gi, '-')}`;
314+
}
315+
316+
async function prepareMetadataUploads({ releaseAssets, updateSupportFiles }) {
317+
if (updateSupportFiles.length === 0) {
318+
return;
319+
}
320+
321+
const releaseAssetsByDir = new Map();
322+
for (const asset of releaseAssets) {
323+
if (!releaseAssetsByDir.has(asset.artifactDir)) {
324+
releaseAssetsByDir.set(asset.artifactDir, { assets: [], arch: asset.arch });
325+
}
326+
const entry = releaseAssetsByDir.get(asset.artifactDir);
327+
entry.assets.push(asset);
328+
if (!entry.arch && asset.arch) {
329+
entry.arch = asset.arch;
330+
}
331+
}
332+
333+
const windowsMetadata = updateSupportFiles.filter((file) => {
334+
const name = path.basename(file.filePath).toLowerCase();
335+
return name === 'latest.yml' && file.artifactDir.startsWith('docforge-windows-');
336+
});
337+
338+
if (windowsMetadata.length === 0) {
339+
return;
340+
}
341+
342+
const canonical =
343+
windowsMetadata.find((file) => {
344+
const info = releaseAssetsByDir.get(file.artifactDir);
345+
return info?.arch === 'x64';
346+
}) ?? windowsMetadata[0];
347+
348+
for (const file of windowsMetadata) {
349+
const info = releaseAssetsByDir.get(file.artifactDir);
350+
const channelName = normaliseWindowsChannelName(info?.arch ?? 'windows');
351+
if (!channelName) {
352+
continue;
353+
}
354+
const aliasName = `${channelName}.yml`;
355+
const aliasPath = path.join(path.dirname(file.filePath), aliasName);
356+
await fs.copyFile(file.filePath, aliasPath);
357+
updateSupportFiles.push({
358+
filePath: aliasPath,
359+
artifactDir: file.artifactDir,
360+
assetName: aliasName,
361+
upload: true,
362+
});
363+
364+
file.assetName = path.basename(file.filePath);
365+
if (file === canonical) {
366+
file.upload = true;
367+
} else {
368+
file.upload = false;
369+
}
370+
}
371+
}
372+
299373
async function updateMetadataFiles(metadataFiles, releaseAssets) {
300374
if (metadataFiles.length === 0) {
301375
return;
@@ -550,9 +624,26 @@ async function main() {
550624
const releaseNotes = sections.join('\n').replace(/\n{3,}/g, '\n\n');
551625
await fs.writeFile(outputPath, `${releaseNotes}\n`, 'utf8');
552626

627+
await prepareMetadataUploads({ releaseAssets, updateSupportFiles });
628+
updateSupportFiles.sort((a, b) => a.filePath.localeCompare(b.filePath));
629+
553630
const manifestEntries = [
554-
...releaseAssets.map((asset) => path.relative(process.cwd(), asset.filePath)),
555-
...updateSupportFiles.map((file) => path.relative(process.cwd(), file.filePath)),
631+
...releaseAssets.map((asset) => {
632+
const relative = path.relative(process.cwd(), asset.filePath);
633+
if (asset.assetName && path.basename(asset.filePath) !== asset.assetName) {
634+
return `${relative}#${asset.assetName}`;
635+
}
636+
return relative;
637+
}),
638+
...updateSupportFiles
639+
.filter((file) => file.upload !== false)
640+
.map((file) => {
641+
const relative = path.relative(process.cwd(), file.filePath);
642+
if (file.assetName && path.basename(file.filePath) !== file.assetName) {
643+
return `${relative}#${file.assetName}`;
644+
}
645+
return relative;
646+
}),
556647
];
557648
const manifest = manifestEntries.join('\n');
558649
await fs.writeFile(filesOutputPath, `${manifest}\n`, 'utf8');

scripts/test-auto-update.mjs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,14 +363,33 @@ export async function analyseMetadataEntry({
363363
function describeMissingMetadataAssets(assetEntries, metadataAssets) {
364364
const missing = [];
365365
const windowsInstallers = assetEntries.filter((asset) => asset.name.endsWith('.exe'));
366+
367+
if (windowsInstallers.length === 0) {
368+
return missing;
369+
}
370+
371+
const installerList = windowsInstallers.map((asset) => asset.name).join(', ');
366372
const hasLatest = metadataAssets.some((asset) => asset.name === 'latest.yml');
373+
const hasWindowsChannel = metadataAssets.some(
374+
(asset) => asset.name.startsWith('win32-') && asset.name.endsWith('.yml'),
375+
);
376+
377+
if (!hasLatest) {
378+
missing.push(
379+
[
380+
'latest.yml (required for legacy Windows auto-update clients)',
381+
installerList ? `Windows installers detected: ${installerList}` : null,
382+
]
383+
.filter(Boolean)
384+
.join(' - '),
385+
);
386+
}
367387

368-
if (windowsInstallers.length > 0 && !hasLatest) {
369-
const installers = windowsInstallers.map((asset) => asset.name).join(', ');
388+
if (!hasWindowsChannel) {
370389
missing.push(
371390
[
372-
'latest.yml (required for Windows auto-update)',
373-
installers ? `Windows installers detected: ${installers}` : null,
391+
'win32-<arch>.yml (required for architecture-specific Windows auto-update channels)',
392+
installerList ? `Windows installers detected: ${installerList}` : null,
374393
]
375394
.filter(Boolean)
376395
.join(' - '),

0 commit comments

Comments
 (0)