#!/usr/bin/env python3 """Generate CI/CD artifact receipts without reading secrets. The output is intentionally metadata-only. It records package manifests, source digests, build identity, SBOM/provenance receipt names, and retention policy evidence for Gitea Actions logs. It does not read environment secrets, runtime volumes, raw sessions, SQLite databases, or `.env` files. """ from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import re import sys import tomllib from typing import Any SCHEMA_VERSION = "awoooi_cicd_artifact_receipts_v1" SBOM_SCHEMA_VERSION = "awoooi_cicd_sbom_v1" PROVENANCE_SCHEMA_VERSION = "awoooi_cicd_provenance_v1" RETENTION_SCHEMA_VERSION = "awoooi_cicd_artifact_retention_v1" PACKAGE_RELEASE_SCHEMA_VERSION = "awoooi_cicd_package_release_promotion_v1" PYPROJECT_PATHS = [ "apps/api/pyproject.toml", "packages/lewooogo-data/pyproject.toml", "packages/lewooogo-brain/pyproject.toml", "scripts/aider_watch_client/pyproject.toml", ] PACKAGE_JSON_PATHS = [ "package.json", "apps/web/package.json", "packages/lewooogo-core/package.json", "packages/shared-types/package.json", "packages/eslint-config/package.json", "packages/tsconfig/package.json", ] LOCKFILE_PATHS = [ "pnpm-lock.yaml", "apps/api/requirements.txt", "apps/sensor/requirements.txt", ] def _sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _write_json(path: Path, payload: dict[str, Any]) -> str: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) return _sha256_file(path) def _safe_component_name(raw: str) -> str: value = raw.strip() if not value: return "unknown" # Preserve package scope names while stripping version specifiers/extras. match = re.match(r"([A-Za-z0-9_.@/-]+)", value) return match.group(1) if match else value[:80] def _python_manifest_components(repo_root: Path) -> list[dict[str, Any]]: components: list[dict[str, Any]] = [] for rel in PYPROJECT_PATHS: path = repo_root / rel if not path.is_file(): continue data = tomllib.loads(path.read_text(encoding="utf-8")) project = data.get("project") or {} project_name = str(project.get("name") or path.parent.name) project_version = str(project.get("version") or "0.0.0") components.append( { "type": "application", "ecosystem": "python", "name": project_name, "version": project_version, "manifest_path": rel, "manifest_sha256": _sha256_file(path), } ) dependencies = list(project.get("dependencies") or []) optional = project.get("optional-dependencies") or {} for group, values in sorted(optional.items()): dependencies.extend(f"{item} ; extra == '{group}'" for item in values or []) for dep in dependencies: components.append( { "type": "library", "ecosystem": "python", "name": _safe_component_name(str(dep)), "version": "", "manifest_path": rel, "scope": "runtime_or_optional", } ) return components def _node_manifest_components(repo_root: Path) -> list[dict[str, Any]]: components: list[dict[str, Any]] = [] dependency_fields = ( "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", ) for rel in PACKAGE_JSON_PATHS: path = repo_root / rel if not path.is_file(): continue data = json.loads(path.read_text(encoding="utf-8")) package_name = str(data.get("name") or path.parent.name) package_version = str(data.get("version") or "0.0.0") components.append( { "type": "application", "ecosystem": "node", "name": package_name, "version": package_version, "manifest_path": rel, "manifest_sha256": _sha256_file(path), } ) for field in dependency_fields: deps = data.get(field) or {} if not isinstance(deps, dict): continue for name, version in sorted(deps.items()): components.append( { "type": "library", "ecosystem": "node", "name": str(name), "version": str(version), "manifest_path": rel, "scope": field, } ) return components def _existing_file_digests(repo_root: Path, paths: list[str]) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] for rel in paths: path = repo_root / rel if path.is_file(): rows.append({"path": rel, "sha256": _sha256_file(path)}) return rows def build_receipts(repo_root: Path, out_dir: Path) -> dict[str, Any]: components = _python_manifest_components(repo_root) + _node_manifest_components( repo_root ) lockfile_digests = _existing_file_digests(repo_root, LOCKFILE_PATHS) source_digests = _existing_file_digests( repo_root, [ ".gitea/workflows/cd.yaml", "apps/api/Dockerfile", "apps/web/Dockerfile", "product.awoooi.yaml", ], ) source_sha = ( os.environ.get("GITHUB_SHA") or os.environ.get("CI_COMMIT_SHA") or "local" ) run_id = os.environ.get("GITHUB_RUN_ID") or os.environ.get("CI_RUN_ID") or "local" run_attempt = os.environ.get("GITHUB_RUN_ATTEMPT") or "1" sbom = { "schema_version": SBOM_SCHEMA_VERSION, "source_control_authority": "gitea", "format": "metadata_only_sbom", "component_count": len(components), "components": components, "lockfile_digests": lockfile_digests, "operation_boundaries": { "secret_value_read_allowed": False, "env_file_read_allowed": False, "external_registry_lookup_allowed": False, "package_install_allowed": False, }, } sbom_sha = _write_json(out_dir / "awoooi-ci-sbom.json", sbom) provenance = { "schema_version": PROVENANCE_SCHEMA_VERSION, "source_control_authority": "gitea", "builder": { "id": "gitea-actions-self-hosted", "workflow": ".gitea/workflows/cd.yaml", "runner_isolation_contract": "explicit_labels_non110_preferred_controlled_runtime", }, "subject": { "name": "awoooi", "source_sha": source_sha, "run_id": run_id, "run_attempt": run_attempt, }, "build_type": "gitea-actions-docker-build-and-argocd-deploy", "slsa_source_contract": { "isolated_build_lane_required": True, "digest_bound_subject_required": True, "provenance_receipt_generated": True, "verified_builder_identity_required_for_release_closure": True, }, "source_digests": source_digests, "sbom_sha256": sbom_sha, "operation_boundaries": { "repo_write_allowed": False, "workflow_trigger_allowed": False, "secret_value_read_allowed": False, "github_api_allowed": False, }, } provenance_sha = _write_json(out_dir / "awoooi-ci-provenance.json", provenance) retention = { "schema_version": RETENTION_SCHEMA_VERSION, "source_control_authority": "gitea", "retention_policy": { "test_log_days": 30, "build_receipt_days": 90, "deploy_marker_days": 365, "sbom_provenance_days": 365, "backup_dr_receipt_days": 365, }, "artifact_receipts": [ { "artifact_id": "awoooi-ci-sbom", "path": "awoooi-ci-sbom.json", "sha256": sbom_sha, "retention_days": 365, }, { "artifact_id": "awoooi-ci-provenance", "path": "awoooi-ci-provenance.json", "sha256": provenance_sha, "retention_days": 365, }, ], "cache_key_contract": { "python_deps_hash_input": "apps/api/pyproject.toml", "node_deps_hash_input": "pnpm-lock.yaml", "docker_cache_image": "registry.wooo.work/awoooi/api:latest", }, "operation_boundaries": { "artifact_upload_attempted": False, "package_registry_write_attempted": False, "secret_value_read_allowed": False, }, } retention_sha = _write_json( out_dir / "awoooi-ci-artifact-retention-manifest.json", retention, ) package_release = { "schema_version": PACKAGE_RELEASE_SCHEMA_VERSION, "source_control_authority": "gitea", "release_channel_contract": { "primary_package_registry": "gitea_packages", "container_registry": "registry.wooo.work/awoooi", "release_tag_pattern": "awoooi-api-{short_sha}", "rollback_artifact_pattern": "awoooi-api-{previous_short_sha}", "changelog_receipt_required": True, "sbom_ref_required": True, "provenance_ref_required": True, "deploy_marker_ref_required": True, }, "promotion_subject": { "name": "awoooi", "source_sha": source_sha, "run_id": run_id, "run_attempt": run_attempt, "sbom_sha256": sbom_sha, "provenance_sha256": provenance_sha, "artifact_retention_sha256": retention_sha, }, "rollback_receipt_contract": { "rollback_source": "previous_deploy_marker", "rollback_requires_image_digest": True, "rollback_requires_package_or_release_ref": True, "rollback_requires_post_apply_readback": True, }, "operation_boundaries": { "package_registry_write_attempted": False, "gitea_release_write_attempted": False, "tag_write_attempted": False, "secret_value_read_allowed": False, "github_api_allowed": False, }, } package_release_sha = _write_json( out_dir / "awoooi-ci-package-release-promotion.json", package_release, ) manifest = { "schema_version": SCHEMA_VERSION, "source_control_authority": "gitea", "source_sha": source_sha, "run_id": run_id, "receipt_count": 4, "receipts": [ { "id": "sbom", "path": str(out_dir / "awoooi-ci-sbom.json"), "sha256": sbom_sha, }, { "id": "provenance", "path": str(out_dir / "awoooi-ci-provenance.json"), "sha256": provenance_sha, }, { "id": "artifact_retention", "path": str(out_dir / "awoooi-ci-artifact-retention-manifest.json"), "sha256": retention_sha, }, { "id": "package_release_promotion", "path": str(out_dir / "awoooi-ci-package-release-promotion.json"), "sha256": package_release_sha, }, ], "ready": True, "operation_boundaries": { "secret_values_collected": False, "github_api_used": False, "gitea_api_write_performed": False, "host_write_performed": False, }, } manifest_sha = _write_json( out_dir / "awoooi-ci-artifact-receipts.manifest.json", manifest ) manifest["manifest_sha256"] = manifest_sha return manifest def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo-root", type=Path, default=Path.cwd()) parser.add_argument( "--out-dir", type=Path, default=Path("/tmp/awoooi-cicd-artifacts") ) args = parser.parse_args(argv) repo_root = args.repo_root.resolve() out_dir = args.out_dir.resolve() manifest = build_receipts(repo_root, out_dir) print( f"cicd_artifact_receipt_manifest={out_dir / 'awoooi-ci-artifact-receipts.manifest.json'}" ) print(f"cicd_artifact_receipt_manifest_sha256={manifest['manifest_sha256']}") for receipt in manifest["receipts"]: print(f"cicd_artifact_receipt={receipt['id']} sha256={receipt['sha256']}") return 0 if __name__ == "__main__": sys.exit(main())