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:
parent
4fa61e443f
commit
5763de3b26
@ -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
|
||||
|
2
.github/workflows/build-macos-m1.yml
vendored
2
.github/workflows/build-macos-m1.yml
vendored
@ -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
4
.gitignore
vendored
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
|
69
packages/app-desktop/tools/generateLatestArm64Yml.ts
Normal file
69
packages/app-desktop/tools/generateLatestArm64Yml.ts
Normal 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;
|
||||
};
|
105
packages/app-desktop/tools/githubReleasesUtils.ts
Normal file
105
packages/app-desktop/tools/githubReleasesUtils.ts
Normal 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.`);
|
||||
}
|
||||
};
|
93
packages/app-desktop/tools/modifyReleaseAssets.ts
Normal file
93
packages/app-desktop/tools/modifyReleaseAssets.ts
Normal 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();
|
@ -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();
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user