Skip to content

Linting & Code Quality Tools

This project uses multiple linters to maintain code quality across Ruby, ERB, JavaScript, and SCSS.

Hybrid — local on demand, GitHub on PR. As of May 2026 the per-PR static-analysis workflows (pronto.yml, yard-lint.yml, bundler-cache-warm.yml) have been retired in favor of a single bin/ci script that runs both locally on demand and on PR via .github/workflows/ci.yml.

WhereWhat runsGate
bin/ci (local, on demand)Reek diff · yard-lint diff · bundle-audit · full Minitest suiteManual; nothing auto-runs on push
.github/workflows/ci.yml (PR)bin/ci --quick (lint + security; no test suite)Per-PR check
.github/workflows/ci.yml (workflow_dispatch)Full bin/ci (manual button to also run the test suite on a runner)Manual
CodeRabbit (PR)AI review across the diff (RuboCop + Brakeman context, semantic suggestions)Inline comments only — no merge gate
design-md-lint.ymlCustom DESIGN.md validator (only when DESIGN.md is touched)PR check
yard-docs.ymlGenerates + deploys YARD docsOn push to master

There is no pre-push hook. Devs run bin/ci manually before opening a PR; the GH PR check catches anyone who skips the local run.

A single entrypoint that runs every static check + the full test suite, in order, and reports a final pass/fail summary. ~6-8 minutes for a typical change (linters fast, test suite ~5 min).

Terminal window
bin/ci # full pipeline (default for pre-push)
bin/ci --quick # skip the test suite (linters + security only)
bin/ci --tests-only # skip the linters, just the test suite
bin/ci --since=HEAD~1 # tighten the diff window for diff-only checks
bin/ci --help # full option reference

Steps:

  1. Reek on the diff (Pronto runner vs origin/master by default)
  2. yard-lint on touched Ruby files (app/**/*.rb, lib/**/*.rb)
  3. bundle-audit (Gemfile.lock CVE scan)
  4. Minitest full suite

Steps don’t short-circuit — every step runs even if an earlier one failed, so you see the full picture in one run.

RuboCop and Brakeman are intentionally not gated by bin/ci. CodeRabbit reviews both on every PR (.coderabbit.yamltools.rubocop / tools.brakeman), and the codebase has pre-existing backlogs in both: Metrics/* warnings on legacy long classes for RuboCop, and High-confidence findings on legacy controllers/views for Brakeman. Gating push on those would block every commit until they’re fixed. Run manually as needed:

Terminal window
mise exec -- bundle exec rubocop <files> # one file at a time
mise exec -- bundle exec brakeman --quiet --no-pager -w 3 # High-confidence only

No pre-push hook is installed. Run bin/ci manually before opening a PR — the GH PR check (.github/workflows/ci.yml) re-runs the static checks on the runner so a forgotten local run still gets caught.

Terminal window
# Ruby/Rails
bundle exec rubocop # Ruby style & best practices
bundle exec rubocop --autocorrect-all # Auto-fix Ruby issues
bundle exec brakeman # Security vulnerabilities
bundle exec bundler-audit check --update # Dependency vulnerabilities
bundle exec database_consistency # Model/DB consistency (eager_loads app; needs clean boot)
# Pronto — runs rubocop + reek + brakeman ONLY on lines changed vs origin/master
# (CI narrows this to reek-only on PRs; see Pronto section)
bundle exec pronto run # All committed changes on the branch
bundle exec pronto run --unstaged # Working-tree changes
bundle exec pronto run --staged # Staged changes only
bundle exec pronto run -r reek # Match what CI posts on PRs
# ERB Templates (Herb — HTML-aware)
yarn lint:erb # Lint views and components
yarn lint:erb:fix # Auto-fix ERB issues
npx @herb-tools/linter app/views # Lint specific directory
# JavaScript
yarn lint:js # ESLint for JS/JSX
yarn eslint . --fix # Auto-fix JS issues
# CSS/SCSS
yarn lint:css # Stylelint for SCSS
yarn lint:css:fix # Auto-fix SCSS issues
# All JS + CSS
yarn lint # Run both JS and CSS linters
  • Config: .rubocop.yml
  • Purpose: Ruby style, syntax, and best practices

Plugins included:

PluginPurpose
rubocop-railsRails-specific cops
rubocop-performancePerformance optimizations
rubocop-minitestMinitest best practices
rubocop-capybaraCapybara test syntax
rubocop-factory_botFactoryBot syntax
rubocop-yardYARD documentation
  • Purpose: Static security analysis
  • Run: bundle exec brakeman
  • Purpose: Check for known vulnerabilities in dependencies
  • Run: bundle exec bundler-audit check --update
  • Suppressions: .bundler-audit.yml lists GHSAs we’ve intentionally deferred. Each entry carries an exposure analysis and a re-evaluate date — when you see a new advisory in CI output, either upgrade or add a justified suppression with the same shape; never silence one with a bare GHSA id and no comment.
  • Config: .pronto.yml (runners list, default commit), per-tool config (.rubocop.yml, .reek.yml).

  • Purpose: Re-runs rubocop, reek, and brakeman but reports only on lines that differ between HEAD and origin/master. Each runner re-uses its own project config, so behaviour is identical to running the underlying tool — Pronto just filters down to “things this branch introduced”.

  • Local-only as of May 2026. pronto.yml was retired in favor of bin/ci calling pronto -r reek locally on pre-push. CodeRabbit still reviews RuboCop + Brakeman on PRs (semantic, with model context), so there’s no value in re-posting Pronto’s output to GitHub.

    Static-analysis split end-to-end:

    ToolWhere it runsNotes
    bin/ci → Pronto/Reeklocal pre-pushOnly signal CodeRabbit doesn’t carry.
    bin/ci → Brakemanlocal pre-push-w 2 (Medium+ confidence).
    bin/ci → yard-lintlocal pre-pushYARD docstrings on touched files.
    bin/ci → bundler-auditlocal pre-pushGemfile.lock CVE scan.
    bin/ci → Minitestlocal pre-pushFull test suite.
    CodeRabbit → RuboCopPR diffAI review w/ rubocop context.
    CodeRabbit → BrakemanPR diffAI review w/ brakeman context (in addition to bin/ci’s gate).
    CodeRabbit (AI review)PR diffSemantic review, custom path instructions, pre-merge checks.
  • Why diff-only at all? The full rubocop / rubycritic reports surface thousands of pre-existing offenses that nobody is going to fix mid-feature. Pronto narrows the feedback to “things this branch introduced”, which is what you actually want during code review.

  • Native dep: Pronto pulls in rugged, which needs cmake. Installed via the project Brewfile (brew bundle).

Local usage:

Terminal window
bundle exec pronto run # diff vs origin/master, all runners
bundle exec pronto run --unstaged # uncommitted working-tree changes
bundle exec pronto run --staged # staged changes (good for pre-commit)
bundle exec pronto run -r reek # mirror what CI posts on PRs
bundle exec pronto run -r rubocop # single runner (any of: rubocop, reek, brakeman)
bundle exec pronto run -c HEAD~3 # diff vs an arbitrary commit-ish
bundle exec pronto run -f text # default formatter; use `json` for piping

CI: .github/workflows/pronto.yml runs on every pull request, narrows pronto to -r reek, and posts inline review comments on the PR diff plus a commit status. Requires the BUNDLE_GEMS__CONTRIBSYS__COM repo secret (Sidekiq Pro credentials, same as any other Ruby CI job would need).

Large legacy PRs: the workflow falls back to a status-only formatter if it hits GitHub’s secondary rate limit on PR-review content creation (typical when Pronto first encounters a huge pre-existing diff). The full report still appears in the CI logs via the text formatter — search the workflow log for Pronto:: to see findings.

Cache warming: .github/workflows/bundler-cache-warm.yml runs on pushes to master that touch Gemfile* / .ruby-version / .tool-versions. It seeds the ruby/setup-ruby@v1 bundler cache so new PRs hit a warm cache on their very first run (~1.5 min of setup vs ~5 min cold). Manual re-warm: gh workflow run bundler-cache-warm.yml --ref master.

Disabling on a single line: Pronto has no syntax of its own — use the underlying tool’s pragma (# rubocop:disable … for local rubocop runs, # :reek:SmellName for Reek, etc.). If a runner becomes too noisy on diffs, exclude paths in .pronto.yml or tune the underlying config (.reek.yml, .rubocop.yml).

  • Config: .database_consistency.yml
  • Purpose: Targeted checks only — RedundantIndexChecker, EnumValueChecker, EnumTypeChecker. A default gem run on this codebase is mostly noise (optional FKs, View* models, unique indexes without model validators, etc.).
  • Autofix: bundle exec database_consistency -f (review all changes)
  • Note: Needs clean boot (eager_load). Recent enum alignments: Rma#replacement_order_type vs PG order_type; AmazonAPlusContent#status as string-backed enum (migration backfills legacy '0''3' rows).
  • Config: .herb.yml
  • Purpose: HTML-aware ERB parser with intelligent linting
  • Package: @herb-tools/linter (npm)

Key features:

  • Understands HTML structure and context
  • Detects unclosed tags, invalid nesting
  • Smart indentation awareness
  • VS Code extension: Herb LSP (marcoroth.herb-lsp)

Run:

Terminal window
yarn lint:erb # Lint views and components
yarn lint:erb:fix # Auto-fix issues
npx @herb-tools/linter --simple # Quick summary output

(The Ruby erb_lint / better_html stack was removed; Herb covers ERB linting for this project.)

  • Config: eslint.config.mjs
  • Purpose: JavaScript/JSX linting

Plugins:

  • React
  • React Hooks
  • JSX Accessibility
  • Import
  • Config: .stylelintrc.json
  • Purpose: SCSS/CSS linting

Rules focus on:

  • Proper SCSS syntax
  • Nesting depth limits
  • Property ordering (relaxed)
  • No vendor prefix warnings

Add to your CI pipeline:

# Example GitHub Actions
- name: Ruby Linting
run: |
bundle exec rubocop --parallel
bundle exec brakeman --no-pager
- name: Security Audit
run: bundle exec bundler-audit check --update
- name: ERB Linting
run: yarn lint:erb
- name: JS Linting
run: yarn lint:js
- name: CSS Linting
run: yarn lint:css

Add to .husky/pre-commit:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run quick lints on staged files only
yarn pretty-quick --staged
bundle exec rubocop --force-exclusion $(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$' | tr '\n' ' ')
# rubocop:disable Metrics/AbcSize
def complex_method
# ...
end
# rubocop:enable Metrics/AbcSize
// eslint-disable-next-line no-console
console.log('debug');
/* stylelint-disable-next-line selector-class-pattern */
.legacyClassName { }