Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/object-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: object-evidence

# REPORT-ONLY. Builds the object-level evidence record (scripts/object_model.py, object-evidence/1):
# which production function / method got which coverage, which mutation verdicts, and which fields are
# UNVERIFIABLE and why. It then verifies the record at three separate levels (L1 integrity, L2 binding to
# the inputs, L3 completeness against an independent enumeration). It is NOT a required check, it gates
# nothing else, and nothing is skipped because of it. The coverage kind is statement (Go) or region (Rust):
# there is no branch coverage here (the stable toolchain gives none), and nothing may be named so.
# The mutation verdicts are not run here (they take a FULL run); the field is UNVERIFIABLE
# (MEASUREMENT_MISSING) in this record and the PR-03 completeness check owns that evidence.
on:
pull_request:
branches: ["**"]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: object-evidence-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
object-evidence:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.26.11"

- name: Install pinned Rust toolchain (rust-toolchain.toml)
run: |
set -euo pipefail
rustup show active-toolchain || rustup toolchain install
rustup component add llvm-tools-preview

- name: Install cargo-llvm-cov and cargo-mutants
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-mutants

- name: Measure (real tools, one directory of inputs)
env:
T: ${{ runner.temp }}/object-evidence
run: |
set -euo pipefail
mkdir -p "$T"
(cd tools/objinventory && go build -o "$T/objinventory" .)
# Go, root module: the production files of the scope, the cover profile of the whole module
python3 scripts/object_model.py scope-list --root . --scope .measurement-scope.json --lang go --class production > "$T/go-files.txt"
"$T/objinventory" $(cat "$T/go-files.txt") > "$T/go-inventory.json"
go test -count=1 -covermode=set -coverprofile="$T/go-cover.out" ./...
# Go, the separate goastdump module: its own inventory and profile
(cd tools/goastdump && "$T/objinventory" main.go > "$T/goastdump-inventory.json" \
&& go test -count=1 -covermode=set -coverprofile="$T/goastdump-cover.out" ./...)
# Rust: the function-level export and the mutant listing (the exit status is checked by `set -e`)
cargo llvm-cov --all --locked --json --output-path "$T/llvm-cov.json"
cargo mutants --workspace --list --json > "$T/rust-list.json"
{
echo "go=$(go version | awk '{print $3}')"
echo "rustc=$(rustc --version | awk '{print $2}')"
echo "cargo-llvm-cov=$(cargo llvm-cov --version | awk '{print $2}')"
echo "cargo-mutants=$(cargo mutants --version | awk '{print $2}')"
echo "objinventory=objinventory/1"
} > "$T/tools.txt"
cat "$T/tools.txt"

- name: Build the evidence record
env:
T: ${{ runner.temp }}/object-evidence
run: |
set -euo pipefail
tools=()
while IFS= read -r line; do tools+=(--tool "$line"); done < "$T/tools.txt"
python3 scripts/object_model.py build --root . --scope .measurement-scope.json --out "$T/evidence.json" \
--go-unit "root,.,$T/go-inventory.json,$T/go-cover.out" \
--go-unit "goastdump,tools/goastdump,$T/goastdump-inventory.json,$T/goastdump-cover.out,detail" \
--llvm-cov "$T/llvm-cov.json" --rust-list "$T/rust-list.json" --rust-prefix "$PWD/" "${tools[@]}"

- name: Verify the record (L1 integrity, L2 binding to the inputs, L3 completeness)
env:
T: ${{ runner.temp }}/object-evidence
run: |
set -euo pipefail
python3 scripts/object_model.py verify --record "$T/evidence.json" --root . --scope .measurement-scope.json \
--rust-list "$T/rust-list.json" \
--input "go-inventory:root=$T/go-inventory.json" --input "go-coverprofile:root=$T/go-cover.out" \
--input "go-inventory:goastdump=$T/goastdump-inventory.json" --input "go-coverprofile:goastdump=$T/goastdump-cover.out" \
--input "llvm-cov-json=$T/llvm-cov.json" --input "rust-mutant-list=$T/rust-list.json" \
--input "scope=.measurement-scope.json" | tee "$T/verify.txt"

- name: Keep the record
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: object-evidence
path: |
${{ runner.temp }}/object-evidence/evidence.json
${{ runner.temp }}/object-evidence/verify.txt
${{ runner.temp }}/object-evidence/tools.txt
if-no-files-found: warn
8 changes: 8 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ jobs:
GO_META_REQUIRE_GO: "1"
run: python -m pytest

- name: objinventory (Go function spans of the object-level measurement)
# Separate module (tools/objinventory/go.mod), like goastdump: not covered by the root
# `go test ./...`, so it is tested here. Report-only tooling: nothing gates on its output.
working-directory: tools/objinventory
run: |
go vet ./...
go test ./...

- name: goastdump (go/ast front end of go.meta.gen.py)
# Separate module (tools/goastdump/go.mod): not covered by the root
# `go test ./...`, so it is tested here.
Expand Down
87 changes: 87 additions & 0 deletions .measurement-scope.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"schema": "measurement-scope/1",
"version": 1,
"comment": "Which files the object-level measurement covers. The first matching rule decides; a tracked Go or Rust file that matches no rule is an error (nothing is left out silently). A change of this file is a change of the gate logic and is reviewed as such.",
"rules": [
{
"lang": "go",
"match": "*_test.go",
"class": "test",
"reason": "test code is not a production object"
},
{
"lang": "go",
"match": "*/testdata/*",
"class": "test-data",
"reason": "fixtures read by tests; Go tooling ignores testdata"
},
{
"lang": "go",
"match": "tests/*",
"class": "test-data",
"reason": "test fixtures and the tests of the scripts: not part of the Go module's production code (a source file stored here would also become a package of the root module)"
},
{
"lang": "go",
"match": "tools/goastdump/*",
"class": "separate",
"unit": "goastdump",
"reason": "a separate Go module (tools/goastdump/go.mod): measured and reported on its own, never averaged into the root module (owner decision 2026-11-14)"
},
{
"lang": "go",
"match": "tools/objinventory/*",
"class": "tool",
"unit": "objinventory",
"reason": "the measuring tool itself (a separate Go module); it is tested in the python job and is not an object of the Relay"
},
{
"lang": "go",
"match": "tools/statechain-demo/*",
"class": "experimental",
"reason": "live demo entry, exercised by make demo; already excluded from the coverage floors in verify.yml"
},
{
"lang": "go",
"match": "tools/trustflow-demo/*",
"class": "experimental",
"reason": "live cgo demo entry, exercised by make demo-trustflow; excluded from the coverage floors in verify.yml"
},
{
"lang": "go",
"match": "tools/oci-sign-demo/*",
"class": "experimental",
"reason": "self-verifying demo entry, exercised by make demo-oci-sign; excluded from the coverage floors in verify.yml"
},
{
"lang": "go",
"match": "*.go",
"class": "production",
"reason": "every other Go file of the root module"
},
{
"lang": "rust",
"match": "tests/*",
"class": "test-data",
"reason": "test fixtures and the tests of the scripts: not Rust workspace members"
},
{
"lang": "rust",
"match": "*/tests/*",
"class": "test",
"reason": "integration tests are not production objects"
},
{
"lang": "rust",
"match": "*/benches/*",
"class": "test",
"reason": "benchmarks are not production objects"
},
{
"lang": "rust",
"match": "*.rs",
"class": "production",
"reason": "every other Rust source (a #[cfg(test)] region inside a file is excluded by the object model, counted, not dropped)"
}
]
}
2 changes: 2 additions & 0 deletions scripts/mutation_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def changed_files(base: str, head: str) -> list[str]:
"scripts/baseline_guard.py",
"scripts/count_mutants.py",
"scripts/quality_gate.py",
"scripts/object_model.py",
".measurement-scope.json",
".mutation-baseline.json",
".github/workflows/mutation.yml",
".github/workflows/mutation-pr.yml",
Expand Down
Loading
Loading