Wesley Dean
Making MegaLinter Faster: A Four-Part Optimization Series image

Making MegaLinter Faster: A Four-Part Optimization Series

4 min read

I did not set out to write a four-part series about MegaLinter performance. I set out to understand why three private repositories had consumed roughly 3,000 GitHub Actions minutes in about five days.

The investigation grew because each answer exposed a different question. Moving MegaLinter onto Jenkins infrastructure I already owned did not make the pipeline faster; initially, it made it dramatically slower. That forced me to look past the choice of CI platform and examine what the pipeline was actually spending time doing.

Over the following four posts, I will trace that investigation from GitHub Actions usage through scanner caching and workload selection. The series focuses on understanding where CI time goes, which costs can be reused, and which work belongs in the pipeline rather than trying to prove that Jenkins is faster than GitHub Actions.

Part 1: Moving MegaLinter Off GitHub Actions

The first post starts with the original problem: MegaLinter was running repeatedly against draft pull requests while I was still pushing incremental work. I moved much of that execution onto Jenkins, where a representative run went from roughly 170 seconds on GitHub Actions to about 660 seconds locally.

The eventual improvement came from control over the execution environment: keeping large images nearby, preserving useful state, and separating portable repository behavior from infrastructure-specific optimization.

Part 2: Griping About Grype

Once the larger Jenkins costs came down, Grype became the dominant outlier. It was taking roughly three minutes to scan a small Bash project whose generated artifact was about 12.3 KB.

A Nexus proxy improved database delivery without materially improving runtime. The more important discovery was that Grype’s prepared vulnerability database lived inside a disposable container and disappeared after every run.

Persisting that state reduced a representative Grype execution from roughly 190 seconds to about four seconds.

Part 3: Beyond Grype

Grype raised the next question: which other scanners were reconstructing useful state inside containers that were about to be deleted?

I generalized the persistence model across MegaLinter while deliberately keeping the configured linter set intact. The steady-state Jenkins pipeline fell from roughly three minutes and 39 seconds to 58 seconds without disabling the checks that had been enabled when the optimization work began.

That distinction matters because it isolates the value of caching and persistence from the different strategy of removing work.

Part 4: Making MegaLinter Do Less Work

Only after the 58-second result did I change the workload itself.

This repository is a small Bash project, so some enabled linters were redundant or had no meaningful input. Making the linter selection repository-specific reduced the complete job to 37 seconds. A subsequent run with SARIF output disabled completed in 36 seconds.

The final post separates two different kinds of optimization: performing useful work more efficiently and deciding that some work does not belong in the pipeline.

The thread running through the series

I find the changing critical path more useful than the smallest runtime.

The critical path changed after every meaningful improvement. Grype dominated until its database persisted. Once Grype’s runtime fell, other scanners became visible. Once their execution costs came down, workload selection became worth examining.

The specific timings belong to this repository, these tools, and these workers. The method travels better: measure what is actually happening, understand the cost before changing it, preserve expensive reusable state when its lifecycle should exceed the process that creates it, remove work when it no longer provides useful evidence, and measure again.

Posts in this Series

Tags