In your pipeline

Fail the build, not the customer

Check generated invoices in the pull request, before a customer or access point rejects them.

The whole integration

one step in your workflow
- uses: attestwire/validate-einvoice-action@v1
  with:
    files: "invoices/**/*.xml"
    sarif: einvoice.sarif

Fatal findings fail the job. The action annotates the pull request, writes a linked findings table, and can export SARIF.

the upload step
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: einvoice.sarif

It runs offline, in the runner

No API key, account or network call. The action bundles a pinned engine version, so the same version runs the same rules.

Upgrade the rules by changing the action version in a pull request. You can review any changed findings before enforcing them.

Add api-key to use the hosted ruleset instead. Each response names the engine and ruleset used for that document.

Two pure functions, if you are not on GitHub

The action is a wrapper. Underneath it, the library exports the same two reports directly:

SARIF and JUnit, from the package
import { validateInput, toSarif, toJunitXml } from "@attestwire/en16931";

const { errors } = validateInput(invoice);
const provenance = { engineVersion: "0.7.3", profile: "xrechnung-ubl" };

writeFileSync("einvoice.sarif", JSON.stringify(toSarif(errors, provenance)));
writeFileSync("einvoice.xml", toJunitXml(errors, provenance));

Jenkins, GitLab, CircleCI, Buildkite and Azure Pipelines all ingest the JUnit XML that Ant defined and Surefire extended; GitHub reads the SARIF.

Both are pure functions. No clock, no filesystem, no environment: every value that varies between runs — the engine version, the ruleset versions, the timestamp, the document URI — is passed in. The same inputs give byte-identical output, so two reports diff to nothing rather than to a changed timestamp on every line. A report that cannot name the validator version that produced it cannot be checked later, which is why engineVersion is the one required field.

Neither function decides whether your build fails. SARIF has no "fail the build" flag; the gate is your repository's, configured where gates belong. toJunitXml maps a fatal finding to <failure>, and the mapping is documented rather than left to be inferred.

Exit codes

ExitWhen
0 Every document validated, and nothing crossed the fail-on threshold.
1 A document produced a finding at or above fail-on.
1 A file could not be read, parsed, or is not an invoice.
1 The files pattern matched nothing.
1 An input is invalid, or (api mode) the key was rejected or the API unreachable.

A run over zero files fails. An unreadable file also fails instead of being skipped. Invalid settings are reported before files are opened.

Informational findings never fail a build under any setting. A warning fails only under fail-on: warning, because a warning here is something the official validator raises and then accepts the document anyway, and turning a build red for one by default would misrepresent the authority.

Use it as a preflight

A release pipeline that ships documents into a regulated flow may still want the official validator in it.

What this action runs is a semantic pre-flight over the parsed input model. It is not a schematron over the document, and rules that constrain the XML itself — BR-01, BR-DE-13, BR-DE-21 on BT-24 — do not run at all. A document we accept can still be rejected by the reference validator, and the limits page lists every gap rather than a representative sample.

A practical setup is to run this action on pull requests and keep the official validator in the release job. One gives developers a fast, actionable result; the other remains the external authority.

What we can show you is our own homework against those validators, failures included: the KoSIT record, the Peppol record, the DGFiP record.

Next.

The action's README, every input and output → · The library, if you would rather call the functions yourself → · The other integrations → · What we don't do →