commit
b8bb0df66f
@ -0,0 +1,192 @@ |
|||||||
|
import {Octokit} from 'octokit' |
||||||
|
import * as fs from 'fs' |
||||||
|
import * as path from 'path' |
||||||
|
import YAML from 'yaml' |
||||||
|
import crypto from 'crypto' |
||||||
|
|
||||||
|
const owner = 'bunsenstraat' |
||||||
|
const repo = 'remix-desktop' |
||||||
|
const headers = { |
||||||
|
'X-GitHub-Api-Version': '2022-11-28', |
||||||
|
} |
||||||
|
|
||||||
|
const octokit = new Octokit({ |
||||||
|
auth: process.env.GH_TOKEN, |
||||||
|
}) |
||||||
|
|
||||||
|
async function getAllReleases() { |
||||||
|
const releases = await octokit.request('GET /repos/{owner}/{repo}/releases', { |
||||||
|
owner: owner, |
||||||
|
repo: repo, |
||||||
|
headers: headers, |
||||||
|
}) |
||||||
|
return releases.data |
||||||
|
} |
||||||
|
|
||||||
|
async function uploadReleaseAsset(release, name, file) { |
||||||
|
const upload_url = release.upload_url |
||||||
|
console.log(`Uploading ${name} to ${upload_url}`) |
||||||
|
octokit.request({ |
||||||
|
method: "POST", |
||||||
|
url: upload_url, |
||||||
|
headers: { |
||||||
|
"content-type": "text/plain", |
||||||
|
}, |
||||||
|
data: fs.readFileSync(file), |
||||||
|
name, |
||||||
|
label: name |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async function getVersionFromPackageJson() { |
||||||
|
// ignore ts error
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const packageJson = require(__dirname + '/../../../apps/remixdesktop/package.json') |
||||||
|
return packageJson.version |
||||||
|
} |
||||||
|
|
||||||
|
async function readReleaseFilesFromLocalDirectory() { |
||||||
|
const directoryPath = path.join(__dirname, '../../../release') |
||||||
|
const files = fs.readdirSync(directoryPath) |
||||||
|
return files |
||||||
|
} |
||||||
|
|
||||||
|
async function removeAsset(asset) { |
||||||
|
await octokit.request('DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}', { |
||||||
|
owner: owner, |
||||||
|
repo: repo, |
||||||
|
asset_id: asset.id, |
||||||
|
headers: headers, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async function hashFile(file): Promise<string> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const hash = crypto.createHash('sha512').setEncoding('base64'); |
||||||
|
// hash.on('error', reject).setEncoding(encoding);
|
||||||
|
fs.createReadStream( |
||||||
|
file, |
||||||
|
Object.assign({}, {}, { |
||||||
|
highWaterMark: 1024 * 1024, |
||||||
|
/* better to use more memory but hash faster */ |
||||||
|
}) |
||||||
|
) |
||||||
|
.on('error', reject) |
||||||
|
.on('end', () => { |
||||||
|
hash.end(); |
||||||
|
console.log('hash done'); |
||||||
|
console.log(hash.read()); |
||||||
|
resolve(hash.digest('base64')); |
||||||
|
}) |
||||||
|
.pipe( |
||||||
|
hash, |
||||||
|
{ |
||||||
|
end: false, |
||||||
|
} |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async function main() { |
||||||
|
const allReleases = await getAllReleases() |
||||||
|
const version = await getVersionFromPackageJson() |
||||||
|
console.log(`preparing release version: ${version}`) |
||||||
|
let release |
||||||
|
allReleases.find((r) => { |
||||||
|
if (r.tag_name === `v${version}`) { |
||||||
|
release = r |
||||||
|
} |
||||||
|
}) |
||||||
|
if (!release) { |
||||||
|
console.log('No release found.') |
||||||
|
// create release
|
||||||
|
console.log(`Creating release ${version}`) |
||||||
|
const r = await octokit.request('POST /repos/{owner}/{repo}/releases', { |
||||||
|
owner: owner, |
||||||
|
repo: repo, |
||||||
|
tag_name: `v${version}`, |
||||||
|
name: `${version}`, |
||||||
|
draft: true, |
||||||
|
headers: headers, |
||||||
|
}) |
||||||
|
release = r.data |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
let ymlFiles = await readReleaseFilesFromLocalDirectory() |
||||||
|
ymlFiles = ymlFiles.filter((file) => file.endsWith('.yml') && file.startsWith('latest')) |
||||||
|
|
||||||
|
console.log(`Found ${ymlFiles.length} yml files to upload`) |
||||||
|
|
||||||
|
// read and parse yml latest files
|
||||||
|
// the yml files contain the sha512 hash and file size of the executable
|
||||||
|
// we need to recalculate the hash and file size of the executable
|
||||||
|
// and update the yml files
|
||||||
|
// this is because the executable is resigned after the yml files are created
|
||||||
|
for (const file of ymlFiles) { |
||||||
|
const content = fs.readFileSync(path.join(__dirname, '../../../release', file), 'utf8') |
||||||
|
const parsed = YAML.parse(content) |
||||||
|
const hashes:{ |
||||||
|
url: string, |
||||||
|
sha512: string, |
||||||
|
size: number |
||||||
|
}[] = [] |
||||||
|
if(parsed.files) { |
||||||
|
console.log(`Found`, parsed.files) |
||||||
|
for (const f of parsed.files) { |
||||||
|
const executable = f.url |
||||||
|
const exists = fs.existsSync(path.join(__dirname, '../../../release', executable)) |
||||||
|
if (!exists) { |
||||||
|
console.log(`File ${executable} does not exist on local fs. Skipping...`) |
||||||
|
continue |
||||||
|
}else{ |
||||||
|
console.log(`File ${executable} exists on local fs. Recalculating hash...`) |
||||||
|
// calculate sha512 hash of executable
|
||||||
|
const hash:string = await hashFile(path.join(__dirname, '../../../release', executable)) |
||||||
|
console.log(hash) |
||||||
|
// calculate file size of executable
|
||||||
|
const stats = fs.statSync(path.join(__dirname, '../../../release', executable)) |
||||||
|
const fileSizeInBytes = stats.size |
||||||
|
console.log(fileSizeInBytes) |
||||||
|
hashes.push({ |
||||||
|
url: executable, |
||||||
|
sha512: hash, |
||||||
|
size: fileSizeInBytes |
||||||
|
}) |
||||||
|
if(parsed.path === executable) { |
||||||
|
parsed.sha512 = hash |
||||||
|
parsed.size = fileSizeInBytes |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
console.log(hashes) |
||||||
|
parsed.files = hashes |
||||||
|
const newYml = YAML.stringify(parsed) |
||||||
|
fs.writeFileSync(path.join(__dirname, '../../../release', file), newYml) |
||||||
|
} |
||||||
|
|
||||||
|
let files = await readReleaseFilesFromLocalDirectory() |
||||||
|
|
||||||
|
files = files.filter((file) => file.endsWith('.zip') || file.endsWith('.dmg') || file.endsWith('.exe') || file.endsWith('.AppImage') || file.endsWith('.snap') || file.endsWith('.deb') || file.startsWith('latest')) |
||||||
|
console.log(`Found ${files.length} files to upload`) |
||||||
|
console.log(files) |
||||||
|
if (!release.draft) { |
||||||
|
console.log(`Release ${version} is not a draft. Aborting...`) |
||||||
|
return |
||||||
|
} |
||||||
|
// upload files
|
||||||
|
for (const file of files) { |
||||||
|
// check if it is already uploaded
|
||||||
|
const asset = release.assets.find((a) => a.label === file) |
||||||
|
if (asset) { |
||||||
|
console.log(`Asset ${file} already uploaded... replacing it`) |
||||||
|
// remove it first
|
||||||
|
await removeAsset(asset) |
||||||
|
} |
||||||
|
await uploadReleaseAsset(release, file, path.join(__dirname, '../../../release', file)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
main() |
||||||
|
|
@ -0,0 +1,55 @@ |
|||||||
|
import { ElectronPlugin } from '@remixproject/engine-electron' |
||||||
|
|
||||||
|
const profile = { |
||||||
|
displayName: 'appUpdater', |
||||||
|
name: 'appUpdater', |
||||||
|
description: 'appUpdater', |
||||||
|
} |
||||||
|
|
||||||
|
export class appUpdaterPlugin extends ElectronPlugin { |
||||||
|
constructor() { |
||||||
|
console.log('appUpdaterPlugin') |
||||||
|
super(profile) |
||||||
|
} |
||||||
|
|
||||||
|
onActivation(): void { |
||||||
|
this.on('appUpdater', 'askForUpdate', () => { |
||||||
|
console.log('askForUpdate') |
||||||
|
const upgradeModal = { |
||||||
|
id: 'confirmUpdate', |
||||||
|
title: 'An update is available', |
||||||
|
message: `A new version of Remix Desktop is available. Do you want to update?`, |
||||||
|
modalType: 'modal', |
||||||
|
okLabel: 'Yes', |
||||||
|
cancelLabel: 'No', |
||||||
|
okFn: () => { |
||||||
|
this.call('appUpdater', 'download') |
||||||
|
}, |
||||||
|
cancelFn: () => { |
||||||
|
|
||||||
|
}, |
||||||
|
hideFn: () => null |
||||||
|
} |
||||||
|
this.call('notification', 'modal', upgradeModal) |
||||||
|
}) |
||||||
|
this.on('appUpdater', 'downloadReady', () => { |
||||||
|
console.log('downloadReady') |
||||||
|
const upgradeModal = { |
||||||
|
id: 'confirmInstall', |
||||||
|
title: 'An update is ready to install', |
||||||
|
message: `A new version of Remix Desktop is ready to install. Do you want to install it now? This will close Remix Desktop.`, |
||||||
|
modalType: 'modal', |
||||||
|
okLabel: 'Yes', |
||||||
|
cancelLabel: 'No', |
||||||
|
okFn: () => { |
||||||
|
this.call('appUpdater', 'install') |
||||||
|
}, |
||||||
|
cancelFn: () => { |
||||||
|
|
||||||
|
}, |
||||||
|
hideFn: () => null |
||||||
|
} |
||||||
|
this.call('notification', 'modal', upgradeModal) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" |
||||||
|
import { Profile } from "@remixproject/plugin-utils" |
||||||
|
import { autoUpdater } from "electron-updater" |
||||||
|
import { app } from 'electron'; |
||||||
|
|
||||||
|
const profile = { |
||||||
|
displayName: 'appUpdater', |
||||||
|
name: 'appUpdater', |
||||||
|
description: 'appUpdater', |
||||||
|
} |
||||||
|
|
||||||
|
export class AppUpdaterPlugin extends ElectronBasePlugin { |
||||||
|
clients: AppUpdaterPluginClient[] = [] |
||||||
|
constructor() { |
||||||
|
console.log('AppUpdaterPlugin') |
||||||
|
super(profile, clientProfile, AppUpdaterPluginClient) |
||||||
|
this.methods = [...super.methods] |
||||||
|
|
||||||
|
autoUpdater.autoDownload = false |
||||||
|
autoUpdater.disableDifferentialDownload = true |
||||||
|
|
||||||
|
autoUpdater.on('checking-for-update', () => { |
||||||
|
console.log('Checking for update...'); |
||||||
|
this.sendToLog('Checking for update...') |
||||||
|
}) |
||||||
|
autoUpdater.on('update-available', (info: any) => { |
||||||
|
console.log('Update available.', info); |
||||||
|
this.sendToLog('Update available.') |
||||||
|
for (const client of this.clients) { |
||||||
|
client.askForUpdate() |
||||||
|
} |
||||||
|
}) |
||||||
|
autoUpdater.on('update-not-available', () => { |
||||||
|
console.log('Update not available.'); |
||||||
|
this.sendToLog('App is already up to date.') |
||||||
|
|
||||||
|
}) |
||||||
|
autoUpdater.on('error', (err) => { |
||||||
|
console.log('Error in auto-updater. ' + err); |
||||||
|
this.sendToLog('Cannot find updates...') |
||||||
|
}) |
||||||
|
autoUpdater.on('download-progress', (progressObj) => { |
||||||
|
let log_message = "Download speed: " + progressObj.bytesPerSecond; |
||||||
|
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'; |
||||||
|
log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')'; |
||||||
|
console.log(log_message); |
||||||
|
this.sendToLog(log_message) |
||||||
|
}) |
||||||
|
autoUpdater.on('update-downloaded', (info) => { |
||||||
|
console.log('Update downloaded'); |
||||||
|
this.sendToLog('Update downloaded') |
||||||
|
for(const client of this.clients) { |
||||||
|
client.downloadReady() |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async sendToLog(message: string): Promise<void> { |
||||||
|
for (const client of this.clients) { |
||||||
|
client.call('terminal', 'log', { |
||||||
|
type: 'log', |
||||||
|
value: message, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
const clientProfile: Profile = { |
||||||
|
name: 'appUpdater', |
||||||
|
displayName: 'appUpdater', |
||||||
|
description: 'appUpdater', |
||||||
|
methods: ['checkForUpdates', 'download', 'install'], |
||||||
|
} |
||||||
|
|
||||||
|
class AppUpdaterPluginClient extends ElectronBasePluginClient { |
||||||
|
constructor(webContentsId: number, profile: Profile) { |
||||||
|
console.log('AppUpdaterPluginClient') |
||||||
|
super(webContentsId, profile) |
||||||
|
} |
||||||
|
|
||||||
|
async onActivation(): Promise<void> { |
||||||
|
console.log('onActivation', 'appUpdaterPluginClient') |
||||||
|
this.onload(async () => { |
||||||
|
console.log('onload', 'appUpdaterPluginClient') |
||||||
|
this.emit('loaded') |
||||||
|
await this.checkForUpdates() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async askForUpdate(): Promise<void> { |
||||||
|
this.emit('askForUpdate') |
||||||
|
} |
||||||
|
|
||||||
|
async downloadReady(): Promise<void> { |
||||||
|
this.emit('downloadReady') |
||||||
|
} |
||||||
|
|
||||||
|
async download(): Promise<void> { |
||||||
|
autoUpdater.downloadUpdate() |
||||||
|
} |
||||||
|
|
||||||
|
async install(): Promise<void> { |
||||||
|
autoUpdater.quitAndInstall() |
||||||
|
app.exit() |
||||||
|
} |
||||||
|
|
||||||
|
async checkForUpdates(): Promise<void> { |
||||||
|
console.log('checkForUpdates') |
||||||
|
this.call('terminal', 'log', { |
||||||
|
type: 'log', |
||||||
|
value: 'Remix Desktop version: ' + autoUpdater.currentVersion, |
||||||
|
}) |
||||||
|
autoUpdater.checkForUpdates() |
||||||
|
} |
||||||
|
} |
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue