feat: release toolbox portal 0.1.0
This commit is contained in:
137
scripts/release-lock.mjs
Normal file
137
scripts/release-lock.mjs
Normal file
@@ -0,0 +1,137 @@
|
||||
import { parseToolboxCatalog } from '@add-ideas/toolbox-contract';
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
const REVERSE_DNS_ID = /^[a-z0-9]+(?:[.-][a-z0-9]+)+$/;
|
||||
const TARGET_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
function record(value, field) {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
||||
throw new Error(`${field} must be an object.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value, field) {
|
||||
if (typeof value !== 'string' || value.trim() === '')
|
||||
throw new Error(`${field} must be a non-empty string.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function onlyKeys(value, allowed, field) {
|
||||
const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
|
||||
if (unexpected.length > 0)
|
||||
throw new Error(`${field} contains unsupported field: ${unexpected[0]}.`);
|
||||
}
|
||||
|
||||
function usesLatestAlias(value) {
|
||||
let decoded = value;
|
||||
try {
|
||||
decoded = decodeURIComponent(value);
|
||||
} catch {
|
||||
// An invalid encoded URL will be rejected when the artifact is acquired.
|
||||
}
|
||||
return /(?:^|[^a-z0-9])latest(?:[^a-z0-9]|$)/i.test(decoded);
|
||||
}
|
||||
|
||||
export function parseReleaseLock(value) {
|
||||
const lock = record(value, 'lock');
|
||||
onlyKeys(
|
||||
lock,
|
||||
new Set([
|
||||
'$schema',
|
||||
'schemaVersion',
|
||||
'releaseVersion',
|
||||
'portalVersion',
|
||||
'catalogue',
|
||||
'apps',
|
||||
]),
|
||||
'lock'
|
||||
);
|
||||
if (lock.schemaVersion !== 1)
|
||||
throw new Error('lock.schemaVersion must be 1.');
|
||||
string(lock.releaseVersion, 'lock.releaseVersion');
|
||||
if (!SEMVER.test(lock.releaseVersion))
|
||||
throw new Error('lock.releaseVersion must use semantic versioning.');
|
||||
string(lock.portalVersion, 'lock.portalVersion');
|
||||
if (!SEMVER.test(lock.portalVersion))
|
||||
throw new Error('lock.portalVersion must use semantic versioning.');
|
||||
|
||||
const catalogue = record(lock.catalogue, 'lock.catalogue');
|
||||
onlyKeys(
|
||||
catalogue,
|
||||
new Set(['id', 'name', 'home', 'theme']),
|
||||
'lock.catalogue'
|
||||
);
|
||||
string(catalogue.id, 'lock.catalogue.id');
|
||||
if (!REVERSE_DNS_ID.test(catalogue.id))
|
||||
throw new Error('lock.catalogue.id must be a reverse-DNS identifier.');
|
||||
string(catalogue.name, 'lock.catalogue.name');
|
||||
string(catalogue.home, 'lock.catalogue.home');
|
||||
const theme = record(catalogue.theme, 'lock.catalogue.theme');
|
||||
onlyKeys(theme, new Set(['mode', 'brand']), 'lock.catalogue.theme');
|
||||
if (!['light', 'dark', 'system'].includes(theme.mode))
|
||||
throw new Error('lock.catalogue.theme.mode is invalid.');
|
||||
string(theme.brand, 'lock.catalogue.theme.brand');
|
||||
|
||||
if (!Array.isArray(lock.apps) || lock.apps.length === 0)
|
||||
throw new Error('lock.apps must contain at least one pinned app.');
|
||||
const ids = new Set();
|
||||
const targets = new Set();
|
||||
lock.apps.forEach((rawApp, index) => {
|
||||
const app = record(rawApp, `lock.apps[${index}]`);
|
||||
onlyKeys(
|
||||
app,
|
||||
new Set(['id', 'version', 'artifact', 'sha256', 'target']),
|
||||
`lock.apps[${index}]`
|
||||
);
|
||||
string(app.id, `lock.apps[${index}].id`);
|
||||
if (!REVERSE_DNS_ID.test(app.id))
|
||||
throw new Error(
|
||||
`lock.apps[${index}].id must be a reverse-DNS identifier.`
|
||||
);
|
||||
string(app.version, `lock.apps[${index}].version`);
|
||||
if (!SEMVER.test(app.version))
|
||||
throw new Error(
|
||||
`lock.apps[${index}].version must use semantic versioning.`
|
||||
);
|
||||
string(app.artifact, `lock.apps[${index}].artifact`);
|
||||
if (usesLatestAlias(app.artifact))
|
||||
throw new Error(
|
||||
`lock.apps[${index}].artifact must not use a latest alias.`
|
||||
);
|
||||
string(app.sha256, `lock.apps[${index}].sha256`);
|
||||
if (!SHA256.test(app.sha256))
|
||||
throw new Error(
|
||||
`lock.apps[${index}].sha256 must be 64 lowercase hexadecimal characters.`
|
||||
);
|
||||
string(app.target, `lock.apps[${index}].target`);
|
||||
if (!TARGET_SLUG.test(app.target))
|
||||
throw new Error(`lock.apps[${index}].target must be a safe URL slug.`);
|
||||
if (ids.has(app.id))
|
||||
throw new Error(`Duplicate app id in lock: ${app.id}.`);
|
||||
if (targets.has(app.target))
|
||||
throw new Error(`Duplicate app target in lock: ${app.target}.`);
|
||||
ids.add(app.id);
|
||||
targets.add(app.target);
|
||||
});
|
||||
return lock;
|
||||
}
|
||||
|
||||
export function buildCatalogue(lock) {
|
||||
const catalogue = {
|
||||
schemaVersion: 1,
|
||||
id: lock.catalogue.id,
|
||||
name: lock.catalogue.name,
|
||||
home: lock.catalogue.home,
|
||||
theme: lock.catalogue.theme,
|
||||
apps: lock.apps.map((app) => ({
|
||||
manifest: `./apps/${app.target}/toolbox-app.json`,
|
||||
enabled: true,
|
||||
})),
|
||||
};
|
||||
return {
|
||||
$schema:
|
||||
'https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json',
|
||||
...parseToolboxCatalog(catalogue),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user