diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7f4849d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.venv +.superpowers +.pytest_cache +**/__pycache__ +*.egg-info +build +dist +docs +tests +.tools diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0db9525..42ff1eb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,11 @@ updates: groups: github-actions: patterns: ["*"] + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + groups: + docker: + patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8233aa3..e04afe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,3 +50,25 @@ jobs: test-protocol EPC-24-00137/* --pdf-engine $engine --pdf $engine.pdf --lang de head -c 4 $engine.pdf | grep -q '%PDF' done + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: docker build -t test-protocol . + - name: Runs unprivileged by default + run: test "$(docker run --rm --entrypoint id test-protocol -u)" = 10001 + - name: HTML and PDF from a mounted folder, exit status 0 for PASS and 1 for FAIL + working-directory: examples + run: | + run() { docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" test-protocol "$@"; } + run EPC-24-00137/* --limits limits.toml -o pass.html --pdf pass.pdf --lang de + grep -q 'EPC-24-00137' pass.html + head -c 4 pass.pdf | grep -q '%PDF' + test "$(stat -c %u pass.pdf)" = "$(id -u)" + set +e + run EPC-24-00142/* --limits limits.toml --pdf fail.pdf + code=$? + set -e + test $code -eq 1 + head -c 4 fail.pdf | grep -q '%PDF' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0573d9e..976cf60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# Publishes to PyPI when a version tag (v1.2.3) is pushed: runs the tests, +# Publishes to PyPI and ghcr.io when a version tag (v1.2.3) is pushed: runs the tests, # checks that the tag matches the package version, builds, then publishes. # Needs a Trusted Publisher on PyPI for this repository and workflow # (environment "pypi"); no API token is stored in the repository. @@ -52,6 +52,34 @@ jobs: name: dist path: dist/ + docker: + needs: [test, build, publish] # only an image for a version that is on PyPI + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + - id: version + name: Version from the tag; "latest" only for final releases (no rc/a/b/dev) + run: | + version="${GITHUB_REF_NAME#v}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then prerelease=false; else prerelease=true; fi + echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT" + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v7 + with: + context: . + push: true + tags: | + ghcr.io/steitec/test-protocol:${{ steps.version.outputs.version }} + ${{ steps.version.outputs.prerelease == 'false' && 'ghcr.io/steitec/test-protocol:latest' || '' }} + publish: needs: build runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index b68c5d3..49bf9ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.1 — 2026-11-08 + +- Docker image `ghcr.io/steitec/test-protocol` with WeasyPrint and its system libraries, + built with every release, so PDF output needs no local installation + ## 0.1.0 — 2026-10-29 First release. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b7428d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# test-protocol in a container, with WeasyPrint and its system libraries, so PDF +# output needs no local installation. Runs in /data; mount your results there and +# run as your own user so the files you get back belong to you. +# +# docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" ghcr.io/steitec/test-protocol \ +# result.json --limits limits.toml -o protocol.html --pdf protocol.pdf +FROM python:3.13-slim +LABEL org.opencontainers.image.source=https://github.com/steitec/test-protocol \ + org.opencontainers.image.description="Turn raw test results into a printable test protocol (HTML, PDF, JSON)" \ + org.opencontainers.image.licenses=MIT + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 fonts-liberation \ + && rm -rf /var/lib/apt/lists/* + +# The protocol asks for system-ui, which fontconfig would otherwise resolve to the +# first font it finds (a monospaced one here). +COPY <<'FONTS' /etc/fonts/local.conf + + + + system-uiLiberation Sans + sans-serifLiberation Sans + +FONTS + +WORKDIR /src +COPY pyproject.toml README.md LICENSE ./ +COPY src ./src +RUN pip install --no-cache-dir ".[pdf]" && rm -rf /src + +RUN useradd --create-home --uid 10001 protocol && install -d -o protocol /data +# With --user there is no home directory; fontconfig still needs a place for its cache. +ENV XDG_CACHE_HOME=/tmp/.cache +USER protocol +WORKDIR /data + +ENTRYPOINT ["test-protocol"] +CMD ["--help"] diff --git a/README.md b/README.md index 9bca49b..27b54b2 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,24 @@ Python 3.10 or newer. For PDF output you need either WeasyPrint (the `pdf` extra; on Windows it needs the GTK/Pango libraries, see the WeasyPrint docs) or Chrome, Chromium or Edge, which the tool finds on its own. +## Docker + +The image contains WeasyPrint and its system libraries, so PDF output needs no +local installation. Run it in the folder with your results: + +```console +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" ghcr.io/steitec/test-protocol \ + result.json --limits limits.toml -o protocol.html --pdf protocol.pdf +``` + +`--user` makes the written files yours; without it the container runs as an +unprivileged user that cannot write to your folder. On Docker Desktop use +`--user 0:0` instead, because it shows the folder as owned by root inside the +container and still writes the files as you on the host. The exit status is the +same as without Docker (0 PASS, 1 FAIL, 2 bad input). The image is built with +every release and tagged with the version and `latest`; pin the version in CI. +PDF fonts come from the image (Liberation Sans). + ## Inputs The format is detected from the file; `--format` forces it. diff --git a/pyproject.toml b/pyproject.toml index 06b8c89..37c7b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "test-protocol" -version = "0.1.0" +version = "0.1.1" description = "Turn raw test results (CSV, JSON, JUnit XML) into a printable test protocol with limits, PASS/FAIL verdict, traceability and signature fields" readme = "README.md" license = "MIT" diff --git a/src/test_protocol/__init__.py b/src/test_protocol/__init__.py index 11e277c..66b82ce 100644 --- a/src/test_protocol/__init__.py +++ b/src/test_protocol/__init__.py @@ -1,3 +1,3 @@ """Turn raw test results into a printable test protocol.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/tests/test_cli.py b/tests/test_cli.py index 09aa58b..b40589a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -141,4 +141,4 @@ def fail(*args, **kwargs): def test_module_entry(): result = subprocess.run([sys.executable, "-m", "test_protocol", "--version"], capture_output=True, text=True) - assert result.stdout.strip() == "test-protocol 0.1.0" + assert result.stdout.strip() == "test-protocol 0.1.1"