Skip to content

CI Quality Gate Tutorial

This walkthrough sets up a pull-request check that fails when code quality regresses and posts a markdown diff summary to the PR. It complements the short pointers in README.md and the full reference in docs/ci-integration.md (badges, quality gates, GitHub Action, diff regression).

unilyze runs at SyntaxOnly in CI (no Unity install required). Code Health and Maintainability Index are stable at this level; smell counts are level-dependent — see docs/metrics.md.

Prerequisites

dotnet tool install --global Unilyze

When developing unilyze from source, prefix commands with:

dotnet run --project src/Unilyze -f net10.0 --

Exit codes

All unilyze commands share one contract:

Code Meaning
0 Success / gate passed
1 Usage error (unknown option, missing file, invalid argument)
2 Quality gate failed (badge with --fail-under / --fail-over, or diff with --fail-on-regression)

CI should treat exit 2 as a failed check, not a crash.

Step 1: Capture an after snapshot

On every PR, analyze the checked-out tree and write JSON:

unilyze -p . -f json -o after.json

Example output (stderr):

Analysis level: SyntaxOnly
Written to after.json

The resolved analysisLevel is also stored in the JSON ("analysisLevel": "SyntaxOnly").

Step 2: Absolute threshold gate (badge)

Block merges when minimum Code Health drops below a floor. This gate is independent of a baseline file — useful as a repo-wide policy line.

unilyze badge -p . --metric codehealth --fail-under 7
echo $?   # 0 = pass, 2 = fail

On pass, stdout is shields.io endpoint JSON (unchanged by gate flags):

{"schemaVersion":1,"label":"code health","message":"9.7 / 8.0","color":"brightgreen","analysisLevel":"SyntaxOnly"}

On fail, stderr explains the reason and exit code is 2:

gate failed: min CodeHealth 8 < 10

Other gate combinations:

unilyze badge -p . --metric smells --fail-over 5        # warning count (any critical always fails)

Compatibility note: unilyze badge -p . --metric mi --fail-under 70 remains available for existing workflows, but MI is a reference metric. Use the CodeHealth gate for new workflows.

Thresholds are inclusive at the boundary: a value exactly equal to --fail-under passes; strictly below fails.

Step 3: Regression gate (diff)

Compare the PR against a baseline and fail when project-wide aggregates worsen: average or min Code Health dropped, or warning/critical smell counts increased.

Option A: Cached baseline JSON (fastest)

Store a baseline snapshot on a branch or in CI artifacts (this repo dogfoods a badges branch — see .github/workflows/badges.yml):

# Assume baseline.json was produced earlier with the same -p path and filters
unilyze diff baseline.json after.json --fail-on-regression
echo $?   # 0 = no regression, 2 = regression detected

Identical snapshots always pass:

unilyze diff after.json after.json -f markdown --fail-on-regression
# stderr: Diff summary; stdout: markdown tables; exit 0

Option B: Git ref baseline (--base-ref)

No hand-built before.json required. unilyze materializes the git ref in a temporary worktree, analyzes it, and diffs against your after snapshot:

git fetch origin main   # or checkout with fetch-depth: 0
unilyze -p . -f json -o after.json
unilyze diff --base-ref origin/main after.json --fail-on-regression

Re-analysis of the base ref roughly doubles CI time; prefer a cached baseline when you control storage.

Use -p to override the project path (default: projectPath from the after snapshot). Pin the base-side level with --level when needed.

Step 4: Post markdown to the PR

diff -f markdown emits GitHub-flavored tables for $GITHUB_STEP_SUMMARY and PR comments. Gate flags do not change the markdown body — only the exit code.

unilyze diff baseline.json after.json -f markdown >> "$GITHUB_STEP_SUMMARY"
unilyze diff baseline.json after.json -f markdown | gh pr comment "$PR_NUMBER" --body-file -

Example markdown excerpt:

**Verdict:** PASS

### Code Health

| Metric | Before | After | Delta |
| --- | --- | --- | --- |
| Avg CH | 9.7 | 9.7 | 0 |
| Min CH | 8 | 8 | 0 |

With --fail-on-regression, a failing gate still prints the full markdown to stdout; CI marks the step failed via exit 2.

Step 5: Wire into GitHub Actions

Do not duplicate the full workflow YAML here. Canonical sources:

A minimal PR gate job runs these shell steps (adapt paths to your project):

dotnet tool install --global Unilyze
unilyze -p . -f json -o after.json
unilyze badge -p . --metric codehealth --fail-under 7
unilyze diff --base-ref origin/main after.json -f markdown --fail-on-regression >> "$GITHUB_STEP_SUMMARY"

For private repositories, generate SVG badges with unilyze badge --format svg and commit them under .github/badges/ instead of using shields.io endpoint URLs — see docs/ci-integration.md#private-repositories.

Aggregate vs per-type regression

--fail-on-regression evaluates project-wide aggregates. A single type can show Degraded: 1 in the diff summary while aggregates stay flat (another type improved enough to offset it), yielding exit 0. To gate on any individual type degrading, inspect the per-type Degraded count in the summary instead.

Official GitHub Action

Use bigdra50/unilyze@v1 for a composite step that bundles install, snapshot, gates, and optional PR comments. Manual shell steps above remain valid — exit-code contract and gate semantics are identical. Full input table: docs/ci-integration.md.

Quick local smoke test

Dogfood against this repository (verified commands):

mkdir -p /tmp/unilyze-gate-test
unilyze -p . -f json -o /tmp/unilyze-gate-test/after.json
unilyze badge -p . --metric codehealth --fail-under 7
unilyze diff /tmp/unilyze-gate-test/after.json /tmp/unilyze-gate-test/after.json \
  -f markdown --fail-on-regression

All three commands should exit 0 on a healthy tree.