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
6 changes: 2 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 7 additions & 7 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.7.1
2 changes: 1 addition & 1 deletion jep_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""JEP CLI v0.6."""

__version__ = "0.6.0"
__version__ = "0.7.1"
2 changes: 1 addition & 1 deletion jep_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
17 changes: 11 additions & 6 deletions jep_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
29 changes: 29 additions & 0 deletions scripts/check_release.py
Original file line number Diff line number Diff line change
@@ -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}")
46 changes: 35 additions & 11 deletions tests/test_acceptance.py
Original file line number Diff line number Diff line change
@@ -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
Loading