feat(editor-bridge): Tier 5 — reconcile + mark-deployed (deploy-closure loop) + runbook fix - #218
Conversation
…ploy loop) + runbook fix Adds the merged→deployed loop closure for the editor → kayak_data bridge: - worker.reconcile() — advance pr_open rows by reading PR state: → merged (recording pr_merge_sha) or → pr_closed. A REST read failure escalates (exit non-zero) and leaves the row pr_open for the next pass. - worker.mark_deployed() — advance merged rows → deployed once the PR's merge commit is in the deployed --dataset-ref (ancestry), then resolve the parent change_request (SA-lite loop closer). No GitHub token needed. - git_ops.is_ancestor() — git merge-base --is-ancestor (0/1; raises on an unknown SHA so an absent commit is "not deployed", not a false positive). - CLI: `levels editor-bridge reconcile` + `mark-deployed --dataset-ref <sha> [--dataset-repo <path>]`. reconcile gated on enabled; mark-deployed isn't (it's a post-deploy hook needing only DB + local git). operations.md (the bundled doc fix): the merge gate is now a **ruleset with an admin bypass** — a blanket "require ≥1 approval" would deadlock a sole maintainer on their *own* dataset PRs (no self-approval), so bypass the admin while leaving the bot bound. And the key-install step owns the key by the **worker/service user, not www-data** (the trap a `0400` www-data-owned key falls into), with both an affirmative + negative read probe. Adversarial-review fixes: - M1: reconcile no longer transitions a PR that GitHub reports merged before its merge_commit_sha settles (async window) — a merged row is never re-read, so capturing a null SHA would strand it forever; it now waits in pr_open. - N1: mark_deployed always verifies via is_ancestor (reflexive), dropping the bare `==` short-circuit that could mark a row deployed against a checkout that never contained the commit. - N3: _resolve_parent is idempotent (no-op if the parent is already resolved), so a second/overlapping pass can't append the deploy note twice (full lease deferred to the enablement PR). Tests: reconcile (merged/closed/open/read-failure-escalates/merged-without-SHA- waits/multi-row), mark-deployed (exact/ancestor/unknown-sha/pr_merge_sha-None/ non-git-repo/idempotent-no-dup-note), is_ancestor, + CLI. 1932 green; ruff + mypy clean. Ships dormant — no timer, no schema/metadata change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018i8zX4iJDjh6nwLCYRVaRf
Adversarial review — Tier 5 (
|
|
Adversarial review at Findings:
Verification:
|
…-checkout requirement Review follow-up on #218 (both reviews: no new MAJOR; two real notes): P2 — mark-deployed's ancestry needs a real git checkout, but the paired-release deployer materializes the dataset as a `git archive`/`tar` snapshot at $RELEASE_DIR/dataset (no .git) and runs activation with DATASET_DIR pointed there. A deploy-hook invocation against that path would silently resolve nothing. The command already fails SAFE (GitOpError → row left merged → never a false resolve), but the no-op was indistinguishable from "no merged rows." Now cmd_mark_deployed counts the still-merged rows and prints "N merged row(s) but none resolvable in <repo> — point --dataset-repo at a git checkout fetched to the dataset ref", and operations.md spells out that --dataset-repo must be a git checkout (not the release snapshot) — the kayak-deploy hook (deferred to enablement) must point at a real clone. The manual-run default (DATASET_DIR = a clone) is unaffected. P3 — the deploy-close path doesn't email the proposer like the manual review.php close does. Deferred to the enablement PR (the bridge ships dormant; worker-side email is non-trivial); _resolve_parent's docstring now flags it. Test: cmd_mark_deployed against a non-git --dataset-repo with a merged row → exit 0 + the visible "none resolvable" message. 1932 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018i8zX4iJDjh6nwLCYRVaRf
|
Triage of the two review passes (fixes in P2 (mark-deployed vs the paired-release dataset path) — partly fixed here, rest deferred to enablement. You're right that the deployer materializes
The actual P3 (no proposer email on deploy-close) — deferred to the enablement PR. Correct: Net: #218 stays a clean, fail-safe, dormant install; the two findings are about the enablement wiring (hook git-source + notification), now documented and made visible. 1933 green. — Claude · Opus-4.8(1M) · x-high |
Adversarial review — update
|
|
Follow-up adversarial review at The updated commit addresses my earlier P2 for this dormant PR shape: The earlier P3 around proposer email is also now documented in I did not find new blocking issues in the update. Verification:
|
Disposition — both adversarial passes at
|
Tier 5 — close the merged → deployed loop
Completes the editor → kayak_data bridge: the worker (#217) opens PRs; this resolves them after they merge + deploy. Builds on #213–#217 (all merged).
reconcile— read PR state forpr_openrows →merged(recordpr_merge_sha) orpr_closed. A REST read failure escalates (exit non-zero) and leaves the rowpr_openfor the next pass.mark-deployed --dataset-ref <sha>— advancemergedrows →deployedonce the PR's merge commit is in the deployed ref (ancestry, so several PRs merged since the last deploy resolve from one deploy), and resolve the parentchange_request(the SA-lite loop closer). No GitHub token needed → suitable as a post-deploy hook.git_ops.is_ancestor—git merge-base --is-ancestor; an unknown SHA raises → treated as "not deployed" (never a false positive).Bundled
operations.mdfix (the merge-gate + key-owner corrections)www-data. Documents the exact trap hit during provisioning (a0400key owned bywww-datais readable by PHP-FPM), with an affirmative + negativesudo -u … catprobe.Adversarial self-review (fixed before this PR)
reconcileno longer transitions a PR that GitHub reportsmergedbefore itsmerge_commit_shasettles (documented async window) — since amergedrow is never re-read, capturing a null SHA would strand it forever (mark-deployed needs the SHA). It now waits inpr_open.mark_deployedalways verifies viais_ancestor(reflexive), dropping the bare==short-circuit that could mark a row deployed against a checkout that never contained the commit._resolve_parentis idempotent (no-op if the parent is alreadyresolved) so overlapping passes can't double-append the deploy note (a full lease is deferred to the enablement PR).Tests
reconcile (merged / closed / open / read-failure-escalates / merged-without-SHA-waits / multi-row); mark-deployed (exact / ancestor / unknown-sha / pr_merge_sha=None / non-git-repo degrades / idempotent no-dup-note);
is_ancestor(yes/no/reflexive/unknown-raises/option-guard); CLI. Full suite 1932 + ruff + mypy green.Scope / deploy impact
Ships dormant — no systemd timer, no schema/metadata change, no new dependency. Deferred to the enablement PR: the
queue(manual requeue) subcommand, the systemd timers, thekayak-deploypost-activationmark-deployedhook, and a worker lease/retry-backoff. The bridge is not in service until the #215 reviewed-base TOCTOU + M3 PHP fix lands and the GitHub App is provisioned (docs/operations.md).🤖 Generated with Claude Code
— Claude · Opus-4.8(1M) · x-high