generated from coulomb/repo-seed
feat(cli): add open-reuse validate and register portfolio integrations
Some checks failed
ci / validate-registry (push) Has been cancelled
Some checks failed
ci / validate-registry (push) Has been cancelled
Implement Integration Definition validator CLI with schema and index checks, pytest suite, and CI workflow. Register open-cmis-tck and issue-core-gitea in the integration index. Closes OPEN-WP-0003 and OPEN-WP-0004.
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **Language:** Markdown-first registry and planning repo (no application runtime yet)
|
- **Language:** Python CLI + Markdown/YAML registry artifacts
|
||||||
- **Key deps:** State Hub ADR-001 workplans, `registry/indexes/capabilities.yaml`
|
- **Key deps:** State Hub ADR-001 workplans, `jsonschema`, `pyyaml`,
|
||||||
|
`registry/indexes/integrations.yaml`
|
||||||
|
|
||||||
## Dev Commands
|
## Dev Commands
|
||||||
|
|
||||||
@@ -12,6 +13,12 @@ cat INTENT.md
|
|||||||
cat SCOPE.md
|
cat SCOPE.md
|
||||||
ls workplans/
|
ls workplans/
|
||||||
|
|
||||||
|
# Install and validate
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
open-reuse validate
|
||||||
|
open-reuse validate --repos-base /home/worsch
|
||||||
|
pytest -q
|
||||||
|
|
||||||
# After workplan or registry edits — from ~/state-hub
|
# After workplan or registry edits — from ~/state-hub
|
||||||
make fix-consistency REPO=open-reuse
|
make fix-consistency REPO=open-reuse
|
||||||
|
|
||||||
|
|||||||
28
.gitea/workflows/ci.yml
Normal file
28
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-registry:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install package
|
||||||
|
run: python -m pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Validate integration registry index
|
||||||
|
run: open-reuse validate --indexed-only
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest -q
|
||||||
16
AGENTS.md
16
AGENTS.md
@@ -156,6 +156,22 @@ get wrong.
|
|||||||
<!-- Append repo-specific agent instructions below this marker.
|
<!-- Append repo-specific agent instructions below this marker.
|
||||||
The state-hub template sync preserves content after this line. -->
|
The state-hub template sync preserves content after this line. -->
|
||||||
|
|
||||||
|
## Integration Registry CLI
|
||||||
|
|
||||||
|
Install and validate integration definitions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
open-reuse validate
|
||||||
|
open-reuse validate --repos-base /home/worsch --fail-on-warnings
|
||||||
|
pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- Schema: `schemas/integration.schema.yaml`
|
||||||
|
- Index: `registry/indexes/integrations.yaml`
|
||||||
|
- Template: `templates/integration-entry.template.yaml`
|
||||||
|
- Authoring guide: `registry/README.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Workplan Convention (ADR-001)
|
## Workplan Convention (ADR-001)
|
||||||
|
|||||||
3
open_reuse/__init__.py
Normal file
3
open_reuse/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""open-reuse integration registry tooling."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
74
open_reuse/cli.py
Normal file
74
open_reuse/cli.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from open_reuse.validate import run_validate
|
||||||
|
|
||||||
|
|
||||||
|
def _build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(prog="open-reuse")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
validate = subparsers.add_parser(
|
||||||
|
"validate",
|
||||||
|
help="Validate integration definitions and registry index",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"paths",
|
||||||
|
nargs="*",
|
||||||
|
type=Path,
|
||||||
|
help="Integration definition files (default: registry/integrations/*.integration.yaml)",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"--root",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help="open-reuse repository root (auto-detected when omitted)",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"--repos-base",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help="Base directory containing consuming repos for external definition checks",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"--no-index",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip indexes/integrations.yaml checks",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"--indexed-only",
|
||||||
|
action="store_true",
|
||||||
|
help="When checking index, skip local definition drift warnings",
|
||||||
|
)
|
||||||
|
validate.add_argument(
|
||||||
|
"--fail-on-warnings",
|
||||||
|
action="store_true",
|
||||||
|
help="Exit non-zero when promotion-gate warnings are present",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = _build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.command == "validate":
|
||||||
|
targets = args.paths or None
|
||||||
|
return run_validate(
|
||||||
|
root=args.root,
|
||||||
|
targets=targets,
|
||||||
|
repos_base=args.repos_base,
|
||||||
|
fail_on_warnings=args.fail_on_warnings,
|
||||||
|
check_index=not args.no_index,
|
||||||
|
indexed_only=args.indexed_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.error(f"unknown command: {args.command}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
91
open_reuse/registry.py
Normal file
91
open_reuse/registry.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
INDEX_REQUIRED_FIELDS = (
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"status",
|
||||||
|
"owner",
|
||||||
|
"reuse_mode",
|
||||||
|
"path",
|
||||||
|
"repo",
|
||||||
|
"upstream",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repo_root(root: Path | None = None) -> Path:
|
||||||
|
if root is not None:
|
||||||
|
return root.resolve()
|
||||||
|
candidate = PACKAGE_ROOT
|
||||||
|
markers = (
|
||||||
|
candidate / "schemas" / "integration.schema.yaml",
|
||||||
|
candidate / "registry" / "indexes" / "integrations.yaml",
|
||||||
|
)
|
||||||
|
if all(path.exists() for path in markers):
|
||||||
|
return candidate
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Could not resolve open-reuse repo root; pass --root explicitly."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def registry_paths(repo_root: Path) -> dict[str, Path]:
|
||||||
|
registry = repo_root / "registry"
|
||||||
|
return {
|
||||||
|
"registry": registry,
|
||||||
|
"integrations": registry / "integrations",
|
||||||
|
"index": registry / "indexes" / "integrations.yaml",
|
||||||
|
"schema": repo_root / "schemas" / "integration.schema.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema(repo_root: Path) -> dict[str, Any]:
|
||||||
|
schema_path = registry_paths(repo_root)["schema"]
|
||||||
|
with schema_path.open(encoding="utf-8") as handle:
|
||||||
|
return yaml.safe_load(handle)
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml(path: Path) -> dict[str, Any]:
|
||||||
|
with path.open(encoding="utf-8") as handle:
|
||||||
|
data = yaml.safe_load(handle)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"{path}: expected YAML mapping at root")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def load_index(repo_root: Path) -> dict[str, Any]:
|
||||||
|
index_path = registry_paths(repo_root)["index"]
|
||||||
|
if not index_path.exists():
|
||||||
|
return {"integrations": []}
|
||||||
|
return load_yaml(index_path)
|
||||||
|
|
||||||
|
|
||||||
|
def integration_paths(repo_root: Path, targets: list[Path] | None = None) -> list[Path]:
|
||||||
|
if targets:
|
||||||
|
return [path.resolve() for path in targets]
|
||||||
|
integrations_dir = registry_paths(repo_root)["integrations"]
|
||||||
|
if not integrations_dir.exists():
|
||||||
|
return []
|
||||||
|
return sorted(
|
||||||
|
path
|
||||||
|
for path in integrations_dir.glob("*.integration.yaml")
|
||||||
|
if path.is_file()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_external_definition(
|
||||||
|
repo_slug: str,
|
||||||
|
relative_path: str,
|
||||||
|
repos_base: Path | None,
|
||||||
|
) -> Path | None:
|
||||||
|
if repos_base is None:
|
||||||
|
return None
|
||||||
|
candidate = (repos_base / repo_slug / relative_path).resolve()
|
||||||
|
if candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
209
open_reuse/validate.py
Normal file
209
open_reuse/validate.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jsonschema import Draft202012Validator
|
||||||
|
|
||||||
|
from open_reuse.registry import (
|
||||||
|
INDEX_REQUIRED_FIELDS,
|
||||||
|
integration_paths,
|
||||||
|
load_index,
|
||||||
|
load_schema,
|
||||||
|
load_yaml,
|
||||||
|
registry_paths,
|
||||||
|
resolve_external_definition,
|
||||||
|
resolve_repo_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_schema_error(path: Path, error: Any) -> str:
|
||||||
|
location = ".".join(str(part) for part in error.path) or "(root)"
|
||||||
|
return f"{path}: schema error at {location}: {error.message}"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_definition(
|
||||||
|
path: Path,
|
||||||
|
schema: dict[str, Any],
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
try:
|
||||||
|
data = load_yaml(path)
|
||||||
|
except (OSError, yaml.YAMLError, ValueError) as exc:
|
||||||
|
return [f"{path}: failed to load YAML: {exc}"], warnings
|
||||||
|
|
||||||
|
validator = Draft202012Validator(schema)
|
||||||
|
schema_errors = sorted(validator.iter_errors(data), key=lambda item: list(item.path))
|
||||||
|
if schema_errors:
|
||||||
|
errors.extend(_format_schema_error(path, item) for item in schema_errors)
|
||||||
|
return errors, warnings
|
||||||
|
|
||||||
|
warnings.extend(_promotion_gate_warnings(path, data))
|
||||||
|
return errors, warnings
|
||||||
|
|
||||||
|
|
||||||
|
def _promotion_gate_warnings(path: Path, data: dict[str, Any]) -> list[str]:
|
||||||
|
warnings: list[str] = []
|
||||||
|
status = data.get("status", "draft")
|
||||||
|
maintenance = data.get("maintenance", {})
|
||||||
|
|
||||||
|
if status == "active" and not maintenance.get("maintainers"):
|
||||||
|
warnings.append(
|
||||||
|
f"{path}: active integration missing maintenance.maintainers"
|
||||||
|
)
|
||||||
|
if status in {"registered", "active"}:
|
||||||
|
if not data.get("boundary"):
|
||||||
|
warnings.append(f"{path}: {status} integration missing boundary block")
|
||||||
|
if not data.get("validation", {}).get("harness"):
|
||||||
|
warnings.append(f"{path}: {status} integration missing validation.harness")
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def validate_index(
|
||||||
|
repo_root: Path,
|
||||||
|
*,
|
||||||
|
repos_base: Path | None,
|
||||||
|
indexed_only: bool = False,
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
index = load_index(repo_root)
|
||||||
|
entries = index.get("integrations", [])
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
return ["indexes/integrations.yaml: integrations must be a list"], warnings
|
||||||
|
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
indexed_ids: set[str] = set()
|
||||||
|
for row in entries:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
errors.append("indexes/integrations.yaml: integration row must be a mapping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
integration_id = row.get("id")
|
||||||
|
if not integration_id:
|
||||||
|
errors.append("indexes/integrations.yaml: integration row missing id")
|
||||||
|
continue
|
||||||
|
if integration_id in seen_ids:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: duplicate integration id '{integration_id}'"
|
||||||
|
)
|
||||||
|
seen_ids.add(integration_id)
|
||||||
|
indexed_ids.add(integration_id)
|
||||||
|
|
||||||
|
for field in INDEX_REQUIRED_FIELDS:
|
||||||
|
if field not in row:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' missing required field '{field}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
upstream = row.get("upstream")
|
||||||
|
if upstream is not None and not isinstance(upstream, dict):
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' upstream must be a mapping"
|
||||||
|
)
|
||||||
|
elif isinstance(upstream, dict) and "name" not in upstream:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' upstream missing name"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo_slug = row.get("repo")
|
||||||
|
rel_path = row.get("path")
|
||||||
|
if not repo_slug or not rel_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
definition_path = resolve_external_definition(repo_slug, rel_path, repos_base)
|
||||||
|
if definition_path is None:
|
||||||
|
if repos_base is None:
|
||||||
|
warnings.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' definition not checked "
|
||||||
|
f"(pass --repos-base to verify {repo_slug}/{rel_path})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warnings.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' definition not found at "
|
||||||
|
f"{repos_base / repo_slug / rel_path}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
definition = load_yaml(definition_path)
|
||||||
|
except (OSError, yaml.YAMLError, ValueError) as exc:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' definition load failed: {exc}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if definition.get("id") != integration_id:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' id mismatch in "
|
||||||
|
f"{definition_path} (found '{definition.get('id')}')"
|
||||||
|
)
|
||||||
|
index_mode = row.get("reuse_mode")
|
||||||
|
definition_mode = definition.get("reuse", {}).get("primary_reuse_mode")
|
||||||
|
if index_mode and definition_mode and index_mode != definition_mode:
|
||||||
|
errors.append(
|
||||||
|
f"indexes/integrations.yaml: '{integration_id}' reuse_mode '{index_mode}' "
|
||||||
|
f"does not match definition '{definition_mode}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not indexed_only:
|
||||||
|
local_paths = integration_paths(repo_root)
|
||||||
|
for path in local_paths:
|
||||||
|
try:
|
||||||
|
definition = load_yaml(path)
|
||||||
|
except (OSError, yaml.YAMLError, ValueError) as exc:
|
||||||
|
errors.append(f"{path}: failed to load local definition: {exc}")
|
||||||
|
continue
|
||||||
|
integration_id = definition.get("id")
|
||||||
|
if integration_id and integration_id not in indexed_ids:
|
||||||
|
warnings.append(
|
||||||
|
f"{path}: local definition '{integration_id}' missing index row"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors, warnings
|
||||||
|
|
||||||
|
|
||||||
|
def run_validate(
|
||||||
|
*,
|
||||||
|
root: Path | None,
|
||||||
|
targets: list[Path] | None,
|
||||||
|
repos_base: Path | None,
|
||||||
|
fail_on_warnings: bool,
|
||||||
|
check_index: bool,
|
||||||
|
indexed_only: bool,
|
||||||
|
) -> int:
|
||||||
|
repo_root = resolve_repo_root(root)
|
||||||
|
schema = load_schema(repo_root)
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
definition_paths = integration_paths(repo_root, targets)
|
||||||
|
if targets is None and not definition_paths and not check_index:
|
||||||
|
definition_paths = []
|
||||||
|
|
||||||
|
for path in definition_paths:
|
||||||
|
file_errors, file_warnings = validate_definition(path, schema)
|
||||||
|
errors.extend(file_errors)
|
||||||
|
warnings.extend(file_warnings)
|
||||||
|
|
||||||
|
if check_index:
|
||||||
|
index_errors, index_warnings = validate_index(
|
||||||
|
repo_root,
|
||||||
|
repos_base=repos_base,
|
||||||
|
indexed_only=indexed_only,
|
||||||
|
)
|
||||||
|
errors.extend(index_errors)
|
||||||
|
warnings.extend(index_warnings)
|
||||||
|
|
||||||
|
for warning in warnings:
|
||||||
|
print(f"warning: {warning}")
|
||||||
|
for error in errors:
|
||||||
|
print(f"error: {error}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return 1
|
||||||
|
if fail_on_warnings and warnings:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
26
pyproject.toml
Normal file
26
pyproject.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "open-reuse"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Integration registry tooling for open-reuse"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"jsonschema>=4.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
open-reuse = "open_reuse.cli:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["open_reuse*"]
|
||||||
@@ -132,14 +132,35 @@ Prove Value
|
|||||||
5. Set `schema_version: open-reuse.integration.v0.1`.
|
5. Set `schema_version: open-reuse.integration.v0.1`.
|
||||||
6. Add a row to `registry/indexes/integrations.yaml` with `id`, `path`, `repo`,
|
6. Add a row to `registry/indexes/integrations.yaml` with `id`, `path`, `repo`,
|
||||||
`reuse_mode`, and `upstream` summary.
|
`reuse_mode`, and `upstream` summary.
|
||||||
7. Validate manually (checklist below) before setting `status: active`.
|
7. Run `open-reuse validate --repos-base <portfolio-root>` before setting
|
||||||
|
`status: active`.
|
||||||
|
|
||||||
Early adopters may use `schema_version: open-reuse.integration.v1`; the schema
|
Early adopters may use `schema_version: open-reuse.integration.v1`; the schema
|
||||||
accepts both. New entries should use v0.1.
|
accepts both. New entries should use v0.1.
|
||||||
|
|
||||||
|
## Automated validation
|
||||||
|
|
||||||
|
Primary validation path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install (from repo root)
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Validate local definitions and registry index
|
||||||
|
open-reuse validate
|
||||||
|
|
||||||
|
# Validate index rows against consuming-repo definitions
|
||||||
|
open-reuse validate --repos-base /home/worsch
|
||||||
|
|
||||||
|
# Treat promotion-gate warnings as failures (CI default for strict checks)
|
||||||
|
open-reuse validate --repos-base /home/worsch --fail-on-warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
CI runs `open-reuse validate --indexed-only` on every push to `main`.
|
||||||
|
|
||||||
## Manual validation checklist
|
## Manual validation checklist
|
||||||
|
|
||||||
Use until an automated CLI validator ships.
|
Fallback when the CLI is unavailable.
|
||||||
|
|
||||||
### Required fields
|
### Required fields
|
||||||
|
|
||||||
@@ -174,14 +195,17 @@ Update actions: `ignore`, `monitor-only`, `open-issue`, `open-update-proposal`,
|
|||||||
- [ ] `reuse_mode` matches `reuse.primary_reuse_mode` in the definition
|
- [ ] `reuse_mode` matches `reuse.primary_reuse_mode` in the definition
|
||||||
- [ ] `upstream.name` matches the definition
|
- [ ] `upstream.name` matches the definition
|
||||||
|
|
||||||
## Reference integration
|
## Reference integrations
|
||||||
|
|
||||||
`markitect-quarkdown` provides the first real-world adapter integration:
|
| ID | Repo | Reuse mode | Definition path |
|
||||||
|
| -- | ---- | ---------- | --------------- |
|
||||||
|
| `markitect-quarkdown` | markitect-quarkdown | adapter | `integration/quarkdown.integration.yaml` |
|
||||||
|
| `open-cmis-tck` | open-cmis-tck | adapter | `integration/opencmis-tck.integration.yaml` |
|
||||||
|
| `issue-core-gitea` | issue-core | adapter | `integration/gitea-backend.integration.yaml` |
|
||||||
|
|
||||||
- Definition: `markitect-quarkdown/integration/quarkdown.integration.yaml`
|
`markitect-quarkdown` is the primary worked example for adapter + cli-boundary
|
||||||
- Index row: `indexes/integrations.yaml` → `markitect-quarkdown`
|
reuse. `open-cmis-tck` illustrates cli-boundary orchestration of an upstream
|
||||||
|
test harness. `issue-core-gitea` illustrates a remote API backend adapter.
|
||||||
Use it as a worked example for adapter + cli-boundary reuse.
|
|
||||||
|
|
||||||
## Capability registry
|
## Capability registry
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,33 @@ integrations:
|
|||||||
notes: >
|
notes: >
|
||||||
Reference integration for adapter reuse mode. Definition lives in the
|
Reference integration for adapter reuse mode. Definition lives in the
|
||||||
consuming repository; this index row is the registry discovery surface.
|
consuming repository; this index row is the registry discovery surface.
|
||||||
|
|
||||||
|
- id: open-cmis-tck
|
||||||
|
name: Apache Chemistry OpenCMIS TCK Harness
|
||||||
|
status: registered
|
||||||
|
owner: open-cmis-tck
|
||||||
|
reuse_mode: adapter
|
||||||
|
risk_level: medium
|
||||||
|
path: integration/opencmis-tck.integration.yaml
|
||||||
|
repo: open-cmis-tck
|
||||||
|
upstream:
|
||||||
|
name: Apache Chemistry OpenCMIS TCK
|
||||||
|
project_url: https://github.com/apache/chemistry-opencmis
|
||||||
|
notes: >
|
||||||
|
Guide-board extension wrapping OpenCMIS TCK ConsoleRunner through a
|
||||||
|
cli-boundary adapter.
|
||||||
|
|
||||||
|
- id: issue-core-gitea
|
||||||
|
name: issue-core Gitea Backend
|
||||||
|
status: registered
|
||||||
|
owner: issue-core
|
||||||
|
reuse_mode: adapter
|
||||||
|
risk_level: medium
|
||||||
|
path: integration/gitea-backend.integration.yaml
|
||||||
|
repo: issue-core
|
||||||
|
upstream:
|
||||||
|
name: Gitea
|
||||||
|
project_url: https://github.com/go-gitea/gitea
|
||||||
|
notes: >
|
||||||
|
RemoteBackend adapter mapping issue-core task lifecycle onto the Gitea
|
||||||
|
issues API.
|
||||||
19
tests/fixtures/invalid.integration.yaml
vendored
Normal file
19
tests/fixtures/invalid.integration.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: open-reuse.integration.v0.1
|
||||||
|
id: INVALID_ID
|
||||||
|
name: Fixture Invalid Integration
|
||||||
|
|
||||||
|
upstream:
|
||||||
|
name: Example Upstream
|
||||||
|
|
||||||
|
reuse:
|
||||||
|
primary_reuse_mode: not-a-real-mode
|
||||||
|
|
||||||
|
boundary:
|
||||||
|
type: adapter
|
||||||
|
|
||||||
|
validation:
|
||||||
|
harness: echo ok
|
||||||
|
|
||||||
|
maintenance:
|
||||||
|
escalation_conditions:
|
||||||
|
- validation failure
|
||||||
26
tests/fixtures/valid.integration.yaml
vendored
Normal file
26
tests/fixtures/valid.integration.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
schema_version: open-reuse.integration.v0.1
|
||||||
|
id: fixture-valid
|
||||||
|
name: Fixture Valid Integration
|
||||||
|
status: registered
|
||||||
|
owner: open-reuse
|
||||||
|
|
||||||
|
upstream:
|
||||||
|
name: Example Upstream
|
||||||
|
project_url: https://example.com/upstream
|
||||||
|
|
||||||
|
reuse:
|
||||||
|
primary_reuse_mode: adapter
|
||||||
|
|
||||||
|
boundary:
|
||||||
|
type: adapter
|
||||||
|
local_adapter: fixture.adapter.Adapter
|
||||||
|
reused_surface: upstream API
|
||||||
|
|
||||||
|
validation:
|
||||||
|
harness: python3 -m pytest tests/
|
||||||
|
|
||||||
|
maintenance:
|
||||||
|
maintainers:
|
||||||
|
- fixture-team
|
||||||
|
escalation_conditions:
|
||||||
|
- validation failure
|
||||||
120
tests/test_validate.py
Normal file
120
tests/test_validate.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from open_reuse.registry import PACKAGE_ROOT, resolve_repo_root
|
||||||
|
from open_reuse.validate import run_validate, validate_definition
|
||||||
|
from open_reuse.registry import load_schema
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
REPO_ROOT = resolve_repo_root()
|
||||||
|
SCHEMA = load_schema(REPO_ROOT)
|
||||||
|
MARKITECT_QUARKDOWN = Path("/home/worsch/markitect-quarkdown/integration/quarkdown.integration.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_fixture_passes_schema() -> None:
|
||||||
|
errors, warnings = validate_definition(FIXTURES / "valid.integration.yaml", SCHEMA)
|
||||||
|
assert errors == []
|
||||||
|
assert warnings == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_fixture_fails_schema() -> None:
|
||||||
|
errors, warnings = validate_definition(FIXTURES / "invalid.integration.yaml", SCHEMA)
|
||||||
|
assert errors
|
||||||
|
assert any("schema error" in item for item in errors)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not MARKITECT_QUARKDOWN.is_file(), reason="markitect-quarkdown not present")
|
||||||
|
def test_markitect_quarkdown_reference_passes_schema() -> None:
|
||||||
|
errors, warnings = validate_definition(MARKITECT_QUARKDOWN, SCHEMA)
|
||||||
|
assert errors == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_command_success_on_fixture() -> None:
|
||||||
|
code = run_validate(
|
||||||
|
root=REPO_ROOT,
|
||||||
|
targets=[FIXTURES / "valid.integration.yaml"],
|
||||||
|
repos_base=None,
|
||||||
|
fail_on_warnings=False,
|
||||||
|
check_index=False,
|
||||||
|
indexed_only=False,
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_command_fails_on_invalid_fixture() -> None:
|
||||||
|
code = run_validate(
|
||||||
|
root=REPO_ROOT,
|
||||||
|
targets=[FIXTURES / "invalid.integration.yaml"],
|
||||||
|
repos_base=None,
|
||||||
|
fail_on_warnings=False,
|
||||||
|
check_index=False,
|
||||||
|
indexed_only=False,
|
||||||
|
)
|
||||||
|
assert code == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_requires_fields() -> None:
|
||||||
|
code = run_validate(
|
||||||
|
root=REPO_ROOT,
|
||||||
|
targets=[],
|
||||||
|
repos_base=None,
|
||||||
|
fail_on_warnings=False,
|
||||||
|
check_index=True,
|
||||||
|
indexed_only=True,
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not MARKITECT_QUARKDOWN.is_file(), reason="markitect-quarkdown not present")
|
||||||
|
def test_index_consistency_with_repos_base() -> None:
|
||||||
|
code = run_validate(
|
||||||
|
root=REPO_ROOT,
|
||||||
|
targets=[],
|
||||||
|
repos_base=Path("/home/worsch"),
|
||||||
|
fail_on_warnings=False,
|
||||||
|
check_index=True,
|
||||||
|
indexed_only=True,
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_without_maintainers_warns_with_fail_on_warnings() -> None:
|
||||||
|
active_fixture = FIXTURES / "active-missing-maintainers.integration.yaml"
|
||||||
|
active_fixture.write_text(
|
||||||
|
"""
|
||||||
|
schema_version: open-reuse.integration.v0.1
|
||||||
|
id: active-missing-maintainers
|
||||||
|
name: Active Missing Maintainers
|
||||||
|
status: active
|
||||||
|
owner: open-reuse
|
||||||
|
upstream:
|
||||||
|
name: Example Upstream
|
||||||
|
reuse:
|
||||||
|
primary_reuse_mode: adapter
|
||||||
|
boundary:
|
||||||
|
type: adapter
|
||||||
|
local_adapter: fixture.adapter.Adapter
|
||||||
|
validation:
|
||||||
|
harness: python3 -m pytest
|
||||||
|
maintenance:
|
||||||
|
escalation_conditions:
|
||||||
|
- validation failure
|
||||||
|
""".strip()
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
code = run_validate(
|
||||||
|
root=REPO_ROOT,
|
||||||
|
targets=[active_fixture],
|
||||||
|
repos_base=None,
|
||||||
|
fail_on_warnings=True,
|
||||||
|
check_index=False,
|
||||||
|
indexed_only=False,
|
||||||
|
)
|
||||||
|
assert code == 1
|
||||||
|
finally:
|
||||||
|
active_fixture.unlink(missing_ok=True)
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Integration CLI validator"
|
title: "Integration CLI validator"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: open-reuse
|
repo: open-reuse
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: infotech
|
topic_slug: infotech
|
||||||
created: "2026-06-24"
|
created: "2026-06-24"
|
||||||
@@ -32,56 +32,47 @@ patterns from reuse-surface `reuse-surface validate`.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0003-T01
|
id: OPEN-WP-0003-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "70b8cead-3c16-4e48-ab8f-ee6f7cedf25e"
|
state_hub_task_id: "70b8cead-3c16-4e48-ab8f-ee6f7cedf25e"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `pyproject.toml`, `open_reuse/` package skeleton, and entry point
|
Result 2026-06-24: Added `pyproject.toml`, `open_reuse/` package, and CLI entry
|
||||||
`open-reuse` with a `validate` subcommand stub. Include `jsonschema` and `pyyaml`
|
point. Documented install commands in `AGENTS.md` and
|
||||||
as dependencies. Document install and run commands in `AGENTS.md` and
|
|
||||||
`.claude/rules/stack-and-commands.md`.
|
`.claude/rules/stack-and-commands.md`.
|
||||||
|
|
||||||
## Implement validate command
|
## Implement validate command
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0003-T02
|
id: OPEN-WP-0003-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "0f8c8e25-1c1f-4f94-8771-71bcc3402db2"
|
state_hub_task_id: "0f8c8e25-1c1f-4f94-8771-71bcc3402db2"
|
||||||
```
|
```
|
||||||
|
|
||||||
Implement `open-reuse validate` with:
|
Result 2026-06-24: Implemented schema validation, index consistency checks,
|
||||||
|
`--repos-base` external definition resolution, and `--fail-on-warnings`.
|
||||||
- Schema validation of one or more `*.integration.yaml` files (default: scan
|
|
||||||
`registry/integrations/` if present).
|
|
||||||
- Index checks: every `indexes/integrations.yaml` row has required fields; `id`
|
|
||||||
and `reuse_mode` are consistent when the definition file is reachable.
|
|
||||||
- `--fail-on-warnings` for promotion-gate checks (missing maintainers on
|
|
||||||
`active` status, missing index row for local definitions).
|
|
||||||
- Exit code 0 on success, non-zero on errors.
|
|
||||||
|
|
||||||
## Add tests and CI
|
## Add tests and CI
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0003-T03
|
id: OPEN-WP-0003-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "e84e7b00-43ec-429e-afa0-42cb8eaa0074"
|
state_hub_task_id: "e84e7b00-43ec-429e-afa0-42cb8eaa0074"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `tests/test_validate.py` covering schema pass/fail, index consistency, and
|
Result 2026-06-24: Added `tests/test_validate.py` (8 tests) and
|
||||||
the markitect-quarkdown reference fixture. Add `.gitea/workflows/ci.yml` running
|
`.gitea/workflows/ci.yml`.
|
||||||
pytest on push/PR to `main`.
|
|
||||||
|
|
||||||
## Update registry documentation
|
## Update registry documentation
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0003-T04
|
id: OPEN-WP-0003-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "cfd38f45-0c0a-4790-b666-c211983b3ee9"
|
state_hub_task_id: "cfd38f45-0c0a-4790-b666-c211983b3ee9"
|
||||||
```
|
```
|
||||||
|
|
||||||
Update `registry/README.md` to reference `open-reuse validate` as the primary
|
Result 2026-06-24: Updated `registry/README.md` with CLI as primary validation
|
||||||
validation path. Mark manual checklist as fallback until CI is green.
|
path; manual checklist retained as fallback.
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Integration portfolio registration"
|
title: "Integration portfolio registration"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: open-reuse
|
repo: open-reuse
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: infotech
|
topic_slug: infotech
|
||||||
created: "2026-06-24"
|
created: "2026-06-24"
|
||||||
@@ -32,55 +32,50 @@ open-reuse.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0004-T01
|
id: OPEN-WP-0004-T01
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "c4367626-a4a6-4c57-bf83-ba54df3e6df0"
|
state_hub_task_id: "c4367626-a4a6-4c57-bf83-ba54df3e6df0"
|
||||||
```
|
```
|
||||||
|
|
||||||
Scan the local repo portfolio (reuse-surface `local-repo-roster.yaml`, domain
|
Result 2026-06-24: Surveyed 60-repo roster. Top candidates with proven code +
|
||||||
`INTENT.md` files, and existing `*.integration.yaml` files) for proven
|
tests: `open-cmis-tck` (OpenCMIS TCK), `issue-core` (Gitea API), `llm-connect`
|
||||||
integrations not yet indexed. Produce a short candidate list with owner repo,
|
(multi-provider, needs-boundary-work), `sand-boxer` (E2B/Modal), `tele-mcp`
|
||||||
upstream project, reuse mode estimate, and registration readiness
|
(Prometheus/Loki/k8s). Only `markitect-quarkdown` had an existing definition.
|
||||||
(ready / needs-definition / needs-boundary-work).
|
|
||||||
|
|
||||||
## Prioritize and assign targets
|
## Prioritize and assign targets
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0004-T02
|
id: OPEN-WP-0004-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "bd2c5dc8-814e-4045-86ed-2f15db4faf59"
|
state_hub_task_id: "bd2c5dc8-814e-4045-86ed-2f15db4faf59"
|
||||||
```
|
```
|
||||||
|
|
||||||
Select 2–3 candidates for registration in this workplan cycle. Record the
|
Result 2026-06-24: Selected `open-cmis-tck` and `issue-core-gitea` for this
|
||||||
selection and rationale in the task result. Defer remaining candidates to
|
cycle — clearest adapter boundaries and strongest test coverage. Deferred
|
||||||
backlog with explicit blockers.
|
`llm-connect` (multi-upstream boundary work), `sand-boxer` (multi-backend
|
||||||
|
registration shape), and `tele-mcp` (upstream version matrix) to backlog.
|
||||||
|
|
||||||
## Author integration definitions
|
## Author integration definitions
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0004-T03
|
id: OPEN-WP-0004-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "f12d9856-fd9f-44f0-b841-a82f521149c3"
|
state_hub_task_id: "f12d9856-fd9f-44f0-b841-a82f521149c3"
|
||||||
```
|
```
|
||||||
|
|
||||||
For each selected candidate, ensure a conforming Integration Definition exists
|
Result 2026-06-24: Added `integration/opencmis-tck.integration.yaml` in
|
||||||
in the consuming repo at `integration/<id>.integration.yaml` using
|
open-cmis-tck and `integration/gitea-backend.integration.yaml` in issue-core.
|
||||||
`templates/integration-entry.template.yaml`. Complete at minimum: upstream,
|
|
||||||
reuse classification, boundary, validation harness, and maintainers. Coordinate
|
|
||||||
PRs in owning repos where definitions are missing.
|
|
||||||
|
|
||||||
## Expand registry index
|
## Expand registry index
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-WP-0004-T04
|
id: OPEN-WP-0004-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "c59c5fe2-6f55-4097-8419-a73593abd589"
|
state_hub_task_id: "c59c5fe2-6f55-4097-8419-a73593abd589"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add index rows to `registry/indexes/integrations.yaml` for each registered
|
Result 2026-06-24: Added index rows for `open-cmis-tck` and `issue-core-gitea`.
|
||||||
integration. Update `registry/README.md` reference section. Run
|
Verified with `open-reuse validate --repos-base /home/worsch` (all checks pass).
|
||||||
`open-reuse validate` (or manual checklist) and confirm all new rows pass
|
|
||||||
promotion gates for `registered` or `active` status.
|
|
||||||
Reference in New Issue
Block a user