Skip to content

D199 — The Cask's install steps are postflight_steps; the next sync repairs the service

Status: implemented (2026-09-12). Amends D121 for the Homebrew path; supersedes D192's finding that the Cask deprecation was not fixable here.

Context. Every brew install/upgrade printed Calling postflight is deprecated! Use postflight_steps instead. It is an odeprecated, which Homebrew's normal cycle turns into an odisabled — at which point the Cask no longer loads and every upgrade fails. D192 recorded it as upstream's: GoReleaser (2.18.1, and its main at the time of writing) still renders homebrew_casks[].hooks.post.install as postflight do. That is true of hooks, but custom_block is GoReleaser's own escape hatch for stanzas its template does not model, and it renders verbatim at the top of the Cask.

The replacement is not a rename. postflight_steps is a declarative DSL (run, move, symlink, …) executed in Homebrew's sandbox: HOME points at a temporary directory, reads of the real home are denied and network access is off unless a step asks for it. upgrade-repair needs all three — the service definition in ~/Library/LaunchAgents, the client's .cartographer.yaml and every provider's configuration, launchctl, and /health on loopback. Punching those holes (network_access, the real HOME, a writable_paths entry per provider directory) was rejected: the list would have to track every provider added, and launchctl inside the sandbox is unverified.

Decision. - The Cask declares only the quarantine removal, as postflight_steps written through custom_block (run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{staged_path}}"]), and sets no hooks. Verified by installing the generated Cask from a local tap: no deprecation warning, the step runs inside the sandbox. - The repair moves into the binary: cartographer sync — the session-start hook, the scheduled timer or a manual run — first replaces a native service still running the previous binary, with the same Manager.Replace as upgrade-repair (graceful SIGTERM, bounded /health + version proof), then syncs as usual. It acts only on unambiguous evidence: an installed, running service whose program resolves to this same file (so a service running another binary — an install.sh copy next to the Cask, a dev build — is never restarted in a loop that cannot converge), reached over the loopback endpoint this client syncs against (D121's eligibility rule), answering /health ok with a different, non-empty version. An unreachable or unhealthy service, a dev build, a dry run: nothing happens and sync's own diagnostics apply. A failed replacement is a warning naming cartographer upgrade-repair; the sync proceeds. - It runs under the client-state lock (D172), so concurrent session-start syncs replace the service once: the next one in line finds it current. - install.sh update and upgrade-repair are unchanged: outside a sandbox the eager repair still works, and the lazy one then finds nothing to do.

Consequences. After brew upgrade the old process keeps serving until the next agent session start (or timer tick, or cartographer sync), which then pays a few seconds of drain and relaunch once. cartographer status keeps reporting the skew in between. The test-install guard asserts the new contract on the template's YAML (comments excluded): postflight_steps, the quarantine removal on the staged path, and no hooks, postflight do or upgrade-repair.