Skip to content

Pin npm shakapacker version to match gem in example generation#2558

Merged
justin808 merged 4 commits intomasterfrom
jg/fix-shakapacker-960-compat
Mar 8, 2026
Merged

Pin npm shakapacker version to match gem in example generation#2558
justin808 merged 4 commits intomasterfrom
jg/fix-shakapacker-960-compat

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Mar 8, 2026

Summary

  • Fix CI example generation failure when a new shakapacker version is published to npm before/independently of the gem
  • shakapacker:install adds the npm package with a caret range (e.g., ^9.5.0), so npm install can resolve to a newer minor version (e.g., 9.6.0) while the gem remains at 9.5.0
  • Added pin_shakapacker_npm_version that reads the exact gem version from the example app's Gemfile.lock and pins the npm package.json entry to match

Fixes the examples (3.4, latest) CI failure seen on PR #2516 where npm shakapacker resolved to 9.6.0 while the gem was 9.5.0.

Test plan

  • CI examples (3.4, latest) job passes
  • Verify that generated example apps have matching gem and npm shakapacker versions

🤖 Generated with Claude Code


Note

Low Risk
Small, localized change to example-generation rake tasks that only adjusts dependency pinning and should not affect runtime application behavior.

Overview
Prevents CI/example app generation failures caused by shakapacker gem/npm version drift by pinning the npm shakapacker dependency to the exact gem version after shakapacker:install.

Adds helpers to read the installed Shakapacker gem version from Gemfile.lock and rewrite package.json when it contains a semver range (e.g., ^X.Y.Z) before running npm install for non-react-pinned examples.

Written by Cursor Bugbot for commit 39fbbb6. Configure here.

Summary by CodeRabbit

  • New Features
    • Automatically pins the npm Shakapacker dependency to the installed gem version during example app generation to keep versions consistent.
  • Bug Fixes
    • Safer package.json handling with improved error logging and rewrite-only-on-change behavior.
  • Documentation
    • Added guidance for updating and pinning Shakapacker versions and regenerating lockfiles.
  • Tests
    • New tests covering the pinning behavior and generator workflow ordering.

shakapacker:install adds the npm package with a semver caret range
(e.g., ^9.5.0), which allows npm install to resolve a newer minor
version. When a new shakapacker release is published to npm before
or independently of the gem, this causes Shakapacker's version
consistency check to fail (e.g., gem 9.5.0 vs npm 9.6.0).

Fix by reading the exact gem version from the example app's
Gemfile.lock and pinning the npm package to match before running
npm install.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 8, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dae7c88d-0567-4d49-beb8-cdb45fcc20f8

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac88c0 and 4c5b384.

📒 Files selected for processing (2)
  • react_on_rails/rakelib/shakapacker_examples.rake
  • react_on_rails/spec/react_on_rails/shakapacker_examples_rake_spec.rb

Walkthrough

Adds two Rake helper methods in the shakapacker_examples namespace to read Shakapacker gem version from Gemfile.lock and pin the shakapacker entry in an example app's package.json to that exact version during example generation. Also adds CONTRIBUTING guidance and tests for the new behavior.

Changes

Cohort / File(s) Summary
Version Pinning Helpers
react_on_rails/rakelib/shakapacker_examples.rake
Adds pin_shakapacker_npm_version(dir) and shakapacker_gem_version_from_lockfile(dir). Reads Gemfile.lock, parses package.json, pins shakapacker in dependencies/devDependencies to gem version when present, logs/parses errors, writes only on change, and integrates pinning into example-generation flow.
Tests for Pinning
react_on_rails/spec/react_on_rails/shakapacker_examples_rake_spec.rb
Adds RSpec coverage for the new helpers and verifies call sequencing in the example generation task (bundle install → pin_shakapacker_npm_version → npm install). Mocks IO/filesystem and asserts behavior for prerelease versions.
Contributing Documentation
CONTRIBUTING.md
Inserts "Updating the Shakapacker Version for Testing" guidance describing where to pin Shakapacker in source files, which lockfiles to regenerate, and notes on example apps resolving the version automatically (section duplicated).

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer
  participant Rake as Rake Task (shakapacker_examples)
  participant FS as File System (Gemfile.lock, package.json)
  participant NPM as npm

  Dev->>Rake: invoke example generation
  Rake->>FS: read Gemfile.lock (extract shakapacker version)
  FS-->>Rake: gem version (or nil)
  alt gem version present
    Rake->>FS: read package.json
    FS-->>Rake: package.json contents
    Rake->>Rake: update dependencies/devDependencies shakapacker => gem version
    Rake->>FS: write package.json (if changed)
  end
  Rake->>NPM: run npm install (example dir)
  NPM-->>Dev: installs complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I sniffed the lockfile, found the gem’s bright tune,

I nudged package.json beneath the silvery moon,
Now bundler and npm dance in step and time,
Examples build tidy, versions aligned in rhyme.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: pinning npm shakapacker version to match the gem version in the example generation process, which is the core objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jg/fix-shakapacker-960-compat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39fbbb66db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +29 to +31
deps = package_json["dependencies"] || {}
current = deps["shakapacker"]
return unless current && current != gem_version
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin shakapacker from devDependencies too

pin_shakapacker_npm_version only checks package_json["dependencies"], so it silently skips pinning when shakapacker is declared under devDependencies (which is how this repo’s app package.json files are structured). In that case the caret range remains unchanged, npm install can still pull a newer minor npm package than the gem version, and the same gem/npm version mismatch this commit is trying to prevent can still occur.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 6ac88c0. pin_shakapacker_npm_version now iterates over both dependencies and devDependencies sections.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR adds a pin_shakapacker_npm_version helper to the example generation rake task that reads the exact shakapacker gem version from Gemfile.lock and rewrites the package.json dependencies entry to match it exactly, preventing npm's caret-range resolution from installing a newer minor version before npm install runs. The fix is well-targeted and addresses a real CI flakiness root cause.

Key observations:

  • The fix is only applied in the else (latest-React) branch; the pinned_react_version? branch runs npm install --legacy-peer-deps --install-links without first calling pin_shakapacker_npm_version, leaving those examples exposed to the same mismatch if shakapacker 8.x receives a new minor release.
  • The lockfile regex (\d+\.\d+\.\d+) only matches standard three-part semver; pre-release gem versions (e.g., 9.6.0.rc1) will silently bypass pinning.
  • JSON.pretty_generate is already used elsewhere in the file for package.json writes, so the formatting behaviour is consistent with the existing codebase.

Confidence Score: 3/5

  • Safe to merge for the immediate CI fix, but the incomplete coverage of the pinned_react_version? branch and the fragile regex leave known gaps.
  • The core logic is sound and directly fixes the reported CI failure. However, the pinned_react_version? branch receives no pinning call, meaning the same bug can resurface for those examples if shakapacker 8.x is updated. The pre-release regex gap is lower risk but could silently regress. Neither issue blocks the described fix, but they reduce confidence in completeness.
  • react_on_rails/rakelib/shakapacker_examples.rake — lines 148 (missing pin call in pinned_react_version? branch) and 43 (regex pattern).

Important Files Changed

Filename Overview
react_on_rails/rakelib/shakapacker_examples.rake Adds pin_shakapacker_npm_version and shakapacker_gem_version_from_lockfile helpers to synchronize the npm shakapacker version with the gem version before npm install. The fix is only applied in the else (latest React) branch, leaving the pinned_react_version? branch vulnerable to the same mismatch; the lockfile regex also silently fails for pre-release gem versions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[shakapacker:install\nadds '^X.Y.Z' to package.json] --> B{example_type.pinned_react_version?}

    B -- yes --> C[apply_react_version\nwrites package.json\nReact deps only]
    C --> D[bundle_install_in]
    D --> E["npm install\n--legacy-peer-deps --install-links\n⚠️ shakapacker still '^X.Y.Z'"]
    E --> F[shakapacker:binstubs\n⚠️ gem/npm mismatch risk]

    B -- no --> G[pin_shakapacker_npm_version\nread Gemfile.lock version\nwrite exact version to package.json]
    G --> H{Gemfile.lock match\nX.Y.Z regex?}
    H -- matched --> I[overwrite package.json\nshakapacker = exact version]
    H -- no match\npre-release / missing --> J[silently skips pinning\n⚠️ mismatch risk]
    I --> K["npm install --install-links\nnpm resolves exact version ✅"]
    J --> K
Loading

Comments Outside Diff (1)

  1. react_on_rails/rakelib/shakapacker_examples.rake, line 148 (link)

    pin_shakapacker_npm_version not called in pinned_react_version? branch

    The same version-mismatch bug this PR fixes can also affect pinned-React-version examples. shakapacker:install always writes "^X.Y.Z" to package.json (line 120), and this branch resolves it with npm install --legacy-peer-deps --install-links without first pinning the npm shakapacker version. If shakapacker 8.x receives a new minor release, these examples will see the same mismatch that this PR fixes for the else branch.

    Consider calling pin_shakapacker_npm_version before this npm install too:

Last reviewed commit: 39fbbb6

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+)\)/)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Regex skips pre-release gem versions

The pattern \d+\.\d+\.\d+ only matches standard three-part semver (9.5.0). If shakapacker ever ships a pre-release gem (e.g., 9.6.0.beta1 or 9.6.0.rc1), the regex won't match, gem_version will be nil, and the function will silently skip pinning — re-exposing the same mismatch the PR is trying to prevent.

A broader pattern that also captures pre-release suffixes would be safer:

Suggested change
match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+)\)/)
match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+[^)]*)\)/)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 6ac88c0. Changed regex to ([^)]+) which captures the full version string including prerelease segments like 9.6.0.beta.0.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 8, 2026

The approach is sound - reading the gem version from Gemfile.lock and pinning the npm entry before npm install is the right way to prevent semver range resolution from causing version drift.

Inconsistent error handling: pin_shakapacker_npm_version parses package.json without a rescue JSON::ParserError block, while update_package_json_for_react_version catches and re-raises that error. Both follow the same read-parse-write pattern - matching the existing style would be cleaner.

Detached hash when dependencies is absent: deps = package_json["dependencies"] || {} — if the key is nil, deps is a fresh {} not stored back into package_json, so any mutation would be silently lost. In practice safe because the guard exits early when deps["shakapacker"] is nil, but the code reads as if mutating deps always propagates.

pinned_react_version? branch not covered: The fix is applied only in the else branch. The pinned_react_version? path runs npm install after apply_react_version without pinning the shakapacker npm version. Likely intentional (gem version already constrained differently for older React), but a brief comment explaining the omission would prevent future confusion.

Overall: Small, focused, correct. The Gemfile.lock regex is right and the exact-version pin strategy is the right call.

package_json_path = File.join(dir, "package.json")
return unless File.exist?(package_json_path)

package_json = JSON.parse(File.read(package_json_path))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unlike update_package_json_for_react_version which wraps this in begin/rescue JSON::ParserError, this method has no error handling. Since the file was just written by shakapacker:install it should be valid JSON, but for consistency consider adding the same rescue pattern.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 6ac88c0. Added rescue JSON::ParserError with descriptive error message, matching the pattern in update_package_json_for_react_version.

return unless File.exist?(package_json_path)

package_json = JSON.parse(File.read(package_json_path))
deps = package_json["dependencies"] || {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If package_json["dependencies"] is nil, deps is a detached {} — mutations to it won't propagate to package_json. The guard on line 31 makes this safe in practice (nil deps means nil current, so we return early), but the intent is clearer written as:

Suggested change
deps = package_json["dependencies"] || {}
deps = package_json["dependencies"]
return unless deps&.key?("shakapacker")
current = deps["shakapacker"]
return if current == gem_version

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react_on_rails/rakelib/shakapacker_examples.rake (1)

137-160: ⚠️ Potential issue | 🟠 Major

Pin shakapacker before both npm install paths.

Every example goes through bundle exec rake shakapacker:install before this branch, but only the else path rewrites package.json to exact versions. The pinned_react_version? path still reaches the npm install with the original caret range, so shakapacker:binstubs can hit the same gem/npm version check failure that this PR protects against in the else branch.

Suggested fix
+      # Keep the npm package aligned with the installed gem before any npm install.
+      pin_shakapacker_npm_version(example_type.dir)
+
       # Apply specific React version for compatibility testing examples
       if example_type.pinned_react_version?
         apply_react_version(example_type.dir, example_type.react_version_string)
         # Re-run bundle install to ensure dependencies are resolved correctly
         bundle_install_in(example_type.dir)
@@ -148,9 +151,6 @@
         # Use --install-links to copy file: dependencies instead of symlinking,
         # preventing duplicate React instances from webpack resolving through symlinks
         sh_in_dir(example_type.dir, "npm install --legacy-peer-deps --install-links")
       else
-        # Pin the npm shakapacker version to exactly match the installed gem version.
-        # shakapacker:install may add "^X.Y.Z" to package.json, which allows npm to
-        # resolve a newer minor version (e.g., 9.6.0 when gem is 9.5.0), causing
-        # Shakapacker's gem/npm version consistency check to fail.
-        pin_shakapacker_npm_version(example_type.dir)
         # Use --install-links to copy file: dependencies instead of symlinking,
         # preventing duplicate React instances from webpack resolving through symlinks
         sh_in_dir(example_type.dir, "npm install --install-links")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/rakelib/shakapacker_examples.rake` around lines 137 - 160, In
the pinned_react_version? branch you still run npm install with the original
caret range, so call pin_shakapacker_npm_version(example_type.dir) before any
npm install there (i.e., after bundle_install_in and before sh_in_dir that runs
"npm install --legacy-peer-deps --install-links") so package.json is rewritten
to exact shakapacker versions (and do this before unbundled_sh_in_dir("bundle
exec rake shakapacker:binstubs") as well).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react_on_rails/rakelib/shakapacker_examples.rake`:
- Around line 39-44: The current regex in shakapacker_gem_version_from_lockfile
only captures strict x.y.z and misses prerelease labels; update the match on
File.read(lockfile).match(...) to capture full version tokens including
prerelease segments (e.g. 9.6.0.beta.0 or 8.0.0.rc.2) — for example, match the
entire parenthesized version string instead of only digits (keep using the same
function name shakapacker_gem_version_from_lockfile and the existing match
variable and match&.[](1) return style) so prerelease versions are returned
instead of nil.

---

Outside diff comments:
In `@react_on_rails/rakelib/shakapacker_examples.rake`:
- Around line 137-160: In the pinned_react_version? branch you still run npm
install with the original caret range, so call
pin_shakapacker_npm_version(example_type.dir) before any npm install there
(i.e., after bundle_install_in and before sh_in_dir that runs "npm install
--legacy-peer-deps --install-links") so package.json is rewritten to exact
shakapacker versions (and do this before unbundled_sh_in_dir("bundle exec rake
shakapacker:binstubs") as well).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8494674c-9bf8-4d63-947a-a885fea153b3

📥 Commits

Reviewing files that changed from the base of the PR and between f2a0ab9 and 39fbbb6.

📒 Files selected for processing (1)
  • react_on_rails/rakelib/shakapacker_examples.rake

Comment thread react_on_rails/rakelib/shakapacker_examples.rake
Document all the places where the shakapacker version is pinned
(gem and npm, across OSS and Pro) so contributors know what to
update when bumping to a new release.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d75c22e7c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+)\)/)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept prerelease gem versions when pinning npm shakapacker

shakapacker_gem_version_from_lockfile only matches versions shaped like X.Y.Z, so it returns nil for valid lockfile entries such as shakapacker (9.6.0.rc1) or other prerelease/build forms. In that case pin_shakapacker_npm_version silently skips pinning, and npm install can still resolve a different package version than the gem, recreating the mismatch this change is meant to prevent when testing release candidates.

Useful? React with 👍 / 👎.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 8, 2026

Code review notes (see inline comments for specifics): (1) Regex in shakapacker_gem_version_from_lockfile only matches X.Y.Z - pre-release versions like 9.6.0.rc1 would return nil, silently skipping the pin. (2) pin_shakapacker_npm_version is missing rescue JSON::ParserError unlike update_package_json_for_react_version. (3) The pinned_react_version? branch runs npm install without calling pin_shakapacker_npm_version - if intentionally excluded, document why. Minor: JSON.pretty_generate reformats all keys in package.json; CONTRIBUTING.md section should note react-pinned examples are not covered by this helper.

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+)\)/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The regex \d+\.\d+\.\d+ only matches three-part numeric versions. A pre-release gem version like 9.6.0.rc1 or 10.0.0.beta.1 would not match, causing this method to return nil and silently skip the version pin.

Consider broadening the capture group to also accept pre-release segments:

Suggested change
match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+)\)/)
match = File.read(lockfile).match(/^\s+shakapacker\s+\((\d+\.\d+\.\d+[^\)]*)\)/)

This captures everything up to the closing ), preserving e.g. 9.6.0.rc1 correctly so the exact gem version can be pinned in package.json.

package_json_path = File.join(dir, "package.json")
return unless File.exist?(package_json_path)

package_json = JSON.parse(File.read(package_json_path))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pin_shakapacker_npm_version calls JSON.parse without error handling, while the nearby update_package_json_for_react_version wraps the same call in a rescue JSON::ParserError block (lines 59–63) that prints a descriptive message before re-raising.

A malformed package.json here would produce a cryptic Ruby backtrace instead of a useful error. Consider making these consistent:

Suggested change
package_json = JSON.parse(File.read(package_json_path))
begin
package_json = JSON.parse(File.read(package_json_path))
rescue JSON::ParserError => e
puts " ERROR: Failed to parse #{package_json_path}: #{e.message}"
raise
end

# shakapacker:install may add "^X.Y.Z" to package.json, which allows npm to
# resolve a newer minor version (e.g., 9.6.0 when gem is 9.5.0), causing
# Shakapacker's gem/npm version consistency check to fail.
pin_shakapacker_npm_version(example_type.dir)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pinned_react_version? branch (above this else) runs npm install without calling pin_shakapacker_npm_version first. The comment on lines 141–143 explicitly notes that the version-matching concern applies there too ("to ensure the npm shakapacker version matches the gem version").

If those examples are intentionally excluded because they lock an older shakapacker major version that prevents drift (e.g., ^8.2.0 with no published 8.3.x), please add a comment explaining the rationale, for example:

# Note: pinned_react_version? examples use an older shakapacker major (8.2.x)
# that has no newer published minor, so npm version drift is not a concern there.

Otherwise, consider calling pin_shakapacker_npm_version in that branch as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 6ac88c0. Moved pin_shakapacker_npm_version call into both branches (before each npm install) so both pinned_react_version? and default paths get pinned.

- Check both dependencies and devDependencies when pinning shakapacker
- Broaden regex to handle prerelease gem versions (e.g., 9.6.0.beta.0)
- Add JSON::ParserError rescue for consistency with nearby code
- Move pin call before both npm install branches so pinned_react_version?
  examples also get pinned

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 8, 2026

Review: Pin npm shakapacker version to match gem in example generation

The approach is sound and directly addresses the version drift problem. A couple of things worth noting:

Regex fragility in shakapacker_gem_version_from_lockfile

The pattern uses [^)]+ to capture anything inside parens after shakapacker. In a Gemfile.lock the DEPENDENCIES section contains lines like shakapacker (~> 9.0), and the GEM/specs section contains shakapacker (9.5.0). Since String#match returns the first match and specs precede DEPENDENCIES in Bundler output, this works in practice — but it relies on section ordering. A tighter pattern requiring the version to start with a digit would be safer: /^\s{4,}shakapacker \((\d[^)]*)\)/

JSON.pretty_generate silently reformats the whole file

When changed is true, the entire package.json is rewritten with JSON.pretty_generate 2-space indentation. If the generated file used different spacing the format changes beyond the version pin. Harmless for ephemeral gen-examples/ apps, but worth knowing when debugging.

Minor: rubocop disable

The Metrics/CyclomaticComplexity disable on pin_shakapacker_npm_version is understandable, but extracting a small pin_section helper would lower complexity naturally and remove the need for it.

Overall

The fix is correct and well-placed — version is pinned after bundle install (Gemfile.lock has the resolved gem version) and before any npm install call. The CONTRIBUTING.md section is a welcome addition. The regex robustness issue is the only change worth considering before merging.

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\(([^)]+)\)/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pattern [^)]+ will capture any text inside the parens, including semver constraint syntax. A Gemfile.lock DEPENDENCIES section can contain shakapacker (~> 9.0), which this regex would capture as ~> 9.0 if it appeared before the resolved version in the specs section. Locking it to lines where the captured text starts with a digit prevents that:

Suggested change
match = File.read(lockfile).match(/^\s+shakapacker\s+\(([^)]+)\)/)
match = File.read(lockfile).match(/^\s{4,}shakapacker \((\d[^)]*)\)/)

This also tightens the leading-whitespace requirement (\s{4,}) so it matches gem entries in the specs block and not shallower indented section headers.


# Pins the shakapacker npm package version to exactly match the installed gem version.
# Prevents semver range resolution (e.g., ^9.5.0 -> 9.6.0) from causing version mismatches.
def pin_shakapacker_npm_version(dir) # rubocop:disable Metrics/CyclomaticComplexity
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The cyclomatic complexity here is legitimately borderline, but it could be reduced — and the disable removed — by extracting the inner loop body into a small helper:

def pin_shakapacker_in_section(deps, section_name, gem_version)
  return false unless deps&.key?("shakapacker")
  return false if deps["shakapacker"] == gem_version

  puts "  Pinning npm shakapacker in #{section_name} from #{deps['shakapacker']} to exact #{gem_version}"
  deps["shakapacker"] = gem_version
  true
end

Then pin_shakapacker_npm_version just calls pin_shakapacker_in_section for each section and checks if any returned true.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ac88c044b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\(([^)]+)\)/)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parse locked shakapacker version from specs section

shakapacker_gem_version_from_lockfile takes the first shakapacker (...) match in Gemfile.lock, but generated lockfiles include dependency requirement lines (for example, react_on_rails lists shakapacker (>= 6.0)) before the actual installed spec entry. This means gem_version can become a constraint like >= 6.0 instead of the resolved version (e.g., 9.5.0), so pin_shakapacker_npm_version writes a range back to package.json and still allows npm to resolve a mismatched version.

Useful? React with 👍 / 👎.

Pin shakapacker after apply_react_version in the pinned_react_version?
branch to avoid the pin being overwritten by package.json modifications.
Add spec for pin_shakapacker_npm_version behavior.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@justin808 justin808 merged commit b1b772b into master Mar 8, 2026
26 of 29 checks passed
@justin808 justin808 deleted the jg/fix-shakapacker-960-compat branch March 8, 2026 07:10
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 8, 2026

Code Review

The approach here is sound - pinning the npm shakapacker version to the installed gem version after shakapacker:install is the right fix for the CI drift issue.

Potential Bug 1: Pre-release version format mismatch
Ruby gem pre-release versions use dots (e.g. 9.6.0.beta.0) but npm pre-release versions use hyphens (e.g. 9.6.0-beta.0). The spec tests that shakapacker_gem_version_from_lockfile correctly reads 9.6.0.beta.0, but pinning the npm version to that string would likely fail — npm would not recognize 9.6.0.beta.0 as a valid semver. If pre-release gem versions are ever used in CI, this would silently write an invalid version string to package.json.

Potential Bug 2: Regex fragility — could match DEPENDENCIES section
The regex \s+shakapacker\s+\(([^)]+)\) would also match the DEPENDENCIES section of Gemfile.lock if it contains a constraint like shakapacker (>= 6.0). Since String#match returns the first match and GEM specs normally appear before DEPENDENCIES, this works in practice — but it is fragile. A regex anchored to look only inside the GEM specs block would be more robust.

Missing test: non-pinned React (else) branch
The spec verifies ordering for the pinned_react_version? branch but not for the else branch. Adding a parallel test that pin_shakapacker_npm_version is called before npm install --install-links would close the coverage gap.

Minor nits

  • The four-line comment explaining the pinning rationale is duplicated identically in both if/else branches. Since the method name is self-documenting, a one-line comment per branch would be cleaner.
  • rubocop:disable Metrics/CyclomaticComplexity on pin_shakapacker_npm_version is slightly defensive for a method of that size.

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\(([^)]+)\)/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The regex \s+shakapacker\s+\(([^)]+)\) is fragile in two ways: (1) it matches the DEPENDENCIES section too, which may contain shakapacker (>= 6.0) — the first match wins, so if DEPENDENCIES somehow appeared first the captured value would be a constraint, not an exact version; (2) it does not guard against similarly-named gems (e.g. a hypothetical shakapacker-pro). Anchoring with \bshakapacker\b and/or scoping the scan to the GEM specs block would make this more reliable.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c5b384c6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

lockfile = File.join(dir, "Gemfile.lock")
return unless File.exist?(lockfile)

match = File.read(lockfile).match(/^\s+shakapacker\s+\(([^)]+)\)/)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Read shakapacker version from locked gem spec

shakapacker_gem_version_from_lockfile takes the first shakapacker (...) match anywhere in Gemfile.lock, which can be a dependency constraint (for example shakapacker (>= 6.0)) listed under another gem before the actual locked spec entry. In that case pin_shakapacker_npm_version writes a range back to package.json instead of the resolved gem version, so npm can still resolve a different minor version and the gem/npm mismatch this change is meant to prevent can still happen.

Useful? React with 👍 / 👎.

end
end

it "reads prerelease shakapacker gem versions from Gemfile.lock" do
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test verifies that the pre-release version string is read correctly, but there is no follow-on test confirming that pin_shakapacker_npm_version actually writes 9.6.0.beta.0 to package.json and that npm would accept it. Ruby gem pre-release versions use dots (9.6.0.beta.0) while npm expects hyphens (9.6.0-beta.0). If a pre-release gem is ever installed, the pinned npm version string would be invalid and npm install would fail.


Rake::Task["shakapacker_examples:gen_example_app"].invoke
end
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no corresponding test for the else branch (non-pinned React examples) that verifies pin_shakapacker_npm_version is invoked before npm install --install-links. Consider adding a sibling describe block with pinned_react_version?: false to cover that path.

justin808 added a commit that referenced this pull request Mar 8, 2026
## Summary

- Fix CI example generation failure when a new shakapacker version is
published to npm before/independently of the gem
- `shakapacker:install` adds the npm package with a caret range (e.g.,
`^9.5.0`), so `npm install` can resolve to a newer minor version (e.g.,
9.6.0) while the gem remains at 9.5.0
- Added `pin_shakapacker_npm_version` that reads the exact gem version
from the example app's `Gemfile.lock` and pins the npm `package.json`
entry to match

Fixes the `examples (3.4, latest)` CI failure seen on PR #2516 where npm
shakapacker resolved to 9.6.0 while the gem was 9.5.0.

## Test plan

- [ ] CI `examples (3.4, latest)` job passes
- [ ] Verify that generated example apps have matching gem and npm
shakapacker versions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change to example-generation rake tasks that only
adjusts dependency pinning and should not affect runtime application
behavior.
> 
> **Overview**
> Prevents CI/example app generation failures caused by `shakapacker`
gem/npm version drift by **pinning the npm `shakapacker` dependency to
the exact gem version** after `shakapacker:install`.
> 
> Adds helpers to read the installed Shakapacker gem version from
`Gemfile.lock` and rewrite `package.json` when it contains a semver
range (e.g., `^X.Y.Z`) before running `npm install` for non-react-pinned
examples.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
39fbbb6. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Automatically syncs the npm Shakapacker dependency to the installed
gem version during example app generation to prevent mismatches.
* **Bug Fixes**
* Improved error handling when reading package metadata and only
rewrites files when changes occur.
* **Documentation**
* Added CONTRIBUTING guidance on pinning and updating the Shakapacker
version and regenerating lockfiles.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
justin808 added a commit that referenced this pull request Mar 9, 2026
## Summary

- Add changelog entries for 6 user-visible PRs merged since v16.4.0.rc.6
that were missing from `[Unreleased]`
- Update existing #2561 entry to include #2568 contributor credit

### New entries added

| Section | PR | Description |
|---|---|---|
| Added | #2539 | Environment-variable-driven ports in Procfile
templates |
| Fixed | #2417 | Rspack generator config path fix |
| Fixed | #2419 | Precompile hook load-based execution fix |
| Fixed | #2577 | `create-react-on-rails-app` validation improvements |
| Pro Fixed | #2416 | StreamResponse status fallback fix |
| Pro Fixed | #2566 | Empty-string license plan mismatch fix |

### Skipped PRs (not user-visible)

Docs (#2406, #2414, #2479, #2494, #2518, #2537, #2544), CI/internal
(#2533, #2547, #2555, #2557, #2558, #2564), dependabot (#2387, #2541),
dev dependencies (#2559, #2569, #2573).

## Test plan

- [ ] Verify changelog formatting matches existing entries
- [ ] Verify all user-visible PRs since v16.4.0.rc.6 are covered

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changelog updates with no runtime or build behavior
changes.
> 
> **Overview**
> Updates `CHANGELOG.md`’s **[Unreleased]** section to include
previously missing user-facing entries: Procfile templates now support
env-driven ports, several generator/`bin/dev` precompile-hook and
rspack-path fixes are documented, and `create-react-on-rails-app`
validation improvements are noted.
> 
> Also adds two Pro fix entries (StreamResponse status fallback and
license plan empty-string validation) and updates the existing `bin/dev`
precompile-hook entry to credit an additional PR/contributor.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e75d2b5. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 4.6 <[email protected]>
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