Skip to content

Commit 98dd4b8

Browse files
authored
Merge pull request #74 from beNative/codex/fix-release-builds-and-auto-update-logging
Adjust Windows release artifacts and auto-update handling
2 parents 70099e3 + 937f8c9 commit 98dd4b8

2 files changed

Lines changed: 170 additions & 24 deletions

File tree

electron/main.ts

Lines changed: 163 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,113 @@ type FileValidationFailure = {
164164

165165
type FileValidationResult = FileValidationSuccess | FileValidationFailure;
166166

167+
type UpdaterArch = 'x64' | 'ia32' | 'arm64' | 'armv7l';
168+
169+
const mapProcessArchToUpdaterArch = (): UpdaterArch | null => {
170+
switch (process.arch) {
171+
case 'x64':
172+
case 'arm64':
173+
case 'ia32':
174+
return process.arch;
175+
case 'arm':
176+
return 'armv7l';
177+
default:
178+
return null;
179+
}
180+
};
181+
182+
const detectArchFromFileName = (name: string | null | undefined): UpdaterArch | null => {
183+
if (!name) {
184+
return null;
185+
}
186+
const normalized = name.toLowerCase();
187+
const tokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
188+
const hasToken = (token: string) => tokens.includes(token);
189+
190+
if (hasToken('arm64') || hasToken('aarch64')) {
191+
return 'arm64';
192+
}
193+
if (hasToken('armv7l') || hasToken('armhf')) {
194+
return 'armv7l';
195+
}
196+
if (hasToken('ia32') || hasToken('x86') || hasToken('win32')) {
197+
return 'ia32';
198+
}
199+
if (hasToken('x64') || hasToken('amd64') || hasToken('win64')) {
200+
return 'x64';
201+
}
202+
203+
const endsWith32 = /(^|[^0-9])32($|[^0-9])/.test(normalized);
204+
const endsWith64 = /(^|[^0-9])64($|[^0-9])/.test(normalized);
205+
if (endsWith32 && !endsWith64) {
206+
return 'ia32';
207+
}
208+
if (endsWith64 && !endsWith32) {
209+
return 'x64';
210+
}
211+
return null;
212+
};
213+
214+
const normalizeNameForComparison = (name: string): string => {
215+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
216+
};
217+
218+
const determineDownloadedUpdateArch = (info: UpdateDownloadedEvent, downloadedName: string) => {
219+
type ArchCandidate = { priority: number; sources: string[] };
220+
const candidates = new Map<UpdaterArch, ArchCandidate>();
221+
const register = (arch: UpdaterArch | null, priority: number, source: string) => {
222+
if (!arch) {
223+
return;
224+
}
225+
const existing = candidates.get(arch);
226+
if (!existing || priority > existing.priority) {
227+
candidates.set(arch, { priority, sources: [source] });
228+
return;
229+
}
230+
if (priority === existing.priority) {
231+
existing.sources.push(source);
232+
return;
233+
}
234+
existing.sources.push(source);
235+
};
236+
237+
register(mapProcessArchToUpdaterArch(), 10, `process.arch:${process.arch}`);
238+
register(detectArchFromFileName(downloadedName), 40, `downloadedName:${downloadedName}`);
239+
240+
if (typeof (info as any).path === 'string') {
241+
const legacyName = path.basename((info as any).path);
242+
register(detectArchFromFileName(legacyName), 50, `info.path:${legacyName}`);
243+
}
244+
245+
if (Array.isArray(info.files)) {
246+
for (const file of info.files) {
247+
if (typeof file?.url !== 'string') {
248+
continue;
249+
}
250+
const urlName = getFileNameFromUrlLike(file.url);
251+
const arch = detectArchFromFileName(urlName);
252+
const matchesDownloaded = urlName && normalizeNameForComparison(urlName) === normalizeNameForComparison(downloadedName);
253+
register(arch, matchesDownloaded ? 100 : 80, urlName ? `info.files:${urlName}` : 'info.files');
254+
}
255+
}
256+
257+
let selected: { arch: UpdaterArch | null; sources: string[] } = { arch: null, sources: [] };
258+
for (const [arch, candidate] of candidates.entries()) {
259+
if (!selected.arch || candidate.priority > (candidates.get(selected.arch)?.priority ?? -Infinity)) {
260+
selected = { arch, sources: [...candidate.sources] };
261+
}
262+
}
263+
return selected;
264+
};
265+
266+
const filterNamesByArch = (names: string[], arch: UpdaterArch | null): string[] => {
267+
if (!arch) {
268+
return names;
269+
}
270+
const filtered = names.filter(name => detectArchFromFileName(name) === arch);
271+
return filtered.length > 0 ? filtered : names;
272+
};
273+
167274
const buildGitHubApiHeaders = async (): Promise<Record<string, string>> => {
168275
const headers: Record<string, string> = {
169276
'Accept': 'application/vnd.github+json',
@@ -401,23 +508,49 @@ const ensureDownloadedFileMatchesOfficialRelease = async (info: UpdateDownloaded
401508
return { success: false, error: 'No official release filenames available for comparison.', downloadedName, officialNames: [] };
402509
}
403510

404-
if (officialNames.includes(downloadedName)) {
511+
const { arch: inferredArch, sources: archSources } = determineDownloadedUpdateArch(info, downloadedName);
512+
const candidateNames = filterNamesByArch(officialNames, inferredArch);
513+
if (inferredArch) {
514+
mainLogger.info('[AutoUpdate] Inferred update architecture.', {
515+
version: info.version,
516+
inferredArch,
517+
evidence: archSources,
518+
candidateCount: candidateNames.length,
519+
});
520+
} else {
521+
mainLogger.info('[AutoUpdate] Unable to infer update architecture from metadata.', {
522+
version: info.version,
523+
availableCandidates: officialNames.length,
524+
});
525+
}
526+
527+
if (candidateNames.includes(downloadedName)) {
405528
mainLogger.info('[AutoUpdate] Downloaded filename already matches official release asset.', {
406529
downloadedName,
407530
});
408-
return { success: true, filePath: downloadedPath, expectedName: downloadedName, officialNames };
531+
return { success: true, filePath: downloadedPath, expectedName: downloadedName, officialNames: candidateNames };
409532
}
410533

411-
const caseInsensitiveMatch = officialNames.find(name => name.toLowerCase() === downloadedName.toLowerCase());
534+
const caseInsensitiveMatch = candidateNames.find(name => name.toLowerCase() === downloadedName.toLowerCase());
412535
if (caseInsensitiveMatch) {
413536
mainLogger.info('[AutoUpdate] Downloaded filename matches an official asset ignoring case.', {
414537
downloadedName,
415538
expectedName: caseInsensitiveMatch,
416539
});
417-
return { success: true, filePath: downloadedPath, expectedName: caseInsensitiveMatch, officialNames };
540+
return { success: true, filePath: downloadedPath, expectedName: caseInsensitiveMatch, officialNames: candidateNames };
418541
}
419542

420-
const expectedName = officialNames[0];
543+
const normalizedDownloaded = normalizeNameForComparison(downloadedName);
544+
const normalizedMatch = candidateNames.find(name => normalizeNameForComparison(name) === normalizedDownloaded);
545+
if (normalizedMatch) {
546+
mainLogger.info('[AutoUpdate] Downloaded filename matches official asset after normalization.', {
547+
downloadedName,
548+
expectedName: normalizedMatch,
549+
});
550+
return { success: true, filePath: downloadedPath, expectedName: normalizedMatch, officialNames: candidateNames };
551+
}
552+
553+
const expectedName = candidateNames[0];
421554
const expectedPath = path.join(path.dirname(downloadedPath), expectedName);
422555
try {
423556
await safeRenameDownloadedUpdate(downloadedPath, expectedPath);
@@ -433,16 +566,17 @@ const ensureDownloadedFileMatchesOfficialRelease = async (info: UpdateDownloaded
433566
previousName: downloadedName,
434567
expectedName,
435568
helperAdjusted: Boolean(helper),
569+
normalizedAttempted: Boolean(normalizedMatch),
436570
});
437-
return { success: true, filePath: expectedPath, expectedName, renamed: true, officialNames };
571+
return { success: true, filePath: expectedPath, expectedName, renamed: true, officialNames: candidateNames };
438572
} catch (error: any) {
439573
mainLogger.error('[AutoUpdate] Failed to align downloaded filename with official asset.', {
440574
version: info.version,
441575
downloadedName,
442576
expectedName,
443577
error: error instanceof Error ? { message: error.message, stack: error.stack } : { message: String(error) },
444578
});
445-
return { success: false, error: error?.message || String(error), downloadedName, officialNames };
579+
return { success: false, error: error?.message || String(error), downloadedName, officialNames: candidateNames };
446580
}
447581
};
448582

@@ -497,16 +631,19 @@ app.on('ready', async () => {
497631

498632
// --- Auto-updater logic ---
499633
if (app.isPackaged) {
500-
mainLogger.info(`Configuring auto-updater. allowPrerelease: ${settings.allowPrerelease}`);
501-
autoUpdater.allowPrerelease = settings.allowPrerelease ?? true;
634+
const allowPrerelease = settings.allowPrerelease ?? true;
635+
mainLogger.info('[AutoUpdate] Configuring auto-updater.', {
636+
allowPrerelease,
637+
});
638+
autoUpdater.allowPrerelease = allowPrerelease;
502639

503640
autoUpdater.on('checking-for-update', () => {
504-
mainLogger.info('Checking for update...');
641+
mainLogger.info('[AutoUpdate] Checking for update.');
505642
mainWindow?.webContents.send('update-status-change', { status: 'checking', message: 'Checking for updates...' });
506643
});
507644
autoUpdater.on('update-available', (info) => {
508645
lastDownloadedUpdateValidation = null;
509-
mainLogger.info('Update available.', {
646+
mainLogger.info('[AutoUpdate] Update available.', {
510647
version: info.version,
511648
files: Array.isArray(info.files) ? info.files.map(file => ({
512649
url: typeof file?.url === 'string' ? getFileNameFromUrlLike(file.url) : undefined,
@@ -517,14 +654,14 @@ app.on('ready', async () => {
517654
mainWindow?.webContents.send('update-status-change', { status: 'available', message: `Update v${info.version} available. Downloading...` });
518655
});
519656
autoUpdater.on('update-not-available', (info) => {
520-
mainLogger.info('Update not available.', {
657+
mainLogger.info('[AutoUpdate] No update available.', {
521658
version: info?.version,
522659
downloadedFile: info?.downloadedFile,
523660
});
524661
});
525662
autoUpdater.on('error', (err) => {
526663
lastDownloadedUpdateValidation = null;
527-
mainLogger.error('Error in auto-updater.', {
664+
mainLogger.error('[AutoUpdate] Error from auto-updater.', {
528665
message: err?.message,
529666
stack: err?.stack,
530667
name: err?.name,
@@ -534,11 +671,15 @@ app.on('ready', async () => {
534671
mainWindow?.webContents.send('update-status-change', { status: 'error', message: `Error in auto-updater: ${err.message}` });
535672
});
536673
autoUpdater.on('download-progress', (progressObj) => {
537-
const log_message = `Downloaded ${progressObj.percent.toFixed(2)}%`;
538-
mainLogger.debug(log_message);
674+
mainLogger.debug('[AutoUpdate] Download progress update.', {
675+
percent: Number.isFinite(progressObj.percent) ? Number(progressObj.percent.toFixed(2)) : progressObj.percent,
676+
transferred: (progressObj as any)?.transferred,
677+
total: (progressObj as any)?.total,
678+
bytesPerSecond: (progressObj as any)?.bytesPerSecond,
679+
});
539680
});
540681
autoUpdater.on('update-downloaded', (info) => {
541-
mainLogger.info('Update downloaded event received. Validating filename.', {
682+
mainLogger.info('[AutoUpdate] Update downloaded event received. Validating filename.', {
542683
version: info.version,
543684
downloadedFile: info.downloadedFile,
544685
files: Array.isArray(info.files) ? info.files.map(file => ({
@@ -573,7 +714,7 @@ app.on('ready', async () => {
573714
validated: true,
574715
};
575716

576-
mainLogger.info('Update downloaded.', {
717+
mainLogger.info('[AutoUpdate] Update validated and ready to install.', {
577718
version: info.version,
578719
filePath: validationResult.filePath,
579720
alignedWithOfficialName: validationResult.renamed === true,
@@ -594,14 +735,14 @@ app.on('ready', async () => {
594735
});
595736

596737
// Check for updates
597-
mainLogger.info('Triggering auto-updater checkForUpdatesAndNotify call.');
738+
mainLogger.info('[AutoUpdate] Triggering checkForUpdatesAndNotify.');
598739
autoUpdater.checkForUpdatesAndNotify()
599740
.then(result => {
600741
if (!result) {
601-
mainLogger.info('Auto-updater checkForUpdatesAndNotify resolved without update info.');
742+
mainLogger.info('[AutoUpdate] checkForUpdatesAndNotify completed without update info.');
602743
return;
603744
}
604-
mainLogger.info('Auto-updater checkForUpdatesAndNotify resolved.', {
745+
mainLogger.info('[AutoUpdate] checkForUpdatesAndNotify resolved.', {
605746
updateInfo: result.updateInfo ? {
606747
version: result.updateInfo.version,
607748
files: Array.isArray(result.updateInfo.files) ? result.updateInfo.files.map(file => ({
@@ -614,15 +755,15 @@ app.on('ready', async () => {
614755
});
615756
})
616757
.catch(error => {
617-
mainLogger.error('checkForUpdatesAndNotify rejected.', {
758+
mainLogger.error('[AutoUpdate] checkForUpdatesAndNotify rejected.', {
618759
message: error?.message,
619760
stack: error?.stack,
620761
name: error?.name,
621762
});
622763
mainWindow?.webContents.send('update-status-change', { status: 'error', message: `Failed to check for updates: ${error?.message || error}` });
623764
});
624765
} else {
625-
mainLogger.info('App is not packaged, skipping auto-updater.');
766+
mainLogger.info('[AutoUpdate] App is not packaged; skipping auto-updater.');
626767
}
627768
});
628769

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
"build": {
4141
"appId": "com.example.gitautomationdashboard",
4242
"productName": "Git Automation Dashboard",
43-
"artifactName": "${name}-${version}-${os}-${arch}.${ext}",
4443
"directories": {
4544
"app": "dist",
4645
"buildResources": "assets",
@@ -53,13 +52,19 @@
5352
}
5453
],
5554
"win": {
55+
"artifactName": "${name}-${version}-windows-${arch}-setup.${ext}",
5656
"target": [
5757
{
5858
"target": "nsis",
5959
"arch": [
60-
"x64",
6160
"ia32"
6261
]
62+
},
63+
{
64+
"target": "nsis",
65+
"arch": [
66+
"x64"
67+
]
6368
}
6469
],
6570
"icon": "dist/assets/icon.ico"

0 commit comments

Comments
 (0)