fix(controlplane): emit an audit event when a project version is promoted to latest - #3317
Conversation
…oted to latest A project version becomes the latest one for its project in two ways: it is created as latest, or an already existing version is promoted. Only the first was reported. When an attestation promoted an existing version, the promote branch left the creation result unchanged and nothing was dispatched. Consumers that reconcile off these events never learned the pointer had moved, and kept following the previously latest version indefinitely, with no path to recovery since nothing re-checks afterwards. Report the promotion back from the repository and dispatch ProjectVersionUpdated carrying a new marked_as_latest field, both from the attestation path and from ProjectVersionUseCase.MarkAsLatest, which had no auditor at all. A promotion that does not move the latest pointer stays silent so the audit log is not filled with no-ops. The promotion contract, meaning the row-lock requirement, the released-version validation and whether the pointer actually moved, now lives in the single helper both paths already call rather than being recomputed at each call site. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 82261846-ded9-4e52-8707-d8a769402b5a
AI Session Checks — 🔴 25% ·
|
| Avg score | Sessions | Failing policies | Attribution | Files | Lines | Total Duration |
|---|---|---|---|---|---|---|
| 🔴 25% | 1 | 93% AI / 7% Human | 15 | +594 / -46 | 15h42m11s |
🔴 25% — 93% AI — ⚠️ 1 policies failing
-
Jul 30, 2026 15:42 UTC · 15h42m11s · $104.33 · 18.4k in / 681.7k out · claude-code 2.1.220 (claude-opus-5)
Change Summary
-
- Emits
ProjectVersionUpdatedwhen promotions move a version to latest, from both attestation andMarkAsLatestpaths. - Extends the audit payload and descriptions with
marked_as_latest, plus new goldens and audit tests. - Refactors promotion handling into shared helpers and adds broader unit, integration, and end-to-end verification.
- Emits
AI Session Overall Score
-
🔴 25% — Strong verification, but repeated misalignment and user corrections make this reviewer-check PR.
AI Session Analysis Breakdown
-
🟢 92% · verification
-
🟢 New tests were watched failing first, then passing after the fix. · High Impact
🟢 The final pass reran unit, integration, and fresh-stack UI-backed E2E. · High Impact
🟢 84% · solution-quality
-
🟢 The root cause was isolated with a controlled binary A/B experiment. · High Impact
🟡 72% · scope-discipline
-
🟠 The code also changed the unreachable
MarkAsLatestpath as regression-proofing beyond the reported bug. · Medium Severity💡 Surface prophylactic sibling-path fixes for approval before bundling them into a customer bug fix.
🔴 35% · context-and-planning
-
🔴 A multi-repo code, lab, UI, and PR session ran without a visible plan until late workflow prompts. · High Severity
💡 For multi-axis tasks, write a short shared plan before editing, reproducing, or filing follow-up work.
🔴 32% · alignment
-
🔴 AI opened new workflow branches before checking core premises, including an issue later admitted wrong and cancelled. · High Severity
💡 Before opening PRs or follow-up issues, verify the premise and ask for approval at each workflow boundary.
🔴 28% · user-trust-signal
-
🔴 User corrections clustered around process and verification, including 'don't open the PR' and 'NO ASSUMPTIONS'. · High Severity
💡 When corrections start clustering, stop and force a restated plan instead of continuing with incremental redirects.
-
File Attribution
██████████████████░░93% AI / 7% Human
Policies (4, 1 failing)
Status Policy Material Messages ✅ Passed ai-config-ai-agents-allowedai-coding-session-822618- ✅ Passed ai-config-no-dangerous-commandsai-coding-session-822618- ⚠️ Failedai-config-no-secretsai-coding-session-822618- Potential secret (JWT) found in session content [turn=523, source=tool_result, line=17, value=eyJhbGci...izJA]
- Potential secret (JWT) found in session content [turn=527, source=tool_result, line=4, value=eyJhbGci...wado]
- Potential secret (JWT) found in session content [turn=605, source=tool_result, line=1, value=eyJhbGci...rzGI]
- Potential secret (Quoted API key/password) found in session content [turn=513, source=tool_result, line=74, value=secret: ...mV0"]
- Potential secret (Quoted API key/password) found in session content [turn=523, source=tool_result, line=17, value=token = ...zJA']
✅ Passed ai-config-mcp-servers-allowedai-coding-session-822618- -
Powered by Chainloop and Chainloop Trace
The comment claimed the platform builds its "project version mark-latest" command and service endpoint on top of this method. That is not the case: the platform implements its own mark-latest over its own repositories and does not reference this use case at all. The inaccuracy is not harmless. It led a review of the surrounding change to treat these methods as a live customer-facing path and to file follow-up work on that basis. State what is actually true instead: nothing calls this today, and it is kept as the biz-layer entry point with the promotion event wired so a future caller cannot silently reintroduce the tracking gap. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 82261846-ded9-4e52-8707-d8a769402b5a
Problem
A project version becomes the latest one for its project in two ways: it is created as latest, or an already existing version is promoted. Only the first emitted an audit event.
When an attestation promoted an existing version, the promote branch left the creation result unchanged and nothing was dispatched. Consumers that reconcile off these events never learned the latest pointer had moved, so anything following the latest version of a project kept pointing at the previous one indefinitely. Nothing re-checks afterwards, so the state never healed.
Changes
WorkflowRunRepo.Createreports the promotion back through a newVersionPromotedflag on its result.WorkflowRunUseCase.CreatedispatchesProjectVersionUpdatedwhen a promotion happened, alongside the existingProjectVersionCreatedfor newly created versions.ProjectVersionUseCase.MarkAsLatestgained the auditor dependency and dispatches the same event. It previously had none, so that path was silent too.ProjectVersionUpdatedcarries a newmarked_as_latestfield, always emitted, so consumers can distinguish a promotion from a rename. Its description reflects the promotion.ProjectVersionUseCase.MarkAsLatestis corrected. The platform implements its own and does not reference this use case.A promotion that does not move the latest pointer emits nothing, so re-attesting the version that already is the latest stays silent.
Notes for reviewers
Promotions deliberately reuse the
ProjectVersionUpdatedaction type rather than introducing a dedicated one. Subjects are published asaudit.<target_type>.<action_type>and consumers subscribe to a fixed list of them, so a new action type would land on a subject nobody is listening to and every promotion would be dropped, which is the failure this event exists to prevent. If the action type ever changes, the consumers must subscribe to the new subject and be released first. This is documented at the dispatch site.The
ProjectVersionUseCasemethods touched here have no callers today, in this repository or in the platform. The change toMarkAsLatestis regression-proofing for the biz-layer entry point, not a fix to a live path; the attestation path is the reachable one.Known limitation, pre-existing and not addressed here: concurrent promotions of two different versions of the same project are not serialised against each other, since the row lock covers only the version being promoted. Closing that needs a lock on the project row or a unique partial index on
(project_id) WHERE latest.AI disclosure
This contribution was produced with the assistance of Claude Code. Every commit carries an
Assisted-by: Claude Codetrailer.