From c6d0310029b22b6b6cf9a1300ef8f548ea245896 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 7 May 2026 11:07:20 +0200 Subject: [PATCH] fix(state): resolve OCI repo prefix in ad-hoc release dependencies (#2579) When a release `dependencies[].chart` is given as `/` and the matching `repositories:` entry has `oci: true`, helmfile now rewrites it to `oci:///` before passing it to chartify. Without this, chartify's lookup falls into its `helm repo list` branch, which never finds OCI repos because helm 3+ does not register OCI registries as named repos (they live in the `helm registry login` state instead). The user-visible failure was: failed reading adhoc dependencies: no helm list entry found for repository "". please `helm repo add` it! Explicit `oci://` URLs already worked through chartify's OCI branch; this change makes the `/` form behave the same way. Non-OCI repo prefixes, unknown prefixes, single-segment names, and explicit `oci://` URLs all pass through unchanged. A debug log records each rewrite at the call site for easier troubleshooting. Fixes #1756. Signed-off-by: Dominik Schmidt --- pkg/state/helmx.go | 3 ++ pkg/state/state.go | 22 +++++++++++++ pkg/state/state_test.go | 71 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index ca118921..0a75df98 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -427,6 +427,9 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp if err != nil { return nil, clean, err } + } else if rewritten, ok := st.resolveOCIAdhocDepChart(d.Chart); ok { + st.logger.Debugf("ad-hoc dependency %q rewritten to %q (matched OCI repo entry)", d.Chart, rewritten) + chart = rewritten } c.Opts.AdhocChartDependencies = append(c.Opts.AdhocChartDependencies, chartify.ChartDependency{ diff --git a/pkg/state/state.go b/pkg/state/state.go index 106d4a66..cec8f890 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1392,6 +1392,28 @@ func (st *HelmState) GetRepositoryAndNameFromChartName(chartName string) (*Repos return nil, chartName } +// resolveOCIAdhocDepChart rewrites a release `dependencies[].chart` value that +// uses the named-repo prefix form ("repoName/chartName") into a full oci:// URL +// when the prefix matches a `repositories:` entry with `oci: true`. It returns +// (rewritten, true) on a hit and ("", false) otherwise. +// +// This avoids the chartify path that does `helm repo list` to look up the +// repository URL: that lookup never finds OCI repos because helm 3+ does not +// register OCI registries as named repos (it uses `helm registry login` +// instead). By the time chartify sees an `oci://` URL it already takes the +// correct branch, so rewriting here is enough to make the named-repo form +// behave the same as the explicit URL form. +func (st *HelmState) resolveOCIAdhocDepChart(chart string) (string, bool) { + if strings.HasPrefix(chart, "oci://") { + return "", false + } + repo, name := st.GetRepositoryAndNameFromChartName(chart) + if repo == nil || !repo.OCI { + return "", false + } + return "oci://" + strings.TrimSuffix(repo.URL, "/") + "/" + name, true +} + var rwMutexMap sync.Map // getNamedRWMutex retrieves or creates a sync.RWMutex for a given name. diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 8e6f2ba9..9f1038df 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -6039,3 +6039,74 @@ func TestHelmState_getKubeContext(t *testing.T) { }) } } + +// resolveOCIAdhocDepChart should rewrite a release `dependencies[].chart` value +// that uses the named-repo prefix form into a full oci:// URL whenever the +// matching `repositories:` entry has `oci: true`. All other inputs must pass +// through unchanged so we never disturb existing behavior. +func TestResolveOCIAdhocDepChart(t *testing.T) { + state := &HelmState{ + ReleaseSetSpec: ReleaseSetSpec{ + Repositories: []RepositorySpec{ + {Name: "ociregistry", URL: "registry.example.com:5000/charts", OCI: true}, + {Name: "ociregistry-trailing", URL: "registry.example.com:5000/charts/", OCI: true}, + {Name: "stable", URL: "https://charts.helm.sh/stable"}, + }, + }, + } + + tests := []struct { + name string + chart string + wantOK bool + wantChart string + }{ + { + name: "named OCI repo prefix is rewritten to oci:// URL", + chart: "ociregistry/redis", + wantOK: true, + wantChart: "oci://registry.example.com:5000/charts/redis", + }, + { + name: "trailing slash on repo URL does not produce a double slash", + chart: "ociregistry-trailing/redis", + wantOK: true, + wantChart: "oci://registry.example.com:5000/charts/redis", + }, + { + name: "non-OCI repo prefix is left alone for chartify's helm-repo-list path", + chart: "stable/nginx", + wantOK: false, + }, + { + name: "explicit oci:// URL is left alone (already in chartify's OCI branch)", + chart: "oci://registry.example.com:5000/charts/redis", + wantOK: false, + }, + { + name: "unknown repo prefix is left alone", + chart: "unknownrepo/something", + wantOK: false, + }, + { + name: "single-segment chart (no slash) is left alone", + chart: "localchart", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := state.resolveOCIAdhocDepChart(tt.chart) + if ok != tt.wantOK { + t.Errorf("ok: want %v, got %v", tt.wantOK, ok) + } + if tt.wantOK && got != tt.wantChart { + t.Errorf("rewritten chart: want %q, got %q", tt.wantChart, got) + } + if !tt.wantOK && got != "" { + t.Errorf("expected empty rewrite when ok=false, got %q", got) + } + }) + } +}