Closed general

HERO_DECLARATION_DANGLING false positive: repo-existence check misses a file the files API lists (adom/adom-cli, screenshots/hero.png)

Colby Knox · 1mo ago ·closed by Colby Knox

Hit during the adom/adom-cli 4.1.3 publish (2026-09-09). The republish warning said: 'the page's own hero (screenshots/hero.png) exists in neither the tarball nor the page repo: nothing can compose the hero, and the page header is broken until it is restored'. But: page.json declares hero {type: image, path: screenshots/hero.png}; GET /api/pages/adom/adom-cli/files?path=screenshots lists BOTH '._hero.png' and 'hero.png'; a fresh repo clone contains the file. Meanwhile GET /api/pages/adom/adom-cli serves hero: null, so the header presumably really is uncomposed even though the source file is right there.

Two suspects worth checking in the #183 gate's repo-existence lookup: the macOS AppleDouble sibling '._hero.png' sitting beside the real file (a prefix or glob match could trip on it), or the check reading a different tree/ref than the files listing. Tarball-absence is correct and expected (media is excluded from tarballs by design), so the bug is specifically the 'nor the page repo' half, and the null hero suggests composition uses the same broken lookup.

Repro without touching anything: inspect adom/adom-cli as it stands now. If the AppleDouble file is the trigger, a cleanup sweep for ._* files in repos might be worth pairing with the fix.

2 Replies

Colby Knox · 1mo ago

Root cause found, and it is mine from last night. The gate's repo check is readFileBinaryAsync, and since git-wiki 57541b8 (2026-09-08 20:49 UTC) small blob reads are cached per (repo, HEAD commit, path) so a publish stops reading README and page.json three times over. That cache remembered EVERY null result, and a read returns null for more reasons than 'no such path': a refused semaphore slot when git is saturated (GIT_BUSY), a timeout, a lost worker. Each deploy last night restarted six replicas whose boot sweeps flooded git for minutes; one failed read of adom-cli's screenshots/hero.png in that window was then remembered as 'absent' until the repo's next commit, and your 4.1.3 publish hours later asked the same replica and got the cached answer. The file itself was always there: the files API and the hero endpoints stream by blob sha through a different path, which is why they kept working.

Fix (git-wiki, on staging now, prod after the gate): only git's own not-found answers (bad revision, does not exist in, not a valid object name) are cached; anything else returns null once and is asked again. Pinned by a test that refuses one of two concurrent reads with a one-slot semaphore and checks the next read succeeds.

The AppleDouble sibling is not involved: I cloned the page and ran every lookup the gate and the hero use against a bare copy with .hero.png present, and all of them resolve the real file (sha 3a1daec5, 988,677 bytes). A cleanup sweep for .* files is still reasonable hygiene, but it would not have changed this. The page API's hero: null is the one thing I have not yet tied to the same cause; tracing that next.

Colby Knox · 1mo ago

On prod (git-wiki d4257aa, rolled out 14:53 UTC): blob reads now remember only git's own not-found answers, so a busy or timed-out read can no longer be mistaken for a missing file at the next publish. Two closing notes. The 'hero: null' in the page API is not a field the API has ever emitted: it exposes hero_type, hero_path, hero_thumbnail and hero_provenance, all set for adom/adom-cli (image, screenshots/hero.png), and /api/pages/adom/adom-cli/hero serves 200 on every replica, so the header was composing from the real file throughout. And the AppleDouble sibling was not the trigger (every lookup resolves the real file with it present); a ._* cleanup sweep stays optional hygiene. Closing; reopen if the warning shows again on a republish.

Log in to reply.