Skip to content

Commit db511b6

Browse files
authored
[codex] Add example artifact regeneration check (#61)
1 parent d326ecf commit db511b6

5 files changed

Lines changed: 262 additions & 0 deletions

File tree

tools/sbom-diff-and-risk/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ For CI consumption of summary-only output, see
2828
[docs/summary-json-ci-cookbook.md](docs/summary-json-ci-cookbook.md).
2929
For a consumer-facing GitHub Actions example, see
3030
[docs/github-actions-consumer-example.md](docs/github-actions-consumer-example.md).
31+
For regenerating checked-in local example outputs, see
32+
[docs/example-artifact-regeneration.md](docs/example-artifact-regeneration.md).
3133

3234
1. If you want to verify `sbom-diff-and-risk` itself, start with
3335
[docs/verification.md](docs/verification.md).
@@ -316,6 +318,14 @@ The [examples/](examples/) directory includes:
316318
- provenance-aware sample reports at [sample-provenance-report.json](examples/sample-provenance-report.json), [sample-provenance-report.md](examples/sample-provenance-report.md), and [sample-provenance-report.sarif](examples/sample-provenance-report.sarif)
317319
- Scorecard-aware sample reports at [sample-scorecard-report.json](examples/sample-scorecard-report.json), [sample-scorecard-report.md](examples/sample-scorecard-report.md), and [sample-scorecard-report.sarif](examples/sample-scorecard-report.sarif)
318320
- requirements-based sample reports at [sample-requirements-report.json](examples/sample-requirements-report.json) and [sample-requirements-report.md](examples/sample-requirements-report.md)
321+
322+
After changing local example inputs, regenerate checked-in deterministic
323+
examples with:
324+
325+
```bash
326+
python scripts/regenerate-example-artifacts.py
327+
python scripts/regenerate-example-artifacts.py --check
328+
```
319329

320330
## Enforcement Mode
321331

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Example Artifact Regeneration
2+
3+
This page documents how to regenerate the checked-in no-network example
4+
artifacts for `sbom-diff-and-risk`.
5+
6+
Use this when an example input changes, such as
7+
`examples/requirements_before.txt` or `examples/requirements_after.txt`.
8+
The generated sample reports are intentionally committed so reviewers can
9+
compare deterministic output without running enrichment services.
10+
11+
## Regenerate
12+
13+
From `tools/sbom-diff-and-risk`:
14+
15+
```powershell
16+
python scripts/regenerate-example-artifacts.py
17+
```
18+
19+
The script regenerates these local, deterministic artifacts:
20+
21+
- `examples/sample-report.json`
22+
- `examples/sample-summary.json`
23+
- `examples/sample-report.md`
24+
- `examples/sample-policy-warn-report.json`
25+
- `examples/sample-policy-warn-report.md`
26+
- `examples/sample-policy-fail-report.json`
27+
- `examples/sample-policy.json`
28+
- `examples/sample-policy-fail-report.md`
29+
- `examples/sample-requirements-report.json`
30+
- `examples/sample-requirements-report.md`
31+
32+
The strict-policy example intentionally exits with code `1` because it produces
33+
blocking local policy findings. The script treats that as expected while still
34+
capturing the generated reports.
35+
36+
## Check Mode
37+
38+
Use `--check` to verify that generated output matches the checked-in artifacts
39+
without modifying the repository:
40+
41+
```powershell
42+
python scripts/regenerate-example-artifacts.py --check
43+
```
44+
45+
The test suite runs this check mode so stale local JSON, Markdown, summary, or
46+
policy-sidecar examples fail predictably.
47+
48+
## Boundaries
49+
50+
The regeneration script covers no-network JSON, Markdown, summary, and policy
51+
sidecar examples produced through the public CLI.
52+
53+
It does not perform PyPI or Scorecard enrichment, does not call external
54+
services, and does not make dependency safety claims. Provenance-aware,
55+
Scorecard-aware, and SARIF sample artifacts remain covered by their focused
56+
golden tests because those examples include mocked evidence or normalized SARIF
57+
metadata.

tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ No differences means the sample path reproduced the committed example output.
5656
`examples/sample-summary.json` is the summary-only artifact for the same run
5757
and is expected to match `examples/sample-report.json`'s `summary` object.
5858

59+
Maintainers can also verify checked-in no-network JSON, Markdown, summary, and
60+
policy sidecar examples in one pass:
61+
62+
```powershell
63+
python scripts/regenerate-example-artifacts.py --check
64+
```
65+
66+
For the exact regeneration scope, see
67+
[example-artifact-regeneration.md](example-artifact-regeneration.md).
68+
5969
Generate the strict-policy JSON sidecar:
6070

6171
```powershell
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import os
5+
import subprocess
6+
import sys
7+
import tempfile
8+
from dataclasses import dataclass
9+
from pathlib import Path
10+
from typing import Sequence
11+
12+
13+
@dataclass(frozen=True)
14+
class ExampleArtifactSet:
15+
name: str
16+
base_args: tuple[str, ...]
17+
outputs: tuple[tuple[str, str], ...]
18+
expected_exit_codes: tuple[int, ...] = (0,)
19+
20+
21+
ARTIFACT_SETS: tuple[ExampleArtifactSet, ...] = (
22+
ExampleArtifactSet(
23+
name="cyclonedx report, summary, and markdown",
24+
base_args=(
25+
"--before",
26+
"examples/cdx_before.json",
27+
"--after",
28+
"examples/cdx_after.json",
29+
"--format",
30+
"auto",
31+
),
32+
outputs=(
33+
("--out-json", "sample-report.json"),
34+
("--summary-json", "sample-summary.json"),
35+
("--out-md", "sample-report.md"),
36+
),
37+
),
38+
ExampleArtifactSet(
39+
name="warn-only policy report",
40+
base_args=(
41+
"--before",
42+
"examples/cdx_before.json",
43+
"--after",
44+
"examples/cdx_after.json",
45+
"--policy",
46+
"examples/policy-minimal.yml",
47+
),
48+
outputs=(
49+
("--out-json", "sample-policy-warn-report.json"),
50+
("--out-md", "sample-policy-warn-report.md"),
51+
),
52+
),
53+
ExampleArtifactSet(
54+
name="blocking policy report and sidecar",
55+
base_args=(
56+
"--before",
57+
"examples/cdx_before.json",
58+
"--after",
59+
"examples/cdx_after.json",
60+
"--policy",
61+
"examples/policy-strict.yml",
62+
),
63+
outputs=(
64+
("--out-json", "sample-policy-fail-report.json"),
65+
("--policy-json", "sample-policy.json"),
66+
("--out-md", "sample-policy-fail-report.md"),
67+
),
68+
expected_exit_codes=(1,),
69+
),
70+
ExampleArtifactSet(
71+
name="requirements report",
72+
base_args=(
73+
"--before",
74+
"examples/requirements_before.txt",
75+
"--after",
76+
"examples/requirements_after.txt",
77+
"--format",
78+
"auto",
79+
),
80+
outputs=(
81+
("--out-json", "sample-requirements-report.json"),
82+
("--out-md", "sample-requirements-report.md"),
83+
),
84+
),
85+
)
86+
87+
88+
def main(argv: Sequence[str] | None = None) -> int:
89+
parser = argparse.ArgumentParser(
90+
description="Regenerate checked-in no-network example report artifacts.",
91+
)
92+
parser.add_argument(
93+
"--check",
94+
action="store_true",
95+
help="Generate artifacts into a temporary directory and fail if checked-in examples are stale.",
96+
)
97+
args = parser.parse_args(argv)
98+
99+
project_root = Path(__file__).resolve().parents[1]
100+
if args.check:
101+
with tempfile.TemporaryDirectory(prefix="sbom-diff-risk-examples-") as temp_dir:
102+
return _check_artifacts(project_root, Path(temp_dir))
103+
return _write_artifacts(project_root, project_root / "examples")
104+
105+
106+
def _write_artifacts(project_root: Path, output_root: Path) -> int:
107+
for artifact_set in ARTIFACT_SETS:
108+
_run_artifact_set(project_root, output_root, artifact_set)
109+
print(f"generated: {artifact_set.name}")
110+
return 0
111+
112+
113+
def _check_artifacts(project_root: Path, output_root: Path) -> int:
114+
_write_artifacts(project_root, output_root)
115+
116+
examples_dir = project_root / "examples"
117+
stale_files: list[str] = []
118+
for artifact_set in ARTIFACT_SETS:
119+
for _, output_name in artifact_set.outputs:
120+
expected = (examples_dir / output_name).read_text(encoding="utf-8")
121+
generated = (output_root / output_name).read_text(encoding="utf-8")
122+
if generated != expected:
123+
stale_files.append(output_name)
124+
125+
if stale_files:
126+
print("stale example artifacts detected:", file=sys.stderr)
127+
for name in stale_files:
128+
print(f" {name}", file=sys.stderr)
129+
print("run scripts/regenerate-example-artifacts.py and commit the updated files.", file=sys.stderr)
130+
return 1
131+
132+
print("all checked example artifacts are up to date")
133+
return 0
134+
135+
136+
def _run_artifact_set(project_root: Path, output_root: Path, artifact_set: ExampleArtifactSet) -> None:
137+
output_root.mkdir(parents=True, exist_ok=True)
138+
command = [sys.executable, "-m", "sbom_diff_risk.cli", "compare", *artifact_set.base_args]
139+
for flag, output_name in artifact_set.outputs:
140+
command.extend([flag, str(output_root / output_name)])
141+
142+
env = dict(os.environ)
143+
src_path = str(project_root / "src")
144+
env["PYTHONPATH"] = src_path if not env.get("PYTHONPATH") else f"{src_path}{os.pathsep}{env['PYTHONPATH']}"
145+
146+
result = subprocess.run(
147+
command,
148+
cwd=project_root,
149+
text=True,
150+
capture_output=True,
151+
env=env,
152+
)
153+
if result.returncode not in artifact_set.expected_exit_codes:
154+
detail = result.stderr.strip() or result.stdout.strip()
155+
raise RuntimeError(
156+
f"{artifact_set.name} exited with {result.returncode}; "
157+
f"expected {artifact_set.expected_exit_codes}: {detail}"
158+
)
159+
160+
161+
if __name__ == "__main__":
162+
raise SystemExit(main())
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
3+
import subprocess
4+
import sys
5+
from pathlib import Path
6+
7+
8+
def test_regenerate_example_artifacts_check_mode_passes() -> None:
9+
project_root = Path(__file__).resolve().parents[1]
10+
11+
result = subprocess.run(
12+
[
13+
sys.executable,
14+
str(project_root / "scripts" / "regenerate-example-artifacts.py"),
15+
"--check",
16+
],
17+
cwd=project_root,
18+
text=True,
19+
capture_output=True,
20+
)
21+
22+
assert result.returncode == 0, result.stdout + result.stderr
23+
assert "all checked example artifacts are up to date" in result.stdout

0 commit comments

Comments
 (0)