The traceability job has been failing since it was written, and never on anything it checks. It declared no `container:`, so it ran in the runner's default image, which has no node; act_runner executes JS actions with the node it finds inside the job container, and both actions/checkout and upload-artifact are JS. The job died at "Cannot find: node in PATH" before the repository was checked out, and every later step then failed on an empty working tree -- the summary step's "head: cannot open 'docs/traceability.md'" is that, not a missing report. node:20-bookworm, and the choice is not arbitrary: node is the part with no workaround, while Debian 12 already carries python3.11 (tomllib, so the stdlib-only extractor reads traceability.toml) and git (for the pull_request diff step). Nothing in it names this repo, so the file stays copyable into the other two components unedited, which its header claims and this commit keeps true. The kpnpp-builder and jellytau-builder images bake node in for exactly this reason, and Dockerfile.builder-cpu says so in a comment. That knowledge just had not reached the one job with no image of its own. Second failure behind the first: the jray-project submodule was pinned by SSH URL. The runner has no key, so `submodules: recursive` could not have fetched the extractor even with node present. https, like the KPN submodule beside it. Gate and static check both pass locally on this tree -- 42/72 traced, 73.7% in CI scope, 0 orphans, 0 bare-cosine violations -- so what CI reports next is a fact about CI, not about the tree.
170 lines
6.9 KiB
YAML
170 lines
6.9 KiB
YAML
name: Traceability Validation
|
|
|
|
# Mirrors JellyTau's .gitea/workflows/traceability-check.yml. The extractor is
|
|
# stdlib Python, so there is no toolchain install step and no jq.
|
|
#
|
|
# This workflow is component-agnostic: every repo-specific setting - which ID
|
|
# prefixes count, which file suffixes are source, which directories to scan,
|
|
# the threshold - lives in traceability.toml at the repo root, and the same
|
|
# extractor is shared by all three JRay components. Copying this file into
|
|
# another component needs no edits - including the container image below, which
|
|
# is a stock public one and names nothing about this repo.
|
|
#
|
|
# NOTE: the runner here is an Intel N100 with no discrete GPU. This job is only
|
|
# ever static analysis of source comments plus markdown parsing, so it is cheap;
|
|
# the requirements it reports as "tagged but unexecuted" are the ones that need
|
|
# a GPU host, and they are deliberately never counted as covered.
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- master
|
|
- develop
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- master
|
|
- develop
|
|
|
|
jobs:
|
|
validate-traces:
|
|
runs-on: linux/amd64
|
|
name: Check requirement traces
|
|
|
|
# Gitea's act_runner executes JS actions - actions/checkout and
|
|
# upload-artifact are both JS - with the `node` binary found INSIDE the job
|
|
# container, not one it supplies. The runner's default image has none, so
|
|
# without this block the job dies at "Cannot find: node in PATH" before the
|
|
# repository is even checked out, and every later step fails on a missing
|
|
# working tree rather than on anything it was meant to check.
|
|
#
|
|
# node:20-bookworm rather than a Python image because node is the part that
|
|
# cannot be worked around: bookworm's python3 is 3.11, which has tomllib and
|
|
# is therefore already everything the stdlib-only extractor needs. Debian 12
|
|
# also carries the git the pull_request diff step below shells out to.
|
|
container:
|
|
image: node:20-bookworm
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
submodules: recursive
|
|
|
|
- name: Check Python is available
|
|
run: |
|
|
set -e
|
|
command -v python3 >/dev/null 2>&1 || {
|
|
echo "python3 is missing from the runner image."
|
|
echo "The traceability tooling is stdlib-only Python;"
|
|
echo "3.9+ with CLI flags, 3.11+ to read traceability.toml."
|
|
exit 1
|
|
}
|
|
python3 --version
|
|
|
|
# The gate's own arithmetic is the thing being trusted, so its tests run
|
|
# before it does. JellyTau's gate was believed for months while it was
|
|
# dividing by frozen literals; untested gate logic is how that happens.
|
|
- name: Test the extractor
|
|
run: python3 scripts/vendor/jray-project/scripts/traceability/test_extract_traces.py
|
|
|
|
# Threshold policy and every other repo-specific setting live in
|
|
# traceability.toml, not here, so local runs and CI runs cannot disagree
|
|
# about what "passing" means. Denominators come from docs/requirements.md
|
|
# at run time and are never hardcoded -- in this file or anywhere else.
|
|
#
|
|
# A misconfigured run (zero requirements parsed, zero files scanned) is a
|
|
# hard failure rather than a plausible-looking 0%.
|
|
- name: Traceability gate
|
|
run: sh scripts/vendor/jray-project/scripts/traceability/traceability-gate.sh
|
|
|
|
# AR-024's register row names its verification tier as "Static check --
|
|
# no bare cosine outside a tagged EXCEPTION". This is that check, and it
|
|
# belongs here rather than in unit-tests.yml because it is static
|
|
# analysis of source text, like everything else in this job, and needs
|
|
# no toolchain. It blocks: an untagged bare cosine is a defect by the
|
|
# invariant's own wording, not a warning.
|
|
- name: AR-024 — no bare cosine outside a recorded exception
|
|
run: python3 scripts/ci/check_raw_cosine.py
|
|
|
|
- name: Check modified files for traces
|
|
if: github.event_name == 'pull_request'
|
|
run: |
|
|
set -e
|
|
echo "Checking modified sources for TRACES tags..."
|
|
|
|
# The extensions come from the report the gate just wrote, which got
|
|
# them from traceability.toml. Restating them here would be a second
|
|
# place for the source-file definition to live, and the two would
|
|
# drift the first time a language is added.
|
|
PATTERN=$(python3 -c "
|
|
import json, re, sys
|
|
suffixes = json.load(open('traces-report.json'))['config']['sourceSuffixes']
|
|
print('(' + '|'.join(re.escape(s) + '\$' for s in suffixes) + ')')
|
|
")
|
|
echo "Source suffixes from traceability.toml: $PATTERN"
|
|
|
|
CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
|
|
| grep -E "$PATTERN" || true)
|
|
|
|
if [ -z "$CHANGED" ]; then
|
|
echo "No source files changed."
|
|
exit 0
|
|
fi
|
|
|
|
echo "Changed files:"
|
|
echo "$CHANGED" | sed 's/^/ /'
|
|
echo ""
|
|
|
|
# Advisory by design: not every file implements a requirement, and a
|
|
# tag on every function is noise that rots faster than it helps
|
|
# (CLAUDE.md: tag the unit that decides). This step exists to prompt,
|
|
# not to block. The blocking checks are in the gate step above.
|
|
#
|
|
# Piped into the loop rather than a here-string, and `case` rather
|
|
# than `[[ == ]]`, so this works under dash as well as bash. The loop
|
|
# body runs in a subshell, so misses are recorded in a file.
|
|
MISSING=$(mktemp)
|
|
echo "$CHANGED" | while IFS= read -r file; do
|
|
case "$file" in
|
|
*/test_*.py|*_test.py|*Tests.cs|tests/*|*/tests/*) continue ;;
|
|
esac
|
|
[ -f "$file" ] || continue
|
|
if ! grep -q 'TRACES:' "$file"; then
|
|
echo " no TRACES tag: $file"
|
|
echo "$file" >> "$MISSING"
|
|
fi
|
|
done
|
|
|
|
COUNT=$(wc -l < "$MISSING" | tr -d ' ')
|
|
rm -f "$MISSING"
|
|
|
|
if [ "$COUNT" -gt 0 ]; then
|
|
echo ""
|
|
echo "$COUNT changed file(s) carry no requirement tag."
|
|
echo "Format: // TRACES: AR-012, AR-013 | SR-002"
|
|
echo " (pipe separates requirement types, comma separates IDs)"
|
|
echo "A deliberate invariant exception is tagged separately:"
|
|
echo " // EXCEPTION: AR-024 <reason>"
|
|
echo "See CLAUDE.md and SPEC.md section 6."
|
|
fi
|
|
|
|
- name: Report summary
|
|
if: always()
|
|
run: |
|
|
echo "Traceability matrix: docs/traceability.md"
|
|
echo ""
|
|
head -40 docs/traceability.md || true
|
|
|
|
- name: Save reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: traceability-reports
|
|
path: |
|
|
traces-report.json
|
|
docs/traceability.md
|
|
retention-days: 30
|