Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
pull_request:
workflow_dispatch:

jobs:
guardrails:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Check mirrored metadata invariants
run: python check_invariants.py

- name: Smoke install
run: python -m pip install .
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ references in this repository, creates the corresponding mirror commit, pushes
tag `vX.Y.Z`, and opens a GitHub release that points back to the mirrored PyPI
release.

Pull requests run a metadata invariant check plus a smoke install so version
drift is caught before release automation runs.

## Usage

```yaml
Expand Down
59 changes: 59 additions & 0 deletions check_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Validate mirrored release metadata stays in lockstep."""

from __future__ import annotations

import re
import tomllib
from pathlib import Path

ROOT = Path(__file__).resolve().parent
PYPROJECT = ROOT / "pyproject.toml"
README = ROOT / "README.md"

README_REV_PATTERN = re.compile(r"(?m)^\s*rev: v(?P<version>\d+\.\d+\.\d+)\s*$")


def validate_repo_state() -> None:
pyproject_version, dependency_pin = read_pyproject_versions()
if pyproject_version != dependency_pin:
raise RuntimeError(
"pyproject.toml version and spore-lang dependency pin must match "
f"({pyproject_version!r} != {dependency_pin!r})."
)

readme_version = read_readme_version()
if readme_version != pyproject_version:
raise RuntimeError(
"README.md rev must match the pyproject.toml version "
f"({readme_version!r} != {pyproject_version!r})."
)


def read_pyproject_versions() -> tuple[str, str]:
with PYPROJECT.open("rb") as handle:
pyproject = tomllib.load(handle)

project = pyproject["project"]
project_version = str(project["version"])
dependency_pin = read_dependency_pin(project["dependencies"][0])
return project_version, dependency_pin


def read_dependency_pin(requirement: str) -> str:
match = re.fullmatch(r"spore-lang==(?P<version>\d+\.\d+\.\d+)", requirement)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect configured package name in invariant check

sync.py still advertises SPORE_PACKAGE_NAME as a runtime override, but read_dependency_pin hardcodes spore-lang in the new invariant guard. In any environment where the mirror is configured for another package and pyproject.toml is pinned correctly for that package, validate_repo_state() now fails before sync can run, which is a regression from prior behavior.

Useful? React with 👍 / 👎.

if match is None:
raise RuntimeError(
"Expected pyproject.toml to pin spore-lang with an exact release version."
)
return match.group("version")


def read_readme_version() -> str:
matches = README_REV_PATTERN.findall(README.read_text())
if len(matches) != 1:
raise RuntimeError("README.md must contain exactly one mirrored rev line.")
return matches[0]


if __name__ == "__main__":
validate_repo_state()
12 changes: 11 additions & 1 deletion sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from packaging.requirements import Requirement
from packaging.version import Version

from check_invariants import validate_repo_state

ROOT = Path(__file__).resolve().parent
PYPROJECT = ROOT / "pyproject.toml"
README = ROOT / "README.md"
Expand All @@ -37,6 +39,7 @@

def main() -> None:
current_version = read_current_version()
validate_repo_state()
maybe_publish_current_tag(current_version)
versions = find_new_versions(current_version)
if not versions:
Expand All @@ -63,11 +66,18 @@ def read_current_version() -> Version:
with PYPROJECT.open("rb") as handle:
pyproject = tomllib.load(handle)

project_version = Version(pyproject["project"]["version"])
requirement = Requirement(pyproject["project"]["dependencies"][0])
specifier = next(iter(requirement.specifier), None)
if specifier is None or specifier.operator != "==":
raise RuntimeError(f"Expected an exact {PACKAGE_NAME} pin in pyproject.toml.")
return Version(specifier.version)
dependency_version = Version(specifier.version)
if project_version != dependency_version:
raise RuntimeError(
"pyproject.toml version and dependency pin must match "
f"({project_version} != {dependency_version})."
)
return dependency_version


def find_new_versions(current_version: Version) -> list[Version]:
Expand Down
Loading