From a8f821e90787c8a288d73131556c7e58171ff6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=B6nke?= Date: Sun, 17 May 2026 15:17:41 +0000 Subject: [PATCH] ci: add Quality Assurance workflow with XML lint Adds .github/workflows/quality-assurance.yml triggered on push to main and on pull_request. First job, xml-lint, runs three phases via scripts/lint-xml.sh: - well-formed: xmllint --noout per *.xml file - schema: XSD validation of files declaring xsi:noNamespaceSchemaLocation, looked up by basename in dtd/; runs xmllint --xinclude --schema - format: zpretty --check on every *.xml file Each phase uses continue-on-error so pre-existing issues do not block PRs; the workflow always produces a markdown summary on the run page with file counts and failure counts per phase. The script propagates counts via GITHUB_OUTPUT, and prints a `SUMMARY phase=... total=N failed=M` line for visibility in local runs and CI logs. --- .github/workflows/quality-assurance.yml | 57 ++++++++++++++ scripts/lint-xml.sh | 99 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 .github/workflows/quality-assurance.yml create mode 100755 scripts/lint-xml.sh diff --git a/.github/workflows/quality-assurance.yml b/.github/workflows/quality-assurance.yml new file mode 100644 index 00000000..eadeadf6 --- /dev/null +++ b/.github/workflows/quality-assurance.yml @@ -0,0 +1,57 @@ +name: Quality Assurance + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + xml-lint: + name: XML Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install xmllint and zpretty + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libxml2-utils + pip install --no-cache-dir zpretty + + - name: Well-formedness + id: wellformed + continue-on-error: true + run: bash scripts/lint-xml.sh well-formed + + - name: XSD schema validation + id: schema + continue-on-error: true + run: bash scripts/lint-xml.sh schema + + - name: zpretty formatting + id: format + continue-on-error: true + run: bash scripts/lint-xml.sh format + + - name: Summary + if: always() + run: | + { + echo "## XML Lint" + echo "" + echo "| Phase | Files | Failed | Outcome |" + echo "| --- | ---: | ---: | --- |" + echo "| Well-formedness | ${{ steps.wellformed.outputs.total }} | ${{ steps.wellformed.outputs.failed }} | ${{ steps.wellformed.outcome }} |" + echo "| Schema validation | ${{ steps.schema.outputs.total }} | ${{ steps.schema.outputs.failed }} | ${{ steps.schema.outcome }} |" + echo "| zpretty formatting | ${{ steps.format.outputs.total }} | ${{ steps.format.outputs.failed }} | ${{ steps.format.outcome }} |" + echo "" + echo "_Pre-existing failures are recorded but do not fail the workflow. They will be reduced incrementally._" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/lint-xml.sh b/scripts/lint-xml.sh new file mode 100755 index 00000000..926fa5a1 --- /dev/null +++ b/scripts/lint-xml.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# XML quality checks for PenText. +# well-formed - xmllint --noout per file +# schema - XSD validation of files declaring xsi:noNamespaceSchemaLocation +# format - zpretty --check on every *.xml file +# No arg or `all` runs all three; exit non-zero if any phase has failures. +# +# When GITHUB_OUTPUT is set (inside a GitHub Actions step), each phase +# writes `total=` and `failed=` to it so the workflow summary can report +# counts. +set -uo pipefail + +phase="${1:-all}" + +mapfile -t xml_files < <( + find . -type f -name "*.xml" \ + -not -path "./target/*" \ + -not -path "./.git/*" \ + -not -path "./.github/*" \ + | sort +) + +emit_summary() { + local name="$1" total="$2" failed="$3" + echo "SUMMARY phase=$name total=$total failed=$failed" + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "total=$total" + echo "failed=$failed" + } >> "$GITHUB_OUTPUT" + fi +} + +run_wellformed() { + echo "::group::Well-formedness (${#xml_files[@]} files)" + local failed=0 + for f in "${xml_files[@]}"; do + if ! xmllint --noout "$f"; then + failed=$((failed + 1)) + fi + done + echo "::endgroup::" + emit_summary well-formed "${#xml_files[@]}" "$failed" + [[ $failed -eq 0 ]] +} + +run_schema() { + echo "::group::XSD schema validation" + local fails=0 checked=0 + local dtd_dir + dtd_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/dtd" + for f in "${xml_files[@]}"; do + local xsd_rel xsd_name xsd_abs + xsd_rel=$(grep -m1 -oE 'xsi:noNamespaceSchemaLocation="[^"]+"' "$f" \ + | sed -E 's/.*="([^"]+)"/\1/' || true) + [[ -z "$xsd_rel" ]] && continue + xsd_name="$(basename "$xsd_rel")" + xsd_abs="$dtd_dir/$xsd_name" + if [[ ! -f "$xsd_abs" ]]; then + echo "WARN: schema not found in dtd/ for $f -> $xsd_name" >&2 + fails=$((fails + 1)) + continue + fi + checked=$((checked + 1)) + echo "-> $f (schema: $xsd_name)" + if ! xmllint --noout --xinclude --schema "$xsd_abs" "$f"; then + fails=$((fails + 1)) + fi + done + echo "::endgroup::" + emit_summary schema "$checked" "$fails" + [[ $fails -eq 0 ]] +} + +run_format() { + echo "::group::zpretty formatting (${#xml_files[@]} files)" + local out + out=$(zpretty --check "${xml_files[@]}" 2>&1 || true) + echo "$out" + local failed + failed=$(printf '%s\n' "$out" | grep -c "^This file would be rewritten:" || true) + echo "::endgroup::" + emit_summary format "${#xml_files[@]}" "$failed" + [[ $failed -eq 0 ]] +} + +case "$phase" in + well-formed) run_wellformed ;; + schema) run_schema ;; + format) run_format ;; + all) + rc=0 + run_wellformed || rc=1 + run_schema || rc=1 + run_format || rc=1 + exit $rc + ;; + *) echo "usage: $0 {well-formed|schema|format|all}" >&2; exit 2 ;; +esac