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 } }
      end

View on GitHub (pinned to e227c27540)

Solutions

  1. 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).
  2. 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').
  3. Install a different release of the top-level module whose graph resolves cleanly: puppet module install author/mod --version X.Y.Z.
  4. 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

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


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/78f0f24f5d1ceeaa. Report an issue: GitHub.