From e021fd979f97f32673a3c30015f6157df156114d Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 29 Sep 2026 21:32:19 +0200 Subject: [PATCH] Packaging, rtut check, tests CI job; schemas move into the package (RBT-WP-0009-T02) Co-Authored-By: Claude Sonnet 5.5 Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477 --- .forgejo/workflows/tests.yaml | 36 ++++++++++++++ Makefile | 10 +++- README.md | 14 +++++- docs/pack-spec-v0.2.md | 2 +- docs/pack-spec.md | 2 +- pyproject.toml | 27 +++++++++++ rtut/__main__.py | 48 +++++++++++++++++++ rtut/attrs.py | 2 +- rtut/pack.py | 2 +- .../schema}/attribute-set.schema.json | 0 {schema => rtut/schema}/project.schema.json | 0 .../schema}/runbook-pack-v0.2.schema.json | 0 .../schema}/runbook-pack.schema.json | 0 rtut/v2.py | 2 +- tests/test_cli.py | 21 ++++++++ ...inalize-the-runbook-tutorials-workplans.md | 7 ++- 16 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 .forgejo/workflows/tests.yaml create mode 100644 pyproject.toml rename {schema => rtut/schema}/attribute-set.schema.json (100%) rename {schema => rtut/schema}/project.schema.json (100%) rename {schema => rtut/schema}/runbook-pack-v0.2.schema.json (100%) rename {schema => rtut/schema}/runbook-pack.schema.json (100%) diff --git a/.forgejo/workflows/tests.yaml b/.forgejo/workflows/tests.yaml new file mode 100644 index 0000000..31e0cb6 --- /dev/null +++ b/.forgejo/workflows/tests.yaml @@ -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 . diff --git a/Makefile b/Makefile index c8da042..f96dfd5 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 16a6ca5..fcf4ea3 100644 --- a/README.md +++ b/README.md @@ -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.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 # validate every pack python3 -m rtut list # id, engine, maturity, title diff --git a/docs/pack-spec-v0.2.md b/docs/pack-spec-v0.2.md index bb50483..248f12a 100644 --- a/docs/pack-spec-v0.2.md +++ b/docs/pack-spec-v0.2.md @@ -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 diff --git a/docs/pack-spec.md b/docs/pack-spec.md index 40a266e..9805317 100644 --- a/docs/pack-spec.md +++ b/docs/pack-spec.md @@ -2,7 +2,7 @@ A **runbook pack** is a directory `runbooks//` 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. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ecd6347 --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/rtut/__main__.py b/rtut/__main__.py index 0b65efe..f4137e6 100644 --- a/rtut/__main__.py +++ b/rtut/__main__.py @@ -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()) diff --git a/rtut/attrs.py b/rtut/attrs.py index ef6cf99..2a14122 100644 --- a/rtut/attrs.py +++ b/rtut/attrs.py @@ -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]: diff --git a/rtut/pack.py b/rtut/pack.py index 02d718a..61b327f 100644 --- a/rtut/pack.py +++ b/rtut/pack.py @@ -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://)") diff --git a/schema/attribute-set.schema.json b/rtut/schema/attribute-set.schema.json similarity index 100% rename from schema/attribute-set.schema.json rename to rtut/schema/attribute-set.schema.json diff --git a/schema/project.schema.json b/rtut/schema/project.schema.json similarity index 100% rename from schema/project.schema.json rename to rtut/schema/project.schema.json diff --git a/schema/runbook-pack-v0.2.schema.json b/rtut/schema/runbook-pack-v0.2.schema.json similarity index 100% rename from schema/runbook-pack-v0.2.schema.json rename to rtut/schema/runbook-pack-v0.2.schema.json diff --git a/schema/runbook-pack.schema.json b/rtut/schema/runbook-pack.schema.json similarity index 100% rename from schema/runbook-pack.schema.json rename to rtut/schema/runbook-pack.schema.json diff --git a/rtut/v2.py b/rtut/v2.py index 3fc6463..69f8284 100644 --- a/rtut/v2.py +++ b/rtut/v2.py @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index 42d251d..63a9a41 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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) diff --git a/workplans/RBT-WP-0009-finalize-the-runbook-tutorials-workplans.md b/workplans/RBT-WP-0009-finalize-the-runbook-tutorials-workplans.md index 714c40a..423d62e 100644 --- a/workplans/RBT-WP-0009-finalize-the-runbook-tutorials-workplans.md +++ b/workplans/RBT-WP-0009-finalize-the-runbook-tutorials-workplans.md @@ -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