From 11a2c140ce493dd4274bed9a33489ef6174ff5ea Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Thu, 3 Sep 2026 09:49:09 +0200 Subject: [PATCH] chore(portal): include translator-tools v0.1.2 in v0.20.3 release --- package.json | 3 +- release/toolbox.lock.json | 8 +- scripts/update-release-lock.mjs | 241 ++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 scripts/update-release-lock.mjs diff --git a/package.json b/package.json index b46bb1b..d407deb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/release/toolbox.lock.json b/release/toolbox.lock.json index 7261e95..c602762 100644 --- a/release/toolbox.lock.json +++ b/release/toolbox.lock.json @@ -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" }, { diff --git a/scripts/update-release-lock.mjs b/scripts/update-release-lock.mjs new file mode 100644 index 0000000..8d74322 --- /dev/null +++ b/scripts/update-release-lock.mjs @@ -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); +});