puppetlabs/puppet · error · Puppet::ModuleTool::Errors::InvalidDependencyCycleError
'%{module_name}' (%{version}) requested; Invalid dependency
Error message
'%{module_name}' (%{version}) requested; Invalid dependency cycle What it means
Raised by the module tool dependency resolver during install when it revisits a module it has already resolved ('seen') and the version range accumulated so far no longer accepts the release already chosen for it (range === seen[mod][:semver] fails). Despite the name it covers both true cycles (A depends on B depends on A) and chains that circle back with conflicting version conditions. The error carries the module name, the dependency chain and the collected conditions.
Source
Thrown at lib/puppet/module_tool/shared_behaviors.rb:104
range = begin
Puppet::Module.parse_range(@version)
rescue
Puppet::Module.parse_range('>= 0.0.0')
end
else
range = (@conditions[mod]).map do |r|
Puppet::Module.parse_range(r[:dependency])
rescue
Puppet::Module.parse_range('>= 0.0.0')
end.inject(&:&)
end
if @action == :install && seen.include?(mod)
next if range === seen[mod][:semver]
req_module = @module_name
req_versions = @versions[@module_name.to_s].map { |v| v[:semver] }
raise InvalidDependencyCycleError,
:module_name => mod,
:source => (source + [{ :name => mod, :version => source.last[:dependency] }]),
:requested_module => req_module,
:requested_version => @version || annotated_version(req_module, req_versions),
:conditions => @conditions
end
if !(forced? || @installed[mod].empty? || source.last[:name] == :you)
next if range === SemanticPuppet::Version.parse(@installed[mod].first.version)
action = :upgrade
elsif @installed[mod].empty?
action = :install
end
if action == :upgrade
@conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } }
endView on GitHub (pinned to e227c27540)
Solutions
- Break the cycle: edit the offending metadata.json files so no module transitively depends on itself (usually by removing or renaming the dependency that loops back).
- Align version_requirement ranges along the cycle so every revisit accepts the same shared release (e.g. widen '>= 2.1.0' to '>= 2.0.0 < 3.0.0').
- Install a different release of the top-level module whose graph resolves cleanly: puppet module install author/mod --version X.Y.Z.
- As a last resort for local experiments only, bypass resolution with --force (forced? skips these checks); never in production module directories.
Example fix
# before: mod-a metadata depends on mod-b, and mod-b metadata depends back on mod-a
{"name": "author/mod-a", "dependencies": [{"name": "author/mod-b", "version_requirement": ">= 1.0.0"}]}
# after: remove the back-edge in mod-b so the graph is acyclic
{"name": "author/mod-b", "dependencies": [{"name": "puppetlabs/stdlib", "version_requirement": ">= 8.0.0 < 10.0.0"}]} Defensive patterns
Strategy: try-catch
Validate before calling
require 'semantic_puppet'
# resolve locally before installing: intersect all ranges imposed on each module
ranges = constraints_on(mod).map { |r| SemanticPuppet::VersionRange.parse(r) }
shared = ranges.inject(&:&) # intersection collapses to nothing on conflict
abort 'conflicting constraints on ' + mod unless shared
abort 'no release satisfies constraints' unless releases.any? { |v| shared === SemanticPuppet::Version.parse(v) } Try / catch
begin # puppet module install author/mod rescue Puppet::ModuleTool::Errors::InvalidDependencyCycleError => e warn e.message # names the module, version and the cycle conditions exit 1 end
Prevention
- Keep each module's dependency graph acyclic; audit with 'puppet module list --tree' after adding dependencies.
- When forking or renaming modules, update every metadata.json that referenced the old name to avoid phantom back-edges.
- Prefer broad ranges ('>= x.y < z') over exact pins on transitive dependencies.
- Validate packages in CI with 'puppet module build' plus a resolve dry-run before publishing.
When it happens
Trigger: Running 'puppet module install' (action == :install) where the dependency graph loops back to an already-selected module with a tighter range: A requires B >= 2.0, B requires A < 2.0, so when the resolver returns to A the previously selected A 2.1.0 no longer satisfies the intersected range.
Common situations: metadata.json dependency chains that reference each other (mutual dependencies); two paths through the graph constraining the same module to disjoint ranges; hand-edited metadata.json leaving a module depending on its own dependents; renamed module forks that still cross-reference the old name.
Related errors
- Could not %{action} '%{module_name}' (%{version}); no versio
- Could not find a valid module at %{path}
- Could not %{action} '%{module_name}' (%{version}); no versio
- Could not %{action} '%{module_name}' (%{version}); no versio
- Dependency conflict for %{module_name}: Dependency %{name} w
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/78f0f24f5d1ceeaa.
Report an issue: GitHub.