Skip to content

Insert artifact domain paths after /v2/ in bottle URLs#23062

Open
jouve wants to merge 1 commit into
Homebrew:mainfrom
jouve:artifact-domain-oci-path
Open

Insert artifact domain paths after /v2/ in bottle URLs#23062
jouve wants to merge 1 commit into
Homebrew:mainfrom
jouve:artifact-domain-oci-path

Conversation

@jouve

@jouve jouve commented Jul 11, 2026

Copy link
Copy Markdown
  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • AI was used to generate or assist with generating this PR.

I used Claude Code to help draft the patch, tests and this PR text. I manually reviewed the changes and verified them end-to-end against a real Harbor 2.x proxy cache for ghcr.io (brew fetch and brew reinstall of bottles, plus brew vendor-install ruby for the Bash code path), and ran brew lgtm successfully.


What

HOMEBREW_ARTIFACT_DOMAIN rewrites ghcr.io bottle URLs by replacing the https://ghcr.io/ prefix with the configured value. When that value contains a path — e.g. a Harbor or Nexus pull-through proxy for ghcr.io, where the upstream is exposed as a project/repository prefix such as https://example.com/ghcr-io — the result is:

https://example.com/ghcr-io/v2/homebrew/core/hello/manifests/2.12.3

No OCI registry can serve this: the OCI Distribution Spec requires /v2/ at the root of the host, with any prefix acting as part of the repository name.

This PR inserts the artifact domain's path after /v2/ instead:

https://example.com/v2/ghcr-io/homebrew/core/hello/manifests/2.12.3

Artifact domains without a path keep the exact same behaviour as today, as does the fallback to the original ghcr.io URLs (and HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK). The same logic is applied to the portable-ruby URL in vendor-install.sh, with cross-referencing comments to keep both implementations in sync.

Why

This makes HOMEBREW_ARTIFACT_DOMAIN usable with standard OCI registry proxy caches (Harbor, Nexus, Artifactory) that expose proxied upstreams under a repository prefix rather than a dedicated (sub)domain — a common corporate setup where provisioning one virtual host per proxied registry isn't practical. Related demand: #22115.

Reproduction

With any Harbor project ghcr-io configured as a proxy cache for https://ghcr.io:

$ HOMEBREW_ARTIFACT_DOMAIN=https://harbor.example.com/ghcr-io HOMEBREW_DOWNLOAD_CONCURRENCY=1 brew fetch --verbose hello
==> Downloading https://harbor.example.com/ghcr-io/v2/homebrew/core/hello/manifests/2.12.3

The registry answers 404 for this path (the repository would be homebrew/core/hello under an API root that doesn't exist) and every download falls back to ghcr.io, defeating the mirror. With this change:

$ HOMEBREW_ARTIFACT_DOMAIN=https://harbor.example.com/ghcr-io HOMEBREW_DOWNLOAD_CONCURRENCY=1 brew fetch --verbose hello
==> Downloading https://harbor.example.com/v2/ghcr-io/homebrew/core/hello/manifests/2.12.3

Verified end-to-end against a real Harbor 2.x proxy-cache project (with HOMEBREW_DOCKER_REGISTRY_TOKEN set so the auth header is kept): brew fetch --bottle-tag=x86_64_linux hello, brew reinstall yq and brew vendor-install ruby all download through the proxy. Domains without a path still produce https://mirror.example.com/v2/homebrew/core/... (unchanged), covered by the updated tests.

🤖 Generated with Claude Code

HOMEBREW_ARTIFACT_DOMAIN rewrites ghcr.io bottle URLs by replacing the
domain prefix, so a value with a path (e.g. a Harbor proxy cache such as
https://example.com/ghcr-io) produced
https://example.com/ghcr-io/v2/homebrew/core/... which no OCI registry
can serve: the OCI Distribution API requires /v2/ at the root of the
host, with the path acting as a repository prefix.

Insert the artifact domain's path after /v2/ instead, producing
https://example.com/v2/ghcr-io/homebrew/core/... Domains without a path
keep the existing behaviour, as does the fallback to the original
ghcr.io URLs. Apply the same logic to the portable-ruby URL in
vendor-install.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't you just set the HOMEBREW_ARTIFACT_DOMAIN to https://mirror.example.com/v2/ghcr-io instead?

If the issue is that https://mirror.example.com/ghcr-io can literally never ever be valid with any host in any circumstance: that should error out. I don't think you can state that, though, so it seems better that that would be error message improvement rather than behaviour change.

@jouve

jouve commented Jul 12, 2026

Copy link
Copy Markdown
Author

in the current code URL_PREFIX alway has v2 added : URL_PREFIX=https://ghcr.io/v2,

URL_DOMAIN = "ghcr.io"
URL_PREFIX = T.let("https://#{URL_DOMAIN}/v2/".freeze, String)

then if HOMEBREW_ARTIFACT_DOMAIN is set, it replaces the https://ghcr.io part with HOMEBREW_ARTIFACT_DOMAIN, the final v2 is kept
if (domain = Homebrew::EnvConfig.artifact_domain)
artifact_urls = urls.map do |u|
u.sub(%r{^https?://#{GitHubPackages::URL_DOMAIN}/}o, "#{domain.chomp("/")}/")
end

so if I do HOMEBREW_ARTIFACT_DOMAIN=https://mirror.example.com/v2/ghcr-io , the code above generates HOMEBREW_ARTIFACT_DOMAIN=https://mirror.example.com/v2/ghcr-io/v2 with is not a valid as it has one additionnal v2/ at the end.

Containerd use override_path to make this behaviour explicit.

[host."https://mirror.example.com/v2/ghcr-io"]
  capabilities = ["pull", "resolve"]
  override_path = true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants