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
一些检查失败了
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
这个提交包含在:
@@ -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
|
||||
在新工单中引用
屏蔽一个用户