docs: record root cause and live verification of Bug #2

Bug #2 (ls-remote vs fetch disagreeing on the VPS) turned out to be two
separate things, neither a bug in the fetch fix itself: a stale,
unrelated system-wide git-remote-rad binary being silently used in
ad-hoc manual testing (PATH had no ~/.radicle/bin), plus a genuine
cross-device rename bug in rad-lfs-transfer's download path (fixed
separately, ac4a27c) that only surfaced once the real pipeline actually
ran with the fetch fix for the first time. Verified live: triggered
blog-deploy.service for real, full success end-to-end, SHA-256-verified.
This commit is contained in:
Maciek "mab122" Bator 2026-07-14 18:53:45 +02:00
parent a97484c92f
commit 945da6f7f8
1 changed files with 154 additions and 2 deletions

View File

@ -2,8 +2,18 @@
**RESOLVED** (2026-07-14). Fix: `refs/notes/rad-lfs` is now exposed per-peer
(`refs/notes/rad-lfs/<peer-id>`) instead of expecting one canonical bare ref, matching how
patch refs already work — see "Fix actually shipped" at the bottom for the full writeup and
verification. Kept the original report below unedited for context.
patch refs already work — see "Fix actually shipped" for the full writeup and verification.
**Follow-up "Bug #2" also RESOLVED** (2026-07-14, same day) — turned out to be two separate
things, neither a bug in the fix above: a stale, unrelated system-wide `git-remote-rad`
install being silently used in manual ad-hoc testing (not a real pipeline issue), plus one
genuinely new bug (a cross-device `rename()` failure on LFS download) found only once the
real pipeline actually ran with the fix. See "Root cause of Bug #2, actually found" at the
very bottom for the full story — verified live against the production deploy pipeline on
mbator.pl, which completed successfully end-to-end.
Kept the original report and the mid-investigation "Follow-up" section below unedited, for
context on how the debugging actually went.
Found while deploying `rad-lfs-ipfs` to mbator.pl for the blog pipeline (2026-07-14).
Blocks the whole LFS-over-IPFS flow end-to-end, not just a cold-start edge case.
@ -114,6 +124,65 @@ Re-run `git fetch rad` in `/home/blog/repo` on the VPS (as `blog`) — if the fi
the notes ref, that fetch (and the LFS smudge it triggers) is the last remaining piece to
confirm the whole IPFS content-fetch path end-to-end.
## Follow-up: fix deployed, uncovered a second, more specific bug (2026-07-14, same day)
Rebuilt both the local (`~/Blog/Blog`) and VPS (`/home/blog/repo`) checkouts against
`ff762ef8` (the fix + the interactive-passphrase commit) and re-ran `rad lfs init` to pick
up the migrated wildcard refspec (`+refs/notes/rad-lfs/*:refs/notes/rad-lfs/*`; the old
exact-match entry needed manual removal once — `git update-ref -d refs/notes/rad-lfs` — on
the author's machine where a stale *local* plain note ref pre-existed and D/F-conflicted
with the incoming namespaced one; the `remove_refspec_if_present` migration in `init.rs`
correctly stripped the *refspec config*, but doesn't (and structurally can't, from
`init.rs`) also clean up a ref that already exists on disk from before).
**On the author's machine**: `git fetch rad` now genuinely works — pulls
`refs/notes/rad-lfs/<peer>` for real, exactly the repro case from the original bug.
Confirmed fixed there.
**On the VPS (`blog` user, second peer/node)**: same `rad lfs init` migration, but the
actual fetch behaves inconsistently:
- `git ls-remote rad` correctly lists the note:
`c4538c2d... refs/notes/rad-lfs/z6MkkphWkrBU3Buhh4ZN8Gtt9KLzn2DUsx3hZhhXirK7XAyn`
- `git fetch rad` (relying on the configured wildcard refspec) silently skips it — only
reports `master` as `[up to date]`, no attempt at the notes ref, no error.
- An **explicit** fetch of that exact literal ref name (copy-pasted straight from the
`ls-remote` output above) still fails outright:
```
$ git fetch rad refs/notes/rad-lfs/z6MkkphWkrBU3Buhh4ZN8Gtt9KLzn2DUsx3hZhhXirK7XAyn:refs/notes/rad-lfs/z6MkkphWkrBU3Buhh4ZN8Gtt9KLzn2DUsx3hZhhXirK7XAyn
fatal: couldn't find remote ref refs/notes/rad-lfs/z6MkkphWkrBU3Buhh4ZN8Gtt9KLzn2DUsx3hZhhXirK7XAyn
```
So `git-remote-rad`'s ref listing is inconsistent *between its own `list` output and what
`fetch` actually resolves against*, for the identical ref, in the identical process
invocation style — `ls-remote` and `fetch` are supposed to both go through the remote
helper's `list` verb (that's the whole point of `for_fetch()` being one shared function),
so seeing them disagree points at something conditional in how `list` is invoked (fetch
vs. ls-remote may pass different capabilities/args to the helper, e.g. a `list for-push`
vs. plain `list`, or some caching), rather than `for_fetch()`'s ref-selection logic itself
(which is presumably fine, given `ls-remote` gets it right).
Confirmed the underlying data is genuinely present and correct the whole time (verified
directly in `blog`'s node storage, bypassing git-remote-rad entirely):
```
$ git -C /home/blog/.radicle/storage/z2Z16c9C5BaqSQPfbVrGPGG68wAx3 for-each-ref | grep lfs
c4538c2d... commit refs/namespaces/z6MkkphW.../refs/notes/rad-lfs
```
and the note content itself has the right CID and correctly lists `blog`'s own node DID
(`z6Mkpqi56U6...`) as an authorized decrypt recipient. So this is purely a
`git-remote-rad` transport/listing bug, not a storage/replication/encryption issue.
**Not yet root-caused** — flagging with the concrete repro above rather than guessing
further at the Rust internals blind. Two machines, two outcomes, same binary version, same
refspec, same underlying data: something about the *fetch* invocation path differs from
the *ls-remote* invocation path in a way that changes what the helper's `list` returns or
how the client matches against it. Worth checking: does `list.rs` (or whatever calls
`for_fetch()`) get invoked with different arguments for `list` vs. `list for-push` vs. a
fetch-triggered `list`, and does the *local* `remote.<rad>.fetch` refspec (not just the
server-side listing) factor into which lines the helper actually returns — i.e. is there a
second, client-driven filtering step somewhere that behaves differently between the two
call sites?
## Fix actually shipped
Went with the "simplest" option from the list above, since it's also the semantically
@ -189,3 +258,86 @@ problem in practice. Worth a real fix at some point (e.g. `rad lfs init` also en
explicit `+refs/heads/*:refs/remotes/rad/*`-equivalent push default is present, or documenting
"always `git push rad <branch>` explicitly" as a hard requirement), but out of scope for this
specific fetch-bug fix.
## Root cause of Bug #2, actually found (2026-07-14, same day, later)
Got shell access to the VPS (`ssh mbator.pl`, passwordless sudo in a real tty) and reproduced
directly rather than guessing further at the Rust internals. It was **two separate things**,
not one — neither of them a bug in the fetch fix above.
### Part 1: not a code bug at all — a stale, unrelated binary
`sudo -u blog bash -c '...'` (a bare, non-login shell — the same style of ad-hoc command used
for all the manual reproduction so far) resolves `PATH` to the plain system default
(`/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`), which has **no**
`~/.radicle/bin` in it. `which rad` in that context returns `/usr/local/bin/rad` — version
`1.8.0`, a completely unrelated, root-owned, March-2026 system-wide install, nothing to do
with this fork. Confirmed directly:
- With `PATH=/home/blog/.radicle/bin:$PATH` explicitly set: `git ls-remote rad` *and*
`git fetch rad` both correctly see and fetch `refs/notes/rad-lfs/<peer>` — full
`GIT_TRANSPORT_HELPER_DEBUG=1` trace shows `list` correctly advertising it and git correctly
issuing `fetch <oid> <ref>` for it, ending in `[new ref] refs/notes/rad-lfs/<peer>`.
- With the bare/default `PATH` (resolving to `/usr/local/bin/git-remote-rad`, the unrelated old
binary): `git fetch rad` produces **zero output at all** — exactly the silent-skip symptom
originally reported, byte for byte.
So `list`/`for_fetch()` never actually disagreed between `ls-remote` and `fetch` for the same
binary — the two commands were, at some point during manual testing, silently invoking two
*different* binaries. The real automated pipeline was never actually at risk from this: all
three blog-owned systemd units (`radicle-node-blog`, `blog-radicle-watch`, `blog-deploy`)
already have `Environment=...PATH=/home/blog/.radicle/bin:...` correctly set. Confirmed via
`systemctl cat` on all three. This was purely a manual-testing artifact.
### Part 2: a real, new bug — but only surfaces once you get past Part 1
`blog-deploy.service`'s last run before the fork rebuild had failed with the *original*
Bug #1 error (timestamped *before* the 16:1016:14 UTC rebuild/restart) — meaning the real
pipeline, with its correct `PATH`, hadn't actually run even once against the fetch fix yet.
Triggering it manually (`sudo systemctl start blog-deploy.service`) surfaced a genuinely new
failure:
```
Downloading assets/_infra-test/lfs-ipfs-smoketest.jpg (692 B)
Error downloading object: ...: failed to copy downloaded file: cannot replace
".../ .git/lfs/objects/c5/e3/c5e3b0c1..." with "/tmp/rad-lfs-c5e3b0c1...":
rename /tmp/rad-lfs-c5e3b0c1... .../.git/lfs/objects/c5/e3/c5e3b0c1...: invalid cross-device link
```
The download and decryption had already succeeded by this point (692 B, matching exactly) —
this was purely the final handoff to git-lfs failing. Root cause:
`radicle-lfs-transfer/src/main.rs`'s `download_inner` reported a path under
`std::env::temp_dir()` (`/tmp`) back to git-lfs as the "complete" event's path. Git-lfs then
does an *atomic* `rename()` from that path into `.git/lfs/objects/...` — it does not fall back
to a copy if that fails. On this VPS, `/tmp` and the repository are on different
filesystems/mounts, so the rename hits `EXDEV` ("invalid cross-device link") and git-lfs just
errors out rather than retrying with a copy.
Fixed in `radicle-lfs-transfer` (commit `ac4a27c`): resolve `.git/lfs/tmp` (via
`git rev-parse --git-dir`, so this works from a subdirectory too, not just the repo root) and
download there instead, mirroring git-lfs's own existing convention for its own internal temp
downloads — guaranteed same-filesystem by construction, not by hoping `/tmp` happens to line
up. This does reintroduce a `git` binary runtime dependency for `rad-lfs-transfer` (previously
dropped once it stopped needing to read/write notes directly) — an acceptable tradeoff, since
anything using `rad`/git-lfs at all already requires git to be installed anyway. Submodule
pointer bumped in this repo at `a97484c9`.
### Verified live, for real, against production
Deployed the fix to the VPS (`blog` has no Forgejo SSH credentials, so copied the fixed
`main.rs` directly rather than provisioning new access; rebuilt in place with
`~/.cargo/bin`/`~/.radicle/bin` on `PATH`), then actually triggered `blog-deploy.service`:
```
● blog-deploy.service ... Active: inactive (dead) ...
Process: ExecStart=/home/blog/repo/deploy/deploy.sh (code=exited, status=0/SUCCESS)
[blog-deploy] deploying to /srv/mbator.pl...
[blog-deploy] deploy complete: 3f650d2d7e9accfba3468a885fc63984e18ceadb
```
Full success, end to end: `git fetch rad` pulled the notes ref for real, `git lfs pull`
downloaded and decrypted the private object, the site built, and it deployed. Confirmed the
checked-out asset's SHA-256 matches the original exactly
(`c5e3b0c1eb2d6e2f3ef98fe74e70fe431986dc2a401b1eae961e175b5a2164ca`). It doesn't appear under
`/srv/mbator.pl` because it's deliberately unreferenced by any post — expected, by the smoke
test's own design, not a bug.