Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1bccd70
fix(release): use subset check for SBOM/lock comparison
Coding-Dev-Tools Aug 15, 2026
ad0b4e1
fix(release): reject empty SBOM package set before subset check
Coding-Dev-Tools Aug 15, 2026
c129329
fix(release): require engraphis root component in SBOM package set
Coding-Dev-Tools Aug 15, 2026
1672fed
fix(release): validate SBOM metadata.component name and version directly
Coding-Dev-Tools Aug 15, 2026
d45f8c9
fix(release): reject SBOMs containing only the root component
Coding-Dev-Tools Aug 15, 2026
e39df2e
fix(release): validate declared dependency closure and restore --pypr…
Coding-Dev-Tools Aug 15, 2026
9f8f6fb
fix(release): add declared dependency closure test and fix maxsplit d…
Coding-Dev-Tools Aug 15, 2026
483f7fa
fix(release): validate PURL name/version and include optional deps in…
Coding-Dev-Tools Aug 15, 2026
85f85fd
fix(release): limit declared dependency closure to core deps only
Coding-Dev-Tools Aug 15, 2026
7b681ff
fix(release): validate dependency PURL consistency and filter known C…
Coding-Dev-Tools Aug 15, 2026
96ff9b6
fix(release): reject malformed PURLs, narrow CSP filter, restore gala…
Coding-Dev-Tools Aug 15, 2026
358053b
fix(e2e): identify force-graph CSP console errors via message.location()
Coding-Dev-Tools Aug 15, 2026
38e1a65
fix(release): reject SBOM components without valid PyPI PURLs
Coding-Dev-Tools Aug 15, 2026
f5c7aa7
fix(release): reject conflicting package versions in environment lock
Coding-Dev-Tools Aug 15, 2026
71079ff
fix(e2e): restore orbital radius bounds and assert envelope system count
Coding-Dev-Tools Aug 15, 2026
2c10864
fix(e2e): wait for graph count update after filter clear
Coding-Dev-Tools Aug 15, 2026
ce7d070
fix(release): enforce declared dependency version constraints
Coding-Dev-Tools Aug 15, 2026
7d5e003
fix(release): harden evidence and drag regression
Coding-Dev-Tools Aug 17, 2026
0c1ee69
test(graph): tighten envelope visibility contract
Coding-Dev-Tools Aug 17, 2026
273a2f1
Merge remote-tracking branch 'origin/main' into fix/release-sbom-subs…
Coding-Dev-Tools Aug 18, 2026
2e0a677
fix(release): require selected extras in SBOM closure; restore galaxy…
Coding-Dev-Tools Aug 21, 2026
88bc16e
test(release): use double-quoted TOML strings in extras fixtures
Coding-Dev-Tools Aug 21, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
python -m pip list --format=freeze \
| LC_ALL=C sort -f > build-environment-evidence/environment.lock
cyclonedx-py environment --output-reproducible --of JSON \
--pyproject pyproject.toml \
-o build-environment-evidence/engraphis-${GITHUB_REF_NAME#v}.cdx.json

- name: Build source and universal wheel distributions
Expand Down
224 changes: 212 additions & 12 deletions scripts/release_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
except ImportError: # pragma: no cover - supported Python 3.9/3.10
tomllib = None

try: # Prefer the installed packaging module when available.
from packaging.markers import InvalidMarker, Marker
from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import InvalidVersion, Version
except ImportError: # pragma: no cover - fallback for environments without top-level packaging
from pip._vendor.packaging.markers import InvalidMarker, Marker
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.version import InvalidVersion, Version


FORMAT = "engraphis-release-evidence/3"
PACKAGE = "engraphis"
Expand All @@ -30,12 +39,27 @@
_PACKAGE_LOCK_LINE = re.compile(r"([A-Za-z0-9][A-Za-z0-9_.-]*)==([^\s]+)\Z")
_IMAGE_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
_BUILDER_IMAGE = "github-hosted:ubuntu-latest/python-3.11"
# Extras installed by the release workflow (`.github/workflows/release.yml` runs
# `pip install ... ".[all,test]"` before capturing the SBOM), so the captured
# closure must include every marker-applicable requirement they declare.
_RELEASE_EXTRAS = ("all", "test")
_BUILDER_TOOLCHAIN = {
"build": "1.5.0",
"pip": "26.2",
"setuptools": "83.0.0",
"wheel": "0.47.0",
}


def _purl_matches(purl: str, name: str, version: str) -> bool:
"""Return True when a pkg:pypi PURL names *name* at *version*."""
if not purl.startswith("pkg:pypi/"):
return False
remainder = purl[len("pkg:pypi/"):].split("?", 1)[0]
if "@" not in remainder:
return False
purl_name, purl_version = remainder.split("@", 1)
return _canonical_package_name(purl_name) == name and purl_version == version
_GRYPE_VERSION = "0.110.0"
_SECRET_NAME = re.compile(
r"(?:secret|token|password|credential|api[-_]?key|private[-_]?key)", re.IGNORECASE
Expand Down Expand Up @@ -121,6 +145,8 @@ def project_version(root: Path) -> str:
return version




def git_commit(root: Path) -> str:
try:
commit = subprocess.check_output(
Expand Down Expand Up @@ -218,6 +244,113 @@ def _canonical_package_name(value: str) -> str:
return re.sub(r"[-_.]+", "-", value).lower()


def _parse_requirement(requirement: str) -> tuple[str, str | None]:
"""Extract (name, specifier) from a PEP 508 requirement string."""
requirement = requirement.strip()
if not requirement:
return ("", None)
match = re.match(
r'^([A-Za-z0-9][A-Za-z0-9._-]*)'
r'(?:\[.*?\])?'
r'\s*'
r'((?:[<>=!~]=?[^;,\s]+(?:\s*,\s*[<>=!~]=?[^;,\s]+)*)?)',
requirement,
)
if not match:
name = re.split(r"[\s;<(>=!~\[]", requirement, maxsplit=1)[0]
return (name, None)
name = match.group(1)
specifier = match.group(2) if match.group(2) else None
return (name, specifier)


def _version_satisfies(version: str, specifier: str) -> bool:
"""Check if *version* satisfies a PEP 440 specifier."""
if not specifier:
return True
try:
candidate = Version(version)
spec = SpecifierSet(specifier)
except (InvalidVersion, InvalidSpecifier):
return False
return candidate in spec


def _declared_dependencies(root: Path) -> dict[str, str | None]:
"""Return {canonical_name: specifier} required in the captured SBOM closure.

Covers [project].dependencies plus every requirement declared by the extras
the release workflow installs (``_RELEASE_EXTRAS``). PEP 508 environment
markers are evaluated against the running interpreter, which in the release
workflow is the same environment that captures the SBOM; requirements whose
markers do not apply are not required.
"""
pyproject = root / "pyproject.toml"
try:
raw = pyproject.read_text(encoding="utf-8")
except OSError:
return {}
requirements: list[str] = []
if tomllib is not None:
try:
parsed = tomllib.loads(raw)
except (KeyError, ValueError):
parsed = {}
project = parsed.get("project", {}) if isinstance(parsed, dict) else {}
if isinstance(project, dict):
core = project.get("dependencies", [])
if isinstance(core, list):
requirements.extend(item for item in core if isinstance(item, str))
extras = project.get("optional-dependencies", {})
if isinstance(extras, dict):
for extra in _RELEASE_EXTRAS:
group = extras.get(extra)
if isinstance(group, list):
requirements.extend(
item for item in group if isinstance(item, str)
)
else:
project = re.search(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)", raw)
if project is not None:
deps_block = re.search(
r'(?m)^dependencies\s*=\s*\[(.*?)\]', project.group(1), re.DOTALL,
)
if deps_block is not None:
requirements.extend(re.findall(r'"([^"]+)"', deps_block.group(1)))
extras_table = re.search(
r"(?ms)^\[project\.optional-dependencies\]\s*(.*?)(?=^\[|\Z)", raw,
)
if extras_table is not None:
for extra in _RELEASE_EXTRAS:
group = re.search(
r"(?m)^" + re.escape(extra) + r"\s*=\s*\[(.*?)\]",
extras_table.group(1), re.DOTALL,
)
if group is not None:
requirements.extend(re.findall(r'"([^"]+)"', group.group(1)))
deps: dict[str, str | None] = {}
for requirement in requirements:
if not isinstance(requirement, str) or not requirement.strip():
continue
name, specifier = _parse_requirement(requirement)
canonical = _canonical_package_name(name)
if not canonical or canonical == PACKAGE:
continue
marker_text = requirement.split(";", 1)[1].strip() if ";" in requirement else ""
if marker_text:
try:
applies = Marker(marker_text).evaluate()
except InvalidMarker as exc:
raise EvidenceError(
"pyproject.toml declares an unparsable environment marker: "
+ marker_text
) from exc
if not applies:
continue
if canonical not in deps or deps[canonical] is None:
deps[canonical] = specifier
return deps

def _python_sbom_packages(document: dict[str, Any]) -> set[tuple[str, str]]:
packages = set()
metadata_component = document.get("metadata", {}).get("component")
Expand All @@ -238,13 +371,19 @@ def _python_sbom_packages(document: dict[str, Any]) -> set[tuple[str, str]]:
purl = component.get("purl")
name = component.get("name")
version = component.get("version")
if (
isinstance(purl, str)
and purl.startswith("pkg:pypi/")
and isinstance(name, str)
and isinstance(version, str)
):
packages.add((_canonical_package_name(name), version))
if not isinstance(name, str) or not isinstance(version, str):
continue
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
if not isinstance(purl, str) or not purl.startswith("pkg:pypi/"):
raise EvidenceError(
"SBOM component lacks a valid PyPI PURL: "
+ name + "@" + version
)
if not _purl_matches(purl, _canonical_package_name(name), version):
raise EvidenceError(
"SBOM component PURL does not match its name/version: "
+ name + "@" + version + " vs " + purl
)
packages.add((_canonical_package_name(name), version))
return packages


Expand Down Expand Up @@ -272,12 +411,14 @@ def sbom_artifact(root: Path, path: Path) -> dict[str, Any]:
}


def environment_lock_artifact(root: Path, path: Path, sbom: Path) -> dict[str, Any]:
def environment_lock_artifact(
root: Path, path: Path, sbom: Path, version: str) -> dict[str, Any]:
"""Require the exact build freeze to equal the Python SBOM package closure."""
if not path.is_file() or path.is_symlink():
raise EvidenceError("build environment lock is missing")
relative = _relative_path(root, path)
packages: set[tuple[str, str]] = set()
seen_names: set[str] = set()
try:
lines = path.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeDecodeError) as exc:
Expand All @@ -288,12 +429,71 @@ def environment_lock_artifact(root: Path, path: Path, sbom: Path) -> dict[str, A
match = _PACKAGE_LOCK_LINE.fullmatch(line)
if match is None:
raise EvidenceError("build environment lock must contain exact name==version lines")
package = (_canonical_package_name(match.group(1)), match.group(2))
canonical = _canonical_package_name(match.group(1))
package = (canonical, match.group(2))
if package in packages:
raise EvidenceError("build environment lock contains a duplicate package")
if canonical in seen_names:
raise EvidenceError(
"build environment lock contains conflicting versions of " + canonical
)
seen_names.add(canonical)
packages.add(package)
sbom_packages = _python_sbom_packages(_json_object(sbom, "SBOM"))
if packages != sbom_packages:
document = _json_object(sbom, "SBOM")
sbom_packages = _python_sbom_packages(document)
if not sbom_packages:
raise EvidenceError("SBOM contains no Python package components")
metadata_component = document.get("metadata", {}).get("component")
if not isinstance(metadata_component, dict):
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
raise EvidenceError(
"SBOM metadata.component does not identify the " + PACKAGE + " root"
)
root_name = metadata_component.get("name")
root_version = metadata_component.get("version")
root_purl = metadata_component.get("purl")
if (
not isinstance(root_name, str)
or _canonical_package_name(root_name) != PACKAGE
or not isinstance(root_version, str)
or root_version != version
or not isinstance(root_purl, str)
or not _purl_matches(root_purl, PACKAGE, version)
):
raise EvidenceError(
"SBOM metadata.component does not identify the " + PACKAGE
+ " root at version " + version
)
declared = _declared_dependencies(root)
dependency_packages = {
pkg for pkg in sbom_packages
if pkg != (_canonical_package_name(PACKAGE), version)
}
declared_names = {name for name in declared if name != PACKAGE}
sbom_dependency_names = {
name for name, _ in dependency_packages
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
}
missing_declared = declared_names - sbom_dependency_names
Comment on lines +471 to +475

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate transitive packages in the SBOM closure

When a truncated SBOM retains the root and every direct requirement from the selected all/test extras but omits transitive packages, missing_declared is empty and the subset check still succeeds because absent SBOM packages impose no requirement on the lock. For example, transitive-only packages such as anyio can be removed while their lock entries remain, allowing incomplete release evidence to be published; validate the SBOM dependency graph's full reachable closure rather than only direct pyproject.toml names.

Useful? React with 👍 / 👎.

if missing_declared:
raise EvidenceError(
"SBOM is missing declared dependencies: "
+ ", ".join(sorted(missing_declared))
)
# Validate version constraints for declared dependencies
sbom_versions = {name: ver for name, ver in dependency_packages}
for name, specifier in declared.items():
if name == PACKAGE or not specifier or name not in sbom_versions:
continue
sbom_ver = sbom_versions[name]
if not _version_satisfies(sbom_ver, specifier):
raise EvidenceError(
f"SBOM version {name}=={sbom_ver} does not satisfy "
f"declared constraint {specifier}"
)
if not dependency_packages:
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
raise EvidenceError(
"SBOM contains no dependency components beyond the " + PACKAGE + " root"
)
if not sbom_packages.issubset(packages):
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
raise EvidenceError("build environment lock and Python SBOM package closure differ")
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
return {
"filename": path.name,
Expand Down Expand Up @@ -675,7 +875,7 @@ def build_evidence(
artifacts = distribution_artifacts(distribution_directory, version)
artifact_digests = {item["filename"]: item["sha256"] for item in artifacts}
python_sbom = sbom_artifact(root, sbom)
environment = environment_lock_artifact(root, environment_lock, sbom)
environment = environment_lock_artifact(root, environment_lock, sbom, version)
container_sbom = container_sbom_artifact(root, image_sbom, image_digest)
container_scan = container_scan_artifact(root, image_scan)
reproducibility_record = reproducibility_artifact(
Expand Down
Loading