Exit codes & PR annotations
This page is the contract between manni meta and your pipeline. It covers what each exit code means, what each output format looks like, and when color is emitted. If you are wiring manni meta into a gate or parsing its output, this is the reference to build against.
Exit codes
Section titled “Exit codes”manni meta validate returns one of three exit codes. Every CI platform treats a
non-zero exit as a failed step, so the gate is enforced without extra scripting.
| Code | Meaning | When it happens |
|---|---|---|
0 |
Success | Every validated file satisfies its schema set. |
1 |
Validation failures | One or more files failed validation. The run completed; the documents are non-conformant. |
2 |
Operational or usage error | manni meta could not complete the run. That covers an unknown flag, an unknown subcommand, or a missing required argument. It also covers no inputs and no config, no files matched, an unknown --format, an unreadable config file, and an unexpected internal error. |
The distinction between 1 and 2 matters in a pipeline. Exit 1 is a
result: manni meta ran and found problems your authors must fix. Exit 2 is a
failure of the run itself: a misconfiguration or environment problem you must
fix. A gate that treats every non-zero exit the same will still block bad
metadata. Separating the two lets you tell “the docs are wrong” from “the check
is broken.”
A mistyped flag or subcommand belongs to the second group. manni meta validate --nope docs/ exits 2, not 1: nothing was validated, so the run produced no
verdict about your documents at all.
Output formats
Section titled “Output formats”manni meta validate supports five output formats, selected with -f / --format.
The default is pretty.
| Format | Audience | Machine-readable | Available on |
|---|---|---|---|
pretty |
Humans, local runs | No | validate, get, fill, schemas |
json |
Dashboards, scripts, tooling | Yes | validate, get, fill, schemas |
github |
GitHub Actions PR annotations | Yes (one line per finding) | validate, fill |
sarif |
Code scanning, security dashboards | Yes (SARIF 2.1.0) | validate only |
junit |
The CI “Tests” tab | Yes (JUnit XML) | validate only |
pretty
Section titled “pretty”The default. A check mark or cross per file, indented error lines beneath each failing file, and a summary line. Intended for humans reading a terminal or a CI log.
$ manni meta validate bad-timestamp.md✗ bad-timestamp.md /timestamp must match format "date-time" (line 4) [google:okf:0.1]
1 file checked, 0 passed, 1 failed, 1 errorPassing files show as ✓ <file>. Add -q / --quiet to hide passing files and
show only failures plus the summary. The summary line reports
<n> files checked, <n> passed, <n> failed, <n> errors.
A single JSON object printed to stdout, suitable for dashboards or further
processing. The top level has a summary and a results array.
$ manni meta validate bad-timestamp.md --format json{ "summary": { "files": 1, "passed": 0, "failed": 1, "errors": 1 }, "results": [ { "file": "bad-timestamp.md", "format": "markdown", "ok": false, "schemas": ["google:okf:0.1"], "errors": [ { "schema": "google:okf:0.1", "instancePath": "/timestamp", "message": "must match format \"date-time\"", "keyword": "format", "subject": "date-time", "line": 4 } ] } ]}The shape is stable:
summary: counts across the whole run.files: number of files checked.passed: files that satisfied every schema in their set.failed: files with at least one validation error.errors: total individual validation errors across all files.
results: one object per file.file: the file path as manni meta saw it.format: the extractor that read the file (for examplemarkdown).ok:truewhen the file passed every schema in its set.schemas: the schema ids the file was validated against.errors: the individual violations for this file (empty whenok).
- Each error describes one schema violation.
schema: the schema id that produced the error.instancePath: a JSON Pointer to the offending field (""for the document root,"/tags/0"for the first item oftags).message: the human-readable reason. This is generated prose, and an Ajv release may reword it, so match onkeywordinstead when you need stability.keyword: the JSON Schema keyword that failed (required,format,pattern,type, …). manni meta’s own errors useparsewhen a metadata block could not be read andschemawhen a schema set could not be resolved; both keepschema: "(parse)".subject: the identifier within that keyword, which is the missing property, the additional property, the format name, or the expected type. Omitted for keywords whose parameter is a schema-authored value, such aspattern’s regex orminLength’s number.line: 1-based source line, when manni meta can locate it. Omitted when unknown. For example, errors at the document root often have no specific line.col: 1-based column, when the format can supply one. Thehtmlandxmlextractors do. An HTML error points at thecontent=attribute of the<meta>tag, and an XML one at the offending attribute’s value. Frontmatter formats give a line only. It is also omitted for arequiredviolation, which reports the parent object rather than the property that is missing. There is no column for something absent. Treat it as optional.
github
Section titled “github”One GitHub Actions workflow command per error. GitHub renders each line as an annotation pinned to the file and line in the pull request diff. There is no summary line and no per-file heading, only error lines, one per violation. A passing run prints nothing.
The format of each line is:
::error file=<file>,line=<line>,col=<col>::[<schema>] <field> <message>file=is always present.line=is included only when the error has a known line.col=is included only when the error has a known column, which today means anhtmlorxmldocument and a violation other thanrequiredoradditionalProperties. Those two name a property in the message but point at its parent, so a caret would land on the wrong token. The line is still reported.<schema>is the schema id that produced the error.<field>is the JSON Pointer to the offending field, or(root)when the error is at the document root.<message>is the human-readable reason.
A concrete line, from a malformed timestamp field that fails the date-time
format:
::error file=bad-timestamp.md,line=4::[google:okf:0.1] /timestamp must match format "date-time"The same violation in an HTML document also carries a column, pointing at the
content= attribute of the <meta> tag that holds the bad value:
::error file=bad-timestamp.html,line=6,col=28::[google:okf:0.1] /timestamp must match format "date-time"When the line is unknown, line= is dropped and only file= remains. A
required-property error at the document root reports the field as (root), and
carries no column even in a format that can supply one:
::error file=missing-type.md::[google:okf:0.1] (root) must have required property 'type'<message> is escaped as the workflow-command protocol requires: % becomes
%25, a carriage return %0D, and a newline %0A. GitHub decodes them when it
renders the annotation. Without it, a schema pattern containing % would
corrupt the annotation, because Ajv quotes the regex into the message verbatim.
Without it a multi-line message would truncate at the first newline. Do not decode these
yourself if you post-process the lines; treat the message as
percent-escaped-on-those-three.
github annotations from fill
Section titled “github annotations from fill”fill emits the same workflow command, for the
work it could not do. That is one ::error per property the schema lists as
required that was not filled confidently. It is exactly the set that makes
fill exit 1. Skipped optional properties are a normal outcome and stay
silent, matching the exit-code rule.
::error file=docs/api/legacy.md::[fill] /description is required and was not filled (confidence 0.42 is below the 0.7 threshold)Two differences from validate’s annotations:
- No
line=. Avalidatefinding is a violation in the document, so it has a position. Afillreport is about a property that is missing from the document, so there is nothing to point at. GitHub anchors a file-only annotation to line 1. [fill]in place of the schema id, because the annotation is about the fill run rather than about one schema’s verdict.
The message is escaped exactly as above, with % → %25, CR → %0D, and LF →
%0A.
A SARIF 2.1.0 log on stdout, which GitHub code scanning, GitLab, and Azure DevOps ingest directly. Findings become tracked alerts with state across commits.
$ manni meta validate "docs/**/*.md" --format sarif > manni.sarifThree things matter when wiring it up:
validateexits 1 when there are findings, and the findings are the point. Withoutcontinue-on-error: trueon the manni meta step andif: always()on the upload, the job stops before anything is uploaded and the dashboard stays permanently empty. See theupload-sarifrecipe.- A clean run still writes a full envelope, so redirecting to a file always
produces a valid one.
upload-sariffails on an empty file. - Paths are rebased onto the repository root, because that is what GitHub
resolves
artifactLocation.uriagainst. GitHub silently drops results that do not resolve, so a wrong path uploads successfully with zero alerts.
Every finding manni meta validate produces is reported at level: "error".
A finding’s severity maps onto SARIF’s triage axis directly. So a sibling
tool’s advisory findings arrive as level: "warning" through the same
reporter, which is what closed
issue #78. The full field
reference is on
Output formats & exit codes.
JUnit XML on stdout, which Jenkins, GitLab, CircleCI, and Azure render in their “Tests” tab.
$ manni meta validate "docs/**/*.md" --format junit > manni-junit.xmlOne <testcase> per file and one <failure> per violation, so the tab
reads “2 tests, 1 failed” and matches the 2 files checked, 1 failed summary.
GitHub Actions has no native JUnit renderer; on GitHub prefer github for
annotations or sarif for tracked alerts, and keep junit for the platforms
that render it.
Color and TTY behavior
Section titled “Color and TTY behavior”manni meta follows clig.dev conventions. Primary output goes
to stdout, diagnostics go to stderr, and color is emitted only when it is safe
to do so. Color affects only the pretty format; json, github, sarif, and
junit are always plain text so they parse reliably. Those four also keep the
“Using manni.config.yaml” notice on stderr, so a redirected report stays
parseable.
manni meta decides whether to emit ANSI color like this, in order:
--no-color: passing this flag disables color unconditionally.NO_COLOR: if theNO_COLORenvironment variable is set to any non-empty value, color is disabled.- TTY detection: otherwise, color is emitted only when stdout is an interactive terminal (a TTY).
In practice this means color turns off automatically in CI, because a piped or
redirected stream is not a TTY. You rarely need --no-color in a pipeline, but
it is available when you want to force plain output. One example is capturing a
log that a downstream tool will read.