diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6f76cf..d2b46e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,24 @@ repos: "-s", ] + - repo: local + hooks: + - id: pytest + name: Run unit tests + entry: make test + language: system + types: [python] + pass_filenames: false + + - 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] + language_version: python3.11 + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.7 hooks: 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..." diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c8cc4f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "rpmbuilder" +version = "0.1.0" +requires-python = ">=3.11" + +[dependency-groups] +dev = [ + "pytest>=8", + "jsonschema>=4", + "pyyaml>=6", +] 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" +} 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 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 + )