Public: anyone can read this; the crew can change it.
GET /api/v1/users/:username/repos carries an ETag and answers a repeat with 304 when none of that person's repositories it lists has changed #9154
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/users/:username/repos` now carries a weak ETag and answers a matching `If-None-Match` with an empty 304.
shipped production is serving it deploy b070674
GET /api/v1/users/:username/repos now carries a weak ETag and answers a matching If-None-Match with an empty 304.
What changed
- Owner filter:
ListReposOptionsinsrc/core/repos.tsgains anownerfield, whichvisibleClauseapplies.visibleReposMarkertherefore takes the owner filter directly, and there is no second marker helper. listReposByOwner: it now callslistReposVisibleTowith that owner, which removes a hand-copied duplicate of the same WHERE-clause builder. The rows it returns are unchanged.- Route in
src/http/api.ts: the tag is built from the owner id, the viewer (oranon), the limit, the cursor and the marker. Responses carrycache-control: private, no-cacheandvary: authorization, cookie. The marker is computed in atagphase, and a match returns 304 beforelistReposByOwnerruns, so a 304 has noreposphase. An unknown or hidden user is still a 404 with no tag, because that check runs first. - OpenAPI: the route summary now names the ETag,
If-None-Matchand the 304. - Tests:
test/user-repos-etag.test.tscovers each case the issue lists. I did not run it against main to confirm each case fails there, but the tag, 304 andtag-phase assertions can only pass with this change. - Existing test changed:
test/api-personrepos-timing.test.tsexpects the phase ordersession, tag, repos, rendernow. The old order was correct only before this change.
Checks: tsc --noEmit is clean. The new ETag test, the existing repos-ETag test, the timing tests, contract.test.ts, openapi-coverage.test.ts and admin-repos.test.ts pass. I did not run the full suite: scripts/fast.ts could not name a bounded set (it reported 1775 of 2066 test files), so the forge's check on the push is the full run. The output of scripts/gate.ts was cut off, so I can't say it passed. The full suite and gate run once in the forge's check on this push.
Not done: the marker covers only the listed page, as /api/v1/repos does, so a change to a repository on a later page doesn't alter this page's tag.
Try it: curl -i $URL/api/v1/users/alice/repos for the etag, then repeat with -H 'if-none-match: <tag>' to get the 304.
🤖 Generated with Claude Code
GET /api/v1/users/:username/repos now carries an ETag. A repeat request that sends it back gets a 304 until one of that owner's repositories changes (#9147).
The check failed only because no commit in the pull had a Try: line, which the gate requires when src/http/api.ts changes. I added an empty commit with that line and pushed it. I didn't run the gate, tsc or the tests, because no code changed. I haven't seen the forge's check result for the new push.
Try: curl -si -H "authorization: Bearer $FORGE_TOKEN" https://lab--9154--forge-trevato-apps.open-platform.sh/api/v1/users/trevato/repos. Then repeat it with If-None-Match set to the returned ETag and expect a 304.
🤖 Generated with Claude Code
Clean and small: owner filter folded into visibleClause, listReposByOwner now shares it (duplicate SQL removed), marker reused, 304 returned before the repos phase, tag keyed on owner id, hashed viewer, page and marker, 404 precedes the tag. Tests cover each case. Page-only marker is the same trade-off as /api/v1/repos.
Left #9147 open: 1 unticked item remain (Tests in test/user-repos-etag.test.ts: a repeat is a 304; a new repository under that owner makes the next answer a 200 with a new tag; a push changes the tag; that owner's repository turning private changes an anonymous viewer's tag; a repository created under a different owner does not change it; the owner and a stranger never share a tag. Each fails on main.).
GET /api/v1/users/:username/reposnow carries a weak ETag and answers a matchingIf-None-Matchwith an empty 304.What changed
ListReposOptionsinsrc/core/repos.tsgains anownerfield, whichvisibleClauseapplies.visibleReposMarkertherefore takes the owner filter directly, and there is no second marker helper.listReposByOwner: it now callslistReposVisibleTowith that owner, which removes a hand-copied duplicate of the same WHERE-clause builder. The rows it returns are unchanged.src/http/api.ts: the tag is built from the owner id, the viewer (oranon), the limit, the cursor and the marker. Responses carrycache-control: private, no-cacheandvary: authorization, cookie. The marker is computed in atagphase, and a match returns 304 beforelistReposByOwnerruns, so a 304 has noreposphase. An unknown or hidden user is still a 404 with no tag, because that check runs first.If-None-Matchand the 304.test/user-repos-etag.test.tscovers each case the issue lists. I did not run it against main to confirm each case fails there, but the tag, 304 andtag-phase assertions can only pass with this change.test/api-personrepos-timing.test.tsexpects the phase ordersession, tag, repos, rendernow. The old order was correct only before this change.Checks:
tsc --noEmitis clean. The new ETag test, the existing repos-ETag test, the timing tests,contract.test.ts,openapi-coverage.test.tsandadmin-repos.test.tspass. I did not run the full suite:scripts/fast.tscould not name a bounded set (it reported 1775 of 2066 test files), so the forge's check on the push is the full run. The output ofscripts/gate.tswas cut off, so I can't say it passed. The full suite and gate run once in the forge's check on this push.Not done: the marker covers only the listed page, as
/api/v1/reposdoes, so a change to a repository on a later page doesn't alter this page's tag.Try it:
curl -i $URL/api/v1/users/alice/reposfor theetag, then repeat with-H 'if-none-match: <tag>'to get the 304.🤖 Generated with Claude Code
Closes #9147