fix(helm): resolve OCI build-info paths for non-root subpaths#425
fix(helm): resolve OCI build-info paths for non-root subpaths#425vjda wants to merge 7 commits intojfrog:mainfrom
Conversation
Resolve the repo key and subpath inside the helm push build-info flow by validating a small candidate set against the OCI artifacts Helm actually uploaded, so build-info no longer assumes every push lands at repository root. Keep the change scoped to push.go, propagate the resolved Artifactory storage path into artifact lookup and manifest-folder properties, and add focused tests for forms 1-4 plus the path-based handlePushCommand cases.
Resolve OCI dependency layer lookups when charts live under non-root subpaths in Artifactory-backed OCI repositories. The previous flow always searched `chart/version` at the repo root. That worked for root-only layouts, but it missed valid OCI dependencies whose `dep.Repository` points to a nested subpath. Keep the change intentionally scoped to the OCI dependency flow in `layers.go`. Parse the dependency reference structure, generate a small candidate set, and validate candidates with the real artifact search already used by the dependency path. This preserves the existing Classic Helm behavior, avoids reopening the broader shared-resolver exploration, and aligns the error policy with observed AQL behavior: empty results are normal misses, while search errors remain operational failures. As a small follow-up, move the OCI candidate helper into `repository.go` so both push and dependency flows share the same primitive without leaving package-level OCI logic buried in `push.go`.
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
Pull request overview
This PR fixes Helm OCI follow-up Artifactory path resolution when charts/dependencies are stored under non-root subpaths, by generating plausible repo/subpath candidates and validating them via artifact searches before proceeding.
Changes:
- Add a shared OCI repo-candidate primitive (
ociRepoCandidate) and candidate generation helper. - Update OCI push build-info collection to resolve the correct repo key + storage path (including subpaths) before manifest/layer lookup and property application.
- Update OCI dependency layer resolution to use validated repo/subpath candidates instead of assuming
<chart>/<version>at repo root, and add focused unit tests for both flows.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| artifactory/commands/helm/repository.go | Introduces OCI repo/subpath candidate structure and generator shared by push/dependency flows. |
| artifactory/commands/helm/repository_test.go | Adds unit tests for OCI candidate generation behavior. |
| artifactory/commands/helm/push.go | Resolves OCI push artifacts using validated candidates and applies build props to the resolved manifest folder path. |
| artifactory/commands/helm/push_test.go | Adds comprehensive tests for OCI push resolution forms and manifest-folder property application behavior. |
| artifactory/commands/helm/layers.go | Updates OCI dependency AQL lookup to use resolved storage paths (including subpaths) via validated candidates. |
| artifactory/commands/helm/layers_test.go | Adds tests covering non-root OCI dependencies, root-only non-regression, and no-match behavior. |
Comments suppressed due to low confidence (1)
artifactory/commands/helm/push.go:234
- SetProps errors are currently ignored (both the status code and error are discarded). This can cause the push build-info flow to succeed while build properties were never applied. Please propagate/log the SetProps error (and ideally make applyBuildPropertiesOnManifestFolder return it) so failures are surfaced correctly.
func addBuildPropertiesOnArtifacts(serviceManager artifactory.ArtifactoryServicesManager, reader *content.ContentReader, buildProps string) {
propsParams := services.PropsParams{
Reader: reader,
Props: buildProps,
IsRecursive: true,
}
_, _ = serviceManager.SetProps(propsParams)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Tighten the small follow-up changes requested in PR jfrog#425 after the main OCI path fixes were already in place. This commit does not change the core resolution strategy. Instead it improves the behavior and reviewability of the existing hotfix by: - preserving the original manifest retrieval error with `%w` - returning a clearer host-only miss error when the repo key is known but no OCI artifacts are found at the resolved storage path - including the OCI parse error in dependency-side logging - cleaning up a stale comment and a minor test formatting issue The intent is to address the review feedback without widening scope or mixing in unrelated local changes.
|
Addressed all of copilot review comments in a small follow-up commit.
Focused tests were re-run after the changes. |
| subpath string | ||
| } | ||
|
|
||
| func generateRepoCandidates(registry, repository string) []ociRepoCandidate { |
There was a problem hiding this comment.
For ambiguous URLs like oci://helm-repo.art.com/team-a/charts, the code tries:
- First:
team-arepo with subpathcharts/ - Second:
helm-reporepo with subpathteam-a/charts/
In typical JFrog setups, virtual host repos (e.g., helm-repo.art.com → helm-repo repo) are often more common than repos named after subdirectories. Would reversing the order reduce unnecessary AQL queries in practice?
There was a problem hiding this comment.
Thanks for calling this out. I updated the candidate order to try the host-derived OCI repo first and kept the path-based candidate as the fallback.
I don't think this makes the ambiguous cases universally "correct" in a strict sense, since the current flow still resolves on the first successful artifact lookup. So, as I see it, the ordering is effectively a policy choice when both layouts could be plausible.
The reason for this update is practical: Artifactory commonly uses the host-based OCI layout, so preferring that candidate first aligns better with the more typical setup and can avoid an unnecessary AQL query in that case.
I also updated the focused push/dependency tests so they explicitly document the new host-first behavior and its fallback semantics.
Add a small follow-up to the OCI dependency subpath work. This commit documents `generateRepoCandidates` with a proper docstring and adds a debug log when the dependency flow resolves the winning OCI repo/subpath candidate. The intent is to improve maintainability and make dependency-side path resolution easier to inspect during review and troubleshooting, without changing the existing functional behavior.
Propagate SetProps failures in the OCI push build-properties flow instead of silently ignoring them. This tightens the post-push build-info path without changing the broader OCI resolution logic. The previous helper swallowed serviceManager.SetProps errors, so the command could continue as if build properties had been applied successfully. Return the lower-level SetProps error through addBuildPropertiesOnArtifacts and applyBuildPropertiesOnManifestFolder, then let handlePushCommand surface it with the existing contextual wrapper. Add focused tests for both the helper-level failure path and the end-to-end push command behavior when property application fails.
Prefer the host-derived OCI candidate before the path-based fallback when resolving OCI repository layouts that can be addressed in more than one valid way. This does not claim that host-first is universally more correct. For ambiguous layouts, the candidate order is effectively a policy choice because the current flow stops at the first successful artifact lookup. This change makes that policy explicit. The practical goal here is to align the heuristic with the more common Artifactory host-based OCI layout while still preserving the same fallback behavior when the host-derived candidate does not resolve. The accompanying test updates do two things: - rename cases so they describe the new host-first policy precisely - adjust expected search order and fallback behavior accordingly The result is a clearer contract between the helper, `resolveOCIPushArtifacts`, and the OCI dependency flow, with tests that now document the intended order instead of the previous path-first assumption.
|
PR #430 has now been merged and appears to cover the same issue space. I want to leave one point clearly on the record: if this work was already being handled internally, then reviewing this PR, requesting changes on it, and later merging the same solution path through a different internal branch — without linking it back here or leaving any traceability note — is not a good way to manage external contributions. From the contributor side, the experience was:
That creates avoidable wasted effort and leaves the external contributor without basic visibility into what happened. To make things more confusing, the repository’s CONTRIBUTING.md currently says that pull requests should target the I’m not raising this to argue about ownership of the final code, but to highlight a process problem: Otherwise, the repository is effectively asking external contributors to spend time on work that may already be decided elsewhere, without any transparency. Please improve the contributor flow here: |
Fixes #424
Summary
This PR fixes the same underlying OCI storage-path bug in the two affected Helm
OCI flows:
jf helm pushpost-push build-info collectionaddOCILayersForDependencyIn both flows, the bug is not that the OCI artifact is missing. The artifact is
already in Artifactory. The failure happens later, when the CLI resolves the
follow-up Artifactory path as if the chart always lived at the repository root
under:
<chart-name>/<chart-version>That assumption breaks for valid OCI layouts such as:
oci://helm-repo.artifactory.example.com/team-a/chartsoci://artifactory.example.com/helm-repo/team-a/chartsIn those cases, the real Artifactory storage path is:
team-a/charts/<chart-name>/<chart-version>so the follow-up lookup misses the manifest/layers even though the artifacts
exist.
For dependencies, the same pattern appears because:
dep.Idonly containschart-name:chart-versiondep.Repositorychart-name/chart-versionat repo root instead of thereal
subpath/chart-name/chart-versionWhy the bug happens
There are three important details here:
ORAS parsing is useful, but only for OCI syntax parsing.
ORAS was already available in the package as a parser for OCI reference
structure, but the affected flows were still resolving follow-up Artifactory
paths with root-only assumptions.
In other words, ORAS can tell us the OCI
RegistryandRepositorycomponents of a reference, but it does not by itself tell us semantically
which part is the Artifactory repo key for every valid Artifactory access
layout.
Any effective OCI URL that contains a path is potentially ambiguous from
the URL alone.
In other words, once the URL has path segments, the CLI cannot rely on a
single local heuristic and pretend that the repo key is always obvious.
Real Artifactory AQL behavior matters here.
During validation against a real Artifactory instance, wrong repo/path
combinations returned a normal miss (
200+ empty results), while realfailures (for example auth problems) surfaced as operational errors.
That matters because it justifies the final control flow:
That allowed the implementation to stay aligned with actual server behavior
instead of introducing a speculative state machine around candidate search
failures.
For this bug, the safest fix is therefore not "invent a stronger URL-only
heuristic", but to validate the small set of plausible candidates by searching
for the actual OCI artifacts that the flow needs to resolve.
Proposed Fix
This PR keeps the change intentionally small and OCI-only, but it covers the
two broken OCI call sites that matter here.
Instead of reopening the broader shared repository-resolution logic, the fix is
scoped to:
push.gofor post-push build-info resolutionlayers.gofor OCI dependency layer resolutionThe implementation follows the same small pattern in both places:
candidates locally inside the affected OCI flow
by that flow
subpath/chart/version)Why this approach
This approach keeps the fix correct, local, and easy to review.
1. Why not introduce a broader shared repository resolver here?
Because this bug is ultimately about two concrete OCI follow-up flows, not about
redesigning all repository resolution across Helm.
A broader shared resolver would increase the review surface and the regression
risk for code paths that are not required to fix this bug.
This PR is intentionally narrower:
That makes the change easier to reason about and much safer to review upstream.
2. Why not rely on ORAS alone?
Because ORAS is the right tool for OCI syntax parsing, but not for deciding
the Artifactory repo key in every valid Artifactory URL layout.
For example, ORAS can parse
RegistryandRepository, but if the effectiveOCI URL contains path segments, that still does not prove which part is the
Artifactory repo key and which part is the subpath.
So the final rule in this PR is:
That keeps the fix robust without pretending the URL alone is always enough.
3. Why validate using artifact search instead of repository metadata?
Because searching the actual OCI artifacts is the strongest scoped source of
truth for these flows.
Checking only whether a repo exists would still allow false positives:
By validating with the same artifact lookup the flow already needs, the winning
candidate is proven against the concrete pushed chart location, not only against
repo metadata.
4. Why keep the change local to the affected OCI flows instead of a shared resolver?
Because the reported bugs are about concrete OCI follow-up flows, not about a
generic cross-domain repository resolver.
The final scope deliberately stays here:
push.gofor OCI post-push build-infolayers.gofor OCI dependency layer resolutionAnd deliberately stays out of here:
helm pullThat keeps the PR:
Implementation details
push.goresolves OCI push repo/subpath through a push-local helper:resolveOCIPushArtifacts(...)layers.goresolves OCI dependency repo/subpath locally insideaddOCILayersForDependency(...)combinations by searching for the real OCI artifacts
searchPushedArtifacts(...)receives the resolved ArtifactorystoragePathdirectly instead of recomposing a root-only
chart/versionstoragePathinstead ofassuming repo root
Artifactory location
buildPropertiesdead code fromsearchPushedArtifactsnewManifestFolderReader(...)so the build-properties flow keepsa dedicated synthetic folder reader instead of reusing/mutating the search
reader
repository.go(
ociRepoCandidate+generateRepoCandidates) because it is now shared byboth OCI flows, without turning it into a broad shared resolver
against Artifactory: empty results are normal misses; search errors remain
operational failures
Behavior preservation
This PR deliberately preserves the existing overall behavior outside the bug fix:
helm pullIn other words, this is a targeted OCI correctness fix, not a generalized
resolver rewrite.
Test Coverage
Added/updated tests cover:
contain the pushed artifact
-handlePushCommandintegration-style coverage for forms 1–4repository_test.goValidation run locally:
go test -v -race ./artifactory/commands/helm -run '^TestHandlePushCommandResolvesOCIPaths$'go test -v -race ./artifactory/commands/helmgo test -v -race ./artifactory/commands/helm -run '^(TestAddOCILayersForDependency|TestUpdateClassicHelmDependencyChecksumsLeavesExistingChecksumsUntouched|TestGenerateOCIRepoCandidates)$'Manual Validation
In addition to the automated test coverage in this PR, I also validated the fix
manually against a real remote Artifactory instance hosted on
jfrog.io.Validation scope:
jf helm pushflow against a remote OCI repositoryafter push
jf helm dependency buildflow against an OCI dependencylayout that also uses non-root subpaths
Artifactory storage path instead of assuming a root-only
chart/versionlayout
I also prepared local debug-only harnesses around the real package entrypoint
(
HelmCommand.Run()), so both the push and dependency behaviors can bereproduced and debugged locally without mixing environment-specific integration
code into the upstream PR.
I am not including those local integration/debug harnesses as part of the upstream
change because they depend on environment-specific credentials and infrastructure.
For reference, here are representative self-contained snippets for both
entrypoints. They are intentionally concise and use the real debug env var names
so they can be copied with minimal editing.
Snippets of the local debug-only harnesses: