GitHub Actions Workflow: ESLint + Vitest + Nark for TypeScript Projects
By Nark Team
This is a complete GitHub Actions CI workflow for TypeScript projects that runs ESLint for linting, Vitest for testing, and Nark for dependency error handling. Copy it into .github/workflows/ci.yml and it runs on every pull request.
Quick Answer: Three tools, one workflow: ESLint catches code style and language bugs, Vitest catches logic regressions, Nark catches unhandled npm package errors (the category both ESLint and tests miss). All three run on every PR via GitHub Actions.
The Complete Workflow
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
ci:
name: TypeScript CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: TypeScript — type check
run: npx tsc --noEmit
- name: ESLint — lint
run: npx eslint src --ext .ts,.tsx
- name: Vitest — tests
run: npx vitest run
- name: nark — dependency error handling
run: npx nark --tsconfig tsconfig.json
That's the baseline. The sections below show how to customize each step.
What Each Step Catches
TypeScript type check
- name: TypeScript — type check
run: npx tsc --noEmit
The TypeScript compiler validates types, interfaces, function signatures, and structural compatibility. --noEmit runs the check without generating output files.
Run this before ESLint — type errors are more critical than style issues and should fail fast.
ESLint
- name: ESLint — lint
run: npx eslint src --ext .ts,.tsx
ESLint enforces code style rules, language-level best practices, and TypeScript-specific patterns. With @typescript-eslint, it catches:
@typescript-eslint/no-floating-promises— async calls with no error handling@typescript-eslint/no-explicit-any— type safety leaks@typescript-eslint/no-unused-vars— unused variables- Any custom rules your team has configured
To fail on warnings (strict mode):
- name: ESLint
run: npx eslint src --ext .ts,.tsx --max-warnings 0
Vitest
- name: Vitest — tests
run: npx vitest run
Vitest runs your unit and integration tests. The run flag (instead of just vitest) runs once and exits, suitable for CI.
With coverage threshold:
- name: Vitest — tests with coverage
run: npx vitest run --coverage --coverage.thresholds.lines=80
With test results uploaded to GitHub:
- name: Vitest — tests
run: npx vitest run --reporter=junit --outputFile=test-results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results.xml
Nark
- name: nark — dependency error handling
run: npx nark --tsconfig tsconfig.json
Nark checks every call to a profiled npm package (axios, Prisma, Stripe, Redis, etc.) against a machine-readable profile of what that package can throw at runtime.
What Nark catches that ESLint misses:
ESLint sees a try-catch and considers the error handled. Nark checks whether the catch block handles the specific errors the package throws:
// ESLint: PASSES (has try-catch)
// nark: FAILS (AxiosError network error shape not handled)
try {
const data = await axios.get('/api/data');
return data;
} catch (error) {
console.error(error.response.data); // crashes when error.response is undefined
}
What Nark catches that tests miss:
Tests catch what you write tests for. Nark checks the package's profile — all the error cases documented in the package's changelog, issues, and official docs.
Customization Options
Only fail on errors, not warnings
- name: nark
run: npx nark --tsconfig tsconfig.json --min-severity error
Useful when introducing Nark into an existing codebase with accumulated warnings. Start with errors only, then incrementally fix warnings.
Run in parallel to save time
jobs:
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npx tsc --noEmit
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npx eslint src --ext .ts,.tsx
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npx vitest run
nark:
name: Nark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npx nark --tsconfig tsconfig.json
Parallel jobs reduce total CI time. GitHub Actions shows each job's status separately in the PR check.
Cache dependencies for faster runs
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # caches ~/.npm between runs
Or with pnpm:
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
Monorepo setup
Run all three tools across multiple packages:
strategy:
matrix:
package: [apps/api, apps/web, packages/shared]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- name: ESLint
run: npx eslint ${{ matrix.package }}/src --ext .ts,.tsx
- name: Vitest
working-directory: ${{ matrix.package }}
run: npx vitest run
- name: nark
run: npx nark --tsconfig ${{ matrix.package }}/tsconfig.json
Setting Up Each Tool
ESLint setup for TypeScript
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
Minimal .eslintrc.json:
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-explicit-any": "warn"
}
}
Vitest setup
npm install --save-dev vitest @vitest/coverage-v8
vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
},
});
Nark setup
No configuration required. Nark reads your tsconfig.json and scans your TypeScript source files.
# Run locally first to see what it finds
npx nark --tsconfig tsconfig.json
Optional .nark/config.yaml for team config:
# .nark/config.yaml
minSeverity: error # only report errors, not warnings
exclude:
- src/**/*.test.ts # skip test files
- src/mocks/** # skip mock files
Full Workflow with Everything
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
ci:
name: TypeScript CI (${{ matrix.check }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
check: [typecheck, lint, test, nark]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run typecheck
if: matrix.check == 'typecheck'
run: npx tsc --noEmit
- name: Run ESLint
if: matrix.check == 'lint'
run: npx eslint src --ext .ts,.tsx --max-warnings 0
- name: Run Vitest
if: matrix.check == 'test'
run: npx vitest run --coverage
- name: Run nark
if: matrix.check == 'nark'
run: npx nark --tsconfig tsconfig.json
This runs all four checks in parallel. Each check shows up as a separate status check on the PR.
Frequently Asked Questions
Does Nark slow down CI?
Nark typically takes 15-30 seconds on a typical TypeScript project. It uses the TypeScript compiler for parsing, which has some startup overhead. Running it in parallel with Vitest keeps total CI time low.
Do I need to configure Nark for my project?
No. Nark reads your tsconfig.json and automatically finds all TypeScript source files. It has profiles for 160+ npm packages — including axios, prisma, stripe, and redis — and only reports on packages you actually use.
What if ESLint is already configured to catch promise rejections?
@typescript-eslint/no-floating-promises and Nark are complementary. ESLint catches bare async calls without any error handling. Nark catches calls where you have error handling but it's incorrect or incomplete for the specific package. They do not overlap.
Can I run this on a monorepo?
Yes. Use the matrix strategy to run Nark against each package's tsconfig. See the monorepo setup section above.
Try It Now
Add Nark to your existing CI pipeline with a single step:
- name: nark — dependency error handling
run: npx nark --tsconfig tsconfig.json
Or run locally first to see what it finds:
npx nark --tsconfig ./tsconfig.json
Nark checks 160+ npm packages — including axios, prisma, stripe, and redis — for correct error handling and Nark profiles. It's the quality gate that makes your ESLint + Vitest setup complete.