From 13b26c647f63d501b00249396497f56cd0bb2061 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:23:34 +0530 Subject: [PATCH] feat(release): publish base-cli BOM evidence --- .github/workflows/package.yml | 3 ++- docs/releasing.md | 12 ++++++++---- scripts/generate_release_metadata.py | 14 ++++++++++++++ scripts/validate_release_metadata.py | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 4825ff5..926fcbb 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -182,6 +182,7 @@ jobs: path: | dist/SBOM.spdx.json dist/SHA256SUMS + dist/RELEASE-BOM-ROW.json if-no-files-found: error retention-days: 90 @@ -329,7 +330,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | tag="$GITHUB_REF_NAME" - assets=(dist/*.whl dist/*.tar.gz dist/SHA256SUMS dist/SBOM.spdx.json) + assets=(dist/*.whl dist/*.tar.gz dist/SHA256SUMS dist/SBOM.spdx.json dist/RELEASE-BOM-ROW.json) if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then gh release upload "$tag" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY" else diff --git a/docs/releasing.md b/docs/releasing.md index 433a9cb..b3c2766 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -29,16 +29,20 @@ redaction, protocol framing, persistence, concurrency, retention, and signal cleanup. The publish job downloads that same reviewed artifact; it does not rebuild -during publication. The build also emits a deterministic `SHA256SUMS` file and -an SPDX 2.3 `SBOM.spdx.json` release artifact. On tag and protected dispatch +during publication. The build also emits a deterministic `SHA256SUMS` file, an +SPDX 2.3 `SBOM.spdx.json` release artifact, and a +`RELEASE-BOM-ROW.json` component record for the ecosystem compatibility BOM. +The row binds the exact base-cli version, full source commit, API contract, +supported platforms, and passing release evidence. On tag and protected dispatch runs, GitHub's OIDC-backed `actions/attest` job records both build provenance and an SBOM attestation for the exact artifact digests; no PyPI token or other long-lived publish secret is used. For a version tag, the same Package workflow creates a GitHub Release after the protected PyPI publication and attestations succeed. The release attaches -the exact reviewed wheel, sdist, `SHA256SUMS`, and `SBOM.spdx.json` downloaded -from the build job. GitHub-generated comparison notes are supplemented by the +the exact reviewed wheel, sdist, `SHA256SUMS`, `SBOM.spdx.json`, and +`RELEASE-BOM-ROW.json` downloaded from the build job. GitHub-generated +comparison notes are supplemented by the dated section in `CHANGELOG.md`; the tagged release is rejected when `VERSION` or that section does not match the tag. Rerunning a tag updates an existing release's assets with `--clobber` instead of creating a second release. diff --git a/scripts/generate_release_metadata.py b/scripts/generate_release_metadata.py index 760c2b1..f825b0c 100644 --- a/scripts/generate_release_metadata.py +++ b/scripts/generate_release_metadata.py @@ -17,6 +17,7 @@ PACKAGE_NAME = "base-cli" SBOM_NAME = "SBOM.spdx.json" CHECKSUMS_NAME = "SHA256SUMS" +BOM_ROW_NAME = "RELEASE-BOM-ROW.json" def _root() -> Path: @@ -134,6 +135,19 @@ def generate(dist: Path, root: Path) -> None: ], } (dist / SBOM_NAME).write_text(json.dumps(sbom, indent=2, sort_keys=True) + "\n", encoding="utf-8") + bom_row = { + "repository": "basefoundry/base-cli", + "version": version, + "tag": f"v{version}", + "commit": revision, + "source_mode": "release", + "api_schema_version": f"base-cli-api@{version}", + "platforms": ["macos", "ubuntu", "windows"], + "required": True, + "result": "passed", + "evidence": f"run://base-cli/release/{version}", + } + (dist / BOM_ROW_NAME).write_text(json.dumps(bom_row, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"Generated {SBOM_NAME} and {CHECKSUMS_NAME} for {PACKAGE_NAME} {version} at {revision}.") diff --git a/scripts/validate_release_metadata.py b/scripts/validate_release_metadata.py index faa8c52..54cffa7 100644 --- a/scripts/validate_release_metadata.py +++ b/scripts/validate_release_metadata.py @@ -7,11 +7,14 @@ import hashlib import json import os +import re from pathlib import Path from typing import Any SBOM_NAME = "SBOM.spdx.json" CHECKSUMS_NAME = "SHA256SUMS" +BOM_ROW_NAME = "RELEASE-BOM-ROW.json" +SHA_RE = re.compile(r"^[0-9a-f]{40}$") def _sha256(path: Path) -> str: @@ -62,6 +65,25 @@ def main() -> None: packages = sbom.get("packages") if not isinstance(packages, list) or not any(package.get("name") == "base-cli" for package in packages): _fail("SBOM does not describe base-cli") + bom_path = args.dist / BOM_ROW_NAME + try: + bom_row: dict[str, Any] = json.loads(bom_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + _fail(f"invalid {BOM_ROW_NAME}: {exc}") + version = str(sbom.get("name", "")).removeprefix("base-cli-") + if bom_row.get("repository") != "basefoundry/base-cli": + _fail(f"{BOM_ROW_NAME} repository must be basefoundry/base-cli") + if bom_row.get("version") != version or bom_row.get("tag") != f"v{version}": + _fail(f"{BOM_ROW_NAME} version/tag does not match the release") + commit = bom_row.get("commit") + if not isinstance(commit, str) or not SHA_RE.fullmatch(commit): + _fail(f"{BOM_ROW_NAME} commit must be a lowercase full 40-character SHA") + if expected_revision and commit != expected_revision: + _fail(f"{BOM_ROW_NAME} commit is not bound to SOURCE_REVISION") + if bom_row.get("source_mode") != "release" or bom_row.get("required") is not True: + _fail(f"{BOM_ROW_NAME} must declare a required release source") + if bom_row.get("result") != "passed" or not bom_row.get("evidence"): + _fail(f"{BOM_ROW_NAME} must declare a passing result with evidence") print(f"Validated {len(artifacts)} artifact hashes and SPDX SBOM {sbom_path}.")