--- 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).