Skip to content

Commit e611e22

Browse files
fix(builder): append the mirror mappings only after a plain inherited count
Moving inherited git config after the mirror mappings could not win precedence anyway (git breaks insteadOf ties by the order the destination base was registered, so a system or global rewrite still wins) and added edge cases of its own. The mappings are appended after the inherited entries again, without reading or moving them. They are used only when GIT_CONFIG_COUNT is unset, empty, or a plain count of at most four digits once one leading "+" and leading zeros are dropped; otherwise the git configuration is left alone, the build log says the sources are fetched without the mirror, and the build runs as it does today. An image whose own git configuration (system, global or environment) already rewrites a declared repository's URL keeps its rewrite: git prefers it, and the build fetches that source as it does without a mirror. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3e33c73 commit e611e22

4 files changed

Lines changed: 59 additions & 54 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,10 @@ into `templates/factory/code/pyproject.toml.tmpl` yourself.
162162
A git source is declared in the recipe (`git_source_downloads`) by the URL
163163
exactly as `uv.lock` records it up to its query, which is what uv hands git;
164164
the caller fetches it with its own credentials and the Dockerfile maps that
165-
URL onto the mirror, ahead of inherited git config. A normalized URL would
166-
miss the mirror silently. A source core refuses, or whose URL begins an
167-
undeclared locked one (insteadOf matches by prefix), is not declared or
168-
rendered; a warning names the package and why, and the build fetches it.
165+
URL onto the mirror; a normalized URL would miss it silently. A source core
166+
refuses, or whose URL begins an undeclared locked one (insteadOf is a
167+
prefix match), is not declared; a warning says why. An image whose own git
168+
config already rewrites a declared URL keeps that rewrite (git prefers it).
169169
- **The runtime image is pinned.** `docker-image` rejects a bare name and
170170
`:latest`. The default `ghcr.io/codefly-dev/python` companion is built from
171171
core and pinned by the digest of its multi-platform index — prefer it. Its tag

‎templates/builder/Dockerfile.tmpl‎

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,20 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
5858
# With the caller's mirrors, git reads each locked repository from its mirror
5959
# instead of the URL the lock records (uv hands git that URL unchanged), so no
6060
# credential is needed, and a commit missing from the mirror fails here rather
61-
# than prompting for one. The mirror mappings take the first entries of git's
62-
# environment configuration, so they win over an inherited rewrite of the same
63-
# URL, and the entries the environment already carries (GIT_CONFIG_COUNT, read
64-
# as git reads it) move after them, read by name and never evaluated. A count
65-
# git would reject is left as it is, for git to reject as before.
61+
# than prompting for one. The mappings are appended after the git configuration
62+
# the environment already carries, when its GIT_CONFIG_COUNT is a plain count;
63+
# otherwise the configuration is left alone and the sources are fetched as
64+
# without a mirror. An image whose own git configuration (system, global or
65+
# environment) already rewrites a declared repository's URL keeps its rewrite:
66+
# git prefers it, and the build fetches that source as it does without a mirror.
6667
RUN --mount=type=bind,from=uvsources,target=/uvsources \
6768
if [ -n "$(ls -A /uvsources)" ]; then \
68-
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; \
69-
case $n in ''|*[!0-9]*) ;; *) \
70-
i=$n; while [ "$i" -gt 0 ]; do i=$((i-1)); \
71-
eval "k=\${GIT_CONFIG_KEY_$i}"; eval "v=\${GIT_CONFIG_VALUE_$i}"; \
72-
export "GIT_CONFIG_KEY_$((i+{{len .GitSources}}))=$k" "GIT_CONFIG_VALUE_$((i+{{len .GitSources}}))=$v"; \
73-
done; \
74-
export GIT_CONFIG_COUNT=$((n+{{len .GitSources}})) GIT_TERMINAL_PROMPT=0{{range $i, $source := .GitSources}} \
75-
GIT_CONFIG_KEY_{{$i}}='url.file:///uvsources/{{$source.Path}}.insteadOf' GIT_CONFIG_VALUE_{{$i}}='{{$source.Repository}}'{{end}};; \
76-
esac; \
69+
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; \
70+
case $n in ''|*[!0-9]*) n= ;; *) while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; [ ${#n} -le 4 ] || n= ;; esac; \
71+
if [ -z "$n" ]; then echo "inherited GIT_CONFIG_COUNT is not a plain count; git sources are fetched without the mirror"; \
72+
else export GIT_TERMINAL_PROMPT=0{{range $i, $source := .GitSources}} \
73+
"GIT_CONFIG_KEY_$((n+{{$i}}))=url.file:///uvsources/{{$source.Path}}.insteadOf" "GIT_CONFIG_VALUE_$((n+{{$i}}))="'{{$source.Repository}}'{{end}} \
74+
GIT_CONFIG_COUNT=$((n+{{len .GitSources}})); fi; \
7775
fi && \
7876
mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
7977
{{- else}}

‎testdata/dockerfile/git-sources.golden‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,21 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
3636
# With the caller's mirrors, git reads each locked repository from its mirror
3737
# instead of the URL the lock records (uv hands git that URL unchanged), so no
3838
# credential is needed, and a commit missing from the mirror fails here rather
39-
# than prompting for one. The mirror mappings take the first entries of git's
40-
# environment configuration, so they win over an inherited rewrite of the same
41-
# URL, and the entries the environment already carries (GIT_CONFIG_COUNT, read
42-
# as git reads it) move after them, read by name and never evaluated. A count
43-
# git would reject is left as it is, for git to reject as before.
39+
# than prompting for one. The mappings are appended after the git configuration
40+
# the environment already carries, when its GIT_CONFIG_COUNT is a plain count;
41+
# otherwise the configuration is left alone and the sources are fetched as
42+
# without a mirror. An image whose own git configuration (system, global or
43+
# environment) already rewrites a declared repository's URL keeps its rewrite:
44+
# git prefers it, and the build fetches that source as it does without a mirror.
4445
RUN --mount=type=bind,from=uvsources,target=/uvsources \
4546
if [ -n "$(ls -A /uvsources)" ]; then \
46-
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; \
47-
case $n in ''|*[!0-9]*) ;; *) \
48-
i=$n; while [ "$i" -gt 0 ]; do i=$((i-1)); \
49-
eval "k=\${GIT_CONFIG_KEY_$i}"; eval "v=\${GIT_CONFIG_VALUE_$i}"; \
50-
export "GIT_CONFIG_KEY_$((i+2))=$k" "GIT_CONFIG_VALUE_$((i+2))=$v"; \
51-
done; \
52-
export GIT_CONFIG_COUNT=$((n+2)) GIT_TERMINAL_PROMPT=0 \
53-
GIT_CONFIG_KEY_0='url.file:///uvsources/c9fa8363b851c2643d648edad1c8c7892904f717385511fe4771422d0817a55e.git.insteadOf' GIT_CONFIG_VALUE_0='https://git.example.invalid/acme/mono' \
54-
GIT_CONFIG_KEY_1='url.file:///uvsources/d2b987abe743521016c33e82dba28324380629b9d2544f9e5213ee4299a3af6c.git.insteadOf' GIT_CONFIG_VALUE_1='ssh://git@git.example.invalid:2222/acme/tools.git';; \
55-
esac; \
47+
n=${GIT_CONFIG_COUNT:-0}; n=${n#+}; \
48+
case $n in ''|*[!0-9]*) n= ;; *) while [ "${n#0}" != "$n" ] && [ -n "${n#0}" ]; do n=${n#0}; done; [ ${#n} -le 4 ] || n= ;; esac; \
49+
if [ -z "$n" ]; then echo "inherited GIT_CONFIG_COUNT is not a plain count; git sources are fetched without the mirror"; \
50+
else export GIT_TERMINAL_PROMPT=0 \
51+
"GIT_CONFIG_KEY_$((n+0))=url.file:///uvsources/c9fa8363b851c2643d648edad1c8c7892904f717385511fe4771422d0817a55e.git.insteadOf" "GIT_CONFIG_VALUE_$((n+0))="'https://git.example.invalid/acme/mono' \
52+
"GIT_CONFIG_KEY_$((n+1))=url.file:///uvsources/d2b987abe743521016c33e82dba28324380629b9d2544f9e5213ee4299a3af6c.git.insteadOf" "GIT_CONFIG_VALUE_$((n+1))="'ssh://git@git.example.invalid:2222/acme/tools.git' \
53+
GIT_CONFIG_COUNT=$((n+2)); fi; \
5654
fi && \
5755
mkdir -p /opt/uv/python && uv sync --frozen --no-dev --no-install-project --no-editable
5856

‎uv_git_sources_recipe_test.go‎

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -342,36 +342,45 @@ func runMapping(t *testing.T, lock string, env []string, script string) string {
342342
}
343343

344344
// TestGitSourceMappingInTheBuilderImage runs the rendered mapping where it
345-
// runs: the mirror wins over an inherited rewrite of the same URL, inherited
346-
// entries survive whatever form their count takes, and a source left out of
347-
// the declaration is not rewritten by a mapping that begins it.
345+
// runs: appended after the git configuration the environment carries when its
346+
// count is plain, leaving that configuration alone otherwise, and never
347+
// rewriting a source left out of the declaration.
348348
func TestGitSourceMappingInTheBuilderImage(t *testing.T) {
349349
if _, err := exec.LookPath("docker"); err != nil {
350350
t.Skip("docker is not installed: the mapping cannot be run")
351351
}
352352
const mono = "https://git.example.invalid/acme/mono"
353353
mirror := "file:///uvsources/" + services.GitSourceMirrorPath(mono)
354-
t.Run("inherited rewrite of a declared URL", func(t *testing.T) {
355-
out := runMapping(t, gitSourcesLock, []string{"GIT_CONFIG_COUNT=2",
356-
"GIT_CONFIG_KEY_0=url.https://proxy.invalid/.insteadOf", "GIT_CONFIG_VALUE_0=" + mono,
357-
"GIT_CONFIG_KEY_1=user.name", "GIT_CONFIG_VALUE_1=inherited $HOME `x` \"q\""},
358-
"git ls-remote --get-url "+mono+" && git config --get user.name && git config --get url.https://proxy.invalid/.insteadOf && echo count=$GIT_CONFIG_COUNT")
359-
require.Equal(t, mirror+"\ninherited $HOME `x` \"q\"\n"+mono+"\ncount=4\n", out)
360-
})
361-
for _, count := range []string{"01", "+1"} {
354+
inherited := []string{"GIT_CONFIG_KEY_0=user.name", "GIT_CONFIG_VALUE_0=inherited"}
355+
for _, count := range []string{"", "1", "01", "+1"} {
362356
t.Run("count "+count, func(t *testing.T) {
363-
out := runMapping(t, gitSourcesLock, []string{"GIT_CONFIG_COUNT=" + count, "GIT_CONFIG_KEY_0=user.name", "GIT_CONFIG_VALUE_0=inherited"},
364-
"git ls-remote --get-url "+mono+" && git config --get user.name && echo count=$GIT_CONFIG_COUNT")
365-
require.Equal(t, mirror+"\ninherited\ncount=3\n", out)
357+
env := append([]string{"GIT_CONFIG_COUNT=" + count}, inherited...)
358+
if count == "" {
359+
env = nil
360+
}
361+
out := runMapping(t, gitSourcesLock, env, "git ls-remote --get-url "+mono+" && { git config --get user.name || true; } && echo count=$GIT_CONFIG_COUNT")
362+
want := mirror + "\ninherited\ncount=3\n"
363+
if count == "" {
364+
want = mirror + "\ncount=2\n"
365+
}
366+
require.Equal(t, want, out)
366367
})
367368
}
368-
t.Run("count 08", func(t *testing.T) {
369-
env := []string{"GIT_CONFIG_COUNT=08"}
370-
for i := 0; i < 8; i++ {
371-
env = append(env, fmt.Sprintf("GIT_CONFIG_KEY_%d=test.k%d", i, i), fmt.Sprintf("GIT_CONFIG_VALUE_%d=v%d", i, i))
372-
}
373-
out := runMapping(t, gitSourcesLock, env, "git ls-remote --get-url "+mono+" && git config --get test.k0 && git config --get test.k7 && echo count=$GIT_CONFIG_COUNT")
374-
require.Equal(t, mirror+"\nv0\nv7\ncount=10\n", out)
369+
for _, count := range []string{" 1", "-0", "99999", "abc"} {
370+
t.Run("count "+count, func(t *testing.T) {
371+
env := append([]string{"GIT_CONFIG_COUNT=" + count}, inherited...)
372+
out := runMapping(t, gitSourcesLock, env, `echo "count=[$GIT_CONFIG_COUNT] keys=$(env | grep -c '^GIT_CONFIG_KEY_') prompt=[$GIT_TERMINAL_PROMPT]"`)
373+
require.Equal(t, "inherited GIT_CONFIG_COUNT is not a plain count; git sources are fetched without the mirror\ncount=["+count+"] keys=1 prompt=[]\n", out)
374+
})
375+
}
376+
// The limitation, as it is today without a mirror: an image whose own git
377+
// configuration rewrites a declared URL keeps its rewrite, which git
378+
// prefers, so the source is fetched where the image says, not broken.
379+
t.Run("inherited rewrite of a declared URL", func(t *testing.T) {
380+
out := runMapping(t, gitSourcesLock, []string{"GIT_CONFIG_COUNT=1",
381+
"GIT_CONFIG_KEY_0=url.https://proxy.invalid/.insteadOf", "GIT_CONFIG_VALUE_0=" + mono},
382+
"git ls-remote --get-url "+mono+" && echo count=$GIT_CONFIG_COUNT")
383+
require.Equal(t, "https://proxy.invalid/\ncount=3\n", out)
375384
})
376385
t.Run("an undeclared source the declared URL begins", func(t *testing.T) {
377386
out := runMapping(t, lockWith(mono, mono+"@v1", "https://git.example.invalid/acme/tools"), nil,

0 commit comments

Comments
 (0)