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
45 changes: 45 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Lint

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
# Deterministic quality gates, ordered fastest-fail-first. Uses npm ci so the
# dependency tree (and therefore coverage numbers) exactly matches a local
# run against the same package-lock.json.
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Lint
run: npm run lint

- name: Unit tests with coverage
run: npm run test:coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
16 changes: 3 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ permissions:
contents: read

jobs:
test:
# Playwright integration tests run separately from the Quality workflow so a
# slow or flaky browser run never blocks the quality gates or coverage upload.
integration:
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -25,18 +27,6 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Unit tests with coverage
run: npm run test:coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

- name: Get installed Playwright version
id: playwright-version
run: echo "version=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")" >> "$GITHUB_OUTPUT"
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ playwright-report/
coverage/
storybook-static/

# Stryker mutation-testing workdir and reports (see configs/stryker.config.json)
.stryker-tmp/
reports/

# Device-specific hidden files (OS-generated, not project config)
.DS_Store
._*
Expand Down
21 changes: 13 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
ci:
autoupdate_schedule: quarterly
# The eslint hook below runs the repo's own eslint out of node_modules, which
# pre-commit.ci cannot install (no network at hook runtime); the `npm run lint`
# step in the Test workflow enforces linting in CI instead.
skip:
- eslint

default_install_hook_types:
- pre-commit
Expand All @@ -20,18 +25,18 @@ repos:
additional_dependencies:
- prettier@3.9.5

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.39.1
- repo: local
hooks:
- id: eslint
name: eslint
# Runs the repo's own eslint (the exact package set and config `npm run lint`
# uses), not an isolated hook environment: type-aware rules need the app's
# node_modules for real type information, and TypeScript does not honor the
# NODE_PATH an isolated environment would rely on. Requires `npm ci` first.
entry: npx --no -- eslint --fix --config configs/eslint.config.cjs
language: system
types: [file]
files: \.(js|cjs|mjs|ts|tsx)$
args: [--fix, --config, configs/eslint.config.cjs]
additional_dependencies:
- eslint@9.39.5
- "@eslint/js@9.39.5"
- typescript-eslint@8.64.0
- typescript@5.9.3

- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 1.0.6

#### 🏠 Internal

- Widened Vitest coverage to all of `src/` (was `src/lib/` only), added a `json` reporter, and set ratchet thresholds (statements 30, branches 25, functions 34, lines 30) just below current levels so a coverage regression fails locally and in CI before the Codecov upload ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))
- Moved the deterministic gates into their own fail-fast Lint workflow (typecheck, lint, unit tests with coverage, Codecov upload, with the upload now pointing at `coverage/lcov.info` explicitly), leaving the Test workflow with just the Playwright `integration` job, so browser runs never block the coverage upload ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))
- Tightened ESLint to type-aware linting: `recommended-type-checked` over `src/` and `configs/` via `projectService`, plus `no-floating-promises`, `no-misused-promises`, `no-explicit-any`, `no-unnecessary-condition`, `await-thenable`, `complexity` (max 15), and `max-depth` (max 4); fixed the violations this surfaced (unsafe `JSON.parse` typing, promise rejections without `Error` reasons, promise-returning event handlers, unnecessary type assertions/conditions, and three functions refactored below the complexity cap) ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))
- Added ESLint and typescript-eslint as pinned devDependencies with an `npm run lint` script ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))
- Converted the pre-commit `eslint` hook from an isolated mirrors-eslint environment to a local hook that runs the repo's own eslint from `node_modules`, so pre-commit and `npm run lint` share one environment; type-aware rules need the app's dependency types, which an isolated hook environment cannot see (TypeScript ignores `NODE_PATH`), and its run had pushed a wrong auto-fix. pre-commit.ci skips the hook (it cannot run `npm ci`); the `npm run lint` CI step enforces linting ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))
- Added optional Stryker mutation testing (`npm run test:mutation`, config at `configs/stryker.config.json`); not a CI gate since it is slow, and mutation score, not raw coverage, is the real signal of test quality ([#57](https://github.com/brain-bbqs/bbqs-uploader/pull/57))

## 1.0.5

#### 🚀 Enhancement
Expand Down
4 changes: 3 additions & 1 deletion configs/appVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export function resolveAppVersion(): string {
if (process.env.CHROMATIC_STATIC_VERSION) {
return CHROMATIC_PLACEHOLDER_VERSION;
}
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")) as {
version: string;
};
return pkg.version;
}
49 changes: 43 additions & 6 deletions configs/eslint.config.cjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
// CommonJS on purpose: eslint runs via the pre-commit hook in its own isolated environment
// (see .pre-commit-config.yaml), where these packages are only reachable through the NODE_PATH
// pre-commit sets — which require() honors but ESM import does not. There is no eslint in the
// app's own devDependencies.
// Loaded by `npm run lint` and by the pre-commit eslint hook, which runs the repo's
// own eslint from node_modules (see .pre-commit-config.yaml) so both share one
// environment. CommonJS (.cjs) so require() can load it despite "type": "module".
const eslint = require("@eslint/js");
const tseslint = require("typescript-eslint");
const path = require("node:path");

module.exports = tseslint.config(
{ ignores: ["dist/", "coverage/", "storybook-static/", "test-results/", "playwright-report/"] },
{
ignores: [
"dist/",
"coverage/",
"storybook-static/",
"test-results/",
"playwright-report/",
"reports/",
".stryker-tmp/",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
// Type-aware linting for everything the TypeScript project covers (src/ plus the
// *.ts files under configs/; tests/ and stories/ are outside the tsconfig, so
// type information is not available there).
files: ["src/**/*.ts", "configs/**/*.ts"],
extends: [...tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: path.join(__dirname, ".."),
},
},
rules: {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/await-thenable": "error",
},
},
{
// Deterministic complexity caps everywhere, type info not required.
rules: {
complexity: ["error", 15],
"max-depth": ["error", 4],
},
},
{
// CommonJS config files (this one and configs/prettier.config.cjs) use CJS globals
// and require() by definition.
files: ["**/*.cjs"],
languageOptions: { globals: { module: "writable", require: "readonly" } },
languageOptions: { globals: { module: "writable", require: "readonly", __dirname: "readonly" } },
rules: { "@typescript-eslint/no-require-imports": "off" },
},
);
2 changes: 1 addition & 1 deletion configs/storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const config: StorybookConfig = {
name: "@storybook/html-vite",
options: {},
},
async viteFinal(config) {
viteFinal(config) {
config.define = {
...config.define,
__APP_VERSION__: JSON.stringify(resolveAppVersion()),
Expand Down
12 changes: 12 additions & 0 deletions configs/stryker.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker-js/master/packages/api/schema/stryker-core.json",
"testRunner": "vitest",
"vitest": {
"configFile": "configs/vitest.config.ts"
},
"mutate": ["src/**/*.ts"],
"reporters": ["html", "clear-text", "progress"],
"tempDirName": ".stryker-tmp",
"incremental": true,
"incrementalFile": "reports/stryker-incremental.json"
}
15 changes: 13 additions & 2 deletions configs/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ export default defineConfig({
include: ["tests/unit/**/*.test.ts"],
coverage: {
provider: "v8",
include: ["src/lib/**/*.ts"],
reporter: ["text", "lcov"],
include: ["src/**/*.ts"],
exclude: ["**/*.d.ts", "**/*.test.ts", "**/*.spec.ts", "stories/**", "tests/**", "configs/**"],
reporter: ["text", "lcov", "json"],
// Ratchet thresholds: set just below the current measured coverage so any
// regression fails locally (and in CI) before the Codecov upload. Raise
// these as coverage of src/ui and src/main.ts improves; they are not a
// target, only a floor.
thresholds: {
statements: 30,
branches: 25,
functions: 34,
lines: 30,
},
},
},
});
Loading
Loading