GCA · INTEGRATIONS

    CI / CD Integration

    Running Gadriel Code unattended on a CI runner — Layer 4.

    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

    Every CI integration is the same four steps, whatever the provider:

    StepWhat it doesNotes
    Full-history checkoutFetch 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 gadrielPull the self-contained binary via npm.npm install -g gadriel. No native build step; the binary ships in the package.
    AuthenticateCache a valid license token on the runner.gadriel auth login --token "$SECRET", or run local-only — see Auth in CI.
    Scangadriel code scan . --layer l4 --fail-on <threshold>.Writes findings.json, renders the report bundle, and sets the exit code that gates the job.
    Upload artifactsPublish 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.

    # .github/workflows/gadriel-code.yml
    name: Gadriel Code Security (L4)
    on:
    push:
    branches: [main]
    workflow_dispatch:
    permissions:
    contents: read
    jobs:
    scan:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout (full history for the git-history secret sweep)
    uses: actions/checkout@v4
    with:
    fetch-depth: 0 # REQUIRED — L4 sweeps every commit for secrets
    - name: Set up Node.js
    uses: actions/setup-node@v4
    with:
    node-version: '20'
    - name: Install Gadriel
    run: npm install -g gadriel
    - name: Authenticate
    run: gadriel auth login --token "$GADRIEL_TOKEN"
    env:
    GADRIEL_TOKEN: ${{ secrets.GADRIEL_TOKEN }}
    - name: Pre-warm the OSV CVE snapshot
    run: gadriel code policies --osv
    - name: Scan
    run: gadriel code scan . --layer l4 --fail-on critical --non-interactive
    - name: Upload security artifacts
    if: always() # publish even when the gate trips
    uses: actions/upload-artifact@v4
    with:
    name: gadriel-findings
    path: |
    .security/findings.json
    .security/reports/

    Notes:

    • fetch-depth: 0 is not optional if you want the git-history secret sweep. actions/checkout defaults 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-history and you can drop fetch-depth: 0.
    • --non-interactive is 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.

    # .gitlab-ci.yml
    gadriel-code:
    stage: test
    image: node:20
    variables:
    GIT_DEPTH: 0 # full history for the git-history secret sweep
    before_script:
    - npm install -g gadriel
    - gadriel auth login --token "$GADRIEL_TOKEN" # GADRIEL_TOKEN = masked CI variable
    - gadriel code policies --osv # pre-warm the CVE snapshot
    script:
    - >
    gadriel code scan . --layer l4 --fail-on critical --non-interactive
    --output findings/findings.json --output-format ocsf
    artifacts:
    when: always # keep artifacts even when the gate trips
    paths:
    - findings/findings.json
    - .security/reports/

    Notes:

    • --output findings/findings.json writes 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 ocsf is the default OCSF envelope. SARIF is not yet supported (--output-format sarif exits with a clear message); until it lands, publish the OCSF JSON.
    • Set GADRIEL_TOKEN as 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.

    #!/usr/bin/env bash
    set -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.

    CodeMeaningCI action
    0Clean — no findings at or above --fail-on.Job passes.
    1Security gate tripped — a finding met or exceeded --fail-on.Fail the job (this is the block signal).
    2Tooling 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.
    7License 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 46 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:

    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:

    gadriel 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:

    gadriel code policies --osv # auto-detects ecosystems from your manifests
    gadriel 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 via GADRIEL_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 (or PREFLIGHT_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:

    # 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
    • --ephemeral writes 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, --ephemeral is effectively a dry run — artifacts are written then immediately discarded.
    • --no-html skips 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):

    on:
    pull_request:
    # ... checkout with fetch-depth: 0, install, auth as above ...
    - name: Scan the PR diff
    run: 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.

    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.

    LayerWhereCommand surfaceRecommended --fail-onRationale
    L2Local pre-commit hookgadriel code scan --stagedcriticalBlock only the worst at commit time so the developer is never stuck on borderline findings.
    L3PR / branch pushgadriel code scan --layer l3critical → tighten to highStart at critical while a team onboards; move to high once the false-positive rate is trusted.
    L4Merge to maingadriel code scan --layer l4high (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 critical everywhere, watch the false-positive program settle, then ratchet L4 (and later L3) down to high.
    • 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.

    </invoke>