Packaging, rtut check, tests CI job; schemas move into the package (RBT-WP-0009-T02)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 21:32:19 +02:00
parent 6d3414d8b7
commit e021fd979f
16 changed files with 163 additions and 10 deletions

View file

@ -0,0 +1,36 @@
# Runs the test suite and validates every runbook pack, attribute set, project and template.
# Follows the fleet pattern of work-records.yaml: no actions/checkout (the runner has no git), the repo comes
# from the Forgejo archive endpoint. The engine needs Python 3.12 (pyproject: requires-python), so the job runs in a
# python:3.12 container instead of the node:20-bookworm default, which ships an older Python.
# NOT YET RUN on a real runner: verify the first execution and adjust the container or label if the runner refuses it.
name: Tests
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
tests:
runs-on: ubuntu-latest
container:
image: python:3.12-slim
steps:
- name: Fetch the repository, install, test and check
run: |
set -eu
apt-get update -qq >/dev/null
apt-get install -y -qq wget >/dev/null
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
REF="${GITHUB_SHA:-main}"; SHORT="${REF:0:7}"
BASE="https://forgejo.coulomb.social"
mkdir -p "$WORK/repo"
# Forgejo archive endpoint accepts short SHA; full SHA can hang.
wget -qO "$WORK/repo.tar.gz" "${BASE}/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
tar xzf "$WORK/repo.tar.gz" -C "$WORK/repo" --strip-components=1
cd "$WORK/repo"
python -m pip install -q -e ".[test]"
python -m pytest -q
python -m rtut check .

View file

@ -1,9 +1,15 @@
.PHONY: help test validate fix-consistency
.PHONY: help install test check validate ui fix-consistency
help: ## List targets
@grep -E '^[a-z-]+:.*##' Makefile | sed 's/:.*##/ -/'
install: ## Install the engine and its test dependencies into the current environment (editable)
python3 -m pip install -e ".[test]"
test: ## Run the test suite
python3 -m pytest tests
python3 -m pytest
check: ## Validate every pack, attribute set, project and template in this repository
python3 -m rtut check .
validate: ## Validate runbook packs under runbooks/
python3 -m rtut validate runbooks
ui: ## Serve the local UI for this repository on 127.0.0.1:8877
python3 -m rtut ui .
fix-consistency: ## Sync workplans to State Hub: make fix-consistency REPO=runbook-tutorials
statehub fix-consistency --repo $(REPO)

View file

@ -4,11 +4,23 @@ A user interface specialized to present runbook tutorials that with parameters
can guide through actual IT administration and operation tasks.
- **Intent:** `INTENT.md` · **Scope:** `SCOPE.md`
- **Pack specification:** `docs/pack-spec.md` (v0.1, runnable) and `docs/pack-spec-v0.2.md` (v0.2: stages, gates, datapoints, actors, modes; validates, not yet runnable). Schemas in `schema/`. UX findings: `docs/ux-review-2026-09-29.md`
- **Pack specification:** `docs/pack-spec.md` (v0.1, runnable) and `docs/pack-spec-v0.2.md` (v0.2: stages, gates, datapoints, actors, modes; validates, not yet runnable). Schemas in `rtut/schema/`. UX findings: `docs/ux-review-2026-09-29.md`
- **Packs live where the procedure lives:** `runbooks/<pack>/pack.yaml` in the owning repo.
Examples: `net-kingdom/runbooks/`, and here `runbooks/example-hello/` and
`runbooks/showcase-lighthouse/` (a harmless story to try the UI on).
## Install
Python 3.12 or newer.
```bash
make install # editable install with the test dependencies (pip install -e ".[test]")
make check # validate every pack, attribute set, project and template
make test # run the tests
```
A plain `pip install .` works too and provides the `rtut` command; the JSON schemas ship inside the package.
```bash
python3 -m rtut validate <repo-root> # validate every pack
python3 -m rtut list <repo-root> # id, engine, maturity, title

View file

@ -2,7 +2,7 @@
Status: **adopted 2026-09-29** (Bernd's review answers are in the Decisions section).
Derived from `docs/ux-review-2026-09-29.md`. v0.1 stays valid; v0.2 is a superset.
Machine-checkable form: `schema/runbook-pack-v0.2.schema.json` plus the semantic
Machine-checkable form: `rtut/schema/runbook-pack-v0.2.schema.json` plus the semantic
checks in `rtut/v2.py`. The engine and UI do not run v0.2 packs yet (RBT-WP-0007).
## Principle

View file

@ -2,7 +2,7 @@
A **runbook pack** is a directory `runbooks/<pack-dir>/` in the repository that
owns the procedure. It contains `pack.yaml`, validated by
`schema/runbook-pack.schema.json`, plus optional supporting files. The engine
`rtut/schema/runbook-pack.schema.json`, plus optional supporting files. The engine
discovers packs by scanning `runbooks/*/pack.yaml` under a repository root, or
under any root given on the command line.

27
pyproject.toml Normal file
View file

@ -0,0 +1,27 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "runbook-tutorials"
version = "0.1.0"
description = "Runbook-tutorial engine and local UI: guided, parameterized operator runbooks; packs live in the repos that own them."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
dependencies = ["PyYAML>=6", "jsonschema>=4"]
[project.optional-dependencies]
test = ["pytest>=7"]
[project.scripts]
rtut = "rtut.__main__:cli"
[tool.setuptools]
packages = ["rtut"]
[tool.setuptools.package-data]
rtut = ["schema/*.json"]
[tool.pytest.ini_options]
testpaths = ["tests"]

View file

@ -59,6 +59,45 @@ def _run(pk, a) -> int:
return 1
def _check(root: str) -> int:
"""One command for CI and for humans: everything declarative in a repository must validate."""
from . import project as J
from .attrs import Registry
reg = Registry([Path(root)])
bad = 0
print("runbooks")
for p in reg.packs.values():
print(f" {'ok ' if p.ok else 'FAIL'} {p.id}")
for err in p.errors:
print(f" - {err}")
bad += not p.ok
print("attribute sets")
for sid, d in reg.sets.items():
print(f" {'ok ' if d.ok else 'FAIL'} {sid}")
for err in d.errors:
print(f" - {err}")
bad += not d.ok
print("projects")
for pid, d in reg.projects.items():
errs = J.validate(reg, d)
print(f" {'ok ' if not errs else 'FAIL'} {pid}")
for err in errs:
print(f" - {err}")
bad += bool(errs)
print("templates")
for tid, d in reg.templates.items():
print(f" {'ok ' if d.ok else 'FAIL'} {tid}")
for err in d.errors:
print(f" - {err}")
bad += not d.ok
total = len(reg.packs) + len(reg.sets) + len(reg.projects) + len(reg.templates)
if not total:
print("nothing to check", file=sys.stderr)
return 1
print(f"{total - bad} of {total} ok")
return 1 if bad else 0
def _project(a) -> int:
from . import project as J
from .attrs import Registry
@ -135,6 +174,8 @@ def main(argv: list[str] | None = None) -> int:
sp = sub.add_parser("ui", help="serve the local UI on 127.0.0.1")
sp.add_argument("roots", nargs="*", default=["."])
sp.add_argument("--port", type=int, default=8877)
sp = sub.add_parser("check", help="validate every pack, attribute set, project and template under a repository")
sp.add_argument("root", nargs="?", default=".")
sp = sub.add_parser("maturity", help="derived maturity of a pack at its current revision")
sp.add_argument("manifest")
sp = sub.add_parser("review", help="record that you reviewed the pack at its current revision")
@ -172,6 +213,8 @@ def main(argv: list[str] | None = None) -> int:
except KeyboardInterrupt:
pass
return 0
if a.cmd == "check":
return _check(a.root)
if a.cmd == "project":
return _project(a)
if a.cmd in ("maturity", "review"):
@ -257,5 +300,10 @@ def main(argv: list[str] | None = None) -> int:
return 0
def cli() -> None:
"""Console-script entry point (`rtut`)."""
sys.exit(main())
if __name__ == "__main__":
sys.exit(main())

View file

@ -16,7 +16,7 @@ from jsonschema import Draft202012Validator
from . import pack as P
from .pack import SECRET_RE
SCHEMA_DIR = Path(__file__).resolve().parent.parent / "schema"
SCHEMA_DIR = Path(__file__).resolve().parent / "schema"
def _schema_errors(data: dict, schema_file: str) -> list[str]:

View file

@ -9,7 +9,7 @@ from pathlib import Path
import yaml
from jsonschema import Draft202012Validator
SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schema" / "runbook-pack.schema.json"
SCHEMA_PATH = Path(__file__).resolve().parent / "schema" / "runbook-pack.schema.json"
PLACEHOLDER = re.compile(r"\{\{\s*([a-z][a-z0-9_]*)\s*\}\}")
SECRET_RE = re.compile(
r"(hvs\.[A-Za-z0-9]{8,}|s\.[A-Za-z0-9]{24}|-----BEGIN [A-Z ]*PRIVATE KEY|otpauth://)")

View file

@ -9,7 +9,7 @@ from jsonschema import Draft202012Validator
from .pack import PLACEHOLDER, SECRET_RE
SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schema" / "runbook-pack-v0.2.schema.json"
SCHEMA_PATH = Path(__file__).resolve().parent / "schema" / "runbook-pack-v0.2.schema.json"
MAX_DEPTH = 3

View file

@ -97,3 +97,24 @@ def test_ui_and_launch_arguments_parse(repo):
from rtut.__main__ import main as m
with pytest.raises(SystemExit):
m(["ui", "--port", "not-a-port"])
def test_check_validates_everything_declarative_and_reports_problems(repo, capsys):
(repo / "runbooks/v1/pack.yaml").write_text((repo / "runbooks/v1/pack.yaml").read_text().replace("engine: native", "engine: nonsense"))
code, out, _ = run(capsys, "check", repo)
assert code == 1 and "FAIL rtut.showcase-lighthouse" in out and "ok rtut.showcase-lighthouse-v2" in out
assert "attribute sets" in out and "ok harbour-defaults" in out and "templates" in out and "of" in out.splitlines()[-1]
def test_check_passes_on_a_clean_repository_and_fails_on_an_empty_one(repo, capsys, tmp_path):
code, out, _ = run(capsys, "check", repo)
assert code == 0 and out.rstrip().endswith("4 of 4 ok") # two packs, one attribute set, one template
empty = tmp_path / "empty"; empty.mkdir()
code, _, err = run(capsys, "check", empty)
assert code == 1 and "nothing to check" in err
def test_the_schemas_ship_inside_the_package(repo):
from rtut import pack, v2, attrs
for path in (pack.SCHEMA_PATH, v2.SCHEMA_PATH, attrs.SCHEMA_DIR / "project.schema.json", attrs.SCHEMA_DIR / "attribute-set.schema.json"):
assert path.exists() and "rtut/schema" in str(path)

View file

@ -40,12 +40,15 @@ Also found and fixed while doing it: the CLI had no tests, and `rtut list` crash
```task
id: RBT-WP-0009-T02
status: todo
status: done
priority: high
```
Declare the dependencies (`pyproject.toml`: PyYAML, jsonschema, pytest) so a fresh checkout can run; add `rtut check` (packs, attribute
Done 2026-09-29. Declare the dependencies (`pyproject.toml`: PyYAML, jsonschema, pytest) so a fresh checkout can run; add `rtut check` (packs, attribute
sets, projects and templates in one go) and `make check`; replace the copied smoke-only CI with a job that runs the tests and the checks.
Also: the JSON schemas moved into the package (`rtut/schema/`) so a plain `pip install .` works (verified); `rtut check` validates packs, attribute
sets, projects and templates; `.forgejo/workflows/tests.yaml` runs tests and checks in a python:3.12 container. **The CI job has not run on a real
runner** (no way to trigger one from here); check its first execution.
## Documents and bookkeeping