GitLab has no official CodeCharter integration as a component, but a few lines of YAML are enough to run the CLI cleanly in the pipeline.
If your repository is hosted on GitHub and only the build runs on GitLab, you can additionally publish a branded CodeCharter check run and PR comment from this pipeline — see Publish checks from any CI.
Minimal setup
.gitlab-ci.yml:
codecharter:
image: mcr.microsoft.com/dotnet/sdk:9.0
stage: test
before_script:
- curl -sSL -H "Authorization: Bearer $CODECHARTER_API_KEY" -o codecharter.tar.gz https://codecharter.tools/api/v1/cli/linux-x64/latest
- mkdir -p /opt/codecharter && tar -xzf codecharter.tar.gz -C /opt/codecharter
- chmod +x /opt/codecharter/codecharter
- export PATH="/opt/codecharter:$PATH"
script:
- codecharter analyze MySolution.sln --fail-on error --output sarif --output-file codecharter.sarif
artifacts:
when: always
paths:
- codecharter.sarif
Replace MySolution.sln with the path to your solution or project file —
analyze expects a .sln, .slnx, or .csproj file, not a directory.
Getting an API key
- Generate a key in the portal under API Keys.
- In GitLab, go to
Settings → CI/CD → Variablesand store it asCODECHARTER_API_KEYwith "Protected" and "Masked" enabled.
The variable does double duty: before_script uses it to download the CLI,
and the analyze step uses it to automatically mint a short-lived license
when none is cached. It must therefore be available to the whole job, which
a GitLab CI/CD variable is by default.
SARIF and GitLab SAST
GitLab's artifacts.reports.sast does not ingest SARIF — it expects GitLab's
own security report JSON schema, which CodeCharter does not produce. Pointing
reports.sast at the SARIF file would surface nothing in the GitLab UI.
The SARIF file (SARIF 2.1.0) is still useful as a plain artifact: download it from the pipeline, feed it to SARIF-aware tools, or archive it for audits. To get findings onto merge requests, use the Code Quality converter under Merge request reports.
Caching
CodeCharter has no analysis-result cache, so there is no speedup to be had from
caching analysis output. The one thing worth caching is the rule-bundle cache:
when your project uses portal rule profiles (a .codecharter/codecharter.lock.json
in the .codecharter/ directory), downloaded bundles are stored in
.codecharter/cache. Persisting that directory skips re-downloading the bundles on
warm runs:
codecharter:
# ... as above ...
cache:
key: codecharter-$CI_COMMIT_REF_SLUG
paths:
- .codecharter/cache/
If your solution file is not at the repo root, adjust the path accordingly. Without portal profiles there is nothing to cache and you can skip this section.
Version pinning
Instead of /latest/, pull a specific CLI version:
- curl -sSL -H "Authorization: Bearer $CODECHARTER_API_KEY"
-o codecharter.tar.gz
https://codecharter.tools/api/v1/cli/linux-x64/1.0.12
Recommendation: always pin in CI, see Versioning.
Every download response carries an X-CodeCharter-Sha256 header. Compare it
against the checksum of the downloaded archive if you want to verify download
integrity in the job.
Self-hosted GitLab runners
A self-hosted runner needs access to codecharter.tools on every
pipeline run — the before_script above downloads the CLI each time. No other
changes are required: the minimal setup uses mcr.microsoft.com/dotnet/sdk
from Microsoft's public registry. If your organisation mirrors Docker images
internally, pull the .NET SDK image from your own mirror instead.
In air-gapped networks: mirror the CLI binary to the runner and use a local path instead of a curl download.
Merge request reports
GitLab's "Code Quality" section on MRs requires a JSON file in the GitLab Code Quality schema (different from SARIF). CodeCharter does not produce this format out of the box — you must write the converter yourself.
A minimal jq mapping from CodeCharter JSON output to the GitLab Code Quality schema
looks like this:
codecharter analyze MySolution.sln --fail-on error --output json --output-file codecharter.json
jq '[.violations[] | {
description: .message,
check_name: .ruleId,
severity: (if .severity == "error" then "major" elif .severity == "warning" then "minor" else "info" end),
fingerprint: .fingerprint,
location: { path: .filePath, lines: { begin: .lineNumber } }
}]' codecharter.json > codequality.json
Then wire the output file to the GitLab artifact:
artifacts:
reports:
codequality: codequality.json
The JSON output already contains a stable fingerprint per violation that does
not depend on line numbers, so GitLab does not show duplicate annotations when
unrelated lines shift.
Merge request pipelines: only changed lines
In MR pipelines you can restrict both the reported findings and the --fail-on
gate to lines changed by the MR with --git-ref:
codecharter-mr:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: 0
script:
- codecharter analyze MySolution.sln --fail-on error --git-ref $CI_MERGE_REQUEST_DIFF_BASE_SHA..HEAD
--git-ref runs git diff <range> --unified=0 internally, so the base commit
must be present in the clone (hence GIT_DEPTH: 0).
For legacy codebases there is also baseline support: --write-baseline records
the current findings once, and --baseline then makes output and --fail-on
apply only to new findings.
Exit codes
For pipeline debugging, codecharter analyze uses these exit codes:
| Code | Meaning |
|---|---|
| 0 | No gating findings |
| 1 | Findings at or above the --fail-on level; without --fail-on, any finding at all |
| 2 | Usage error (invalid options, corrupt baseline, ...) |
| 3 | Solution or project could not be loaded |
| 6 | License error |
If the job suddenly fails at the download or analyze step with HTTP 402, the
subscription has expired — both the CLI download and the license minting return
402 in that case.
Pipeline variables
Useful variables available in the job:
| Variable | Usage |
|---|---|
$CI_PROJECT_DIR |
Repo root |
$CI_COMMIT_REF_SLUG |
Branch name for caching |
$CI_MERGE_REQUEST_IID |
MR number when present |
$CI_MERGE_REQUEST_DIFF_BASE_SHA |
MR base commit for --git-ref |
$CODECHARTER_API_KEY |
Your secret |