roam-code provides a composite GitHub Action for automated code analysis on every pull request. No Docker build, no API keys, sub-60s PR analysis.
Copy this workflow into your repository at .github/workflows/roam.yml:
name: roam-code Analysis
on:
pull_request:
branches: [main, master]
push:
branches: [main, master]
permissions:
contents: read
pull-requests: write
security-events: write # Required for SARIF upload
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Cranot/roam-code@main
with:
commands: 'health pr-risk'
sarif: 'true'
comment: 'true'
gate: 'health_score>=60'That is all you need. The action installs roam-code, indexes your codebase, runs the requested analysis commands, posts a sticky PR comment with results, uploads SARIF findings to GitHub Code Scanning, and enforces quality gates.
| Input | Default | Description |
|---|---|---|
version |
latest |
roam-code version to install from PyPI. Use a pinned version for reproducibility (e.g., 11.1.2). |
commands |
health |
Space-separated roam commands to run. Each command produces JSON output that feeds into the PR comment and quality gate. |
changed-only |
false |
Incremental CI mode. Adapts supported commands to changed files and transitive dependents (when detectable). |
changed-depth |
3 |
Dependency depth used when computing changed+dependent file scope in changed-only mode. |
base-ref |
(auto) | Optional explicit base ref/SHA for incremental mode. Default is PR base SHA (or push before SHA). |
sarif |
false |
When true, exports SARIF for the selected SARIF command set and uploads a guarded combined SARIF file to GitHub Code Scanning. Requires security-events: write permission. |
sarif-commands |
auto |
Space-separated commands to export via --sarif. auto picks the SARIF-capable subset of commands (health, dead, complexity, rules, secrets, algo). |
sarif-category |
roam-code |
Base SARIF upload category. The action appends job/runtime suffixes to reduce collisions. |
sarif-max-runs |
20 |
Pre-upload guardrail: maximum runs kept in combined SARIF. Extra runs are dropped from the tail with a warning. |
sarif-max-results |
25000 |
Pre-upload guardrail: maximum results per run. Extra results are dropped from the tail with a warning. |
sarif-max-bytes |
10000000 |
Pre-upload guardrail: maximum SARIF JSON bytes (conservative cap before upload). |
comment |
true |
When true and running on a pull request, upserts one marker-managed sticky PR comment (idempotent) and removes duplicate sticky comments if they exist. Requires pull-requests: write permission. |
gate |
(empty) | Quality gate expression. Supports scalar checks (key>=value) and trend-aware functions (velocity(metric)<=0, direction(metric)!=worsening). The action exits with code 5 when the gate fails. |
cache |
true |
Cache pip packages and the .roam/ SQLite index between runs for faster incremental analysis. |
python-version |
3.11 |
Python version to use. Supports 3.9 through 3.13. |
| Output | Description |
|---|---|
health-score |
The health score (0-100) if the health command was included. |
exit-code |
The exit code from analysis. 0 = success, 5 = gate failure, 1 = error. |
sarif-file |
Path to the generated SARIF file (when sarif is true). |
sarif-category |
Resolved category used for SARIF upload. |
sarif-truncated |
true when SARIF guardrails dropped runs/results before upload. |
sarif-results |
Final SARIF result count after guardrails. |
changed-only |
Whether incremental mode was enabled. |
base-ref |
Resolved base ref/SHA used for incremental mode. |
affected-count |
Number of changed+dependent files detected for incremental mode. |
Quality gates let you enforce minimum standards on every PR. The gate expression is evaluated against the JSON summary of each analysis command.
key operator value
Where:
keyis any field in the JSON summary (e.g.,health_score,tangle_ratio,risk_score,issue_count)operatoris one of:>=,<=,>,<,=valueis a number
Trend-aware functions are also supported:
latest(metric)— latest value from trend payloadsdelta(metric)— first-to-last change over the trend windowslope(metric)— per-snapshot slopevelocity(metric)— worsening velocity (positive means trending worse)direction(metric)— semantic direction (improving,worsening,stable)
You can combine multiple expressions with commas:
gate: 'health_score>=70,velocity(cycle_count)<=0'# Require health score of at least 70
gate: 'health_score>=70'
# Require tangle ratio below 5%
gate: 'tangle_ratio<=5'
# Require zero critical issues
gate: 'issue_count=0'
# Fail if cycle count is accelerating in a bad direction
gate: 'velocity(cycle_count)<=0'
# Require trend direction to avoid worsening health
gate: 'direction(health_score)!=worsening'When a gate fails, the action:
- Prints an error annotation with the actual vs required value
- Exits with code 5 (distinct from code 1 for crashes)
- Marks the check as failed in the PR
The PR comment will show the gate result as PASSED or FAILED.
When sarif: 'true', the action:
- Generates SARIF per command from
sarif-commands(orautosubset) - Merges SARIF runs into one payload
- Applies upload guardrails (
sarif-max-runs,sarif-max-results,sarif-max-bytes) - Uploads via
github/codeql-action/upload-sarifusing resolvedsarif-category - Emits truncation warning metadata when guardrails drop findings
- Results appear in the Security tab under Code scanning alerts
This requires the security-events: write permission and GitHub Advanced
Security (free for public repositories).
sarif-max-runsandsarif-max-resultsalign with documented GitHub SARIF scale constraints.sarif-max-bytesis a conservative pre-upload byte cap to reduce failed uploads on large payloads.- If truncation occurs, use
sarif-truncatedandsarif-resultsoutputs to surface that in downstream CI steps.
roam-code generates SARIF results for:
health/cycle-- Dependency cycleshealth/god-component-- Components with excessive couplinghealth/bottleneck-- High-betweenness bottleneck symbolshealth/layer-violation-- Architectural layer violations
When cache: 'true' (default), the action caches:
-
pip packages -- Keyed on OS, Python version, and roam-code version. Avoids re-downloading roam-code and its dependencies on every run.
-
.roam/directory -- The SQLite index database. Keyed on OS and a hash of all source files (*.py,*.js,*.ts,*.go, etc.). When source files change, the cache misses androam initrebuilds only the changed files (incremental indexing).
Cache hits reduce analysis time from 30-60s to under 10s on typical codebases.
Set changed-only: 'true' to run incremental PR analysis.
- The action resolves a base ref (PR base SHA by default) and computes changed
plus transitive dependent files via
roam affected. - Supported commands are auto-adapted:
verify,syntax-check,test-gaps,suggest-reviewers,fileget the affected file set.pr-risk,pr-diff,semantic-diff,affected,api-changesget base/range aware flags.
- Unsupported commands still run in normal full-repo mode.
Example:
- uses: Cranot/roam-code@main
with:
commands: 'verify pr-risk api-changes'
changed-only: 'true'
changed-depth: '3'Any roam command can be passed via the commands input. Common choices:
| Command | What it does |
|---|---|
health |
Overall health score (0-100), cycles, god components, bottlenecks |
pr-risk |
PR risk score based on changed files, blast radius, coupling |
complexity |
Cognitive complexity analysis with severity ratings |
dead |
Dead code detection (unreferenced exports) |
debt |
Technical debt inventory |
fitness |
Fitness function evaluation against project rules |
breaking |
Detect breaking API changes |
conventions |
Naming convention violations |
Run roam --help for all 139 commands.
| Code | Meaning |
|---|---|
| 0 | Success -- analysis completed, no gate failures |
| 1 | Error -- unexpected failure or crash |
| 2 | Usage error -- invalid arguments or flags |
| 3 | Index missing -- roam init not run (should not happen with the action) |
| 5 | Gate failure -- quality gate check failed |
- uses: Cranot/roam-code@main
with:
commands: 'health complexity dead'
sarif-commands: 'health complexity dead'
sarif-category: 'roam-code-pr-${{ github.ref_name }}-${{ matrix.python-version }}'
gate: 'health_score>=80'
sarif: 'true'- uses: Cranot/roam-code@main
id: roam
with:
commands: 'pr-risk'
comment: 'false'
- name: Check risk score
if: steps.roam.outputs.exit-code != '0'
run: echo "Analysis found issues (exit ${{ steps.roam.outputs.exit-code }})"- uses: Cranot/roam-code@v11.1.2
with:
version: '11.1.2'
cache: 'false'
commands: 'health'- uses: Cranot/roam-code@main
id: analysis
with:
commands: 'health'
- name: Report health score
run: echo "Health score is ${{ steps.analysis.outputs.health-score }}"Add security-events: write to your workflow permissions:
permissions:
security-events: writeAdd pull-requests: write to your workflow permissions:
permissions:
pull-requests: writeThe first run indexes the entire codebase. Subsequent runs with cache: 'true'
will restore the cached index and only re-index changed files. Typical
improvement: 30-60s down to under 10s.
The gate key must exactly match a field in the JSON summary. Run
roam --json health locally to see available fields:
roam --json health | python3 -m json.tool | grep -A5 '"summary"'