Files
home-network-k3s-impl/ci
jakman85andClaude Opus 5 f7d3884ad2 fix(ci): scope the requirement matcher to the diff and to non-doc files
`ci/touched-requirements.sh` documents its detection as "R-* tokens literally
present in the diff of changed files". It did not do that — it grepped the ENTIRE
CONTENT of every changed file. So a change inherited every requirement those files
had ever mentioned in prose, and `--require-all` then blocked the merge on
requirements the author never went near.

On this branch that meant editing the decision log and the tofu docs dragged in
five unrelated requirements, all on untouched lines, all legitimately uncovered:

  [coverage] REQUIRE: FAILED — required requirement(s) uncovered: [5 IDs]

The practical effect was that the PM could not append to the decision log without
failing CI — which is a fair part of why the log went stale in the first place.
This is the defect already logged as "FOLLOW-UP (WS-TEST): fix the matcher so real
requirements need not be exempted".

Two scoping rules, both learned from the gate blocking merges it should not have:

  1. Scan the ADDED/REMOVED lines of the diff, as the header always claimed.
     Removed lines count too — deleting a test's requirement tag, or a policy's
     self-declared ID, touches that requirement just as much as adding one.

  2. Skip `*.md`. An ID in prose is a REFERENCE, not a declaration: explaining a
     requirement is not changing behaviour against it. Rule 1 alone is not enough —
     a commit that documents a mis-attribution names the IDs involved and thereby
     re-triggers the very mis-attribution it fixes, and the decision log can never
     record which requirements a change concerned.

This TIGHTENS attribution rather than weakening the gate:
  - no requirement is exempted, and DOCUMENTED_GAPS is untouched;
  - authoritative declarations (`conformance.home.k3s/R-*` tags, Kyverno policies,
    ServiceDefinitions) all live in YAML, on lines a real change must touch;
  - the coarse path->requirement fallback is unchanged and file-type agnostic, so
    the doc subtrees that matter (conformance/, schemas/, catalog/, clusters/, ci/)
    stay covered either way.

Alternatives rejected: adding the five IDs to DOCUMENTED_GAPS genuinely weakens the
gate; scrubbing requirement IDs out of prose (precedent f9e9640) would mean editing
the decision log's own history to appease a matcher.

Gate semantics change — flagged in DECISIONS-LOG.md for review.

Also drops three R-TEST-06 mentions this branch had added to prose and refreshes
the now-stale "gate is RED" notes.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-27 00:06:28 -05:00
..

ci/ — portable merge gate (runnable today, without Gitea)

Task T-0.2 · R-TEST-02 (reproducible, single-command) · Owner WS-GITOPS (CI).

The same merge gate the Gitea Actions workflow runs (.gitea/workflows/), as plain scripts + a Makefile so the gates are runnable now, before any Gitea host or cluster exists. CI and a laptop call the identical scripts, so make -C ci gate == the CI merge gate.

Quick start

make -C ci gate        # the single command: run every stage + the R-TEST-03 decision
make -C ci help        # list targets
make -C ci tools       # which pinned tools are installed locally
make -C ci touched     # print the touched-requirement subset for this branch vs main

Individual stages:

make -C ci lint        # yaml/lint                          (no cluster; live now)
make -C ci l0          # ServiceDefinition schema + Kyverno  (no cluster; live now)
make -C ci l1          # L1 rendering checks                 (no cluster; live now)
make -C ci conformance # L2/L3 in-cluster Chainsaw Job       (SKIPS unless CLUSTER_AVAILABLE=true)

Exit-code contract

Every stage script uses: 0 = pass · 1 = fail (blocks merge) · 3 = SKIPPED (cluster-gated with no cluster, or an unwired seam / missing tool). A 3 is never counted as green — that is how the gate stays honest in Phase 0 (no fake green).

Reproducibility (R-TEST-02)

  • Version-pinned: all tool versions live in versions.env — the single source of truth shared by the local scripts and the CI installer. Bumping a version is a normal feature-branch → main change.
  • Single command, environment-agnostic: make -C ci gate on any machine with the pinned tools.
  • Deterministic: no cluster state is assumed; L2/L3 SKIP unless a cluster is explicitly declared.

Files

ci/
  Makefile                 targets: lint · l0 · l1 · conformance · gate · touched · tools · versions
  versions.env             PINNED tool versions (R-TEST-02) — single source of truth
  lib.sh                   shared helpers: logging, tool checks, cluster detection, artifact paths
  yamllint.yaml            lint config for the yaml/lint stage
  lint.sh                  Stage: yaml/lint
  l0.sh                    Stage: ServiceDefinition schema validation + Kyverno L0 policy gate
  l1.sh                    Stage: L1 rendering checks (Chainsaw L1 via the conformance runner)
  conformance.sh           Stage: L2/L3 in-cluster Chainsaw Job (trigger + collect)
  touched-requirements.sh  compute the R-* subset a change touches (drives the R-TEST-03 filter)
  gate.sh                  run all stages + the merge decision (the single command)
  README.md                this file

Integration seams — entrypoints owned by OTHER agents

This gate is an orchestrator. It does not implement schema validation, Kyverno policies, or the conformance runner — those are other workstreams' deliverables. Each seam is env-overridable so the owning agent / PM can reconcile the real entrypoint without editing these scripts. Defaults below are this workstream's assumptions — flagged for the PM to verify at integration.

Seam Owner / task Env override Assumed default Assumed interface
ServiceDefinition schema validator WS-GITOPS · T-0.3 (schemas/) SCHEMA_VALIDATE_CMD schemas/validate.sh validate.sh catalog/*.yaml → exit 0/1; --version supported
Kyverno L0 policies + tests WS-TEST · T-0.5 (platform/kyverno/) KYVERNO_POLICY_DIR platform/kyverno contains kyverno-test.yaml files discoverable by kyverno test <dir>
Conformance runner (Chainsaw) WS-TEST · T-0.4 (conformance/) CONFORMANCE_CMD conformance/run.sh run.sh --layer <l1|l2,l3> [--requirements R-A,R-B] [--trigger in-cluster-job] --junit <file> --coverage <dir>
Catalog (ServiceDefinitions) WS-APPS CATALOG_DIR catalog *.yaml ServiceDefinition files

Artifact locations (git-ignored): JUnit → results/junit/*.xml; coverage → reports/coverage/**. Override with JUNIT_DIR / COVERAGE_DIR if the runner writes elsewhere.

A seam that is missing or not executable reports SKIPPED (exit 3), not a false pass — so during parallel Phase 0 development the gate is green-honest, and each seam goes live automatically the moment its owner delivers a runnable entrypoint.

Cluster gating

CLUSTER_AVAILABLE=true (env locally, vars.CLUSTER_AVAILABLE in Gitea) makes the L2/L3 stage run and be required; anything else makes it SKIP (Phase 0). REQUIRE_CLUSTER_TESTS=true (Phase 1+) turns any unverified cluster-needing touched requirement into a hard block.