Linting & Code Quality Tools
This project uses multiple linters to maintain code quality across Ruby, ERB, JavaScript, and SCSS.
Where checks run
Section titled “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
Section titled “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 suitebin/ci --since=HEAD~1 # tighten the diff window for diff-only checksbin/ci --help # full option referenceSteps:
- 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 timemise exec -- bundle exec brakeman --quiet --no-pager -w 3 # High-confidence onlyNo 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
Section titled “Quick Reference”# Ruby/Railsbundle exec rubocop # Ruby style & best practicesbundle exec rubocop --autocorrect-all # Auto-fix Ruby issuesbundle exec brakeman # Security vulnerabilitiesbundle exec bundler-audit check --update # Dependency vulnerabilitiesbundle 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 branchbundle exec pronto run --unstaged # Working-tree changesbundle exec pronto run --staged # Staged changes onlybundle exec pronto run -r reek # Match what CI posts on PRs
# ERB Templates (Herb — HTML-aware)yarn lint:erb # Lint views and componentsyarn lint:erb:fix # Auto-fix ERB issuesnpx @herb-tools/linter app/views # Lint specific directory
# JavaScriptyarn lint:js # ESLint for JS/JSXyarn eslint . --fix # Auto-fix JS issues
# CSS/SCSSyarn lint:css # Stylelint for SCSSyarn lint:css:fix # Auto-fix SCSS issues
# All JS + CSSyarn lint # Run both JS and CSS lintersRuby Linters
Section titled “Ruby Linters”Rubocop (Core)
Section titled “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
Section titled “Brakeman”- Purpose: Static security analysis
- Run:
bundle exec brakeman
Bundler Audit
Section titled “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)
Section titled “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 ofbin/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 runnersbundle exec pronto run --unstaged # uncommitted working-tree changesbundle exec pronto run --staged # staged changes (good for pre-commit)bundle exec pronto run -r reek # mirror what CI posts on PRsbundle exec pronto run -r rubocop # single runner (any of: rubocop, reek, brakeman)bundle exec pronto run -c HEAD~3 # diff vs an arbitrary commit-ishbundle exec pronto run -f text # default formatter; use `json` for pipingCI: .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
Section titled “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)
Section titled “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 componentsyarn lint:erb:fix # Auto-fix issuesnpx @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
Section titled “JavaScript Linting”ESLint
Section titled “ESLint”- Config:
eslint.config.mjs - Purpose: JavaScript/JSX linting
Plugins:
- React
- React Hooks
- JSX Accessibility
- Import
CSS/SCSS Linting
Section titled “CSS/SCSS Linting”Stylelint
Section titled “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
Section titled “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:cssPre-commit Hook (Optional)
Section titled “Pre-commit Hook (Optional)”Add to .husky/pre-commit:
#!/bin/sh. "$(dirname "$0")/_/husky.sh"
# Run quick lints on staged files onlyyarn pretty-quick --stagedbundle exec rubocop --force-exclusion $(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$' | tr '\n' ' ')Disabling Rules
Section titled “Disabling Rules”Rubocop
Section titled “Rubocop”# rubocop:disable Metrics/AbcSizedef complex_method # ...end# rubocop:enable Metrics/AbcSizeESLint
Section titled “ESLint”// eslint-disable-next-line no-consoleconsole.log('debug');Stylelint
Section titled “Stylelint”/* stylelint-disable-next-line selector-class-pattern */.legacyClassName { }