diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2f47d5..24c2bb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Publish versioned release on: push: branches: [main] - paths: [VERSION, '.github/workflows/release.yml'] + paths: [VERSION] workflow_dispatch: permissions: contents: read @@ -23,9 +23,7 @@ jobs: - run: pip install build twine pytest pytest-asyncio httpx -e . - run: python -m pytest tests -q - run: python -m build && twine check dist/* - - name: Validate release version - run: | - python -c "import pathlib,re; v=pathlib.Path('VERSION').read_text().strip(); assert re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:a[0-9]+)?',v)" + - run: python scripts/check_release.py - uses: actions/upload-artifact@v4 with: name: distributions diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index d16d8bc..db32642 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,9 +1,9 @@ -# Release 0.7.0 +# Release 0.7.1 -- Default create and verify commands target JEP Core 0.7 versioned API endpoints. -- Validation uses `status` and independent checks rather than cumulative levels. -- Current create path no longer assumes a Core nonce. -- Historical pre-0.7 verification is available only through explicit `--legacy` - decoder selection. -- Repository links and examples now point to the canonical `hjs-spec/jep-core`. +Release the current JEP Core 0.7 CLI after repairing the acceptance regression gate. +- Default creation and verification use `/v0.7/events/*`; audience is optional unless a selected profile requires it. +- Explicit `--legacy` / `--legacy-format` options preserve the historical decoder contract. No fallback after current validation fails. +- Invalid and indeterminate verification return exit code 2; input or transport errors return 1. +- Large inline JSON is parsed before filesystem probing. +- Package, import, user-agent, tag, and built distribution versions are aligned and checked before publication. diff --git a/VERSION b/VERSION index faef31a..39e898a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.7.1 diff --git a/jep_cli/__init__.py b/jep_cli/__init__.py index 39e24bc..87ee72b 100644 --- a/jep_cli/__init__.py +++ b/jep_cli/__init__.py @@ -1,3 +1,3 @@ """JEP CLI v0.6.""" -__version__ = "0.6.0" +__version__ = "0.7.1" diff --git a/jep_cli/client.py b/jep_cli/client.py index feec2b5..c6be8b8 100644 --- a/jep_cli/client.py +++ b/jep_cli/client.py @@ -44,7 +44,7 @@ def _json(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Di data = None headers = { "content-type": "application/json", - "user-agent": "JEP-CLI/0.7.0", + "user-agent": "JEP-CLI/0.7.1", } if self.api_key: headers["authorization"] = f"Bearer {self.api_key}" diff --git a/jep_cli/main.py b/jep_cli/main.py index 21b6dbc..1ab7052 100644 --- a/jep_cli/main.py +++ b/jep_cli/main.py @@ -19,14 +19,19 @@ def parse_json_value(value: str) -> Any: if value == "-": return json.load(sys.stdin) - path = Path(value) - if path.exists() and path.is_file(): - return json.loads(path.read_text(encoding="utf-8")) - try: return json.loads(value) except json.JSONDecodeError: - return value + pass + # Inline event JSON may be longer than the filesystem filename limit. + path = Path(value) + try: + is_file = path.is_file() + except OSError: + is_file = False + if is_file: + return json.loads(path.read_text(encoding="utf-8")) + return value def parse_ext_items(items: list[str]) -> Dict[str, Any]: @@ -140,7 +145,7 @@ def build_parser() -> argparse.ArgumentParser: verify = sub.add_parser("verify", help="Verify a JEP event") verify.add_argument("event", help="Event JSON object, event JSON file path, or '-' for stdin") verify.add_argument("--mode", default="archival", choices=["archival", "acceptance"], help="Validation mode") - verify.add_argument("--expected-audience", default="", help="Required receiving audience for acceptance") + verify.add_argument("--expected-audience", default="", help="Optional audience required by the selected acceptance profile") verify.add_argument("--max-age-seconds", type=int, default=None, help="Optional profile freshness window") verify.add_argument("--legacy", action="store_true", help="Use explicit pre-0.7 legacy verifier") verify.add_argument("--legacy-format", default="json-sorted-v1", choices=["json-sorted-v1", "hf-space-v06"], help="Historical decoder format; used only with --legacy") diff --git a/pyproject.toml b/pyproject.toml index 00900ef..9a2a1f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jep-cli" -version = "0.7.0" +version = "0.7.1" description = "Command-line client for JEP Core 0.7 with explicit legacy compatibility" readme = "README.md" license = { text = "MIT" } diff --git a/scripts/check_release.py b/scripts/check_release.py new file mode 100644 index 0000000..b62fec7 --- /dev/null +++ b/scripts/check_release.py @@ -0,0 +1,29 @@ +"""Reject release tags whose built Python metadata disagrees with VERSION.""" +from email.parser import BytesParser +from pathlib import Path +import re +import tarfile +import tomllib +import zipfile + +version = Path("VERSION").read_text().strip() +project = tomllib.loads(Path("pyproject.toml").read_text())["project"] +assert re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:a[0-9]+)?", version), version +assert project["version"] == version, "VERSION differs from pyproject.toml" +def check(raw): + metadata = BytesParser().parsebytes(raw) + assert metadata["Version"] == version, "Built package has a different version" + normalize = lambda text: re.sub(r"[-_.]+", "-", text).lower() + assert normalize(metadata["Name"]) == normalize(project["name"]), "Wrong distribution name" + +wheels, sources = list(Path("dist").glob("*.whl")), list(Path("dist").glob("*.tar.gz")) +assert len(wheels) == len(sources) == 1, "Expected one wheel and one source distribution" +with zipfile.ZipFile(wheels[0]) as archive: + names = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")] + assert len(names) == 1 + check(archive.read(names[0])) +with tarfile.open(sources[0]) as archive: + names = [name for name in archive.getnames() if name.count("/") == 1 and name.endswith("/PKG-INFO")] + assert len(names) == 1 + check(archive.extractfile(names[0]).read()) +print(f"Release metadata verified: {project['name']} {version}") diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index ecdfa95..4e03b89 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -1,16 +1,40 @@ -import json from jep_cli import main as module -def test_acceptance_requires_expected_audience(capsys): - assert module.main(["verify", '{"jep":"1"}', "--mode", "acceptance"]) == 1 - assert "expected-audience" in capsys.readouterr().err +def test_acceptance_does_not_require_core_audience(monkeypatch, capsys): + class FakeClient: + def verify_event(self, payload): + assert payload["mode"] == "acceptance" + assert "expected_audience" not in payload + return { + "status": "valid", + "mode": "acceptance", + "profile": "jep-core-0.7", + "checks": {"syntax": "pass", "cryptographic": "pass", "event_identity": "pass"}, + "acceptance": {"outcome": "accepted", "effect_applied": True}, + } + monkeypatch.setattr(module, "build_client", lambda args: FakeClient()) + code = module.main([ + "verify", + '{"jep":"1","id":"urn:uuid:x","verb":"J","who":"a","when":1,"what":{"claim":"x"},"sig":"s"}', + "--mode", "acceptance", + ]) + assert code == 0 + assert '"status": "valid"' in capsys.readouterr().out -def test_verification_failure_returns_nonzero(monkeypatch): - class Client: - def verify_event(self, payload): - assert payload["expected_audience"] == "receiver" - return {"valid": False} - monkeypatch.setattr(module, "build_client", lambda args: Client()) - assert module.main(["verify", '{}', "--mode", "acceptance", "--expected-audience", "receiver"]) == 2 + +def test_failure_and_indeterminate_return_nonzero(monkeypatch): + for status in ("invalid", "indeterminate"): + class Client: + def verify_event(self, payload): + assert payload["expected_audience"] == "receiver" + return {"status": status} + monkeypatch.setattr(module, "build_client", lambda args: Client()) + assert module.main(["verify", '{}', "--mode", "acceptance", "--expected-audience", "receiver"]) == 2 + + +def test_large_inline_event_is_not_treated_as_a_filename(): + import json + event = {"what": {"claim": "x" * 4096}} + assert module.parse_json_value(json.dumps(event)) == event