activity-core/activity-definitions/daily-sbom-catchup.md
tegwick 26934e25b9
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 21s
Enforce bounded operation guardrails
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
2026-08-23 12:31:13 +02:00

121 lines
4.9 KiB
Markdown

---
id: daily-sbom-catchup
name: Daily SBOM Catch-up
enabled: true
owner: custodian-agent
governance: custodian
status: active
trigger:
type: cron
cron_expression: "15 9 * * 1-5" # weekdays 09:15, after the daily triage window
timezone: Europe/Berlin
misfire_policy: skip
context_sources:
- type: sbom-nexus
query: catch_up
operation: sbom_nexus_ingest
required: true
params:
limit: 3 # catch_up_limit — operator knob, not a nexus constant
apply: true # declared bounded side-effect; manual runs require confirmation
bind_to: context.catchup
# One ranked call returns only the N repos that have lacked a current SBOM the
# longest (never-scanned first) plus fleet counts. There is deliberately no
# `for_each` over the stale set: the 2026-08-17 fire emitted 75 tasks that way.
---
# Daily SBOM Catch-up
> **Enabled after production proof on 2026-08-22.** Two bounded manual fires
> processed three distinct repositories each, spawned zero tasks, and persisted
> terminal `no-checkout` evidence. The superseded weekly schedule remains off.
Replaces `weekly-sbom-staleness` / `flag-stale-sbom` (ACTIVITY-WP-0030).
The weekly check reported the backlog — 111 / 111 repos stale on 2026-08-18,
93 never scanned — and spawned one task per stale repo, so it could never
close it. This definition **updates N instead of reporting N**.
Runs weekdays at 09:15 Berlin, after the daily triage window.
## Contract
Context source `sbom-nexus / catch_up` (`context_resolvers/sbom_nexus.py`):
| Direction | Shape |
| --- | --- |
| input | `{"limit": 3}` |
| output | `{repos, selected_count, stale_count, never_count, total_count, limit}` |
| each repo | `repo_slug`, `last_sbom_at`, `sbom_age_days`, `has_sbom`, `checkout_available`, optional immutable `source_ref` |
Ranking (never-scanned first, then oldest `last_sbom_at`) belongs to
sbom-nexus. The adapter truncates to `limit` so an over-long response can never
widen the bounded side-effect below.
## Task emission
None. This definition carries **no `rule` block** — that is the point of the
replacement. `tasks_spawned` must stay 0 on every fire, and no Forgejo issues
are emitted (ACTIVITY-WP-0022).
## Evidence
```instruction
id: daily-sbom-catchup-report
trusted_fields: []
model: deterministic
temperature: 0
max_tokens: 1
prompt: |
Deterministic SBOM catch-up report from context.catchup (no LLM).
output_schema: ""
review_advisory: false
report_sinks:
- type: state-hub-progress
event_type: sbom_catchup
author: activity-core
topic_id: cee7bedf-2b48-46ef-8601-006474f2ad7a
```
The progress event names the repos selected, updated, and skipped with a
reason (`no-checkout`, `no-manifest`, `ingest-error`), plus the fleet counters
so `never_count` can be watched declining day over day.
Controlled-source outcomes additionally include `source-unavailable` and
`source-rejected`. They remain terminal for the selected repository and never
cause the same fire to select a replacement target.
## Bounded side-effect (ACTIVITY-WP-0030-T02 / ACTIVITY-WP-0033)
`params.apply: true` declares the write, but context resolution remains
read-only. The workflow records the already-truncated N targets in Temporal
history, then a dedicated activity applies exactly that fixed set. Completed
per-repository outcomes are heartbeated so an ordinary activity retry resumes
after them instead of selecting or writing a second batch.
A target with an explicitly unavailable checkout is recorded through
`POST /sbom/{slug}/skip` as `no-checkout`; other targets use the terminal ingest
route, which returns `ingested`, `no-manifest`, or `ingest-error`. Each request
sends a stable per-run/per-repository `Idempotency-Key`. Transport failures,
timeouts, and malformed responses fail visibly and are never converted into a
synthetic skip because the remote write may have committed. Nexus must enforce
the operation key to close the remaining crash window between a committed POST
and Activity Core's next heartbeat. The resulting `updated` and `skipped`
arrays are part of the run context and progress report. There is no task or
issue emission.
When Nexus returns a `forgejo-archive-v1` `source_ref`, the workflow freezes it
with the original target set and sends that exact object in the ingest body.
Retries reuse the same full commit SHA even if the repository default branch
has advanced.
## Enable checklist
1. CUST-WP-0062-T02/T03 done: `sbom-nexus` stood up, `GET /sbom/catch-up`
returns oldest-N in one call. **Done 2026-08-22.**
2. `SBOM_NEXUS_URL` reachable from the railiance01 worker.
3. ACTIVITY-WP-0030-T02 ingest side-effect implemented and dry-run proven.
4. `weekly-sbom-staleness` confirmed off in source **and** production
(ACTIVITY-WP-0030-T03).
5. Project into `k8s/railiance/20-runtime.yaml`, `enabled: true`, sync
schedules, capture evidence via `./scripts/prod_automation_status.sh`
(ACTIVITY-WP-0030-T04).