CI / Diff-Based Analysis
View SourceGuide to using Ragex and MetaCredo for incremental, diff-based code analysis in CI/CD pipelines. Only files changed in a pull request are analyzed, giving fast feedback without noise from pre-existing issues.
Table of Contents
- Quick Start
- How It Works
- mix ragex.analyze --diff
- mix ragex.ci
- Output Formats
- GitHub Actions Integration
- Other CI Systems
- API Reference
- Configuration
- Troubleshooting
Quick Start
Add one line to your CI pipeline:
mix ragex.ci --format github
This runs both Ragex analysis (security, complexity, dead code, etc.)
and MetaCredo checks (72 cross-language static analysis checks) on the
files changed in the current branch vs origin/main. Issues appear as
inline PR annotations in GitHub.
How It Works
git diff origin/main...HEAD
|
v
[changed files list] --- filter by supported extensions
|
v
[Ragex: index only those files into knowledge graph]
|
v
[Run all enabled analyses on the full graph]
|
v
[Filter results to only report issues in changed files]
|
v
[MetaCredo: run 72 checks on changed files only]
|
v
[Output in CI / GitHub / JSON format]
|
v
[Exit non-zero if issues found]Key design decisions:
- Indexing is scoped: only changed files are parsed and added to the knowledge graph, which is the main performance win (seconds instead of minutes for large projects).
- Whole-project analyses still work: dead code detection, circular dependencies, coupling metrics, etc. run on the full graph but only report issues that touch changed files. This means a PR that introduces a new circular dependency will still be flagged.
- Deleted files are excluded: the diff filter (
ACMR) only includes Added, Copied, Modified, and Renamed files.
mix ragex.analyze --diff
The --diff flag enables diff-based mode on the existing mix ragex.analyze
task. It implies --ci (machine-friendly output, non-zero exit on issues).
Options
| Flag | Default | Description |
|---|---|---|
--diff | false | Enable diff-based analysis |
--base REF | origin/main | Base git ref (the target branch) |
--head REF | HEAD | Head git ref (the PR branch) |
--format FORMAT | text | Output format: text, json, markdown, github |
All other mix ragex.analyze flags work as usual (--security,
--complexity, --severity, etc.).
Examples
# Analyze only changed files, CI text output
mix ragex.analyze --diff
# Custom base branch
mix ragex.analyze --diff --base origin/develop
# Only security checks on changed files, GitHub annotations
mix ragex.analyze --diff --security --format github
# JSON output for downstream tooling
mix ragex.analyze --diff --format json --output report.json
mix ragex.ci
A convenience task that runs both tools in sequence:
mix ragex.analyze --diff(all Ragex analyses)mix metacredo --diff --strict(all MetaCredo checks)
Exits with code 1 if either tool finds issues.
Options
| Flag | Default | Description |
|---|---|---|
--base REF | origin/main | Base git ref |
--head REF | HEAD | Head git ref |
--format FORMAT | text | Output format: text, github |
Examples
# Default: diff against origin/main
mix ragex.ci
# GitHub Actions with inline annotations
mix ragex.ci --format github
# Custom base ref (e.g. from $GITHUB_BASE_REF)
mix ragex.ci --base origin/develop
Output Formats
text (default)
Machine-readable one-line-per-issue format (no ANSI colors):
SECURITY: sql_injection (critical) lib/repo.ex:42 - SQL concatenation with user input
COMPLEXITY: MyApp.Worker.process/3 (cyclomatic=18)
ragex: 2 issue(s) foundgithub
GitHub Actions workflow commands that produce inline PR annotations:
::error file=lib/repo.ex,line=42::SECURITY sql_injection: SQL concatenation with user input
::warning file=lib/worker.ex,line=15::COMPLEXITY MyApp.Worker.process/3 cyclomatic=18
ragex: 2 issue(s) foundGitHub renders these as annotations directly on the PR diff:
::error-- red annotation, blocks merge with branch protection::warning-- yellow annotation, informational::notice-- grey annotation, low priority
json
Full structured report for downstream processing:
mix ragex.analyze --diff --format json --output report.json
markdown
Human-readable report with headers and formatting:
mix ragex.analyze --diff --format markdown --output report.md
GitHub Actions Integration
Recommended workflow
Add this to .github/workflows/ci.yml:
analysis:
name: Diff Analysis
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git diff to work
- uses: erlef/setup-beam@v1
with:
elixir-version: '1.19'
otp-version: '28'
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
- name: Install dependencies
run: mix deps.get
- name: Run diff analysis
run: mix ragex.ci --base origin/${{ github.base_ref }} --format githubImportant notes:
fetch-depth: 0is required sogit diffcan access the full history. Without it, GitHub Actions performs a shallow clone and the diff will fail.origin/${{ github.base_ref }}resolves to the PR's target branch (e.g.origin/main). This is the correct base ref for PR analysis.
Separate jobs for ragex and metacredo
If you prefer separate CI jobs (e.g. for independent failure reporting):
ragex-analysis:
name: Ragex Analysis
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: erlef/setup-beam@v1
with:
elixir-version: '1.19'
otp-version: '28'
- run: mix deps.get
- run: mix ragex.analyze --diff --base origin/${{ github.base_ref }} --format github
metacredo-analysis:
name: MetaCredo Analysis
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: erlef/setup-beam@v1
with:
elixir-version: '1.19'
otp-version: '28'
- run: mix deps.get
- run: mix metacredo --diff --base origin/${{ github.base_ref }} --format github --strictOther CI Systems
GitLab CI
ragex-analysis:
stage: test
only:
- merge_requests
script:
- mix deps.get
- mix ragex.ci --base origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAMEGeneric (any CI)
#!/bin/bash
# ci-analysis.sh
set -e
BASE_REF="${CI_BASE_REF:-origin/main}"
# Run analysis, exit non-zero on issues
mix ragex.analyze --diff --base "$BASE_REF" --ci
mix metacredo --diff --base "$BASE_REF" --strict
Pre-commit hook
#!/bin/bash
# .git/hooks/pre-commit
# Analyze staged files against HEAD
STAGED=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ex|exs|py|rb|erl)$')
if [ -n "$STAGED" ]; then
echo "Running analysis on staged files..."
mix ragex.analyze --diff --base HEAD~1 --head HEAD --ci
fi
API Reference
Ragex.Git.Diff
The diff file resolver module. Tries egit (libgit2 NIF) first, falls
back to the git CLI.
# Get changed files between two refs
{:ok, files} = Ragex.Git.Diff.changed_files("/path/to/repo",
base: "origin/main",
head: "HEAD",
filter: "ACMR",
extensions: [".ex", ".exs"]
)
# => {:ok, ["lib/foo.ex", "lib/bar.ex"]}
# Convenience: resolve repo root + changed files
{:ok, repo_root, files} = Ragex.Git.Diff.changed_files_for_path(".")
# Bang version (raises on error)
files = Ragex.Git.Diff.changed_files!("/path/to/repo")Ragex.Analysis.Runner
# Analyze only specific files (used by diff mode)
{:ok, result} = Runner.analyze_files(["/abs/path/lib/foo.ex", "/abs/path/lib/bar.ex"])
# Filter analysis results to changed files
changed_set = MapSet.new(["lib/foo.ex", "lib/bar.ex"])
filtered = Runner.filter_results_by_files(results, changed_set)The filter preserves structural analyses (circulars, coupling, god modules) unmodified, since they describe project-wide properties. Per-file analyses (security, complexity, smells, duplicates, dead code) are filtered to only include issues in the changed file set.
MetaCredo.Git
Standalone git helper (no ragex dependency):
# Resolve repo root
repo = MetaCredo.Git.repo_root(File.cwd!())
# Get changed files
{:ok, files} = MetaCredo.Git.changed_files(repo,
base: "origin/main",
head: "HEAD",
extensions: [".ex", ".exs"]
)Configuration
Base ref
The default base ref is origin/main. Override it for projects that use
a different default branch:
# develop-based workflow
mix ragex.ci --base origin/develop
# Release branch
mix ragex.ci --base origin/release/v2
Selecting analyses
In diff mode, all analyses are enabled by default. To run only specific checks:
# Only security + circular dependency detection
mix ragex.analyze --diff --security --circulars --format github
Severity filtering
# Only critical and high severity issues (skip medium/low)
mix ragex.analyze --diff --severity high --format github
Troubleshooting
"fatal: bad revision 'origin/main...HEAD'"
Cause: the remote origin/main branch is not available in the CI
checkout.
Fix: ensure fetch-depth: 0 in actions/checkout@v4, or explicitly
fetch the base branch:
git fetch origin main
mix ragex.ci --base origin/main
"No changed files found in diff, nothing to analyze"
Cause: the diff between base and head produced no files matching supported extensions.
This is expected when only non-code files changed (e.g. README, images). The task exits cleanly with code 0.
"--diff requires a git repository, but none was found"
Cause: running outside a git repository.
Fix: ensure the working directory is inside a git repo. In CI, the checkout step typically handles this.
Slow on first run
The first mix ragex.ci invocation in CI compiles the project and its
dependencies (including EXLA for ML). Subsequent runs use the cache.
Tips for faster CI:
- Cache
deps/and_build/between runs - Use
actions/cache@v4keyed onmix.lock - Consider a separate
depsjob that other jobs depend on
Version: Ragex 0.16.0 Last Updated: May 2026