feat: a Homebrew tap, because brew owns what it installs - #13
Merged
Conversation
paddock ships pre-compiled binaries and self-updates. homebrew-core rejects
both — a formula must build from source, and "software that updates itself
conflicts with Homebrew's version and upgrade management" — and casks are not
the escape hatch, since core rejecting a CLI does not make it cask-eligible.
So distribution is a personal tap, one command:
brew install lntvan166/paddock/paddock
Fully qualified, not `brew tap` then `brew install`: since Homebrew 6.0.0 a
non-official tap needs explicit trust, and the qualified name grants it for
this one formula in a single step.
`paddock update` now refuses inside a keg. This is not politeness. The
Homebrew prefix is USER-writable, so the rename(2) would have SUCCEEDED,
leaving `brew info paddock` reporting a version that is no longer the bytes on
disk and the next `brew upgrade` reverting the operator with neither side
saying anything. The existing "installed by a package manager" hint only fires
when rename FAILS, which under brew it does not — so the guard is proactive,
resolves realpath first (brew links <prefix>/bin at the keg, and execPath may
hand back either), and refuses before the download rather than after 83MB.
`--check` still reports, because it writes nothing, and names `brew upgrade
paddock` rather than the command that now declines.
`/Cellar/` is matched as a path SEGMENT, so every prefix is covered — including
a custom one, which enumerating the known three would have missed — while
`~/Cellars/…` is not a false positive.
The formula's checksums come from the release's own SHA256SUMS, so it cannot
disagree with the artifacts it points at, and it carries depends_on "herdr":
a tap formula may depend on a core one, so brew guarantees the thing paddock
is useless without. No version constraint, because the herdr check is
directional and core never moves backwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ReleaseBanner` printed `paddock update` to everyone. Under Homebrew that command now refuses by design, so the notice named an action that does not work — the same defect as a mislabelled Approve button, which CLAUDE.md rules out for exactly this reason. A control is labelled from what actually owns the install, never guessed. The install's owner is a fact the client cannot derive, so it rides the WS envelope beside `latestKnown`: `managedBy: "homebrew" | null`. A union rather than a boolean because the command differs per manager, so a second packager adds a case instead of a second field. `detectManagedBy` is where the realpath hop lives, extracted rather than inlined at the composition root because a boot-time expression in index.ts cannot be tested — and this one has a branch worth testing: brew leaves a symlink in <prefix>/bin and `process.execPath` may hand back either that or the keg. Resolved ONCE at boot, as a value rather than a getter, because the path of a running executable cannot change and `update` refuses to swap it. `managedBy` is REQUIRED on `HealthBody`, matching the reasoning already recorded there for `latestKnown`: an optional field lets a later edit drop it silently, and a phone would read the absence as "unmanaged" and print the wrong command again. Making it required turned that into the type error that found both wiring sites. Verified end to end, not only in units: a binary compiled into a Cellar path reports `managedBy: "homebrew"` on /api/health, refuses `update` through both the keg path and the <prefix>/bin symlink, leaves the binary byte-identical with no temp file behind, and still reports the new version under `--check`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Pushed
Two things worth noting for review:
Verified end to end rather than in units alone — a binary compiled into a Cellar path: Binary byte-identical afterwards, no
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
brew install lntvan166/paddock/paddock— one command, plus apaddock updatethat refuses to fight brew over the same binary.why a tap and not
homebrew/coreCore is closed on two independent counts, neither of which is effort:
Package-Acceptance-Policy.md).Acceptable-Formulae.md). That ispaddock update, exactly.Casks are not the escape hatch: "A rejection from homebrew/core does not by itself make the software eligible for homebrew/cask."
herdr, by contrast, is a core formula — source tarball,
rust+zigbuild deps, bottled by Homebrew's CI, ~32k stars. That is the template if paddock ever qualifies. Recorded indocs/decisions.mdso it isn't re-litigated.the bug this would have shipped
The Homebrew prefix is user-writable, so
paddock update'srename(2)over a keg succeeds.brew info paddockwould then report a version that is no longer the bytes on disk, and the nextbrew upgradewould revert the operator — silently, both directions. The existing"installed by a package manager"hint only fires whenrenamefails, which under brew it never does.So the guard is proactive: resolve
realpathfirst (brew links<prefix>/binat the keg, andprocess.execPathmay return either), then refuse before the download rather than after 83MB.--checkstill works — it writes nothing — and namesbrew upgrade paddockinstead of the command that now declines./Cellar/is matched as a whole path segment: every prefix is covered including a custom one, while~/Cellars/…andwine-cellar/are not false positives.the formula
SHA256SUMS, so formula and artifacts cannot disagree.depends_on "herdr"— a tap formula may depend on a core one, so brew guarantees the thing paddock is useless without. No version constraint: the herdr check is directional and core never moves backwards.scripts/render-formula.tsfrom a template that declares its own platforms, so a platform change is a template edit and the script doesn't move. A missing asset throws naming all missing ones.verification
make check·make check-clean·make test→ 1050 pass, 0 fail. 16 new tests, each watched fail first. Two caught real mistakes: a workflow comment containing the literal|| trueit forbids, andcheck-cleancatching absolute home paths in test fixtures (content fixed, not the scanner).Rendered formula verified
ruby -c→ Syntax OK, with correct per-platform digests.needs, before this is useful
lntvan166/homebrew-paddock— created, public.HOMEBREW_TAP_TOKEN— set. First exercised by the next release; the tap step runs after the upload, so a bad token fails the job after the binaries ship rather than silently.known gap, deliberately not in this PR
ReleaseBanner.tsxstill tells every operator to runpaddock update, which now declines under brew. Fixing it needs a field throughroutes.ts→src/shared/types.ts→ store →App.tsx→ component — six files across the full dependency chain, including the one payload contract. Kept separate.🤖 Generated with Claude Code