Want static analysis to run not just locally, but also on every pull request? Want bugs to be caught before they reach the main branch? In this article, we'll show how to set it up in the GitHub Actions pipeline.
Static analysis in CI
One effective way to use static analysis regularly is to integrate it into a Continuous Integration (CI) workflow, which delivers regular code merges into a shared repository, automated builds, and testing.
CI typically runs a set of various tests, but it doesn't mean that testing should be our only safeguard. Static analysis can detect errors and potential vulnerabilities even before we run tests, which strengthens the overall process.
Using static analysis in CI helps catch issues early, avoid pushing errors to the main branch, and boosts the overall project quality.
However, regular static analysis is crucial. Running it just once, say, before release, causes problems like that:
- marking up warnings will take longer;
- the analysis quality will drop because developers may overlook some warnings when there are too many;
- debugging gets more complex, since bugs are detected much later, and makes developers revisit and rework code.
What is a quality gate?
To maintain robust code analysis quality in CI, it's a prudent choice to use a quality gate, a set of criteria used to define whether code changes meet the required quality to move to the next pipeline stage. If at least one criteria fails, the quality gate is not passed, and the pipeline halts.
The quality gate allows developers to set clear quality criteria, define metrics to automatically accept or reject code, and make quality requirements transparent and measurable.
It also automatically blocks the next pipeline stages if the quality gate fails. Why run tests if the code doesn't meet our quality criteria?
Therefore, static analysis should be the first stage in the pipeline. This ensures a logical quality assurance flow and allows the CI system to provide quick feedback on code changes.
How to trigger a pipeline?
When we talk about continuous integration, it's worth emphasizing continuous—we need to define when CI should run automatically.
A solid option would be to run the pipeline after each commit, but this could create unnecessary load on the infrastructure. It'd also be useful to encourage developers to locally run the analysis of code changes, both static analysis and tests. Therefore, triggering checks for every for every branch update may be excessive.
It'd be better to run the pipeline when the pull or merge request is opened. This ensures the code about to be integrated into the main branch is checked, and it won't introduce new issues into the application.
Preparing static analysis in GitHub Actions
Let's see how to hook up static analysis by integrating PVS-Studio into GitHub Actions.
Note. You can read more about integrating PVS-Studio into GitHub Actions in the documentation.
How to pre-configure static analysis
Initially, we have a GitHub repository with a C# project built with MSBuild. First, we should enter the PVS-Studio license in the repository settings. On the Settings tab of the repository, go to the Secrets and Variables section. Then, in the Actions subsection, click New repository secret. The name is PVS_STUDIO_CREDENTIALS
, and the value should be user's name and the license key, separated by a space:
How to write a pipeline?
Now we can start writing the pipeline. Create a YAML
file in the .github/workflows
directory. In the example, we will call it build-analyze.yaml
. Let's break it down step by step to see what's going on.
At the beginning, we define the workflow's name and conditions under which it'll be run. Earlier, we decided to trigger static analysis whenever a new pull request is opened.
name: Build and analyze with PVS-Studio
on:
pull_request:
branches: [main]
Next is the list of jobs to run. In our case, there's only one job, but we could also add others, like unit tests.
For this job, we need to prevent bots from committing changes. However, they'll push other commits to accelerate the analysis process, we'll talk about it later_._ Additionally, we need to specify the operating system to be used—in our case, Ubuntu 24.04. After that, the steps for performing jobs will be described.
jobs:
build-analyze:
if: github.actor != 'github-actions[bot]'
permissions: write-all
runs-on: ubuntu-24.04
steps:
Before we start testing new features, we need to check out the source code from the version control system using built-in GitHub Actions presets and install the required dependencies. In this case, these are .NET SDK 9.0, pvs-studio-dotnet
, and pvs-studio
because we analyze the C# project built with MSBuild.
- name: Check out repository code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install tools
run: |
run: |
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt \
| sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo add-apt-repository ppa:dotnet/backports
sudo apt update
sudo apt-get install -y dotnet-sdk-9.0
sudo apt install pvs-studio
sudo apt install pvs-studio-dotnet
pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
Note. You can read more about how to install PVS-Studio on Linux in the documentation.
Now let's move on to the main steps—first, compiling the project. If it doesn't compile, there's no reason to run the analysis:
- name: Build
run: |
dotnet build sast-in-ci-example.sln
If everything is OK, run the analysis using the pvs-studio-dotnet
command-line utility:
- name: Analyze
run: |
pvs-studio-dotnet \
-t sast-in-ci-example.sln \
-C .pvsconfig \
-o ./sast-in-ci-example.json \
-r --disableLicenseExpirationCheck -F
Let's see what flags used here.
-t
passes the solution file to analyze.
-C
passes the configuration file, .pvsconfig
. In the example, this file contains settings for excluding certain paths from the analysis:
//V_EXCLUDE_PATH **AssemblyInfo.cs
//V_EXCLUDE_PATH **AssemblyAttributes.cs
-o
specifies the path where to save the analyzer report.
-r
displays analysis progress in the build logs.
–disableLicenseExpirationCheck
hides the message about the expiring license. In this example, this is necessary because the analysis runs on a trial version.
Note. You can request a trial PVS-Studio license here.
-F
enables the modified file analysis mode, where the analyzer evaluates the hashes of the project files and compares them with previously evaluated hashes from the .json
file in the project's .pvs-studio
directory. Only files with changed hashes are analyzed, allowing focus on files modified in the current pull request.
Note. The modified file analysis mode only works for projects built with MSBuild. You may read about this mode and its alternatives for other projects in this article.
After the analysis, we need to convert reports via workflow to the next step.
- name: Convert report
if: always()
run: |
plog-converter sast-in-ci-example.json \
-t json -n relative -R toRelative -r $PWD
plog-converter relative.json \
-t sarif -n pvs-report -r file://
We replace the absolute paths in the original report with relative paths using the first call to the command-line utility, plog-converter
, substituting the source tree root label (|?|
) for the current directory.
The second call to the utility converts the report to SARIF format for CodeQL, a tool for working with static analyzers within GitHub, and replaces the previously set source tree root labels with file://
to obtain the valid CodeQL URI.
Note. You can read more about how the
plog-converter
utility works in the documentation.
Now we upload the previously generated report to CodeQL. It enables the analyzer to interact with GitHub and to view the report directly on the pull request page:
- name: Publish report
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pvs-report.sarif
category: PVS-Studio
At this step, we specify the tool being used as well as the path to the analyzer report.
Note that the last two steps contain if: always()
to run the analysis. Why? When warnings are detected, the analyzer will return a non-zero return code, causing the analysis to fail. However, we still should upload the report to CodeQL to pass the quality gate for merging branches.
"It'd be possible to configure the run of these stages only when the analysis fails," someone may think. But it's not that simple either. If the analyzer doesn't detect any issues, we still need to upload an empty report for GitHub to indicate no errors and approve the merge.
Then we commit the file in which the analyzer stores the project dependency hashes. This ensures that only files changed in the pull request will be analyzed in the next run. It's important that this step runs only if the analysis stage completes successfully and confirms that there are no issues with the current changes and they can be safely merged into the main branch:
- name: Commit dependency caches
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
run: |
git config user.name 'github-actions[bot]'
git config user.email \
'github-actions[bot]@users.noreply.github.com'
git add .pvs-studio
if ! git diff --cached --quiet; then
git commit \
-m "[skip actions] PVS-Studio dependency caches"
git pull --rebase origin "$BRANCH_NAME"
git push origin HEAD:"$BRANCH_NAME"
else
echo "No changes to commit, skipping push"
fi
These commits should originate from the user github-actions[bot]
, and the message should contain [skip actions]
, so that GitHub Actions doesn't run a re-check after such a commit.
Full text of the YAML file
name: Build and PVS-Studio analysis
on:
pull_request:
branches: [main]
jobs:
build-analyze:
if: github.actor != 'github-actions[bot]'
permissions: write-all
runs-on: ubuntu-24.04
steps:
- name: Check out repository code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install tools
run: |
run: |
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt \
| sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo add-apt-repository ppa:dotnet/backports
sudo apt update
sudo apt-get install -y dotnet-sdk-9.0
sudo apt install pvs-studio
sudo apt install pvs-studio-dotnet
pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
- name: Build
run: |
dotnet build sast-in-ci-example.sln
- name: Analyze
run: |
pvs-studio-dotnet -t sast-in-ci-example.sln \
-o ./sast-in-ci-example.json -r \
--disableLicenseExpirationCheck -F \
-C .pvsconfig
- name: Convert report
if: always()
run: |
plog-converter sast-in-ci-example.json \
-t json -n relative -R toRelative -r $PWD
plog-converter relative.json -t sarif -n pvs-report -r file://
- name: Publish report
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pvs-report.sarif
category: PVS-Studio
- name: Commit dependency caches
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .pvs-studio
if ! git diff --cached --quiet; then
git commit -m "[skip actions] PVS-Studio dependency caches"
git pull --rebase origin "$BRANCH_NAME"
git push origin HEAD:"$BRANCH_NAME"
else
echo "No changes to commit, skipping push"
fi
How to set up a ruleset
After we've configured the workflow, we need to create a ruleset that will work with the project's main branch.
Navigate to the repository settings and open the Rules section. Then, click New ruleset—this will open a form for creating a new ruleset.
At the top, set its name and change the Enforcement status to Active, so that the ruleset is applied automatically after creation:
Next, in the Target branches section, specify the project's main branch:
Now, in the Branch rules section, configure the settings to protect the repository's main branch.
Select the option that requires branches to be up-to-date before merging. This ensures that CI checks run on the latest code.
Next, select Block force pushes, which prevent previously unreviewed code from entering the main branch.
Finally, select Require code scanning results to prevent merging into the main branch without running static analysis. In the dropdown, select PVS-Studio as the used tool and set the critical error level. In our case, we'll set both values to All, meaning that any error in the source code is critical for the test project:
Next, you just need to save the settings.
Some issues
At first, the example required all checks to pass before merging (the Require status checks to pass setting), in addition to the branch merge rules mentioned earlier. But this led to a problem: If this option is enabled, GitHub waits for the workflow to finish after a bot commit. However, the workflow won't start because the YAML file tells it to skip checks after bot commits.
To work around this problem, let's add another stage to the beginning of the workflow, which will return exit 0
when the check is triggered by a bot commit. It turns out that GitHub Actions generally doesn't want to run a check for a commit that occurred during the previous check :(
Testing
Since this is a test project, we can intentionally make a mistake in the source code to check whether the pipeline will work :)
if (a < b && a < b) <=
{
Console.WriteLine(
"Here will be a PVS-Studio warning :)"
);
}
The PVS-Studio warning: V3001 There are identical sub-expressions 'a < b' to the left and to the right of the '&&' operator. Program.cs 4 1
After creating a pull request from a new branch with this code, GitHub automatically triggers the check. You'll then see a nice results window directly on the pull request page:
Scroll down, and we can notice another message—merging is blocked because PVS-Studio issued some warnings:
Clicking Code scanning results, we'll see a page with analysis results.
In this case, there is only one warning that we've added before.
Other options
In this article, we looked at how to use PVS-Studio with GitHub Actions, but that's not the only option out there.
Besides GitHub Actions, PVS-Studio analyzer integrates nicely with other cloud-based CI systems like CircleCI, Travis CI, GitLab, and Azure DevOps.
Moreover, PVS-Studio supports integration with CI systems like Jenkins and TeamCity.
Don't forget about web dashboards like SonarQube, DefectDojo, and CodeChecker to handle reports from static analyzers—PVS-Studio can work with them too.
Note. You can read more about how to run CodeChecker and import PVS-Studio analysis results here.
La commedia è finita
Integrating static analysis into CI is easier than it seems. Just don't treat static analysis as an extra or just a checkbox—it'd be better to make it a natural part of the development workflow for maintaining code quality.
A well-configured analyzer that runs automatically during pull requests can catch issues before they reach the main branch and provide developers with fast feedback. This helps the whole team feel more confident about its product quality.
Static analyzers won't replace developers, but these tools can save time and reduce stress, and leave more energy to write cool code lines instead of fixing bugs.
Top comments (0)