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
3 changes: 2 additions & 1 deletion .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions scripts/generate_release_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}.")


Expand Down
22 changes: 22 additions & 0 deletions scripts/validate_release_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}.")


Expand Down
Loading