Skip to content

Commit 387903c

Browse files
committed
Inject credentials from UV_DEFAULT_INDEX for native uv
1 parent e2d59f9 commit 387903c

2 files changed

Lines changed: 449 additions & 15 deletions

File tree

artifactory/commands/python/native_uv.go

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,23 @@ func (c *NativeUVCommand) injectCredentials(workingDir, deployerRepo string, ser
163163

164164
injectedAny := false
165165

166-
// UV_INDEX_URL and UV_DEFAULT_INDEX are UV's global default index env vars.
167-
// Log them for visibility; per-named-index injection still proceeds because
168-
// these vars only affect the default/fallback index, not named [[tool.uv.index]] entries.
169-
if os.Getenv("UV_INDEX_URL") != "" || os.Getenv("UV_DEFAULT_INDEX") != "" {
170-
log.Info("UV auth: global index env var set (UV_INDEX_URL or UV_DEFAULT_INDEX); per-index credential injection still proceeds below")
166+
// UV_INDEX_URL is the legacy single-URL form with no name= grammar, so credentials
167+
// cannot be injected via UV's per-index env vars. Warn the user explicitly.
168+
if os.Getenv("UV_INDEX_URL") != "" {
169+
log.Warn("UV auth: UV_INDEX_URL is set but has no name form; " +
170+
"credentials cannot be injected via UV per-index env vars. " +
171+
"Use UV_INDEX or UV_DEFAULT_INDEX with name=url format, " +
172+
"embed credentials in the URL, or use ~/.netrc.")
171173
}
172174

173175
// Use script inline indexes when --script is active; fall back to pyproject.toml entries.
174176
indexes := scriptIndexes
175177
if len(indexes) == 0 {
176178
indexes = uvReadIndexesFromToml(workingDir)
177179
}
180+
// Merge UV_DEFAULT_INDEX / UV_INDEX entries — env vars override config-file entries
181+
// to match UV's own precedence rules.
182+
indexes = uvMergeIndexes(indexes, uvReadIndexesFromEnv())
178183
for _, idx := range indexes {
179184
envName := uvIndexEnvName(idx.Name)
180185
userKey := uvIndexUsernameKey(envName)
@@ -454,6 +459,110 @@ func uvReadIndexesFromToml(workingDir string) []uvIndexEntry {
454459
return entries
455460
}
456461

462+
// uvParseIndexEnvEntry parses a single "[name=]url" entry from UV_INDEX / UV_DEFAULT_INDEX.
463+
// Splits on the first '=' only (URL query strings can contain '=', names cannot).
464+
// Returns ok=false for empty, unnamed, or malformed entries.
465+
func uvParseIndexEnvEntry(raw string) (uvIndexEntry, bool) {
466+
raw = strings.TrimSpace(raw)
467+
if raw == "" {
468+
return uvIndexEntry{}, false
469+
}
470+
parts := strings.SplitN(raw, "=", 2)
471+
if len(parts) != 2 {
472+
return uvIndexEntry{}, false
473+
}
474+
name := strings.TrimSpace(parts[0])
475+
indexURL := strings.TrimSpace(parts[1])
476+
if name == "" || indexURL == "" {
477+
return uvIndexEntry{}, false
478+
}
479+
return uvIndexEntry{Name: name, URL: indexURL}, true
480+
}
481+
482+
// uvReadIndexesFromEnv parses UV_DEFAULT_INDEX and UV_INDEX into named index entries.
483+
// UV's per-index credential env vars (UV_INDEX_<NAME>_USERNAME/PASSWORD) only work for
484+
// named entries, so unnamed entries are skipped with a warning.
485+
func uvReadIndexesFromEnv() []uvIndexEntry {
486+
var entries []uvIndexEntry
487+
warnUnnamed := func(envVar, raw string) {
488+
log.Warn(fmt.Sprintf(
489+
"UV auth: %s entry %q has no name; credentials cannot be injected via UV per-index env vars. "+
490+
"Use name=url format, embed credentials in the URL, or use ~/.netrc.",
491+
envVar, raw))
492+
}
493+
if def := strings.TrimSpace(os.Getenv("UV_DEFAULT_INDEX")); def != "" {
494+
if entry, ok := uvParseIndexEnvEntry(def); ok {
495+
entry.Default = true
496+
entries = append(entries, entry)
497+
} else {
498+
warnUnnamed("UV_DEFAULT_INDEX", def)
499+
}
500+
}
501+
if list := os.Getenv("UV_INDEX"); list != "" {
502+
for _, raw := range strings.Fields(list) {
503+
if entry, ok := uvParseIndexEnvEntry(raw); ok {
504+
entries = append(entries, entry)
505+
} else {
506+
warnUnnamed("UV_INDEX", raw)
507+
}
508+
}
509+
}
510+
return entries
511+
}
512+
513+
// uvResolveBuildInfoIndexURL returns the (envVarName, url) pair to use as a fallback
514+
// index URL for build-info dependency enrichment when no pyproject.toml index is
515+
// configured. Walks UV_DEFAULT_INDEX → first UV_INDEX entry → UV_INDEX_URL (legacy).
516+
// For UV_DEFAULT_INDEX / UV_INDEX, the optional "name=" prefix is stripped.
517+
func uvResolveBuildInfoIndexURL() (envVar, indexURL string) {
518+
if val := strings.TrimSpace(os.Getenv("UV_DEFAULT_INDEX")); val != "" {
519+
if entry, ok := uvParseIndexEnvEntry(val); ok {
520+
return "UV_DEFAULT_INDEX", entry.URL
521+
}
522+
return "UV_DEFAULT_INDEX", val
523+
}
524+
if list := os.Getenv("UV_INDEX"); list != "" {
525+
for _, raw := range strings.Fields(list) {
526+
if entry, ok := uvParseIndexEnvEntry(raw); ok {
527+
return "UV_INDEX", entry.URL
528+
}
529+
}
530+
}
531+
if val := strings.TrimSpace(os.Getenv("UV_INDEX_URL")); val != "" {
532+
return "UV_INDEX_URL", val
533+
}
534+
return "", ""
535+
}
536+
537+
// uvMergeIndexes returns the union of base and override entries, deduped by
538+
// uvIndexEnvName(name) (UV's canonical env-var suffix). Override entries appear first
539+
// in their declared order, then non-conflicting base entries — matching UV's own
540+
// precedence rule that env vars override pyproject.toml.
541+
func uvMergeIndexes(base, override []uvIndexEntry) []uvIndexEntry {
542+
if len(override) == 0 {
543+
return base
544+
}
545+
seen := make(map[string]bool, len(override)+len(base))
546+
result := make([]uvIndexEntry, 0, len(override)+len(base))
547+
for _, idx := range override {
548+
envName := uvIndexEnvName(idx.Name)
549+
if seen[envName] {
550+
continue
551+
}
552+
seen[envName] = true
553+
result = append(result, idx)
554+
}
555+
for _, idx := range base {
556+
envName := uvIndexEnvName(idx.Name)
557+
if seen[envName] {
558+
continue
559+
}
560+
seen[envName] = true
561+
result = append(result, idx)
562+
}
563+
return result
564+
}
565+
457566
// ── Credential helpers ────────────────────────────────────────────────────────
458567

459568
// uvIndexHasNativeCredentials returns true when any native UV mechanism
@@ -676,20 +785,17 @@ func uvGetBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfi
676785
case "sync", "install", "lock", "add", "remove", "run":
677786
if len(bi.Modules) > 0 && len(bi.Modules[0].Dependencies) > 0 {
678787
// Resolve enrichment repo: prefer [[tool.uv.index]] in pyproject.toml,
679-
// fall back to UV_DEFAULT_INDEX / UV_INDEX_URL env vars (used when no
680-
// pyproject.toml index is configured, e.g. CI workflows that inject creds
681-
// via environment).
788+
// fall back to UV_DEFAULT_INDEX, then the first UV_INDEX entry, then the
789+
// legacy UV_INDEX_URL (used when no pyproject.toml index is configured,
790+
// e.g. CI workflows that inject creds via environment).
682791
repoKey := uvResolverRepoFromToml(workingDir)
683792
indexURL := uvIndexURLFromToml(workingDir)
684793
if repoKey == "" {
685-
for _, envVar := range []string{"UV_DEFAULT_INDEX", "UV_INDEX_URL"} {
686-
if val := os.Getenv(envVar); val != "" {
687-
repoKey = uvExtractRepoKeyFromURL(val)
794+
if envVar, val := uvResolveBuildInfoIndexURL(); val != "" {
795+
if rk := uvExtractRepoKeyFromURL(val); rk != "" {
796+
repoKey = rk
688797
indexURL = val
689-
if repoKey != "" {
690-
log.Debug(fmt.Sprintf("UV build-info: using %s for dependency enrichment repo: %s", envVar, repoKey))
691-
break
692-
}
798+
log.Debug(fmt.Sprintf("UV build-info: using %s for dependency enrichment repo: %s", envVar, repoKey))
693799
}
694800
}
695801
}

0 commit comments

Comments
 (0)