Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ forerunner = "codeforerunner.cli:main"
where = ["src"]

[tool.setuptools.package-data]
codeforerunner = ["py.typed", "prompts/**/*.md", "tasks.json"]
codeforerunner = ["py.typed", "prompts/**/*.md", "tasks.json", "release_surfaces.json"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
68 changes: 68 additions & 0 deletions src/codeforerunner/release_surfaces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"$comment": "Release Surface Manifest — single source of truth for which release surfaces exist and their release policy (version source, registry target, auth mode, required validations). See release_surfaces.py for the accessor API.",
"surfaces": [
{
"name": "pypi",
"kind": "package_registry",
"registry": "https://pypi.org/project/codeforerunner/",
"auth_mode": "oidc",
"workflow": "publish.yml",
"version_source": { "file": "pyproject.toml", "kind": "toml_path", "selector": ["project", "version"] },
"validations": ["check_versions", "tag_matches_version", "validate_codex_marketplace", "build_sdist_wheel"]
},
{
"name": "npmjs",
"kind": "package_registry",
"registry": "https://www.npmjs.com/package/codeforerunner",
"auth_mode": "oidc",
"workflow": "npm-publish.yml",
"version_source": { "file": "package.json", "kind": "json_path", "selector": ["version"] },
"validations": ["tag_matches_version", "provenance"]
},
{
"name": "github-packages",
"kind": "package_registry",
"registry": "https://npm.pkg.github.com",
"auth_mode": "github_token",
"workflow": "npm-publish.yml",
"version_source": { "file": "package.json", "kind": "json_path", "selector": ["version"] },
"validations": []
},
{
"name": "docker",
"kind": "container_registry",
"registry": "ghcr.io/derek-palmer/codeforerunner, heyderekp/codeforerunner",
"auth_mode": "github_token",
"workflow": "docker-publish.yml",
"version_source": { "file": "pyproject.toml", "kind": "toml_path", "selector": ["project", "version"] },
"validations": ["check_versions", "tag_matches_version"]
},
{
"name": "codex-marketplace",
"kind": "plugin_marketplace",
"registry": "plugins/codex/marketplace.json (GitHub release asset)",
"auth_mode": "github_token",
"workflow": "codex-marketplace-publish.yml",
"version_source": { "file": "plugins/codex/marketplace.json", "kind": "json_path", "selector": ["marketplace", "version"] },
"validations": ["check_versions", "validate_codex_marketplace", "tag_matches_version"]
},
{
"name": "installer-shim-sh",
"kind": "installer_shim",
"registry": "install.sh (pins codeforerunner@<version> from npm)",
"auth_mode": "none",
"workflow": null,
"version_source": { "file": "install.sh", "kind": "regex", "selector": "^NPM_PKG=\"codeforerunner@([^\"]+)\"" },
"validations": ["check_versions"]
},
{
"name": "installer-shim-ps1",
"kind": "installer_shim",
"registry": "install.ps1 (pins codeforerunner@<version> from npm)",
"auth_mode": "none",
"workflow": null,
"version_source": { "file": "install.ps1", "kind": "regex", "selector": "^\\$NpmPkg\\s*=\\s*\"codeforerunner@([^\"]+)\"" },
"validations": ["check_versions"]
}
]
}
109 changes: 109 additions & 0 deletions src/codeforerunner/release_surfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Release Surface Manifest — single source of truth for release policy.

Catalogs the surfaces codeforerunner publishes to (PyPI, npmjs, GitHub
Packages, Docker, the Codex marketplace, installer shims) and, for each, the
version source, registry target, auth mode, and required validations.

The manifest itself lives in ``release_surfaces.json``; this module is the
typed accessor over it, mirroring the Task Registry (``tasks.py``). Version
values are read lazily via :func:`read_surface_version` from a repo checkout —
nothing here touches the filesystem at import time beyond the packaged JSON.
"""

from __future__ import annotations

import importlib.resources
import json
import re
import tomllib
from dataclasses import dataclass
from pathlib import Path

KINDS = frozenset(
{"package_registry", "container_registry", "plugin_marketplace", "installer_shim"}
)
AUTH_MODES = frozenset({"oidc", "github_token", "pat", "none"})
VERSION_SOURCE_KINDS = frozenset({"toml_path", "json_path", "regex"})


@dataclass(frozen=True)
class ReleaseSurface:
name: str
kind: str
registry: str
auth_mode: str
workflow: str | None
version_source: dict
validations: tuple[str, ...]


def _load() -> list[ReleaseSurface]:
data = json.loads(
importlib.resources.files("codeforerunner")
.joinpath("release_surfaces.json")
.read_text(encoding="utf-8")
)
return [
ReleaseSurface(
name=entry["name"],
kind=entry["kind"],
registry=entry["registry"],
auth_mode=entry["auth_mode"],
workflow=entry.get("workflow"),
version_source=entry["version_source"],
validations=tuple(entry.get("validations", [])),
)
for entry in data["surfaces"]
]


_SURFACES = _load()
_BY_NAME: dict[str, ReleaseSurface] = {s.name: s for s in _SURFACES}


def all_surfaces() -> list[ReleaseSurface]:
return list(_SURFACES)


def names() -> tuple[str, ...]:
return tuple(s.name for s in _SURFACES)


def get(name: str) -> ReleaseSurface:
try:
return _BY_NAME[name]
except KeyError:
raise KeyError(f"unknown release surface: {name!r}") from None


def version_bearing_surfaces() -> list[ReleaseSurface]:
"""Surfaces that pin a published version (all of them, currently)."""
return [s for s in _SURFACES if s.version_source]


def _navigate(data, selector):
for key in selector:
data = data[key]
return data


def read_surface_version(surface: ReleaseSurface, repo_root: Path) -> str:
"""Read a surface's version from its declared source under ``repo_root``.

Lets version-drift checks be driven entirely by the manifest: read every
version-bearing surface and assert the values agree.
"""
src = surface.version_source
path = repo_root / src["file"]
kind = src["kind"]
if kind == "toml_path":
with path.open("rb") as f:
return str(_navigate(tomllib.load(f), src["selector"]))
if kind == "json_path":
return str(_navigate(json.loads(path.read_text(encoding="utf-8")), src["selector"]))
if kind == "regex":
m = re.search(src["selector"], path.read_text(encoding="utf-8"), re.MULTILINE)
if not m:
raise ValueError(f"{surface.name}: pattern did not match in {src['file']}")
return m.group(1)
raise ValueError(f"{surface.name}: unknown version_source kind {kind!r}")
75 changes: 75 additions & 0 deletions tests/test_release_surfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Behavior tests for the Release Surface Manifest (release_surfaces.py)."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from codeforerunner import release_surfaces as rs

REPO_ROOT = Path(__file__).resolve().parent.parent


def test_manifest_loads_with_valid_shape():
surfaces = rs.all_surfaces()
assert surfaces, "manifest must declare at least one surface"
for s in surfaces:
assert s.name, "surface needs a name"
assert s.kind in rs.KINDS, f"{s.name}: bad kind {s.kind!r}"
assert s.auth_mode in rs.AUTH_MODES, f"{s.name}: bad auth_mode {s.auth_mode!r}"
assert isinstance(s.validations, tuple)
vs = s.version_source
assert isinstance(vs, dict) and "file" in vs and "kind" in vs
assert vs["kind"] in rs.VERSION_SOURCE_KINDS, f"{s.name}: bad version_source kind"


def test_surface_names_are_unique():
names = rs.names()
assert len(names) == len(set(names)), f"duplicate surface names in {names!r}"


def test_known_release_surfaces_are_represented():
expected = {
"pypi",
"npmjs",
"github-packages",
"docker",
"codex-marketplace",
"installer-shim-sh",
"installer-shim-ps1",
}
assert expected <= set(rs.names()), f"missing surfaces: {expected - set(rs.names())}"


def test_get_raises_on_unknown_surface():
with pytest.raises(KeyError):
rs.get("does-not-exist")


def test_version_sources_resolve_and_agree_in_this_checkout():
# Manifest-driven version-parity check: every version-bearing surface reads
# a concrete version from this repo, and they all agree (no drift).
versions = {
s.name: rs.read_surface_version(s, REPO_ROOT)
for s in rs.version_bearing_surfaces()
}
assert all(versions.values()), f"a surface resolved an empty version: {versions}"
assert len(set(versions.values())) == 1, f"version drift across surfaces: {versions}"


def test_read_surface_version_detects_drift(tmp_path):
# Build a fake checkout where pyproject and package.json disagree; the
# manifest-driven reader must surface two distinct values.
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "codeforerunner"\nversion = "0.4.5"\n', encoding="utf-8"
)
(tmp_path / "package.json").write_text(
json.dumps({"name": "codeforerunner", "version": "0.4.6"}), encoding="utf-8"
)
pypi = rs.get("pypi")
npmjs = rs.get("npmjs")
assert rs.read_surface_version(pypi, tmp_path) == "0.4.5"
assert rs.read_surface_version(npmjs, tmp_path) == "0.4.6"
assert rs.read_surface_version(pypi, tmp_path) != rs.read_surface_version(npmjs, tmp_path)