Commit remaining workspace changes
This commit is contained in:
@@ -36,6 +36,7 @@ from src.models.clean_release import (
|
||||
ReleaseCandidateStatus,
|
||||
)
|
||||
from src.services.clean_release.approval_service import approve_candidate
|
||||
from src.services.clean_release.artifact_catalog_loader import load_bootstrap_artifacts
|
||||
from src.services.clean_release.compliance_execution_service import ComplianceExecutionService
|
||||
from src.services.clean_release.enums import CandidateStatus
|
||||
from src.services.clean_release.manifest_service import build_manifest_snapshot
|
||||
@@ -270,6 +271,15 @@ class CleanReleaseTUI:
|
||||
status=ReleaseCandidateStatus.DRAFT,
|
||||
)
|
||||
repository.save_candidate(candidate)
|
||||
imported_artifacts = load_bootstrap_artifacts(
|
||||
os.getenv("CLEAN_TUI_ARTIFACTS_JSON", "").strip(),
|
||||
candidate.id,
|
||||
)
|
||||
for artifact in imported_artifacts:
|
||||
repository.save_artifact(artifact)
|
||||
if imported_artifacts:
|
||||
candidate.transition_to(CandidateStatus.PREPARED)
|
||||
repository.save_candidate(candidate)
|
||||
|
||||
registry_id = payload.get("registry_id", "REG-1")
|
||||
entries = [
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# [DEF:backend.src.services.clean_release.artifact_catalog_loader:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: clean-release, artifacts, bootstrap, json, tui
|
||||
# @PURPOSE: Load bootstrap artifact catalogs for clean release real-mode flows.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.clean_release.CandidateArtifact
|
||||
# @INVARIANT: Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ...models.clean_release import CandidateArtifact
|
||||
|
||||
|
||||
# [DEF:load_bootstrap_artifacts:Function]
|
||||
# @PURPOSE: Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows.
|
||||
# @PRE: path points to readable JSON file; payload is list[artifact] or {"artifacts": list[artifact]}.
|
||||
# @POST: Returns non-mutated CandidateArtifact models with required fields populated.
|
||||
def load_bootstrap_artifacts(path: str, candidate_id: str) -> List[CandidateArtifact]:
|
||||
if not path or not path.strip():
|
||||
return []
|
||||
if not candidate_id or not candidate_id.strip():
|
||||
raise ValueError("candidate_id must be non-empty for artifact catalog import")
|
||||
|
||||
catalog_path = Path(path)
|
||||
payload = json.loads(catalog_path.read_text(encoding="utf-8"))
|
||||
raw_artifacts = payload.get("artifacts") if isinstance(payload, dict) else payload
|
||||
if not isinstance(raw_artifacts, list):
|
||||
raise ValueError("artifact catalog must be a list or an object with 'artifacts' list")
|
||||
|
||||
artifacts: List[CandidateArtifact] = []
|
||||
for index, raw_artifact in enumerate(raw_artifacts, start=1):
|
||||
if not isinstance(raw_artifact, dict):
|
||||
raise ValueError(f"artifact #{index} must be an object")
|
||||
|
||||
artifact_id = str(raw_artifact.get("id", "")).strip()
|
||||
artifact_path = str(raw_artifact.get("path", "")).strip()
|
||||
artifact_sha256 = str(raw_artifact.get("sha256", "")).strip()
|
||||
artifact_size = raw_artifact.get("size")
|
||||
if not artifact_id:
|
||||
raise ValueError(f"artifact #{index} missing required field 'id'")
|
||||
if not artifact_path:
|
||||
raise ValueError(f"artifact #{index} missing required field 'path'")
|
||||
if not artifact_sha256:
|
||||
raise ValueError(f"artifact #{index} missing required field 'sha256'")
|
||||
if not isinstance(artifact_size, int) or artifact_size < 0:
|
||||
raise ValueError(f"artifact #{index} field 'size' must be non-negative integer")
|
||||
|
||||
category = str(raw_artifact.get("detected_category") or raw_artifact.get("category") or "").strip() or None
|
||||
source_uri = str(raw_artifact.get("source_uri", "")).strip() or None
|
||||
source_host = str(raw_artifact.get("source_host", "")).strip() or None
|
||||
metadata_json = raw_artifact.get("metadata_json")
|
||||
if metadata_json is None:
|
||||
metadata_json = {
|
||||
key: value
|
||||
for key, value in raw_artifact.items()
|
||||
if key
|
||||
not in {
|
||||
"id",
|
||||
"path",
|
||||
"sha256",
|
||||
"size",
|
||||
"category",
|
||||
"detected_category",
|
||||
"source_uri",
|
||||
"source_host",
|
||||
"metadata_json",
|
||||
}
|
||||
}
|
||||
if not isinstance(metadata_json, dict):
|
||||
raise ValueError(f"artifact #{index} field 'metadata_json' must be object")
|
||||
|
||||
artifacts.append(
|
||||
CandidateArtifact(
|
||||
id=artifact_id,
|
||||
candidate_id=candidate_id,
|
||||
path=artifact_path,
|
||||
sha256=artifact_sha256,
|
||||
size=artifact_size,
|
||||
detected_category=category,
|
||||
declared_category=category,
|
||||
source_uri=source_uri,
|
||||
source_host=source_host,
|
||||
metadata_json=metadata_json,
|
||||
)
|
||||
)
|
||||
|
||||
return artifacts
|
||||
# [/DEF:load_bootstrap_artifacts:Function]
|
||||
|
||||
# [/DEF:backend.src.services.clean_release.artifact_catalog_loader:Module]
|
||||
Reference in New Issue
Block a user