From 26c399598c7946f3e3b3c69a7ad84fae059c902c Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 17 May 2026 11:14:35 +1000 Subject: [PATCH 1/4] feat: JSON Schema validation for metadata.yaml as pre-commit hook Adds schema/metadata.json (JSON Schema draft-07) as the authoritative schema for all rpms/*/metadata.yaml files. Key constraints: - additionalProperties: false at both the top level and builds items, so unknown fields are rejected outright - name allows dots (fixes neovim-glibc-2.17) - github is optional (fixes claude-code, which has no GitHub repo) - repository enum includes fedora/42, fedora/43, fedora/44 - dist_tag boolean field documented in schema Pre-commit hook (check-jsonschema 0.37.2) runs against every file matching ^rpms/[^/]+/metadata\.yaml$ on every commit. Also fixes rpms/claude-code/metadata.yaml: removes the unknown claude_ai field and normalises field order. --- .pre-commit-config.yaml | 8 +++ rpms/claude-code/metadata.yaml | 31 +++++---- schema/metadata.json | 111 +++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 schema/metadata.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6f76cf..a4922d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,14 @@ repos: "-s", ] + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.37.2 + hooks: + - id: check-jsonschema + name: Validate RPM package metadata + files: ^rpms/[^/]+/metadata\.yaml$ + args: [--schemafile, schema/metadata.json] + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.7 hooks: diff --git a/rpms/claude-code/metadata.yaml b/rpms/claude-code/metadata.yaml index 01a8210..a099e9f 100644 --- a/rpms/claude-code/metadata.yaml +++ b/rpms/claude-code/metadata.yaml @@ -1,19 +1,18 @@ -arch: amd64 -builds: -- image: git.unkin.net/unkin/almalinux8-rpmbuilder:latest - release: 1 - repository: - - almalinux/el8 - version: 2.1.126 -- image: git.unkin.net/unkin/almalinux9-rpmbuilder:latest - release: 1 - repository: - - almalinux/el9 - version: 2.1.126 -claude_ai: true +name: claude-code description: Claude Code - Anthropic's agentic AI coding tool +arch: amd64 +platform: linux +maintainer: Anthropic homepage: https://claude.ai/code license: Proprietary -maintainer: Anthropic -name: claude-code -platform: linux +builds: +- repository: + - almalinux/el8 + image: git.unkin.net/unkin/almalinux8-rpmbuilder:latest + release: 1 + version: 2.1.126 +- repository: + - almalinux/el9 + image: git.unkin.net/unkin/almalinux9-rpmbuilder:latest + release: 1 + version: 2.1.126 diff --git a/schema/metadata.json b/schema/metadata.json new file mode 100644 index 0000000..61ed7d3 --- /dev/null +++ b/schema/metadata.json @@ -0,0 +1,111 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "description": "Schema for rpms/*/metadata.yaml files", + "properties": { + "arch": { + "enum": [ + "amd64", + "arm64", + "x86_64" + ], + "type": "string" + }, + "builds": { + "items": { + "additionalProperties": false, + "properties": { + "image": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9\\-_.:/@]+$", + "type": "string" + }, + "release": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "number" + } + ] + }, + "repository": { + "items": { + "enum": [ + "almalinux/el8", + "almalinux/el9", + "fedora/42", + "fedora/43", + "fedora/44" + ], + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "version": { + "minLength": 1, + "pattern": "^[0-9]+(\\.[0-9]+)*(-[a-zA-Z0-9]+)*$", + "type": "string" + } + }, + "required": [ + "repository", + "image", + "release", + "version" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "description": { + "minLength": 1, + "type": "string" + }, + "dist_tag": { + "type": "boolean" + }, + "github": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9\\-_]+/[a-zA-Z0-9\\-_.]+$", + "type": "string" + }, + "github_release_pattern": { + "minLength": 1, + "type": "string" + }, + "homepage": { + "minLength": 1, + "pattern": "^https?://.+", + "type": "string" + }, + "license": { + "minLength": 1, + "type": "string" + }, + "maintainer": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9\\-_.]*$", + "type": "string" + }, + "platform": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "description", + "builds" + ], + "title": "RPM Package Metadata", + "type": "object" +} -- 2.47.3 From f46e8bfc0b0936547c5f17746209e56c25d38c9a Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 17 May 2026 11:35:03 +1000 Subject: [PATCH 2/4] feat: add pytest pre-commit hook and tests scaffold (issue #162) - pyproject.toml with pytest>=8 in [dependency-groups.dev] so uv run --group dev pytest resolves without a global install - tests/__init__.py and tests/conftest.py skeleton referencing issue #162 - pre-commit local hook runs pytest tests/ on Python file changes; exit code 5 (no tests collected) is treated as success so commits are not blocked while the test suite is being built out --- .pre-commit-config.yaml | 9 +++++++++ pyproject.toml | 9 +++++++++ tests/__init__.py | 0 tests/conftest.py | 2 ++ 4 files changed, 20 insertions(+) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4922d6..7460332 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,15 @@ repos: "-s", ] + - repo: local + hooks: + - id: pytest + name: Run unit tests + entry: bash -c 'uv run --group dev pytest tests/ -q; rc=$?; [ $rc -eq 5 ] && exit 0 || exit $rc' + language: system + types: [python] + pass_filenames: false + - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.37.2 hooks: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..76677c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "rpmbuilder" +version = "0.1.0" +requires-python = ">=3.11" + +[dependency-groups] +dev = [ + "pytest>=8", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fe2d9d4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,2 @@ +# Tests for tools/build and tools/update-gh. +# See https://git.unkin.net/unkin/rpmbuilder/issues/162 -- 2.47.3 From ff349877844a48efaf82e68fb243d142f549bb3c Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 17 May 2026 11:40:59 +1000 Subject: [PATCH 3/4] refactor: use make test in pre-commit hook --- .pre-commit-config.yaml | 2 +- Makefile | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7460332..c0e8e3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: pytest name: Run unit tests - entry: bash -c 'uv run --group dev pytest tests/ -q; rc=$?; [ $rc -eq 5 ] && exit 0 || exit $rc' + entry: make test language: system types: [python] pass_filenames: false diff --git a/Makefile b/Makefile index 70ee592..bc97295 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DISTRO ?= almalinux/el9 PACKAGES := $(shell find $(ROOT_DIR)/rpms -mindepth 1 -maxdepth 1 -type d -exec test -f {}/metadata.yaml \; -print | xargs -n1 basename | sort) # Default target to build all packages -.PHONY: all list build clean +.PHONY: all list build clean test all: build-all # List all available packages @@ -47,6 +47,10 @@ dry-run: @echo "Dry run - showing what would be built for distro $(DISTRO):" $(BUILD_TOOL) build-all --distro $(DISTRO) --dry-run +# Run unit tests +test: + @uv run --group dev pytest tests/ -q; rc=$$?; [ $$rc -eq 5 ] && exit 0 || exit $$rc + # Clean target clean: @echo "Cleaning build artifacts..." -- 2.47.3 From db5a829429e573f6f8788f211af91ae1fe4c5307 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 17 May 2026 11:41:50 +1000 Subject: [PATCH 4/4] test: validate all metadata.yaml files against schema in pytest --- .pre-commit-config.yaml | 1 + pyproject.toml | 2 ++ tests/test_metadata.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/test_metadata.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0e8e3c..d2b46e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,7 @@ repos: name: Validate RPM package metadata files: ^rpms/[^/]+/metadata\.yaml$ args: [--schemafile, schema/metadata.json] + language_version: python3.11 - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.7 diff --git a/pyproject.toml b/pyproject.toml index 76677c9..c8cc4f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,6 @@ requires-python = ">=3.11" [dependency-groups] dev = [ "pytest>=8", + "jsonschema>=4", + "pyyaml>=6", ] diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..533b0eb --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,36 @@ +"""Validate every rpms/*/metadata.yaml against schema/metadata.json.""" + +import json +from pathlib import Path + +import jsonschema +import pytest +import yaml + +REPO_ROOT = Path(__file__).parent.parent +SCHEMA_FILE = REPO_ROOT / "schema" / "metadata.json" +RPMS_DIR = REPO_ROOT / "rpms" + + +@pytest.fixture(scope="session") +def schema(): + with open(SCHEMA_FILE) as f: + return json.load(f) + + +def metadata_files(): + return sorted(RPMS_DIR.glob("*/metadata.yaml")) + + +@pytest.mark.parametrize("metadata_file", metadata_files(), ids=lambda p: p.parent.name) +def test_metadata_valid(metadata_file, schema): + with open(metadata_file) as f: + data = yaml.safe_load(f) + + validator = jsonschema.Draft7Validator(schema) + errors = sorted(validator.iter_errors(data), key=str) + + assert not errors, "\n".join( + f" {'.'.join(str(p) for p in e.absolute_path) or '(root)'}: {e.message}" + for e in errors + ) -- 2.47.3