spantruth 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. spantruth-0.3.0/.context/CONTEXT_llm_telemetry_testing_landscape.md +117 -0
  2. spantruth-0.3.0/.editorconfig +12 -0
  3. spantruth-0.3.0/.github/CODEOWNERS +1 -0
  4. spantruth-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +24 -0
  5. spantruth-0.3.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
  6. spantruth-0.3.0/.github/ISSUE_TEMPLATE/dispute.yml +30 -0
  7. spantruth-0.3.0/.github/ISSUE_TEMPLATE/subject.yml +24 -0
  8. spantruth-0.3.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  9. spantruth-0.3.0/.github/dependabot.yml +20 -0
  10. spantruth-0.3.0/.github/workflows/ci.yml +106 -0
  11. spantruth-0.3.0/.github/workflows/cla.yml +26 -0
  12. spantruth-0.3.0/.github/workflows/release.yml +53 -0
  13. spantruth-0.3.0/.github/workflows/report.yml +61 -0
  14. spantruth-0.3.0/.gitignore +12 -0
  15. spantruth-0.3.0/.plans/PLAN_FEAT_run-anywhere.md +288 -0
  16. spantruth-0.3.0/.pre-commit-config.yaml +14 -0
  17. spantruth-0.3.0/.python-version +1 -0
  18. spantruth-0.3.0/AGENTS.md +23 -0
  19. spantruth-0.3.0/CHANGELOG.md +139 -0
  20. spantruth-0.3.0/CITATION.cff +18 -0
  21. spantruth-0.3.0/CONTRIBUTING.md +96 -0
  22. spantruth-0.3.0/GOVERNANCE.md +24 -0
  23. spantruth-0.3.0/LICENSE +202 -0
  24. spantruth-0.3.0/MAINTAINERS.md +5 -0
  25. spantruth-0.3.0/PKG-INFO +259 -0
  26. spantruth-0.3.0/PLAN.md +275 -0
  27. spantruth-0.3.0/README.md +235 -0
  28. spantruth-0.3.0/action.yml +115 -0
  29. spantruth-0.3.0/docs/assets/report.png +0 -0
  30. spantruth-0.3.0/known_issues.toml +52 -0
  31. spantruth-0.3.0/pyproject.toml +80 -0
  32. spantruth-0.3.0/results/2026-11-03-pinned.json +21941 -0
  33. spantruth-0.3.0/src/spantruth/__init__.py +3 -0
  34. spantruth-0.3.0/src/spantruth/calls.py +181 -0
  35. spantruth-0.3.0/src/spantruth/checks.py +210 -0
  36. spantruth-0.3.0/src/spantruth/cli.py +214 -0
  37. spantruth-0.3.0/src/spantruth/conventions.py +128 -0
  38. spantruth-0.3.0/src/spantruth/driver.py +168 -0
  39. spantruth-0.3.0/src/spantruth/expectations.py +116 -0
  40. spantruth-0.3.0/src/spantruth/fixtures/openai-responses/NOTICE.md +30 -0
  41. spantruth-0.3.0/src/spantruth/fixtures/openai-responses/stream.missing-finish-abort.json +80 -0
  42. spantruth-0.3.0/src/spantruth/mockserver.py +121 -0
  43. spantruth-0.3.0/src/spantruth/model.py +115 -0
  44. spantruth-0.3.0/src/spantruth/paths.py +26 -0
  45. spantruth-0.3.0/src/spantruth/receiver.py +164 -0
  46. spantruth-0.3.0/src/spantruth/recorded.py +51 -0
  47. spantruth-0.3.0/src/spantruth/reference.py +276 -0
  48. spantruth-0.3.0/src/spantruth/report.py +179 -0
  49. spantruth-0.3.0/src/spantruth/rules.py +91 -0
  50. spantruth-0.3.0/src/spantruth/runner.py +244 -0
  51. spantruth-0.3.0/src/spantruth/scenarios.py +353 -0
  52. spantruth-0.3.0/src/spantruth/site.py +234 -0
  53. spantruth-0.3.0/src/spantruth/subjects.py +191 -0
  54. spantruth-0.3.0/src/spantruth/wire_anthropic.py +163 -0
  55. spantruth-0.3.0/src/spantruth/wire_openai.py +178 -0
  56. spantruth-0.3.0/subjects/_base.toml +20 -0
  57. spantruth-0.3.0/subjects/anthropic-sdk/instrument.py +9 -0
  58. spantruth-0.3.0/subjects/anthropic-sdk/subject.toml +7 -0
  59. spantruth-0.3.0/subjects/control-faulty/expected.toml +22 -0
  60. spantruth-0.3.0/subjects/control-faulty/instrument.py +9 -0
  61. spantruth-0.3.0/subjects/control-faulty/subject.toml +5 -0
  62. spantruth-0.3.0/subjects/control-honest/instrument.py +7 -0
  63. spantruth-0.3.0/subjects/control-honest/subject.toml +5 -0
  64. spantruth-0.3.0/subjects/openinference/instrument.py +9 -0
  65. spantruth-0.3.0/subjects/openinference/subject.toml +8 -0
  66. spantruth-0.3.0/subjects/openllmetry/instrument.py +9 -0
  67. spantruth-0.3.0/subjects/openllmetry/subject.toml +8 -0
  68. spantruth-0.3.0/subjects/otel-official/instrument.py +9 -0
  69. spantruth-0.3.0/subjects/otel-official/subject.toml +11 -0
  70. spantruth-0.3.0/tests/test_action.py +109 -0
  71. spantruth-0.3.0/tests/test_checks.py +208 -0
  72. spantruth-0.3.0/tests/test_controls_e2e.py +39 -0
  73. spantruth-0.3.0/tests/test_expectations.py +214 -0
  74. spantruth-0.3.0/tests/test_packaging.py +148 -0
  75. spantruth-0.3.0/tests/test_recorded.py +67 -0
  76. spantruth-0.3.0/tests/test_report.py +184 -0
  77. spantruth-0.3.0/tests/test_runner.py +33 -0
  78. spantruth-0.3.0/tests/test_servers.py +115 -0
  79. spantruth-0.3.0/tests/test_subjects.py +230 -0
  80. spantruth-0.3.0/tests/test_wire.py +143 -0
  81. spantruth-0.3.0/tests/test_workflows.py +31 -0
  82. spantruth-0.3.0/uv.lock +943 -0
@@ -0,0 +1,117 @@
1
+ # The landscape around testing LLM telemetry values
2
+
3
+ Researched on 2026-11-03. Every number has its source and date. Anything marked "unverified" was not
4
+ confirmed at the source. Re-check a number before you quote it in public.
5
+
6
+ ## Prior art
7
+
8
+ Nothing found on 2026-11-03 does all of these together: a fake provider, the real provider SDK, the
9
+ production instrumentation packages, an OTLP receiver, and values compared with what the provider sent,
10
+ including failure paths.
11
+
12
+ | Project | What it checks | Difference from a value check |
13
+ | --- | --- | --- |
14
+ | [semantic-conventions-genai `reference/`](https://github.com/open-telemetry/semantic-conventions-genai/tree/main/reference) | Hand-written reference instrumentations (built on `opentelemetry-util-genai`) of about 40 client SDKs and frameworks, run against a deterministic mock server. The [conformance runner](https://github.com/open-telemetry/semantic-conventions-conformance) validates the captured telemetry with Weaver and generates a matrix of supported attributes. | The instrumentation is a reference, not the package users install. The checks are names, types and required attributes, not equality with the provider's numbers. |
15
+ | [Weaver live-check](https://github.com/open-telemetry/weaver/blob/main/docs/usage.md) | Received OTLP against a semantic-convention registry, with OPA policies. `--fail-on` sets the exit level. `--diagnostic-format gh_workflow_command` writes workflow annotations. | Conformance to the registry, not to the wire. |
16
+ | [pytest-genai-semconv](https://github.com/Krishnachaitanyakc/pytest-genai-semconv) (0.1.0, 2026-07-06) | A pytest plugin with in-memory span capture and `assert_chat_span(...)` helpers: presence, types, span kind. | Tests your own spans. No fake provider, no real SDK, no failure paths. |
17
+ | [kartsan03/wiretruth](https://github.com/kartsan03/wiretruth) | What the provider SDK returns to the caller, from recorded responses. Owns the `wiretruth` name on PyPI (0.1.0 on 2026-10-11, 0.2.0 on 2026-10-13). | Checks the SDK, not the span. Its fixture format is replayable. |
18
+ | [4ktLuffy/spanproof](https://github.com/4ktLuffy/spanproof) | Sentry SDK integrations, with fixtures validated by the provider SDK's types. | One vendor. |
19
+ | [arthi-arumugam-git/wrong-numbers](https://github.com/arthi-arumugam-git/wrong-numbers) | A field guide to wrong numbers in eval, tracing and cost libraries. | Documentation, not a harness. |
20
+ | [jkelly-dev1/llm-observability-stack](https://github.com/jkelly-dev1/llm-observability-stack) | A personal study. Cost computed from spans was 44% below the provider's usage, because the span copied an `input_tokens` field that excludes the cached prefix. | Not a tool. Evidence for the cache-token class. |
21
+
22
+ The reference scenarios name their metrics `gen_ai.client.inference.usage.input_tokens` (a counter) and
23
+ `gen_ai.client.inference.operation.input_tokens` (a histogram), with matching output, cache and
24
+ reasoning names. Source: `reference/scenarios/anthropic/scenario.py` on the `main` branch, 2026-11-03.
25
+
26
+ ## The semantic conventions
27
+
28
+ - The GenAI conventions moved to their own repository,
29
+ [semantic-conventions-genai](https://github.com/open-telemetry/semantic-conventions-genai), in
30
+ semconv v1.42. Every GenAI attribute in its registry has `stability: development`.
31
+ - Token attributes: `gen_ai.usage.input_tokens` (SHOULD include cached tokens),
32
+ `gen_ai.usage.output_tokens` (SHOULD include reasoning tokens), `gen_ai.usage.cache_read.input_tokens`,
33
+ `gen_ai.usage.cache_write.input_tokens` and `gen_ai.usage.reasoning.output_tokens`.
34
+ - The cache-write attribute was `cache_creation.input_tokens` first. The rename is live, and
35
+ instrumentations lag: [pydantic-ai#11779](https://github.com/pydantic/pydantic-ai/issues/11779)
36
+ (2026-10-28). The date of the rename is unverified.
37
+ - Since v1.37.0 (2025-08-25), content goes in `gen_ai.input.messages`, `gen_ai.output.messages` and
38
+ `gen_ai.system_instructions`, or in the `gen_ai.client.inference.operation.details` event.
39
+
40
+ ## Who would run a value check
41
+
42
+ pypistats "last month" and npm downloads, read on 2026-11-03. Download counts mix SDK use with
43
+ instrumentation use, so they rank reach, not need.
44
+
45
+ | Library | Downloads a month | Emits OTel GenAI spans |
46
+ | --- | --- | --- |
47
+ | LiteLLM | 92.2M | Through its OTel callback |
48
+ | Langfuse SDK | 23.6M | OTel-based SDK |
49
+ | MLflow | 19.9M | OTel export |
50
+ | Pydantic Logfire | 13.0M | Yes |
51
+ | opentelemetry-util-genai (official) | 4.91M | Yes |
52
+ | OpenLLMetry anthropic and openai | 3.02M and 2.86M | Yes |
53
+ | OpenInference openai and anthropic | 2.25M and 0.78M | Its own `llm.*` names |
54
+ | OpenLIT | 278k | Yes |
55
+ | Vercel AI SDK (`ai`, npm) | 118.4M | `experimental_telemetry`, mixed names (unverified) |
56
+
57
+ Provider SDKs: openai 295.9M, anthropic 150.2M, google-genai 137.6M, groq 16.8M and mistralai 16.2M a
58
+ month. On npm: `@anthropic-ai/sdk` 181.4M, `openai` 162.0M and `@google/genai` 91.6M.
59
+
60
+ ## Demand: the bug classes repeat
61
+
62
+ Issues and pull requests created between 2026-05-03 and 2026-11-03, with their state on 2026-11-03.
63
+ Four classes repeat across eight independent projects: cache tokens dropped or counted twice, reasoning
64
+ tokens dropped, spans that never end, and one call reported on two spans.
65
+
66
+ - Cache tokens: [openllmetry#4665](https://github.com/traceloop/openllmetry/issues/4665),
67
+ [openllmetry#4583](https://github.com/traceloop/openllmetry/issues/4583) (`prompt_tokens_details` is
68
+ None), [mlflow#26907](https://github.com/mlflow/mlflow/issues/26907) (final counts in `message_delta`
69
+ ignored), [mlflow#26817](https://github.com/mlflow/mlflow/issues/26817),
70
+ [litellm#49558](https://github.com/BerriAI/litellm/pull/49558),
71
+ [langfuse#19483](https://github.com/langfuse/langfuse/pull/19483) and
72
+ [pydantic-ai#11779](https://github.com/pydantic/pydantic-ai/issues/11779).
73
+ - Reasoning tokens: [openllmetry#4660](https://github.com/traceloop/openllmetry/issues/4660) (streamed
74
+ chat), [litellm#49557](https://github.com/BerriAI/litellm/pull/49557) and
75
+ [python-contrib#4987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4987).
76
+ - Spans that never end: [openllmetry#4608](https://github.com/traceloop/openllmetry/issues/4608),
77
+ [openllmetry#4597](https://github.com/traceloop/openllmetry/issues/4597),
78
+ [openllmetry#4650](https://github.com/traceloop/openllmetry/pull/4650) and
79
+ [openinference#4215](https://github.com/Arize-ai/openinference/pull/4215).
80
+ - One call on two spans: [openllmetry#4609](https://github.com/traceloop/openllmetry/issues/4609).
81
+ - Calls through other entry points record nothing:
82
+ [logfire#2549](https://github.com/pydantic/logfire/issues/2549) (`with_raw_response` and
83
+ `with_streaming_response`).
84
+ - Responses API paths: [openinference#4185](https://github.com/Arize-ai/openinference/issues/4185) and
85
+ [logfire#2565](https://github.com/pydantic/logfire/pull/2565).
86
+
87
+ ## How conformance suites became the default
88
+
89
+ From primary sources, read on 2026-11-03.
90
+
91
+ - **[Bowtie](https://github.com/bowtie-json-schema/bowtie)** is the closest model. Each JSON Schema
92
+ implementation runs in a container with a small harness that speaks a JSON-lines protocol on stdin and
93
+ stdout. Harnesses live in the implementers' directories. Bowtie has a report site, shields.io endpoint
94
+ badges, a composite setup action that installs it with uv, and `bowtie summary --format markdown` for
95
+ the job summary. A skipped test carries an issue URL. Its author wrote most early harnesses himself.
96
+ It launched with 12 implementations and now has 24.
97
+ - **test262** and **web-platform-tests** keep known failures in the implementer's repository and
98
+ publish results per implementation (test262.fyi, wpt.fyi).
99
+ - **Kubernetes conformance** gives a trademark to certified products, and the trademark drives
100
+ adoption.
101
+ - **Protocol Buffers conformance** uses a small "testee" per language over stdin and stdout, the same
102
+ idea as the Bowtie harness.
103
+
104
+ Patterns that drove adoption: a tiny harness as the unit of contribution, the harness in the
105
+ implementer's repository, a public result and a badge per implementation, known failures recorded with
106
+ an issue link, a one-step CI path, a neutral home, and a funded maintainer who writes the first
107
+ harnesses.
108
+
109
+ Weaknesses seen: a frozen toolchain (Autobahn is pinned to Python 2), no stable test identity (Bowtie
110
+ matches skips by description), governance that never became formal, and no documented dispute path.
111
+
112
+ Launch posts alone rarely drove adoption: the promptfoo Show HN scored 14 points. Integrations in the
113
+ places users already work mattered more.
114
+
115
+ ## Related Context
116
+
117
+ None yet.
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+ indent_size = 4
10
+
11
+ [*.{yml,yaml,json,toml,md,html,js,css}]
12
+ indent_size = 2
@@ -0,0 +1 @@
1
+ * @chrikrah
@@ -0,0 +1,24 @@
1
+ name: Bug in spantruth
2
+ description: The harness itself misbehaves, for example a cell is inconclusive or a run fails.
3
+ labels: ["bug"]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened
9
+ description: The command you ran, and its output.
10
+ render: shell
11
+ validations:
12
+ required: true
13
+ - type: input
14
+ id: version
15
+ attributes:
16
+ label: spantruth version or commit
17
+ validations:
18
+ required: true
19
+ - type: input
20
+ id: env
21
+ attributes:
22
+ label: Operating system and Python version
23
+ validations:
24
+ required: true
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Question or idea
4
+ url: https://github.com/Plexito-de/spantruth/discussions
5
+ about: Ask a question or float an idea in Discussions.
6
+ - name: Security problem
7
+ url: https://github.com/Plexito-de/spantruth/security/advisories/new
8
+ about: Report a vulnerability privately. Do not open a public issue.
@@ -0,0 +1,30 @@
1
+ name: Dispute a finding
2
+ description: A cell on the report is red or yellow and you think the check is wrong.
3
+ labels: ["dispute"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thank you. Maintainers of the library under test are especially welcome here. We decide
9
+ every dispute on one question: does the cited rule say what the check claims?
10
+ - type: input
11
+ id: cell
12
+ attributes:
13
+ label: Subject and scenario
14
+ placeholder: openllmetry, anthropic/stream
15
+ validations:
16
+ required: true
17
+ - type: input
18
+ id: check
19
+ attributes:
20
+ label: Check
21
+ placeholder: output_tokens
22
+ validations:
23
+ required: true
24
+ - type: textarea
25
+ id: why
26
+ attributes:
27
+ label: Why the finding is wrong
28
+ description: Quote the spec, the provider documentation, or your library's documented behavior.
29
+ validations:
30
+ required: true
@@ -0,0 +1,24 @@
1
+ name: Add a subject, provider or scenario
2
+ description: Propose a library, a provider API, or a failure mode to cover.
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: dropdown
6
+ id: kind
7
+ attributes:
8
+ label: What to add
9
+ options: [A subject (instrumentation library), A provider, A scenario]
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: what
14
+ attributes:
15
+ label: Details
16
+ description: Name, link, and for a scenario the provider behavior it reproduces, with a link to the provider documentation.
17
+ validations:
18
+ required: true
19
+ - type: checkboxes
20
+ id: help
21
+ attributes:
22
+ label: Contribution
23
+ options:
24
+ - label: I would like to open the pull request myself.
@@ -0,0 +1,15 @@
1
+ ## What this changes
2
+
3
+ <!-- One or two sentences. Link the issue it closes. -->
4
+
5
+ ## How it was tested
6
+
7
+ <!-- The commands you ran and their output. -->
8
+
9
+ ## Checklist
10
+
11
+ - [ ] The title is a Conventional Commit, for example `feat(subjects): add logfire`.
12
+ - [ ] A test covers the change, and it fails when the change is reverted.
13
+ - [ ] `control-honest` passes, and `control-faulty` fails exactly its expected checks.
14
+ - [ ] Every new red cell is explained: the line of code that causes it, or a note that I could not find it.
15
+ - [ ] `CHANGELOG.md` is updated under Unreleased.
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ cooldown:
8
+ default-days: 14
9
+ groups:
10
+ actions:
11
+ patterns: ["*"]
12
+ - package-ecosystem: uv
13
+ directory: /
14
+ schedule:
15
+ interval: weekly
16
+ cooldown:
17
+ default-days: 14
18
+ groups:
19
+ python:
20
+ patterns: ["*"]
@@ -0,0 +1,106 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions: {}
9
+
10
+ concurrency:
11
+ group: ci-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ lint:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
+ with:
22
+ persist-credentials: false
23
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
24
+ - run: uv sync --locked
25
+ - run: uv run ruff check .
26
+ - run: uv run ruff format --check .
27
+
28
+ test:
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: read
32
+ strategy:
33
+ matrix:
34
+ python: ["3.11", "3.12"]
35
+ steps:
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37
+ with:
38
+ persist-credentials: false
39
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
40
+ - run: uv sync --locked --python ${{ matrix.python }}
41
+ - run: uv run pytest -m "not slow"
42
+
43
+ controls:
44
+ # The falsification of the whole suite: both controls, end to end, in real subprocesses.
45
+ runs-on: ubuntu-latest
46
+ permissions:
47
+ contents: read
48
+ steps:
49
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
50
+ with:
51
+ persist-credentials: false
52
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
53
+ - run: uv sync --locked
54
+ - run: uv run pytest -m slow
55
+
56
+ zizmor:
57
+ runs-on: ubuntu-latest
58
+ permissions:
59
+ contents: read
60
+ security-events: write
61
+ steps:
62
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
63
+ with:
64
+ persist-credentials: false
65
+ - uses: zizmorcore/zizmor-action@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
66
+
67
+ action:
68
+ # The action as a user runs it. The last run compares with an empty expectation file, so it
69
+ # must fail with exit code 1: a gate that cannot fail has measured nothing. `uses: $/` is
70
+ # GitHub's self-repository syntax (July 2026): this repository at the workflow's own commit,
71
+ # independent of what a step left in the workspace. It is not a typo for `./`.
72
+ runs-on: ubuntu-latest
73
+ permissions:
74
+ contents: read
75
+ steps:
76
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
77
+ with:
78
+ persist-credentials: false
79
+ - uses: $/
80
+ with:
81
+ subject: control-honest
82
+ scenario: |
83
+ openai/basic
84
+ anthropic/stream-early-exit
85
+ - uses: $/
86
+ with:
87
+ subject: control-faulty
88
+ expect: subjects/control-faulty/expected.toml
89
+ scenario: |
90
+ openai/stream-no-usage
91
+ anthropic/cache-hit
92
+ anthropic/stream-early-exit
93
+ - run: touch "$RUNNER_TEMP/empty.toml"
94
+ - id: differs
95
+ uses: $/
96
+ continue-on-error: true
97
+ with:
98
+ subject: control-faulty
99
+ expect: ${{ runner.temp }}/empty.toml
100
+ scenario: openai/stream-no-usage
101
+ artifact-name: spantruth-control-faulty-differs
102
+ - name: The run against an empty expectation file failed with exit code 1
103
+ env:
104
+ OUTCOME: ${{ steps.differs.outcome }}
105
+ CODE: ${{ steps.differs.outputs.exit-code }}
106
+ run: test "$OUTCOME" = failure && test "$CODE" = 1
@@ -0,0 +1,26 @@
1
+ name: CLA
2
+
3
+ # pull_request_target is required so the check can comment on pull requests from forks.
4
+ # The reusable workflow never checks out or runs code from the pull request.
5
+ on:
6
+ pull_request_target: # zizmor: ignore[dangerous-triggers]
7
+ types: [opened, synchronize, reopened]
8
+ issue_comment:
9
+ types: [created]
10
+
11
+ permissions: {}
12
+
13
+ jobs:
14
+ cla:
15
+ if: >-
16
+ github.event_name == 'pull_request_target' ||
17
+ (github.event.issue.pull_request &&
18
+ (contains(github.event.comment.body, 'I have read the CLA Document') ||
19
+ contains(github.event.comment.body, 'recheck')))
20
+ permissions:
21
+ contents: write
22
+ pull-requests: write
23
+ statuses: write
24
+ uses: Plexito-de/.github/.github/workflows/cla.yml@main # zizmor: ignore[unpinned-uses] our own org's workflow
25
+ with:
26
+ allowlist: "chrikrah,dependabot[bot],github-actions[bot]"
@@ -0,0 +1,53 @@
1
+ name: Release
2
+
3
+ # A release is a tag with notes and a wheel on PyPI. The wheel holds the harness, the CLI and the
4
+ # subjects, so `uvx spantruth` runs without a clone.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ check:
13
+ # The tag, the package version and the wheel name must agree, or the action ref, the wheel and
14
+ # the results document would each claim a different version.
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ steps:
19
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20
+ with:
21
+ persist-credentials: false
22
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
23
+ - run: uv build
24
+ - name: The tag matches the package version
25
+ env:
26
+ TAG: ${{ github.ref_name }}
27
+ run: test -f "dist/spantruth-${TAG#v}-py3-none-any.whl"
28
+
29
+ github-release:
30
+ needs: check
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ contents: write
34
+ steps:
35
+ - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
36
+ with:
37
+ generate_release_notes: true
38
+
39
+ pypi:
40
+ needs: check
41
+ runs-on: ubuntu-latest
42
+ # Trusted publishing: PyPI trusts this workflow and this environment, so no token is stored.
43
+ environment: pypi
44
+ permissions:
45
+ contents: read
46
+ id-token: write
47
+ steps:
48
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
49
+ with:
50
+ persist-credentials: false
51
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
52
+ - run: uv build
53
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
@@ -0,0 +1,61 @@
1
+ name: Report
2
+
3
+ # Runs every subject at its pinned versions and at its latest release, and publishes the
4
+ # matrix to GitHub Pages.
5
+ on:
6
+ schedule:
7
+ - cron: "17 3 * * *"
8
+ push:
9
+ branches: [main]
10
+ workflow_dispatch:
11
+
12
+ permissions: {}
13
+
14
+ concurrency:
15
+ group: report
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ run:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
+ with:
26
+ persist-credentials: false
27
+ - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
28
+ - run: uv sync --locked
29
+ # Exit code 3 means the harness could not run a cell or build an environment, for example
30
+ # when a new upstream release fails to install. Publish the report anyway, so the gap shows
31
+ # on the page, and flag the run.
32
+ - name: Pinned versions
33
+ run: |
34
+ rc=0
35
+ uv run spantruth run --out site/pinned.json || rc=$?
36
+ if [ "$rc" -eq 3 ]; then echo "::warning::incomplete pinned run"; elif [ "$rc" -ne 0 ]; then exit "$rc"; fi
37
+ - name: Latest releases
38
+ run: |
39
+ rc=0
40
+ uv run spantruth run --latest --out site/latest.json || rc=$?
41
+ if [ "$rc" -eq 3 ]; then echo "::warning::incomplete latest run"; elif [ "$rc" -ne 0 ]; then exit "$rc"; fi
42
+ - name: Render
43
+ run: |
44
+ uv run spantruth report site/pinned.json --html site/index.html
45
+ uv run spantruth report site/latest.json --html site/latest.html --badges site/badges
46
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
47
+ with:
48
+ path: site
49
+
50
+ deploy:
51
+ needs: run
52
+ runs-on: ubuntu-latest
53
+ permissions:
54
+ pages: write
55
+ id-token: write
56
+ environment:
57
+ name: github-pages
58
+ url: ${{ steps.deploy.outputs.page_url }}
59
+ steps:
60
+ - id: deploy
61
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .spantruth/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ site/
11
+ results/*
12
+ !results/2026-11-03-pinned.json