Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a2c140ce | ||
|
|
3b1a13b84d | ||
|
|
cd54847af8 | ||
|
|
9dbbdb5ed2 | ||
|
|
f3c634120c | ||
|
|
ae74d22cde | ||
|
|
4a664e90eb | ||
|
|
13af32f535 |
+2
-2
@@ -5,5 +5,5 @@ TRAEFIK_NETWORK=internal
|
|||||||
TRAEFIK_CERT_RESOLVER=netcup
|
TRAEFIK_CERT_RESOLVER=netcup
|
||||||
|
|
||||||
# Keep these two values paired when deploying a newer published toolbox release.
|
# Keep these two values paired when deploying a newer published toolbox release.
|
||||||
TOOLBOX_RELEASE_VERSION=0.20.0
|
TOOLBOX_RELEASE_VERSION=0.20.2
|
||||||
TOOLBOX_RELEASE_SHA256=450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed
|
TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ ARG NGINX_IMAGE=nginxinc/nginx-unprivileged:1.31.3-alpine@sha256:18d67281256ded3
|
|||||||
|
|
||||||
FROM ${NGINX_IMAGE} AS release
|
FROM ${NGINX_IMAGE} AS release
|
||||||
|
|
||||||
ARG TOOLBOX_RELEASE_VERSION=0.20.0
|
ARG TOOLBOX_RELEASE_VERSION=0.20.2
|
||||||
ARG TOOLBOX_RELEASE_SHA256=450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed
|
ARG TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7
|
||||||
ARG TOOLBOX_RELEASE_BASE_URL=https://git.add-ideas.de/lotobo/toolbox-portal/releases/download
|
ARG TOOLBOX_RELEASE_BASE_URL=https://git.add-ideas.de/lotobo/toolbox-portal/releases/download
|
||||||
|
|
||||||
RUN set -eu; \
|
RUN set -eu; \
|
||||||
@@ -43,6 +43,7 @@ RUN set -eu; \
|
|||||||
test -f /tmp/toolbox/apps/diff/toolbox-app.json; \
|
test -f /tmp/toolbox/apps/diff/toolbox-app.json; \
|
||||||
test -f /tmp/toolbox/apps/time/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/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/unicode/toolbox-app.json; \
|
||||||
test -f /tmp/toolbox/apps/flow/toolbox-app.json; \
|
test -f /tmp/toolbox/apps/flow/toolbox-app.json; \
|
||||||
test -f /tmp/toolbox/apps/subtitle/toolbox-app.json; \
|
test -f /tmp/toolbox/apps/subtitle/toolbox-app.json; \
|
||||||
|
|||||||
@@ -99,7 +99,17 @@ Deploy a fresh clone with:
|
|||||||
```sh
|
```sh
|
||||||
git clone https://git.add-ideas.de/lotobo/toolbox-portal.git
|
git clone https://git.add-ideas.de/lotobo/toolbox-portal.git
|
||||||
cd toolbox-portal
|
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
|
docker compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Containerfile
|
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
|
restart: unless-stopped
|
||||||
read_only: true
|
read_only: true
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+3
-3
@@ -6,9 +6,9 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Containerfile.release
|
dockerfile: Containerfile.release
|
||||||
args:
|
args:
|
||||||
TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.0}"
|
TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.2}"
|
||||||
TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-450618902a1ee87405da463881e6f51f685691c2081259aff9c25a4aa6fe22ed}"
|
TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7}"
|
||||||
image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.0}"
|
image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.2}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@
|
|||||||
"check": "npm run format:check && npm run lint && npm run test && npm run build",
|
"check": "npm run format:check && npm run lint && npm run test && npm run build",
|
||||||
"licenses:generate": "node scripts/licenses.mjs",
|
"licenses:generate": "node scripts/licenses.mjs",
|
||||||
"assemble": "node scripts/assemble.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": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "^0.3.0",
|
"@add-ideas/toolbox-contract": "^0.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./toolbox-release-lock.schema.json",
|
"$schema": "./toolbox-release-lock.schema.json",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"releaseVersion": "0.20.0",
|
"releaseVersion": "0.20.3",
|
||||||
"portalVersion": "0.2.23",
|
"portalVersion": "0.2.23",
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"id": "de.add-ideas.toolbox",
|
"id": "de.add-ideas.toolbox",
|
||||||
@@ -188,6 +188,13 @@
|
|||||||
"sha256": "a1b635278da1baf844051fb0bbfc94022596bb674091c4c39ec164cf34bd5f62",
|
"sha256": "a1b635278da1baf844051fb0bbfc94022596bb674091c4c39ec164cf34bd5f62",
|
||||||
"target": "text"
|
"target": "text"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "de.add-ideas.translator-tools",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "de.add-ideas.unicode-tools",
|
"id": "de.add-ideas.unicode-tools",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -53,6 +53,7 @@ const expectedAppTargets = [
|
|||||||
'diff',
|
'diff',
|
||||||
'time',
|
'time',
|
||||||
'text',
|
'text',
|
||||||
|
'translator',
|
||||||
'unicode',
|
'unicode',
|
||||||
'flow',
|
'flow',
|
||||||
'subtitle',
|
'subtitle',
|
||||||
|
|||||||
@@ -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