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:
- Reek on the diff (Pronto runner vs
origin/masterby default) - yard-lint on touched Ruby files (
app/**/*.rb,lib/**/*.rb) - bundle-audit (Gemfile.lock CVE scan)
- 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.yaml → tools.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.ymllists 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
HEADandorigin/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.ymlwas retired in favor of
bin/cicallingpronto -r reeklocally 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/Reeklocal pre-push Only signal CodeRabbit doesn't carry. bin/ci→ Brakemanlocal pre-push -w 2(Medium+ confidence).bin/ci→ yard-lintlocal pre-push YARD docstrings on touched files. bin/ci→ bundler-auditlocal pre-push Gemfile.lockCVE scan.bin/ci→ Minitestlocal 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/rubycriticreports 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 needscmake. Installed via the projectBrewfile(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_typevs PGorder_type;AmazonAPlusContent#statusas 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 { }