feat: resolve dependency version ranges#3572
feat: resolve dependency version ranges#3572kichristensen wants to merge 9 commits intogetporter:mainfrom
Conversation
6aa9aab to
4627e47
Compare
Add DependenciesConfig struct, constants for the four strategies (exact, max-patch, max-minor, min), and GetDependenciesVersionStrategy accessor that defaults to "exact". Signed-off-by: Kim Christensen <kimworking@gmail.com>
- Add versionStrategy field and WithVersionStrategy builder - Update filterAndSelectTag to accept a version constraint and respect min vs max ordering - Update determineDefaultTag(v2) to pass constraint through - Fix ResolveVersion (v1): resolve ranges via strategy; error on exact + range-only - Fix ResolveVersionv2 (v2): resolve version constraint string via strategy instead of returning the ref unchanged - Update tests: replace "not implemented" case with strategy cases Signed-off-by: Kim Christensen <kimworking@gmail.com>
- Add DependenciesVersionStrategy to BundleExecutionOptions - Wire flag > global config fallback in identifyDependencies - Add --dependencies-version-strategy flag to install and upgrade commands with viper-key binding to dependencies.version-strategy Signed-off-by: Kim Christensen <kimworking@gmail.com>
- Config accessor: TestGetDependenciesVersionStrategy (unit) - Config loading: file, env var, env-over-file (unit) - Extended bundle v2: TestExtendedBundle_ResolveVersionv2 with all strategies and error cases (unit) - Integration: config file, env var, multi-context config loading of dependencies.version-strategy (integration tag) Signed-off-by: Kim Christensen <kimworking@gmail.com>
- Add registryListTagsAdapter to fix registry mock wiring - Add unit tests (pkg/porter/dependencies_test.go) covering exact/max-patch/min/max-minor strategies and config fallback - Add integration tests (tests/integration/) for install and upgrade verifying strategy selection via MockPullBundle - Register new integration test in both CI workflows Signed-off-by: Kim Christensen <kimworking@gmail.com>
max-patch now restricts resolution to the same major.minor as the default version in the dependency reference. max-minor restricts to the same major version. Both strategies derive the narrowing constraint from the semver tag encoded in the bundle's dependency reference (e.g. example.com/mysql:v1.2.3). Add unit and integration tests covering both behaviors. Signed-off-by: Kim Christensen <kimworking@gmail.com>
Add dependencies.version-strategy to the configuration reference and config file example. Document the semantic difference between max-patch, max-minor, min, and exact, including how the default tag anchors the range. Add a Version Ranges section to the dependencies authoring guide with examples and a strategy comparison table. Signed-off-by: Kim Christensen <kimworking@gmail.com>
Signed-off-by: Kim Christensen <kimworking@gmail.com>
4627e47 to
bbfaf65
Compare
Signed-off-by: Kim Christensen <kimworking@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR adds configurable semver range resolution for Porter bundle dependencies, allowing Porter to select an appropriate bundle tag when a dependency specifies version constraints instead of a fully pinned reference.
Changes:
- Introduces
dependencies.version-strategy(config/env/flag) to control how dependency version ranges are resolved (exact,max-patch,max-minor,min). - Implements range/constraint-based tag selection in
cnab.ExtendedBundleand wires it through install/upgrade dependency resolution. - Adds unit + integration tests, updates CLI/reference docs, and adds CI jobs for the new integration test suite.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/cnab/extended_bundle.go |
Implements version-strategy-driven tag selection for dependency ranges/constraints. |
pkg/cnab/extended_bundle_test.go |
Adds tests covering range resolution for both dependency v1 and v2. |
pkg/config/datastore.go |
Adds config constants and dependencies.version-strategy config struct. |
pkg/config/config.go |
Adds Config.GetDependenciesVersionStrategy() accessor with defaulting. |
pkg/config/datastore_test.go |
Tests defaulting and configured strategy accessor behavior. |
pkg/config/loader_test.go |
Tests loading/overriding dependencies.version-strategy from file/env. |
pkg/porter/lifecycle.go |
Adds DependenciesVersionStrategy to bundle execution options. |
pkg/porter/dependencies.go |
Wires config/flag strategy into dependency resolution; adds registry ListTags adapter. |
pkg/porter/dependencies_test.go |
Adds unit tests for dependency resolution behavior under strategies. |
cmd/porter/installations.go |
Adds --dependencies-version-strategy flag and viper binding for install/upgrade. |
tests/integration/dependencies_version_strategy_test.go |
Adds end-to-end integration coverage for strategy behavior during install/upgrade. |
tests/integration/config_test.go |
Adds integration tests for config/env/multi-context loading of strategy. |
docs/content/docs/configuration/configuration.md |
Documents the new dependencies.version-strategy setting. |
docs/content/docs/development/authoring-a-bundle/working-with-dependencies.md |
Documents version range semantics and strategy behaviors for bundle authors. |
docs/content/docs/references/cli/install*.md / upgrade*.md |
Updates CLI reference docs to include the new flag. |
.github/workflows/porter-integration-*.yml |
Adds a dedicated CI job to run the new integration test suite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Inject registry provider for dependency resolution. | ||
| // registryListTagsAdapter bridges the cnabtooci.RegistryProvider | ||
| // (concrete opts) and the registryListTags interface in the cnab | ||
| // package (opts interface{}) to avoid a circular import. | ||
| regOpts := cnabtooci.RegistryOptions{InsecureRegistry: e.parentOpts.InsecureRegistry} | ||
| eb := bun.WithRegistry(e.Resolver.Registry, regOpts) | ||
| adapter := ®istryListTagsAdapter{reg: e.Resolver.Registry, opts: regOpts} | ||
| eb := bun.WithRegistry(adapter, regOpts) | ||
|
|
There was a problem hiding this comment.
The new registryListTagsAdapter fixes the ListTags signature mismatch for dependency resolution here, but other call sites still pass a raw cnabtooci.RegistryProvider into ExtendedBundle.WithRegistry (e.g. pkg/porter/explain.go generatePrintable), which will still fail the registryListTags type assertion at runtime when resolving dependencies that require tag listing (no tag / version ranges). Consider centralizing the adapter (or otherwise aligning the interface) so all dependency-resolution call paths use a registry that satisfies cnab.registryListTags.
| } | ||
|
|
||
| return OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err) | ||
| // Version ranges are specified |
There was a problem hiding this comment.
ResolveVersion currently errors for any dependency that specifies version ranges when the strategy is "exact" (or unset), even if the dependency reference already includes a pinned tag/digest (e.g. example.com/mysql:v1.2.3). Per the PR description/docs, exact should only error when the dependency provides only a range/constraint and no pinned default; otherwise it should just use the pinned reference as-is. Consider short-circuiting to return ref, nil when ref.HasTag() (and/or ref.HasDigest()) before emitting the strategy error, and add a test for the pinned-tag+range+exact case.
| // Version ranges are specified | |
| // Version ranges are specified | |
| // If the reference is already pinned (by tag or digest), just use it as-is. | |
| if ref.HasTag() || ref.HasDigest() { | |
| return ref, nil | |
| } |
| return ref, nil | ||
|
|
||
| // dep.Version is a semver constraint string | ||
| if b.versionStrategy == "" || b.versionStrategy == "exact" { |
There was a problem hiding this comment.
ResolveVersionv2 errors whenever dep.Version (constraint) is set and the strategy is "exact"/unset, even if the bundle reference already has an explicit tag or digest. The documented behavior for exact is to use the default implementation exactly as referenced, and only error when a constraint/range is provided without a pinned default. Consider returning the pinned ref when ref.HasTag()/ref.HasDigest() under exact, and only requiring a non-exact strategy when the ref is repository-only.
| if b.versionStrategy == "" || b.versionStrategy == "exact" { | |
| if b.versionStrategy == "" || b.versionStrategy == "exact" { | |
| // With the exact (or unset) strategy, a pinned reference (tag or digest) is used as-is. | |
| // Only error when the reference is repository-only and a version constraint is provided. | |
| if ref.HasTag() || ref.HasDigest() { | |
| return ref, nil | |
| } |
| func (b *ExtendedBundle) strategyConstraintFromRef(ref OCIReference) string { | ||
| if !ref.HasVersion() { | ||
| return "" | ||
| } | ||
| defaultVer, err := semver.NewVersion(ref.Tag()) | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| return computeStrategyConstraint(defaultVer, b.versionStrategy) | ||
| } |
There was a problem hiding this comment.
strategyConstraintFromRef uses semver.NewVersion(ref.Tag()) after checking ref.HasVersion(). However, Porter encodes semver build metadata in tags by replacing '+' with '_' (see semverFromTag/tagFromSemver in oci_reference.go). In that case ref.HasVersion() can be true but semver.NewVersion(ref.Tag()) will fail and you silently skip applying the strategy constraint. Consider parsing with semverFromTag(ref.Tag()) (or equivalent underscore->plus normalization) to match Porter's tag semantics.
| version, err := semver.NewVersion(tag) | ||
| if err == nil { | ||
| if !allowPrereleases && version.Prerelease() != "" { | ||
| continue | ||
| } | ||
| if constraint != nil && !constraint.Check(version) { | ||
| continue | ||
| } | ||
| if sConstraint != nil && !sConstraint.Check(version) { | ||
| continue | ||
| } | ||
| versions = append(versions, version) | ||
| } |
There was a problem hiding this comment.
filterAndSelectTag parses candidate tags with semver.NewVersion(tag). Porter tags may include build metadata where '+' has been converted to '' (see tagFromSemver/semverFromTag), which semver.NewVersion won't parse. That would cause valid semver tags to be ignored during selection (and could incorrectly fall back to latest or error). Consider using semverFromTag(tag) (or normalizing '' back to '+') when converting tags to semver versions.
| // DependenciesVersionStrategy controls how dependency version ranges are resolved. | ||
| // Allowed values: exact, max-patch, max-minor, min. | ||
| // Defaults to the global config value (dependencies.version-strategy). | ||
| DependenciesVersionStrategy string |
There was a problem hiding this comment.
DependenciesVersionStrategy is documented with a fixed set of allowed values, but BundleExecutionOptions.Validate doesn't currently validate this field. As implemented, an unknown non-empty value will be treated like a "max" strategy (descending sort + no additional constraint) and could mask user/config typos. Consider validating the flag/config value against the supported set (exact/max-patch/max-minor/min) during options validation and returning a clear error for invalid values.
What does this change
Adds a
dependencies.version-strategysetting that controls how Porter selects a bundle version when a dependency declares a semver version range instead of a pinned tag.Supported strategies:
exact(default) — error if the dependency specifies only a range and no pinned tagmax-patch— pick the highest patch release within the samemajor.minoras the default tag in the dependency referencemax-minor— pick the highest minor+patch release within the same major as the default tagmin— pick the lowest release satisfying the stated rangeThe setting can be configured via the config file, the
PORTER_DEPENDENCIES_VERSION_STRATEGYenvironment variable, or the--dependencies-version-strategyflag oninstallandupgrade.What issue does it fix
Partially resolves #2683 (part of epic #2678).
Not implemented: version reconciliation when two dependencies reference the same bundle with overlapping ranges (shared node selection described in #2683).
Notes for the reviewer
max-patchandmax-minorderive their narrowing constraint from the semver tag encoded in the bundle's dependency reference (e.g.example.com/mysql:v1.2.3). If no version tag is present in the default reference, both strategies fall back to picking the highest tag satisfying the stated range.Checklist