Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ jobs:
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run vitest
run: npm test
2 changes: 2 additions & 0 deletions .woodpecker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ steps:
image: node:20-slim
commands:
- npm ci
- npm run lint
- npm test

test-node-22:
image: node:22-slim
commands:
- npm ci
- npm run lint
- npm test
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **ESLint flat config + CI gate** (P5-L). New `eslint.config.js`
with rules tuned for high-signal bug catching rather than style
preferences: `no-unused-vars` (with `^_` opt-out), `eqeqeq`,
`no-console` (allowing only `error`/`warn` outside tests),
`prefer-const`, `no-var`. Tests get a relaxed variant
(`no-unused-vars` as a warning, console allowed). Migrations
ignore unused args to honor sequelize-cli's
`(queryInterface, Sequelize)` contract. Wired into GitHub Actions
and Woodpecker so every PR now runs `npm run lint` ahead of the
vitest suite. `npm run lint:fix` available for autofixable rules.
- **`createdAt` / `updatedAt` on every domain entity** (P4-K). New
migration adds two `TIMESTAMPTZ NOT NULL DEFAULT now()` columns to
18 tables (everything except `IdempotencyKey`, which already
Expand Down
120 changes: 120 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Aaron K. Clark
//
// ESLint flat config. Keep rules minimal and high-signal — the
// goal is to catch the bugs that would otherwise survive review,
// not to enforce style preferences. Style debates belong in code
// review, not in CI failures.

const js = require('@eslint/js');
const globals = require('globals');

module.exports = [
{
// Files this config applies to. We exclude generated output
// and the openapi spec (which has long string literals that
// would otherwise trip max-len if we ever add it).
ignores: [
'node_modules/**',
'coverage/**',
'dist/**',
// sequelize-cli output (DBs etc.)
'**/*.bacpac',
],
},

// Server code (CJS, Node globals)
{
files: ['app/**/*.js', 'server.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
...js.configs.recommended.rules,
// The codebase uses unused leading-underscore params as
// intentional placeholders (e.g. `(err, req, res, next)`
// four-arg express error handlers must keep `next`).
'no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
// `==` vs `===` — banned tooling for SQL-emitted string
// comparisons drifting around. Always strict.
eqeqeq: ['error', 'always', { null: 'ignore' }],
// Stray console.* calls were a real bug source pre-pino.
// Only allow `console.error`/`console.warn` as last-resort
// fallbacks; everything else routes through `log.*`.
'no-console': ['error', { allow: ['error', 'warn'] }],
// Prefer `const` for variables never reassigned.
'prefer-const': 'error',
// `var` is banned outright — no hoisting surprises.
'no-var': 'error',
},
},

// Migrations: pre-existing _ignored second-arg `Sequelize`
// convention is sequelize-cli's contract. Permit it.
{
files: ['app/migrations/**/*.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'commonjs',
globals: { ...globals.node },
},
rules: {
...js.configs.recommended.rules,
'no-unused-vars': ['error', { args: 'none' }],
eqeqeq: ['error', 'always', { null: 'ignore' }],
},
},

// Tests: vitest globals + ESM-style imports.
{
files: ['tests/**/*.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: {
...globals.node,
// vitest doesn't inject these globally by default,
// but the codebase imports them explicitly so we
// don't need to globalize them. Keeping this empty
// intentionally — lint helps catch a missing import.
},
},
rules: {
...js.configs.recommended.rules,
// Tests intentionally instantiate models with placeholder
// ids, throw away helpers, mock callbacks, etc.
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
// Tests are allowed to console.log for debugging output
// that's only seen on failure.
'no-console': 'off',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'prefer-const': 'warn',
},
},

// Standalone helper scripts in /tmp or one-shots — pull from
// the same config-less defaults but don't enforce as strictly.
{
files: ['scripts/**/*.js'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'commonjs',
globals: { ...globals.node },
},
rules: {
...js.configs.recommended.rules,
},
},
];
Loading
Loading