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}
]
}
Individual commands (check links, check orphans, etc.)¶
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.
uvx (zero-setup)
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:
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
# --ci automatically applies --strict, --no-header, and --format github-annotations
# for inline PR feedback
run: uvx zenzic check all --ci
- name: Check references and credentials
run: uvx zenzic check references
astral-sh/setup-uv (pinned version)
Use astral-sh/setup-uv when you need a pinned Zenzic version,
faster installs on repeated runs (cached wheel), or when your project
already uses uv for dependency management:
name: Documentation Quality
on:
push:
branches: [main]
paths: ['docs/**', 'mkdocs.yml']
jobs:
zenzic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup uv
uses: astral-sh/setup-uv@v8
with:
enable-cache: true
- name: Lint documentation
run: uvx zenzic check all --ci
- name: Check references and credentials
run: uvx zenzic check references
The enable-cache: true option reuses uv tool cache data across runs,
reducing repeated dependency downloads.
Exit code 2 means a credential was detected in a reference URL. Exit code 3 means a link resolves to an OS system path (path traversal guard). Both require immediate investigation — rotate any exposed credential and remove the offending link.
zenzic-action (recommended)
The official PythonWoods/zenzic-action composite action
installs uv, runs Zenzic, validates SARIF integrity, and uploads findings to GitHub Code Scanning — all
in one step. Findings are published to Security → Code Scanning and can surface as PR annotations when
GitHub Code Scanning annotations are enabled for the repository:
name: Documentation Quality
on:
push:
branches: [main]
paths: ['docs/**', 'mkdocs.yml']
pull_request:
branches: [main]
jobs:
zenzic:
name: Documentation Quality Gate
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # required for SARIF upload
steps:
- uses: actions/checkout@v6
- name: Run Zenzic
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.13.1" # pin to a stable release
format: sarif # emit SARIF for Code Scanning
upload-sarif: "true"
fail-on-error: "true"
Security incidents (exit 2 and 3) are never suppressed by fail-on-error: "false" — the
Exit Code Contract is enforced by the action itself.
Action inputs reference¶
| Input | Default | Description |
|---|---|---|
version |
action pin | Zenzic version to install. Uses the pinned default declared in the action manifest; any explicit value uses uvx "zenzic==X.Y.Z". Pin in production. |
ci |
false |
Run with --ci to enable --strict and inline GitHub PR annotations. Note: Not recommended when using format: sarif as annotations are handled by Code Scanning. |
only |
"" |
Comma-separated list of Z-Codes (e.g. "Z104,Z402") to restrict checks to specific issues. |
format |
sarif |
Output format: text, json, github-annotations, or sarif. Use sarif for GitHub Code Scanning. |
sarif-file |
zenzic-results.sarif |
Path for the generated SARIF file (only when format: sarif). |
upload-sarif |
true |
Upload SARIF to GitHub Code Scanning via github/codeql-action/upload-sarif delegated by the wrapper. |
strict |
false |
Treat warnings as errors (passes --strict to Zenzic). |
fail-on-error |
true |
Fail the workflow step on quality findings (exit 1). Does not affect exit 2 or 3. |
config-file |
"" |
Optional path to a .zenzic.toml file inside the workspace. |
audit |
false |
Run sovereign audit mode to bypass inline and file-level suppressions. |
diff-base |
"" |
Use a JSON baseline file for zenzic diff quality-gate comparisons. |
guard-scan |
false |
Run zenzic guard scan before the main gate as a Defense-in-Depth check. |
check-stamp |
true |
Run zenzic score --check-stamp after the audit and fail on stale badge stamps. |
Action outputs¶
| Output | Description |
|---|---|
sarif-file |
Generated SARIF file path in the workspace (set when format: sarif) |
findings-count |
Total number of findings reported in the SARIF run |
score |
Documentation Quality Score (0–100). Empty when format is not json or sarif. |
suppression-debt-pts |
Debt points deducted from score due to active suppressions. |
cap-exceeded |
true when suppression CAP is exceeded and blocks the build. |
Reading results in GitHub¶
After the workflow runs, Zenzic findings appear in three places:
- Security → Code Scanning — each finding listed with file, line, severity, and
Zxxxcode. - Pull Request → Files changed — inline annotations on the exact line where the issue was detected.
- Checks tab — the step name appears as failed if
fail-on-error: "true"and exit 1 or higher.
The findings-count output can be consumed by downstream steps to drive custom badge updates or
Slack notifications without re-parsing the SARIF file:
- name: Run Zenzic
id: zenzic
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.13.1"
- name: Post finding count
run: echo "Zenzic found ${{ steps.zenzic.outputs.findings-count }} issues"
Progressive Adoption¶
For large legacy repositories, fixing hundreds of warnings at once is impossible. The only parameter allows teams to adopt Zenzic progressively by enforcing only critical checks (e.g., Broken Links and Credential Leaks) while ignoring structural debt until the team is ready.
By setting ci: "true", the action natively injects the --ci flag under the hood, enabling inline PR annotations for the selected violations without requiring GitHub Code Scanning (SARIF) integration.
- name: Zenzic Progressive Gate
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.13.1"
ci: "true" # Native inline PR annotations (no SARIF required)
only: "Z101,Z201" # Gate ONLY fails on broken links and leaked secrets
fail-on-error: "true"
Security incidents are always fatal
fail-on-error: "false" suppresses exit 1 (quality findings) only. Exit 2 (credential scanner — credential
detected) and exit 3 (path traversal guard — path traversal) are never suppressible by any input.
The action enforces this unconditionally. See the
Exit Code Contract.
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¶
- On
main: Zenzic runs, saves the score as.zenzic-score.json, and uploads it as a CI artifact. - On every PR: The artifact from
mainis downloaded as the baseline. Zenzic runs on the PR branch and callszenzic diff --base <baseline>to compare. - Verdict: The workflow reads regression signals from
zenzic diffand applies the repository's chosen merge policy.
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.13.1"
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.13.1"
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.
GitHub Branch Protection: Required Checks¶
Protect main and enable Require status checks to pass before merging.
Operational Profile: zenzic-doc¶
Required checks:
- Build
- Audit
- Lint PR Title
- Check DCO
Operational rule:
- The Build check must run on every pull request.
- Do not use paths filters on the pull_request trigger in .github/workflows/ci.yml for zenzic-doc.
- Keep paths filters on push to main if you want to optimize post-merge CI minutes.
Rationale:
- Build is the structural integrity gate for MDX and site compilation.
- If Build is required but skipped on PR, merge can be blocked in expected/pending state.
- If Build is not required, a fatal docs regression can merge and break the live site.
Operational Profile: zenzic (core)¶
Recommended required checks:
- Audit (ubuntu-latest, 3.10)
- Audit (ubuntu-latest, 3.14)
- Lint PR Title
- Check DCO
Why these checks:
- Audit enforces tests, quality gate, and badge freshness in CI.
- Lint PR Title enforces the PR title convention.
- Check DCO enforces Signed-off-by for every commit.
Important: every required check must run on pull_request. If a required workflow is skipped (for example due to path filters), the PR can remain blocked in expected/pending state.
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.
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.13.1"
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:
- name: Run Zenzic Documentation Quality Gate
uses: PythonWoods/zenzic-action@<version>
with:
version: "0.13.1"
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:
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:
- 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-stamp — never --stamp. CI is an
immutable validator, not a file editor:
- 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:
Regression Detection¶
zenzic diff compares the current score against the saved .zenzic-score.json baseline and fails if the score dropped:
- 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¶
| Code | Meaning | Badge action |
|---|---|---|
0 |
All checks passed | Keep badge green |
1 |
One or more checks failed | Set badge to failing / ef4444 |
2 |
Credential scanner: credential detected | Rotate credential immediately |
3 |
Path traversal guard: path traversal detected | Remove 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¶
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:
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:
| Condition | Strategy | Command used |
|---|---|---|
ZENZIC_CORE_PATH set, or ../zenzic exists |
Core Maintainer — uses local source tree | uv run --project <path> python scripts/verify_codes_parity.py |
| Local core not found | Fail-Closed — no fallback allowed | Session 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.