From 5f11341cbc8d0bce4f4ded77681c591d3dd75e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=B6nke?= Date: Thu, 23 Jul 2026 08:42:40 +0000 Subject: [PATCH] Add a GitHub Action and a GitLab CI template Both wrap the released standalone jar so XML documents can be validated as part of a pipeline: download the release (optionally SHA-256-verified), run each file against a validation configuration, fail on any rejection with the failing rule ids printed, and expose the reports plus the accepted/rejected counts. The action self-test validates a conformant XRechnung reference instance and asserts a corrupted one is rejected. --- .github/workflows/action-test.yml | 77 ++++++++++++++ README.md | 18 ++++ action.yml | 160 ++++++++++++++++++++++++++++++ gitlab/validator.gitlab-ci.yml | 82 +++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 .github/workflows/action-test.yml create mode 100644 action.yml create mode 100644 gitlab/validator.gitlab-ci.yml diff --git a/.github/workflows/action-test.yml b/.github/workflows/action-test.yml new file mode 100644 index 0000000..c3acd9f --- /dev/null +++ b/.github/workflows/action-test.yml @@ -0,0 +1,77 @@ +name: Action self-test + +on: + pull_request: + paths: + - action.yml + - gitlab/** + - .github/workflows/action-test.yml + push: + branches: [main, feature/ci-action] + workflow_dispatch: + +permissions: + contents: read + +env: + CONFIGURATION_URL: https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/v2026-01-31/xrechnung-3.0.2-validator-configuration-2026-01-31.zip + CONFIGURATION_SHA256: 6a5a5911a421b25fbc423f62f93f894df7b236f5d73ca4f84bb222a945082704 + VALIDATOR_SHA256: 244978514ad48f67c7573acfffc8f4fd73d81feda6f276710033f9913579857e + # A conformant XRechnung 3.0.2 reference instance from the KoSIT test suite. + SAMPLE_URL: https://raw.githubusercontent.com/itplr-kosit/xrechnung-testsuite/b3791d47a2e1446b1751a27cb732aac294231293/src/test/business-cases/standard/01.01a-INVOICE_uncefact.xml + +jobs: + accepts-a-valid-document: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Fetch a conformant reference instance + run: | + mkdir samples + curl -fsSL "$SAMPLE_URL" -o samples/invoice.xml + + - name: Validate (must accept) + id: validate + uses: ./ + with: + version: "1.6.2" + checksum: ${{ env.VALIDATOR_SHA256 }} + configuration: ${{ env.CONFIGURATION_URL }} + configuration-checksum: ${{ env.CONFIGURATION_SHA256 }} + files: samples/ + + - name: Outputs are reported + run: | + test "${{ steps.validate.outputs.accepted }}" = "1" + test "${{ steps.validate.outputs.rejected }}" = "0" + test -d "${{ steps.validate.outputs.report-dir }}" + + rejects-a-broken-document: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Fetch and break a reference instance + run: | + mkdir samples + curl -fsSL "$SAMPLE_URL" -o samples/invoice.xml + # Corrupt the issue date (BT-2): schema-invalid, must be rejected. + sed -i '0,/format="102">[0-9]*not-a-date-standalone.jar -s -D The [daemon documentation](./docs/daemon.md) shows more usage details and further configuration options. +### Continuous Integration + +The repository ships a GitHub Action and a GitLab CI template so documents can be validated as part of a pipeline. + +Validate a directory of XML documents against the current XRechnung configuration with the GitHub Action: + +```yaml +- uses: itplr-kosit/validator@main + with: + version: "1.6.2" + configuration: "https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/v2026-01-31/xrechnung-3.0.2-validator-configuration-2026-01-31.zip" + files: invoices/ +``` + +The action downloads the released standalone jar, runs every document and fails the job if any document is rejected, printing the failing rule ids. The optional `checksum` and `configuration-checksum` inputs verify the downloads against a SHA-256 pin. The reports are written to `report-dir` (default `validator-reports`), and the `accepted`/`rejected` counts are exposed as outputs. + +On GitLab, include [`gitlab/validator.gitlab-ci.yml`](./gitlab/validator.gitlab-ci.yml) and extend the `.kosit-validate` job template; its variables mirror the action inputs. + ## Packages The Validator distribution contains the following artifacts: diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..8fd81af --- /dev/null +++ b/action.yml @@ -0,0 +1,160 @@ +name: KoSIT Validator +description: >- + Validate XML documents against a validator configuration (e.g. XRechnung) + using the KoSIT validator. Downloads a released validator jar, optionally + verifies checksums, runs each file and fails if any document is rejected. +author: KoSIT contributors +branding: + icon: check-circle + color: green + +inputs: + version: + description: >- + Validator release to use (a tag of this repository without the leading + "v", e.g. "1.6.2"). The standalone jar is downloaded from the GitHub + release of this repository. + required: false + default: "1.6.2" + checksum: + description: >- + Optional SHA-256 checksum of the validator jar. When set, the download + is verified against it and the action fails on a mismatch. + required: false + default: "" + configuration: + description: >- + The validator configuration: either a URL to a configuration zip (for + example a release asset of validator-configuration-xrechnung) or a + path to an already unpacked configuration directory containing + scenarios.xml. + required: true + configuration-checksum: + description: >- + Optional SHA-256 checksum of the configuration zip. Only used when + "configuration" is a URL. + required: false + default: "" + files: + description: >- + XML documents to validate: a file, a directory (validated recursively, + *.xml) or a shell glob. + required: true + report-dir: + description: Directory the validation reports are written to. + required: false + default: validator-reports + +outputs: + report-dir: + description: Directory containing the validation reports. + value: ${{ steps.validate.outputs.report-dir }} + accepted: + description: Number of accepted documents. + value: ${{ steps.validate.outputs.accepted }} + rejected: + description: Number of rejected documents. + value: ${{ steps.validate.outputs.rejected }} + +runs: + using: composite + steps: + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Validate + id: validate + shell: bash + env: + VERSION: ${{ inputs.version }} + CHECKSUM: ${{ inputs.checksum }} + CONFIGURATION: ${{ inputs.configuration }} + CONFIGURATION_CHECKSUM: ${{ inputs.configuration-checksum }} + FILES: ${{ inputs.files }} + REPORT_DIR: ${{ inputs.report-dir }} + run: | + set -euo pipefail + + verify() { # + if command -v sha256sum >/dev/null 2>&1; then + echo "$1 $2" | sha256sum -c - >/dev/null + else + echo "$1 $2" | shasum -a 256 -c - >/dev/null + fi + } + + cache="${RUNNER_TEMP:-/tmp}/kosit-validator" + mkdir -p "$cache" + + jar="$cache/validator-$VERSION-standalone.jar" + if [ ! -f "$jar" ]; then + echo "Downloading validator $VERSION..." + curl -fsSL \ + "https://github.com/itplr-kosit/validator/releases/download/v$VERSION/validator-$VERSION-standalone.jar" \ + -o "$jar" + fi + if [ -n "$CHECKSUM" ]; then + verify "$CHECKSUM" "$jar" + fi + + case "$CONFIGURATION" in + http://*|https://*) + config_dir="$cache/configuration" + if [ ! -f "$config_dir/scenarios.xml" ]; then + echo "Downloading configuration..." + curl -fsSL "$CONFIGURATION" -o "$cache/configuration.zip" + if [ -n "$CONFIGURATION_CHECKSUM" ]; then + verify "$CONFIGURATION_CHECKSUM" "$cache/configuration.zip" + fi + unzip -o -q "$cache/configuration.zip" -d "$config_dir" + fi + ;; + *) + config_dir="$CONFIGURATION" + ;; + esac + if [ ! -f "$config_dir/scenarios.xml" ]; then + echo "No scenarios.xml in configuration: $CONFIGURATION" >&2 + exit 2 + fi + + shopt -s nullglob + files=() + if [ -d "$FILES" ]; then + while IFS= read -r f; do files+=("$f"); done \ + < <(find "$FILES" -name '*.xml' -type f | sort) + else + for f in $FILES; do files+=("$f"); done + fi + if [ "${#files[@]}" -eq 0 ]; then + echo "No XML documents matched: $FILES" >&2 + exit 2 + fi + + mkdir -p "$REPORT_DIR" + rule_ids='\[[A-Z][A-Z0-9]*(-[A-Z0-9]+)+\]' + accepted=0 + rejected=0 + for f in "${files[@]}"; do + if output="$(java -jar "$jar" -s "$config_dir/scenarios.xml" \ + -r "$config_dir" -o "$REPORT_DIR" "$f" 2>&1)"; then + accepted=$((accepted + 1)) + echo "PASS $f" + else + rejected=$((rejected + 1)) + report="$REPORT_DIR/$(basename "${f%.xml}")-report.xml" + ids="$( { printf '%s' "$output"; [ -f "$report" ] && cat "$report"; } \ + | grep -oE "$rule_ids" | sort -u | paste -sd' ' - || true)" + echo "FAIL $f ${ids:-}" + printf '%s\n' "$output" | grep -iE 'rejected|error' || true + fi + done + + echo "report-dir=$REPORT_DIR" >> "$GITHUB_OUTPUT" + echo "accepted=$accepted" >> "$GITHUB_OUTPUT" + echo "rejected=$rejected" >> "$GITHUB_OUTPUT" + echo "Accepted: $accepted Rejected: $rejected" + [ "$rejected" -eq 0 ] diff --git a/gitlab/validator.gitlab-ci.yml b/gitlab/validator.gitlab-ci.yml new file mode 100644 index 0000000..c4994a2 --- /dev/null +++ b/gitlab/validator.gitlab-ci.yml @@ -0,0 +1,82 @@ +# Reusable GitLab CI template for the KoSIT validator. +# +# Include it and extend the hidden job: +# +# include: +# - remote: "https://raw.githubusercontent.com/itplr-kosit/validator//gitlab/validator.gitlab-ci.yml" +# +# validate-invoices: +# extends: .kosit-validate +# variables: +# KOSIT_CONFIGURATION_URL: "https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/v2026-01-31/xrechnung-3.0.2-validator-configuration-2026-01-31.zip" +# KOSIT_FILES: "invoices/" +# +# Variables (mirroring the GitHub Action inputs): +# KOSIT_VERSION validator release, default 1.6.2 +# KOSIT_CHECKSUM optional SHA-256 of the validator jar +# KOSIT_CONFIGURATION_URL URL of a configuration zip, or +# KOSIT_CONFIGURATION_DIR path to an unpacked configuration +# KOSIT_CONFIGURATION_CHECKSUM optional SHA-256 of the configuration zip +# KOSIT_FILES file, directory or glob of XML documents +# KOSIT_REPORT_DIR report output directory, default validator-reports + +.kosit-validate: + image: eclipse-temurin:21-jre + variables: + KOSIT_VERSION: "1.6.2" + KOSIT_CHECKSUM: "" + KOSIT_CONFIGURATION_URL: "" + KOSIT_CONFIGURATION_DIR: "" + KOSIT_CONFIGURATION_CHECKSUM: "" + KOSIT_FILES: "" + KOSIT_REPORT_DIR: "validator-reports" + script: + - | + set -euo pipefail + apt-get update -qq >/dev/null && apt-get install -qqy --no-install-recommends curl unzip >/dev/null + + verify() { + echo "$1 $2" | sha256sum -c - >/dev/null + } + + jar="validator-$KOSIT_VERSION-standalone.jar" + if [ ! -f "$jar" ]; then + curl -fsSL "https://github.com/itplr-kosit/validator/releases/download/v$KOSIT_VERSION/$jar" -o "$jar" + fi + if [ -n "$KOSIT_CHECKSUM" ]; then verify "$KOSIT_CHECKSUM" "$jar"; fi + + if [ -n "$KOSIT_CONFIGURATION_URL" ]; then + config_dir="kosit-configuration" + if [ ! -f "$config_dir/scenarios.xml" ]; then + curl -fsSL "$KOSIT_CONFIGURATION_URL" -o configuration.zip + if [ -n "$KOSIT_CONFIGURATION_CHECKSUM" ]; then verify "$KOSIT_CONFIGURATION_CHECKSUM" configuration.zip; fi + unzip -o -q configuration.zip -d "$config_dir" + fi + else + config_dir="$KOSIT_CONFIGURATION_DIR" + fi + test -f "$config_dir/scenarios.xml" + + files="" + if [ -d "$KOSIT_FILES" ]; then + files="$(find "$KOSIT_FILES" -name '*.xml' -type f | sort)" + else + files="$(ls $KOSIT_FILES)" + fi + test -n "$files" + + mkdir -p "$KOSIT_REPORT_DIR" + rejected=0 + for f in $files; do + if java -jar "$jar" -s "$config_dir/scenarios.xml" -r "$config_dir" -o "$KOSIT_REPORT_DIR" "$f"; then + echo "PASS $f" + else + rejected=$((rejected + 1)) + echo "FAIL $f" + fi + done + test "$rejected" -eq 0 + artifacts: + when: always + paths: + - $KOSIT_REPORT_DIR