CI/CD Integration (Layer 4)
Gadriel Code is built to run unattended on a CI runner. This is Layer 4 (L4)
of the four execution layers — the full-repo scan that
fires on merge to your main branch (or on a schedule), builds the codebase graph
from scratch, runs all six scan types, sweeps the entire git history for
secrets, and emits the complete .security/ artifact set.
Because Gadriel is local-first, the runner does all the work: nothing about your code leaves the machine. The only outbound calls are the license check and the OSV CVE snapshot sync, both of which can be disabled or pre-warmed (see Auth in CI and Pre-warming OSV).
L4 follows the product's quality-over-latency principle: a CI scan is allowed to take minutes if that buys thorough analysis. Budget accordingly — see Execution Layers.
Contents
- The moving parts
- GitHub Actions
- GitLab CI
- Generic / other CI
- The exit-code contract
- Auth in CI
- Pre-warming the OSV snapshot
- Not leaving
.security/behind - PR-comment integration (Layer 3)
- Recommended gating policy
The moving parts
Every CI integration is the same four steps, whatever the provider:
| Step | What it does | Notes |
|---|---|---|
| Full-history checkout | Fetch the complete git history. | fetch-depth: 0 (or your provider's equivalent). Required so the L4 git-history secret sweep can walk every commit. A shallow clone hard-fails the sweep — see below. |
Install gadriel | Pull the self-contained binary via npm. | npm install -g gadriel. No native build step; the binary ships in the package. |
| Authenticate | Cache a valid license token on the runner. | gadriel auth login --token "$SECRET", or run local-only — see Auth in CI. |
| Scan | gadriel code scan . --layer l4 --fail-on <threshold>. | Writes findings.json, renders the report bundle, and sets the exit code that gates the job. |
| Upload artifacts | Publish findings.json + the report bundle. | So reviewers can read results without re-running the scan. |
Setting --layer l4 explicitly is recommended even though Gadriel infers l4
from common CI environment variables: it makes the intent unambiguous, drives
correct audit-log attribution, and turns on the git-history sweep
automatically. It does not change which rules run.
GitHub Actions
A ready-to-paste workflow. It runs on pushes to main, blocks the job on any
critical finding, and always uploads the artifacts (even when the gate
trips) so you can inspect what failed.
yaml# .github/workflows/gadriel-code.ymlname: Gadriel Code Security (L4)on:push:branches: [main]workflow_dispatch:permissions:contents: readjobs:scan:runs-on: ubuntu-lateststeps:- name: Checkout (full history for the git-history secret sweep)uses: actions/checkout@v4with:fetch-depth: 0 # REQUIRED — L4 sweeps every commit for secrets- name: Set up Node.jsuses: actions/setup-node@v4with:node-version: '20'- name: Install Gadrielrun: npm install -g gadriel- name: Authenticaterun: gadriel auth login --token "$GADRIEL_TOKEN"env:GADRIEL_TOKEN: ${{ secrets.GADRIEL_TOKEN }}- name: Pre-warm the OSV CVE snapshotrun: gadriel code policies --osv- name: Scanrun: gadriel code scan . --layer l4 --fail-on critical --non-interactive- name: Upload security artifactsif: always() # publish even when the gate tripsuses: actions/upload-artifact@v4with:name: gadriel-findingspath: |.security/findings.json.security/reports/
Notes:
fetch-depth: 0is not optional if you want the git-history secret sweep.actions/checkoutdefaults to a shallow clone (fetch-depth: 1); the L4 sweep hard-fails on a shallow clone with a remediation message (see the exit-code contract). If you deliberately don't want the sweep, pass--no-git-historyand you can dropfetch-depth: 0.--non-interactiveis the CI-friendly alias for--osv-auto-sync=yes; it prevents the first-run OSV prompt from hanging a non-TTY pipeline. Since we pre-warm OSV in the previous step, the sync is already done — but the flag keeps the scan robust if the snapshot is missing.if: always()on the upload step ensures the artifacts survive a tripped gate — that's exactly when you want to read them.
GitLab CI
GitLab surfaces SAST findings from a JSON artifact at a path it collects. Use
--output to land findings.json where the pipeline expects it, and
GIT_DEPTH: 0 for the full-history checkout.
yaml# .gitlab-ci.ymlgadriel-code:stage: testimage: node:20variables:GIT_DEPTH: 0 # full history for the git-history secret sweepbefore_script:- npm install -g gadriel- gadriel auth login --token "$GADRIEL_TOKEN" # GADRIEL_TOKEN = masked CI variable- gadriel code policies --osv # pre-warm the CVE snapshotscript:- >gadriel code scan . --layer l4 --fail-on critical --non-interactive--output findings/findings.json --output-format ocsfartifacts:when: always # keep artifacts even when the gate tripspaths:- findings/findings.json- .security/reports/
Notes:
--output findings/findings.jsonwrites the findings file to a fixed path (parent directories are created automatically), independent of the default.security/findings.json. Point your artifact collector at it.--output-format ocsfis the default OCSF envelope. SARIF is not yet supported (--output-format sarifexits with a clear message); until it lands, publish the OCSF JSON.- Set
GADRIEL_TOKENas a masked, protected CI/CD variable in Settings → CI/CD → Variables.
Generic / other CI
CircleCI, Jenkins, Buildkite, Drone, and friends all follow the same recipe. The two provider-specific concerns are (1) how you request a full-history checkout and (2) how you inject the license token as a secret.
bash#!/usr/bin/env bashset -euo pipefail# 1. Ensure a full clone (provider-specific). For a manual clone:# git fetch --unshallow || true# 2. Install the self-contained binary.npm install -g gadriel# 3. Authenticate from a CI secret.gadriel auth login --token "$GADRIEL_TOKEN"# 4. Pre-warm CVE data (optional but avoids DEGRADED coverage).gadriel code policies --osv# 5. Scan. The process exit code gates the job.gadriel code scan . --layer l4 --fail-on critical --non-interactive \--output findings/findings.json# 6. Publish findings/ and .security/reports/ as build artifacts# using your provider's artifact mechanism.
gadriel code init --ci github,gitlab,circleci,jenkins (or --ci all) scaffolds
starter templates for these providers, each tagged with a GADRIEL_MANAGED
sentinel so re-running init is idempotent and --uninstall removes only
Gadriel-authored files. See init and
Upgrades & Migration.
The exit-code contract
gadriel code scan communicates its verdict through the process exit code —
this is the signal your CI job gates on. Writing the .security/ artifacts
always succeeds regardless of the code.
| Code | Meaning | CI action |
|---|---|---|
| 0 | Clean — no findings at or above --fail-on. | Job passes. |
| 1 | Security gate tripped — a finding met or exceeded --fail-on. | Fail the job (this is the block signal). |
| 2 | Tooling error / crash — scan could not complete (timeout, malformed input, shallow clone with --git-history, stale-beyond-threshold OSV). | Investigate; do not treat as a security pass or a security failure. |
| 7 | License gate — no valid token (missing / revoked / grace-exceeded). | Fix auth, don't retry blindly. See Auth in CI. |
Codes 3 (feature not yet available / fatal render in report) and 4–6
are reserved/unused for scan. See the full
exit-code reference and the
CLI reference.
Shallow-clone hard-fail. When the git-history sweep runs (automatic at
--layer l4) against a shallow checkout, the scan hard-fails as a tooling
error (exit 2) with a message telling you to deepen the clone
(fetch-depth: 0 / GIT_DEPTH: 0 / git fetch --unshallow). Treat exit 2
distinctly from exit 1 in your pipeline logic: exit 1 means "the scan ran and
found blocking issues"; exit 2 means "the tool could not run correctly." Most
CI systems fail the job on any non-zero exit, which is the safe default — but if
you branch on the code, keep the two apart.
--verdict-exit-zero. For a reporting-only pipeline (scan, publish
artifacts, but never block), pass --verdict-exit-zero: it collapses a
verdict-driven non-zero exit to 0 while a genuine tooling error still exits
non-zero, so a downstream step can read findings.json and decide the gate.
Auth in CI
Gadriel is license-gated (see Authentication). A scan that can't find a valid token exits 7. There are two supported CI postures:
1. Token from a CI secret (recommended)
Mint a token at https://app.gadriel.ai/developers/, store it as a secret
(GitHub: Actions secrets; GitLab: a masked CI/CD variable), and log in at the
start of the job:
bashgadriel auth login --token "$GADRIEL_TOKEN"
This caches the token at ~/.gadriel/auth/token.jwt (mode 0600) on the
runner; every subsequent gadriel code command in the job runs without
re-authenticating.
2. Local-only with PREFLIGHT_OFFLINE=1
export PREFLIGHT_OFFLINE=1 puts Gadriel in fully local mode: no outbound
calls, and the cached token is validated locally within its offline-grace
window (default 7 days). Use this on air-gapped runners or to guarantee zero
network egress.
Offline does not skip the license gate. PREFLIGHT_OFFLINE=1 only
suppresses the online validation call — it still requires a valid cached
token within the grace window, and a missing/expired token still exits 7.
Offline mode is about avoiding network calls, not bypassing licensing. See
License/auth errors.
Combine PREFLIGHT_OFFLINE=1 with gadriel code scan --offline to also skip
the OSV sync (your coverage will report DEGRADED; see below).
Pre-warming the OSV snapshot
SCA (dependency-CVE) detection reads a local OSV advisory snapshot. On a fresh
runner the snapshot is absent, and a first scan without it proceeds in
DEGRADED coverage — CVE-dependent rules are skipped and the verdict line
carries a · coverage DEGRADED suffix.
Fetch it once in a dedicated setup step so the scan runs with full coverage:
bashgadriel code policies --osv # auto-detects ecosystems from your manifestsgadriel code policies --osv --force # force a re-fetch (bypass freshness check)
Tips:
- Cache the snapshot between runs. It lives under the per-machine store
(
~/.gadriel/store, overridable viaGADRIEL_STORE_DIR). Caching that path in your CI cache keys avoids re-downloading on every run. - Scope the ecosystems to shrink the download when only some apply:
gadriel code scan --osv-ecosystems=pypi,npm.auto(the default) already fetches only the shards your manifests need. - npm-heavy repos can bump the per-ecosystem size cap and report DEGRADED — see OSV "coverage DEGRADED" for the caps and fixes.
- Air-gapped runners that sync the snapshot out-of-band should scan with
--offline(orPREFLIGHT_OFFLINE=1) and accept DEGRADED, or ship a pre-synced store to the runner.
Not leaving .security/ behind
By default a scan writes its artifacts to .security/ in the workspace. In an
ephemeral CI checkout that's usually fine — the workspace is discarded. But if
the runner reuses the workspace, or you must guarantee no .security/ directory
is left behind, use --ephemeral with --output:
bash# Findings land at findings/findings.json; no .security/ dir is written.gadriel code scan . --layer l4 --fail-on critical \--ephemeral --output findings/findings.json --no-html --non-interactive
--ephemeralwrites all scan artifacts to a discarded TempDir instead of.security/.--output <path>redirects the findings file to a path you keep (parent dirs created automatically). Without--output,--ephemeralis effectively a dry run — artifacts are written then immediately discarded.--no-htmlskips the post-scan HTML re-render, which is wasted work when you're not keeping.security/reports/.
PR-comment integration (Layer 3)
L4 is the merge-time full scan. For per-PR feedback, wire up Layer 3 (L3) — the PR gate. L3 scans the diff against the base branch and posts a single structured PR comment (overall score, per-pillar table, finding counts, top criticals in a collapsible section), edited in place on each push rather than accumulating a wall of stale comments. Secret values are never included in the comment — only the key name.
L3 also runs on a clean CI checkout with full git history, so the git-history
secret sweep is enabled there too. Its default --fail-on threshold is
critical, and the CI job exits 2 when the comment carries the BLOCKING
banner.
A minimal PR-gate step (GitHub Actions, on pull_request):
yamlon:pull_request:# ... checkout with fetch-depth: 0, install, auth as above ...- name: Scan the PR diffrun: gadriel code scan . --layer l3 --fail-on critical --non-interactive
GitHub is fully supported for the in-place PR comment; GitLab/Bitbucket currently get a degraded "new comment per push" experience until their adapters ship. See Execution Layers → L3 for the comment contract in full.
Recommended gating policy
Gate progressively — cheap and forgiving at the developer's desk, strict at the
gate that protects main. The thresholds follow the severity ladder
none < info < low < medium < high < critical.
| Layer | Where | Command surface | Recommended --fail-on | Rationale |
|---|---|---|---|---|
| L2 | Local pre-commit hook | gadriel code scan --staged | critical | Block only the worst at commit time so the developer is never stuck on borderline findings. |
| L3 | PR / branch push | gadriel code scan --layer l3 | critical → tighten to high | Start at critical while a team onboards; move to high once the false-positive rate is trusted. |
| L4 | Merge to main | gadriel code scan --layer l4 | high (stricter than commit) | The last line of defense — nothing high-or-worse should reach the default branch. |
- Start loose, tighten over time. Begin with
--fail-on criticaleverywhere, watch the false-positive program settle, then ratchet L4 (and later L3) down tohigh. - Soak a new policy bundle with
--fail-on none(never gates) before you turn on blocking, to see what would have fired without breaking the build. - Encode the defaults in a version-controlled
.gadriel.toml([scan] fail_on = "high") so every invocation inherits them and CI stays in sync with local runs. See Configuration.
Related documentation
- Execution Layers — L1 → L4, the
--layerselector, and the full exit-code contract. - CLI Reference —
scan— every scan flag. - Authentication — tokens, the license gate, offline mode.
- Configuration —
.gadriel.tomldefaults for the flags above. - Troubleshooting — DEGRADED coverage, exit-code reference, and CI-specific gotchas.
- Upgrades & Migration — keeping CI templates and scaffold current. </content>
