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
|
||||
|
||||
# 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-timeout
|
||||
|
||||
@ -368,6 +368,93 @@ check: check-luacheck check-lychee
|
||||
luacheck --quiet .
|
||||
@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.
|
||||
lint-markdown: check-lychee
|
||||
lychee --config lychee.toml .
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"packages": {
|
||||
"tos-core": "0.2.0",
|
||||
"tos-test": "0.1.0",
|
||||
"tos-boot": "0.1.0",
|
||||
"tos-net": "0.1.0",
|
||||
"tos-ui": "0.1.0",
|
||||
"tos-ai": "0.1.0",
|
||||
"trapos": "0.4.0"
|
||||
"tos-core": "0.3.0",
|
||||
"tos-test": "0.2.0",
|
||||
"tos-boot": "0.2.0",
|
||||
"tos-net": "0.2.0",
|
||||
"tos-ui": "0.2.0",
|
||||
"tos-ai": "0.2.0",
|
||||
"trapos": "0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-ai",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "TrapOS AI hello-world client (opencode proxy)",
|
||||
"dependencies": ["tos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-boot",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "TrapOS boot: startup MOTD and autostart server launcher",
|
||||
"dependencies": ["tos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-core",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
||||
"dependencies": [],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-net",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "TrapOS networking: routed modem messaging, router, ping",
|
||||
"dependencies": ["tos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-test",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "TrapOS test framework and CraftOS-PC suite runner",
|
||||
"dependencies": ["tos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tos-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "TrapOS terminal UI toolkit and demo",
|
||||
"dependencies": ["tos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": ["tos-boot", "tos-net", "tos-ui", "tos-test"],
|
||||
"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