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:
parent
6d3414d8b7
commit
e021fd979f
16 changed files with 163 additions and 10 deletions
36
.forgejo/workflows/tests.yaml
Normal file
36
.forgejo/workflows/tests.yaml
Normal 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 .
|
||||
10
Makefile
10
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)
|
||||
|
|
|
|||
14
README.md
14
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>/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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
27
pyproject.toml
Normal 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"]
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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://)")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue