Skip to content

Quality Gates

Quality gates help you prevent code quality regressions by automatically checking metrics against thresholds on every pull request. When a metric crosses its threshold, the quality gate fails and notifies you.

Quality gates compare metrics from your pull request against the baseline from your main branch. You define thresholds for each metric, and Unentropy evaluates them automatically:

  1. The track-metrics action runs on main branch pushes, building historical data
  2. The quality-gate action runs on pull requests, comparing PR metrics to main branch baseline
  3. If any threshold is violated, the quality gate fails and posts a comment on the PR

Define thresholds in your unentropy.json:

{
"metrics": {
"coverage": {
"$ref": "coverage",
"command": "@collect coverage-lcov ./coverage/lcov.info"
}
},
"qualityGate": {
"thresholds": [
{
"metric": "coverage",
"mode": "min",
"target": 80
}
]
}
}

This prevents coverage from dropping below 80%.

Create .github/workflows/quality-gate.yml:

name: Quality Gate
on:
pull_request:
jobs:
quality-gate:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Run tests with coverage
run: bun test --coverage
- name: Quality Gate Check
uses: unentropy/quality-gate-action@v1

Note: The pull-requests: write permission is required to post comments on PRs.

Quality gates support different comparison modes:

Metric must not drop below the target:

{
"metric": "coverage",
"mode": "min",
"target": 80
}

Use for: Coverage percentages, test counts

Metric must not exceed the target:

{
"metric": "bundle",
"mode": "max",
"target": 500000
}

Use for: Bundle sizes, build times, dependency counts

Metric must not decrease from baseline:

{
"metric": "coverage",
"mode": "no-regression"
}

Use for: Ensuring metrics don’t regress, regardless of absolute value

Metric can increase, but not by more than a percentage:

{
"metric": "bundle",
"mode": "delta-max-drop",
"target": 5
}

This allows bundle size to increase by up to 5% from baseline.

Use for: Controlled growth metrics (bundle size, build time)

The quality gate itself can operate in different modes:

Evaluates thresholds and posts PR comments, but never fails the build:

{
"qualityGate": {
"mode": "soft",
"thresholds": [...]
}
}

Use this to observe behavior before enforcing rules.

Fails the build when any threshold is violated:

{
"qualityGate": {
"mode": "hard",
"thresholds": [...]
}
}

Use this to block merges when quality standards aren’t met.

Disables quality gate evaluation:

{
"qualityGate": {
"mode": "off"
}
}

Tip: Start with soft mode to tune your thresholds. Switch to hard once they’re stable.

When the quality gate runs, it posts a comment on your PR showing:

  • Current value for each metric
  • Baseline value from main branch
  • Change direction (increase/decrease)
  • Pass/fail status for each threshold

Example comment:

Quality Gate: ✅ Passed
| Metric | Baseline | Current | Change | Status |
|--------|----------|---------|--------|--------|
| Coverage | 87.5% | 88.2% | +0.7% | ✅ Pass |
| Bundle Size | 240 KB | 238 KB | -2 KB | ✅ Pass |
| LOC | 4,521 | 4,580 | +59 | ℹ️ No threshold |

If a threshold fails:

Quality Gate: ❌ Failed
| Metric | Baseline | Current | Change | Status |
|--------|----------|---------|--------|--------|
| Coverage | 87.5% | 79.2% | -8.3% | ❌ Failed (min: 80%) |

Configure thresholds for as many metrics as needed:

{
"qualityGate": {
"mode": "soft",
"thresholds": [
{
"metric": "coverage",
"mode": "min",
"target": 80
},
{
"metric": "bundle",
"mode": "max",
"target": 500000
},
{
"metric": "loc",
"mode": "delta-max-drop",
"target": 10
}
]
}
}

The quality gate passes only if all thresholds pass.

Prevent coverage from dropping:

{
"metrics": {
"coverage": {
"$ref": "coverage",
"command": "@collect coverage-lcov ./coverage/lcov.info"
}
},
"qualityGate": {
"mode": "hard",
"thresholds": [
{
"metric": "coverage",
"mode": "no-regression"
}
]
}
}

Keep bundle size under control:

{
"metrics": {
"bundle": {
"$ref": "size",
"command": "@collect size ./dist/**/*.js"
}
},
"qualityGate": {
"mode": "hard",
"thresholds": [
{
"metric": "bundle",
"mode": "max",
"target": 500000
},
{
"metric": "bundle",
"mode": "delta-max-drop",
"target": 5
}
]
}
}

This ensures bundle never exceeds 500KB and never increases by more than 5%.

Monitor multiple metrics:

{
"metrics": {
"coverage": {
"$ref": "coverage",
"command": "@collect coverage-lcov ./coverage/lcov.info"
},
"bundle": {
"$ref": "size",
"command": "@collect size ./dist"
},
"loc": {
"$ref": "loc"
}
},
"qualityGate": {
"mode": "soft",
"thresholds": [
{
"metric": "coverage",
"mode": "min",
"target": 80
},
{
"metric": "bundle",
"mode": "delta-max-drop",
"target": 5
}
]
}
}

LOC is tracked but not gated (no threshold defined).

When no baseline data exists for a metric (first PR, new metric), the quality gate handles it gracefully:

  • PR comment shows current value
  • Indicates “No baseline available”
  • Does not fail the quality gate
  • Subsequent PRs will have baseline data

This lets you add new metrics or thresholds without blocking work.

Begin with soft mode to observe metric behavior:

{
"qualityGate": {
"mode": "soft",
"thresholds": [...]
}
}

Review PR comments for several weeks. Adjust thresholds if needed. Switch to hard mode once stable.

When you’re unsure of the right absolute threshold, use no-regression:

{
"metric": "coverage",
"mode": "no-regression"
}

This prevents backsliding without setting arbitrary targets.

For metrics that naturally grow (like LOC), use delta-max-drop:

{
"metric": "loc",
"mode": "delta-max-drop",
"target": 15
}

This flags unusually large changes (more than 15% growth) while allowing normal expansion.

Don’t gate everything. Focus on metrics that truly impact quality:

  • Coverage: Usually worth gating
  • Bundle size: Important for web apps
  • Build time: Helpful for CI efficiency
  • LOC: Often better tracked than gated

Quality Gate Always Passes (When It Shouldn’t)

Section titled “Quality Gate Always Passes (When It Shouldn’t)”

Problem: Threshold violations aren’t detected

Solutions:

  • Verify metric names in thresholds match metric IDs in configuration exactly
  • Check that track-metrics action has run on main branch to create baseline data
  • Ensure storage backend is correctly configured and accessible by both workflows
  • Review threshold mode and target values

Problem: PR comment shows “No baseline available”

Solutions:

  • Run track-metrics action on main branch at least once
  • Verify storage backend is working (check workflow logs)
  • Ensure both workflows use the same storage configuration
  • Wait for main branch workflow to complete before opening PR

Problem: No comment appears on PR

Solutions:

  • Verify pull-requests: write permission in workflow
  • Check workflow logs for GitHub API errors
  • Ensure GITHUB_TOKEN has sufficient permissions
  • Review repository settings for Actions permissions

Problem: Every PR fails the quality gate

Solution: Switch to soft mode temporarily and review several PRs to understand typical metric changes. Adjust thresholds to realistic values based on actual data.