Skip to main content

CI/CD Integration

Zenzic is automation-ready out of the box. The --format json flag and --save option expose machine-readable output that any CI/CD system can consume to drive dynamic badges, quality gates, and regression detection.


JSON Output

Every check command supports --format json:

# Aggregated report for all checks
zenzic check all --format json

# Individual checks
zenzic check links --format json
zenzic check references --format json

# Scoring and regression
zenzic score --format json
zenzic diff --format json

zenzic check all --format json

{
"links": ["guides/setup.md:12 — Link target 'install.md' not found"],
"orphans": ["old-page.md"],
"snippets": [{"file": "api/ref.md", "line": 5, "message": "Snippet target not found"}],
"placeholders": [{"file": "index.md", "line": 1, "issue": "TODO", "detail": "Fix this"}],
"unused_assets": ["images/old-logo.png"],
"references": [],
"nav_contract": []
}

zenzic score --format json

{
"project": "zenzic",
"score": 100,
"threshold": 0,
"status": "success",
"timestamp": "2026-03-24T12:00:00+00:00",
"categories": [
{"name": "structural", "weight": 0.30, "issues": 0, "category_score": 30.0, "contribution": 30.0},
{"name": "content", "weight": 0.20, "issues": 0, "category_score": 20.0, "contribution": 20.0},
{"name": "navigation", "weight": 0.25, "issues": 0, "category_score": 25.0, "contribution": 25.0},
{"name": "brand", "weight": 0.25, "issues": 0, "category_score": 25.0, "contribution": 25.0}
]
}

Each individual check command returns a uniform findings structure:

{
"findings": [
{"rel_path": "guides/setup.md", "line_no": 42, "code": "Z104", "severity": "error", "message": "guides/setup.md:42: 'install.md' not found in docs"}
],
"summary": {
"errors": 1, "warnings": 0, "info": 0,
"security_incidents": 0, "security_breaches": 0,
"elapsed_seconds": 0.042
}
}

Exit codes are preserved in JSON mode: exit 0 when only warnings are found, exit 1 on errors (or warnings under --strict), exit 2 on credential scanner findings, exit 3 on path traversal guard — the same contract as terminal output.


GitHub Actions: Zenzic Credential Gate

The simplest integration — fails the build on any documentation error.

No Python setup required. uvx fetches and runs Zenzic in a throwaway environment on every run. Ideal for documentation-only repositories or teams that do not otherwise need a Python environment in their CI:

.github/workflows/zenzic.yml
name: Documentation Quality

on:
push:
branches: [main]
paths: ['docs/**', 'mkdocs.yml']

jobs:
zenzic:
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v6

- name: Lint documentation

run: uvx zenzic check all --strict

- name: Check references and credentials

run: uvx zenzic check references

Zenzic Quality Gate — The Diff Protocol

The Zenzic Quality Gate uses zenzic diff to compare the current score against a saved baseline. Teams can wire the resulting verdict as a blocking or observational gate in workflow policy.

How it works

  1. On main: Zenzic runs, saves the score as .zenzic-score.json, and uploads it as a CI artifact.
  2. On every PR: The artifact from main is downloaded as the baseline. Zenzic runs on the PR branch and calls zenzic diff --base <baseline> to compare.
  3. Verdict: The workflow reads regression signals from zenzic diff and applies the repository's chosen merge policy.
.github/workflows/zenzic-quality-gate.yml
name: Zenzic Quality Gate

on:
push:
branches: [main]
pull_request:

jobs:
# On main: save the authoritative baseline
baseline:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6

- name: Run Zenzic and save baseline
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.9.2"
format: json # triggers .zenzic-score.json snapshot
upload-sarif: "false"

- name: Upload baseline artifact
uses: actions/upload-artifact@v4
with:
name: zenzic-baseline
path: .zenzic-score.json
retention-days: 90

# On PRs: compare against main baseline
quality-gate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6

- name: Download main baseline
uses: actions/download-artifact@v4
with:
name: zenzic-baseline
path: .zenzic-baseline/
continue-on-error: true # first PR on a new repo has no baseline yet

- name: Zenzic — Quality Gate
uses: PythonWoods/zenzic-action@<version>
id: zenzic
with:
version: "0.9.2"
format: sarif
upload-sarif: "true"
diff-base: ".zenzic-baseline/.zenzic-score.json"

- name: Report quality score
if: always()
run: |
echo "Score: ${{ steps.zenzic.outputs.score }}"
echo "Suppression debt: ${{ steps.zenzic.outputs.suppression-debt-pts }} pts"
echo "Findings: ${{ steps.zenzic.outputs.findings-count }}"
Quality Regression via zenzic diff

When a score regression is detected, the workflow receives a non-zero diff verdict and can enforce either blocking or observational behavior, depending on policy.


Audit Mode in CI — Sovereign Audit

The audit: "true" input forces a sovereign audit: all active zenzic:ignore inline comments and all governance.per_file_ignores entries are bypassed. Every finding that would normally be hidden by a suppression is surfaced.

Use audit mode in:

  • Nightly builds — weekly sanity check that suppressed debt remains intentional.
  • Security Review workflows — before a release, verify the unfiltered documentation state.
  • Debt reduction sprints — see the full scope of what is being suppressed before raising or lowering suppression_cap.
.github/workflows/zenzic-audit.yml
name: Zenzic Sovereign Audit

on:
schedule:
- cron: "0 3 * * 1" # every Monday at 03:00 UTC
workflow_dispatch:

jobs:
audit:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6

- name: Sovereign Audit (suppressions bypassed)
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.9.2"
format: sarif
upload-sarif: "true"
audit: "true" # bypass all zenzic:ignore and per_file_ignores
fail-on-error: "false" # audit is observational, not blocking

Results appear in the Security → Code Scanning tab. Every suppressed finding is visible alongside active ones. This is the unfiltered truth of your documentation.


Defense-in-Depth: Secret Guard

The guard-scan: "true" input runs zenzic guard scan as a standalone step before the main quality gate. Use this in repositories where contributors may bypass pre-commit hooks with git commit --no-verify:

.github/workflows/zenzic.yml
- name: Run Zenzic Documentation Quality Gate
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.9.2"
guard-scan: "true" # zenzic guard scan runs before check all
format: sarif
upload-sarif: "true"

If guard scan detects a hardcoded credential or forbidden pattern, it exits non-zero and terminates the job. fail-on-error: "false" does not suppress this — the guard scan is always fatal, consistent with the Exit 2 security contract.


Native Badge Freshness Gate

The legacy dynamic badge workflow is deprecated. Use native badge stamping and freshness checks:

zenzic score --stamp
zenzic score --check-stamp

Recommended CI behavior: run zenzic score --check-stamp after zenzic check to enforce freshness of stamped badges without external badge plumbing.

The Badge Freshness Gate

zenzic score --check-stamp compares the badge URL embedded in your README.md (or any file listed in badge_stamp_files) against the score Zenzic computes at that moment. If they differ, the command exits 1 and prints an actionable message:

[FAILED] Badge (score) in README.md is stale.
Run 'zenzic score --stamp' locally and commit the updated files to resolve this.

The gate is read-only — it never modifies files. It only validates what is already committed.

Optional: automate with pre-commit

Zenzic operates zero-config out of the box — the CI gate alone is sufficient. If you use pre-commit integration and want to automate badge stamping so you never have to run --stamp manually, you can optionally add this hook:

.pre-commit-config.yaml
- repo: local
hooks:
- id: zenzic-score-stamp
name: Zenzic Score Badge (stamp)
entry: zenzic score --stamp --no-header
language: system
stages: [pre-commit]
pass_filenames: false
always_run: true

With this hook, the badge is updated automatically on every git commit. If the score changed, pre-commit fails and reports that README.md was modified — stage the file and run git commit again to proceed.

Without pre-commit

Run the stamp manually before pushing:

zenzic score --stamp
git add README.md README.it.md # or whichever files are in badge_stamp_files
git commit --amend --no-edit # or a new commit
git push

If you skip this step and CI finds a stale badge, the workflow fails with the error above. Follow the message instructions, stamp locally, and push again.

CI configuration (read-only gate)

In GitHub Actions, use only --check-stampnever --stamp. CI is an immutable validator, not a file editor:

.github/workflows/zenzic.yml
- name: Check badge freshness
run: uvx zenzic score --check-stamp --no-header

When using zenzic-action, the badge freshness gate is enabled by default — no additional configuration needed:

- name: Run Zenzic
uses: PythonWoods/zenzic-action@v1

Regression Detection

zenzic diff compares the current score against the saved .zenzic-score.json baseline and fails if the score dropped:

.github/workflows/zenzic.yml
- name: Detect score regression
run: |
uvx zenzic score --save # update snapshot
uvx zenzic diff --threshold 5 # fail if score drops > 5 points

For the full Zenzic Quality Gate setup with PR blocking and baseline artifact upload, see the Diff Protocol section above.


Exit Codes Reference

CodeMeaningBadge action
0All checks passedKeep badge green
1One or more checks failedSet badge to failing / ef4444
2Credential scanner: credential detectedRotate credential immediately
3Path traversal guard: path traversal detectedRemove offending link immediately

For the full badge copy-paste reference, see Official Badges.


Credential Recovery — When a Credential Is Detected

A credential detection (exit code 2) is not a failed build. It is a security incident. The recovery playbook is short and non-negotiable:

Step 1 — Identify the exposure

The Zenzic Report tells you everything you need:

✘ Z201 docs/how-to/configure.md:4 Secret detected (aws-access-key)
Credential: AKIA************MPLE
→ Exit code 2 — rotate immediately.

Note the file, the line, and the credential type. The credential is always masked in the report — Zenzic never prints the full value.

Step 2 — Rotate the credential

Before doing anything else — rotate the key in your cloud provider’s console. Do not commit the fix first. A rotated key is inert even if it remains briefly in your git history.

Step 3 — Remove from source

Delete or replace the secret in the file Zenzic flagged. Commit the fix.

Step 4 — Rewrite history if necessary

If the credential appeared in a previous commit that has already been pushed:

# Interactive rebase to the commit that introduced the secret
git rebase -i <commit-before-secret>^

# Or use git-filter-repo (preferred over BFG for new projects)
git filter-repo --path docs/how-to/configure.md --force
Force-push requires coordination

Rewriting published history requires a force-push. Coordinate with your team before doing this on a shared branch. If the repository is public, assume the credential is already compromised regardless of history rewriting — rotation is mandatory.

Step 5 — Verify no credentials are detected

uvx zenzic check all
# Expected: exit 0, no credentials detected

Only exit 0 means the recovery is complete.

Why Zenzic saves you from BFG Repo Cleaner

BFG Repo Cleaner is an irreversible tool for purging secrets from git history. Its use is a symptom of a process failure: the secret reached the repository undetected. Zenzic prevents this by catching credentials before they enter the CI pipeline — ideally via a pre-commit hook. See pre-commit integration to add Zenzic as a local gate that runs on every git commit.


Pre-commit Integration

The credential scanner in CI is your last line of defence. The pre-commit hook is your first:

.pre-commit-config.yaml
repos:

- repo: local

hooks:

- id: zenzic-credentials

name: Zenzic Credentials
language: system
entry: uvx zenzic check all
pass_filenames: false
stages: [pre-commit]

With this hook, git commit will refuse to proceed if Zenzic detects a credential, a broken link, or any exit-1 quality finding. The feedback is instant, the fix is local, and the secret never touches the remote.


Doc-Code Parity

Every Zxxx finding code documented in docs/ must have a registered entry in src/zenzic/core/codes.py in the core package — and vice versa. This bidirectional invariant is enforced by the verify-codes-parity Nox session:

# Run standalone
nox -s verify-codes-parity

# Runs automatically as part of the full local gate
just verify

The session uses Sovereign Resolution (Fail-Closed) to locate codes.py:

ConditionStrategyCommand used
ZENZIC_CORE_PATH set, or ../zenzic existsCore Maintainer — uses local source treeuv run --project <path> python scripts/verify_codes_parity.py
Local core not foundFail-Closed — no fallback allowedSession fails with core path error

External contributors must provide a local core checkout (ZENZIC_CORE_PATH, ./_zenzic_core, or ../zenzic) to run nox -s verify-codes-parity.