feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,126 @@
import { LoggerWrapper } from '@joplin/utils/Logger';
import { releases3 } from '../../utils/checkForUpdatesUtilsTestData';
import AutoUpdaterService from './AutoUpdaterService';
import { BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
jest.mock('electron', () => ({
BrowserWindow: jest.fn(),
}));
jest.mock('electron-updater', () => {
const mockAutoUpdater = {
setFeedURL: jest.fn(),
checkForUpdates: jest.fn(),
on: jest.fn(),
quitAndInstall: jest.fn(),
};
return { autoUpdater: mockAutoUpdater };
});
describe('AutoUpdaterService', () => {
let service: AutoUpdaterService;
let mockLogger: LoggerWrapper;
beforeEach(() => {
const mockWindow = new BrowserWindow();
mockLogger = { info: jest.fn(), error: jest.fn(), debug: jest.fn(), warn: jest.fn() };
service = new AutoUpdaterService(mockWindow, mockLogger, false, true);
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(releases3),
}),
) as jest.Mock;
});
// electron-updater rebuilds the file name from the channel, so a wrong
// channel would silently fetch the x64 metadata on an arm64 machine.
test.each([
['win32', 'arm64', 'latest-win-arm64'],
['win32', 'x64', 'latest'],
['darwin', 'arm64', 'latest'],
])('should set the update channel for %s %s', async (platform, arch, expectedChannel) => {
// releases3 predates the Windows arm64 build, so add its asset.
const releases = JSON.parse(JSON.stringify(releases3));
releases[0].assets.push({
name: 'latest-win-arm64.yml',
browser_download_url: 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-win-arm64.yml',
});
global.fetch = jest.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(releases),
})) as jest.Mock;
const originalPlatform = process.platform;
const originalArch = process.arch;
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
Object.defineProperty(process, 'arch', { value: arch, configurable: true });
try {
await service.checkForUpdates(true);
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
Object.defineProperty(process, 'arch', { value: originalArch, configurable: true });
}
expect(autoUpdater.setFeedURL).toHaveBeenCalledWith(
expect.objectContaining({ channel: expectedChannel }),
);
});
it('should correctly fetch and process the latest prerelease', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
expect(release.tag_name).toBe('v3.1.3');
});
it('should correctly fetch and process the latest release', async () => {
const release = await service.fetchLatestRelease(false);
expect(release).toBeDefined();
expect(release.tag_name).toBe('v3.1.2');
});
it('should return the correct download URL for Windows x32', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'win32', 'ia32');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
});
it('should return the correct download URL for Windows x64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'win32', 'x64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
});
it('should return the correct download URL for Windows arm64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-win-arm64.yml';
release.assets.push({ name: 'latest-win-arm64.yml', browser_download_url: url });
expect(service.getDownloadUrlForPlatform(release, 'win32', 'arm64')).toBe(url);
});
it('should return the correct download URL for Mac x64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'x64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac.yml');
});
it('should return the correct download URL for Mac arm64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'arm64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac-arm64.yml');
});
it('should throw an error for Linux', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
expect(() => service.getDownloadUrlForPlatform(release, 'linux', 'amd64')).toThrow('The AutoUpdaterService does not support the following platform: linux');
});
});

查看文件

@@ -0,0 +1,224 @@
import { BrowserWindow } from 'electron';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import path = require('path');
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { GitHubRelease, GitHubReleaseAsset, handleReleaseResponseError } from '../../utils/checkForUpdatesUtils';
import * as semver from 'semver';
export enum AutoUpdaterEvents {
CheckingForUpdate = 'checking-for-update',
UpdateAvailable = 'update-available',
UpdateNotAvailable = 'update-not-available',
Error = 'error',
DownloadProgress = 'download-progress',
UpdateDownloaded = 'update-downloaded',
}
export const defaultUpdateInterval = 12 * 60 * 60 * 1000;
export const initialUpdateStartup = 5 * 1000;
const releasesLink = 'https://objects.joplinusercontent.com/r/releases';
export type Architecture = typeof process.arch;
interface PlatformAssets {
[platform: string]: {
[arch in Architecture]?: string;
};
}
const supportedPlatformAssets: PlatformAssets = {
'darwin': {
'x64': 'latest-mac.yml',
'arm64': 'latest-mac-arm64.yml',
},
// arm64 has its own file because electron-builder names the metadata
// `latest.yml` for every Windows arch, and it's built on a separate runner.
// https://github.com/electron-userland/electron-builder/issues/6372
'win32': {
'x64': 'latest.yml',
'ia32': 'latest.yml',
'arm64': 'latest-win-arm64.yml',
},
};
export interface AutoUpdaterServiceInterface {
checkForUpdates(isManualCheck: boolean): void;
updateApp(): void;
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
}
export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
private window_: BrowserWindow;
private logger_: LoggerWrapper;
private devMode_: boolean;
private enableDevMode = true; // force the updater to work in "dev" mode
private enableAutoDownload = true; // automatically download an update when it is found
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
private includePreReleases_ = false;
private allowDowngrade = false;
private isManualCheckInProgress = false;
private isUpdateInProgress = false;
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
this.window_ = mainWindow;
this.logger_ = logger;
this.devMode_ = devMode;
this.includePreReleases_ = includePreReleases;
this.configureAutoUpdater();
}
public checkForUpdates = async (isManualCheck = false): Promise<void> => {
try {
this.isManualCheckInProgress = isManualCheck;
await this.checkForLatestRelease();
} catch (error) {
this.logger_.error('Failed to check for updates:', error);
if (error.message.includes('ERR_CONNECTION_REFUSED')) {
this.logger_.info('Server is not reachable. Will try again later.');
}
}
};
public updateApp = (): void => {
autoUpdater.quitAndInstall(false, true);
};
public fetchLatestRelease = async (includePreReleases: boolean): Promise<GitHubRelease> => {
const releases = await this.fetchReleases(includePreReleases);
const release = releases[0];
if (!release) {
throw new Error('No suitable release found');
}
return release;
};
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string {
if (!supportedPlatformAssets[platform]) {
throw new Error(`The AutoUpdaterService does not support the following platform: ${platform}`);
}
const platformAssets = supportedPlatformAssets[platform];
const assetName: string | undefined = platformAssets ? platformAssets[arch as Architecture] : undefined;
if (!assetName) {
throw new Error(`The AutoUpdaterService does not support the architecture: ${arch} for platform: ${platform}`);
}
const asset: GitHubReleaseAsset = release.assets.find(a => a.name === assetName);
if (!asset) {
throw new Error(`Yml file: ${assetName} not found for version: ${release.tag_name} platform: ${platform} and architecture: ${arch}`);
}
return asset.browser_download_url;
}
private fetchReleases = async (includePreReleases: boolean): Promise<GitHubRelease[]> => {
const response = await fetch(releasesLink);
if (!response.ok) {
const responseText = await response.text();
this.logger_.error(`Cannot get latest release info (${response.status}): ${responseText.substr(0, 500)}`);
handleReleaseResponseError(response.status, responseText);
}
const releases: GitHubRelease[] = await response.json();
const sortedReleasesByVersion = releases.sort((a, b) => semver.rcompare(a.tag_name, b.tag_name));
const filteredReleases = sortedReleasesByVersion.filter(release => includePreReleases || !release.prerelease);
return filteredReleases;
};
private checkForLatestRelease = async (): Promise<void> => {
if (this.isUpdateInProgress) {
this.logger_.info('An update check is already in progress');
return;
}
this.isUpdateInProgress = true;
try {
const release: GitHubRelease = await this.fetchLatestRelease(this.includePreReleases_);
try {
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName(), process.arch);
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
// The file name is rebuilt from the channel, so without this Windows
// arm64 would request latest.yml and get the x64 build.
const channel = shim.platformName() === 'win32' && process.arch === 'arm64' ? 'latest-win-arm64' : 'latest';
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl, channel });
const result = await autoUpdater.checkForUpdates();
// Wait for the installation to finish. By default, .checkForUpdates runs in the background
await result.downloadPromise;
} catch (error) {
this.logger_.error(`Update download url failed: ${error.message}`);
this.isUpdateInProgress = false;
}
} catch (error) {
this.logger_.error(`Fetching releases failed: ${error.message}`);
this.isUpdateInProgress = false;
} finally {
this.isManualCheckInProgress = false;
}
};
private configureAutoUpdater = (): void => {
autoUpdater.logger = (this.logger_) as Logger;
if (this.devMode_) {
this.logger_.info('Development mode: using dev-app-update.yml');
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml');
autoUpdater.forceDevUpdateConfig = this.enableDevMode;
}
autoUpdater.autoDownload = this.enableAutoDownload;
autoUpdater.autoInstallOnAppQuit = this.autoInstallOnAppQuit;
autoUpdater.allowPrerelease = this.includePreReleases_;
autoUpdater.allowDowngrade = this.allowDowngrade;
autoUpdater.on(AutoUpdaterEvents.CheckingForUpdate, this.onCheckingForUpdate);
autoUpdater.on(AutoUpdaterEvents.UpdateNotAvailable, this.onUpdateNotAvailable);
autoUpdater.on(AutoUpdaterEvents.UpdateAvailable, this.onUpdateAvailable);
autoUpdater.on(AutoUpdaterEvents.DownloadProgress, this.onDownloadProgress);
autoUpdater.on(AutoUpdaterEvents.UpdateDownloaded, this.onUpdateDownloaded);
autoUpdater.on(AutoUpdaterEvents.Error, this.onError);
};
private onCheckingForUpdate = () => {
this.logger_.info('Checking for update...');
};
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
if (this.isManualCheckInProgress) {
this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable);
}
this.isUpdateInProgress = false;
this.logger_.info('Update not available.');
};
private onUpdateAvailable = (info: UpdateInfo): void => {
this.logger_.info(`Update available: ${info.version}.`);
};
private onDownloadProgress = (progressObj: { bytesPerSecond: number; percent: number; transferred: number; total: number }): void => {
this.logger_.info(`Download progress... ${progressObj.percent}% completed`);
};
private onUpdateDownloaded = (info: UpdateInfo): void => {
this.logger_.info('Update downloaded.');
this.isUpdateInProgress = false;
void this.promptUserToUpdate(info);
};
private onError = (error: Error): void => {
this.logger_.error('Error in auto-updater.', error);
this.isUpdateInProgress = false;
};
private promptUserToUpdate = async (info: UpdateInfo): Promise<void> => {
this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info);
};
}

查看文件

@@ -0,0 +1,7 @@
# -- local sever
# provider: generic
# url: http://localhost:8080
# -- github releases
provider: github
owner: laurent22
repo: joplin