Running in CI/CD
Stave is a standard CLI tool with stable exit codes, making it straightforward to run in any CI/CD pipeline.
This guide covers running the Stave CLI in CI environments. Stave does not have dedicated CI integration features — it runs the same way in CI as it does locally.
GitHub Actions (Docker)
Build the demo image in CI and use it to run evaluations:
name: Stave Safety Check
on:
pull_request:
paths:
- 'terraform/**'
- 'observations/**'
jobs:
stave-apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Stave image
run: docker build -f docs-content/demo/Dockerfile -t stave-demo .
- name: Validate inputs
run: |
docker run --rm -v ${{ github.workspace }}:/work -w /work stave-demo \
stave validate \
--controls ./controls \
--observations ./observations \
--strict
- name: Evaluate safety
run: |
docker run --rm -v ${{ github.workspace }}:/work -w /work stave-demo \
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format json \
> evaluation.json
- name: Upload findings
if: failure()
uses: actions/upload-artifact@v4
with:
name: stave-findings
path: evaluation.json
GitHub Actions (Build from Source)
If you need to build from source, cache the binary to avoid rebuilding on every run:
name: Stave Safety Check
on:
pull_request:
paths:
- 'terraform/**'
- 'observations/**'
jobs:
stave-apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Cache Stave binary
id: cache-stave
uses: actions/cache@v4
with:
path: /usr/local/bin/stave
key: stave-${{ hashFiles('.stave-version') }}
- name: Build Stave
if: steps.cache-stave.outputs.cache-hit != 'true'
run: |
git clone https://github.com/sufield/stave.git /tmp/stave
cd /tmp/stave && make build
cp /tmp/stave/stave /usr/local/bin/
- name: Validate inputs
run: |
stave validate \
--controls ./controls \
--observations ./observations \
--strict
- name: Evaluate safety
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format json \
> evaluation.json
- name: Upload findings
if: failure()
uses: actions/upload-artifact@v4
with:
name: stave-findings
path: evaluation.json
Exit Code Handling
Stave exits with code 3 when violations are found, which GitHub Actions treats as a failure. Use this to gate merges:
- name: Check for violations
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--quiet
# Exit 0 = safe, exit 3 = violations (fails the step)
To capture violations without failing the build:
- name: Evaluate (non-blocking)
id: evaluate
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
> evaluation.json || rc=$?
echo "violations=$(jq '.summary.violations' evaluation.json)" >> "$GITHUB_OUTPUT"
exit 0 # Don't fail the step
- name: Comment on PR
if: steps.evaluate.outputs.violations != '0'
run: |
echo "Stave found ${{ steps.evaluate.outputs.violations }} violations"
GitLab CI
Build Stave from source in the CI job:
stave-apply:
stage: test
image: golang:1.26.3
before_script:
- git clone https://github.com/sufield/stave.git /tmp/stave
- cd /tmp/stave && make build
- cp /tmp/stave/stave /usr/local/bin/
script:
- stave validate --controls ./controls --observations ./observations --strict
- stave apply --controls ./controls --observations ./observations --max-unsafe 7d
artifacts:
when: on_failure
paths:
- evaluation.json
rules:
- changes:
- terraform/**/*
- observations/**/*
Deterministic Evaluation
For reproducible CI runs, use --now with a fixed timestamp. This ensures the same inputs always produce the same output:
- name: Deterministic evaluation
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--now 2026-01-15T00:00:00Z \
> evaluation.json
diff evaluation.json expected/evaluation.json
Using --strict Validation
In CI, use --strict with validate to treat warnings as errors. This catches issues like single snapshots (insufficient for duration tracking) or unsorted timestamps:
stave validate --controls ./controls --observations ./observations --strict
Quiet Mode for Scripts
Use --quiet to suppress all output and rely only on exit codes:
if stave apply --quiet --controls ./controls --observations ./observations --max-unsafe 7d; then
echo "All resources safe"
else
echo "Violations found (exit $?)"
exit 1
fi
SARIF Output for GitHub Code Scanning
Stave produces SARIF v2.1.0 output that GitHub Actions automatically renders in the "Security" tab and highlights violations in PR diffs:
- name: Evaluate with SARIF
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format sarif \
> results.sarif || true
- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
This surfaces HIPAA violations directly in the "Files Changed" tab of pull requests — no extra tooling needed.
Baseline Workflow (Track Violations Over Time)
Use stave enforce baseline to track violations across commits and
fail CI only when violations increase:
- name: Save baseline (on main)
if: github.ref == 'refs/heads/main'
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format json > evaluation.json
stave enforce baseline save --in evaluation.json --out baseline.json
- name: Check against baseline (on PRs)
if: github.event_name == 'pull_request'
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format json > evaluation.json
stave enforce baseline check \
--baseline baseline.json \
--in evaluation.json
# Fails if new violations introduced
Gate Command (Policy-Based Merge Blocking)
The enforce gate command applies a policy to evaluation results and
returns exit code 3 if the policy is violated:
- name: Gate on policy
run: |
stave apply \
--controls ./controls \
--observations ./observations \
--max-unsafe 7d \
--format json > evaluation.json
stave enforce gate \
--in evaluation.json \
--policy any
# --policy any: fail on any violation
# --policy critical: fail only on critical severity
CI Diff (Show Changes in PRs)
Compare the current evaluation against a baseline to show exactly which violations are new and which were resolved:
- name: Diff against baseline
run: |
stave ci diff \
--baseline baseline.json \
--current evaluation.json \
--format json
Output includes introduced (new violations) and resolved (fixed
since baseline).
Environment Variables for CI
Configure stave without flags using STAVE_* environment variables:
env:
STAVE_MAX_UNSAFE: "7d"
STAVE_CI_FAILURE_POLICY: "critical"
STAVE_PROJECT_ROOT: ${{ github.workspace }}
Resolution priority: environment variable > project config > user config > default. This allows per-environment overrides without changing the command invocation.
Security Audit in CI
Run a full security audit and upload the artifact bundle:
- name: Security audit
run: |
stave security-audit \
--format sarif \
--fail-on CRITICAL \
--compliance-framework hipaa
# Exit 1 if HIGH or CRITICAL findings
- name: Upload audit bundle
if: always()
uses: actions/upload-artifact@v4
with:
name: security-audit
path: security-audit-*/
Parsing Results with jq
# Count violations
stave apply --controls ./ctl --observations ./obs --format json | jq '.summary.violations'
# List violated control IDs
stave apply --controls ./ctl --observations ./obs --format json | jq -r '.findings[].control_id' | sort -u
# Extract resource IDs with violations
stave apply --controls ./ctl --observations ./obs --format json | jq -r '.findings[].resource_id'
# Check safety status
stave apply --controls ./ctl --observations ./obs --format json | jq -r '.safety_status'