┌──────────────────────────────────────────────────────────────┐
│ QUALITY PIPELINE │
│ (a.k.a. "The Gauntlet") │
├──────────────────────────────────────────────────────────────┤
│ │
│ The Foundation: THE RULEBOOK │
│ ┌────────────────────────────────────────────────┐ │
│ │ .eslintrc.yml — the opinionated ruleset │ │
│ │ max-lines, complexity, prop-types, camelcase │ │
│ │ + smart overrides for reducers/sagas/tests │ │
│ └────────────────────────────────────────────────┘ │
│ │ powers both layers below │
│ v │
│ Layer 1: COMMIT TIME (local) │
│ ┌────────────────────────────────────────────────┐ │
│ │ lint-staged │ │
│ │ • ESLint --fix on staged *.js files │ │
│ │ • Prettier on staged *.json files │ │
│ │ Catches: formatting, auto-fixable errors │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ Layer 2: PR TIME (GitHub Actions — run in parallel) │
│ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │
│ │ JS Linter │ │ Duplicate i18n │ │ Gitleaks │ │
│ │ (full ESLint)│ │ (cross-file) │ │ (secrets) │ │
│ └──────┬───────┘ └───────┬─────────┘ └─────┬─────┘ │
│ └────────────┬────┘ │ │
│ v │ │
│ Merge blocked until ALL pass ◄───┘ │
│ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ QUALITY PIPELINE │
│ (a.k.a. "The Gauntlet") │
├──────────────────────────────────────────────────────────────┤
│ │
│ The Foundation: THE RULEBOOK │
│ ┌────────────────────────────────────────────────┐ │
│ │ .eslintrc.yml — the opinionated ruleset │ │
│ │ max-lines, complexity, prop-types, camelcase │ │
│ │ + smart overrides for reducers/sagas/tests │ │
│ └────────────────────────────────────────────────┘ │
│ │ powers both layers below │
│ v │
│ Layer 1: COMMIT TIME (local) │
│ ┌────────────────────────────────────────────────┐ │
│ │ lint-staged │ │
│ │ • ESLint --fix on staged *.js files │ │
│ │ • Prettier on staged *.json files │ │
│ │ Catches: formatting, auto-fixable errors │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ Layer 2: PR TIME (GitHub Actions — run in parallel) │
│ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │
│ │ JS Linter │ │ Duplicate i18n │ │ Gitleaks │ │
│ │ (full ESLint)│ │ (cross-file) │ │ (secrets) │ │
│ └──────┬───────┘ └───────┬─────────┘ └─────┬─────┘ │
│ └────────────┬────┘ │ │
│ v │ │
│ Merge blocked until ALL pass ◄───┘ │
│ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ QUALITY PIPELINE │
│ (a.k.a. "The Gauntlet") │
├──────────────────────────────────────────────────────────────┤
│ │
│ The Foundation: THE RULEBOOK │
│ ┌────────────────────────────────────────────────┐ │
│ │ .eslintrc.yml — the opinionated ruleset │ │
│ │ max-lines, complexity, prop-types, camelcase │ │
│ │ + smart overrides for reducers/sagas/tests │ │
│ └────────────────────────────────────────────────┘ │
│ │ powers both layers below │
│ v │
│ Layer 1: COMMIT TIME (local) │
│ ┌────────────────────────────────────────────────┐ │
│ │ lint-staged │ │
│ │ • ESLint --fix on staged *.js files │ │
│ │ • Prettier on staged *.json files │ │
│ │ Catches: formatting, auto-fixable errors │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ Layer 2: PR TIME (GitHub Actions — run in parallel) │
│ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │
│ │ JS Linter │ │ Duplicate i18n │ │ Gitleaks │ │
│ │ (full ESLint)│ │ (cross-file) │ │ (secrets) │ │
│ └──────┬───────┘ └───────┬─────────┘ └─────┬─────┘ │
│ └────────────┬────┘ │ │
│ v │ │
│ Merge blocked until ALL pass ◄───┘ │
│ │
└──────────────────────────────────────────────────────────────┘
{ "lint-staged": { "*.js": "npm run lint:eslint:fix", "*.json": "prettier --write" }, "pre-commit": "lint:staged"
}
{ "lint-staged": { "*.js": "npm run lint:eslint:fix", "*.json": "prettier --write" }, "pre-commit": "lint:staged"
}
{ "lint-staged": { "*.js": "npm run lint:eslint:fix", "*.json": "prettier --write" }, "pre-commit": "lint:staged"
}
name: JavaScript Linting Validator on: pull_request: types: [opened, reopened, synchronize] branches: - '*' jobs: js-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' # Nuclear option: clean slate every time - run: rm -f .npmrc - run: rm -rf node_modules package-lock.json - run: npm cache clean --force # Install only linting deps (not the whole project) - name: Install ESLint + Prettier + Babel deps run: | npm install "@babel/core@^7.26.7" \ "@babel/eslint-parser@^7.26.5" \ "eslint@^8.57.0" \ "eslint-config-prettier@^10.0.1" \ "eslint-plugin-react@^7.37.4" \ "eslint-plugin-prettier@^5.2.3" \ "prettier@^3.4.2" \ # ... ~20 more packages --no-save --silent - run: npx eslint .
name: JavaScript Linting Validator on: pull_request: types: [opened, reopened, synchronize] branches: - '*' jobs: js-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' # Nuclear option: clean slate every time - run: rm -f .npmrc - run: rm -rf node_modules package-lock.json - run: npm cache clean --force # Install only linting deps (not the whole project) - name: Install ESLint + Prettier + Babel deps run: | npm install "@babel/core@^7.26.7" \ "@babel/eslint-parser@^7.26.5" \ "eslint@^8.57.0" \ "eslint-config-prettier@^10.0.1" \ "eslint-plugin-react@^7.37.4" \ "eslint-plugin-prettier@^5.2.3" \ "prettier@^3.4.2" \ # ... ~20 more packages --no-save --silent - run: npx eslint .
name: JavaScript Linting Validator on: pull_request: types: [opened, reopened, synchronize] branches: - '*' jobs: js-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' # Nuclear option: clean slate every time - run: rm -f .npmrc - run: rm -rf node_modules package-lock.json - run: npm cache clean --force # Install only linting deps (not the whole project) - name: Install ESLint + Prettier + Babel deps run: | npm install "@babel/core@^7.26.7" \ "@babel/eslint-parser@^7.26.5" \ "eslint@^8.57.0" \ "eslint-config-prettier@^10.0.1" \ "eslint-plugin-react@^7.37.4" \ "eslint-plugin-prettier@^5.2.3" \ "prettier@^3.4.2" \ # ... ~20 more packages --no-save --silent - run: npx eslint .
// In CreateEntity/messages.js
defineMessages({ saveButton: { id: 'app.createEntity.save', defaultMessage: 'Save' },
}); // In EditEntity/messages.js (different dev, different sprint)
defineMessages({ saveAction: { id: 'app.editEntity.save', defaultMessage: 'Save' },
});
// In CreateEntity/messages.js
defineMessages({ saveButton: { id: 'app.createEntity.save', defaultMessage: 'Save' },
}); // In EditEntity/messages.js (different dev, different sprint)
defineMessages({ saveAction: { id: 'app.editEntity.save', defaultMessage: 'Save' },
});
// In CreateEntity/messages.js
defineMessages({ saveButton: { id: 'app.createEntity.save', defaultMessage: 'Save' },
}); // In EditEntity/messages.js (different dev, different sprint)
defineMessages({ saveAction: { id: 'app.editEntity.save', defaultMessage: 'Save' },
});
name: Check Duplicate i18n Messages on: pull_request: types: [opened, reopened, synchronize] branches: ['*'] jobs: check-duplicates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' - run: node scripts/check-duplicate-messages.js
name: Check Duplicate i18n Messages on: pull_request: types: [opened, reopened, synchronize] branches: ['*'] jobs: check-duplicates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' - run: node scripts/check-duplicate-messages.js
name: Check Duplicate i18n Messages on: pull_request: types: [opened, reopened, synchronize] branches: ['*'] jobs: check-duplicates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 'lts/*' - run: node scripts/check-duplicate-messages.js
Found 47 message files to check CROSS-FILE DUPLICATE MESSAGE VALUE FOUND
Value: "Save" In app/components/pages/CreateEntity/messages.js: - ID: app.createEntity.save In app/components/pages/EditEntity/messages.js: - ID: app.editEntity.save Cross-file duplicate message values found.
Found 47 message files to check CROSS-FILE DUPLICATE MESSAGE VALUE FOUND
Value: "Save" In app/components/pages/CreateEntity/messages.js: - ID: app.createEntity.save In app/components/pages/EditEntity/messages.js: - ID: app.editEntity.save Cross-file duplicate message values found.
Found 47 message files to check CROSS-FILE DUPLICATE MESSAGE VALUE FOUND
Value: "Save" In app/components/pages/CreateEntity/messages.js: - ID: app.createEntity.save In app/components/pages/EditEntity/messages.js: - ID: app.editEntity.save Cross-file duplicate message values found.
name: Gitleaks Secret Scan on: pull_request: types: [opened, reopened, synchronize] branches: [master] jobs: gitleaks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Gitleaks Secret Scan on: pull_request: types: [opened, reopened, synchronize] branches: [master] jobs: gitleaks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Gitleaks Secret Scan on: pull_request: types: [opened, reopened, synchronize] branches: [master] jobs: gitleaks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
max-lines: - error - max: 500
max-lines: - error - max: 500
max-lines: - error - max: 500
complexity: - error - max: 10
complexity: - error - max: 10
complexity: - error - max: 10
react/prop-types: error
react/require-default-props: error
react/prop-types: error
react/require-default-props: error
react/prop-types: error
react/require-default-props: error
camelcase: - error - properties: always
camelcase: - error - properties: always
camelcase: - error - properties: always
prettier/prettier: - error - { singleQuote: true, printWidth: 100, endOfLine: 'lf' }
prettier/prettier: - error - { singleQuote: true, printWidth: 100, endOfLine: 'lf' }
prettier/prettier: - error - { singleQuote: true, printWidth: 100, endOfLine: 'lf' }
overrides: - files: '**/reducer.js' rules: complexity: off - files: '**/saga.js' rules: complexity: off camelcase: off - files: '**/*.test.js' rules: react/prop-types: off - files: '**/tests/**/*.test.js' rules: complexity: off camelcase: off max-lines: off
overrides: - files: '**/reducer.js' rules: complexity: off - files: '**/saga.js' rules: complexity: off camelcase: off - files: '**/*.test.js' rules: react/prop-types: off - files: '**/tests/**/*.test.js' rules: complexity: off camelcase: off max-lines: off
overrides: - files: '**/reducer.js' rules: complexity: off - files: '**/saga.js' rules: complexity: off camelcase: off - files: '**/*.test.js' rules: react/prop-types: off - files: '**/tests/**/*.test.js' rules: complexity: off camelcase: off max-lines: off - Components quietly ballooning past 700 lines — because "I'll refactor it later" (narrator: they did not refactor it later)
- Formatting roulette: tabs here, spaces there, trailing commas in this file but not that one
- Duplicate i18n messages across different feature modules — the same English string defined in three different messages.js files with three different IDs, leading to translation mismatches and wasted localization budget (more on this — it's a bigger deal than it sounds)
- console.log('HERE') statements left behind like breadcrumbs from a debugging session that ended three sprints ago
- var declarations showing up in a modern codebase, haunting us like a ghost from JavaScript past - Recursively finds every messages.js and message.js file
- Regex-extracts all { id: '...', defaultMessage: '...' } definitions — handling single-line, multi-line, and template literal formats
- Builds a map of message values to their file locations
- Flags any defaultMessage that appears in two or more different files - Start with the rules. Define what "good code" means in your linter config. Be opinionated — it's easier to relax rules than to introduce them into a codebase that's already feral.
- Enforce locally. lint-staged + pre-commit hooks fix the easy stuff automatically. Zero friction.
- Enforce in CI. GitHub Actions catch what local hooks miss — and unlike pre-commit hooks, they can't be skipped with --no-verify.
- Write overrides, not exceptions. Instead of // eslint-disable-next-line scattered across hundreds of files, configure overrides by file pattern. Encode your team's decisions in the config, not in comments.
- Build custom checks for your pain. The duplicate i18n check isn't in any plugin — we wrote it because it was our problem. Every codebase has unique failure modes. Write scripts to catch yours.