Files
jellytau/.gitea/workflows/traceability-check.yml
T
dtourolle 9c75e74ea3
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m52s
🏗️ Build and Test JellyTau / Supply Chain (push) Failing after 29s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m35s
Traceability Validation / Check Requirement Traces (push) Successful in 11s
Build & Release / Run Tests (push) Successful in 14m53s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m22s
Build & Release / Build Linux (push) Successful in 20m53s
Build & Release / Build Windows (push) Successful in 15m41s
Build & Release / Build Android (push) Successful in 30m46s
Build & Release / Create Release (push) Successful in 38s
fix(ci): give the builder image what linuxdeploy needs for the AppImage
The v0.10.0 release build failed in Build Linux after 16 minutes:

  failed to bundle project: xdg-open binary not found
  /usr/bin/xdg-open: No such file or directory

linuxdeploy embeds xdg-open into the AppImage and aborts the whole bundle
when it is absent. deb and rpm had already bundled fine; only AppImage
was affected.

This is the one failure tonight that building locally could not have
caught, and the reason is worth writing down: a developer machine is a
desktop and always has xdg-utils, so the AppImage builds there and fails
on a minimal server image. The asymmetry is the bug. Every other release
defect this evening was found by building locally first; this one needed
the runner.

xdg-utils, desktop-file-utils and zsync are added together rather than
one at a time. Each round trip costs an image rebuild plus a failed
release build, and those three are what linuxdeploy commonly reaches for
(xdg-open, desktop-file-validate, and zsync for delta updates).

Workflows move to jellytau-builder:2026.08.1, built and pushed with all
three verified present inside it before this commit.

ci-operations.md gains two things learned here: that an apt addition
invalidates the layer above the cargo-install steps, so it is a ~20 minute
rebuild rather than the ~2 minutes the trailing layer normally gives; and
that Tauri's AppImage bundler downloads linuxdeploy, AppRun and two plugin
scripts from GitHub during the build, so an AppImage build depends on
GitHub being reachable from the runner.
2026-08-22 02:52:32 +02:00

188 lines
6.7 KiB
YAML

name: Traceability Validation
on:
push:
branches:
- master
- main
- develop
pull_request:
branches:
- master
- main
- develop
jobs:
validate-traces:
runs-on: linux/amd64
name: Check Requirement Traces
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# bun is baked into jellytau-builder (see Dockerfile.builder); no setup-bun
# action needed — fetching it stalls on this Gitea runner.
- name: Install dependencies
run: bun install
- name: Extract traces
run: |
echo "🔍 Extracting requirement traces..."
bun run traces:json > traces-report.json
- name: Validate traces
run: |
set -e
echo "📊 Validating requirement traceability..."
echo ""
# Denominators come from docs/requirements.md at run time — NEVER
# hardcode them here. This step previously divided by frozen literals
# (UR/39, IR/24, DR/48, JA/3, total 114) while the file had grown to
# 211 requirements, so it reported 158% coverage and the threshold
# below could never trip. See docs/traceability-ci.md.
TOTAL_TRACES=$(jq '.totalTraces' traces-report.json)
COVERED=$(jq '.coverage.covered' traces-report.json)
TOTAL_REQS=$(jq '.coverage.total' traces-report.json)
COVERAGE=$(jq '.coverage.percent' traces-report.json)
echo "✅ TRACES Found: $TOTAL_TRACES"
echo ""
echo "📋 Coverage Summary (traced / defined):"
for T in UR IR DR JA; do
TRACED=$(jq --arg t "$T" '[.byType[$t][] | select(. != null)] | length' traces-report.json)
DEFINED=$(jq --arg t "$T" '.defined[$t]' traces-report.json)
echo " $T: $TRACED / $DEFINED"
done
echo ""
echo "📈 Overall Coverage: $COVERED / $TOTAL_REQS ($COVERAGE%)"
echo ""
# Traced IDs that requirements.md does not define (typo, or a deleted
# requirement). These do not count toward coverage.
ORPHANED=$(jq -c '.coverage.orphaned' traces-report.json)
if [ "$ORPHANED" != "[]" ]; then
echo "⚠️ Traced but not defined in requirements.md: $ORPHANED"
echo ""
fi
# A ratio above 100% means the computation is broken — the exact
# condition that hid the stale-denominator bug. Fail loudly.
if [ "$COVERAGE" -gt 100 ]; then
echo "❌ ERROR: Coverage ($COVERAGE%) exceeds 100% — the gate is miscomputing."
echo " Orphaned IDs: $ORPHANED"
exit 1
fi
# Minimum coverage. RATCHET POLICY: this number only ever goes UP.
#
# It sits a few points under the coverage actually achieved, so a real
# regression trips it. It was 50 while true coverage was 86%, which
# meant nearly half the matrix could rot before CI said a word — a
# gate that cannot fail is not a gate.
#
# When coverage rises durably, raise this to just under the new figure
# (`bun run traces:coverage` prints it). Never lower it to make a red
# build pass — add the missing TRACES comments instead.
#
# Keep in sync with MIN_COVERAGE_PERCENT in scripts/extract-traces.ts;
# scripts/extract-traces.test.ts fails if the two drift apart.
MIN_THRESHOLD=89
if [ "$COVERAGE" -lt "$MIN_THRESHOLD" ]; then
echo "❌ ERROR: Coverage ($COVERAGE%) is below minimum threshold ($MIN_THRESHOLD%)"
exit 1
fi
echo "✅ Coverage is acceptable ($COVERAGE% >= $MIN_THRESHOLD%)"
# Every ID named by a TRACES comment must be defined as a table row in
# docs/requirements.md. The extractor used to accept any well-formed ID
# silently, so a typo or a rename that missed a call site passed CI
# unnoticed (DR-189 and UT-188 lived in three source files, defined
# nowhere, for months). This covers UT/IT too, which the coverage
# orphan list above deliberately ignores.
- name: Validate requirement IDs
run: bun run traces:validate
- name: Check modified files
if: github.event_name == 'pull_request'
run: |
echo "🔍 Checking modified files for traces..."
echo ""
# Get changed files
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(ts|tsx|svelte|rs)$' || echo "")
if [ -z "$CHANGED" ]; then
echo "✅ No TypeScript/Rust files changed"
exit 0
fi
echo "📝 Changed files:"
echo "$CHANGED" | sed 's/^/ /'
echo ""
# Check each file
# Pipe into the loop instead of a here-string (<<<) so this step works
# under POSIX sh/dash, not just bash. Use `case` instead of `[[ == ]]`
# for the same reason. The loop runs in a subshell (so a counter var
# wouldn't survive), so we record warnings in a temp file and count it
# afterwards.
MISSING_FILE=$(mktemp)
echo "$CHANGED" | while IFS= read -r file; do
# Skip test files
case "$file" in
*.test.*) continue ;;
esac
if [ -f "$file" ]; then
if ! grep -q "TRACES:" "$file"; then
echo "⚠️ Missing TRACES: $file"
echo "$file" >> "$MISSING_FILE"
fi
fi
done
MISSING_TRACES=$(wc -l < "$MISSING_FILE" | tr -d ' ')
rm -f "$MISSING_FILE"
if [ "$MISSING_TRACES" -gt 0 ]; then
echo ""
echo "📝 Recommendation: Add TRACES comments to new/modified code"
echo " Format: // TRACES: UR-001, UR-002 | DR-003"
echo ""
echo "💡 For more info, see: scripts/README.md"
fi
- name: Generate full report
if: always()
run: |
echo "📄 Generating full traceability report..."
bun run traces:markdown
- name: Display report summary
if: always()
run: |
echo ""
echo "📊 Full Report Generated"
echo "📁 Location: docs/traceability.md"
echo ""
head -50 docs/traceability.md || true
- name: Save artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: traceability-reports
path: |
traces-report.json
docs/traceability.md
retention-days: 30