Skip to content

feat: resolve dependency version ranges#3572

Draft
kichristensen wants to merge 9 commits intogetporter:mainfrom
kichristensen:dependencyRanges
Draft

feat: resolve dependency version ranges#3572
kichristensen wants to merge 9 commits intogetporter:mainfrom
kichristensen:dependencyRanges

Conversation

@kichristensen
Copy link
Contributor

@kichristensen kichristensen commented Mar 21, 2026

What does this change

Adds a dependencies.version-strategy setting 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 tag
  • max-patch — pick the highest patch release within the same major.minor as the default tag in the dependency reference
  • max-minor — pick the highest minor+patch release within the same major as the default tag
  • min — pick the lowest release satisfying the stated range

The setting can be configured via the config file, the PORTER_DEPENDENCIES_VERSION_STRATEGY environment variable, or the --dependencies-version-strategy flag on install and upgrade.

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-patch and max-minor derive 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

  • Did you write tests?
  • Did you write documentation?
  • Did you change porter.yaml or a storage document record? Update the corresponding schema file.
  • If this is your first pull request, please add your name to the bottom of our Contributors list. Thank you for making Porter better! 🙇‍♀️

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>
Signed-off-by: Kim Christensen <kimworking@gmail.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ExtendedBundle and 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.

Comment on lines +207 to +214
// 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 := &registryListTagsAdapter{reg: e.Resolver.Registry, opts: regOpts}
eb := bun.WithRegistry(adapter, regOpts)

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

return OCIReference{}, fmt.Errorf("not implemented: dependency version range specified for %s: %w", name, err)
// Version ranges are specified
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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
}

Copilot uses AI. Check for mistakes.
return ref, nil

// dep.Version is a semver constraint string
if b.versionStrategy == "" || b.versionStrategy == "exact" {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Comment on lines +390 to +399
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)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 459 to 471
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)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +88
// 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
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolve dependency to using version ranges

2 participants