chore(portal): include translator-tools v0.1.2 in v0.20.3 release
Verify / verify (push) Canceled after 0s
Verify / verify (push) Canceled after 0s
This commit is contained in:
+2
-1
@@ -18,7 +18,8 @@
|
||||
"check": "npm run format:check && npm run lint && npm run test && npm run build",
|
||||
"licenses:generate": "node scripts/licenses.mjs",
|
||||
"assemble": "node scripts/assemble.mjs",
|
||||
"package:static": "node scripts/package-static.mjs"
|
||||
"package:static": "node scripts/package-static.mjs",
|
||||
"release:update": "node scripts/update-release-lock.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "^0.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "./toolbox-release-lock.schema.json",
|
||||
"schemaVersion": 1,
|
||||
"releaseVersion": "0.20.2",
|
||||
"releaseVersion": "0.20.3",
|
||||
"portalVersion": "0.2.23",
|
||||
"catalogue": {
|
||||
"id": "de.add-ideas.toolbox",
|
||||
@@ -190,9 +190,9 @@
|
||||
},
|
||||
{
|
||||
"id": "de.add-ideas.translator-tools",
|
||||
"version": "0.1.1",
|
||||
"artifact": "https://git.add-ideas.de/lotobo/translator-tools/releases/download/v0.1.1/translator-tools-0.1.1.zip",
|
||||
"sha256": "9ff7a14f7b0730bee327d78d2e25364f53a16afd8b675acb0ad81bee9a1710f4",
|
||||
"version": "0.1.2",
|
||||
"artifact": "https://git.add-ideas.de/lotobo/translator-tools/releases/download/v0.1.2/translator-tools-0.1.2.zip",
|
||||
"sha256": "2ff24f97e0c3893c4d31dc347bd760d91a0d4a565fb392ec1a1b22ba9835c34f",
|
||||
"target": "translator"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { access, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { parseReleaseLock } from './release-lock.mjs';
|
||||
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
const HEX_HASH = /^[a-f0-9]{64}$/;
|
||||
|
||||
function usage() {
|
||||
return `Usage: node scripts/update-release-lock.mjs --app-id ID --app-version VERSION --artifact URL --sha256 HEX --release-version SEMVER [options]
|
||||
|
||||
Options:
|
||||
--lock PATH Path to release lock (default: release/toolbox.lock.json)
|
||||
--portal-version SEMVER Optional portal package override
|
||||
--app-id ID App id in lock (for example de.add-ideas.translator-tools)
|
||||
--app-version VERSION App semantic version
|
||||
--artifact URL Artifact URL/path/file: URL
|
||||
--sha256 HEX Optional artifact checksum; required for remote artifacts
|
||||
--release-version VERSION New toolbox release version
|
||||
--assemble Run npm run build and npm run assemble after patching
|
||||
--force Pass --force to assemble command
|
||||
--help Show this message
|
||||
`;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
if (argv.includes('--help')) {
|
||||
console.log(usage());
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const option = argv[index];
|
||||
if (!option.startsWith('--')) {
|
||||
throw new Error(`Unknown argument: ${option}`);
|
||||
}
|
||||
|
||||
if (option === '--assemble' || option === '--force') {
|
||||
args.set(option, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined) {
|
||||
throw new Error(`${option} requires a value.`);
|
||||
}
|
||||
args.set(option, value);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
lockFile: args.get('--lock') ?? 'release/toolbox.lock.json',
|
||||
appId: args.get('--app-id'),
|
||||
appVersion: args.get('--app-version'),
|
||||
artifact: args.get('--artifact'),
|
||||
sha256: args.get('--sha256'),
|
||||
releaseVersion: args.get('--release-version'),
|
||||
portalVersion: args.get('--portal-version'),
|
||||
assemble: args.get('--assemble') === true,
|
||||
forceAssemble: args.get('--force') === true,
|
||||
};
|
||||
}
|
||||
|
||||
function bumpPatch(version) {
|
||||
const [major, minor, patchAndSuffix] = version.split('.');
|
||||
const [patch] = patchAndSuffix.split('-');
|
||||
const nextPatch = Number.parseInt(patch, 10) + 1;
|
||||
return `${major}.${minor}.${nextPatch}`;
|
||||
}
|
||||
|
||||
async function sha256ForPath(filePath) {
|
||||
await access(filePath);
|
||||
const hash = createHash('sha256');
|
||||
const input = createReadStream(filePath);
|
||||
await new Promise((resolve, reject) => {
|
||||
input.on('data', (chunk) => hash.update(chunk));
|
||||
input.on('error', reject);
|
||||
input.on('end', resolve);
|
||||
});
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
async function computeRemoteSha256(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to download artifact (${response.status} ${response.statusText}).`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Artifact download did not provide a response body.');
|
||||
}
|
||||
|
||||
const hash = createHash('sha256');
|
||||
const reader = response.body.getReader();
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
hash.update(Buffer.from(value));
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function resolveArtifactPath(artifact, lockDirectory) {
|
||||
if (/^file:/i.test(artifact)) return new URL(artifact).pathname;
|
||||
if (/^https?:/i.test(artifact)) return null;
|
||||
return path.resolve(lockDirectory, artifact);
|
||||
}
|
||||
|
||||
function toShaIfNeeded(hash) {
|
||||
if (!hash) return null;
|
||||
const normalized = hash.toLowerCase();
|
||||
if (!HEX_HASH.test(normalized)) {
|
||||
throw new Error('Provided --sha256 is not a valid 64 hex hash.');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function command(cwd) {
|
||||
return (name, args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(name, args, {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) =>
|
||||
code === 0
|
||||
? resolve(undefined)
|
||||
: reject(new Error(`${name} ${args.join(' ')} failed with code ${code}`))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (
|
||||
!options.appId ||
|
||||
!options.appVersion ||
|
||||
!options.artifact ||
|
||||
!options.releaseVersion
|
||||
) {
|
||||
throw new Error(`Missing required options.
|
||||
|
||||
${usage()}`);
|
||||
}
|
||||
if (!SEMVER.test(options.releaseVersion)) {
|
||||
throw new Error(`--release-version must be semver: ${options.releaseVersion}`);
|
||||
}
|
||||
if (!SEMVER.test(options.appVersion)) {
|
||||
throw new Error(`--app-version must be semver: ${options.appVersion}`);
|
||||
}
|
||||
|
||||
const lockPath = path.resolve(options.lockFile);
|
||||
const lockDir = path.dirname(lockPath);
|
||||
const lockJson = JSON.parse(await readFile(lockPath, 'utf8'));
|
||||
const lock = parseReleaseLock(lockJson);
|
||||
const nextReleaseVersion = options.releaseVersion ?? bumpPatch(lock.releaseVersion);
|
||||
if (!SEMVER.test(nextReleaseVersion)) {
|
||||
throw new Error(`Invalid release version: ${nextReleaseVersion}`);
|
||||
}
|
||||
|
||||
const appIndex = lock.apps.findIndex((app) => app.id === options.appId);
|
||||
if (appIndex < 0) {
|
||||
throw new Error(`App id not found in lock: ${options.appId}`);
|
||||
}
|
||||
|
||||
const artifactPath = resolveArtifactPath(options.artifact, lockDir);
|
||||
let sha256 = toShaIfNeeded(options.sha256);
|
||||
if (!sha256) {
|
||||
if (artifactPath) {
|
||||
sha256 = await sha256ForPath(artifactPath);
|
||||
} else {
|
||||
sha256 = await computeRemoteSha256(options.artifact);
|
||||
}
|
||||
}
|
||||
if (!HEX_HASH.test(sha256)) {
|
||||
throw new Error('Computed checksum is malformed.');
|
||||
}
|
||||
|
||||
const updatedApps = [...lock.apps];
|
||||
const previous = updatedApps[appIndex];
|
||||
updatedApps[appIndex] = {
|
||||
...previous,
|
||||
version: options.appVersion,
|
||||
artifact: options.artifact,
|
||||
sha256,
|
||||
};
|
||||
|
||||
const nextLock = {
|
||||
...lock,
|
||||
releaseVersion: nextReleaseVersion,
|
||||
apps: updatedApps,
|
||||
};
|
||||
if (options.portalVersion) {
|
||||
if (!SEMVER.test(options.portalVersion))
|
||||
throw new Error(`--portal-version must be semver: ${options.portalVersion}`);
|
||||
nextLock.portalVersion = options.portalVersion;
|
||||
}
|
||||
|
||||
parseReleaseLock(nextLock);
|
||||
await writeFile(lockPath, `${JSON.stringify(nextLock, null, 2)}\n`, 'utf8');
|
||||
console.log(`Updated ${path.relative(process.cwd(), lockPath)} (${options.appId})`);
|
||||
|
||||
if (!options.assemble) return;
|
||||
const runCommand = command(process.cwd());
|
||||
await runCommand('npm', ['run', 'build']);
|
||||
const archive = `build/add-ideas-toolbox-${nextReleaseVersion}.zip`;
|
||||
const assembleArgs = [
|
||||
'run',
|
||||
'assemble',
|
||||
'--',
|
||||
'--lock',
|
||||
options.lockFile,
|
||||
'--portal-dist',
|
||||
'dist',
|
||||
'--output',
|
||||
'build/toolbox',
|
||||
'--archive',
|
||||
archive,
|
||||
];
|
||||
if (options.forceAssemble) assembleArgs.push('--force');
|
||||
await runCommand('npm', assembleArgs);
|
||||
console.log(`Assembled release -> ${archive}`);
|
||||
const releaseChecksum = path.join(
|
||||
path.dirname(archive),
|
||||
`${path.basename(archive)}.sha256`
|
||||
);
|
||||
const releaseSha256 = await sha256ForPath(releaseChecksum === undefined ? '' : releaseChecksum);
|
||||
console.log(`Release checksum: ${releaseSha256}`);
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user