chore(packages): validate release metadata
This commit is contained in:
parent
651864017c
commit
d72543c892
89
Justfile
89
Justfile
@ -215,7 +215,7 @@ repl:
|
|||||||
@just trapos --cli
|
@just trapos --cli
|
||||||
|
|
||||||
# Local CI entry point used by Git hooks. Pass args through to `test`.
|
# Local CI entry point used by Git hooks. Pass args through to `test`.
|
||||||
ci *args: check-craftos check
|
ci *args: check-craftos check check-packages
|
||||||
@just test {{args}}
|
@just test {{args}}
|
||||||
@just test-timeout
|
@just test-timeout
|
||||||
|
|
||||||
@ -368,6 +368,93 @@ check: check-luacheck check-lychee
|
|||||||
luacheck --quiet .
|
luacheck --quiet .
|
||||||
@just lint-markdown
|
@just lint-markdown
|
||||||
|
|
||||||
|
# Validate package descriptors and require version bumps for changed package files.
|
||||||
|
check-packages: check-jq
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
repo='{{justfile_directory()}}'
|
||||||
|
cd "$repo"
|
||||||
|
|
||||||
|
semver_gt() {
|
||||||
|
local a_major a_minor a_patch b_major b_minor b_patch
|
||||||
|
IFS=. read -r a_major a_minor a_patch <<<"$1"
|
||||||
|
IFS=. read -r b_major b_minor b_patch <<<"$2"
|
||||||
|
if [ "${a_major:-0}" -gt "${b_major:-0}" ]; then return 0; fi
|
||||||
|
if [ "${a_major:-0}" -lt "${b_major:-0}" ]; then return 1; fi
|
||||||
|
if [ "${a_minor:-0}" -gt "${b_minor:-0}" ]; then return 0; fi
|
||||||
|
if [ "${a_minor:-0}" -lt "${b_minor:-0}" ]; then return 1; fi
|
||||||
|
[ "${a_patch:-0}" -gt "${b_patch:-0}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
packages=()
|
||||||
|
while IFS= read -r name; do packages+=("$name"); done < <(jq -r '.packages | keys[]' packages/index.json | sort)
|
||||||
|
|
||||||
|
for name in "${packages[@]}"; do
|
||||||
|
desc="packages/$name/ccpm.json"
|
||||||
|
if [ ! -f "$desc" ]; then
|
||||||
|
printf '%s\n' "FAIL: packages/index.json lists missing descriptor $desc" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
desc_name="$(jq -r '.name // empty' "$desc")"
|
||||||
|
desc_version="$(jq -r '.version // empty' "$desc")"
|
||||||
|
index_version="$(jq -r --arg name "$name" '.packages[$name] // empty' packages/index.json)"
|
||||||
|
if [ "$desc_name" != "$name" ]; then
|
||||||
|
printf '%s\n' "FAIL: $desc has name '$desc_name', expected '$name'" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
if [ "$desc_version" != "$index_version" ]; then
|
||||||
|
printf '%s\n' "FAIL: $name version differs between descriptor ($desc_version) and packages/index.json ($index_version)" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
last_desc_commit="$(git log -n 1 --format=%H -- "$desc" || true)"
|
||||||
|
if [ -z "$last_desc_commit" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
old_version="$(git show "$last_desc_commit:$desc" | jq -r '.version // empty')"
|
||||||
|
|
||||||
|
files=()
|
||||||
|
while IFS= read -r file; do files+=("$file"); done < <(jq -r '.files[]?' "$desc")
|
||||||
|
if [ "${#files[@]}" -gt 0 ] && ! git diff --quiet "$last_desc_commit" -- "${files[@]}"; then
|
||||||
|
if ! semver_gt "$desc_version" "$old_version"; then
|
||||||
|
printf '%s\n' "FAIL: $name package files changed since $desc was last bumped ($old_version); bump $desc and packages/index.json" >&2
|
||||||
|
git diff --name-only "$last_desc_commit" -- "${files[@]}" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for desc in packages/*/ccpm.json; do
|
||||||
|
[ -e "$desc" ] || continue
|
||||||
|
name="$(jq -r '.name // empty' "$desc")"
|
||||||
|
if ! jq -e --arg name "$name" '.packages[$name] != null' packages/index.json >/dev/null; then
|
||||||
|
printf '%s\n' "FAIL: descriptor $desc is missing from packages/index.json" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
trapos_desc='packages/trapos/ccpm.json'
|
||||||
|
trapos_commit="$(git log -n 1 --format=%H -- "$trapos_desc" || true)"
|
||||||
|
if [ -n "$trapos_commit" ]; then
|
||||||
|
trapos_version="$(jq -r '.version // empty' "$trapos_desc")"
|
||||||
|
old_trapos_version="$(git show "$trapos_commit:$trapos_desc" | jq -r '.version // empty')"
|
||||||
|
deps=()
|
||||||
|
while IFS= read -r dep; do deps+=("packages/$dep/ccpm.json"); done < <(jq -r '.dependencies[]?' "$trapos_desc")
|
||||||
|
if [ "${#deps[@]}" -gt 0 ] && ! git diff --quiet "$trapos_commit" -- "${deps[@]}"; then
|
||||||
|
if ! semver_gt "$trapos_version" "$old_trapos_version"; then
|
||||||
|
printf '%s\n' "FAIL: trapos dependencies changed since trapos was last bumped ($old_trapos_version); bump $trapos_desc and packages/index.json" >&2
|
||||||
|
git diff --name-only "$trapos_commit" -- "${deps[@]}" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then exit 1; fi
|
||||||
|
printf '%s\n' 'OK: package versions aligned'
|
||||||
|
|
||||||
# Validate local markdown links and heading anchors with lychee.
|
# Validate local markdown links and heading anchors with lychee.
|
||||||
lint-markdown: check-lychee
|
lint-markdown: check-lychee
|
||||||
lychee --config lychee.toml .
|
lychee --config lychee.toml .
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"branch": "next",
|
"branch": "next",
|
||||||
"packages": [
|
"packages": [
|
||||||
"trapos"
|
"trapos"
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"packages": {
|
"packages": {
|
||||||
"tos-core": "0.2.0",
|
"tos-core": "0.3.0",
|
||||||
"tos-test": "0.1.0",
|
"tos-test": "0.2.0",
|
||||||
"tos-boot": "0.1.0",
|
"tos-boot": "0.2.0",
|
||||||
"tos-net": "0.1.0",
|
"tos-net": "0.2.0",
|
||||||
"tos-ui": "0.1.0",
|
"tos-ui": "0.2.0",
|
||||||
"tos-ai": "0.1.0",
|
"tos-ai": "0.2.0",
|
||||||
"trapos": "0.4.0"
|
"trapos": "0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-ai",
|
"name": "tos-ai",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "TrapOS AI hello-world client (opencode proxy)",
|
"description": "TrapOS AI hello-world client (opencode proxy)",
|
||||||
"dependencies": ["tos-core"],
|
"dependencies": ["tos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-boot",
|
"name": "tos-boot",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "TrapOS boot: startup MOTD and autostart server launcher",
|
"description": "TrapOS boot: startup MOTD and autostart server launcher",
|
||||||
"dependencies": ["tos-core"],
|
"dependencies": ["tos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-core",
|
"name": "tos-core",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-net",
|
"name": "tos-net",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "TrapOS networking: routed modem messaging, router, ping",
|
"description": "TrapOS networking: routed modem messaging, router, ping",
|
||||||
"dependencies": ["tos-core"],
|
"dependencies": ["tos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-test",
|
"name": "tos-test",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "TrapOS test framework and CraftOS-PC suite runner",
|
"description": "TrapOS test framework and CraftOS-PC suite runner",
|
||||||
"dependencies": ["tos-core"],
|
"dependencies": ["tos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tos-ui",
|
"name": "tos-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "TrapOS terminal UI toolkit and demo",
|
"description": "TrapOS terminal UI toolkit and demo",
|
||||||
"dependencies": ["tos-core"],
|
"dependencies": ["tos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos",
|
"name": "trapos",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "TrapOS full install meta-package",
|
"description": "TrapOS full install meta-package",
|
||||||
"dependencies": ["tos-boot", "tos-net", "tos-ui", "tos-test"],
|
"dependencies": ["tos-boot", "tos-net", "tos-ui", "tos-test"],
|
||||||
"files": [],
|
"files": [],
|
||||||
|
|||||||
101
tests/packages.lua
Normal file
101
tests/packages.lua
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
local createLibTest = require('/apis/libtest');
|
||||||
|
|
||||||
|
local testlib = createLibTest({ ... });
|
||||||
|
|
||||||
|
local PACKAGES_DIR = '/trapos/packages';
|
||||||
|
local INDEX_PATH = PACKAGES_DIR .. '/index.json';
|
||||||
|
|
||||||
|
local function readJson(path)
|
||||||
|
local file = fs.open(path, 'r');
|
||||||
|
testlib.assertTrue(file, 'missing JSON file: ' .. path);
|
||||||
|
local body = file.readAll();
|
||||||
|
file.close();
|
||||||
|
local value = textutils.unserializeJSON(body);
|
||||||
|
testlib.assertTrue(value, 'invalid JSON file: ' .. path);
|
||||||
|
return value;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortedKeys(map)
|
||||||
|
local keys = {};
|
||||||
|
for key in pairs(map or {}) do
|
||||||
|
keys[#keys + 1] = key;
|
||||||
|
end
|
||||||
|
table.sort(keys);
|
||||||
|
return keys;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function descriptorPaths()
|
||||||
|
local paths = {};
|
||||||
|
for _, name in ipairs(fs.list(PACKAGES_DIR)) do
|
||||||
|
local dir = PACKAGES_DIR .. '/' .. name;
|
||||||
|
local path = dir .. '/ccpm.json';
|
||||||
|
if fs.isDir(dir) and fs.exists(path) then
|
||||||
|
paths[#paths + 1] = { name = name, path = path };
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(paths, function(a, b) return a.name < b.name; end);
|
||||||
|
return paths;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function packageMap()
|
||||||
|
local packages = {};
|
||||||
|
for _, entry in ipairs(descriptorPaths()) do
|
||||||
|
packages[entry.name] = readJson(entry.path);
|
||||||
|
end
|
||||||
|
return packages;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertSemver(version, label)
|
||||||
|
testlib.assertTrue(type(version) == 'string', label .. ' version must be a string');
|
||||||
|
testlib.assertTrue(string.match(version, '^%d+%.%d+%.%d+$') ~= nil, label .. ' version must be x.y.z');
|
||||||
|
end
|
||||||
|
|
||||||
|
testlib.test('package index matches descriptors', function()
|
||||||
|
local index = readJson(INDEX_PATH);
|
||||||
|
local packages = packageMap();
|
||||||
|
local indexNames = sortedKeys(index.packages);
|
||||||
|
local descriptorNames = sortedKeys(packages);
|
||||||
|
|
||||||
|
testlib.assertEquals(#indexNames, #descriptorNames, 'index and descriptor counts differ');
|
||||||
|
for i, name in ipairs(descriptorNames) do
|
||||||
|
testlib.assertEquals(indexNames[i], name, 'index package names differ');
|
||||||
|
testlib.assertEquals(index.packages[name], packages[name].version, name .. ' index version differs');
|
||||||
|
end
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('package descriptors are internally consistent', function()
|
||||||
|
local packages = packageMap();
|
||||||
|
for name, desc in pairs(packages) do
|
||||||
|
testlib.assertEquals(desc.name, name, name .. ' descriptor name differs from directory');
|
||||||
|
assertSemver(desc.version, name);
|
||||||
|
testlib.assertTrue(type(desc.dependencies) == 'table', name .. ' dependencies must be a table');
|
||||||
|
testlib.assertTrue(type(desc.files) == 'table', name .. ' files must be a table');
|
||||||
|
testlib.assertTrue(type(desc.autostart) == 'table', name .. ' autostart must be a table');
|
||||||
|
|
||||||
|
if name ~= 'trapos' then
|
||||||
|
testlib.assertTrue(#desc.files > 0, name .. ' must ship at least one file');
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, dependency in ipairs(desc.dependencies) do
|
||||||
|
testlib.assertTrue(packages[dependency] ~= nil, name .. ' depends on unknown package ' .. dependency);
|
||||||
|
end
|
||||||
|
|
||||||
|
local files = {};
|
||||||
|
for _, filePath in ipairs(desc.files) do
|
||||||
|
files[filePath] = true;
|
||||||
|
testlib.assertTrue(fs.exists('/trapos/' .. filePath), name .. ' missing shipped file ' .. filePath);
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, autostart in ipairs(desc.autostart) do
|
||||||
|
testlib.assertTrue(files[autostart .. '.lua'], name .. ' autostart not listed in files: ' .. autostart);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('root manifest tracks trapos package version', function()
|
||||||
|
local manifest = readJson('/trapos/manifest.json');
|
||||||
|
local trapos = readJson(PACKAGES_DIR .. '/trapos/ccpm.json');
|
||||||
|
testlib.assertEquals(manifest.version, trapos.version);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.run();
|
||||||
Loading…
Reference in New Issue
Block a user