docs(adr-0056): Scoop+Homebrew validated; record the Caddy HEAD fix
Amendment 3: scoop install and brew install both verified end-to-end on real Windows + macOS (install + run), resolving Amendment 2's unverified caveats (incl. the HEAD:main push). Records the root cause + fix for the Homebrew 403: Gitea (>=1.25) 303-redirects release downloads to a method-bound SigV4 presigned S3 URL; brew resolves with HEAD, captures the HEAD-signed URL, then GETs it -> 403 (GET-only tools are unaffected). Fixed by a server-side Caddy rule that answers HEAD on release-download paths directly (200, no redirect) so the download GET re-runs the redirect fresh. That rule lives in the Gitea edge config, NOT this repo - documented so a server rebuild doesn't silently break brew. Includes the reference Caddyfile block. Also notes the ad-hoc mac signature runs fine via brew; Developer-ID + notarization stays parked on the Apple org conversion. Remaining D3: winget.
This commit is contained in:
@@ -176,3 +176,65 @@ whether macOS Gatekeeper accepts the **ad-hoc-signed** mac binary via
|
||||
requirement and `brew`'s curl download sets no quarantine xattr, unlike a
|
||||
browser download — but this rides on the still-parked Developer-ID signing
|
||||
decision). **Remaining D3:** winget (komac on Linux CI, or a manual PR).
|
||||
|
||||
## Amendment 3 — 2026-06-19: validated end-to-end; a presigned-URL/HEAD gotcha + the Caddy fix it needs
|
||||
|
||||
**Scoop and Homebrew now install and run end-to-end** (`v0.2.0`,
|
||||
user-verified on real Windows + macOS): `scoop install rdbms-playground`
|
||||
and `brew install lazyeval/tap/rdbms-playground` both fetch, checksum,
|
||||
install, and the installed binary launches. Amendment 2's "unverified"
|
||||
caveats are resolved (the `HEAD:main` push populated both repos cleanly).
|
||||
|
||||
**Distribution depends on a server-side Caddy rule — RECORD THIS: it lives
|
||||
in the Gitea edge config, NOT this repo, and if it is lost in a server
|
||||
rebuild Homebrew silently 403s again.** Symptom: `brew install` failed with
|
||||
`curl (22) … 403` on the asset URL. Root cause: Gitea (≥1.25; here 1.26.2)
|
||||
serves release-asset downloads by **303-redirecting to a method-bound
|
||||
AWS-SigV4 presigned S3 URL** (OVH), signed for the *incoming* request's HTTP
|
||||
verb. Homebrew **resolves the URL with a HEAD, captures the returned
|
||||
(HEAD-signed) presigned URL, then runs the download GET against that
|
||||
captured URL** → GET-on-a-HEAD-signed-URL → 403. Reproduced precisely:
|
||||
`GET on HEAD-resolved = 403`, `HEAD on HEAD-resolved = 200`, `GET on
|
||||
GET-resolved = 200`, `HEAD on GET-resolved = 403`. GET-only tools
|
||||
(`install.sh`, `install.ps1`, `cargo binstall`, curl) are unaffected — they
|
||||
GET the original URL and let it redirect fresh to a GET-signed URL. This is
|
||||
a **Homebrew defect** (it reuses an ephemeral, method-scoped credential as
|
||||
if it were a durable resource locator); Gitea's per-request method-bound
|
||||
signing is correct, and SigV4 *cannot* sign one URL for two verbs.
|
||||
`SERVE_DIRECT=false` (Gitea proxies, no presign) would also fix it but was
|
||||
declined — not reshaping storage for one client.
|
||||
|
||||
**Fix (deployed, "Tier A"): a Caddy rule that answers HEAD on
|
||||
release-download paths directly** — 200, no redirect, no body — so brew's
|
||||
resolve records the *original* URL (no presigned credential captured) and
|
||||
its download GET runs through the redirect fresh → GET-signed → 200. Scoped
|
||||
to `method HEAD` + the release-download path; **GET is untouched**, so every
|
||||
working channel is unaffected, and a HEAD carries no payload so no client
|
||||
can *break* (at most a HEAD-probing download manager loses a progress-bar
|
||||
size — cosmetic). Reference Caddyfile (place before the Gitea
|
||||
reverse_proxy):
|
||||
|
||||
```
|
||||
@release_head {
|
||||
method HEAD
|
||||
path_regexp ^/[^/]+/[^/]+/releases/download/.+
|
||||
}
|
||||
handle @release_head {
|
||||
header Accept-Ranges bytes
|
||||
header Content-Type application/octet-stream
|
||||
respond 200
|
||||
}
|
||||
```
|
||||
|
||||
A "Tier B" variant (a sidecar that fetches and returns the *real*
|
||||
Content-Length so even HEAD-probing clients stay fully faithful) was specced
|
||||
but proved unnecessary — brew is happy with the bare 200.
|
||||
|
||||
**macOS signing:** the brew-installed binary **runs** under the current
|
||||
**ad-hoc** signature (`codesign --sign -`) — confirmed on Apple Silicon.
|
||||
**Developer-ID signing + notarization remains parked** pending the user's
|
||||
Apple account conversion to an Organization (GA plan / ADR-ci-003); it is
|
||||
needed for *browser-download* trust, not for the package-manager paths,
|
||||
which are all working now.
|
||||
|
||||
**Remaining D3:** winget only.
|
||||
|
||||
Reference in New Issue
Block a user