Skip to content

(CAT-2600) Make bolt opt-in in generated module Gemfile#641

Draft
gavindidrichsen wants to merge 1 commit into
mainfrom
cat-2600-fix-bolt-gem-conflic
Draft

(CAT-2600) Make bolt opt-in in generated module Gemfile#641
gavindidrichsen wants to merge 1 commit into
mainfrom
cat-2600-fix-bolt-gem-conflic

Conversation

@gavindidrichsen

@gavindidrichsen gavindidrichsen commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Problem

CAT-2600: pdk validate fails with version solving has failed on a repo validating against Puppet 8.10.0.

CAT-2357 added an unconditional, unpinned bolt to the generated module Gemfile (moduleroot/Gemfile.erb). The bolt line is sourced from puppetcore, so Bundler resolves it to bolt 5.x, whose gemspec hard-requires puppet >= 8.11 and facter ~> 4.11 — both puppetcore-only. PDK pins puppet to the version it ships (8.10.0), so resolution dies before any validator runs:

Because every version of bolt depends on puppet ~> 8.11
 and Gemfile depends on bolt >= 0,
 puppet ~> 8.11 is required.
So, because Gemfile depends on puppet = 8.10.0,
 version solving has failed.

Every module inherited bolt's Puppet floor — even modules that never touch bolt.

Fix

Guard the bolt line with if bolt_version, exactly mirroring the existing hiera line, so bolt only enters the Gemfile when the user explicitly opts in via BOLT_GEM_VERSION:

-gems['bolt'] = location_for(bolt_version, nil, { source: gemsource_puppetcore })
+gems['bolt'] = location_for(bolt_version, nil, { source: gemsource_puppetcore }) if bolt_version

This is safe: bolt specs are already gated behind ENV['GEM_BOLT'] (spec/spec_helper.rb.erb), so a normal pdk validate run never needs bolt present. Users who do want bolt set BOLT_GEM_VERSION and take responsibility for a compatible puppet/bolt pair.

Verification

Using bundle lock --print (non-destructive) with the bolt 5.x constraint stood up via a local bolt-private path gem:

  • Beforegem 'puppet', '8.10.0' + unconditional gem 'bolt'version solving has failed.
  • After (default run, BOLT_GEM_VERSION unset) — Gemfile carries no bolt line → resolves to puppet (8.10.0); validation proceeds.
  • Guard logicBOLT_GEM_VERSION unset → bolt line skipped; set → bolt line emitted.

🤖 Generated with Claude Code

How it works (before vs after)

bolt is in the resolution either way, because the default Gemfile pulls puppet_litmus (~> 2.5)bolt (>= 4.0, < 6.0). The fix only changes which bolt Bundler may pick.

Before — the template's direct, unpinned puppetcore bolt forces 5.x, which needs puppet >= 8.11:

flowchart TD
    P["puppet pinned = 8.10.0"]
    D["template: gem 'bolt' UNPINNED, source: puppetcore"] --> B5["puppetcore newest bolt = 5.x → needs puppet >= 8.11"]
    P --> R{"one puppet for all?"}
    B5 --> R
    R -->|"8.10.0 is not >= 8.11"| F["version solving has failed"]
    style F fill:#f8d7da,stroke:#dc3545
Loading

After — bolt is only constrained by puppet_litmus (public), so Bundler picks 4.0.0, happy with 8.10.0:

flowchart TD
    P["puppet pinned = 8.10.0"]
    L["puppet_litmus → bolt >= 4.0, < 6.0 (public)"] --> B4["Bundler picks bolt 4.0.0 (public)"]
    P --> R{"one puppet for all?"}
    B4 --> R
    R --> OK["resolves: puppet 8.10.0 + bolt 4.0.0"]
    style OK fill:#d1e7dd,stroke:#198754
Loading

Opt-in is preserved: set BOLT_GEM_VERSION to pull bolt from puppetcore again (requires an all-puppetcore stack, puppet >= 8.11).

@gavindidrichsen gavindidrichsen requested a review from a team as a code owner June 8, 2026 18:04
@gavindidrichsen gavindidrichsen marked this pull request as draft June 8, 2026 18:06
@gavindidrichsen

Copy link
Copy Markdown
Contributor Author

Test evidence — fix verified, and the customer failure reproduced

I tested this against PDK 3.6.1 (the version from CAT-2600), driving the real pdk CLI, plus a credential-free reproduction of the customer's resolution error.

1. The customer failure is real (reproduced)

bolt 5.x (the version published to puppetcore) declares, in bolt.gemspec:

facter  ~> 4.11        # <== "Ensure puppetcore facter version"
puppet  >= 8.11, < 10

PDK pins puppet to the version it ships (8.10.0). Standing the real bolt gemspec constraint into a Gemfile next to that pin reproduces the exact error from the ticket (bundle lock --print, no install):

Because every version of bolt depends on puppet >= 8.11, < 10
  and Gemfile depends on puppet = 8.10.0,
So, because Gemfile depends on bolt >= 0,
  version solving has failed.

(The published puppetcore bolt expresses the floor as ~> 8.11; same effect. This is the customer's version solving has failed.)

2. The fix verified through pdk (no puppetcore needed)

Module generated from this branch with pdk new module ... --template-url="file://$TEMPLATE_DIR" --template-ref=cat-2600-fix-bolt-gem-conflic:

a. Default run — bolt is now absent, validation succeeds:

$ pdk validate
pdk (INFO): Using Puppet 8.10.0
pdk (INFO): Running all available validators...

Generated Gemfile: gems['bolt'] = location_for(...) if bolt_version → no bolt line emitted.

b. Opt-in still works — BOLT_GEM_VERSION pulls bolt back in:

$ BOLT_GEM_VERSION=4.0.0 pdk validate
pdk (INFO): Using Puppet 8.10.0
pdk (INFO): Running all available validators...
$ grep '^    bolt ' Gemfile.lock
    bolt (4.0.0)

c. Opt into bolt 5.x without puppetcore — fails as expected (confirms bolt 5.x is puppetcore-only):

$ BOLT_GEM_VERSION='~> 5.0' pdk validate
Could not find gem 'bolt (~> 5.0)' in rubygems repository https://rubygems.org/

3. Full end-to-end repro through pdk requires puppetcore

Note for reviewers: the template only sources bolt from puppetcore when PUPPET_FORGE_TOKEN is set, otherwise it falls back to public rubygems.org (where bolt tops out at 4.0.0 and the conflict cannot occur). So reproducing the failure through pdk validate directly needs a puppetcore token:

export PUPPET_FORGE_TOKEN=<token>
git checkout main      # unfixed: unconditional bolt
pdk new module mod_bug --skip-interview --template-url="file://$PWD" --template-ref=main
cd mod_bug && pdk validate     # => version solving has failed
# switch to cat-2600-fix-bolt-gem-conflic and re-run => validates

I did not have a puppetcore token in my test environment, so step 3 is verified via the gemspec-constraint reproduction in section 1; sections 2a–2c were run directly with pdk.

@gavindidrichsen

Copy link
Copy Markdown
Contributor Author

Scenario diagrams — what fails, and why the fix works

A subtlety worth making explicit: bolt is in the resolution either way, because the default module Gemfile already pulls puppet_litmus (~> 2.5), whose gemspec declares bolt (>= 4.0, < 6.0). The fix only changes which bolt Bundler is allowed to pick.

Before (unfixed) — the customer failure

The template adds a direct, unpinned bolt from puppetcore, which forces the newest puppetcore bolt (5.x). That bolt requires puppet >= 8.11, colliding with the puppet = 8.10.0 PDK pins.

flowchart TD
    G["Generated module Gemfile (UNFIXED)"]
    G --> P["puppet pinned = 8.10.0<br/>(PDK PUPPET_GEM_VERSION)"]
    G --> LIT["puppet_litmus ~> 2.5<br/>→ bolt >= 4.0, &lt; 6.0 (public source)"]
    G --> DIR["template line 135: gem 'bolt'<br/>UNPINNED, source: puppetcore"]
    DIR --> B5["puppetcore serves newest bolt = 5.x"]
    B5 --> FLOOR["bolt 5.x requires puppet >= 8.11"]
    P --> R{"one puppet version<br/>for everything?"}
    FLOOR --> R
    R -->|"8.10.0 is not >= 8.11"| FAIL["version solving has failed"]
    style FAIL fill:#f8d7da,stroke:#dc3545
    style DIR fill:#fff3cd,stroke:#ffc107
Loading

Verified against real puppetcore (PUPPET_GEM_VERSION=8.10.0 bundle lock --print on the unfixed generated Gemfile):

Because bolt < 5.1.0 depends on puppet ~> 8.11
  and bolt >= 5.1.0 depends on puppet >= 8.11, < 10,
So, because Gemfile depends on puppet = 8.10.0,
  version solving has failed.

After (this PR) — resolves

With the direct puppetcore bolt line guarded out, bolt is constrained only by puppet_litmus (>= 4.0, < 6.0, public source), so Bundler picks bolt 4.0.0, which is happy with puppet 8.10.0.

flowchart TD
    G["Generated module Gemfile (FIXED: bolt is opt-in)"]
    G --> P["puppet pinned = 8.10.0"]
    G --> LIT["puppet_litmus ~> 2.5<br/>→ bolt >= 4.0, &lt; 6.0 (public source)"]
    LIT --> B4["Bundler free to pick bolt 4.0.0 (public)"]
    B4 --> OK["bolt 4.0.0 works with puppet 8.10.0"]
    P --> R{"one puppet version<br/>for everything?"}
    OK --> R
    R --> PASS["resolves: puppet 8.10.0 + bolt 4.0.0"]
    style PASS fill:#d1e7dd,stroke:#198754
Loading

Verified (same command, fixed generated Gemfile):

    bolt (4.0.0)
    puppet (8.10.0-universal-darwin)

Opt-in path is preserved

A module that genuinely wants bolt sets BOLT_GEM_VERSION (read at validate time); the guarded line then emits gem 'bolt' again. To use bolt 5.x you must also be on a puppetcore puppet >= 8.11 — i.e. an all-puppetcore stack.

flowchart LR
    U["BOLT_GEM_VERSION unset<br/>(normal validate)"] --> N["no bolt line → no 8.11 floor imposed"]
    O["BOLT_GEM_VERSION set<br/>(opt-in)"] --> Y["gem 'bolt' emitted from puppetcore<br/>(needs puppet >= 8.11 to resolve)"]
    style N fill:#d1e7dd,stroke:#198754
    style Y fill:#cfe2ff,stroke:#0d6efd
Loading

Note: on a packaged PDK 3.6.1, pdk validate resolves --local against the package's gem cache (which predates the bolt addition) and reports Could not find gem 'bolt' in locally installed gems; the bundle lock --print resolves above reproduce the customer's network resolution faithfully.

@gavindidrichsen gavindidrichsen force-pushed the cat-2600-fix-bolt-gem-conflic branch from 0badb23 to d40fcf6 Compare June 11, 2026 19:06
CAT-2357 added an unconditional, unpinned `bolt` to the generated module
Gemfile. Because the bolt gem is pulled from puppetcore, Bundler resolves it
to bolt 5.x, whose gemspec hard-requires `puppet >= 8.11` (and `facter ~> 4.11`)
— both puppetcore-only. That collides with the puppet version PDK pins for a
run (e.g. 8.10.0), so `pdk validate` fails at dependency resolution with
"version solving has failed" before any validator runs. Every module inherited
bolt's puppet floor, even modules that never use bolt.

Guard the bolt line with `if bolt_version`, mirroring the existing `hiera`
line, so bolt only enters the Gemfile when the user explicitly opts in via
BOLT_GEM_VERSION. Bolt specs are already gated behind ENV['GEM_BOLT'], so a
normal validate run never needs bolt present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Gavin Didrichsen <gavin.didrichsen@gmail.com>
@gavindidrichsen gavindidrichsen force-pushed the cat-2600-fix-bolt-gem-conflic branch from d40fcf6 to 6185b96 Compare June 18, 2026 13:19
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.

1 participant