1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-17 18:44:45 +02:00

Desktop: Seamless-Updates: generated and uploaded latest-mac-arm64.yml to GitHub Releases (#11042)

This commit is contained in:
Alice 2024-09-15 00:16:42 +03:00 committed by GitHub
parent 4fa61e443f
commit 5763de3b26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 278 additions and 193 deletions

View File

@ -518,8 +518,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js

View File

@ -80,7 +80,7 @@ jobs:
echo "Building and publishing desktop application..."
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
yarn renameReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
else
echo "Building but *not* publishing desktop application..."

4
.gitignore vendored
View File

@ -495,8 +495,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js

View File

@ -2,7 +2,6 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
const sha512 = require('js-sha512');
const crypto = require('crypto');
const distDirName = 'dist';
const distPath = path.join(__dirname, distDirName);
@ -32,87 +31,9 @@ const generateChecksumFile = () => {
return sha512FilePath;
};
const generateLatestArm64Yml = () => {
if (os.platform() !== 'darwin' && process.arch !== 'arm64') {
return;
}
const calculateHash = (filePath) => {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha512');
hashSum.update(fileBuffer);
return hashSum.digest('base64');
};
const getFileSize = (filePath) => {
return fs.statSync(filePath).size;
};
const extractVersion = (filePath) => {
return path.basename(filePath).split('-')[1];
};
const files = fs.readdirSync(distPath);
let dmgPath = '';
let zipPath = '';
for (const file of files) {
if (file.endsWith('arm64.dmg')) {
const fileRenamed = `${file.slice(0, -4)}.DMG`; // renameReleaseAssets script will rename from .dmg to .DMG
dmgPath = path.join(distPath, fileRenamed);
} else if (file.endsWith('arm64.zip')) {
zipPath = path.join(distPath, file);
}
}
const versionFromFilePath = extractVersion(zipPath);
const info = {
version: versionFromFilePath,
dmgPath: dmgPath,
zipPath: zipPath,
releaseDate: new Date().toISOString(),
};
/* eslint-disable no-console */
if (!fs.existsSync(info.dmgPath) || !fs.existsSync(info.zipPath)) {
console.error('One or both executable files do not exist:', info.dmgPath, info.zipPath);
return;
}
console.info('Calculating hash of files...');
const dmgHash = calculateHash(info.dmgPath);
const zipHash = calculateHash(info.zipPath);
console.info('Calculating size of files...');
const dmgSize = getFileSize(info.dmgPath);
const zipSize = getFileSize(info.zipPath);
console.info('Generating content of latest-mac-arm64.yml file...');
const yamlFilePath = path.join(distPath, 'latest-mac-arm64.yml');
const yamlContent = `version: ${info.version}
files:
- url: ${path.basename(info.zipPath)}
sha512: ${zipHash}
size: ${zipSize}
- url: ${path.basename(info.dmgPath)}
sha512: ${dmgHash}
size: ${dmgSize}
path: ${path.basename(info.zipPath)}
sha512: ${zipHash}
releaseDate: '${info.releaseDate}'
`;
fs.writeFileSync(yamlFilePath, yamlContent);
console.log('YML file generated successfully for arm64 architecure.');
const fileContent = fs.readFileSync(yamlFilePath, 'utf8');
console.log('Generated YML Content:\n', fileContent);
/* eslint-enable no-console */
return yamlFilePath;
};
const mainHook = () => {
const sha512FilePath = generateChecksumFile();
const lastestArm64YmlFilePath = generateLatestArm64Yml();
const outputFiles = [sha512FilePath, lastestArm64YmlFilePath].filter(item => item);
const outputFiles = [sha512FilePath].filter(item => item);
return outputFiles;
};

View File

@ -15,7 +15,7 @@
"test": "jest",
"test-ui": "playwright test",
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
},
"repository": {
"type": "git",

View File

@ -0,0 +1,69 @@
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
export interface GenerateInfo {
version: string;
dmgPath: string;
zipPath: string;
releaseDate: string;
}
const calculateHash = (filePath: string): string => {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha512');
hashSum.update(fileBuffer);
return hashSum.digest('base64');
};
const getFileSize = (filePath: string): number => {
return fs.statSync(filePath).size;
};
export const generateLatestArm64Yml = (info: GenerateInfo, destinationPath: string): string | undefined => {
if (!fs.existsSync(info.dmgPath) || !fs.existsSync(info.zipPath)) {
throw new Error(`One or both executable files do not exist: ${info.dmgPath}, ${info.zipPath}`);
}
if (!info.version) {
throw new Error('Version is empty');
}
if (!destinationPath) {
throw new Error('Destination path is empty');
}
console.info('Calculating hash of files...');
const dmgHash: string = calculateHash(info.dmgPath);
const zipHash: string = calculateHash(info.zipPath);
console.info('Calculating size of files...');
const dmgSize: number = getFileSize(info.dmgPath);
const zipSize: number = getFileSize(info.zipPath);
console.info('Generating content of latest-mac-arm64.yml file...');
if (!fs.existsSync(destinationPath)) {
fs.mkdirSync(destinationPath);
}
const yamlFilePath: string = path.join(destinationPath, 'latest-mac-arm64.yml');
const yamlContent = `version: ${info.version}
files:
- url: ${path.basename(info.zipPath)}
sha512: ${zipHash}
size: ${zipSize}
- url: ${path.basename(info.dmgPath)}
sha512: ${dmgHash}
size: ${dmgSize}
path: ${path.basename(info.zipPath)}
sha512: ${zipHash}
releaseDate: '${info.releaseDate}'
`;
fs.writeFileSync(yamlFilePath, yamlContent);
console.log(`YML file for version ${info.version} was generated successfully at ${destinationPath} for arm64.`);
const fileContent: string = fs.readFileSync(yamlFilePath, 'utf8');
console.log('Generated YML Content:\n', fileContent);
return yamlFilePath;
};

View File

@ -0,0 +1,105 @@
import * as fs from 'fs';
import { createWriteStream } from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { GitHubRelease, GitHubReleaseAsset } from '../utils/checkForUpdatesUtils';
const pipeline = promisify(require('stream').pipeline);
export interface Context {
repo: string; // {owner}/{repo}
githubToken: string;
targetTag: string;
}
const apiBaseUrl = 'https://api.github.com/repos/';
const defaultApiHeaders = (context: Context) => ({
'Authorization': `token ${context.githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
});
export const getTargetRelease = async (context: Context, targetTag: string): Promise<GitHubRelease> => {
console.log('Fetching releases...');
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
// The latter doesn't include draft releases.
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
method: 'GET',
headers: defaultApiHeaders(context),
});
const releases = await result.json();
if (!result.ok) {
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
}
for (const release of releases) {
if (release.tag_name === targetTag) {
return release;
}
}
throw new Error(`No release with tag ${targetTag} found!`);
};
// Download a file from Joplin Desktop releases
export const downloadFile = async (asset: GitHubReleaseAsset, destinationDir: string): Promise<string> => {
const downloadPath = path.join(destinationDir, asset.name);
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir);
}
/* eslint-disable no-console */
console.log(`Downloading ${asset.name} to ${downloadPath}`);
const response = await fetch(asset.browser_download_url);
if (!response.ok) {
throw new Error(`Failed to download file: Status Code ${response.status}`);
}
const fileStream = createWriteStream(downloadPath);
await pipeline(response.body, fileStream);
console.log('Download successful!');
/* eslint-enable no-console */
return downloadPath;
};
export const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
const result = await fetch(assetUrl, {
method: 'PATCH',
headers: defaultApiHeaders(context),
body: JSON.stringify({
name: newName,
}),
});
if (!result.ok) {
throw new Error(`Unable to update release asset: ${await result.text()}`);
}
};
export const uploadReleaseAsset = async (context: Context, release: GitHubRelease, filePath: string): Promise<void> => {
console.log(`Uploading file from ${filePath} to release ${release.tag_name}`);
const fileContent = fs.readFileSync(filePath);
const fileName = path.basename(filePath);
const uploadUrl = `https://uploads.github.com/repos/${context.repo}/releases/${release.id}/assets?name=${encodeURIComponent(fileName)}`;
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
...defaultApiHeaders(context),
'Content-Type': 'application/octet-stream',
},
body: fileContent,
});
if (!response.ok) {
throw new Error(`Failed to upload asset: ${await response.text()}`);
} else {
console.log(`${fileName} uploaded successfully.`);
}
};

View File

@ -0,0 +1,93 @@
import path = require('path');
import { parseArgs } from 'util';
import { Context, downloadFile, getTargetRelease, updateReleaseAsset, uploadReleaseAsset } from './githubReleasesUtils';
import { GitHubRelease } from '../utils/checkForUpdatesUtils';
import { GenerateInfo, generateLatestArm64Yml } from './generateLatestArm64Yml';
const basePath = path.join(__dirname, '..');
const downloadDir = path.join(basePath, 'downloads');
// Renames release assets in Joplin Desktop releases
const renameReleaseAssets = async (context: Context, release: GitHubRelease) => {
// Patterns used to rename releases
const renamePatterns: [RegExp, string][] = [
[/-arm64\.dmg$/, '-arm64.DMG'],
];
for (const asset of release.assets) {
for (const [pattern, replacement] of renamePatterns) {
if (asset.name.match(pattern)) {
const newName = asset.name.replace(pattern, replacement);
await updateReleaseAsset(context, asset.url, newName);
// Only rename a release once.
break;
}
}
}
};
// Creates release assets in Joplin Desktop releases
const createReleaseAssets = async (context: Context, release: GitHubRelease) => {
// Create latest-mac-arm64.yml file and publish
let dmgPath;
let zipPath;
for (const asset of release.assets) {
if (asset.name.endsWith('arm64.zip')) {
zipPath = await downloadFile(asset, downloadDir);
} else if (asset.name.endsWith('arm64.DMG')) {
dmgPath = await downloadFile(asset, downloadDir);
}
}
const info: GenerateInfo = {
version: release.tag_name.slice(1),
dmgPath: dmgPath,
zipPath: zipPath,
releaseDate: new Date().toISOString(),
};
const latestArm64FilePath = generateLatestArm64Yml(info, downloadDir);
void uploadReleaseAsset(context, release, latestArm64FilePath);
};
const modifyReleaseAssets = async () => {
const args = parseArgs({
options: {
tag: { type: 'string' },
token: { type: 'string' },
repo: { type: 'string' },
},
});
if (!args.values.tag || !args.values.token || !args.values.repo) {
throw new Error([
'Required arguments: --tag, --token, --repo',
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
' --token should be a GitHub API token',
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
].join('\n'));
}
const context: Context = {
repo: args.values.repo,
githubToken: args.values.token,
targetTag: args.values.tag,
};
const release = await getTargetRelease(context, context.targetTag);
if (!release.assets) {
console.log(release);
throw new Error(`Release ${release.tag_name} missing assets!`);
}
console.log('Renaming release assets for tag', context.targetTag, context.repo);
void renameReleaseAssets(context, release);
console.log('Creating latest-mac-arm64.yml asset for tag', context.targetTag, context.repo);
void createReleaseAssets(context, release);
};
void modifyReleaseAssets();

View File

@ -1,109 +0,0 @@
import { parseArgs } from 'util';
interface Context {
repo: string; // {owner}/{repo}
githubToken: string;
}
const apiBaseUrl = 'https://api.github.com/repos/';
const defaultApiHeaders = (context: Context) => ({
'Authorization': `token ${context.githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
});
const getTargetRelease = async (context: Context, targetTag: string) => {
console.log('Fetching releases...');
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
// The latter doesn't include draft releases.
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
method: 'GET',
headers: defaultApiHeaders(context),
});
const releases = await result.json();
if (!result.ok) {
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
}
for (const release of releases) {
if (release.tag_name === targetTag) {
return release;
}
}
throw new Error(`No release with tag ${targetTag} found!`);
};
const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
const result = await fetch(assetUrl, {
method: 'PATCH',
headers: defaultApiHeaders(context),
body: JSON.stringify({
name: newName,
}),
});
if (!result.ok) {
throw new Error(`Unable to update release asset: ${await result.text()}`);
}
};
// Renames release assets in Joplin Desktop releases
const renameReleaseAssets = async () => {
const args = parseArgs({
options: {
tag: { type: 'string' },
token: { type: 'string' },
repo: { type: 'string' },
},
});
if (!args.values.tag || !args.values.token || !args.values.repo) {
throw new Error([
'Required arguments: --tag, --token, --repo',
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
' --token should be a GitHub API token',
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
].join('\n'));
}
const context: Context = {
repo: args.values.repo,
githubToken: args.values.token,
};
console.log('Renaming release assets for tag', args.values.tag, context.repo);
const release = await getTargetRelease(context, args.values.tag);
if (!release.assets) {
console.log(release);
throw new Error(`Release ${release.name} missing assets!`);
}
// Patterns used to rename releases
const renamePatterns = [
[/-arm64\.dmg$/, '-arm64.DMG'],
];
for (const asset of release.assets) {
for (const [pattern, replacement] of renamePatterns) {
if (asset.name.match(pattern)) {
const newName = asset.name.replace(pattern, replacement);
await updateReleaseAsset(context, asset.url, newName);
// Only rename a release once.
break;
}
}
}
};
void renameReleaseAssets();

View File

@ -7,9 +7,11 @@ export interface CheckForUpdateOptions {
export interface GitHubReleaseAsset {
name: string;
browser_download_url: string;
url?: string;
}
export interface GitHubRelease {
id?: string;
tag_name: string;
prerelease: boolean;
body: string;