CD/CD consolidation

This commit is contained in:
StellaOps Bot
2025-12-26 17:32:23 +02:00
parent a866eb6277
commit c786faae84
638 changed files with 3821 additions and 181 deletions

View 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()

View 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"
}
}

View 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

View 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"]

View 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';
}
}

View 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()