Merge feature/p0-clarification-followups into main
Adopt spec Phase-0 clarifications into the impl and reconcile CI seams; gate green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
12 files changed
+177
-41
No files matched your search
@@ -44,8 +44,27 @@ env:
|
||||
CLUSTER_AVAILABLE: ${{ vars.CLUSTER_AVAILABLE }}
|
||||
|
||||
jobs:
|
||||
# R-TEST-06 hard gate: the specification is read-only to agents. Fail the whole run
|
||||
# if the spec repo is modified or has drifted from ci/SPEC_PIN. Runs first; everything
|
||||
# else depends on it. Requires the spec repo checked out beside the impl repo — wired
|
||||
# for when Gitea hosts both. If the spec repo is unavailable the guard SKIPS (exit 3),
|
||||
# tolerated here (surfaced, never a fake green); a real violation (exit 1) blocks.
|
||||
spec-immutability:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # the implementation repo
|
||||
- name: Check out the read-only spec (sibling path)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ vars.SPEC_REPO_SLUG }} # e.g. Home-Network/home-network-k3s (set in Gitea)
|
||||
path: ../home-network-k3s
|
||||
fetch-depth: 0
|
||||
- name: Verify spec immutability (R-TEST-06)
|
||||
run: make -C ci spec-immutability || [ $? -eq 3 ]
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [spec-immutability]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 } # full history so touched-requirement diff works
|
||||
|
||||
+4
-1
@@ -11,11 +11,14 @@ CI_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help lint schema l0 l1 conformance gate all touched versions tools
|
||||
.PHONY: help spec-immutability lint schema l0 l1 conformance gate all touched versions tools
|
||||
|
||||
help: ## Show this help
|
||||
@awk 'BEGIN{FS=":.*##"} /^[a-zA-Z0-9_-]+:.*##/{printf " \033[36m%-14s\033[0m %s\n",$$1,$$2}' $(MAKEFILE_LIST)
|
||||
|
||||
spec-immutability: ## R-TEST-06: fail if the spec repo is modified or drifted from ci/SPEC_PIN
|
||||
@"$(CI_DIR)spec-immutability.sh"
|
||||
|
||||
lint: ## yaml/lint (no cluster; live now)
|
||||
@"$(CI_DIR)lint.sh"
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# The spec commit (home-network-k3s main) this implementation is built against.
|
||||
# R-TEST-06: agents MUST NOT modify the spec. The spec changes only via the human/
|
||||
# System Architect; each legitimate spec advance is adopted here by bumping this pin
|
||||
# in a reviewed change. CI (ci/spec-immutability.sh) fails if the spec repo is dirty
|
||||
# or its HEAD != this pin — catching any unauthorized/unadopted spec modification.
|
||||
a7fc19fad57c70e5c7b142afa142ac16e8b13091
|
||||
+16
-14
@@ -38,20 +38,22 @@ else
|
||||
log "no touched requirements detected — running full L2/L3 subset as a safety net"
|
||||
fi
|
||||
|
||||
# Contract we ASSUME of conformance/run.sh (flag for PM to reconcile):
|
||||
# --layer l2,l3 behavioral + fault-injection layers
|
||||
# --trigger in-cluster-job create/await the in-cluster Chainsaw Job (Job holds
|
||||
# cluster creds via its scoped SA; CI uses only a
|
||||
# narrow trigger credential, referenced by SECRET NAME
|
||||
# in the workflow: CONFORMANCE_TRIGGER_KUBECONFIG)
|
||||
# --requirements R-A,R-B touched subset (R-TEST-03)
|
||||
# --junit <file> --coverage <dir> collected artifacts feed the gate
|
||||
"$CONFORMANCE_CMD" \
|
||||
--layer l2,l3 \
|
||||
--trigger in-cluster-job \
|
||||
"${req_args[@]}" \
|
||||
--junit "${JUNIT_DIR}/l2l3.junit.xml" \
|
||||
--coverage "${COVERAGE_DIR}"
|
||||
# RECONCILED (PM 2026-08-16): run.sh writes artifacts to conformance/_artifacts/ and does
|
||||
# NOT accept --junit/--coverage. This stage is CLUSTER-GATED (only runs when
|
||||
# CLUSTER_AVAILABLE=true, Phase 1+), so its trigger path is confirmed against a real
|
||||
# cluster in Phase 1 — TODO(Phase 1): drive the in-cluster Chainsaw Job
|
||||
# (conformance/incluster/chainsaw-job.yaml) under the narrow CONFORMANCE_TRIGGER_KUBECONFIG,
|
||||
# and confirm multi-layer (l2 + l3) selection syntax against run.sh. Artifacts are then
|
||||
# COLLECTED from the fixed location below.
|
||||
"$CONFORMANCE_CMD" --layer l2,l3 "${req_args[@]}"
|
||||
|
||||
CONF_ART="$(dirname "$CONFORMANCE_CMD")/_artifacts"
|
||||
if [ -f "${CONF_ART}/report.junit.xml" ]; then
|
||||
cp "${CONF_ART}/report.junit.xml" "${JUNIT_DIR}/l2l3.junit.xml"
|
||||
fi
|
||||
if [ -d "${CONF_ART}/coverage" ]; then
|
||||
cp -r "${CONF_ART}/coverage/." "${COVERAGE_DIR}/"
|
||||
fi
|
||||
|
||||
ok "L2/L3 conformance passed for touched subset"
|
||||
summary "- conformance-l2-l3: **passed**"
|
||||
@@ -37,6 +37,7 @@ run_stage() {
|
||||
summary "## Merge gate (R-TEST-03)"
|
||||
summary ""
|
||||
|
||||
run_stage spec-immutability "${CI_DIR}/spec-immutability.sh"
|
||||
run_stage lint "${CI_DIR}/lint.sh"
|
||||
run_stage l0 "${CI_DIR}/l0.sh"
|
||||
run_stage l1 "${CI_DIR}/l1.sh"
|
||||
@@ -46,6 +47,14 @@ echo >&2
|
||||
log "==== gate decision ===="
|
||||
BLOCK=0
|
||||
|
||||
# spec-immutability (R-TEST-06): a FAIL always blocks (the spec was modified/drifted).
|
||||
# A skip means the spec repo wasn't available to verify — surfaced, not green.
|
||||
case "${OUTCOME[spec-immutability]}" in
|
||||
fail) err "spec-immutability: FAILED -> blocks merge (R-TEST-06)"; BLOCK=1 ;;
|
||||
skip) warn "spec-immutability: skipped (spec repo unavailable) — not verified" ;;
|
||||
pass) ok "spec-immutability: green" ;;
|
||||
esac
|
||||
|
||||
# L0/L1 stages: any failure blocks. A SKIP here means a seam is unwired — in
|
||||
# Phase 0 that is tolerated (placeholders), but surfaced.
|
||||
for s in lint l0 l1; do
|
||||
|
||||
@@ -28,16 +28,19 @@ if [ "${#reqs[@]}" -gt 0 ]; then
|
||||
req_args=(--requirements "$(IFS=,; echo "${reqs[*]}")")
|
||||
fi
|
||||
|
||||
# Contract we ASSUME of conformance/run.sh (flag for PM to reconcile):
|
||||
# --layer l1 select the L1 rendering layer (no cluster)
|
||||
# --requirements R-A,R-B filter to the touched subset
|
||||
# --junit <file> write JUnit XML
|
||||
# --coverage <dir> write the requirement-coverage report
|
||||
"$CONFORMANCE_CMD" \
|
||||
--layer l1 \
|
||||
"${req_args[@]}" \
|
||||
--junit "${JUNIT_DIR}/l1.junit.xml" \
|
||||
--coverage "${COVERAGE_DIR}"
|
||||
# RECONCILED (PM 2026-08-16): conformance/run.sh's real interface is `--layer <l1|l2|l3>`
|
||||
# [--requirements R-A,R-B]; it ALWAYS writes artifacts to a FIXED location
|
||||
# (conformance/_artifacts/) and does NOT accept --junit/--coverage. Invoke by layer +
|
||||
# touched subset, then COLLECT its artifacts into the CI artifact dirs.
|
||||
"$CONFORMANCE_CMD" --layer l1 "${req_args[@]}"
|
||||
|
||||
CONF_ART="$(dirname "$CONFORMANCE_CMD")/_artifacts"
|
||||
if [ -f "${CONF_ART}/report.junit.xml" ]; then
|
||||
cp "${CONF_ART}/report.junit.xml" "${JUNIT_DIR}/l1.junit.xml"
|
||||
fi
|
||||
if [ -d "${CONF_ART}/coverage" ]; then
|
||||
cp -r "${CONF_ART}/coverage/." "${COVERAGE_DIR}/"
|
||||
fi
|
||||
|
||||
ok "L1 rendering checks passed"
|
||||
summary "- l1-render: **passed**"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# ci/spec-immutability.sh — R-TEST-06 gate: the specification is read-only to agents.
|
||||
#
|
||||
# The spec lives in a SEPARATE repo (home-network-k3s). Agents implement against it and
|
||||
# never modify it; only the human/System Architect changes the spec. This gate fails CI if
|
||||
# the spec repo has uncommitted edits or its HEAD has drifted from the pinned commit in
|
||||
# ci/SPEC_PIN — catching any unauthorized (or un-adopted) spec change.
|
||||
#
|
||||
# Adopting a legitimate spec advance = bump ci/SPEC_PIN to the new spec main SHA in a
|
||||
# reviewed PR. That makes every spec movement explicit and gated, per docs/14 / R-TEST-06.
|
||||
#
|
||||
# Exit codes (per ci/gate.sh contract): 0 pass · 1 fail (blocks merge) · 3 skip (spec
|
||||
# repo not available to verify — honest skip, never fake green).
|
||||
set -uo pipefail
|
||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${CI_DIR}/lib.sh"
|
||||
set +e
|
||||
|
||||
PIN="$(grep -vE '^\s*#' "${CI_DIR}/SPEC_PIN" | tr -d '[:space:]')"
|
||||
: "${SPEC_REPO:=${REPO_ROOT}/../home-network-k3s}"
|
||||
|
||||
if [ -z "$PIN" ]; then err "ci/SPEC_PIN has no pinned SHA"; exit 1; fi
|
||||
if [ ! -d "${SPEC_REPO}/.git" ]; then
|
||||
skip "spec repo not present at ${SPEC_REPO} (set SPEC_REPO) — cannot verify immutability"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
dirty="$(git -C "$SPEC_REPO" status --porcelain 2>/dev/null)"
|
||||
head="$(git -C "$SPEC_REPO" rev-parse HEAD 2>/dev/null)"
|
||||
|
||||
rc=0
|
||||
if [ -n "$dirty" ]; then
|
||||
err "spec repo has UNCOMMITTED changes — the spec is read-only to agents (R-TEST-06):"
|
||||
printf '%s\n' "$dirty" >&2
|
||||
rc=1
|
||||
fi
|
||||
if [ "$head" != "$PIN" ]; then
|
||||
err "spec HEAD (${head:0:12}) != pinned (${PIN:0:12})."
|
||||
err " If this is a legitimate spec advance by the System Architect, adopt it by bumping ci/SPEC_PIN."
|
||||
err " Otherwise the spec was modified without authorization — reject."
|
||||
rc=1
|
||||
fi
|
||||
|
||||
[ "$rc" -eq 0 ] && ok "spec immutable: clean and at pinned ${PIN:0:12} (R-TEST-06)"
|
||||
exit "$rc"
|
||||
@@ -8,15 +8,20 @@
|
||||
# Output (stdout): newline-separated, sorted, unique R-* IDs. Nothing else goes
|
||||
# to stdout, so callers can `mapfile`/`$(...)` it directly.
|
||||
#
|
||||
# Detection = union of two signals:
|
||||
# 1. R-* tokens literally present in the diff of changed files (tests, policies,
|
||||
# and ServiceDefinitions carry their R-IDs per docs/14 + ADR-0011).
|
||||
# 2. A coarse path->requirement map for changes that don't self-declare IDs.
|
||||
# SOURCE OF TRUTH (reconciled, PM 2026-08-16): the conformance requirement TAGS are
|
||||
# authoritative. Each Chainsaw test declares the requirements it verifies via
|
||||
# `conformance.home.k3s/<R-ID>` labels; the coverage report (conformance/_artifacts/
|
||||
# coverage/coverage.json) is generated from those tags. This script's job is only to
|
||||
# select which tagged subset a change TOUCHES, then hand those IDs to
|
||||
# `conformance.sh --requirements ...` which runs exactly the tests carrying those tags.
|
||||
#
|
||||
# ASSUMPTION (flag for PM): the authoritative change->requirement mapping is
|
||||
# really WS-TEST's coverage tooling (tests tag their own R-IDs). This script is a
|
||||
# CI-side approximation good enough to *filter* the subset; at integration it
|
||||
# should defer to / cross-check the coverage report emitted by conformance/.
|
||||
# Detection = union of:
|
||||
# 1. R-* tokens literally present in the diff of changed files — including the
|
||||
# authoritative `conformance.home.k3s/R-*` tags on changed tests, and the R-IDs
|
||||
# that policies/ServiceDefinitions self-declare (docs/14 + ADR-0011).
|
||||
# 2. A coarse path->requirement fallback for changes that don't self-declare IDs.
|
||||
# The coarse map (2) NEVER overrides an explicit tag; it only widens coverage for
|
||||
# untagged changes so nothing silently escapes the gate.
|
||||
set -euo pipefail
|
||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Platform label/annotation convention (PROPOSED contract)
|
||||
# Platform label/annotation convention (RATIFIED)
|
||||
|
||||
These label keys are consumed by the Kyverno policies in this directory. The **values**
|
||||
are fixed by the spec; the **keys** are a convention this workstream (WS-TEST) proposes
|
||||
and are a **shared contract** with WS-NET (network/egress rendering), WS-SEC (auth), and
|
||||
WS-GITOPS (ServiceDefinition → manifest rendering). Per
|
||||
[`agents/coordination-protocol.md` §2] a contract like this needs the owning roles'
|
||||
sign-off before it is locked. **Escalated as an open question in the T-0.5 report.**
|
||||
are fixed by the spec; the **keys** are the platform convention, now **ratified in the
|
||||
spec** — `homelab.keithjackson.dev/{egress-class,auth-mode,app-namespace,baseline}` are
|
||||
documented in `docs/03-networking-isolation-and-egress.md` ("Platform label-key
|
||||
convention") as the contract that network policies, Kyverno rules, and renderers key off.
|
||||
Shared contract with WS-NET (egress rendering), WS-SEC (auth), and WS-GITOPS (rendering).
|
||||
|
||||
| Label key | Applied to | Values | Fixed by | Used by policy |
|
||||
|---|---|---|---|---|
|
||||
|
||||
@@ -17,7 +17,7 @@ kind: ValidatingPolicy
|
||||
metadata:
|
||||
name: validate-servicedefinition
|
||||
annotations:
|
||||
homelab.keithjackson.dev/requirements: R-GITOPS-02
|
||||
homelab.keithjackson.dev/requirements: R-GITOPS-02,R-STATE-02,R-STATE-03
|
||||
spec:
|
||||
rules:
|
||||
- name: servicedefinition-shape
|
||||
@@ -63,3 +63,17 @@ spec:
|
||||
check:
|
||||
~.storage:
|
||||
(class == 'longhorn' || class == 'bulk'): true
|
||||
# docs/13 v1.1: longhorn (replicated, irreplaceable) volumes MUST be sized (R-STATE-02).
|
||||
# bulk may omit size (external mount, sized outside the cluster).
|
||||
- message: "a `longhorn` volume requires an explicit `size` (docs/13 v1.1; R-STATE-02)"
|
||||
check:
|
||||
~.storage:
|
||||
(class != 'longhorn' || size != null): true
|
||||
- name: servicedefinition-backup
|
||||
# docs/13 v1.1: irreplaceable (longhorn-tier) state MUST NOT be `reacquirable` — it must
|
||||
# reference an off-cluster backup schedule (R-STATE-03). Only bulk-tier data may be reacquirable.
|
||||
assert:
|
||||
all:
|
||||
- message: "backup.policy 'reacquirable' is not allowed when any volume is on the longhorn (irreplaceable) tier; reference an off-cluster schedule (docs/13 v1.1; R-STATE-03)"
|
||||
check:
|
||||
(backup.policy == 'reacquirable' && storage != null && contains(storage[].class, 'longhorn')): false
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://home.keithjackson.dev/schemas/servicedefinition.schema.json",
|
||||
"title": "ServiceDefinition",
|
||||
"description": "Operator-facing onboarding contract for the home k3s + Longhorn platform. One ServiceDefinition per file under catalog/. Captures INTENT; the platform compiles it into concrete resources (see schemas/renderer-contract.md). Canonical source of truth: spec docs/13-service-catalog.md. Serves R-GITOPS-02; ties into R-NET-*, R-STATE-*, R-TLS-*, R-DNS-*, R-OBS-*, R-UPD-*, R-IDP-*. Schema version: 1.0.0.",
|
||||
"description": "Operator-facing onboarding contract for the home k3s + Longhorn platform. One ServiceDefinition per file under catalog/. Captures INTENT; the platform compiles it into concrete resources (see schemas/renderer-contract.md). Canonical source of truth: spec docs/13-service-catalog.md. Serves R-GITOPS-02; ties into R-NET-*, R-STATE-*, R-TLS-*, R-DNS-*, R-OBS-*, R-UPD-*, R-IDP-*. Schema version: 1.1.0 (adds shape invariant exposure:public => auth.mode!=none, docs/13 v1.1 / docs/15 / R-IDP-02).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "source"],
|
||||
@@ -244,6 +244,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$comment": "public exposure must not be unauthenticated: an explicit auth.mode 'none' is disallowed on public services (docs/13 v1.1, docs/15, R-IDP-02). Absent auth defaults to forward-auth at render time, so it is permitted here.",
|
||||
"if": {
|
||||
"required": ["exposure"],
|
||||
"properties": { "exposure": { "const": "public" } }
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"auth": {
|
||||
"properties": { "mode": { "not": { "const": "none" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# schemas/validate.sh — validate ServiceDefinition YAML against the JSON Schema.
|
||||
# The CI L0/L1 gate's entrypoint (ci/lib.sh SCHEMA_VALIDATE_CMD). No cluster needed
|
||||
# (R-TEST-02). Usage:
|
||||
# validate.sh <file>... # exit 0 = all valid, non-zero = a file failed
|
||||
# validate.sh --version # print the validator version
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCHEMA="${HERE}/servicedefinition.schema.json"
|
||||
|
||||
if [ "${1:-}" = "--version" ]; then
|
||||
exec check-jsonschema --version
|
||||
fi
|
||||
[ "$#" -gt 0 ] || { echo "usage: validate.sh <file>... | --version" >&2; exit 2; }
|
||||
exec check-jsonschema --schemafile "$SCHEMA" "$@"
|
||||
Reference in new issue
Block a user