Skip to content

Conversation

justin808
Copy link
Member

@justin808 justin808 commented Oct 2, 2025

Summary

Adds comprehensive linting and code quality enforcement using RuboCop.

Changes

1. RuboCop Configuration

  • Added RuboCop with extensions (rubocop-rake, rubocop-rspec)
  • Created .rubocop.yml with sensible defaults
  • Enforces frozen string literals, trailing newlines, consistent style

2. Rake Tasks (rakelib/lint.rake)

  • rake lint - Run all linters
  • rake lint:fix - Auto-fix issues
  • rake check_newlines - Verify files end with newlines
  • rake fix_newlines - Fix missing newlines
  • rake ci - Run all checks (specs, lint, newlines)

3. Pre-commit Hook (bin/install-hooks)

  • Installable script for git pre-commit hook
  • Checks newlines on all files
  • Runs RuboCop on staged Ruby files
  • Prevents commits with issues

4. GitHub Actions (.github/workflows/lint.yml)

  • Runs on all PRs and pushes to master
  • Enforces linting in CI
  • Checks for missing newlines

Benefits

  • ✅ Consistent code style across the project
  • ✅ All files guaranteed to end with newlines
  • ✅ Automated enforcement both locally and in CI
  • ✅ Easy to fix issues with rake lint:fix and rake fix_newlines

Installation

To install pre-commit hook locally:

./bin/install-hooks

This is a separate PR from the release script changes for independent review.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Pre-commit hook to run linting and trailing-newline checks before commits.
    • Unified command to run all CI checks (tests, linting, newline validation).
  • Chores

    • Automated linting added to run on pushes and pull requests via GitHub Actions.
    • Introduced RuboCop configuration with sensible defaults and RSpec/Rake rules.
    • Added development dependencies for linting tools.
    • New commands to run and auto-fix lint issues and ensure files end with a newline.

Set up automated linting and code quality tools:

1. Added RuboCop with extensions:
   - rubocop-rake for rake task linting
   - rubocop-rspec for RSpec linting
   - Created .rubocop.yml with sensible defaults

2. Created rake tasks for linting (rakelib/lint.rake):
   - rake lint - Run all linters
   - rake lint:fix - Auto-fix issues
   - rake check_newlines - Verify files end with newlines
   - rake fix_newlines - Fix missing newlines

3. Added pre-commit hook installer (bin/install-hooks):
   - Checks for missing newlines
   - Runs RuboCop on staged Ruby files
   - Prevents commits with linting issues

4. Added GitHub Actions workflow (.github/workflows/lint.yml):
   - Runs on all PRs and pushes to master
   - Enforces linting in CI
   - Checks for missing newlines

5. Updated CI workflow:
   - Added :ci task to run specs, lint, and newline checks
   - Ensures all checks pass before merge

To install pre-commit hook: ./bin/install-hooks

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

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

coderabbitai bot commented Oct 2, 2025

Warning

Rate limit exceeded

@justin808 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 24 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c95af2 and c354937.

📒 Files selected for processing (3)
  • .github/workflows/lint.yml (1 hunks)
  • .rubocop.yml (1 hunks)
  • cypress-on-rails.gemspec (1 hunks)

Walkthrough

Adds linting infrastructure: a GitHub Actions workflow for RuboCop and newline checks, RuboCop configuration, Rake tasks for linting and newline hygiene (plus a CI aggregate task), a git pre-commit hook installer to run checks locally, and development dependencies for RuboCop.

Changes

Cohort / File(s) Summary of Changes
CI Lint Workflow
.github/workflows/lint.yml
Adds a GitHub Actions workflow to run RuboCop and a newline check on pushes/PRs to main/master using Ruby 3.2 with Bundler caching.
RuboCop Configuration
.rubocop.yml
Introduces RuboCop config: plugins (rake, rspec), Ruby 2.6 target, new cops enabled, directory exclusions, layout/style/metrics rules, and RSpec limits.
Rake Lint + CI Tasks
Rakefile, rakelib/lint.rake
Adds a ci task aggregating specs, lint, and newline checks. Provides tasks: rubocop, rubocop:auto_correct, lint, lint:fix, check_newlines, fix_newlines.
Git Hook Installer
bin/install-hooks
Adds a script to install a pre-commit hook that enforces trailing newline checks and runs RuboCop on staged Ruby files; aborts commit on failure.
Dev Dependencies
cypress-on-rails.gemspec
Adds development dependencies: rubocop, rubocop-rake, rubocop-rspec.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant GH as GitHub
  participant GA as Actions: Lint Job
  participant Ruby as Ruby/Rubygems
  participant Rake as Rake Tasks

  Dev->>GH: Push / Open PR (main/master)
  GH-->>GA: Trigger workflow
  GA->>GA: Checkout repo
  GA->>Ruby: Setup Ruby 3.2 + Bundler cache
  GA->>Rake: bundle exec rubocop
  Rake-->>GA: RuboCop status
  GA->>Rake: bundle exec rake check_newlines
  Rake-->>GA: Newline check status
  GA-->>GH: Report job status (pass/fail)
  note over GA: New/changed: automated lint + newline checks
Loading
sequenceDiagram
  autonumber
  actor Dev as Developer
  participant Hook as pre-commit hook
  participant Rake as Rake
  participant RC as RuboCop

  Dev->>Hook: git commit
  Hook->>Rake: rake check_newlines
  alt Missing final newline
    Rake-->>Hook: Non-zero exit
    Hook-->>Dev: Abort commit with message
  else All files ok
    Hook->>Hook: Detect staged *.rb/*.rake
    alt Ruby files staged
      Hook->>RC: bundle exec rubocop --only staged files
      alt Lint failures
        RC-->>Hook: Non-zero exit
        Hook-->>Dev: Abort commit with message
      else Clean
        RC-->>Hook: Zero exit
        Hook-->>Dev: Commit proceeds
      end
    else No Ruby files
      Hook-->>Dev: Commit proceeds
    end
  end
  note over Hook: New local guardrails before commit
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I thump my paws on tidy ground,
Newlines bloom where ends are found.
RuboCop whispers, “straighten that brace,”
Rake runs by in a linting race.
Hooks on duty, carrots aligned—
Commit with joy, code well-defined. 🥕

Pre-merge checks and finishing touches

✅ 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 concisely and accurately summarizes the primary change of adding RuboCop linting and newline enforcement across the project without unnecessary detail or noise.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

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

claude bot commented Oct 2, 2025

Code Review - PR #193: Add RuboCop linting and newline enforcement

Summary

This PR adds comprehensive linting infrastructure with RuboCop, automated newline enforcement, and CI/CD integration. Overall, this is a well-structured and valuable addition to the project that will improve code quality and consistency.

✅ Strengths

1. Comprehensive Linting Setup

  • Well-configured .rubocop.yml with sensible defaults for a gem project
  • Appropriate exclusions (vendor, tmp, pkg, node_modules, specs_e2e)
  • Target Ruby version 2.6 aligns with gem compatibility needs
  • Good balance between strictness and pragmatism

2. Developer Experience

  • Clear rake tasks with intuitive naming (lint, lint:fix, check_newlines, fix_newlines)
  • Excellent pre-commit hook with helpful error messages and auto-fix suggestions
  • Optional hook installation via ./bin/install-hooks - non-invasive approach
  • CI task consolidation with rake ci

3. CI/CD Integration

  • Separate workflow file keeps concerns separated
  • Uses modern GitHub Actions (checkout@v4, ruby/setup-ruby@v1)
  • Bundler caching for faster CI runs

⚠️ Issues & Suggestions

High Priority

1. Ruby Version Mismatch in CI (.github/workflows/lint.yml:19)
The lint workflow uses Ruby 3.2, but .rubocop.yml targets Ruby 2.6, and the main test workflow tests against 2.7.6, 3.1.3, and 3.2.2. Recommend using Ruby 2.7.6 (minimum supported) to ensure RuboCop rules work across all versions.

2. Missing File Encoding Check in Newline Tasks (rakelib/lint.rake:28, 50)
File.read without explicit encoding may fail on files with non-UTF-8 encoding or binary files. Add encoding handling and rescue for binary files.

3. Inefficient File Globbing (rakelib/lint.rake:23, 45)
Reads all files then filters, including large directories like vendor/ and node_modules/. Exclude directories in glob pattern or use regex-based rejection.

Medium Priority

4. Pre-commit Hook Runs Against All Files (bin/install-hooks:16)
Checks ALL files on every commit, even if only one file changed. This can be slow in large repos. Only check staged files to improve performance.

5. Shell Script Quoting Issue (bin/install-hooks:24, 26)
If filenames contain spaces, the RuboCop command will break. Use xargs for proper handling of filenames with spaces.

6. RuboCop Configuration: Frozen String Literals (.rubocop.yml:54-58)
Enabling this will require adding frozen_string_literal comments to ALL Ruby files in the project. This wasn't mentioned in the PR description. Either run rake lint:fix before merging to add all frozen string literals across the codebase, or start with Enabled: false and enable in a follow-up PR.

7. Missing Version Constraints (cypress-on-rails.gemspec:25-27)
No version constraints could lead to breaking changes when RuboCop releases major versions. Add version constraints like '~> 1.50' for rubocop.

Low Priority

8. Task Description Clarity (rakelib/lint.rake:8-9)
The -A flag does auto-correct all (unsafe corrections too). Consider documenting this in the task description.

9. CI Task Doesn't Match Default (Rakefile:9, 11)
default doesn't include linting, but ci does. This inconsistency means local rake runs won't catch lint issues. Consider whether default should also run lint.

10. Missing CHANGELOG Entry
No CHANGELOG.md entry for these significant changes. Add entry documenting new rake tasks, RuboCop addition, and pre-commit hook.

🔒 Security Considerations

✅ No security concerns identified:

  • Pre-commit hook is opt-in (requires manual installation)
  • No credentials or secrets handling
  • RuboCop extensions are from trusted sources

🧪 Test Coverage

⚠️ Missing Tests:

  • No tests for the new rake tasks (check_newlines, fix_newlines)
  • bin/install-hooks script is untested

Consider adding basic specs to verify the rake tasks work correctly.

📊 Performance Considerations

🎯 Recommendations Summary

Before Merging:

  1. ✋ Fix Ruby version mismatch in lint.yml (Usage instructions #1)
  2. ✋ Add encoding handling to newline tasks (Seems to generate a cypress directory in the main folder #2)
  3. ✋ Either disable frozen string literals or run autofixes on entire codebase (Fixes for first run #6)
  4. 👍 Add version constraints to RuboCop gems (Making cypress on rails more generic #7)
  5. 👍 Add CHANGELOG entry (Add deprecation warnings #10)

Follow-up PRs:

Overall Assessment

LGTM with changes requested 🟡

This is high-quality work that will significantly improve the project's code quality and maintainability. The configuration is sensible, the developer experience is excellent, and the CI integration is clean. The issues identified are mostly minor optimizations, except for items #1, #2, and #6 which should be addressed before merging.

Great job on this PR! 🎉

Copy link

@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: 4

🧹 Nitpick comments (3)
cypress-on-rails.gemspec (1)

25-27: Pin RuboCop versions to prevent breaking changes.

Development dependencies without version constraints can introduce breaking changes or security vulnerabilities when newer versions are released. Consider pinning to major versions to ensure consistent behavior across environments.

Apply this diff to add version constraints:

-  s.add_development_dependency 'rubocop'
-  s.add_development_dependency 'rubocop-rake'
-  s.add_development_dependency 'rubocop-rspec'
+  s.add_development_dependency 'rubocop', '~> 1.0'
+  s.add_development_dependency 'rubocop-rake', '~> 0.6'
+  s.add_development_dependency 'rubocop-rspec', '~> 3.0'
rakelib/lint.rake (2)

23-29: Consider memory efficiency for large files.

Reading entire files into memory (line 28) could cause issues with large files (e.g., lockfiles, generated assets). Consider checking file size before reading or using a buffered approach that only reads the last bytes.

Apply this diff to add a size check:

     next unless File.file?(file)
 
+    # Skip files larger than 10MB to avoid memory issues
+    next if File.size(file) > 10 * 1024 * 1024
+    
     content = File.read(file)
     files_without_newline << file unless content.empty? || content.end_with?("\n")

Alternatively, for a more robust solution that handles large files:

# Check only the last byte(s) for newline without loading entire file
File.open(file, 'rb') do |f|
  f.seek(-1, IO::SEEK_END) rescue next
  files_without_newline << file unless f.read(1) == "\n"
end

23-25: Extract duplicate file filtering logic.

The file exclusion logic is duplicated between check_newlines and fix_newlines. Extract this into a shared helper method to follow DRY principles and ease maintenance.

Add this helper at the top of the file (after line 1):

# frozen_string_literal: true

def files_to_check
  Dir.glob('**/*.{rb,rake,yml,yaml,md,gemspec,ru,erb,js,json}').each do |file|
    next if file.include?('vendor/') || file.include?('node_modules/') || file.include?('.git/')
    next if file.include?('pkg/') || file.include?('tmp/') || file.include?('coverage/')
    next unless File.file?(file)
    yield file
  end
end

Then update both tasks:

 task :check_newlines do
   files_without_newline = []
 
-  Dir.glob('**/*.{rb,rake,yml,yaml,md,gemspec,ru,erb,js,json}').each do |file|
-    next if file.include?('vendor/') || file.include?('node_modules/') || file.include?('.git/')
-    next if file.include?('pkg/') || file.include?('tmp/') || file.include?('coverage/')
-    next unless File.file?(file)
-
+  files_to_check do |file|
     content = File.read(file)
     files_without_newline << file unless content.empty? || content.end_with?("\n")
   end

Also applies to: 45-47

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8615fc7 and 5c95af2.

📒 Files selected for processing (6)
  • .github/workflows/lint.yml (1 hunks)
  • .rubocop.yml (1 hunks)
  • Rakefile (1 hunks)
  • bin/install-hooks (1 hunks)
  • cypress-on-rails.gemspec (1 hunks)
  • rakelib/lint.rake (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: claude-review
  • GitHub Check: rails_6_1
  • GitHub Check: rails_7_2
  • GitHub Check: rails_8
🔇 Additional comments (3)
rakelib/lint.rake (1)

3-11: LGTM!

The RuboCop task definitions are correct and use safe, hardcoded commands.

Rakefile (1)

8-9: LGTM!

The CI task provides a convenient entry point for running all checks. The task dependencies are correctly defined and the tasks exist in the appropriate files.

.rubocop.yml (1)

5-7: Verify Ruby 2.6 support or update to a supported version
Ruby 2.6 reached EOL in March 2022 and no longer receives security updates. Confirm whether the project must still support 2.6; if not, bump TargetRubyVersion in .rubocop.yml to 2.7 or 3.x.

Comment on lines +14 to +20
# Check for files missing newlines
echo "Checking for files missing newlines..."
bundle exec rake check_newlines
if [ $? -ne 0 ]; then
echo "❌ Some files are missing final newlines. Run 'bundle exec rake fix_newlines' to fix."
exit 1
fi
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Newline check runs on all files, not just staged.

The check_newlines task (line 16) checks all files in the repository, not just staged files. This is inconsistent with the RuboCop check which only lints staged files. This could:

  • Fail commits due to existing issues in unstaged files
  • Prevent developers from making incremental fixes
  • Slow down pre-commit hooks unnecessarily

Consider checking only staged files or documenting this intentional behavior.

If you want to check only staged files, apply this diff:

   # Check for files missing newlines
   echo "Checking for files missing newlines..."
-  bundle exec rake check_newlines
+  staged_files=$(git diff --cached --name-only --diff-filter=ACM)
+  if [ -n "$staged_files" ]; then
+    echo "$staged_files" | while IFS= read -r file; do
+      if [ -f "$file" ] && ! [ -s "$file" ]; then
+        continue
+      fi
+      if [ -f "$file" ] && [ -n "$(tail -c 1 "$file")" ]; then
+        echo "❌ File missing final newline: $file"
+        exit 1
+      fi
+    done
+  fi
-  if [ $? -ne 0 ]; then
-    echo "❌ Some files are missing final newlines. Run 'bundle exec rake fix_newlines' to fix."
-    exit 1
-  fi

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +24 to +30
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(rb|rake)$')
if [ -n "$files" ]; then
bundle exec rubocop $files
if [ $? -ne 0 ]; then
echo "❌ RuboCop failed. Fix issues or run 'bundle exec rake lint:fix'"
exit 1
fi
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Fix command injection vulnerability in git hook.

Line 26 has an unquoted variable $files which creates a command injection vulnerability. If a malicious actor creates a file with special characters in the name (spaces, semicolons, backticks, etc.), they could execute arbitrary commands during commit.

Apply this diff to properly quote the variable:

   # Run RuboCop on staged Ruby files
   echo "Running RuboCop on staged files..."
   files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(rb|rake)$')
   if [ -n "$files" ]; then
-    bundle exec rubocop $files
+    bundle exec rubocop $files
     if [ $? -ne 0 ]; then
       echo "❌ RuboCop failed. Fix issues or run 'bundle exec rake lint:fix'"
       exit 1

Wait, I need to check the actual quoting. Let me re-examine line 26 in the provided code.

Actually, looking at line 26: bundle exec rubocop $files, the variable IS unquoted. This needs to be fixed:

-    bundle exec rubocop $files
+    bundle exec rubocop "$files"

However, this still has an issue - if there are multiple files, they'll be treated as a single argument. The proper fix is:

   files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(rb|rake)$')
   if [ -n "$files" ]; then
-    bundle exec rubocop $files
+    echo "$files" | xargs bundle exec rubocop
     if [ $? -ne 0 ]; then

Or use a while loop for better handling:

-  files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(rb|rake)$')
-  if [ -n "$files" ]; then
-    bundle exec rubocop $files
-    if [ $? -ne 0 ]; then
-      echo "❌ RuboCop failed. Fix issues or run 'bundle exec rake lint:fix'"
-      exit 1
-    fi
-  fi
+  git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(rb|rake)$' | \
+  while IFS= read -r file; do
+    bundle exec rubocop "$file"
+    if [ $? -ne 0 ]; then
+      echo "❌ RuboCop failed. Fix issues or run 'bundle exec rake lint:fix'"
+      exit 1
+    fi
+  done

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In bin/install-hooks around lines 24 to 30, the script passes an unquoted $files
variable to rubocop which creates a command-injection and argument-splitting
issue; update the hook to safely handle filenames by iterating over the list of
staged files (or by using an array/quoted expansion) and invoking rubocop with
each filename as a separate, properly quoted argument, ensuring the variable is
quoted and/or looped so filenames with spaces or special characters cannot be
interpreted as shell metacharacters.

Comment on lines +45 to +55
Dir.glob('**/*.{rb,rake,yml,yaml,md,gemspec,ru,erb,js,json}').each do |file|
next if file.include?('vendor/') || file.include?('node_modules/') || file.include?('.git/')
next if file.include?('pkg/') || file.include?('tmp/') || file.include?('coverage/')
next unless File.file?(file)

content = File.read(file)
unless content.empty? || content.end_with?("\n")
File.write(file, content + "\n")
fixed_files << file
end
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and consider TOCTOU risks.

The current implementation has potential issues:

  1. Memory efficiency: Same concern as check_newlines - reading large files into memory
  2. TOCTOU race condition: File could be modified between read (line 50) and write (line 52)
  3. No error handling: File.write could fail partway through, potentially corrupting files
  4. Atomicity: Should write to temp file and rename for atomic updates

Apply this diff to improve safety:

     content = File.read(file)
     unless content.empty? || content.end_with?("\n")
-      File.write(file, content + "\n")
-      fixed_files << file
+      begin
+        # Write to temp file first for atomicity
+        temp_file = "#{file}.tmp"
+        File.write(temp_file, content + "\n")
+        File.rename(temp_file, file)
+        fixed_files << file
+      rescue => e
+        warn "Failed to fix #{file}: #{e.message}"
+        File.delete(temp_file) if File.exist?(temp_file)
+      end
     end

Or consider using File.open with append mode which is safer for just adding a newline:

unless content.empty? || content.end_with?("\n")
  begin
    File.open(file, 'a') { |f| f.write("\n") }
    fixed_files << file
  rescue => e
    warn "Failed to fix #{file}: #{e.message}"
  end
end
🤖 Prompt for AI Agents
In rakelib/lint.rake around lines 45–55, avoid reading entire file into memory
and fix TOCTOU/atomicity and error handling: instead of File.read + File.write,
open the file and check only the final byte (seek to the end or use File.size?)
to determine if a trailing newline is missing, then either append a newline with
File.open(file, 'a') inside a begin/rescue to handle and log errors, or write to
a temporary file and perform an atomic rename (write to tmp in same directory,
fsync, then File.rename) to avoid corruption; ensure you rescue exceptions from
IO operations, warn or log failures including the filename and error message,
and only add the file to fixed_files on successful write/rename.

- Set minimum Ruby version to 3.0 across all configurations
- Updated .rubocop.yml TargetRubyVersion from 2.6 to 3.0
- Updated GitHub Actions workflow to use Ruby 3.0
- Added required_ruby_version >= 3.0.0 to gemspec
- Pinned RuboCop versions for consistency:
  - rubocop ~> 1.81
  - rubocop-rake ~> 0.7
  - rubocop-rspec ~> 3.7
- Updated to use new plugin syntax instead of require

This ensures consistency between CI, local development, and RuboCop
configuration. Ruby 3.0 is a reasonable minimum for modern projects.

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

Co-Authored-By: Claude <noreply@anthropic.com>
@MUTOgen
Copy link
Contributor

MUTOgen commented Oct 3, 2025

LGTM
Let's try this out on CI

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.

2 participants