Compare commits

...
5 Commits
Author SHA1 Message Date
zemion 68a7bcf89f chore(release): bundle Auth Tools 0.3.2 in Toolbox 0.20.6
Verify / verify (push) Canceled after 0s
2026-10-04 12:17:00 +02:00
zemion 673960a5cf v0.20.5 sha256 correction
Verify / verify (push) Canceled after 0s
2026-09-03 17:30:50 +02:00
zemion 33d6cd1cfd v0.20.5 additional commit
Verify / verify (push) Canceled after 0s
2026-09-03 17:28:10 +02:00
zemion d1d4b3f44e Swith to v0.20.3
Verify / verify (push) Canceled after 0s
2026-09-03 10:31:09 +02:00
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
16 changed files with 955 additions and 81 deletions
+2 -2
View File
@@ -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.2 TOOLBOX_RELEASE_VERSION=0.20.6
TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7 TOOLBOX_RELEASE_SHA256=37ccd887793d7a147ceb3b69933faa969a006c59d6b1fbec775d3622279cedac
+2 -2
View File
@@ -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.2 ARG TOOLBOX_RELEASE_VERSION=0.20.6
ARG TOOLBOX_RELEASE_SHA256=66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7 ARG TOOLBOX_RELEASE_SHA256=37ccd887793d7a147ceb3b69933faa969a006c59d6b1fbec775d3622279cedac
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; \
+36 -6
View File
@@ -1,7 +1,7 @@
# add·ideas Toolbox Portal # add·ideas Toolbox Portal
`toolbox-portal` is the static launcher and release assembler for the add·ideas `toolbox-portal` is the static launcher and release assembler for the add·ideas
browser toolbox. Version `0.2.23` reads one same-origin catalogue, shows each app browser toolbox. Version `0.2.24` reads one same-origin catalogue, shows each app
as a compact launch tile, and keeps personal pins, drag-and-drop ordering, as a compact launch tile, and keeps personal pins, drag-and-drop ordering,
visibility, and appearance in the current browser. Pinned tools have their own visibility, and appearance in the current browser. Pinned tools have their own
section; search, category, and clickable tag filters stay close to the tool section; search, category, and clickable tag filters stay close to the tool
@@ -80,7 +80,7 @@ privacy and executable-code warning. Light, dark, and system modes are supported
## Production deployment behind Traefik ## Production deployment behind Traefik
The committed `compose.yaml` is the shortest production path. Its release The committed `compose.yaml` is the shortest production path. Its release
container downloads the immutable Toolbox 0.20.0 ZIP during the image build, container downloads the immutable Toolbox 0.20.6 ZIP during the image build,
verifies SHA-256 before extracting it, and then copies only the verified static verifies SHA-256 before extracting it, and then copies only the verified static
files into the pinned unprivileged nginx image. Node.js and a local portal files into the pinned unprivileged nginx image. Node.js and a local portal
assembly are not required on the deployment host. assembly are not required on the deployment host.
@@ -121,6 +121,26 @@ release version, and matching checksum can be overridden through environment
variables; copy `.env.example` to `.env` only when an override is needed. For variables; copy `.env.example` to `.env` only when an override is needed. For
example, set `TOOLBOX_HOST=staging.toolbox.add-ideas.de` for a staging host. example, set `TOOLBOX_HOST=staging.toolbox.add-ideas.de` for a staging host.
## One-shot release wrapper
For release assembly and publishing, use the repository-local wrapper:
```sh
./scripts/release.sh
```
That script performs (in order):
- optional `npm test`
- `npm run build`
- `npm run assemble` (using the lockfile’s `releaseVersion`)
- tag and push (`v<releaseVersion>`)
- publish/update a Gitea release and upload
`add-ideas-toolbox-<version>.zip` and `.sha256`
Useful flags include `--skip-tests`, `--skip-git-push`, `--skip-publish`,
`--tag`, `--owner`, `--repo`, and `--dry-run`.
The same container also exposes only the packaged authentication app at The same container also exposes only the packaged authentication app at
`https://auth.toolbox.add-ideas.de/`. Traefik adds the internal `/apps/auth` `https://auth.toolbox.add-ideas.de/`. Traefik adds the internal `/apps/auth`
prefix while the public URL remains `/`, so assets remain relative and the browser prefix while the public URL remains `/`, so assets remain relative and the browser
@@ -154,10 +174,10 @@ The app release should also publish a matching `.sha256` sidecar. The manifest
must declare the pinned reverse-DNS id and version. Application and portal must declare the pinned reverse-DNS id and version. Application and portal
versions are independent. versions are independent.
`release/toolbox.lock.json` is the reviewed Toolbox v0.20.0 / Portal v0.2.23 `release/toolbox.lock.json` is the reviewed Toolbox v0.20.6 / Portal v0.2.24
lock. Its URLs and checksums pin PDF Tools 0.4.4, XSLT Tools 0.4.3, OneNote lock. Its URLs and checksums pin PDF Tools 0.4.4, XSLT Tools 0.4.3, OneNote
Tools 0.3.4, Audio & Video Tools 0.3.0, Regex Tools 0.4.2, OTP & Passkey Tools Tools 0.3.4, Audio & Video Tools 0.3.0, Regex Tools 0.4.2, OTP & Passkey Tools
0.3.1, Sudoku Tools 0.2.1, and the coordinated 0.2.0 releases of SVG, Colour, 0.3.2, Translator Tools 0.1.3, Sudoku Tools 0.2.1, and the coordinated 0.2.0 releases of SVG, Colour,
Office, Image, File, EPUB, Archive, Privacy, Crypto, Barcode, Network, Geo, Office, Image, File, EPUB, Archive, Privacy, Crypto, Barcode, Network, Geo,
Helper, Random, Data, Diff, Time, Text, Unicode, Flow, Subtitle, MIDI, 3D, Helper, Random, Data, Diff, Time, Text, Unicode, Flow, Subtitle, MIDI, 3D,
Query, Scan, Package, Label, Font, Fixture, Minimize, Format Lab, Repro, Schema, Query, Scan, Package, Label, Font, Fixture, Minimize, Format Lab, Repro, Schema,
@@ -365,6 +385,16 @@ the exact tag, install with `npm ci`, run the same verification/assembly command
and upload only the already-created ZIP/checksum and OCI image. The workflow and upload only the already-created ZIP/checksum and OCI image. The workflow
must not re-resolve app versions or substitute a newer release. must not re-resolve app versions or substitute a newer release.
## Toolbox 0.20.6 token QR display
Toolbox 0.20.6 bundles OTP & Passkey Tools 0.3.2. Use **Show QR code** in
the credential editor or beside an imported token to scan its enrollment code
with another device. The large dialog preserves issuer, account, secret,
algorithm, digits, period or HOTP counter, and provides a local SVG download.
QR codes remain black on white in both themes. Rendering errors are visible,
and Escape closes the display. The deployment defaults pin this release and
its verified archive checksum; pull `main` and rebuild with Docker Compose.
## Toolbox 0.20.0 portfolio expansion ## Toolbox 0.20.0 portfolio expansion
Toolbox 0.20.0 publishes the coordinated multi-repository depth pass. It adds: Toolbox 0.20.0 publishes the coordinated multi-repository depth pass. It adds:
@@ -394,14 +424,14 @@ exact immutable app archives and checksums.
## Existing tools ## Existing tools
| Tool | State and boundary | Principal workflows | Important concrete scope | Critical considerations and integrations | | Tool | State and boundary | Principal workflows | Important concrete scope | Critical considerations and integrations |
| -------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`pdf-tools`** | Existing; dedicated repository | Merge, split, reorder, rotate and export PDF pages | Thumbnail workspace; multi-document operations; ZIP and PDF output; saved local workspace | Workers and IndexedDB; large-document memory control; future signature inspection through `crypto-tools`; metadata and safe-sharing through `privacy-tools` | | **`pdf-tools`** | Existing; dedicated repository | Merge, split, reorder, rotate and export PDF pages | Thumbnail workspace; multi-document operations; ZIP and PDF output; saved local workspace | Workers and IndexedDB; large-document memory control; future signature inspection through `crypto-tools`; metadata and safe-sharing through `privacy-tools` |
| **`xslt-tools`** | Existing; dedicated repository | Develop, test and run XSLT transformations | XML/XSLT editors; transformation results; local files; saved projects; validation and diagnostics | Lazy SaxonJS loading; relocatable assets; useful handoffs to `data-tools`, `schema-tools` and `diff-tools` | | **`xslt-tools`** | Existing; dedicated repository | Develop, test and run XSLT transformations | XML/XSLT editors; transformation results; local files; saved projects; validation and diagnostics | Lazy SaxonJS loading; relocatable assets; useful handoffs to `data-tools`, `schema-tools` and `diff-tools` |
| **`onenote-tools`** | Existing rich-reader release; dedicated repository | Open, browse, inspect and export OneNote sections and packages locally | Desktop/unfragmented FSSHTTPB `.one`/`.onetoc2`; none/LZX/MSZIP/Quantum and multi-cabinet `.onepkg`; trees, rich text, images, tables, ink, attachments and export | Native TypeScript worker with no Wasm; bounded parsing, rendering and export; fragmented FSSHTTP fails safely; no editing or pixel-perfect fidelity | | **`onenote-tools`** | Existing rich-reader release; dedicated repository | Open, browse, inspect and export OneNote sections and packages locally | Desktop/unfragmented FSSHTTPB `.one`/`.onetoc2`; none/LZX/MSZIP/Quantum and multi-cabinet `.onepkg`; trees, rich text, images, tables, ink, attachments and export | Native TypeScript worker with no Wasm; bounded parsing, rendering and export; fragmented FSSHTTP fails safely; no editing or pixel-perfect fidelity |
| **`av-tools`** | Existing local-first v0.3.0 release; dedicated repository | Convert and lightly edit audio and video locally in the browser | Immediate native playback and basic file information; progressive stream inspection on demand; quick conversion; trim, split and crossfade concatenate; crop, resize, normalize, fades, waveform, metadata, chapters, subtitles, scene thumbnails/contact sheets, presets and export | Reviewed ffmpeg.wasm 0.12.10 ST/MT build with verified Opus and bundled label font; bounded queue, temporary storage and resource policy; isolation enables MT with automatic ST fallback; versioned core assets are immutable | | **`av-tools`** | Existing local-first v0.3.0 release; dedicated repository | Convert and lightly edit audio and video locally in the browser | Immediate native playback and basic file information; progressive stream inspection on demand; quick conversion; trim, split and crossfade concatenate; crop, resize, normalize, fades, waveform, metadata, chapters, subtitles, scene thumbnails/contact sheets, presets and export | Reviewed ffmpeg.wasm 0.12.10 ST/MT build with verified Opus and bundled label font; bounded queue, temporary storage and resource policy; isolation enables MT with automatic ST fallback; versioned core assets are immutable |
| **`regex-tools`** | Current local-first v0.4.2 release; dedicated repository | Develop, explain, test and apply JavaScript, PCRE2, PHP, Perl, Python, Ruby, Java, C++, Go, .NET, Rust and Scala/JVM-compatible regular expressions | Deterministic syntax trees; engine-native matching, captures and bounded replacement; stable double-buffered live results; PCRE2 tracing and C17 generation; comparison; corpus apply; tests; risk/growth/benchmark analysis; generated cases; bounded minimization; validated ECMAScript formatting; project import/export and optional local persistence | Open-source regexpp and pinned local WebAssembly runtimes for PCRE2, PHP preg, legacy Perl, CPython, CRuby, TeaVM Java/Scala compatibility, libc++ C++, Go, .NET and Rust in killable workers; explicit source/licence inventories, resource limits and engine-specific offset semantics | | **`regex-tools`** | Current local-first v0.4.2 release; dedicated repository | Develop, explain, test and apply JavaScript, PCRE2, PHP, Perl, Python, Ruby, Java, C++, Go, .NET, Rust and Scala/JVM-compatible regular expressions | Deterministic syntax trees; engine-native matching, captures and bounded replacement; stable double-buffered live results; PCRE2 tracing and C17 generation; comparison; corpus apply; tests; risk/growth/benchmark analysis; generated cases; bounded minimization; validated ECMAScript formatting; project import/export and optional local persistence | Open-source regexpp and pinned local WebAssembly runtimes for PCRE2, PHP preg, legacy Perl, CPython, CRuby, TeaVM Java/Scala compatibility, libc++ C++, Go, .NET and Rust in killable workers; explicit source/licence inventories, resource limits and engine-specific offset semantics |
| **`svg-tools`** | Current local-first v0.2.0 release; dedicated repository | Inspect and edit SVG source, structure, geometry, references and accessibility locally | Synchronized source/tree/canvas/inspector; path and transform tools; reference analysis; optimization; app-owned, validated opacity-animation preview; exact, sanitized, raster and project export | Untrusted SVG is sanitized into an opaque-origin sandbox; bounded parsing and decompression, explicit security findings and a URI-scoped controller header exception; later milestone slices remain intentionally tracked in the app repository | | **`svg-tools`** | Current local-first v0.2.0 release; dedicated repository | Inspect and edit SVG source, structure, geometry, references and accessibility locally | Synchronized source/tree/canvas/inspector; path and transform tools; reference analysis; optimization; app-owned, validated opacity-animation preview; exact, sanitized, raster and project export | Untrusted SVG is sanitized into an opaque-origin sandbox; bounded parsing and decompression, explicit security findings and a URI-scoped controller header exception; later milestone slices remain intentionally tracked in the app repository |
| **`auth-tools`** | Current local-first v0.3.1 release; dedicated repository | Generate, migrate and diagnose OTP credentials, and inspect, verify and test WebAuthn/passkey ceremonies locally | HOTP/TOTP/OCRA with time travel, resynchronization and rotation planning; QR, Google, Aegis and encrypted PSKC migration; WebAuthn extension experiments and replayable traces; assertion, attestation, signer-chain and historical metadata-policy evaluation | Authentication material remains memory-only unless explicitly exported; redaction is deliberate but not a secrecy guarantee; live ceremonies and camera scanning are enabled only at the exact dedicated `auth.toolbox.add-ideas.de` origin, while the shared Portal path remains inspect-only; v0.3.1 corrects source provenance and cross-timezone artifact reproducibility; no raw CTAP administration | | **`auth-tools`** | Current local-first v0.3.2 release; dedicated repository | Generate, migrate and diagnose OTP credentials, and inspect, verify and test WebAuthn/passkey ceremonies locally | HOTP/TOTP/OCRA with time travel, resynchronization and rotation planning; large per-token enrollment QR display with SVG download; QR, Google, Aegis and encrypted PSKC migration; WebAuthn extension experiments and replayable traces; assertion, attestation, signer-chain and historical metadata-policy evaluation | Authentication material remains memory-only unless explicitly exported; enrollment QR codes contain the token secret; redaction is deliberate but not a secrecy guarantee; live ceremonies and camera scanning are enabled only at the exact dedicated `auth.toolbox.add-ideas.de` origin, while the shared Portal path remains inspect-only; no raw CTAP administration |
| **`sudoku-tools`** | Current local-first v0.2.1 release; dedicated repository | Set, play, generate, analyse and solve classic, Killer and rich variant Sudoku locally | 4×4–16×16 grids; registry-backed constraint packs including cages, lines, dots, XV, inequalities, indexing and Fog of War; staged hints, candidate maintenance, advanced logical techniques, setter quality checks, mixed-variant generation, source-preserving f-puzzles/SudokuPad interoperability, autosave recovery and searchable local projects | Worker-bounded solving, generation and quality analysis; strict inert imported-visual validation; fog-safe play assistance; IndexedDB history with memory fallback; stable workspace geometry, responsive zoom and pan, a 3×3 standard keypad, tap selection, patterned colours, accessibility controls and an offline-capable PWA shell | | **`sudoku-tools`** | Current local-first v0.2.1 release; dedicated repository | Set, play, generate, analyse and solve classic, Killer and rich variant Sudoku locally | 4×4–16×16 grids; registry-backed constraint packs including cages, lines, dots, XV, inequalities, indexing and Fog of War; staged hints, candidate maintenance, advanced logical techniques, setter quality checks, mixed-variant generation, source-preserving f-puzzles/SudokuPad interoperability, autosave recovery and searchable local projects | Worker-bounded solving, generation and quality analysis; strict inert imported-visual validation; fog-safe play assistance; IndexedDB history with memory fallback; stable workspace geometry, responsive zoom and pan, a 3×3 standard keypad, tap selection, patterned colours, accessibility controls and an offline-capable PWA shell |
| **`colour-tools`** | Current local-first v0.2.0 release; dedicated repository | Convert, composite, interpolate, pick, sample, analyse and export colours locally | General colour conversion; arbitrary RGBA compositing; multi-stop colour steps and gradient editing; large visual/native/EyeDropper pickers; local image sampling and palette extraction; contrast, gamut, Delta E and colour-vision analysis; harmonies; DTCG 2025.10 token import/export with local reference resolution; directional contrast matrices; CSS and recipe export | Processing and image access remain local; colour spaces, encoded versus linear-light compositing, interpolation and gamut-mapping assumptions are explicit; mathematical conversion is distinguished from display simulation; integrates with SVG, image and token tools | | **`colour-tools`** | Current local-first v0.2.0 release; dedicated repository | Convert, composite, interpolate, pick, sample, analyse and export colours locally | General colour conversion; arbitrary RGBA compositing; multi-stop colour steps and gradient editing; large visual/native/EyeDropper pickers; local image sampling and palette extraction; contrast, gamut, Delta E and colour-vision analysis; harmonies; DTCG 2025.10 token import/export with local reference resolution; directional contrast matrices; CSS and recipe export | Processing and image access remain local; colour spaces, encoded versus linear-light compositing, interpolation and gamut-mapping assumptions are explicit; mathematical conversion is distinguished from display simulation; integrates with SVG, image and token tools |
| **`office-tools`** | Current local-first v0.2.0 read-only release; dedicated repository | Open, inspect and view word-processing documents, spreadsheets and presentations locally; download exact source copies and bounded ODF semantic exports | DOCX/DOCM/DOTX/DOTM, XLSX/XLSM/XLTX/XLTM and PPTX/PPTM/POTX/POTM/PPSX/PPSM plus ODT/ODS/ODP; search/navigation/zoom; text, tables, images, styles, metadata, cached formula values and notes; exact source-copy plus ODF text/CSV/outline/JSON export | Bounded worker-based inert package and XML parsers never execute macros, scripts, formulas, active content or external resources; legacy binary DOC, XLS and PPT are unsupported; viewing is intentionally read-only and does not promise pixel-perfect Office-suite layout fidelity | | **`office-tools`** | Current local-first v0.2.0 read-only release; dedicated repository | Open, inspect and view word-processing documents, spreadsheets and presentations locally; download exact source copies and bounded ODF semantic exports | DOCX/DOCM/DOTX/DOTM, XLSX/XLSM/XLTX/XLTM and PPTX/PPTM/POTX/POTM/PPSX/PPSM plus ODT/ODS/ODP; search/navigation/zoom; text, tables, images, styles, metadata, cached formula values and notes; exact source-copy plus ODF text/CSV/outline/JSON export | Bounded worker-based inert package and XML parsers never execute macros, scripts, formulas, active content or external resources; legacy binary DOC, XLS and PPT are unsupported; viewing is intentionally read-only and does not promise pixel-perfect Office-suite layout fidelity |
+1 -1
View File
@@ -3,7 +3,7 @@ services:
build: build:
context: . context: .
dockerfile: Containerfile dockerfile: Containerfile
image: git.add-ideas.de/lotobo/toolbox:0.20.2 image: git.add-ideas.de/lotobo/toolbox:0.20.6
restart: unless-stopped restart: unless-stopped
read_only: true read_only: true
ports: ports:
+3 -3
View File
@@ -6,9 +6,9 @@ services:
context: . context: .
dockerfile: Containerfile.release dockerfile: Containerfile.release
args: args:
TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.2}" TOOLBOX_RELEASE_VERSION: "${TOOLBOX_RELEASE_VERSION:-0.20.6}"
TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-66f02f95092ef80b9e49cda91c8401400e2ee1d2c640ab20a3a7810bf265c2c7}" TOOLBOX_RELEASE_SHA256: "${TOOLBOX_RELEASE_SHA256:-37ccd887793d7a147ceb3b69933faa969a006c59d6b1fbec775d3622279cedac}"
image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.2}" image: "git.add-ideas.de/lotobo/toolbox:${TOOLBOX_RELEASE_VERSION:-0.20.6}"
restart: unless-stopped restart: unless-stopped
read_only: true read_only: true
tmpfs: tmpfs:
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@add-ideas/toolbox-portal", "name": "@add-ideas/toolbox-portal",
"version": "0.2.23", "version": "0.2.24",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@add-ideas/toolbox-portal", "name": "@add-ideas/toolbox-portal",
"version": "0.2.23", "version": "0.2.24",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "^0.3.0", "@add-ideas/toolbox-contract": "^0.3.0",
+4 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@add-ideas/toolbox-portal", "name": "@add-ideas/toolbox-portal",
"version": "0.2.23", "version": "0.2.24",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -18,7 +18,9 @@
"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",
"release": "./release.sh"
}, },
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "^0.3.0", "@add-ideas/toolbox-contract": "^0.3.0",
+2 -2
View File
@@ -1,8 +1,8 @@
# Corresponding source # Corresponding source
The source code corresponding to add·ideas Toolbox Portal 0.2.23 is available at: The source code corresponding to add·ideas Toolbox Portal 0.2.24 is available at:
https://git.add-ideas.de/lotobo/toolbox-portal/src/tag/v0.20.0 https://git.add-ideas.de/lotobo/toolbox-portal/src/tag/v0.20.6
Each bundled application contains its own `SOURCE.md`, license, and third-party Each bundled application contains its own `SOURCE.md`, license, and third-party
license materials. The application sources are also linked from the portal. license materials. The application sources are also linked from the portal.
+4
View File
@@ -109,6 +109,10 @@
"manifest": "./apps/text/toolbox-app.json", "manifest": "./apps/text/toolbox-app.json",
"enabled": true "enabled": true
}, },
{
"manifest": "./apps/translator/toolbox-app.json",
"enabled": true
},
{ {
"manifest": "./apps/unicode/toolbox-app.json", "manifest": "./apps/unicode/toolbox-app.json",
"enabled": true "enabled": true
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${SCRIPT_DIR}/scripts/release.sh" "$@"
+8 -8
View File
@@ -1,8 +1,8 @@
{ {
"$schema": "./toolbox-release-lock.schema.json", "$schema": "./toolbox-release-lock.schema.json",
"schemaVersion": 1, "schemaVersion": 1,
"releaseVersion": "0.20.2", "releaseVersion": "0.20.6",
"portalVersion": "0.2.23", "portalVersion": "0.2.24",
"catalogue": { "catalogue": {
"id": "de.add-ideas.toolbox", "id": "de.add-ideas.toolbox",
"name": "add·ideas Toolbox", "name": "add·ideas Toolbox",
@@ -57,9 +57,9 @@
}, },
{ {
"id": "de.add-ideas.auth-tools", "id": "de.add-ideas.auth-tools",
"version": "0.3.1", "version": "0.3.2",
"artifact": "https://git.add-ideas.de/lotobo/auth-tools/releases/download/v0.3.1/auth-tools-0.3.1.zip", "artifact": "https://git.add-ideas.de/lotobo/auth-tools/releases/download/v0.3.2/auth-tools-0.3.2.zip",
"sha256": "60e90492fb50d2ce23298b3a3af84b1e3be02bbf5dea01d478f889d65eb66f3a", "sha256": "55938731b79f6ac3ec1834b0d1cfa368c671f979a3dad6b01ccaa9498372b40e",
"target": "auth" "target": "auth"
}, },
{ {
@@ -190,9 +190,9 @@
}, },
{ {
"id": "de.add-ideas.translator-tools", "id": "de.add-ideas.translator-tools",
"version": "0.1.1", "version": "0.1.3",
"artifact": "https://git.add-ideas.de/lotobo/translator-tools/releases/download/v0.1.1/translator-tools-0.1.1.zip", "artifact": "https://git.add-ideas.de/lotobo/translator-tools/releases/download/v0.1.3/translator-tools-0.1.3.zip",
"sha256": "9ff7a14f7b0730bee327d78d2e25364f53a16afd8b675acb0ad81bee9a1710f4", "sha256": "75482910c8a58e9169845bf4a3699a0385bc5ccff95259eef60004bae6814e22",
"target": "translator" "target": "translator"
}, },
{ {
+1 -1
View File
@@ -83,7 +83,7 @@ function makeLock(artifact: string, sha256: string) {
return { return {
schemaVersion: 1, schemaVersion: 1,
releaseVersion: '0.1.0', releaseVersion: '0.1.0',
portalVersion: '0.2.23', portalVersion: '0.2.24',
catalogue: { catalogue: {
id: 'de.add-ideas.toolbox', id: 'de.add-ideas.toolbox',
name: 'Test Toolbox', name: 'Test Toolbox',
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env node
import path from 'node:path';
import { readFile, access } from 'node:fs/promises';
import { createHash } from 'node:crypto';
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
function usage() {
return `Usage: node scripts/publish-release.mjs [options]
Uploads prepared release artifacts to a Gitea release.
Options:
--api-base URL Gitea API base (default: https://git.add-ideas.de/api/v1)
--owner OWNER Gitea repository owner (default: lotobo)
--repo REPO Gitea repository (default: toolbox-portal)
--tag TAG Release tag, e.g. v0.20.3 (default: from --lock-file)
--target HASH Target commit SHA for release creation
--name NAME Release name
--body BODY Optional release body
--archive PATH Release ZIP path
--checksum PATH SHA256 file path
--lock-file PATH Release lock file (default: release/toolbox.lock.json)
--token TOKEN Gitea API token; defaults to GITEA_TOKEN env
--dry-run Print actions without contacting Gitea
--help Show this help
`;
}
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 arg = argv[index];
if (!arg.startsWith('--')) throw new Error(`Unknown argument ${arg}`);
if (arg === '--dry-run') {
args.set(arg, true);
continue;
}
const value = argv[index + 1];
if (value === undefined) throw new Error(`Missing value for ${arg}`);
args.set(arg, value);
index += 1;
}
return {
apiBase: args.get('--api-base') ?? 'https://git.add-ideas.de/api/v1',
owner: args.get('--owner') ?? 'lotobo',
repo: args.get('--repo') ?? 'toolbox-portal',
tag: args.get('--tag'),
target: args.get('--target'),
name: args.get('--name') ?? 'add-ideas Toolbox release',
body: args.get('--body'),
archive: args.get('--archive'),
checksum: args.get('--checksum'),
lockFile: args.get('--lock-file') ?? 'release/toolbox.lock.json',
token: args.get('--token') ?? process.env.GITEA_TOKEN,
dryRun: args.get('--dry-run') === true,
};
}
async function readTokenFromEnvFile() {
const xdgConfig = process.env.XDG_CONFIG_HOME;
const homeConfig = process.env.HOME
? `${process.env.HOME}/.config/gitea/gitea.env`
: null;
const candidates = [];
if (xdgConfig) candidates.push(`${xdgConfig}/gitea/gitea.env`);
if (homeConfig) candidates.push(homeConfig);
for (const candidate of candidates) {
try {
await access(candidate);
} catch {
continue;
}
const fileContent = await readFile(candidate, 'utf8');
const tokenLine = fileContent
.split('\n')
.map((line) => line.trim())
.find((line) => line.startsWith('GITEA_TOKEN='));
if (!tokenLine) continue;
const token = tokenLine.split('=', 2)[1]?.trim();
if (token) return token;
}
return null;
}
async function giteaRequest(url, options, { allowNotFound = false } = {}) {
const response = await fetch(url, {
...options,
headers: {
Accept: 'application/json',
...(options.headers ?? {}),
},
});
if (allowNotFound && response.status === 404) {
return { notFound: true, status: 404 };
}
const text = await response.text();
const payload = text
? (() => {
try {
return JSON.parse(text);
} catch {
return { raw: text };
}
})()
: null;
if (!response.ok) {
const detail =
payload && typeof payload === 'object' && payload.error
? payload.error
: (payload?.raw ?? text ?? '');
throw new Error(
`Gitea API request failed (${response.status} ${response.statusText}): ${detail}`
);
}
return payload;
}
function sha256Digest(content) {
return createHash('sha256').update(content).digest('hex');
}
function parseChecksumFile(raw) {
const [first] = String(raw).trim().split(/\s+/);
return first?.toLowerCase() ?? '';
}
function parseReleaseVersionFromLock(lockJson) {
return lockJson.releaseVersion;
}
async function run() {
const options = parseArguments(process.argv.slice(2));
const lock = await readFile(options.lockFile, 'utf8');
const lockJson = JSON.parse(lock);
if (!SEMVER.test(lockJson.releaseVersion)) {
throw new Error(
`Invalid releaseVersion in lock: ${lockJson.releaseVersion}`
);
}
const releaseVersion = parseReleaseVersionFromLock(lockJson);
const releaseTag = options.tag ?? `v${releaseVersion}`;
const dryRun = options.dryRun === true;
let token = options.token;
if (!token && !dryRun) token = await readTokenFromEnvFile();
if (!token && !dryRun) {
throw new Error('GITEA_TOKEN is required. Set env or provide --token.');
}
if (!token && dryRun) token = 'dry-run-token';
if (!options.archive) {
throw new Error('Missing --archive');
}
if (!options.checksum) {
throw new Error('Missing --checksum');
}
const archive = path.resolve(options.archive);
const checksumFile = path.resolve(options.checksum);
const [archiveData, checksumRaw] = await Promise.all([
readFile(archive),
readFile(checksumFile, 'utf8'),
]);
const expectedChecksum = parseChecksumFile(checksumRaw);
const computedChecksum = sha256Digest(archiveData);
if (!expectedChecksum) {
throw new Error(`Checksum file is empty: ${checksumFile}`);
}
if (!/^[a-f0-9]{64}$/i.test(expectedChecksum)) {
throw new Error(`Invalid checksum format in ${checksumFile}`);
}
if (expectedChecksum.toLowerCase() !== computedChecksum.toLowerCase()) {
throw new Error(
`Checksum mismatch for ${options.archive}. computed=${computedChecksum} file=${expectedChecksum}`
);
}
if (dryRun) {
console.log(
`DRY-RUN: would ensure or create release ${releaseTag} on ${options.owner}/${options.repo}.`
);
console.log(
`DRY-RUN: would upload assets ${path.basename(archive)} and ${path.basename(checksumFile)}.`
);
return;
}
const apiBase = options.apiBase.replace(/\/+$/, '');
const releasePrefix = `${apiBase}/repos/${options.owner}/${options.repo}`;
const auth = `token ${token}`;
const headers = { Authorization: auth };
const tagUrl = `${releasePrefix}/releases/tags/${encodeURIComponent(releaseTag)}`;
const tagLookup = await giteaRequest(
tagUrl,
{ method: 'GET', headers },
{ allowNotFound: true }
);
let release;
if (tagLookup?.notFound) {
const body = {
tag_name: releaseTag,
target_commitish: options.target || 'main',
name: options.name,
body:
options.body ||
`Automated release of add-ideas toolbox ${releaseTag} from lock release ${releaseVersion}.`,
draft: false,
prerelease: false,
};
if (dryRun) {
console.log(
`DRY-RUN: would create release ${releaseTag} on ${options.owner}/${options.repo}`
);
release = { id: '<dry-run>', assets: [] };
} else {
release = await giteaRequest(`${releasePrefix}/releases`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
console.log(`Created release ${releaseTag} (${release.id})`);
}
} else {
release = tagLookup;
if (dryRun) {
console.log(
`DRY-RUN: would reuse existing release ${releaseTag} (${release?.id ?? 'unknown'})`
);
} else {
console.log(`Using existing release ${releaseTag} (${release.id})`);
}
}
if (dryRun) {
console.log(
'DRY-RUN: would upload assets:',
options.archive,
options.checksum
);
return;
}
const existingAssets = release.assets ?? [];
const candidateAssets = [
{ path: archive, name: path.basename(archive) },
{ path: checksumFile, name: path.basename(checksumFile) },
];
for (const asset of candidateAssets) {
const duplicate = existingAssets.find((entry) => entry.name === asset.name);
if (duplicate) {
console.log(
`Deleting existing asset ${asset.name} from release ${releaseTag}`
);
await giteaRequest(
`${releasePrefix}/releases/assets/${duplicate.id}`,
{ method: 'DELETE', headers },
{ allowNotFound: true }
);
}
const payload = await readFile(asset.path);
const form = new FormData();
form.set('attachment', new Blob([payload]), asset.name);
console.log(`Uploading ${asset.name}`);
await giteaRequest(
`${releasePrefix}/releases/${release.id}/assets?name=${encodeURIComponent(asset.name)}`,
{
method: 'POST',
headers,
body: form,
}
);
}
console.log(
`Release ${releaseTag} published on ${options.owner}/${options.repo}`
);
}
run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_DIR}"
usage() {
cat <<'USAGE'
Usage: scripts/release.sh [options]
Bundles the current lockfile into a portal archive and publishes the release to Gitea.
Options:
--lock PATH Lock file to use (default: release/toolbox.lock.json)
--tag TAG Release tag to create/push (default: v<releaseVersion>)
--owner OWNER Gitea owner/org (default: lotobo)
--repo REPO Gitea repository (default: toolbox-portal)
--api-base URL Gitea API base URL (default: https://git.add-ideas.de/api/v1)
--remote REMOTE Git remote used to push the tag (default: origin)
--build-dir DIR Build directory (default: build)
--output-subdir DIR Assembled output directory under build-dir (default: toolbox)
--skip-tests Skip npm test
--skip-assemble Skip npm run assemble
--skip-build Skip npm run build
--skip-tag Skip tagging/push of the release tag
--skip-git-push Skip pushing git branch and tag
--skip-publish Skip Gitea release publish/upload
--force Force overwrite of build outputs
--dry-run Print commands without running remote publishing operations
--help Show this help
Environment:
GITEA_TOKEN Personal token for Gitea API calls
GITEA_API_BASE Optional override for API base URL
Examples:
scripts/release.sh
scripts/release.sh --tag v0.20.4 --skip-tests
USAGE
}
LOCK_FILE="release/toolbox.lock.json"
OWNER="lotobo"
REPO="toolbox-portal"
API_BASE="${GITEA_API_BASE:-https://git.add-ideas.de/api/v1}"
REMOTE="origin"
BUILD_DIR="build"
OUTPUT_SUBDIR="toolbox"
SKIP_TESTS=0
SKIP_ASSEMBLE=0
SKIP_BUILD=0
SKIP_TAG=0
SKIP_GIT_PUSH=0
SKIP_PUBLISH=0
FORCE=0
DRY_RUN=0
while [ "$#" -gt 0 ]; do
case "$1" in
--lock)
LOCK_FILE="$2"
shift 2
;;
--tag)
TAG_OVERRIDE="$2"
shift 2
;;
--owner)
OWNER="$2"
shift 2
;;
--repo)
REPO="$2"
shift 2
;;
--api-base)
API_BASE="$2"
shift 2
;;
--remote)
REMOTE="$2"
shift 2
;;
--build-dir)
BUILD_DIR="$2"
shift 2
;;
--output-subdir)
OUTPUT_SUBDIR="$2"
shift 2
;;
--skip-tests)
SKIP_TESTS=1
shift
;;
--skip-assemble)
SKIP_ASSEMBLE=1
shift
;;
--skip-build)
SKIP_BUILD=1
shift
;;
--skip-tag)
SKIP_TAG=1
shift
;;
--skip-git-push)
SKIP_GIT_PUSH=1
shift
;;
--skip-publish)
SKIP_PUBLISH=1
shift
;;
--force)
FORCE=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
run() {
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY-RUN: $*"
return 0
fi
"$@"
}
RELEASE_VERSION="$(node -e "const fs=require('fs');const path=process.argv[1];const lock=JSON.parse(fs.readFileSync(path,'utf8'));if(!lock?.releaseVersion){process.exit(1);}console.log(lock.releaseVersion);" "$LOCK_FILE")"
if [ -z "$RELEASE_VERSION" ]; then
echo "releaseVersion could not be read from ${LOCK_FILE}" >&2
exit 1
fi
if [ -n "${TAG_OVERRIDE:-}" ]; then
RELEASE_TAG="$TAG_OVERRIDE"
else
RELEASE_TAG="v${RELEASE_VERSION}"
fi
ZIP_FILE="${BUILD_DIR}/add-ideas-toolbox-${RELEASE_VERSION}.zip"
CHECKSUM_FILE="${ZIP_FILE}.sha256"
TARGET_DIR="${BUILD_DIR}/${OUTPUT_SUBDIR}"
if [ "$SKIP_TESTS" -eq 0 ]; then
run npm test
fi
if [ "$SKIP_BUILD" -eq 0 ]; then
run npm run build
fi
if [ "$SKIP_ASSEMBLE" -eq 0 ]; then
ASSEMBLE_ARGS=(
npm
run
assemble
--
--lock
"$LOCK_FILE"
--portal-dist
dist
--output
"$TARGET_DIR"
--archive
"$ZIP_FILE"
)
if [ "$FORCE" -eq 1 ]; then
ASSEMBLE_ARGS+=(--force)
fi
run "${ASSEMBLE_ARGS[@]}"
fi
if [ ! -s "$ZIP_FILE" ]; then
echo "Release archive missing: $ZIP_FILE" >&2
exit 1
fi
if [ ! -s "$CHECKSUM_FILE" ]; then
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY-RUN: would create ${CHECKSUM_FILE}"
else
sha256sum "$ZIP_FILE" > "$CHECKSUM_FILE"
fi
else
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY-RUN: would refresh ${CHECKSUM_FILE}"
else
# ensure checksum always matches the current archive state
sha256sum "$ZIP_FILE" > "$CHECKSUM_FILE"
fi
fi
if [ "$SKIP_TAG" -eq 0 ]; then
if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then
CURRENT_COMMIT="$(git rev-parse "${RELEASE_TAG}^{commit}")"
HEAD_COMMIT="$(git rev-parse HEAD)"
if [ "$CURRENT_COMMIT" != "$HEAD_COMMIT" ]; then
echo "Tag ${RELEASE_TAG} already exists at ${CURRENT_COMMIT}, but HEAD is ${HEAD_COMMIT}." >&2
exit 1
fi
echo "Tag exists at HEAD: ${RELEASE_TAG}"
else
echo "Creating tag ${RELEASE_TAG}"
run git tag -a "$RELEASE_TAG" -m "toolbox ${RELEASE_TAG}"
fi
if [ "$SKIP_GIT_PUSH" -eq 0 ]; then
run git push "$REMOTE" HEAD
run git push "$REMOTE" "$RELEASE_TAG"
fi
fi
if [ "$SKIP_PUBLISH" -eq 0 ]; then
TOKEN="${GITEA_TOKEN:-}"
if [ -z "$TOKEN" ]; then
if [ -f "${HOME}/.config/gitea/gitea.env" ]; then
# shellcheck disable=SC1091
. "${HOME}/.config/gitea/gitea.env"
TOKEN="${GITEA_TOKEN:-}"
fi
fi
if [ -z "$TOKEN" ]; then
echo "GITEA_TOKEN is required for publishing (or use --skip-publish)." >&2
exit 1
fi
run node scripts/publish-release.mjs \
--api-base "$API_BASE" \
--owner "$OWNER" \
--repo "$REPO" \
--tag "$RELEASE_TAG" \
--target "$(git rev-parse HEAD)" \
--name "Toolbox ${RELEASE_TAG}" \
--archive "$ZIP_FILE" \
--checksum "$CHECKSUM_FILE" \
--lock-file "$LOCK_FILE" \
--token "$TOKEN"
if [ "$DRY_RUN" -eq 1 ]; then
echo "Dry run complete. No assets were uploaded."
else
echo "Published ${RELEASE_TAG} to Gitea."
fi
fi
echo "Release wrapper finished for ${RELEASE_TAG}"
+254
View File
@@ -0,0 +1,254 @@
#!/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 = `${archive}.sha256`;
const releaseSha256 = await sha256ForPath(archive);
await writeFile(
releaseChecksum,
`${releaseSha256} ${path.basename(archive)}\n`,
'utf8'
);
console.log(`Assembled release checksum: ${releaseSha256}`);
}
run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
+2 -2
View File
@@ -489,9 +489,9 @@ export default function App() {
</span> </span>
</p> </p>
<span> <span>
Portal v0.2.23 ·{' '} Portal v0.2.24 ·{' '}
<a <a
href="https://git.add-ideas.de/lotobo/toolbox-portal/src/tag/v0.20.0" href="https://git.add-ideas.de/lotobo/toolbox-portal/src/tag/v0.20.6"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >