This is the third article in my CI/CD series for TypeScript monorepos. In the first article in this series, I covered setting up Jest with an 80% coverage gate, the testing infrastructure that everything else depends on. In the second article in this series, I walked through Husky and GitHub Actions to enforce typechecking, linting, and tests on every PR. This article adds the security and code quality scanning layer on top.
I run a TypeScript monorepo, NestJS backend, Next.js frontend, shared packages. With the test infrastructure and basic CI pipeline in place, the next gap was clear: nothing was checking for vulnerable dependencies, static code quality issues, or catching the subtle problems that slip through automated testing.
Here's what I was still missing:
npm auditThis article is the exact setup I put in place to fix all three. It's not theory, it's the config I'm running today. By the end, every PR in my repo goes through:
Let's walk through each one.

To add automated security and code quality checks to every GitHub pull request: (1) create a Snyk account and add the snyk/actions workflow, (2) connect your repo to SonarQube Cloud and add the SonarSource/sonarqube-scan-action workflow, (3) install the Kilo GitHub App and enable Code Reviews in your Kilo dashboard, and (4) configure Dependabot with open-pull-requests-limit: 1 to prevent PR flooding. All four run automatically on every PR with no manual intervention required.
What it does: Snyk scans your package.json (and lockfile) against its vulnerability database and fails the check if it finds any issues above your severity threshold. It catches CVEs in direct and transitive dependencies.
Why it matters for TypeScript projects: You're pulling in dozens of packages per service. One vulnerable transitive dependency in your NestJS backend can expose an entire API. Snyk catches this automatically, before the merge.
Cost: Free for open source and small teams. Paid tiers add team management, license compliance, and container scanning.
Step 1: Create a Snyk account
Go to snyk.io and sign up. Connect your GitHub account.
Step 2: Get your Snyk API token
In your Snyk dashboard: Settings → General → Auth Token. Copy it.
Step 3: Add it as a GitHub secret
In your repo: Settings → Secrets and variables → Actions → New repository secret
Name it SNYK_TOKEN.
Step 4: Add the workflow
# .github/workflows/snyk.yml
name: Snyk Security Scan
on:
pull_request:
branches:
- main
- develop
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
The --severity-threshold=high flag means Snyk only fails the build on high or critical CVEs. You can drop it to medium if you want stricter enforcement, I recommend starting with high to avoid alert fatigue.
For a monorepo, run Snyk in each package directory:
- name: Snyk - backend
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
working-directory: ./backend
- name: Snyk - frontend
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
working-directory: ./frontend
Once this is running, you'll see a Snyk check on every PR. If it fails, you have a vulnerable package somewhere in the dependency tree. Fix it before merging.
What it does: SonarQube Cloud (formerly SonarCloud) runs static analysis on your source code and reports code smells, bugs, security hotspots, duplications, and test coverage gaps. It posts a "Quality Gate" result directly on the PR.
Why it matters: This is the one that catches AI-generated code problems. When you use Copilot or Claude to write a chunk of code, it often compiles and even passes tests, but it introduces dead code, deeply nested conditionals, or subtle security patterns that SonarQube flags. I've caught several real issues this way.
Cost: Free for public repos. Paid for private (pricing is per line of code analyzed).
Step 1: Create a SonarQube Cloud account
Go to sonarcloud.io and sign up with your GitHub account.
Step 2: Set up your organization and project
Click Analyze new project → Select your GitHub repo → Follow the setup wizard. SonarQube Cloud will create an organization tied to your GitHub org.
Step 3: Get your SONAR_TOKEN
In SonarQube Cloud: My Account → Security → Generate Token. Copy it.
Step 4: Add the GitHub secret
Settings → Secrets and variables → Actions → New repository secret
Name it SONAR_TOKEN.
Step 5: Add sonar-project.properties
Create this file in your repo root:
# sonar-project.properties
sonar.projectKey=your-org_your-repo
sonar.organization=your-org
sonar.sources=.
sonar.exclusions=**/node_modules/**,**/*.test.ts,**/*.spec.ts
sonar.tests=.
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts
sonar.typescript.lcov.reportPaths=coverage/lcov.info
Replace your-org_your-repo and your-org with the values SonarQube Cloud assigned during setup.
Step 6: Add the workflow
# .github/workflows/sonar.yml
name: SonarQube Cloud Analysis
on:
pull_request:
branches:
- main
- develop
jobs:
sonarcloud:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for branch analysis
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
The fetch-depth: 0 on checkout is important, SonarQube needs full git history for accurate branch analysis and new-code tracking.
Once this is running, SonarQube Cloud will post a PR decoration showing the Quality Gate status (pass/fail) and a breakdown of new issues introduced by the PR. You can configure the Quality Gate thresholds in the SonarQube Cloud dashboard.
What it does: Kilo's Code Review Agent connects to GitHub via a GitHub App and automatically reviews every pull request when it's opened or updated. It analyzes the diff and posts inline comments on specific lines, plus a summary comment with overall findings. Severity tags (critical, warning, info) make triage fast.
Why it matters: Human code review is valuable but slow. Kilo does the first pass, catching the obvious issues, missed edge cases, and potential bugs, before anyone spends time in the review queue. In practice, it catches things I'd otherwise miss because I'm reviewing my own code.
Cost: Requires a Kilo Code account at app.kilo.ai. Uses credits per review based on the AI model used. No GitHub Actions runner cost since it runs on Kilo's infrastructure.
This is the easiest of the three to set up, there's no workflow YAML to write. Kilo runs entirely through its GitHub App.
Step 1: Create a Kilo Code account
Go to app.kilo.ai and create an account.
Step 2: Install the KiloConnect GitHub App
In your Kilo dashboard: Integrations → GitHub → Configure
You'll be redirected to GitHub to authorize the KiloConnect app. Choose whether to grant access to all repositories or specific ones, then click Install & Authorize.
Step 3: Configure the Code Review Agent
Navigate to Code Reviews in your Kilo dashboard (personal: app.kilo.ai/code-reviews, or your organization dashboard).
Configure:
Step 4: Open a pull request
That's it. When you open a PR (or push new commits to one), Kilo automatically triggers a review. It posts a summary comment and inline comments directly on the PR diff.
What the review looks like in practice:
Kilo posts a top-level comment summarizing findings, then inline comments like:
⚠️ warning: This function has a cyclomatic complexity of 14. Consider breaking it into smaller functions for maintainability.
🔴 critical: Potential SQL injection risk, user input is being concatenated directly into the query string at line 47.
When you push new commits, the previous review is automatically cancelled and a fresh one starts, no stale feedback cluttering the PR.
Note: Kilo Code Review runs on Kilo's cloud infrastructure. No GitHub Actions workflow file is needed for the Code Review feature specifically. If you want to trigger Kilo as a command-line agent via PR comments (e.g., /kilo do X), there's a separate GitHub Action for that, but for automated code review, the GitHub App handles everything.
Dependabot is great in theory and exhausting in practice if you don't tune it. By default, it can open a separate PR for every outdated package, which, in an active monorepo, means waking up to 15–20 open PRs that all need attention.
The fix is two settings:
open-pull-requests-limit: 1 per ecosystem, caps how many open Dependabot PRs exist at oncegroups, bundles related packages (like all dev dependencies) into a single PRHere's the config I'm running for a three-package monorepo (root, /frontend, /backend):
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 1
groups:
dev-dependencies:
patterns:
- "*"
exclude-patterns:
- "eslint*"
- "prettier*"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "daily"
open-pull-requests-limit: 1
- package-ecosystem: "npm"
directory: "/backend"
schedule:
interval: "daily"
open-pull-requests-limit: 1
Why I excluded eslint* and prettier* from the group: These tools sometimes have breaking config changes between major versions. I'd rather review their PRs individually than have a grouped update silently break my linting setup.
With this config, I get at most one new Dependabot PR per package directory per day. When the previous one merges, a new one opens. The board stays manageable.
When a developer opens a PR against main or develop:
package.json files for CVEs. If a high/critical vulnerability exists in the dependency tree, the check fails.The result: by the time a human reviewer looks at the PR, the obvious issues are already flagged. Vulnerable dependencies are blocked. Code quality regressions are visible. The human review can focus on architecture, intent, and context, not syntax or CVEs.
Dependabot runs in the background, feeding the CI pipeline with one dependency update per day per package. Each Dependabot PR goes through the same Snyk and SonarQube checks, which is exactly the behavior you want, you're not patching vulnerabilities while introducing new ones.
This is the final layer of the three-part CI/CD stack. The first article in this series gave you a test infrastructure with an 80% coverage gate. The second article in this series wired up Husky and GitHub Actions so typecheck, lint, and tests run on every PR automatically. This article adds the security and quality scanning layer that catches what tests and linting can't.
This setup took me a few hours to put together and has already saved real time. Snyk caught a vulnerable version of jsonwebtoken in a Dependabot PR before I merged it. SonarQube flagged a dead code path in an AI-generated NestJS service. Kilo caught a missing null check I'd have caught eventually, but it caught it in five minutes instead of during QA.
None of these tools require ongoing maintenance once they're configured. They just run. That's the point.
If you're building on TypeScript, whether it's a NestJS/Next.js monorepo like mine or a single-service project, this is a reasonable security and quality baseline. The free tiers on Snyk and SonarQube make it accessible even for solo projects. Kilo's credits-based pricing means you only pay when a review runs.
Set it up once. Let it run on every PR.
Snyk offers a free tier that includes limited scans per month for private repositories, and is fully free for open source projects. For small teams (up to 3 developers), the free tier is typically sufficient for CI/CD integration. Paid plans unlock unlimited tests, team management, and additional features like container and IaC scanning.
Yes. You configure a single sonar-project.properties file at the root with sonar.sources pointing to your source directories. For monorepos, you can also set up multiple SonarQube projects (one per package) and use the sonar.projectKey field to separate them. The SonarSource/sonarqube-scan-action supports this pattern natively.
Kilo has a configurable max review time (5–30 minutes). For very large PRs, it may not review every file in the diff, it prioritizes the most effective changes. If reviews are timing out on large PRs, increase the max review time in your Code Reviews configuration on the Kilo dashboard. Alternatively, keeping PRs small (a good practice anyway) ensures Kilo can do a complete review every time.
Sources: Snyk GitHub Actions documentation, SonarQube Cloud GitHub Actions setup, Kilo Code Reviews documentation, GitHub Dependabot configuration reference