fix(github): keep the PR progress comment updating between operation dispatch waves - #1104
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a gap in SchemaBot’s GitHub progress-comment updates for operation-keyed applies: when the parent apply lease is intentionally unheld between dispatch waves, the comment observer can now still safely edit the tracked PR progress comment by claiming a durable, cross-pod “progress-comment authority” on the apply_comments row.
Changes:
- Add a durable, CAS-style progress-comment authority (owner + heartbeat) that gates GitHub side effects when no parent apply lease exists during operation-scoped work.
- Extend storage with
ClaimProgressCommentAuthorityand persist authority fields onapply_comments(MySQL + Postgres schemas). - Add unit + integration tests covering operation-scoped in-flight detection, single-winner authority behavior, and lease-held behavior remaining unchanged.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/webhook/comment_observer.go | Adds progress-comment authority fallback when parent apply lease is legitimately unheld; avoids freezing progress comments between dispatch waves. |
| pkg/webhook/comment_observer_test.go | Adds unit tests for determining when operation-scoped work is considered “in flight.” |
| pkg/webhook/comment_authority_integration_test.go | Adds integration tests to verify progress edits occur without a parent lease, and that concurrent observers converge on one editor. |
| pkg/storage/storage.go | Extends ApplyCommentStore interface and defines the staleness window constant for authority takeover. |
| pkg/storage/internal/sqlstore/apply_comments.go | Implements ClaimProgressCommentAuthority as a conditional update with stale takeover behavior. |
| pkg/storage/internal/sqlstore/apply_comments_test.go | Adds store-level tests for authority claim/renew/lose and stale takeover behavior. |
| pkg/schema/postgres/apply_comments.sql | Adds observer_owner and observer_heartbeat_at columns for Postgres schema parity. |
| pkg/schema/mysql/apply_comments.sql | Adds observer_owner and observer_heartbeat_at columns for MySQL schema parity. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…dispatch waves An apply whose work is dispatched under operation leases holds the parent apply lease only transiently per dispatch wave, so the comment observer's lease gate refused every progress edit between waves and the tracked PR progress comment froze for the life of the apply. Give the observer a durable, cross-pod-safe authority for exactly that shape: a conditional-update ownership claim recorded on the tracked progress comment row (observer_owner / observer_heartbeat_at), admitted only while operation-keyed work is in flight (undispatched manifest keys or non-terminal keyed operations), renewed on every admitted side effect, and transferable only after the holder's heartbeat goes stale — so two pods can never both believe they own the comment. Applies that hold a parent apply lease keep the lease as the sole authority, and every skipped edit still logs with the full triage attribute set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hority claim The claim also fails when no tracked progress comment row exists yet, not only when a peer observer holds it; the skip log must state both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f6bba1d to
a41fe14
Compare
|
🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1104, a41fe14. Verdict: 11 findings — no blockers; 4 non-blocking (1 deploy hazard, 2 invariant gaps, 1 coverage gap), 7 suggestions. Non-blocking
General suggestions
The one thing that could have broken, verifiedThe cross-pod CAS in Verified correct
This review was generated by Claude Code (claude-opus-5). |
…r's drive-mode split The authority admitted any non-terminal keyed operation, but a single-operation apply drives under the parent apply lease, so an unheld lease there means no driver and must stay fail-closed. The gate now requires the shapes that genuinely drive under operation leases: a multi-operation rollout with keyed work still running, or a generation manifest still expecting undispatched keys. The decision is made once per observer callback instead of once per side-effect check, and against freshly read rows: a re-read of the parent lease columns denies the authority when a dispatch wave re-claimed the parent (or the projection settled the apply terminal) after the poller's snapshot, so the authority holder never edits concurrently with a lease holder. One decision replaces the per-check operation scan and claim write, and renews the holder's heartbeat once per callback, well inside its staleness window. The expected peer-pod claim losses now log at debug rather than info, which widens the observer's logger interface with Debug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fresh-row claim semantics (first claim wins, same-owner renewal including identical values, peer loss against a live holder, per-apply independence) move into the storagetest parity suite so MySQL and PostgreSQL prove them from the same scenario. The aged-row stale takeover stays per-dialect: each suite backdates the holder's heartbeat with its own SQL and proves a peer takes over only past the staleness window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 All 11 findings addressed in bfe6d40 (gate rework) and 5295aea (cross-dialect tests). Per finding:
Verification: build + vet clean (plain and This reply was generated by Claude Code (Claude Fable 5). |
…t-dialect-classify * origin/main: fix(github): keep the PR progress comment updating between operation dispatch waves (#1104) fix(tern): classify materialized change DDL with the target dialect parser (#1187) fix(engine): resolve a cancel or stop that arrives before remote dispatch (#1184) fix: default connect and write timeouts on managed database connections (#1182) fix(storage): index the apply-operation claim ordering (#1180) fix(tern): generalize control resume state and complete cancels with no live engine work (#1179) fix(github): name each table's outcome in unsuccessful apply summaries (#1186) ci: peel tern and webhook into a dedicated integration shard (#1166) fix(engine): report a drained schema change's terminal outcome instead of pending (#1114) feat(serve): contain gRPC handler panics with recovery interceptors (#1164) feat(observability): tell operators when a log window hides older entries (#1185) fix(tern): settle sequential tasks when the engine loses in-flight work (#1113) fix(github): align PR comment severity glyphs with the shared vocabulary (#1135) fix(tern): release a database held by a stopped schema change (#1175) fix(plan): canonicalize drift DDL with the target's dialect parser (#1177) fix(e2e): stop injecting connection kills once the k8s pause is observed (#1178) # Conflicts: # pkg/webhook/templates/plan.go
…lassify' into kiran01bm/apply-comment-dialect * origin/kiran01bm/plan-comment-dialect-classify: fix(github): line-break non-MySQL DDL, schema labels for postgres fix(github): keep the PR progress comment updating between operation dispatch waves (#1104) fix(tern): classify materialized change DDL with the target dialect parser (#1187) fix(engine): resolve a cancel or stop that arrives before remote dispatch (#1184) fix: default connect and write timeouts on managed database connections (#1182) fix(storage): index the apply-operation claim ordering (#1180) fix(tern): generalize control resume state and complete cancels with no live engine work (#1179) fix(github): name each table's outcome in unsuccessful apply summaries (#1186) ci: peel tern and webhook into a dedicated integration shard (#1166) fix(engine): report a drained schema change's terminal outcome instead of pending (#1114) feat(serve): contain gRPC handler panics with recovery interceptors (#1164) feat(observability): tell operators when a log window hides older entries (#1185) fix(tern): settle sequential tasks when the engine loses in-flight work (#1113) fix(github): align PR comment severity glyphs with the shared vocabulary (#1135) fix(tern): release a database held by a stopped schema change (#1175) fix(plan): canonicalize drift DDL with the target's dialect parser (#1177) fix(e2e): stop injecting connection kills once the k8s pause is observed (#1178) # Conflicts: # pkg/webhook/templates/apply.go
Why this matters
Every GitHub side effect from the comment observer is gated on a valid parent apply lease. That matches the whole-deployment drive, where one driver holds the parent lease end to end — but operation-keyed applies claim the parent only transiently per dispatch wave and drive their operations under operation leases. Between waves no parent lease exists, so every progress edit was skipped fail-closed and the PR progress comment froze at its initial text for the entire apply. An operator watching the PR saw a stale comment while the apply was actively making progress — the worst kind of wrong, because the fail-closed skip looks identical to "nothing is happening."
What it does
Gives the observer a second, equally fail-closed way to earn the right to edit: a durable claim on the tracked comment's database row. The observer writes its own name onto the row with a conditional update — the write only succeeds if no other observer holds the row, or the holder has gone quiet past the staleness window. Exactly one observer across all pods wins at a time; everyone else sees the row is taken and skips. If the claim can't be confirmed for any reason, the observer skips the edit — it never guesses.
observer_owner,observer_heartbeat_at), renewed once per admitted callback, with takeover allowed only after the apply-lease staleness window. Losing the claim, or any storage uncertainty, skips the GitHub side effect and logs with the apply's triage attributes — never a spurious edit. The expected peer-pod losses log at debug; genuine uncertainty logs at error.Operational note
The PostgreSQL bootstrapper creates missing tables but does not alter existing ones, so an already-bootstrapped PostgreSQL storage database needs the new columns before this deploys — otherwise startup fails with
storage table "apply_comments" is missing expected columns:MySQL needs no action:
EnsureSchemadiffs and applies the addition on startup.How it fits
Operation-keyed dispatch is the fan-out foundation for multi-deployment and sharded applies, and those are exactly the applies whose progress comments matter most — long-running, many moving parts, watched from the PR. This makes the comment observer's authority model match the drive model: whichever shape holds the work, exactly one observer keeps the PR current, and uncertainty always resolves to silence rather than a wrong edit.
Opened by Claude (Fable 5).