Compare commits

..
6 Commits
Author SHA1 Message Date
zemion 11a2c140ce chore(portal): include translator-tools v0.1.2 in v0.20.3 release
Verify / verify (push) Canceled after 0s
2026-09-03 09:49:09 +02:00
zemion 3b1a13b84d chore(portal): bump embedded release to 0.20.2 with translator-tools 0.1.1
Verify / verify (push) Canceled after 0s
2026-09-03 09:16:53 +02:00
zemion cd54847af8 chore(portal): add deploy helper for untagged HEAD
Verify / verify (push) Canceled after 0s
2026-09-03 08:40:49 +02:00
zemion 9dbbdb5ed2 commit containerfile
Verify / verify (push) Canceled after 0s
2026-09-03 01:01:29 +02:00
zemion f3c634120c toolbox-portal: update translator-tools to v0.1.1
Verify / verify (push) Canceled after 0s
2026-09-02 23:58:17 +02:00
zemion ae74d22cde chore(toolbox-portal): default compose to release 0.20.1
Verify / verify (push) Canceled after 0s
2026-09-02 22:19:44 +02:00
10 changed files with 291 additions and 14 deletions
+2 -2
View File
@@ -5,5 +5,5 @@ TRAEFIK_NETWORK=internal
TRAEFIK_CERT_RESOLVER=netcup
# Keep these two values paired when deploying a newer published toolbox release.
TOOLBOX_RELEASE_VERSION=0.20.0
TOOLBOX_RELEASE_SHA256=450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed
TOOLBOX_RELEASE_VERSION=0.20.2
TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7
+3 -2
View File
@@ -2,8 +2,8 @@ ARG NGINX_IMAGE=nginxinc/nginx-unprivileged:1.31.3-alpine@sha256:18d67281256ded3
FROM ${NGINX_IMAGE} AS release
ARG TOOLBOX_RELEASE_VERSION=0.20.0
ARG TOOLBOX_RELEASE_SHA256=450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed
ARG TOOLBOX_RELEASE_VERSION=0.20.2
ARG TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7
ARG TOOLBOX_RELEASE_BASE_URL=https://git.add-ideas.de/lotobo/toolbox-portal/releases/download
RUN set -eu; \
@@ -43,6 +43,7 @@ RUN set -eu; \
test -f /tmp/toolbox/apps/diff/toolbox-app.json; \
test -f /tmp/toolbox/apps/time/toolbox-app.json; \
test -f /tmp/toolbox/apps/text/toolbox-app.json; \
test -f /tmp/toolbox/apps/translator/toolbox-app.json; \
test -f /tmp/toolbox/apps/unicode/toolbox-app.json; \
test -f /tmp/toolbox/apps/flow/toolbox-app.json; \
test -f /tmp/toolbox/apps/subtitle/toolbox-app.json; \
+11 -1
View File
@@ -99,7 +99,17 @@ Deploy a fresh clone with:
```sh
git clone https://git.add-ideas.de/lotobo/toolbox-portal.git
cd toolbox-portal
docker compose up -d --build
./scripts/deploy-update.sh
```
If you prefer the inline command:
```sh
git fetch origin --tags && \
git pull --ff-only origin main && \
{ TAG="$(git describe --tags --exact-match 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || true)"; \
if [ -n "$TAG" ]; then echo "$TAG"; else echo "warning: no matching tags found"; fi; } && \
docker compose up -d --build && \
docker compose ps
```
+1 -1
View File
@@ -3,7 +3,7 @@ services:
build:
context: .
dockerfile: Containerfile
image: git.add-ideas.de/lotobo/toolbox:0.20.0
image: git.add-ideas.de/lotobo/toolbox:0.20.2
restart: unless-stopped
read_only: true
ports:
+3 -3
View File
@@ -6,9 +6,9 @@ services:
context: .
dockerfile: Containerfile.release
args:
TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.0}"
TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed}"
image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.0}"
TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.2}"
TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7}"
image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.2}"
restart: unless-stopped
read_only: true
tmpfs:
+2 -1
View File
@@ -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",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"$schema": "./toolbox-release-lock.schema.json",
"schemaVersion": 1,
"releaseVersion": "0.20.1",
"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.0",
"artifact": "https://git.add-ideas.de/lotobo/translator-tools/releases/download/v0.1.0/translator-tools-0.1.0.zip",
"sha256": "8f18e7c629a51ed628e642945374f731b14e97168098318a2331bb419eceee8d",
"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"
},
{
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_DIR}"
git fetch origin --tags
git pull --ff-only origin main
if ! TAG="$(git describe --tags --exact-match 2>/dev/null)"; then
TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
if [ -z "${TAG}" ]; then
echo "warning: current commit is untagged and no tagged ancestor was found"
else
echo "warning: current commit is untagged; nearest tag is ${TAG}"
fi
else
echo "tagged: ${TAG}"
fi
docker compose up -d --build
docker compose ps
+1
View File
@@ -53,6 +53,7 @@ const expectedAppTargets = [
'diff',
'time',
'text',
'translator',
'unicode',
'flow',
'subtitle',
+241
View File
@@ -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);
});