Linting & Code Quality Tools

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

Where checks run

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.

Where What runs Gate
bin/ci (local, on demand) Reek diff · yard-lint diff · bundle-audit · full Minitest suite Manual; 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.yml Custom DESIGN.md validator (only when DESIGN.md is touched) PR check
yard-docs.yml Generates + deploys YARD docs On 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.

bin/ci — the local runner

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).

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:

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.

Quick Reference

# 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

Ruby Linters

Rubocop (Core)

  • Config: .rubocop.yml
  • Purpose: Ruby style, syntax, and best practices

Plugins included:

Plugin Purpose
rubocop-rails Rails-specific cops
rubocop-performance Performance optimizations
rubocop-minitest Minitest best practices
rubocop-capybara Capybara test syntax
rubocop-factory_bot FactoryBot syntax
rubocop-yard YARD documentation

Brakeman

  • Purpose: Static security analysis
  • Run: bundle exec brakeman

Bundler Audit

  • 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.

Pronto (diff-only meta-runner)

  • 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:

    Tool Where it runs Notes
    bin/ci → Pronto/Reek local pre-push Only signal CodeRabbit doesn't carry.
    bin/ci → Brakeman local pre-push -w 2 (Medium+ confidence).
    bin/ci → yard-lint local pre-push YARD docstrings on touched files.
    bin/ci → bundler-audit local pre-push Gemfile.lock CVE scan.
    bin/ci → Minitest local pre-push Full test suite.
    CodeRabbit → RuboCop PR diff AI review w/ rubocop context.
    CodeRabbit → Brakeman PR diff AI review w/ brakeman context (in addition to bin/ci's gate).
    CodeRabbit (AI review) PR diff Semantic 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:

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).

Database Consistency

  • 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).

ERB Linting (Herb)

  • 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:

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.)

JavaScript Linting

ESLint

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

Plugins:

  • React
  • React Hooks
  • JSX Accessibility
  • Import

CSS/SCSS Linting

Stylelint

  • Config: .stylelintrc.json
  • Purpose: SCSS/CSS linting

Rules focus on:

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

CI/CD Integration

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

Pre-commit Hook (Optional)

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' ' ')

Disabling Rules

Rubocop

# rubocop:disable Metrics/AbcSize
def complex_method
  # ...
end
# rubocop:enable Metrics/AbcSize

ESLint

// eslint-disable-next-line no-console
console.log('debug');

Stylelint

/* stylelint-disable-next-line selector-class-pattern */
.legacyClassName { }