CD/CD consolidation
This commit is contained in:
BIN
devops/release/__pycache__/build_release.cpython-312.pyc
Normal file
BIN
devops/release/__pycache__/build_release.cpython-312.pyc
Normal file
Binary file not shown.
BIN
devops/release/__pycache__/verify_release.cpython-312.pyc
Normal file
BIN
devops/release/__pycache__/verify_release.cpython-312.pyc
Normal file
Binary file not shown.
89
devops/release/check_release_manifest.py
Normal file
89
devops/release/check_release_manifest.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fail-fast validator for release manifests and downloads manifest.
|
||||
Checks presence of required components and expected fields so release pipelines
|
||||
can surface missing artefacts early (instead of blocking deploy tasks later).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
REQUIRED_COMPONENTS = [
|
||||
"orchestrator",
|
||||
"policy-registry",
|
||||
"vex-lens",
|
||||
"issuer-directory",
|
||||
"findings-ledger",
|
||||
"vuln-explorer-api",
|
||||
"packs-registry",
|
||||
"task-runner",
|
||||
"web-ui",
|
||||
]
|
||||
|
||||
|
||||
def load_yaml(path: Path):
|
||||
try:
|
||||
return yaml.safe_load(path.read_text())
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"ERROR: failed to parse {path}: {exc}")
|
||||
|
||||
|
||||
def check_manifest(manifest_path: Path) -> list[str]:
|
||||
data = load_yaml(manifest_path)
|
||||
comps = {c.get("name") for c in data.get("release", {}).get("components", [])}
|
||||
missing = [c for c in REQUIRED_COMPONENTS if c not in comps]
|
||||
return missing
|
||||
|
||||
|
||||
def check_downloads(downloads_path: Path) -> list[str]:
|
||||
missing = []
|
||||
try:
|
||||
data = json.loads(downloads_path.read_text())
|
||||
except Exception as exc:
|
||||
return [f"{downloads_path}: invalid JSON ({exc})"]
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
missing.append(f"{downloads_path}: no items found")
|
||||
for idx, item in enumerate(items):
|
||||
for field in ("name", "type"):
|
||||
if field not in item:
|
||||
missing.append(f"{downloads_path}: item {idx} missing '{field}'")
|
||||
if item.get("type") == "container" and "image" not in item:
|
||||
missing.append(f"{downloads_path}: item {idx} missing 'image'")
|
||||
if item.get("type") == "archive" and "sha256" not in item:
|
||||
missing.append(f"{downloads_path}: item {idx} missing 'sha256'")
|
||||
return missing
|
||||
|
||||
|
||||
def main():
|
||||
manifest = Path("deploy/releases/2025.09-stable.yaml")
|
||||
airgap = Path("deploy/releases/2025.09-airgap.yaml")
|
||||
downloads = Path("deploy/downloads/manifest.json")
|
||||
|
||||
errors: list[str] = []
|
||||
for path in (manifest, airgap):
|
||||
if not path.exists():
|
||||
errors.append(f"{path}: file missing")
|
||||
continue
|
||||
missing = check_manifest(path)
|
||||
if missing:
|
||||
errors.append(f"{path}: missing components -> {', '.join(missing)}")
|
||||
if downloads.exists():
|
||||
errors.extend(check_downloads(downloads))
|
||||
else:
|
||||
errors.append(f"{downloads}: file missing")
|
||||
|
||||
if errors:
|
||||
print("FAIL\n" + "\n".join(f"- {e}" for e in errors))
|
||||
sys.exit(1)
|
||||
|
||||
print("OK: required components present and downloads manifest is well-formed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
127
devops/release/components.json
Normal file
127
devops/release/components.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"registry": "registry.stella-ops.org/stellaops",
|
||||
"platforms": ["linux/amd64", "linux/arm64"],
|
||||
"defaultChannel": "edge",
|
||||
"docker": {
|
||||
"sdkImage": "mcr.microsoft.com/dotnet/nightly/sdk:10.0",
|
||||
"runtimeImage": "mcr.microsoft.com/dotnet/nightly/aspnet:10.0",
|
||||
"nodeImage": "node:20.14.0-bookworm",
|
||||
"nginxImage": "nginx:1.27-alpine"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"name": "authority",
|
||||
"repository": "authority",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Authority/StellaOps.Authority/StellaOps.Authority/StellaOps.Authority.csproj",
|
||||
"entrypoint": "StellaOps.Authority.dll"
|
||||
},
|
||||
{
|
||||
"name": "signer",
|
||||
"repository": "signer",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/StellaOps.Signer.WebService.csproj",
|
||||
"entrypoint": "StellaOps.Signer.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "attestor",
|
||||
"repository": "attestor",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj",
|
||||
"entrypoint": "StellaOps.Attestor.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "scanner-web",
|
||||
"repository": "scanner-web",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj",
|
||||
"entrypoint": "StellaOps.Scanner.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "scanner-worker",
|
||||
"repository": "scanner-worker",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj",
|
||||
"entrypoint": "StellaOps.Scanner.Worker.dll"
|
||||
},
|
||||
{
|
||||
"name": "concelier",
|
||||
"repository": "concelier",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj",
|
||||
"entrypoint": "StellaOps.Concelier.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "excititor",
|
||||
"repository": "excititor",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj",
|
||||
"entrypoint": "StellaOps.Excititor.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "advisory-ai-web",
|
||||
"repository": "advisory-ai-web",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/StellaOps.AdvisoryAI.WebService.csproj",
|
||||
"entrypoint": "StellaOps.AdvisoryAI.WebService.dll"
|
||||
},
|
||||
{
|
||||
"name": "advisory-ai-worker",
|
||||
"repository": "advisory-ai-worker",
|
||||
"kind": "dotnet-service",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.dotnet-service",
|
||||
"project": "src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj",
|
||||
"entrypoint": "StellaOps.AdvisoryAI.Worker.dll"
|
||||
},
|
||||
{
|
||||
"name": "web-ui",
|
||||
"repository": "web-ui",
|
||||
"kind": "angular-ui",
|
||||
"context": ".",
|
||||
"dockerfile": "ops/devops/release/docker/Dockerfile.angular-ui"
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"project": "src/Cli/StellaOps.Cli/StellaOps.Cli.csproj",
|
||||
"runtimes": [
|
||||
"linux-x64",
|
||||
"linux-arm64",
|
||||
"osx-x64",
|
||||
"osx-arm64",
|
||||
"win-x64"
|
||||
],
|
||||
"packagePrefix": "stella",
|
||||
"outputDir": "out/release/cli"
|
||||
},
|
||||
"helm": {
|
||||
"chartPath": "deploy/helm/stellaops",
|
||||
"outputDir": "out/release/helm"
|
||||
},
|
||||
"compose": {
|
||||
"files": [
|
||||
"deploy/compose/docker-compose.dev.yaml",
|
||||
"deploy/compose/docker-compose.stage.yaml",
|
||||
"deploy/compose/docker-compose.airgap.yaml"
|
||||
]
|
||||
},
|
||||
"buildxPlugin": {
|
||||
"project": "src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj"
|
||||
}
|
||||
}
|
||||
31
devops/release/docker/Dockerfile.angular-ui
Normal file
31
devops/release/docker/Dockerfile.angular-ui
Normal file
@@ -0,0 +1,31 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
|
||||
ARG NODE_IMAGE=node:20.14.0-bookworm
|
||||
ARG NGINX_IMAGE=nginx:1.27-alpine
|
||||
ARG VERSION=0.0.0
|
||||
ARG CHANNEL=dev
|
||||
ARG GIT_SHA=0000000
|
||||
ARG SOURCE_DATE_EPOCH=0
|
||||
|
||||
FROM ${NODE_IMAGE} AS build
|
||||
WORKDIR /workspace
|
||||
ENV CI=1 \
|
||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
|
||||
COPY src/Web/StellaOps.Web/package.json src/Web/StellaOps.Web/package-lock.json ./
|
||||
RUN npm ci --prefer-offline --no-audit --no-fund
|
||||
COPY src/Web/StellaOps.Web/ ./
|
||||
RUN npm run build -- --configuration=production
|
||||
|
||||
FROM ${NGINX_IMAGE} AS runtime
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
ARG GIT_SHA
|
||||
WORKDIR /usr/share/nginx/html
|
||||
RUN rm -rf ./*
|
||||
COPY --from=build /workspace/dist/stellaops-web/ /usr/share/nginx/html/
|
||||
COPY ops/devops/release/docker/nginx-default.conf /etc/nginx/conf.d/default.conf
|
||||
LABEL org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${GIT_SHA}" \
|
||||
org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/conselier" \
|
||||
org.stellaops.release.channel="${CHANNEL}"
|
||||
EXPOSE 8080
|
||||
52
devops/release/docker/Dockerfile.dotnet-service
Normal file
52
devops/release/docker/Dockerfile.dotnet-service
Normal file
@@ -0,0 +1,52 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
|
||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/nightly/sdk:10.0
|
||||
ARG RUNTIME_IMAGE=gcr.io/distroless/dotnet/aspnet:latest
|
||||
|
||||
ARG PROJECT
|
||||
ARG ENTRYPOINT_DLL
|
||||
ARG VERSION=0.0.0
|
||||
ARG CHANNEL=dev
|
||||
ARG GIT_SHA=0000000
|
||||
ARG SOURCE_DATE_EPOCH=0
|
||||
|
||||
FROM ${SDK_IMAGE} AS build
|
||||
ARG PROJECT
|
||||
ARG GIT_SHA
|
||||
ARG SOURCE_DATE_EPOCH
|
||||
WORKDIR /src
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 \
|
||||
NUGET_XMLDOC_MODE=skip \
|
||||
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.nuget/packages \
|
||||
dotnet restore "${PROJECT}"
|
||||
RUN --mount=type=cache,target=/root/.nuget/packages \
|
||||
dotnet publish "${PROJECT}" \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false \
|
||||
/p:ContinuousIntegrationBuild=true \
|
||||
/p:SourceRevisionId=${GIT_SHA} \
|
||||
/p:Deterministic=true \
|
||||
/p:TreatWarningsAsErrors=true
|
||||
|
||||
FROM ${RUNTIME_IMAGE} AS runtime
|
||||
WORKDIR /app
|
||||
ARG ENTRYPOINT_DLL
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
ARG GIT_SHA
|
||||
ENV DOTNET_EnableDiagnostics=0 \
|
||||
ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
COPY --from=build /app/publish/ ./
|
||||
RUN set -eu; \
|
||||
printf '#!/usr/bin/env sh\nset -e\nexec dotnet %s "$@"\n' "${ENTRYPOINT_DLL}" > /entrypoint.sh; \
|
||||
chmod +x /entrypoint.sh
|
||||
EXPOSE 8080
|
||||
LABEL org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${GIT_SHA}" \
|
||||
org.opencontainers.image.source="https://git.stella-ops.org/stella-ops/conselier" \
|
||||
org.stellaops.release.channel="${CHANNEL}"
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
22
devops/release/docker/nginx-default.conf
Normal file
22
devops/release/docker/nginx-default.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|css|svg|png|jpg|jpeg|gif|ico|woff2?)$ {
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
add_header Content-Type text/plain;
|
||||
return 200 'ok';
|
||||
}
|
||||
}
|
||||
232
devops/release/test_verify_release.py
Normal file
232
devops/release/test_verify_release.py
Normal file
@@ -0,0 +1,232 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parent))
|
||||
|
||||
from build_release import write_manifest # type: ignore import-not-found
|
||||
from verify_release import VerificationError, compute_sha256, verify_release
|
||||
|
||||
|
||||
class VerifyReleaseTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._temp = tempfile.TemporaryDirectory()
|
||||
self.base_path = Path(self._temp.name)
|
||||
self.out_dir = self.base_path / "out"
|
||||
self.release_dir = self.out_dir / "release"
|
||||
self.release_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._temp.cleanup()
|
||||
|
||||
def _relative_to_out(self, path: Path) -> str:
|
||||
return path.relative_to(self.out_dir).as_posix()
|
||||
|
||||
def _write_json(self, path: Path, payload: dict[str, object]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
def _create_sample_release(self) -> None:
|
||||
sbom_path = self.release_dir / "artifacts/sboms/sample.cyclonedx.json"
|
||||
sbom_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sbom_path.write_text('{"bomFormat":"CycloneDX","specVersion":"1.5"}\n', encoding="utf-8")
|
||||
sbom_sha = compute_sha256(sbom_path)
|
||||
|
||||
provenance_path = self.release_dir / "artifacts/provenance/sample.provenance.json"
|
||||
self._write_json(
|
||||
provenance_path,
|
||||
{
|
||||
"buildDefinition": {"buildType": "https://example/build", "externalParameters": {}},
|
||||
"runDetails": {"builder": {"id": "https://example/ci"}},
|
||||
},
|
||||
)
|
||||
provenance_sha = compute_sha256(provenance_path)
|
||||
|
||||
signature_path = self.release_dir / "artifacts/signatures/sample.signature"
|
||||
signature_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
signature_path.write_text("signature-data\n", encoding="utf-8")
|
||||
signature_sha = compute_sha256(signature_path)
|
||||
|
||||
metadata_path = self.release_dir / "artifacts/metadata/sample.metadata.json"
|
||||
self._write_json(metadata_path, {"digest": "sha256:1234"})
|
||||
metadata_sha = compute_sha256(metadata_path)
|
||||
|
||||
chart_path = self.release_dir / "helm/stellaops-1.0.0.tgz"
|
||||
chart_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
chart_path.write_bytes(b"helm-chart-data")
|
||||
chart_sha = compute_sha256(chart_path)
|
||||
|
||||
compose_path = self.release_dir.parent / "deploy/compose/docker-compose.dev.yaml"
|
||||
compose_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
compose_path.write_text("services: {}\n", encoding="utf-8")
|
||||
compose_sha = compute_sha256(compose_path)
|
||||
|
||||
debug_file = self.release_dir / "debug/.build-id/ab/cdef.debug"
|
||||
debug_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
debug_file.write_bytes(b"\x7fELFDEBUGDATA")
|
||||
debug_sha = compute_sha256(debug_file)
|
||||
|
||||
debug_manifest_path = self.release_dir / "debug/debug-manifest.json"
|
||||
debug_manifest = OrderedDict(
|
||||
(
|
||||
("generatedAt", "2025-10-26T00:00:00Z"),
|
||||
("version", "1.0.0"),
|
||||
("channel", "edge"),
|
||||
(
|
||||
"artifacts",
|
||||
[
|
||||
OrderedDict(
|
||||
(
|
||||
("buildId", "abcdef1234"),
|
||||
("platform", "linux/amd64"),
|
||||
("debugPath", "debug/.build-id/ab/cdef.debug"),
|
||||
("sha256", debug_sha),
|
||||
("size", debug_file.stat().st_size),
|
||||
("components", ["sample"]),
|
||||
("images", ["registry.example/sample@sha256:feedface"]),
|
||||
("sources", ["app/sample.dll"]),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
self._write_json(debug_manifest_path, debug_manifest)
|
||||
debug_manifest_sha = compute_sha256(debug_manifest_path)
|
||||
(debug_manifest_path.with_suffix(debug_manifest_path.suffix + ".sha256")).write_text(
|
||||
f"{debug_manifest_sha} {debug_manifest_path.name}\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
manifest = OrderedDict(
|
||||
(
|
||||
(
|
||||
"release",
|
||||
OrderedDict(
|
||||
(
|
||||
("version", "1.0.0"),
|
||||
("channel", "edge"),
|
||||
("date", "2025-10-26T00:00:00Z"),
|
||||
("calendar", "2025.10"),
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"components",
|
||||
[
|
||||
OrderedDict(
|
||||
(
|
||||
("name", "sample"),
|
||||
("image", "registry.example/sample@sha256:feedface"),
|
||||
("tags", ["registry.example/sample:1.0.0"]),
|
||||
(
|
||||
"sbom",
|
||||
OrderedDict(
|
||||
(
|
||||
("path", self._relative_to_out(sbom_path)),
|
||||
("sha256", sbom_sha),
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"provenance",
|
||||
OrderedDict(
|
||||
(
|
||||
("path", self._relative_to_out(provenance_path)),
|
||||
("sha256", provenance_sha),
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"signature",
|
||||
OrderedDict(
|
||||
(
|
||||
("path", self._relative_to_out(signature_path)),
|
||||
("sha256", signature_sha),
|
||||
("ref", "sigstore://example"),
|
||||
("tlogUploaded", True),
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
"metadata",
|
||||
OrderedDict(
|
||||
(
|
||||
("path", self._relative_to_out(metadata_path)),
|
||||
("sha256", metadata_sha),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"charts",
|
||||
[
|
||||
OrderedDict(
|
||||
(
|
||||
("name", "stellaops"),
|
||||
("version", "1.0.0"),
|
||||
("path", self._relative_to_out(chart_path)),
|
||||
("sha256", chart_sha),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"compose",
|
||||
[
|
||||
OrderedDict(
|
||||
(
|
||||
("name", "docker-compose.dev.yaml"),
|
||||
("path", compose_path.relative_to(self.out_dir).as_posix()),
|
||||
("sha256", compose_sha),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"debugStore",
|
||||
OrderedDict(
|
||||
(
|
||||
("manifest", "debug/debug-manifest.json"),
|
||||
("sha256", debug_manifest_sha),
|
||||
("entries", 1),
|
||||
("platforms", ["linux/amd64"]),
|
||||
("directory", "debug/.build-id"),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
write_manifest(manifest, self.release_dir)
|
||||
|
||||
def test_verify_release_success(self) -> None:
|
||||
self._create_sample_release()
|
||||
# Should not raise
|
||||
verify_release(self.release_dir)
|
||||
|
||||
def test_verify_release_detects_sha_mismatch(self) -> None:
|
||||
self._create_sample_release()
|
||||
tampered = self.release_dir / "artifacts/sboms/sample.cyclonedx.json"
|
||||
tampered.write_text("tampered\n", encoding="utf-8")
|
||||
with self.assertRaises(VerificationError):
|
||||
verify_release(self.release_dir)
|
||||
|
||||
def test_verify_release_detects_missing_debug_file(self) -> None:
|
||||
self._create_sample_release()
|
||||
debug_file = self.release_dir / "debug/.build-id/ab/cdef.debug"
|
||||
debug_file.unlink()
|
||||
with self.assertRaises(VerificationError):
|
||||
verify_release(self.release_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user