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
10 changes: 4 additions & 6 deletions .github/workflows/mirror-oss.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,9 @@ jobs:
--endpoint "${OSS_ENDPOINT}" \
--output-format json > bucket-versioning.json
python - <<'PY'
import json
from pathlib import Path
from scripts.ossutil_json import load_ossutil_json

value = json.loads(Path("bucket-versioning.json").read_text(encoding="utf-8"))
value = load_ossutil_json("bucket-versioning.json")

def statuses(node):
if isinstance(node, dict):
Expand Down Expand Up @@ -187,12 +186,11 @@ jobs:
--endpoint "${OSS_ENDPOINT}" \
--output-format json > "${metadata}"
METADATA_PATH="${metadata}" python - <<'PY'
import json
import os
import re
from pathlib import Path
from scripts.ossutil_json import load_ossutil_json

value = json.loads(Path(os.environ["METADATA_PATH"]).read_text(encoding="utf-8"))
value = load_ossutil_json(os.environ["METADATA_PATH"])

def version_ids(node):
if isinstance(node, dict):
Expand Down
34 changes: 34 additions & 0 deletions scripts/ossutil_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Parse the JSON document emitted by ossutil API commands.

ossutil 2.x appends a human-readable elapsed-time line to otherwise valid JSON
output. Keep accepting that documented CLI decoration while rejecting any
other trailing bytes so API responses remain fail-closed.
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

_ELAPSED_SUFFIX = re.compile(r"\d+(?:\.\d+)?\(s\) elapsed")


def load_ossutil_json(path: str | Path) -> Any:
"""Load one JSON document from an ossutil output file.

The parser accepts leading whitespace and ossutil's trailing elapsed-time
line, but rejects any other trailing output or malformed JSON.
"""

raw = Path(path).read_text(encoding="utf-8")
document = raw.lstrip("\ufeff \t\r\n")
if not document.startswith(("{", "[")):
raise ValueError("ossutil output does not start with a JSON object or array")
decoder = json.JSONDecoder()
value, end = decoder.raw_decode(document)
trailing = document[end:].strip()
if trailing and not _ELAPSED_SUFFIX.fullmatch(trailing):
raise ValueError("unexpected output after ossutil JSON document")
return value
33 changes: 33 additions & 0 deletions tests/test_ossutil_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

import json

import pytest

from scripts.ossutil_json import load_ossutil_json


def test_load_ossutil_json_accepts_elapsed_suffix(tmp_path) -> None:
output = tmp_path / "response.json"
output.write_text(
json.dumps({"Status": "Enabled"}, indent=2) + "\n\n0.012345(s) elapsed\n",
encoding="utf-8",
)

assert load_ossutil_json(output) == {"Status": "Enabled"}


def test_load_ossutil_json_rejects_unexpected_trailing_output(tmp_path) -> None:
output = tmp_path / "response.json"
output.write_text('{"Status": "Enabled"}\nwarning\n', encoding="utf-8")

with pytest.raises(ValueError, match="unexpected output"):
load_ossutil_json(output)


def test_load_ossutil_json_rejects_missing_document(tmp_path) -> None:
output = tmp_path / "response.json"
output.write_text("0.012345(s) elapsed\n", encoding="utf-8")

with pytest.raises(ValueError, match="does not start"):
load_ossutil_json(output)
2 changes: 2 additions & 0 deletions tests/test_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def test_oss_workflow_has_no_moving_alias_and_uses_reviewed_shared_bucket() -> N
assert "expected one non-null OSS Version ID" in workflow
assert "oss-version-ids.json" in workflow
assert 'active != {"enabled"}' in workflow
assert "load_ossutil_json" in workflow
assert "scripts.ossutil_json" in workflow


def test_all_external_actions_are_pinned_to_full_commit_sha() -> None:
Expand Down
Loading