From dfcf8f9a38257bfaa4417646427c885bce84b118 Mon Sep 17 00:00:00 2001 From: stacknil Date: Sun, 5 Jul 2026 09:59:20 +0800 Subject: [PATCH] chore(sbom): finalize v1.0.0 release --- .github/workflows/sbom-diff-and-risk-ci.yml | 7 ++ README.md | 14 ++- scripts/validate-reviewer-routes.py | 12 ++- tools/sbom-diff-and-risk/README.md | 7 +- .../RELEASE_NOTES_v1.0.0.md | 24 +++-- .../docs/github-actions-consumer-example.md | 4 +- .../docs/release-provenance.md | 30 +++--- .../sbom-diff-and-risk/docs/reviewer-brief.md | 5 +- .../docs/reviewer-evidence-pack.md | 22 ++--- .../sbom-diff-and-risk/docs/reviewer-path.md | 10 +- .../scripts/normalize_sdist.py | 98 +++++++++++++++++++ .../tests/test_release_workflow.py | 54 ++++++++++ .../tests/test_reproducible_sdist.py | 57 +++++++++++ 13 files changed, 284 insertions(+), 60 deletions(-) create mode 100644 tools/sbom-diff-and-risk/scripts/normalize_sdist.py create mode 100644 tools/sbom-diff-and-risk/tests/test_reproducible_sdist.py diff --git a/.github/workflows/sbom-diff-and-risk-ci.yml b/.github/workflows/sbom-diff-and-risk-ci.yml index 6c7df1f..9d4dc98 100644 --- a/.github/workflows/sbom-diff-and-risk-ci.yml +++ b/.github/workflows/sbom-diff-and-risk-ci.yml @@ -92,9 +92,16 @@ jobs: - name: Install build tooling run: python -m pip install build + - name: Set reproducible build epoch + shell: bash + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "${GITHUB_ENV}" + - name: Build distributable artifacts run: python -m build + - name: Normalize source distribution timestamps + run: python scripts/normalize_sdist.py --epoch "${SOURCE_DATE_EPOCH}" dist/*.tar.gz + - name: Generate SHA256 checksum manifest shell: bash run: | diff --git a/README.md b/README.md index 39be770..3054105 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,7 @@ Project: [`sbom-diff-and-risk`](tools/sbom-diff-and-risk/README.md) Status: -Main is the `v1.0.0` final candidate. The currently published release remains -`v1.0-rc.1` until the final tag is explicitly approved and created. +Stable GitHub release: `v1.0.0`. Production PyPI publishing remains deferred. What to review: Deterministic SBOM/dependency diffing, JSON/Markdown/SARIF output, local policy @@ -99,7 +98,7 @@ Useful entry points: - [Reviewer path](tools/sbom-diff-and-risk/docs/reviewer-path.md) - [Reviewer brief](tools/sbom-diff-and-risk/docs/reviewer-brief.md) - [Reviewer evidence pack](tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md) -- [v1.0.0 release notes draft][release-notes-v100] +- [v1.0.0 release notes][release-notes-v100] - [v1.0-rc.1 release notes][release-notes-v10rc1] - [Examples](tools/sbom-diff-and-risk/examples/) @@ -226,11 +225,10 @@ the review question: ## Status -- Current published flagship release candidate: `sbom-diff-and-risk` `v1.0-rc.1` -- GitHub Release assets: available for `v1.0-rc.1` -- GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate. -- Next release policy: GitHub `v1.0.0` final, with production PyPI still deferred -- Main package metadata: `1.0.0` final candidate +- Current stable flagship release: `sbom-diff-and-risk` `v1.0.0` +- GitHub Release assets: available for `v1.0.0` +- GitHub Latest: `v1.0.0` +- Package metadata: `1.0.0` - TestPyPI Trusted Publishing dry-run: completed - Production PyPI publishing: intentionally deferred diff --git a/scripts/validate-reviewer-routes.py b/scripts/validate-reviewer-routes.py index fe72142..22dd29f 100644 --- a/scripts/validate-reviewer-routes.py +++ b/scripts/validate-reviewer-routes.py @@ -176,7 +176,8 @@ "not a package safety verdict engine", "not part of the `sbom-diff-and-risk` release surface", "why the scientific-computing background helps", - "GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate.", + "Current stable flagship release: `sbom-diff-and-risk` `v1.0.0`", + "GitHub Latest: `v1.0.0`", "Production PyPI publishing: intentionally deferred", ), Path("docs/reviewer-brief.md"): ( @@ -266,9 +267,9 @@ "any malware verdict", ), Path("tools/sbom-diff-and-risk/docs/release-provenance.md"): ( - "publish `v1.0.0` as the stable GitHub Release contract", - "production PyPI publishing deferred", - "marks a final tag such as `v1.0.0` as GitHub Latest", + "`v1.0.0` is the stable GitHub Release contract and is marked GitHub Latest", + "Production PyPI publishing remains deferred as a separate contract", + "marks final tags such as `v1.0.0` as GitHub Latest", "workflow artifact attestation", ), Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): ( @@ -287,7 +288,8 @@ "runs the tool, uploads `policy.json`, and fails or passes from the policy exit code", "not current PyPI package truth", "not current repository reputation", - "GitHub Latest may still show `v0.9.0` because `v1.0-rc.1` is a release candidate.", + "`v1.0.0` is the stable GitHub release and GitHub Latest", + "`v1.0-rc.1` remains historical prerelease evidence", "It does not decide whether a dependency is safe.", ), Path("projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md"): ( diff --git a/tools/sbom-diff-and-risk/README.md b/tools/sbom-diff-and-risk/README.md index 10d5983..ecf9bfb 100644 --- a/tools/sbom-diff-and-risk/README.md +++ b/tools/sbom-diff-and-risk/README.md @@ -1,9 +1,8 @@ # sbom-diff-and-risk -Main is the `v1.0.0` Policy Evidence final candidate. The currently published -release remains `v1.0-rc.1` until the final tag is explicitly approved and -created. The selected release policy is a stable GitHub `v1.0.0` release while -production PyPI publishing remains intentionally deferred. +`v1.0.0` is the stable Policy Evidence GitHub release. GitHub Release assets +are the supported distribution surface for this version, while production +PyPI publishing remains intentionally deferred. `sbom-diff-and-risk` is a local, deterministic CLI for comparing two SBOMs or dependency manifests and producing JSON plus Markdown reports. diff --git a/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0.0.md b/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0.0.md index a9cce0b..418be85 100644 --- a/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0.0.md +++ b/tools/sbom-diff-and-risk/RELEASE_NOTES_v1.0.0.md @@ -31,22 +31,34 @@ A stable, bounded release surface for dependency diff and local policy evidence. - The TestPyPI Trusted Publishing dry-run remains evidence of the test publishing path only. -## Compatibility and boundaries +## v1.0-rc.1 to v1.0.0 compatibility + +- The CLI commands and flags are unchanged from `v1.0-rc.1`. +- The package version moves from PEP 440 `1.0rc1` to `1.0.0`. +- JSON and Markdown report structure remains compatible, except that the rc + evidence label `enrichment_recorded` is replaced by the more specific + `scorecard_recorded` value. Consumers matching the old string must update. +- SARIF remains a conservative subset. The final release additively records + `policy_matched`, `policy_level`, and `policy_rule_id` on direct mapped + findings, and emits `sdr.new_package` only when `new_package` matched policy. +- Production PyPI publishing remains deferred; moving from rc to final changes + the GitHub Release contract, not the PyPI publishing contract. + +## Boundaries - Default analysis remains local-file based and deterministic. - No default network enrichment was added. - No CVE lookup, advisory resolution, malware scanning, or package safety verdict was added. - Policy warnings and failures remain bounded local policy decisions. -- `enrichment_recorded` was an rc label and is replaced by the more specific - `scorecard_recorded` label in the final contract. ## Release evidence The tag-gated GitHub Actions workflow builds the wheel and source distribution, -generates a SHA256 checksum manifest, records workflow artifact attestations, -and publishes the same built files as GitHub Release assets. Final tags are -explicitly marked as GitHub Latest; rc tags remain prereleases. +normalizes build timestamps from the tagged commit time, generates a SHA256 +checksum manifest, records workflow artifact attestations, and publishes the +same built files as GitHub Release assets. Final tags are explicitly marked as +GitHub Latest; rc tags remain prereleases. Expected assets: diff --git a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md index a3cd432..9845f77 100644 --- a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md +++ b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md @@ -12,9 +12,7 @@ Production PyPI publishing is intentionally deferred, so consumers should not install `sbom-diff-and-risk` from production PyPI. Use a GitHub Release asset or a local checkout instead. -The checked-in workflow pins the selected stable contract, `v1.0.0`. Before -that final release is published, use the currently available `v1.0-rc.1` -release asset instead. +The checked-in workflow pins the stable GitHub Release contract, `v1.0.0`. ## Minimal policy workflow diff --git a/tools/sbom-diff-and-risk/docs/release-provenance.md b/tools/sbom-diff-and-risk/docs/release-provenance.md index 475c09a..7ac17ed 100644 --- a/tools/sbom-diff-and-risk/docs/release-provenance.md +++ b/tools/sbom-diff-and-risk/docs/release-provenance.md @@ -11,30 +11,36 @@ This page is only about the `sbom-diff-and-risk` tool's own GitHub Releases. If ## v1.0 release policy -Chosen policy: publish `v1.0.0` as the stable GitHub Release contract while -keeping production PyPI publishing deferred. +`v1.0.0` is the stable GitHub Release contract and is marked GitHub Latest. +Production PyPI publishing remains deferred as a separate contract. For `v1.0.0`, stability applies to the CLI, report schemas, policy decision examples, evidence-confidence labels, reviewer case, and GitHub Release asset -verification path. It does not claim production PyPI availability. Until the -final tag is explicitly approved and created, `v1.0-rc.1` remains the current -published release candidate. +verification path. It does not claim production PyPI availability. The tag workflow marks rc tags as prereleases and not Latest. It explicitly -marks a final tag such as `v1.0.0` as GitHub Latest. +marks final tags such as `v1.0.0` as GitHub Latest. Release assets produced by the updated workflow also include a deterministic SHA256 checksum manifest named `sbom-diff-and-risk-SHA256SUMS.txt`. The manifest is written with filenames sorted in a stable order. It is not a separate provenance system; it is a local byte-integrity check that helps reviewers confirm downloaded wheel and source distribution files match the hashes published with the same GitHub Release. +For repeat builds of the same commit, the workflow sets `SOURCE_DATE_EPOCH` to +the tagged commit timestamp. It also normalizes gzip and tar timestamps in the +source distribution before checksums and attestations are generated. This +removes build-clock variance from the wheel and source distribution while +preserving package contents. + ## What the release workflow now does For version tags matching `v*`, the `sbom-diff-and-risk-ci` workflow: -1. builds the wheel and source distribution in `build-and-attest` -2. generates `dist/sbom-diff-and-risk-SHA256SUMS.txt` with SHA256 hashes for the built `.whl` and `.tar.gz` -3. uploads the distributions and checksum manifest as the workflow artifact `sbom-diff-and-risk-dist` -4. generates a workflow artifact attestation for the built distribution files -5. downloads that same workflow artifact in `publish-release-assets` -6. publishes those exact `.whl` and `.tar.gz` files plus the checksum manifest as GitHub Release assets for the matching tag +1. sets `SOURCE_DATE_EPOCH` from the tagged commit timestamp +2. builds the wheel and source distribution in `build-and-attest` +3. normalizes source-distribution gzip and tar timestamps +4. generates `dist/sbom-diff-and-risk-SHA256SUMS.txt` with SHA256 hashes for the built `.whl` and `.tar.gz` +5. uploads the distributions and checksum manifest as the workflow artifact `sbom-diff-and-risk-dist` +6. generates a workflow artifact attestation for the built distribution files +7. downloads that same workflow artifact in `publish-release-assets` +8. publishes those exact `.whl` and `.tar.gz` files plus the checksum manifest as GitHub Release assets for the matching tag This intentionally reuses the same workflow-built bytes for both the workflow artifact and the release asset surfaces. It does not add PyPI publishing or a separate rebuild-only release pipeline. diff --git a/tools/sbom-diff-and-risk/docs/reviewer-brief.md b/tools/sbom-diff-and-risk/docs/reviewer-brief.md index 444117a..78b1faf 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-brief.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-brief.md @@ -4,10 +4,7 @@ `sbom-diff-and-risk` is a local CLI for comparing two SBOMs or dependency manifests and producing deterministic review artifacts: JSON, Markdown, and SARIF. It is built for conservative supply-chain review, not for vulnerability scanning or package reputation scoring. -Current released version: `v1.0-rc.1` release candidate. - -Main is the `v1.0.0` final candidate. The chosen release policy is a stable -GitHub final release while production PyPI remains deferred. +Current stable GitHub release: `v1.0.0`. Production PyPI remains deferred. For the shortest ordered review route, use [reviewer-path.md](reviewer-path.md). diff --git a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md index 3ba685b..e89b011 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md @@ -14,7 +14,7 @@ workflow templates. `sbom-diff-and-risk` is a local-first deterministic CLI for comparing SBOMs and dependency manifests. It is designed to produce stable review evidence for dependency changes. -Current released version: `v1.0-rc.1` release candidate. +Current stable GitHub release: `v1.0.0`. Core identity: @@ -149,22 +149,22 @@ For CI dashboard, job-summary, and local-threshold examples that consume ## Release Verification Path -Start with the GitHub Release for the version under review. For `v1.0-rc.1`, +Start with the GitHub Release for the version under review. For `v1.0.0`, inspect the release and assets: ```powershell -gh release view v1.0-rc.1 ` +gh release view v1.0.0 ` --repo stacknil/scientific-computing-toolkit ` --json tagName,name,isDraft,isPrerelease,assets,url ``` Expected release assets: -- `sbom_diff_and_risk-1.0rc1-py3-none-any.whl` -- `sbom_diff_and_risk-1.0rc1.tar.gz` +- `sbom_diff_and_risk-1.0.0-py3-none-any.whl` +- `sbom_diff_and_risk-1.0.0.tar.gz` - `sbom-diff-and-risk-SHA256SUMS.txt` -For this rc, `isPrerelease` should be `true`. +For this stable release, `isPrerelease` should be `false`. The checksum manifest checks local downloaded distribution bytes before or alongside provenance verification: @@ -194,13 +194,13 @@ For workflow-built artifacts downloaded from a trusted workflow run, verify artifact attestations with the signer workflow: ```powershell -gh attestation verify path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl ` +gh attestation verify path/to/sbom_diff_and_risk-1.0.0-py3-none-any.whl ` --repo stacknil/scientific-computing-toolkit ` --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml ``` ```powershell -gh attestation verify path/to/sbom_diff_and_risk-1.0rc1.tar.gz ` +gh attestation verify path/to/sbom_diff_and_risk-1.0.0.tar.gz ` --repo stacknil/scientific-computing-toolkit ` --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml ``` @@ -210,15 +210,15 @@ releases. Use them only when the repository release is immutable and GitHub has generated release attestations: ```powershell -gh release view v1.0-rc.1 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url +gh release view v1.0.0 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url ``` If `isImmutable` is true, release verification can check the release record and downloaded release assets: ```powershell -gh release verify v1.0-rc.1 --repo stacknil/scientific-computing-toolkit -gh release verify-asset v1.0-rc.1 path/to/sbom_diff_and_risk-1.0rc1-py3-none-any.whl --repo stacknil/scientific-computing-toolkit +gh release verify v1.0.0 --repo stacknil/scientific-computing-toolkit +gh release verify-asset v1.0.0 path/to/sbom_diff_and_risk-1.0.0-py3-none-any.whl --repo stacknil/scientific-computing-toolkit ``` If `isImmutable` is false, use the workflow artifact attestation path as the primary artifact verification story. diff --git a/tools/sbom-diff-and-risk/docs/reviewer-path.md b/tools/sbom-diff-and-risk/docs/reviewer-path.md index 48874cb..0b8345d 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-path.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-path.md @@ -199,13 +199,9 @@ Use this section only when the review question is about the released `sbom-diff-and-risk` tool artifacts. It is not the path for judging third-party dependency safety. -Release status note: GitHub Latest may still show `v0.9.0` because -`v1.0-rc.1` is a release candidate. Reviewers checking the current release -candidate should open the explicit `v1.0-rc.1` release page. - -Main is prepared as the `v1.0.0` final candidate. The selected policy is a -stable GitHub final release with production PyPI still deferred; this is not a -claim that the final tag already exists. +Release status: `v1.0.0` is the stable GitHub release and GitHub Latest. +`v1.0-rc.1` remains historical prerelease evidence. Production PyPI publishing +is still deferred as a separate contract. | Evidence surface | Use when | Read | | --- | --- | --- | diff --git a/tools/sbom-diff-and-risk/scripts/normalize_sdist.py b/tools/sbom-diff-and-risk/scripts/normalize_sdist.py new file mode 100644 index 0000000..ff7745f --- /dev/null +++ b/tools/sbom-diff-and-risk/scripts/normalize_sdist.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import argparse +import gzip +import os +import tarfile +import tempfile +from collections.abc import Sequence +from pathlib import Path + + +_VOLATILE_PAX_HEADERS = {"atime", "ctime", "mtime"} + + +def normalize_sdist(path: Path, *, epoch: int) -> None: + if epoch < 0: + raise ValueError("epoch must be non-negative") + + resolved = path.resolve() + if not resolved.is_file(): + raise FileNotFoundError(f"source distribution not found: {path}") + if not resolved.name.endswith(".tar.gz"): + raise ValueError(f"source distribution must end with .tar.gz: {path}") + + temporary_path: Path | None = None + try: + with tempfile.NamedTemporaryFile( + dir=resolved.parent, + prefix=f".{resolved.name}.", + suffix=".tmp", + delete=False, + ) as temporary: + temporary_path = Path(temporary.name) + with tarfile.open(resolved, mode="r:gz") as source: + members = sorted(source.getmembers(), key=lambda member: member.name) + with gzip.GzipFile( + filename="", + mode="wb", + fileobj=temporary, + mtime=epoch, + ) as compressed: + with tarfile.open( + fileobj=compressed, + mode="w", + format=tarfile.PAX_FORMAT, + ) as destination: + for member in members: + content = source.extractfile(member) if member.isfile() else None + try: + member.mtime = epoch + member.pax_headers = { + key: value + for key, value in member.pax_headers.items() + if key not in _VOLATILE_PAX_HEADERS + } + destination.addfile(member, content) + finally: + if content is not None: + content.close() + + temporary_path.replace(resolved) + except Exception: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) + raise + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Normalize gzip and tar timestamps in Python source distributions.", + ) + parser.add_argument("paths", nargs="+", type=Path, help="Source distribution .tar.gz files.") + parser.add_argument( + "--epoch", + type=int, + default=None, + help="Timestamp to record. Defaults to SOURCE_DATE_EPOCH.", + ) + args = parser.parse_args(argv) + + epoch = args.epoch + if epoch is None: + value = os.environ.get("SOURCE_DATE_EPOCH") + if value is None: + parser.error("--epoch or SOURCE_DATE_EPOCH is required") + try: + epoch = int(value) + except ValueError: + parser.error("SOURCE_DATE_EPOCH must be an integer") + + for path in args.paths: + normalize_sdist(path, epoch=epoch) + print(f"normalized reproducible sdist: {path.name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/sbom-diff-and-risk/tests/test_release_workflow.py b/tools/sbom-diff-and-risk/tests/test_release_workflow.py index c49d1ee..f0acc54 100644 --- a/tools/sbom-diff-and-risk/tests/test_release_workflow.py +++ b/tools/sbom-diff-and-risk/tests/test_release_workflow.py @@ -1,6 +1,8 @@ from __future__ import annotations +import json from pathlib import Path +import tomllib import yaml @@ -9,6 +11,11 @@ WORKFLOW = REPO_ROOT / ".github" / "workflows" / "sbom-diff-and-risk-ci.yml" RELEASE_NOTES = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "RELEASE_NOTES_v1.0-rc.1.md" FINAL_RELEASE_NOTES = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "RELEASE_NOTES_v1.0.0.md" +PYPROJECT = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "pyproject.toml" +PACKAGE_INIT = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "src" / "sbom_diff_risk" / "__init__.py" +ROOT_README = REPO_ROOT / "README.md" +TOOL_README = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "README.md" +EXAMPLES = REPO_ROOT / "tools" / "sbom-diff-and-risk" / "examples" def test_release_workflow_marks_rc_tags_as_prereleases() -> None: @@ -34,3 +41,50 @@ def test_release_workflow_marks_final_tags_as_latest() -> None: assert "else" in publish_script assert "create_args+=(--latest)" in publish_script assert "edit_args+=(--latest)" in publish_script + + +def test_release_workflow_normalizes_build_timestamps_before_checksums() -> None: + text = WORKFLOW.read_text(encoding="utf-8") + + epoch_index = text.index("SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)") + build_index = text.index("python -m build", epoch_index) + normalize_index = text.index("python scripts/normalize_sdist.py", build_index) + checksum_index = text.index("Generate SHA256 checksum manifest", normalize_index) + + assert epoch_index < build_index < normalize_index < checksum_index + + +def test_v1_release_metadata_and_public_status_are_aligned() -> None: + project = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))["project"] + package_init = PACKAGE_INIT.read_text(encoding="utf-8") + root_readme = ROOT_README.read_text(encoding="utf-8") + tool_readme = TOOL_README.read_text(encoding="utf-8") + + assert project["version"] == "1.0.0" + assert '__version__ = "1.0.0"' in package_init + assert "Current stable flagship release: `sbom-diff-and-risk` `v1.0.0`" in root_readme + assert "`v1.0.0` is the stable Policy Evidence GitHub release" in tool_readme + assert "final candidate" not in root_readme + assert "final candidate" not in tool_readme + + +def test_v1_release_notes_capture_rc_compatibility_boundary() -> None: + text = FINAL_RELEASE_NOTES.read_text(encoding="utf-8") + + assert "v1.0-rc.1 to v1.0.0 compatibility" in text + assert "CLI commands and flags are unchanged" in text + assert "enrichment_recorded" in text + assert "scorecard_recorded" in text + assert "sdr.new_package" in text + assert "Production PyPI publishing remains deferred" in text + + +def test_v1_sarif_golden_fixtures_use_final_version() -> None: + fixtures = sorted(EXAMPLES.glob("sample-*.sarif")) + + assert fixtures + for fixture in fixtures: + payload = json.loads(fixture.read_text(encoding="utf-8")) + driver = payload["runs"][0]["tool"]["driver"] + assert driver["version"] == "1.0.0", fixture.name + assert driver["semanticVersion"] == "1.0.0", fixture.name diff --git a/tools/sbom-diff-and-risk/tests/test_reproducible_sdist.py b/tools/sbom-diff-and-risk/tests/test_reproducible_sdist.py new file mode 100644 index 0000000..e948ecb --- /dev/null +++ b/tools/sbom-diff-and-risk/tests/test_reproducible_sdist.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import gzip +import hashlib +import io +from pathlib import Path +import subprocess +import sys +import tarfile + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +NORMALIZER = PROJECT_ROOT / "scripts" / "normalize_sdist.py" + + +def test_normalize_sdist_makes_time_variant_archives_byte_identical(tmp_path: Path) -> None: + first = tmp_path / "first.tar.gz" + second = tmp_path / "second.tar.gz" + epoch = 1_700_000_000 + _write_sdist(first, timestamp=epoch + 10) + _write_sdist(second, timestamp=epoch + 20) + + for path in (first, second): + subprocess.run( + [sys.executable, str(NORMALIZER), "--epoch", str(epoch), str(path)], + check=True, + cwd=PROJECT_ROOT, + capture_output=True, + text=True, + ) + + assert hashlib.sha256(first.read_bytes()).digest() == hashlib.sha256(second.read_bytes()).digest() + with tarfile.open(first, mode="r:gz") as archive: + members = archive.getmembers() + assert all(member.mtime == epoch for member in members) + content = archive.extractfile("example-1.0.0/example.txt") + assert content is not None + assert content.read() == b"stable content\n" + + +def _write_sdist(path: Path, *, timestamp: int) -> None: + with path.open("wb") as raw: + with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=timestamp) as compressed: + with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive: + directory = tarfile.TarInfo("example-1.0.0") + directory.type = tarfile.DIRTYPE + directory.mode = 0o755 + directory.mtime = timestamp + directory.pax_headers = {"mtime": f"{timestamp}.25"} + archive.addfile(directory) + + content = b"stable content\n" + member = tarfile.TarInfo("example-1.0.0/example.txt") + member.mode = 0o644 + member.mtime = timestamp + member.size = len(content) + archive.addfile(member, fileobj=io.BytesIO(content))