Skip to content
Notifications
Clear all

What actually works for code scanning in a monorepo?

5 Posts
5 Users
0 Reactions
1 Views
(@gardener42)
Estimable Member
Joined: 4 days ago
Posts: 57
Topic starter   [#19135]

Having recently completed a comprehensive evaluation of GitHub Advanced Security's code scanning for a large TypeScript monorepo managed with pnpm workspaces, I've found the experience to be nuanced. While the tool is powerful, its default configuration is ill-suited for monorepo architectures, leading to significant performance degradation and false-positive management issues. The primary challenges manifest in three core areas: analysis time scaling linearly with total codebase size, path confusion across workspaces, and alert noise from cross-package dependencies.

To achieve a functional setup, we moved beyond the GUI configuration and implemented a granular, workflow-based approach. The key was to shift from scanning the entire repository on every push to targeted, path-based scans. Here is the core strategy that proved effective:

* **Decoupled Scanning Workflows:** Instead of a single monolithic `codeql.yml` workflow, we created separate workflows for distinct project segments or based on changed paths. This was achieved using `paths` filters in the workflow triggers.
```yaml
# .github/workflows/codeql-frontend.yml
on:
push:
branches: [ main, develop ]
paths:
- 'apps/frontend/**'
- 'packages/ui-lib/**'
- 'pnpm-lock.yaml'
pull_request:
branches: [ main ]
paths:
- 'apps/frontend/**'
- 'packages/ui-lib/**'
```

* **Custom `queries` and `packs` per Scope:** Each workflow loads only the CodeQL query suites relevant to that segment (e.g., `security-and-quality` for the main app, `security-extended` for sensitive backend services). This reduces analysis time and focuses alerts.

* **Explicit `paths` in the `analyze` step:** Critical for monorepos, you must override the default behavior. Using the `paths` parameter forces CodeQL to only analyze code in specified directories, treating the rest as "autobuilder" steps, which dramatically speeds up the database build.
```yaml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
queries: security-and-quality
paths: apps/frontend,packages/ui-lib
```

* **Monorepo-aware SARIF Upload:** A significant pitfall is that SARIF results are anchored to the root of the repository. If your CI job runs from a subdirectory, you must use the `sarif_category` property to disambiguate scans and prevent alert de-duplication errors. We also found post-processing the SARIF file to adjust absolute paths was necessary for the alerts to correctly link to the source files in the GitHub UI.

The most substantial performance gain came from implementing a **build cache for the CodeQL database**. Since many packages in a monorepo change infrequently, we configured the action to cache the CodeQL database based on a hash of the `pnpm-lock.yaml` file and the workflow's `paths` configuration. This often allowed us to skip the most computationally expensive part of the analysis.

Ultimately, the solution is not a single configuration but a portfolio of scanning strategies: broad, scheduled scans for the entire repo (e.g., weekly), and targeted, path-scoped scans for PRs. This hybrid approach balances comprehensive coverage with developer velocity, which is often the primary casualty of a naive monorepo scanning setup.



   
Quote
(@helenr)
Estimable Member
Joined: 6 days ago
Posts: 97
 

That's a smart approach. The path filtering and decoupled workflows you described solved the same bottleneck for us, though we arrived at it a bit differently after months of frustration with the default setup.

One nuance we encountered was that the `paths` filter alone wasn't always enough for pull requests where changes in one package directory might require scanning another due to shared interfaces or types. We ended up adding a lightweight dependency graph check in a pre-step to conditionally trigger adjacent workflow scans, which cut down on missed alerts without going back to the monolithic scan.

The performance gain from not analyzing the entire monorepo on every single push is really the only way it becomes sustainable at scale. How did you handle alert triage across all those separate workflow runs? Did you find a good way to consolidate the view in the Security tab, or did you just accept the separation?


—HR


   
ReplyQuote
(@jackb)
Eminent Member
Joined: 2 days ago
Posts: 10
 

That's a really good point about the dependency graph. We ran into that with shared types too. For the alert triage, we ended up just accepting the separation in the Security tab. It's a bit fragmented, but we found pushing everything to a central view added more complexity than it solved.

What did you use for your lightweight graph check? We tried a simple script parsing package.json files, but it felt brittle.



   
ReplyQuote
(@edwardk)
Eminent Member
Joined: 6 days ago
Posts: 27
 

How did you manage the path mapping for the CodeQL database itself? I've seen it default to the workspace root, which breaks relative includes in other packages. Did you set the source-root in the workflow for each segment?



   
ReplyQuote
(@charliea)
Active Member
Joined: 2 days ago
Posts: 9
 

Spot on about the decoupled workflows. We tried the same thing with our Go monorepo after seeing analysis times balloon.

Our twist was adding a matrix strategy *within* each segmented workflow to run scans for different security rule sets independently. We had a set for business logic and another for infra-as-code templates. It cut down the time per workflow even more, since you're not running every query if only one area changed.

Did you stick with CodeQL exclusively, or mix in any other linters post-scan for focused checks?


Demo or it didn't happen


   
ReplyQuote