Skip to content

fix: render private repository file images in Markdown - #1038

Merged
wesm merged 2 commits into
mainfrom
image-issue
Sep 4, 2026
Merged

fix: render private repository file images in Markdown#1038
wesm merged 2 commits into
mainfrom
image-issue

Conversation

@wesm

@wesm wesm commented Sep 4, 2026

Copy link
Copy Markdown
Member

Pull request and issue descriptions in private repositories often embed screenshots that are committed to the repository and linked as https://github.com/{owner}/{repo}/blob/{branch}/docs/image.png?raw=true. The browser cannot load that URL without a GitHub session, so kenn-forge showed a broken image where GitHub shows the screenshot. Only user-attachments uploads went through the authenticated image proxy.

  • Images committed to the repository, linked through blob or raw web URLs or raw.githubusercontent.com, now render through the repo-scoped image proxy.
  • Files in other repositories keep loading directly, so public cross-repository images behave as before.
  • Branch names with slashes resolve correctly even though the web URL does not separate the branch from the file path.

Generated with Claude Code

Pull request and issue descriptions often embed screenshots that are committed to
the repository and linked through a GitHub web URL such as
`https://github.com/{owner}/{repo}/blob/{branch}/docs/image.png?raw=true`. For a
private repository the browser cannot load that URL: the image request carries no
GitHub session, so GitHub answers with a 404 page and the browser shows a broken
image. Only `user-attachments` uploads went through the authenticated image proxy.

The proxy now also serves files from the route's own repository, referenced by
`blob` or `raw` web URLs on the platform host or by `raw.githubusercontent.com` on
github.com. The daemon reads them through the contents API with the normal read
credential chain. Files in other repositories keep loading directly, because a
public repository's file already works in the browser and the route's credential
is only known to be valid for its own repository.

Web URLs do not separate the branch name from the file path, and branch names may
contain slashes, so the daemon tries the shortest ref first and moves past 404s
until the file resolves. The contents API labels every raw response with its own
media type, so the image type is sniffed from the bytes before it is served.

Generated with Claude Code (claude-fable-5-1)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Sep 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (08c45ff)

Verdict: Two medium-severity caching issues require attention.

Medium

  • internal/server/markdown_images.go:70 — Repository images may reference mutable branches or tags, but the endpoint uses a one-year immutable cache policy, causing updated images to remain stale. Use short-lived revalidation for mutable refs or resolve/cache by immutable commit SHA.

  • internal/server/markdown_images.go:62 — The cache key omits stable repository identity and uses only the mutable route and source URL. If a repository is deleted and its owner/name is reused, cached private image bytes could be served to the replacement repository. Include the stable provider or internal repository ID in the cache key.


Reviewers: 2 done | Synthesis: codex, 8s | Total: 8m5s

Repository file images are addressed by a branch or tag, so the bytes behind one
URL can change when the branch moves. The image route told browsers to keep every
proxied image for a year as immutable, and the daemon kept it on disk for two
weeks, which was only correct for content-addressed attachment uploads. Providers
now mark ref-addressed images as mutable, and the route caches those for five
minutes in the browser and on disk while attachments keep the long immutable
policy.

The disk cache was keyed by the owner/name route. Owner/name is a mutable route
in kenn-forge: a repository can be deleted and a different one created under the
same name, and the catalog treats that as a distinct repository. With the old key
the replacement repository would have been served the previous repository's
private image bytes for up to two weeks. The key now uses the stable provider
repository ID, matching the identity rule the rest of the catalog follows. The
cache file format gains a mutability line, so entries written by earlier builds
are treated as misses and fetched again.

Generated with Claude Code (claude-fable-5-1)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@wesm

wesm commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Both review findings are addressed in 3a365dc.

  • Repository file images are now marked mutable by the provider. The route serves them with Cache-Control: private, max-age=300 and the disk cache expires them after five minutes; attachment uploads keep the long immutable policy.
  • The disk cache key now uses the stable provider repository ID instead of the owner/name route, so a replacement repository at a reused route never receives the previous occupant's bytes. A test replaces the route occupant and checks that the second repository fetches its own image.

The failed go test -race job is TestWorkspaceRuntimePlainShellTerminalDeliversActualExitCodeE2E in the workspace PTY suite, which this branch does not touch. It failed on both attempts of that run and passed in the other recent runs.

@roborev-ci

roborev-ci Bot commented Sep 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (3a365dc)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 11m22s

@wesm
wesm merged commit 5c3fd38 into main Sep 4, 2026
17 checks passed
@wesm
wesm deleted the image-issue branch September 4, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant