From fb53a5ef1e3685123e3a8036bd8a6a8f53ff3a70 Mon Sep 17 00:00:00 2001 From: Onehill Date: Wed, 21 Oct 2026 21:07:14 +0200 Subject: [PATCH 1/3] Package the validator as the installable `okgf` command Make the reference validator installable so users can validate bundles without cloning the repo and running `python tools/okgf_validate.py`. - Move the validator into an installable `okgf` package (okgf/validate.py, a console entry point, `python -m okgf`, and `--version`). tools/okgf_validate.py stays as a back-compat shim that re-exports the package API. - Add pyproject.toml (hatchling): PyYAML is the only runtime dependency, with `cryptography` optional behind the `verify` extra; the wheel ships only okgf. - One-line install: install.sh (curl | sh via uv) and a Homebrew formula. - release.yml publishes to PyPI on a version tag via Trusted Publishing (OIDC). - conformance CI installs the package and runs the `okgf` command (and the shim) on Python 3.9 and 3.12. - Docs: install section atop the README, docs/INSTALL.md, and updated references. Signed-off-by: Onehill Agent: Claude Opus 4.8 (instructed by awchristoph) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/conformance.yml | 20 +- .github/workflows/release.yml | 57 ++++++ .gitignore | 5 + CONFORMANCE.md | 13 +- CONTRIBUTING.md | 9 +- README.md | 44 ++++- SECURITY.md | 6 +- SPEC.md | 7 +- conftest.py | 11 ++ docs/INSTALL.md | 77 ++++++++ examples/README.md | 2 +- install.sh | 52 +++++ okgf/__init__.py | 9 + okgf/__main__.py | 6 + okgf/validate.py | 293 ++++++++++++++++++++++++++++ packaging/homebrew/okgf.rb | 44 +++++ pyproject.toml | 61 ++++++ tests/test_okgf_validate.py | 9 +- tools/okgf_validate.py | 305 +++--------------------------- 19 files changed, 714 insertions(+), 316 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 conftest.py create mode 100644 docs/INSTALL.md create mode 100644 install.sh create mode 100644 okgf/__init__.py create mode 100644 okgf/__main__.py create mode 100644 okgf/validate.py create mode 100644 packaging/homebrew/okgf.rb create mode 100644 pyproject.toml diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index e923698..bb59edc 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -8,18 +8,22 @@ on: jobs: validate: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.11" - - name: Install dependencies - run: python -m pip install -r requirements-dev.txt - - name: Validate example bundles (strict, with signature verification) + python-version: ${{ matrix.python-version }} + - name: Install the okgf package (with dev + verify extras) + run: python -m pip install -e ".[dev]" + - name: Validate example bundles with the okgf command (strict, with signature verification) run: | - python tools/okgf_validate.py --strict \ - --pubkey examples/governed/ORG_PUBKEY.pem \ - examples/governed - python tools/okgf_validate.py --strict examples/minimal + okgf --strict --pubkey examples/governed/ORG_PUBKEY.pem examples/governed + okgf --strict examples/minimal + - name: Back-compat shim still validates + run: python tools/okgf_validate.py --strict examples/minimal - name: Run tests run: python -m pytest tests/ -q diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7336024 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: release + +# Publish the `okgf` package to PyPI when a version tag is pushed (e.g. v0.1.0). +# Publishing uses PyPI Trusted Publishing (OIDC): no API token is stored in the repo. The PyPI +# project must list this repository + workflow + the `pypi` environment as a trusted publisher. +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Build sdist and wheel + run: | + python -m pip install --upgrade build + python -m build + - name: Smoke-test the built wheel + run: | + python -m pip install dist/*.whl + okgf --version + okgf --strict examples/minimal + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish-pypi: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # OIDC token for PyPI Trusted Publishing + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + bump-homebrew: + needs: publish-pypi + runs-on: ubuntu-latest + # Optional: open a PR against the tap to update the formula. Needs a HOMEBREW_TAP_TOKEN secret + # with push/PR access to OneHillAI/homebrew-tap. Skipped automatically when the secret is absent. + if: ${{ false }} + steps: + - name: Bump the tap formula + run: echo "Enable this job and set HOMEBREW_TAP_TOKEN to auto-bump OneHillAI/homebrew-tap." diff --git a/.gitignore b/.gitignore index 75c6182..adce4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ __pycache__/ *.pyc .pytest_cache/ +# Build / packaging artifacts +build/ +dist/ +*.egg-info/ +.venv/ diff --git a/CONFORMANCE.md b/CONFORMANCE.md index c1ee42e..389e80e 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -1,8 +1,9 @@ # OKGF Conformance This document defines what it means for a page or bundle to conform to **OKGF v0.1**, and how to check -it with the reference validator in [`tools/okgf_validate.py`](tools/okgf_validate.py). The normative rules -live in [`SPEC.md`](SPEC.md); this file restates them as a checklist and describes the tooling. +it with the reference validator, installable as the `okgf` command (see [`docs/INSTALL.md`](docs/INSTALL.md); +its source is the [`okgf/`](okgf/) package). The normative rules live in [`SPEC.md`](SPEC.md); this file +restates them as a checklist and describes the tooling. ## What conformance means @@ -45,17 +46,17 @@ key is supplied, the validator format-checks the signature and reports it as **u ## Checking conformance The reference validator needs Python 3.9+ and PyYAML. Signature verification additionally needs the -`cryptography` package and a public key. +`cryptography` package (the `verify` extra) and a public key. ```sh # Validate pages and/or bundle directories (structure + governance ranges): -python tools/okgf_validate.py examples/minimal examples/governed +okgf examples/minimal examples/governed # Also cryptographically verify signatures against an org public key: -python tools/okgf_validate.py --pubkey examples/governed/ORG_PUBKEY.pem examples/governed +okgf --pubkey examples/governed/ORG_PUBKEY.pem examples/governed # Treat warnings (e.g. missing recommended OKF fields, unverified signatures) as failures: -python tools/okgf_validate.py --strict examples/governed +okgf --strict examples/governed ``` Exit status is `0` when there are no errors (and, under `--strict`, no warnings), otherwise `1`. A usage or diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c242528..d9ef683 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,8 @@ the reference validator. - **Spec clarifications or fixes** - wording, ambiguities, or errors in `SPEC.md` / `CONFORMANCE.md`. - **New or improved examples** - sample bundles under `examples/` that show a real pattern. -- **Validator improvements** - bug fixes or checks in `tools/okgf_validate.py`, with tests. +- **Validator improvements** - bug fixes or checks in the [`okgf/`](okgf/) package (run as the `okgf` + command), with tests. - **Proposed field or lifecycle changes** - larger changes to the governance model. Open an issue first (see below) so we can discuss before you write it. @@ -22,10 +23,10 @@ must not break OKF conformance. 2. **Branch and make the change.** Keep PRs focused on one thing. 3. **Run the checks locally:** ```sh - pip install -r requirements-dev.txt + pip install -e ".[dev]" python -m pytest tests/ -q - python tools/okgf_validate.py --strict examples/minimal - python tools/okgf_validate.py --strict --pubkey examples/governed/ORG_PUBKEY.pem examples/governed + okgf --strict examples/minimal + okgf --strict --pubkey examples/governed/ORG_PUBKEY.pem examples/governed ``` New or changed behaviour needs a test. New example bundles must pass the validator (CI enforces this). 4. **Open a pull request** against `main`. CI runs the validator and the test suite on every PR. diff --git a/README.md b/README.md index fa6bc1e..623efb5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ is coming soon. ## Table of Contents +- [Install](#install) - [Why OKGF exists](#why-okgf-exists) - [How it works](#how-it-works) - [The review lifecycle](#the-review-lifecycle) @@ -38,6 +39,31 @@ is coming soon. - [Lineage](#lineage) - [Status](#status) · [Contributing](#contributing) · [License](#license) +## Install + +Install the reference validator as the `okgf` command. No system Python required - this uses +[uv](https://docs.astral.sh/uv/) to manage an isolated Python for the tool: + +```sh +curl -fsSL https://raw.githubusercontent.com/OneHillAI/okgf/main/install.sh | sh +``` + +Or via Homebrew, or straight from PyPI: + +```sh +brew install OneHillAI/tap/okgf +pipx install okgf # or: uv tool install okgf, or: pip install okgf +``` + +Then validate a bundle: + +```sh +okgf --strict path/to/bundle +``` + +Signature verification is optional: add the `verify` extra (`OKGF_VERIFY=1` for the installer above, +or `pip install 'okgf[verify]'`). See [`docs/INSTALL.md`](docs/INSTALL.md) for all the options. + ## Why OKGF exists OKF makes knowledge **portable**: markdown plus YAML frontmatter, human- and agent-readable, diffable in @@ -105,25 +131,28 @@ key is published out of band, so anyone can confirm the org really approved it, ## Get started -Validate a bundle with the reference validator (Python plus PyYAML): +With the `okgf` command [installed](#install), validate the example bundles: ```sh -python tools/okgf_validate.py examples/minimal examples/governed +okgf examples/minimal examples/governed ``` Add `--pubkey` to cryptographically verify signatures, and `--strict` to treat warnings as errors: ```sh -python tools/okgf_validate.py --strict --pubkey examples/governed/ORG_PUBKEY.pem examples/governed +okgf --strict --pubkey examples/governed/ORG_PUBKEY.pem examples/governed ``` -Run the tests: +Developing on the validator? Install it editable and run the tests from a checkout: ```sh -pip install -r requirements-dev.txt +pip install -e ".[dev]" python -m pytest tests/ -q ``` +The historical `python tools/okgf_validate.py ...` entry point still works from a checkout; it is a +thin shim over the package. + ## Run a store To actually *use* OKGF, run the reference store: a knowledgebase you ingest into and retrieve from, where @@ -149,8 +178,9 @@ See [`docs/STORE.md`](docs/STORE.md) for the full API and design. - **[`SPEC.md`](SPEC.md)** - the v0.1 specification, the additive delta over OKF field by field. - **[`CONFORMANCE.md`](CONFORMANCE.md)** - what conformance means and how to check it. -- **[`tools/okgf_validate.py`](tools/okgf_validate.py)** - the reference validator, with optional Ed25519 - signature verification. +- **[`okgf/`](okgf/)** - the reference validator, installable as the `okgf` command (see + [`docs/INSTALL.md`](docs/INSTALL.md)), with optional Ed25519 signature verification. + [`tools/okgf_validate.py`](tools/okgf_validate.py) remains as a back-compat shim over it. - **[`store/`](store/)** - a runnable reference store: ingest, trust-filtered retrieval, the review lifecycle, and a zero-dependency HTTP API. See [`docs/STORE.md`](docs/STORE.md). - **[`examples/`](examples/)** - two bundles validated in CI: [`minimal/`](examples/minimal/) (OKF baseline, diff --git a/SECURITY.md b/SECURITY.md index 1276f68..a8ab75f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,9 +5,9 @@ matter and we take reports on both. ## Scope -- **The reference validator** (`tools/okgf_validate.py`) - parsing untrusted bundles (YAML, tar-free but - file I/O), and signature verification. A bug that lets a malformed bundle crash, hang, or mislead the - validator is in scope. +- **The reference validator** (the [`okgf/`](okgf/) package, run as the `okgf` command) - parsing + untrusted bundles (YAML, tar-free but file I/O), and signature verification. A bug that lets a malformed + bundle crash, hang, or mislead the validator is in scope. - **The trust model in the spec** - the `x-okgf-signature` scheme and the conformance rules. If you find a way a page could be treated as trusted when it should not be (or vice versa), that is a spec-level security issue and we want to hear it. diff --git a/SPEC.md b/SPEC.md index d37e456..9076b64 100644 --- a/SPEC.md +++ b/SPEC.md @@ -117,6 +117,7 @@ Consumers MUST tolerate missing governance fields and MUST preserve unrecognized ## Implementation -Reference validator: [`tools/okgf_validate.py`](tools/okgf_validate.py) in this repository checks -single-page and whole-bundle conformance, including optional Ed25519 signature verification. This -**specification** is Apache-2.0, so any tool may implement OKGF freely. +Reference validator: the [`okgf/`](okgf/) package in this repository, installable as the `okgf` +command (see [`docs/INSTALL.md`](docs/INSTALL.md)), checks single-page and whole-bundle conformance, +including optional Ed25519 signature verification. This **specification** is Apache-2.0, so any tool +may implement OKGF freely. diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..81a0852 --- /dev/null +++ b/conftest.py @@ -0,0 +1,11 @@ +"""Make a fresh source checkout importable as the ``okgf`` package during tests. + +The normal path is to install the package first (``pip install -e .``), which makes this a no-op. +This hook additionally lets ``python -m pytest`` work straight from an uninstalled checkout by putting +the repository root on ``sys.path``. +""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..e0524c4 --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,77 @@ +# Installing the OKGF validator + +The reference validator ships as a small Python package named `okgf`. Installing it gives you an +`okgf` command for validating OKGF pages and bundles. The validator depends only on PyYAML; +cryptographic signature verification is an optional extra. + +## One-line install (recommended) + +No system Python required - this uses [uv](https://docs.astral.sh/uv/) to manage an isolated Python +for the tool: + +```sh +curl -fsSL https://raw.githubusercontent.com/OneHillAI/okgf/main/install.sh | sh +``` + +To also enable Ed25519 signature verification, set `OKGF_VERIFY=1`: + +```sh +curl -fsSL https://raw.githubusercontent.com/OneHillAI/okgf/main/install.sh | OKGF_VERIFY=1 sh +``` + +Prefer to read before you run? The script is short: [`install.sh`](../install.sh). It falls back to +`pipx`, and installs `uv` only if neither `uv` nor `pipx` is present. + +## Homebrew (macOS / Linux) + +```sh +brew install OneHillAI/tap/okgf +``` + +## With pip, pipx, or uv + +If you already manage Python environments, install from PyPI directly: + +```sh +pipx install okgf # isolated, recommended for a CLI +uv tool install okgf # same idea, via uv +python -m pip install okgf # into the current environment +``` + +Add signature verification with the `verify` extra: + +```sh +pipx install "okgf[verify]" +``` + +## Verify the install + +```sh +okgf --version +okgf --strict path/to/bundle +okgf --strict --pubkey path/to/ORG_PUBKEY.pem path/to/bundle # verify signatures +``` + +`python -m okgf ...` works identically to the `okgf` command. + +## Optional: signature verification + +Signature checks need the [`cryptography`](https://cryptography.io/) package, pulled in by the +`verify` extra (`OKGF_VERIFY=1` for the one-line installer, or `pip install 'okgf[verify]'`). Without +it, a page that carries an `x-okgf-signature` is still format-checked, but reported as unverified +rather than trusted - the validator never claims a signature is valid when it cannot check it. + +## From a source checkout + +Working on the validator itself, or want to run it without installing? + +```sh +git clone https://github.com/OneHillAI/okgf.git +cd okgf +python -m pip install -e ".[dev]" # editable install with test + verify extras +okgf --strict examples/minimal +python -m pytest tests/ -q +``` + +The historical entry point `python tools/okgf_validate.py ...` still works from a checkout: it is a +thin shim that re-exports the package, kept so older scripts and docs do not break. diff --git a/examples/README.md b/examples/README.md index 66047b2..f91ba64 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,7 +22,7 @@ A fully governed bundle: Verify the signed promotion: ```sh -python ../../tools/okgf_validate.py --pubkey governed/ORG_PUBKEY.pem governed +okgf --pubkey governed/ORG_PUBKEY.pem governed ``` The signature in `billing-model.md` was produced with an example-only key. Do not reuse it; a real diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..87e699a --- /dev/null +++ b/install.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# OKGF validator installer. +# +# Installs the `okgf` command. No system Python required: this prefers uv, which manages an isolated +# Python for the tool. Usage: +# +# curl -fsSL https://raw.githubusercontent.com/OneHillAI/okgf/main/install.sh | sh +# +# Options (environment variables): +# OKGF_VERIFY=1 also install the `verify` extra (Ed25519 signature verification via cryptography) +# +# This is a thin convenience wrapper around a normal PyPI install. If you already manage Python +# environments, `pip install okgf` (or `pipx install okgf`, or `uv tool install okgf`) does the same. +set -eu + +PKG="okgf" +if [ "${OKGF_VERIFY:-0}" = "1" ]; then + PKG="okgf[verify]" +fi + +info() { printf 'okgf-install: %s\n' "$1" >&2; } + +install_with_uv() { + info "installing $PKG with uv" + uv tool install --force "$PKG" +} + +if command -v uv >/dev/null 2>&1; then + install_with_uv +elif command -v pipx >/dev/null 2>&1; then + info "uv not found; installing $PKG with pipx" + pipx install --force "$PKG" +else + info "installing uv (https://astral.sh/uv) to manage the install" + curl -fsSL https://astral.sh/uv/install.sh | sh + # Make uv available on PATH for the rest of this script. + if [ -r "${XDG_CONFIG_HOME:-$HOME/.config}/uv/env" ]; then + # shellcheck disable=SC1091 + . "${XDG_CONFIG_HOME:-$HOME/.config}/uv/env" + fi + PATH="$HOME/.local/bin:$PATH" + export PATH + install_with_uv +fi + +if command -v okgf >/dev/null 2>&1; then + info "installed: $(okgf --version)" + info "try: okgf --strict path/to/bundle" +else + info "installed, but 'okgf' is not on your PATH yet." + info "add your tool bin directory to PATH (uv uses ~/.local/bin) and reopen your shell." +fi diff --git a/okgf/__init__.py b/okgf/__init__.py new file mode 100644 index 0000000..b32283e --- /dev/null +++ b/okgf/__init__.py @@ -0,0 +1,9 @@ +"""OKGF - Open Knowledge and Governance Format reference tooling. + +This package provides the ``okgf`` conformance validator for OKGF pages and bundles. +See https://onehill.org/okgf and the spec in ``SPEC.md``. +""" + +from okgf.validate import __version__, main + +__all__ = ["__version__", "main"] diff --git a/okgf/__main__.py b/okgf/__main__.py new file mode 100644 index 0000000..0148cce --- /dev/null +++ b/okgf/__main__.py @@ -0,0 +1,6 @@ +"""Enable ``python -m okgf`` as an alias for the ``okgf`` console command.""" + +from okgf.validate import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/okgf/validate.py b/okgf/validate.py new file mode 100644 index 0000000..b34c22d --- /dev/null +++ b/okgf/validate.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""OKGF conformance validator. + +Checks a single page or a whole bundle against the OKGF v0.1 specification (see ``SPEC.md`` / +``CONFORMANCE.md``). OKGF is a strict superset of OKF, so this validator enforces: + +1. OKF baseline: every non-reserved ``.md`` has valid YAML frontmatter with a non-empty ``type``; a + bundle's root ``index.md`` declares ``okf_version``. +2. OKGF governance: wherever an ``x-okgf-*`` field is present, its value matches the spec table. + +Unknown frontmatter keys are tolerated and preserved (never an error), per the OKF rule that consumers +must round-trip unrecognized fields. + +Dependencies: Python 3.9+ and PyYAML. Cryptographic signature verification is OPTIONAL and only runs when +a public key is supplied (``--pubkey``) and the ``cryptography`` package is importable; without it, a +present signature is format-checked and reported as unverified rather than trusted. Install the +``verify`` extra (``pip install 'okgf[verify]'``) to pull in ``cryptography``. + +Usage: + okgf PATH [PATH ...] validate pages and/or bundle directories + okgf --pubkey key.pem PATH also cryptographically verify x-okgf-signature + okgf --strict PATH treat warnings as failures + okgf --version print the version and exit + +Exit status: 0 = conformant (no errors), 1 = at least one error (or a warning under --strict). +""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import sys +from dataclasses import dataclass, field +from pathlib import Path + +try: + import yaml +except ImportError: # pragma: no cover - guidance only + sys.stderr.write("okgf: PyYAML is required (pip install pyyaml)\n") + raise SystemExit(2) + +__version__ = "0.1.0" + +SPEC_VERSION = "0.1" +_DELIM = "---" + +# Reserved bundle files: structural, not content pages. +INDEX_FILE = "index.md" +LOG_FILE = "log.md" +RESERVED_FILES = {INDEX_FILE, LOG_FILE, "principles.md", "schema.md"} + +# Governance value tables (SPEC.md "Governance extensions"). +ENUMS = { + "x-okgf-scope": {"personal", "team", "org"}, + "x-okgf-review": {"draft", "proposed", "approved"}, + "x-okgf-tier": {"bronze", "silver", "gold"}, +} +_ED25519_SIG_BYTES = 64 # a raw Ed25519 signature is 64 bytes +_OKF_RECOMMENDED = ("title", "description", "timestamp") + + +@dataclass +class Problem: + path: str + level: str # "error" | "warning" + message: str + + def __str__(self) -> str: + return f"{self.level.upper():7} {self.path}: {self.message}" + + +@dataclass +class Report: + problems: list[Problem] = field(default_factory=list) + pages_checked: int = 0 + bundles_checked: int = 0 + + def error(self, path: str, msg: str) -> None: + self.problems.append(Problem(path, "error", msg)) + + def warning(self, path: str, msg: str) -> None: + self.problems.append(Problem(path, "warning", msg)) + + @property + def errors(self) -> list[Problem]: + return [p for p in self.problems if p.level == "error"] + + @property + def warnings(self) -> list[Problem]: + return [p for p in self.problems if p.level == "warning"] + + +def split_frontmatter(text: str) -> tuple[dict, str]: + """Split ``---``-delimited YAML frontmatter from the body. Returns ``(fm, body)``; ``fm`` is ``None`` + when no well-formed frontmatter block is present (so the caller can distinguish "absent" from "empty"). + Raises ``yaml.YAMLError`` when the block is present but not valid YAML.""" + t = text.lstrip("").lstrip() + if not t.startswith(_DELIM): + return None, text.strip() + rest = t[len(_DELIM):].lstrip("\n") + end = rest.find(f"\n{_DELIM}") + if end == -1: + return None, text.strip() + fm = yaml.safe_load(rest[:end]) + body = rest[end + len(_DELIM) + 1:].lstrip("\n").rstrip() + return (fm if fm is not None else {}), body + + +def _canonical_bytes(fm: dict, body: str) -> bytes: + """The bytes an x-okgf-signature signs: ``\\n\\n<scope>\\n<review>\\n<body>``.""" + parts = [ + str(fm.get("type") or ""), + str(fm.get("title") or ""), + str(fm.get("x-okgf-scope") or ""), + str(fm.get("x-okgf-review") or ""), + body, + ] + return "\n".join(parts).encode("utf-8") + + +def _load_pubkey(path: str): + """Load an Ed25519 public key from a PEM or raw-32-byte file. Returns a verifier or raises.""" + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + from cryptography.hazmat.primitives.serialization import load_pem_public_key + + raw = Path(path).read_bytes() + if raw.lstrip().startswith(b"-----BEGIN"): + return load_pem_public_key(raw) + if len(raw.strip()) == 32: + return Ed25519PublicKey.from_public_bytes(raw.strip()) + # try base64-encoded 32 bytes + decoded = base64.b64decode(raw.strip(), validate=True) + return Ed25519PublicKey.from_public_bytes(decoded) + + +def validate_page(text: str, path: str, report: Report, *, pubkey=None) -> None: + """Validate a single OKGF page (any non-reserved ``.md``).""" + report.pages_checked += 1 + try: + fm, body = split_frontmatter(text) + except yaml.YAMLError as exc: + report.error(path, f"invalid YAML frontmatter: {exc}") + return + if fm is None: + report.error(path, "no YAML frontmatter block (OKF requires one)") + return + if not isinstance(fm, dict): + report.error(path, "frontmatter is not a YAML mapping") + return + + # OKF baseline. + if not str(fm.get("type") or "").strip(): + report.error(path, "missing required non-empty `type`") + for key in _OKF_RECOMMENDED: + if not str(fm.get(key) or "").strip(): + report.warning(path, f"missing recommended OKF field `{key}`") + + # OKGF governance enums. + for key, allowed in ENUMS.items(): + if key in fm: + val = fm[key] + if val not in allowed: + report.error(path, f"`{key}` = {val!r}; must be one of {sorted(allowed)}") + + # Provenance: list of string URIs. + if "x-okgf-sources" in fm: + srcs = fm["x-okgf-sources"] + if not isinstance(srcs, list) or not all(isinstance(s, str) for s in srcs): + report.error(path, "`x-okgf-sources` must be a list of strings (URIs)") + + # Signature: base64 Ed25519, optionally verified. + if "x-okgf-signature" in fm: + _validate_signature(fm, body, path, report, pubkey=pubkey) + + +def _validate_signature(fm: dict, body: str, path: str, report: Report, *, pubkey) -> None: + sig = fm.get("x-okgf-signature") + if not isinstance(sig, str) or not sig.strip(): + report.error(path, "`x-okgf-signature` must be a non-empty base64 string") + return + try: + raw = base64.b64decode(sig.strip(), validate=True) + except (binascii.Error, ValueError): + report.error(path, "`x-okgf-signature` is not valid base64") + return + if len(raw) != _ED25519_SIG_BYTES: + report.error( + path, + f"`x-okgf-signature` decodes to {len(raw)} bytes; an Ed25519 signature is " + f"{_ED25519_SIG_BYTES}", + ) + return + if pubkey is None: + report.warning(path, "`x-okgf-signature` present but unverified (no public key supplied)") + return + from cryptography.exceptions import InvalidSignature + + try: + pubkey.verify(raw, _canonical_bytes(fm, body)) + except InvalidSignature: + report.error(path, "`x-okgf-signature` is present but INVALID (untrusted)") + + +def validate_bundle(root: Path, report: Report, *, pubkey=None) -> None: + """Validate a bundle directory: reserved-file structure plus every content page.""" + report.bundles_checked += 1 + index = root / INDEX_FILE + if not index.is_file(): + report.error(str(root), f"bundle is missing the reserved `{INDEX_FILE}`") + else: + _validate_index(index, report) + + for md in sorted(root.rglob("*.md")): + name = md.name.lower() + if name == INDEX_FILE: + continue + if name == LOG_FILE: + continue # reserved; free-form dated history, not a content page + if name in RESERVED_FILES: + continue + validate_page(md.read_text(encoding="utf-8"), str(md.relative_to(root)), report, pubkey=pubkey) + + +def _validate_index(index: Path, report: Report) -> None: + rel = index.name + try: + fm, _ = split_frontmatter(index.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + report.error(rel, f"invalid YAML frontmatter: {exc}") + return + if not isinstance(fm, dict) or "okf_version" not in fm: + report.error(rel, "root index.md must declare `okf_version` in its frontmatter") + return + declared = str(fm["okf_version"]) + if declared != SPEC_VERSION: + report.warning(rel, f"okf_version {declared!r} != validator's {SPEC_VERSION!r}") + + +def validate_path(path: Path, report: Report, *, pubkey=None) -> None: + if path.is_dir(): + validate_bundle(path, report, pubkey=pubkey) + elif path.is_file() and path.suffix.lower() == ".md": + if path.name.lower() == INDEX_FILE: + _validate_index(path, report) + elif path.name.lower() in RESERVED_FILES: + report.warning(str(path), f"`{path.name}` is a reserved file; nothing to validate") + else: + validate_page(path.read_text(encoding="utf-8"), str(path), report, pubkey=pubkey) + else: + report.error(str(path), "not a .md file or a bundle directory") + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(prog="okgf", description="Validate OKGF pages and bundles.") + ap.add_argument("paths", nargs="+", type=Path, help="pages (.md) or bundle directories") + ap.add_argument("--pubkey", type=str, default=None, help="Ed25519 public key (PEM/raw/base64) to verify signatures") + ap.add_argument("--strict", action="store_true", help="treat warnings as failures") + ap.add_argument("--version", action="version", version=f"okgf {__version__}") + args = ap.parse_args(argv) + + pubkey = None + if args.pubkey: + try: + pubkey = _load_pubkey(args.pubkey) + except ImportError: + sys.stderr.write("okgf: --pubkey needs the `cryptography` package (pip install 'okgf[verify]')\n") + return 2 + except Exception as exc: # noqa: BLE001 - surface any key-loading failure + sys.stderr.write(f"okgf: could not load public key: {exc}\n") + return 2 + + report = Report() + for path in args.paths: + validate_path(path, report, pubkey=pubkey) + + for problem in report.problems: + stream = sys.stderr if problem.level == "error" else sys.stdout + stream.write(str(problem) + "\n") + + n_err, n_warn = len(report.errors), len(report.warnings) + summary = ( + f"okgf: {report.pages_checked} page(s), {report.bundles_checked} bundle(s), " + f"{n_err} error(s), {n_warn} warning(s)" + ) + print(summary) + if n_err or (args.strict and n_warn): + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packaging/homebrew/okgf.rb b/packaging/homebrew/okgf.rb new file mode 100644 index 0000000..b498a8f --- /dev/null +++ b/packaging/homebrew/okgf.rb @@ -0,0 +1,44 @@ +# Homebrew formula for the OKGF validator. +# +# This is the source of truth to copy into the tap repository (OneHillAI/homebrew-tap) as +# Formula/okgf.rb. Users then install with: +# +# brew install OneHillAI/tap/okgf +# +# The `url`/`sha256` below point at the PyPI sdist for a given version. On each release they are +# updated to the new version and its checksum (the release workflow can run `brew bump-formula-pr`, +# or a maintainer updates them by hand). The PyYAML resource is the only runtime dependency. +class Okgf < Formula + include Language::Python::Virtualenv + + desc "Reference validator for the Open Knowledge and Governance Format (OKGF)" + homepage "https://onehill.org/okgf" + url "https://files.pythonhosted.org/packages/source/o/okgf/okgf-0.1.0.tar.gz" + sha256 "REPLACE_WITH_OKGF_SDIST_SHA256" + license "Apache-2.0" + + depends_on "python@3.12" + + resource "PyYAML" do + url "https://files.pythonhosted.org/packages/source/P/PyYAML/pyyaml-6.0.2.tar.gz" + sha256 "REPLACE_WITH_PYYAML_SDIST_SHA256" + end + + def install + virtualenv_install_with_resources + end + + test do + assert_match "okgf #{version}", shell_output("#{bin}/okgf --version") + + (testpath/"index.md").write <<~EOS + --- + okf_version: "0.1" + type: Index + title: Test bundle + --- + # Test bundle + EOS + assert_match "0 error(s)", shell_output("#{bin}/okgf #{testpath}") + end +end diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5ff5f83 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["hatchling>=1.18"] +build-backend = "hatchling.build" + +[project] +name = "okgf" +version = "0.1.0" +description = "Reference validator for the Open Knowledge and Governance Format (OKGF)." +readme = "README.md" +requires-python = ">=3.9" +license = { file = "LICENSE" } +authors = [{ name = "OneHill Foundation" }] +keywords = ["okgf", "okf", "knowledge", "governance", "markdown", "validator"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Text Processing :: Markup", +] +# The validator itself needs only PyYAML. Signature verification is optional (see the `verify` extra). +dependencies = ["PyYAML>=6.0"] + +[project.urls] +Homepage = "https://onehill.org/okgf" +Repository = "https://github.com/OneHillAI/okgf" +Specification = "https://github.com/OneHillAI/okgf/blob/main/SPEC.md" +Issues = "https://github.com/OneHillAI/okgf/issues" + +[project.optional-dependencies] +# Ed25519 signature verification (`okgf --pubkey ...`). +verify = ["cryptography>=42.0"] +# Everything needed to develop and test the package. +dev = ["pytest>=8.0", "cryptography>=42.0", "build>=1.0"] + +[project.scripts] +okgf = "okgf.validate:main" + +[tool.hatch.build.targets.wheel] +# Ship only the validator package; the reference store and examples stay in the repo/sdist. +packages = ["okgf"] + +[tool.hatch.build.targets.sdist] +include = [ + "okgf/", + "tools/okgf_validate.py", + "tests/", + "examples/", + "docs/", + "conftest.py", + "SPEC.md", + "CONFORMANCE.md", + "README.md", + "LICENSE", + "NOTICE", +] diff --git a/tests/test_okgf_validate.py b/tests/test_okgf_validate.py index 3f4e3ba..458dd91 100644 --- a/tests/test_okgf_validate.py +++ b/tests/test_okgf_validate.py @@ -1,17 +1,12 @@ """Tests for the OKGF conformance validator.""" -import importlib.util -import sys from pathlib import Path import pytest -ROOT = Path(__file__).resolve().parent.parent -_spec = importlib.util.spec_from_file_location("okgf_validate", ROOT / "tools" / "okgf_validate.py") -okgf = importlib.util.module_from_spec(_spec) -sys.modules["okgf_validate"] = okgf # dataclasses resolves annotations via the registered module -_spec.loader.exec_module(okgf) +from okgf import validate as okgf +ROOT = Path(__file__).resolve().parent.parent EXAMPLES = ROOT / "examples" diff --git a/tools/okgf_validate.py b/tools/okgf_validate.py index 44ec350..1131025 100644 --- a/tools/okgf_validate.py +++ b/tools/okgf_validate.py @@ -1,288 +1,39 @@ #!/usr/bin/env python3 -"""OKGF conformance validator. +"""Backwards-compatible shim for the OKGF validator. -Checks a single page or a whole bundle against the OKGF v0.1 specification (see ``SPEC.md`` / -``CONFORMANCE.md``). OKGF is a strict superset of OKF, so this validator enforces: - -1. OKF baseline: every non-reserved ``.md`` has valid YAML frontmatter with a non-empty ``type``; a - bundle's root ``index.md`` declares ``okf_version``. -2. OKGF governance: wherever an ``x-okgf-*`` field is present, its value matches the spec table. - -Unknown frontmatter keys are tolerated and preserved (never an error), per the OKF rule that consumers -must round-trip unrecognized fields. - -Dependencies: Python 3.9+ and PyYAML. Cryptographic signature verification is OPTIONAL and only runs when -a public key is supplied (``--pubkey``) and the ``cryptography`` package is importable; without it, a -present signature is format-checked and reported as unverified rather than trusted. - -Usage: - okgf_validate.py PATH [PATH ...] validate pages and/or bundle directories - okgf_validate.py --pubkey key.pem PATH also cryptographically verify x-okgf-signature - okgf_validate.py --strict PATH treat warnings as failures - -Exit status: 0 = conformant (no errors), 1 = at least one error (or a warning under --strict). +The validator now lives in the installable ``okgf`` package: install it (``pip install okgf`` or see +``docs/INSTALL.md``) and run the ``okgf`` command, or ``python -m okgf``. This script is kept so that +existing invocations (``python tools/okgf_validate.py ...``) and imports keep working unchanged; it +re-exports the package API and delegates to it. """ from __future__ import annotations -import argparse -import base64 -import binascii +import os import sys -from dataclasses import dataclass, field -from pathlib import Path - -try: - import yaml -except ImportError: # pragma: no cover - guidance only - sys.stderr.write("okgf_validate: PyYAML is required (pip install pyyaml)\n") - raise SystemExit(2) - -SPEC_VERSION = "0.1" -_DELIM = "---" - -# Reserved bundle files: structural, not content pages. -INDEX_FILE = "index.md" -LOG_FILE = "log.md" -RESERVED_FILES = {INDEX_FILE, LOG_FILE, "principles.md", "schema.md"} - -# Governance value tables (SPEC.md "Governance extensions"). -ENUMS = { - "x-okgf-scope": {"personal", "team", "org"}, - "x-okgf-review": {"draft", "proposed", "approved"}, - "x-okgf-tier": {"bronze", "silver", "gold"}, -} -_ED25519_SIG_BYTES = 64 # a raw Ed25519 signature is 64 bytes -_OKF_RECOMMENDED = ("title", "description", "timestamp") - - -@dataclass -class Problem: - path: str - level: str # "error" | "warning" - message: str - - def __str__(self) -> str: - return f"{self.level.upper():7} {self.path}: {self.message}" - - -@dataclass -class Report: - problems: list[Problem] = field(default_factory=list) - pages_checked: int = 0 - bundles_checked: int = 0 - - def error(self, path: str, msg: str) -> None: - self.problems.append(Problem(path, "error", msg)) - - def warning(self, path: str, msg: str) -> None: - self.problems.append(Problem(path, "warning", msg)) - - @property - def errors(self) -> list[Problem]: - return [p for p in self.problems if p.level == "error"] - - @property - def warnings(self) -> list[Problem]: - return [p for p in self.problems if p.level == "warning"] - - -def split_frontmatter(text: str) -> tuple[dict, str]: - """Split ``---``-delimited YAML frontmatter from the body. Returns ``(fm, body)``; ``fm`` is ``None`` - when no well-formed frontmatter block is present (so the caller can distinguish "absent" from "empty"). - Raises ``yaml.YAMLError`` when the block is present but not valid YAML.""" - t = text.lstrip("").lstrip() - if not t.startswith(_DELIM): - return None, text.strip() - rest = t[len(_DELIM):].lstrip("\n") - end = rest.find(f"\n{_DELIM}") - if end == -1: - return None, text.strip() - fm = yaml.safe_load(rest[:end]) - body = rest[end + len(_DELIM) + 1:].lstrip("\n").rstrip() - return (fm if fm is not None else {}), body - - -def _canonical_bytes(fm: dict, body: str) -> bytes: - """The bytes an x-okgf-signature signs: ``<type>\\n<title>\\n<scope>\\n<review>\\n<body>``.""" - parts = [ - str(fm.get("type") or ""), - str(fm.get("title") or ""), - str(fm.get("x-okgf-scope") or ""), - str(fm.get("x-okgf-review") or ""), - body, - ] - return "\n".join(parts).encode("utf-8") - - -def _load_pubkey(path: str): - """Load an Ed25519 public key from a PEM or raw-32-byte file. Returns a verifier or raises.""" - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - from cryptography.hazmat.primitives.serialization import load_pem_public_key - - raw = Path(path).read_bytes() - if raw.lstrip().startswith(b"-----BEGIN"): - return load_pem_public_key(raw) - if len(raw.strip()) == 32: - return Ed25519PublicKey.from_public_bytes(raw.strip()) - # try base64-encoded 32 bytes - decoded = base64.b64decode(raw.strip(), validate=True) - return Ed25519PublicKey.from_public_bytes(decoded) - - -def validate_page(text: str, path: str, report: Report, *, pubkey=None) -> None: - """Validate a single OKGF page (any non-reserved ``.md``).""" - report.pages_checked += 1 - try: - fm, body = split_frontmatter(text) - except yaml.YAMLError as exc: - report.error(path, f"invalid YAML frontmatter: {exc}") - return - if fm is None: - report.error(path, "no YAML frontmatter block (OKF requires one)") - return - if not isinstance(fm, dict): - report.error(path, "frontmatter is not a YAML mapping") - return - - # OKF baseline. - if not str(fm.get("type") or "").strip(): - report.error(path, "missing required non-empty `type`") - for key in _OKF_RECOMMENDED: - if not str(fm.get(key) or "").strip(): - report.warning(path, f"missing recommended OKF field `{key}`") - - # OKGF governance enums. - for key, allowed in ENUMS.items(): - if key in fm: - val = fm[key] - if val not in allowed: - report.error(path, f"`{key}` = {val!r}; must be one of {sorted(allowed)}") - - # Provenance: list of string URIs. - if "x-okgf-sources" in fm: - srcs = fm["x-okgf-sources"] - if not isinstance(srcs, list) or not all(isinstance(s, str) for s in srcs): - report.error(path, "`x-okgf-sources` must be a list of strings (URIs)") - - # Signature: base64 Ed25519, optionally verified. - if "x-okgf-signature" in fm: - _validate_signature(fm, body, path, report, pubkey=pubkey) - - -def _validate_signature(fm: dict, body: str, path: str, report: Report, *, pubkey) -> None: - sig = fm.get("x-okgf-signature") - if not isinstance(sig, str) or not sig.strip(): - report.error(path, "`x-okgf-signature` must be a non-empty base64 string") - return - try: - raw = base64.b64decode(sig.strip(), validate=True) - except (binascii.Error, ValueError): - report.error(path, "`x-okgf-signature` is not valid base64") - return - if len(raw) != _ED25519_SIG_BYTES: - report.error( - path, - f"`x-okgf-signature` decodes to {len(raw)} bytes; an Ed25519 signature is " - f"{_ED25519_SIG_BYTES}", - ) - return - if pubkey is None: - report.warning(path, "`x-okgf-signature` present but unverified (no public key supplied)") - return - from cryptography.exceptions import InvalidSignature - - try: - pubkey.verify(raw, _canonical_bytes(fm, body)) - except InvalidSignature: - report.error(path, "`x-okgf-signature` is present but INVALID (untrusted)") - - -def validate_bundle(root: Path, report: Report, *, pubkey=None) -> None: - """Validate a bundle directory: reserved-file structure plus every content page.""" - report.bundles_checked += 1 - index = root / INDEX_FILE - if not index.is_file(): - report.error(str(root), f"bundle is missing the reserved `{INDEX_FILE}`") - else: - _validate_index(index, report) - - for md in sorted(root.rglob("*.md")): - name = md.name.lower() - if name == INDEX_FILE: - continue - if name == LOG_FILE: - continue # reserved; free-form dated history, not a content page - if name in RESERVED_FILES: - continue - validate_page(md.read_text(encoding="utf-8"), str(md.relative_to(root)), report, pubkey=pubkey) - - -def _validate_index(index: Path, report: Report) -> None: - rel = index.name - try: - fm, _ = split_frontmatter(index.read_text(encoding="utf-8")) - except yaml.YAMLError as exc: - report.error(rel, f"invalid YAML frontmatter: {exc}") - return - if not isinstance(fm, dict) or "okf_version" not in fm: - report.error(rel, "root index.md must declare `okf_version` in its frontmatter") - return - declared = str(fm["okf_version"]) - if declared != SPEC_VERSION: - report.warning(rel, f"okf_version {declared!r} != validator's {SPEC_VERSION!r}") - - -def validate_path(path: Path, report: Report, *, pubkey=None) -> None: - if path.is_dir(): - validate_bundle(path, report, pubkey=pubkey) - elif path.is_file() and path.suffix.lower() == ".md": - if path.name.lower() == INDEX_FILE: - _validate_index(path, report) - elif path.name.lower() in RESERVED_FILES: - report.warning(str(path), f"`{path.name}` is a reserved file; nothing to validate") - else: - validate_page(path.read_text(encoding="utf-8"), str(path), report, pubkey=pubkey) - else: - report.error(str(path), "not a .md file or a bundle directory") - - -def main(argv: list[str] | None = None) -> int: - ap = argparse.ArgumentParser(description="Validate OKGF pages and bundles.") - ap.add_argument("paths", nargs="+", type=Path, help="pages (.md) or bundle directories") - ap.add_argument("--pubkey", type=str, default=None, help="Ed25519 public key (PEM/raw/base64) to verify signatures") - ap.add_argument("--strict", action="store_true", help="treat warnings as failures") - args = ap.parse_args(argv) - - pubkey = None - if args.pubkey: - try: - pubkey = _load_pubkey(args.pubkey) - except ImportError: - sys.stderr.write("okgf_validate: --pubkey needs the `cryptography` package installed\n") - return 2 - except Exception as exc: # noqa: BLE001 - surface any key-loading failure - sys.stderr.write(f"okgf_validate: could not load public key: {exc}\n") - return 2 - - report = Report() - for path in args.paths: - validate_path(path, report, pubkey=pubkey) - - for problem in report.problems: - stream = sys.stderr if problem.level == "error" else sys.stdout - stream.write(str(problem) + "\n") - - n_err, n_warn = len(report.errors), len(report.warnings) - summary = ( - f"okgf_validate: {report.pages_checked} page(s), {report.bundles_checked} bundle(s), " - f"{n_err} error(s), {n_warn} warning(s)" - ) - print(summary) - if n_err or (args.strict and n_warn): - return 1 - return 0 +# Allow running straight from a source checkout without installing the package first. +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from okgf.validate import * # noqa: E402,F401,F403 (public API re-export) +from okgf.validate import ( # noqa: E402 explicit re-exports, including names used by callers/tests + ENUMS, + INDEX_FILE, + LOG_FILE, + RESERVED_FILES, + SPEC_VERSION, + Problem, + Report, + __version__, + _canonical_bytes, + _load_pubkey, + _validate_signature, + main, + split_frontmatter, + validate_bundle, + validate_page, + validate_path, +) if __name__ == "__main__": raise SystemExit(main()) From b4e45f63eefd0fcd3c28a636c5265b8a3135cb05 Mon Sep 17 00:00:00 2001 From: Onehill <dev@onehill.org> Date: Thu, 22 Oct 2026 09:53:56 +0200 Subject: [PATCH 2/3] ci(conformance): restore the stable `validate` required status context The previous commit put the conformance job behind a build matrix, which made GitHub report the status contexts as `validate (3.9)` and `validate (3.12)`. The branch protection on `main` requires a check named exactly `validate`, so that context was never produced and the PR stayed blocked on a missing required check even though every matrix leg passed. Rename the matrix job to `test` and add a lightweight `validate` gate job that `needs: test`, so the required `validate` context is emitted again and turns green only when both the 3.9 and 3.12 legs pass. No change to what is tested. Signed-off-by: Onehill <dev@onehill.org> Agent: Claude Opus 4.8 (instructed by awchristoph) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- .github/workflows/conformance.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index bb59edc..6a641ef 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -6,7 +6,7 @@ on: pull_request: jobs: - validate: + test: runs-on: ubuntu-latest strategy: fail-fast: false @@ -27,3 +27,12 @@ jobs: run: python tools/okgf_validate.py --strict examples/minimal - name: Run tests run: python -m pytest tests/ -q + + # Stable required-status context. Branch protection requires the single check + # `validate`; this gate passes only when every `test` matrix leg (3.9, 3.12) + # passes, so the required check name does not change with the matrix. + validate: + needs: test + runs-on: ubuntu-latest + steps: + - run: echo "All conformance matrix legs passed." From c69f9a1ff47b9e98f12798c0879523f289b2f186 Mon Sep 17 00:00:00 2001 From: Onehill <dev@onehill.org> Date: Thu, 22 Oct 2026 09:56:42 +0200 Subject: [PATCH 3/3] ci(conformance): scope GITHUB_TOKEN to read-only CodeQL (code scanning) flagged the conformance workflow for not restricting the GITHUB_TOKEN permissions. The workflow only checks out and validates code and never writes to the repository, so add a top-level `permissions: contents: read` block (the recommended minimal default). No change to what runs. Signed-off-by: Onehill <dev@onehill.org> Agent: Claude Opus 4.8 (instructed by awchristoph) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- .github/workflows/conformance.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 6a641ef..f69ab9f 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -5,6 +5,11 @@ on: branches: [main] pull_request: +# Read-only token: this workflow only checks out and validates code, it never +# writes to the repo. Scoping GITHUB_TOKEN down is the CodeQL-recommended default. +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest