feat: project Nexus SBOM state into summaries
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 25s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
tegwick 2026-08-22 20:12:16 +02:00
parent e1e259cf87
commit 76e6eda086
4 changed files with 172 additions and 42 deletions

View file

@ -48,6 +48,9 @@ from api.schemas.managed_repo import (
classification_fields_set,
validate_repo_classification_fields,
)
from api.services.sbom_nexus import SBOMNexusError
from api.services.sbom_nexus import get_json as get_sbom_nexus_json
from api.services.sbom_nexus import reads_from_nexus
from hub_core.routers.repos import create_repos_router
router = APIRouter(prefix="/repos", tags=["repos"])
@ -108,7 +111,7 @@ async def list_repos(
capability_tag: str | None = None,
business_stake: str | None = None,
session: AsyncSession = Depends(get_session),
) -> list[ManagedRepo]:
) -> list[RepoRead]:
"""List repos with optional domain and classification filters."""
response.headers["Cache-Control"] = "max-age=60, stale-while-revalidate=30"
q = (
@ -134,7 +137,7 @@ async def list_repos(
if business_stake:
q = q.where(ManagedRepo.business_stake.contains([business_stake]))
result = await session.execute(q)
return list(result.scalars().all())
return await _project_repo_reads(list(result.scalars().all()))
@router.post("/", response_model=RepoRead, status_code=status.HTTP_201_CREATED)
@ -330,6 +333,7 @@ async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoIS
select(ManagedRepo).where(ManagedRepo.status == "active").order_by(ManagedRepo.name)
)
repos = list(repos_result.scalars().all())
sbom_projections = await _sbom_projection_map()
repo_ids = [r.id for r in repos]
id_to_slug = {r.id: r.slug for r in repos}
@ -385,7 +389,7 @@ async def doi_summary(session: AsyncSession = Depends(get_session)) -> list[DoIS
"local_path": repo.local_path,
"remote_url": repo.remote_url,
"host_paths": repo.host_paths or {},
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
"last_sbom_at": _projected_last_sbom_at(repo, sbom_projections),
"updated_at": str(repo.updated_at) if repo.updated_at else "",
}
fp = compute_fingerprint(
@ -465,6 +469,7 @@ async def get_repo_doi(
Results are cached by fingerprint. Pass ?force_refresh=true to bypass the cache.
"""
repo = await _get_repo_by_slug(slug, session)
sbom_projections = await _sbom_projection_map()
domain_result = await session.execute(select(Domain).where(Domain.id == repo.domain_id))
domain_obj = domain_result.scalar_one_or_none()
@ -484,7 +489,7 @@ async def get_repo_doi(
"local_path": repo.local_path,
"remote_url": repo.remote_url,
"host_paths": repo.host_paths or {},
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
"last_sbom_at": _projected_last_sbom_at(repo, sbom_projections),
"updated_at": str(repo.updated_at) if repo.updated_at else "",
}
fp = compute_fingerprint(repo_dict, str(tpsc_row.latest) if tpsc_row.latest else None,
@ -622,6 +627,15 @@ async def update_repo_with_classification(
return repo
@router.get("/{slug}", response_model=RepoRead)
async def get_repo_with_sbom_projection(
slug: str,
session: AsyncSession = Depends(get_session),
) -> RepoRead:
repo = await _get_repo_by_slug(slug, session)
return (await _project_repo_reads([repo]))[0]
router.include_router(
_core_repo_router(
include_collection_routes=False,
@ -822,3 +836,48 @@ def _repo_doi_dict(repo: ManagedRepo, domain_slug: str | None) -> dict:
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
"updated_at": str(repo.updated_at) if repo.updated_at else "",
}
async def _sbom_projection_map() -> dict[str, datetime | None]:
if not reads_from_nexus():
return {}
try:
repositories = await get_sbom_nexus_json("/repositories/")
except SBOMNexusError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
return {
repository["slug"]: (
datetime.fromisoformat(repository["last_attempt_at"].replace("Z", "+00:00"))
if repository.get("last_attempt_at")
else None
)
for repository in repositories
}
def _projected_last_sbom_at(
repo: ManagedRepo,
projections: dict[str, datetime | None],
) -> str | None:
if repo.slug in projections:
projected = projections[repo.slug]
return projected.isoformat() if projected else None
return str(repo.last_sbom_at) if repo.last_sbom_at else None
async def _project_repo_reads(repositories: list[ManagedRepo]) -> list[RepoRead]:
projections = await _sbom_projection_map()
result: list[RepoRead] = []
for repository in repositories:
read = RepoRead.model_validate(repository)
if repository.slug in projections:
read = read.model_copy(
update={
"last_sbom_at": projections[repository.slug],
"sbom_source": "sbom-nexus"
if projections[repository.slug]
else None,
}
)
result.append(read)
return result