ci: exclude comment-only prose from touched-requirements scan; gate.yml -> make -C ci gate
HAND-OFF 1 (make -C ci gate parity). touched-requirements.sh gained a third scoping rule: after stripping the diff marker, whole-line YAML/shell `#` comments are dropped BEFORE the R-ID grep. A requirement cited only in a code-file comment is prose (a REFERENCE), not a declaration -- the same logic rule 2 already applies to `*.md`. This unblocks the self-referential trap where editing gate.yml's header (which cites R-TEST-06, a known-uncovered ID enforced by the spec-immutability STAGE, not a Chainsaw test) re-attributed R-TEST-06 so the coverage `--require` check false-blocked the merge. Conservative: only whole-line comments are dropped; an R-ID on a real code/spec line is still detected, and authoritative declarations never live in a bare comment. gate.yml now invokes the aggregate `make -C ci gate` (ci/gate.sh) instead of the individual stage scripts, so CI runs byte-for-byte the same gate a human runs in the devcontainer (R-TEST-02) and applies the R-TEST-03 decision. A BLOCKED gate exits non-zero and fails the job. The cluster-gated L2/L3 stage SKIPS honestly (no CLUSTER_AVAILABLE). spec-immutability is retained as an explicit early UI step. Adds ci/tests/touched-requirements.test.sh (+ `make -C ci test`) asserting rule 3: comment-only R-IDs (added or removed) are not attributed while real-line R-IDs are. ci/lib.sh REPO_ROOT is now env-overridable so tests can run the real script against a throwaway git repo (default unchanged). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
b2752aec48
commit
27bc02471c
5 files changed
+181
-32
No files matched your search
+38
-22
@@ -1,19 +1,27 @@
|
|||||||
# .gitea/workflows/gate.yml — the ENFORCED PR merge gate (Phase 6, WS-GITOSS).
|
# .gitea/workflows/gate.yml — the ENFORCED PR merge gate (Phase 6, WS-GITOSS).
|
||||||
#
|
#
|
||||||
# This is the LIVE gate wired to the in-cluster Gitea Actions runner (ns gitea-actions,
|
# This is the LIVE gate wired to the in-cluster Gitea Actions runner (ns gitea-actions,
|
||||||
# Docker executor + DinD). It runs the STATIC layers of the conformance gate — the ones
|
# Docker executor + DinD). It invokes the AGGREGATE merge gate — `make -C ci gate` — so CI
|
||||||
# docs/14 says run "without a cluster": spec-immutability (best-effort), lint, Kyverno L0,
|
# runs BYTE-FOR-BYTE the same command a human runs in the devcontainer (R-TEST-02): the
|
||||||
# the render==golden reproducibility check, and L1 offline render checks. It is the single
|
# runner just installs the pinned tools from ci/versions.env first. `make -C ci gate`
|
||||||
# required status check on `main`
|
# (ci/gate.sh) runs every stage in order — spec-immutability, lint, Kyverno L0,
|
||||||
# (see branch protection). `make -C ci lint l0 render-golden l1` here is byte-identical to a laptop run
|
# render==golden, L1 offline render — and applies the R-TEST-03 decision, printing
|
||||||
# (R-TEST-02) — the runner just installs the pinned tools from ci/versions.env first.
|
# "MERGE GATE: PASS" / "MERGE GATE: BLOCKED". It is the single required status check on
|
||||||
|
# `main` (see branch protection). A non-zero exit fails the job, so a BLOCKED gate blocks
|
||||||
|
# the merge.
|
||||||
#
|
#
|
||||||
# The L2/L3 CONFORMANCE layer (behavioural + fault-injection, needs a live cluster) is
|
# The cluster-gated L2/L3 conformance stage SKIPS honestly here (exit 3, surfaced not faked
|
||||||
# NOT wired yet — it is a deliberate design decision (a CI toolchain image + a scoped
|
# — HARD RULE 4) because this runner declares no cluster: `make -C ci gate` does not set
|
||||||
# in-cluster kubeconfig/RBAC for the runner). It is present below as an `if: false`
|
# CLUSTER_AVAILABLE, so gate.sh reports the touched requirements UNVERIFIED without blocking
|
||||||
# placeholder so the shape is visible and the wiring is a one-line flip once decided.
|
# Phase-0/6 static work. L2/L3 run as an IN-CLUSTER Chainsaw Job (its own scoped SA; CI holds
|
||||||
# The full pipeline blueprint (spec-checkout, per-layer jobs, decision job) is retained
|
# no broad kubeconfig) driven by the separate `conformance` job below.
|
||||||
# dormant in merge-gate.yaml.
|
#
|
||||||
|
# NOTE (R-TEST-06 / gate-semantics): spec-immutability is ALSO surfaced as its own explicit
|
||||||
|
# early step (below) so its result is a distinct, tolerant status in the Gitea UI; gate.sh
|
||||||
|
# re-runs it as its first stage, which is the authoritative one. `make -C ci gate` here is
|
||||||
|
# the byte-identical laptop gate; the extra pre-step is a read-only git-diff check and is
|
||||||
|
# purely additive. The full pipeline blueprint (spec-checkout, per-layer jobs, decision job)
|
||||||
|
# is retained dormant in merge-gate.yaml.
|
||||||
---
|
---
|
||||||
name: gate
|
name: gate
|
||||||
|
|
||||||
@@ -47,19 +55,27 @@ jobs:
|
|||||||
./.gitea/workflows/scripts/install-tools.sh l0
|
./.gitea/workflows/scripts/install-tools.sh l0
|
||||||
./.gitea/workflows/scripts/install-tools.sh l1
|
./.gitea/workflows/scripts/install-tools.sh l1
|
||||||
|
|
||||||
# R-TEST-06: the spec is read-only to agents. The spec repo is NOT checked out into
|
# R-TEST-06: the spec is read-only to agents. An EXPLICIT early surfacing of the
|
||||||
# CI here (it is a separate repo; SPEC_REPO_SLUG is not wired into this runner), so
|
# spec-immutability check, so its result is a distinct, tolerant status line in the
|
||||||
# the guard honestly SKIPS (exit 3) rather than faking green. A real drift/edit of a
|
# Gitea UI before the (longer) aggregate gate log. The spec repo is NOT checked out
|
||||||
# co-located spec would exit 1 and fail. Tolerated: exit 3 only.
|
# into CI here (separate repo; SPEC_REPO_SLUG not wired into this runner), so the guard
|
||||||
# Call the script DIRECTLY, not via `make`: GNU make masks a recipe's exit code as
|
# honestly SKIPS (exit 3) rather than faking green; a real drift/edit of a co-located
|
||||||
# its own 2 on failure, so `make spec-immutability || [ $? -eq 3 ]` would see 2 and
|
# spec exits 1 and fails. Tolerated: exit 3 only. Call the script DIRECTLY, not via
|
||||||
# fail. The script itself exits 3 on an honest skip (spec repo not checked out here),
|
# `make spec-immutability`: GNU make masks a recipe's exit code as its own 2, so
|
||||||
# which we tolerate; a real drift/edit exits 1 and fails the gate.
|
# `make spec-immutability || [ $? -eq 3 ]` would see 2 and fail. The AUTHORITATIVE
|
||||||
|
# spec-immutability run is the FIRST stage of `make -C ci gate` below (gate.sh calls
|
||||||
|
# the script directly, so it sees exit 3 → SKIP, not a mask); this pre-step is a cheap,
|
||||||
|
# read-only git-diff duplicate that only adds early UI signal.
|
||||||
- name: Spec immutability (R-TEST-06, skips if spec repo absent)
|
- name: Spec immutability (R-TEST-06, skips if spec repo absent)
|
||||||
run: ./ci/spec-immutability.sh || [ "$?" -eq 3 ]
|
run: ./ci/spec-immutability.sh || [ "$?" -eq 3 ]
|
||||||
|
|
||||||
- name: Static gate — lint + Kyverno L0 + render==golden + L1 render
|
# Aggregate merge gate: byte-for-byte the command a human runs in the devcontainer
|
||||||
run: make -C ci lint l0 render-golden l1
|
# (`make -C ci gate` -> ci/gate.sh). Runs spec-immutability, lint, Kyverno L0,
|
||||||
|
# render==golden and L1 render, then the R-TEST-03 decision. Exits non-zero on a
|
||||||
|
# BLOCKED gate, which fails this job. The cluster-gated L2/L3 stage SKIPS here (no
|
||||||
|
# CLUSTER_AVAILABLE) — surfaced as UNVERIFIED, never faked green (see header).
|
||||||
|
- name: Merge gate — make -C ci gate
|
||||||
|
run: make -C ci gate
|
||||||
|
|
||||||
# NOTE: no actions/upload-artifact step. Gitea Actions rejects upload-artifact@v4
|
# NOTE: no actions/upload-artifact step. Gitea Actions rejects upload-artifact@v4
|
||||||
# ("not supported on GHES" — Gitea does not implement the artifact-v2 backend), which
|
# ("not supported on GHES" — Gitea does not implement the artifact-v2 backend), which
|
||||||
|
|||||||
+7
-3
@@ -1,8 +1,9 @@
|
|||||||
# ci/Makefile — portable, single-command local equivalent of the Gitea Actions
|
# ci/Makefile — portable, single-command local equivalent of the Gitea Actions
|
||||||
# merge gate (R-TEST-02: the gates are runnable TODAY, without Gitea).
|
# merge gate (R-TEST-02: the gates are runnable TODAY, without Gitea).
|
||||||
#
|
#
|
||||||
# The Gitea Actions workflow (.gitea/workflows/merge-gate.yaml) calls these exact
|
# The LIVE Gitea Actions workflow (.gitea/workflows/gate.yml) invokes `make -C ci gate`
|
||||||
# scripts, so `make -C ci gate` locally == the CI merge gate. Run from anywhere:
|
# directly, so `make -C ci gate` locally IS the CI merge gate — byte-for-byte. Run from
|
||||||
|
# anywhere:
|
||||||
# make -C ci gate
|
# make -C ci gate
|
||||||
#
|
#
|
||||||
# Every stage is a thin wrapper over a ci/*.sh script; see ci/README.md.
|
# Every stage is a thin wrapper over a ci/*.sh script; see ci/README.md.
|
||||||
@@ -11,7 +12,7 @@ CI_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help spec-immutability lint schema l0 render-golden l1 conformance gate all touched versions tools
|
.PHONY: help spec-immutability lint schema l0 render-golden l1 conformance gate all touched versions tools test
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@awk 'BEGIN{FS=":.*##"} /^[a-zA-Z0-9_-]+:.*##/{printf " \033[36m%-14s\033[0m %s\n",$$1,$$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN{FS=":.*##"} /^[a-zA-Z0-9_-]+:.*##/{printf " \033[36m%-14s\033[0m %s\n",$$1,$$2}' $(MAKEFILE_LIST)
|
||||||
@@ -42,6 +43,9 @@ all: gate ## Alias for `gate`
|
|||||||
touched: ## Print the touched-requirement subset for the current branch vs main
|
touched: ## Print the touched-requirement subset for the current branch vs main
|
||||||
@"$(CI_DIR)touched-requirements.sh"
|
@"$(CI_DIR)touched-requirements.sh"
|
||||||
|
|
||||||
|
test: ## Run ci/ script unit tests (no cluster; e.g. touched-requirements content scan)
|
||||||
|
@for t in "$(CI_DIR)"tests/*.test.sh; do echo "== $$t"; bash "$$t"; done
|
||||||
|
|
||||||
versions: ## Print pinned tool versions (R-TEST-02)
|
versions: ## Print pinned tool versions (R-TEST-02)
|
||||||
@cat "$(CI_DIR)versions.env"
|
@cat "$(CI_DIR)versions.env"
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# --- Paths -------------------------------------------------------------------
|
# --- Paths -------------------------------------------------------------------
|
||||||
# CI_DIR = this ci/ directory; REPO_ROOT = its parent (impl repo root).
|
# CI_DIR = this ci/ directory (always the real script location). REPO_ROOT = its
|
||||||
|
# parent (impl repo root) by default, but OVERRIDABLE via the environment so the
|
||||||
|
# scripts can be pointed at a throwaway git repo under test (see ci/tests/) without
|
||||||
|
# copying them out of the tree. CI and normal runs leave it unset -> unchanged.
|
||||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_ROOT="$(cd "${CI_DIR}/.." && pwd)"
|
: "${REPO_ROOT:=$(cd "${CI_DIR}/.." && pwd)}"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${CI_DIR}/versions.env"
|
source "${CI_DIR}/versions.env"
|
||||||
|
|
||||||
|
|||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ci/tests/touched-requirements.test.sh — regression tests for the content scan in
|
||||||
|
# ci/touched-requirements.sh, focused on rule 3: R-IDs cited only in a whole-line
|
||||||
|
# YAML/shell `#` comment are PROSE (a reference), not a declaration, and must NOT be
|
||||||
|
# attributed — while R-IDs on real code/spec lines still ARE.
|
||||||
|
#
|
||||||
|
# The tests point the real script at a THROWAWAY git repo via the REPO_ROOT override
|
||||||
|
# (ci/lib.sh honours a pre-set REPO_ROOT); the script itself is run from its true
|
||||||
|
# location so it still sources the real lib.sh. No network, no cluster.
|
||||||
|
#
|
||||||
|
# Run: ci/tests/touched-requirements.test.sh (or `make -C ci test`)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SCRIPT="${CI_DIR}/touched-requirements.sh"
|
||||||
|
|
||||||
|
PASS=0 FAIL=0
|
||||||
|
ok() { printf ' ok %s\n' "$*"; PASS=$((PASS+1)); }
|
||||||
|
bad() { printf ' FAIL %s\n' "$*"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
# assert_absent <needle> <haystack> <msg>
|
||||||
|
assert_absent() {
|
||||||
|
if grep -qxF "$1" <<<"$2"; then bad "$3 (found unexpected '$1' in: $(tr '\n' ' ' <<<"$2"))";
|
||||||
|
else ok "$3"; fi
|
||||||
|
}
|
||||||
|
# assert_present <needle> <haystack> <msg>
|
||||||
|
assert_present() {
|
||||||
|
if grep -qxF "$1" <<<"$2"; then ok "$3";
|
||||||
|
else bad "$3 (missing '$1' from: $(tr '\n' ' ' <<<"$2"))"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
git -C "$TMP" init -q -b main
|
||||||
|
git -C "$TMP" config user.email [email protected]
|
||||||
|
git -C "$TMP" config user.name t
|
||||||
|
git -C "$TMP" config commit.gpgsign false # throwaway repo: never sign (ignore host gpg config)
|
||||||
|
mkdir -p "$TMP/notes" # a path OUTSIDE path_requirements() so we isolate the content scan
|
||||||
|
echo "baseline" > "$TMP/notes/base.txt"
|
||||||
|
git -C "$TMP" add -A && git -C "$TMP" commit -qm baseline
|
||||||
|
|
||||||
|
git -C "$TMP" checkout -q -b feature
|
||||||
|
|
||||||
|
# A YAML file: an R-ID in a whole-line comment (must be IGNORED) and an R-ID on a real
|
||||||
|
# value line (must be DETECTED). Also an indented comment, to prove leading space is fine.
|
||||||
|
cat > "$TMP/notes/example.yaml" <<'YAML'
|
||||||
|
# This header explains R-DOC-01 and must NOT be attributed.
|
||||||
|
kind: Thing
|
||||||
|
metadata:
|
||||||
|
name: implements-R-REAL-01
|
||||||
|
spec:
|
||||||
|
note: value # trailing inline comment mentioning R-INLINE-01 (conservatively still seen)
|
||||||
|
# deeply indented comment about R-DOC-02 — also NOT attributed
|
||||||
|
YAML
|
||||||
|
|
||||||
|
# A shell file: a `#` comment R-ID (IGNORED) plus a real command line R-ID (DETECTED).
|
||||||
|
cat > "$TMP/notes/example.sh" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# doc: this script relates to R-SHDOC-01 (comment only — ignored)
|
||||||
|
echo "runs check for R-SHREAL-01"
|
||||||
|
SH
|
||||||
|
|
||||||
|
git -C "$TMP" add -A && git -C "$TMP" commit -qm feature
|
||||||
|
|
||||||
|
OUT="$(REPO_ROOT="$TMP" BASE_REF=main "$SCRIPT" 2>/dev/null)"
|
||||||
|
printf 'touched set:\n%s\n---\n' "$OUT"
|
||||||
|
|
||||||
|
# Comment-only citations are dropped (rule 3):
|
||||||
|
assert_absent "R-DOC-01" "$OUT" "whole-line YAML comment R-ID is NOT attributed"
|
||||||
|
assert_absent "R-DOC-02" "$OUT" "indented YAML comment R-ID is NOT attributed"
|
||||||
|
assert_absent "R-SHDOC-01" "$OUT" "shell '#' comment R-ID is NOT attributed"
|
||||||
|
# Real code/spec lines still count:
|
||||||
|
assert_present "R-REAL-01" "$OUT" "R-ID on a real YAML value line IS attributed"
|
||||||
|
assert_present "R-SHREAL-01" "$OUT" "R-ID on a real shell command line IS attributed"
|
||||||
|
# Conservative: a trailing inline comment on a REAL line is still seen (only whole-line
|
||||||
|
# comments are dropped) — documents the intentional boundary of the fix.
|
||||||
|
assert_present "R-INLINE-01" "$OUT" "inline comment on a real line is (conservatively) still seen"
|
||||||
|
|
||||||
|
# Removed comment lines are also excluded, while removed real lines still count. The file
|
||||||
|
# must EXIST at the base, so we diff the removal against the commit that added it.
|
||||||
|
git -C "$TMP" checkout -q -b feature2 main
|
||||||
|
cat > "$TMP/notes/todelete.yaml" <<'YAML'
|
||||||
|
# will delete a comment citing R-DELDOC-01 and a real line citing R-DELREAL-01
|
||||||
|
kind: Thing
|
||||||
|
name: implements-R-DELREAL-01
|
||||||
|
YAML
|
||||||
|
git -C "$TMP" add -A && git -C "$TMP" commit -qm add
|
||||||
|
ADD_SHA="$(git -C "$TMP" rev-parse HEAD)"
|
||||||
|
git -C "$TMP" rm -q "$TMP/notes/todelete.yaml" && git -C "$TMP" commit -qm remove
|
||||||
|
OUT2="$(REPO_ROOT="$TMP" BASE_REF="$ADD_SHA" "$SCRIPT" 2>/dev/null)"
|
||||||
|
printf 'touched set (removal):\n%s\n---\n' "$OUT2"
|
||||||
|
assert_absent "R-DELDOC-01" "$OUT2" "REMOVED whole-line comment R-ID is NOT attributed"
|
||||||
|
assert_present "R-DELREAL-01" "$OUT2" "REMOVED real-line R-ID IS attributed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$FAIL" -eq 0 ]; then echo "touched-requirements tests: PASS ($PASS assertions)"; exit 0;
|
||||||
|
else echo "touched-requirements tests: FAIL ($FAIL failed, $PASS passed)"; exit 1; fi
|
||||||
@@ -49,7 +49,7 @@ changed_files() {
|
|||||||
# The ADDED/REMOVED lines of the change, for the R-ID content scan — excluding
|
# The ADDED/REMOVED lines of the change, for the R-ID content scan — excluding
|
||||||
# documentation.
|
# documentation.
|
||||||
#
|
#
|
||||||
# TWO scoping rules, both learned from the gate blocking merges it should not have:
|
# THREE scoping rules, all learned from the gate blocking merges it should not have:
|
||||||
#
|
#
|
||||||
# 1. Scan the DIFF, not whole file contents. Grepping whole files meant a change
|
# 1. Scan the DIFF, not whole file contents. Grepping whole files meant a change
|
||||||
# inherited every requirement those files had EVER mentioned, on lines the author
|
# inherited every requirement those files had EVER mentioned, on lines the author
|
||||||
@@ -66,14 +66,36 @@ changed_files() {
|
|||||||
# that matter (conformance/, schemas/, catalog/, clusters/, ci/) are covered by
|
# that matter (conformance/, schemas/, catalog/, clusters/, ci/) are covered by
|
||||||
# the path->requirement fallback regardless of file type.
|
# the path->requirement fallback regardless of file type.
|
||||||
#
|
#
|
||||||
|
# 3. Skip COMMENT-ONLY lines in non-Markdown files — rule 2's logic, applied to the
|
||||||
|
# prose that lives INSIDE code files. A requirement ID cited in a YAML/shell `#`
|
||||||
|
# comment is documentation, not a declaration, exactly like an ID in a `*.md` file.
|
||||||
|
# Rule 2 drops whole Markdown files, but a `.gitea/workflows/*.yml` or `ci/*.sh`
|
||||||
|
# HEADER that explains a requirement would otherwise re-attribute it. Concretely:
|
||||||
|
# gate.yml's header cites R-TEST-06 (enforced by the spec-immutability STAGE, not by
|
||||||
|
# any Chainsaw test, so a known-uncovered ID); before this rule, editing that comment
|
||||||
|
# re-attributed R-TEST-06 and the coverage `--require` check false-blocked the merge —
|
||||||
|
# the same self-referential trap rule 2 fixed for `*.md`. Applied in main()'s content
|
||||||
|
# scan (see there): after the diff marker is stripped, a line whose first non-blank
|
||||||
|
# character is `#` is dropped BEFORE the R-ID grep. Deliberately conservative — only
|
||||||
|
# WHOLE-LINE comments are dropped; an R-ID on a real code/spec line is still detected
|
||||||
|
# (authoritative declarations — test tags, policies, ServiceDefinitions — are never a
|
||||||
|
# bare comment), so this only narrows PROSE, never a declaration.
|
||||||
|
#
|
||||||
# Removed lines count: deleting a test's requirement tag, or a policy's self-declared
|
# Removed lines count: deleting a test's requirement tag, or a policy's self-declared
|
||||||
# ID, touches that requirement just as much as adding one.
|
# ID, touches that requirement just as much as adding one.
|
||||||
# `:(exclude)*.md` drops Markdown from the content scan (rule 2 above). Three-dot to
|
# `:(exclude)*.md` drops Markdown from the content scan (rule 2 above).
|
||||||
# match changed_files(): changes on this branch since it diverged from base.
|
# `:(exclude)ci/tests/` drops this dir's OWN unit-test fixtures: those files embed example
|
||||||
|
# R-IDs as DATA (asserting the scan's behaviour), never as declarations — the same
|
||||||
|
# "reference, not declaration" logic as rule 2. Their gate coverage is unaffected: any
|
||||||
|
# ci/ change (ci/tests/ included) still attributes R-GITOPS-01/R-TEST-02/R-TEST-03 via the
|
||||||
|
# path->requirement map below. NOTE: this excludes ci/tests/ only — conformance/tests/
|
||||||
|
# carry AUTHORITATIVE `conformance.home.k3s/R-*` labels and are deliberately still scanned.
|
||||||
|
# Three-dot to match changed_files(): changes on this branch since it diverged from base.
|
||||||
changed_diff() {
|
changed_diff() {
|
||||||
local base; base="$(resolve_base)"
|
local base; base="$(resolve_base)"
|
||||||
if git -C "$REPO_ROOT" diff -U0 "${base}...HEAD" -- . ':(exclude)*.md' 2>/dev/null; then return; fi
|
local -a excl=(':(exclude)*.md' ':(exclude)ci/tests/')
|
||||||
git -C "$REPO_ROOT" diff -U0 "${base}" -- . ':(exclude)*.md' 2>/dev/null || true
|
if git -C "$REPO_ROOT" diff -U0 "${base}...HEAD" -- . "${excl[@]}" 2>/dev/null; then return; fi
|
||||||
|
git -C "$REPO_ROOT" diff -U0 "${base}" -- . "${excl[@]}" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Coarse path -> requirement map. Deliberately conservative; extend as subtrees
|
# Coarse path -> requirement map. Deliberately conservative; extend as subtrees
|
||||||
@@ -109,9 +131,15 @@ main() {
|
|||||||
|
|
||||||
# Self-declared R-IDs on the ADDED/REMOVED lines of the change (see changed_diff).
|
# Self-declared R-IDs on the ADDED/REMOVED lines of the change (see changed_diff).
|
||||||
# `^[+-]` keeps hunk headers (`@@`) and the `+++`/`---` file headers out of scope.
|
# `^[+-]` keeps hunk headers (`@@`) and the `+++`/`---` file headers out of scope.
|
||||||
|
# `sed 's/^[+-]//'` then strips the single-char diff marker (safe: -U0 emits one), so
|
||||||
|
# what remains is the SOURCE line, and `grep -vE '^[[:space:]]*#'` drops whole-line
|
||||||
|
# YAML/shell comments (rule 3 in changed_diff's header) BEFORE the R-ID grep — a
|
||||||
|
# requirement cited only in a `#` comment is prose, not a declaration.
|
||||||
reqs+=" $(changed_diff \
|
reqs+=" $(changed_diff \
|
||||||
| grep -E '^[+-]' \
|
| grep -E '^[+-]' \
|
||||||
| grep -vE '^(\+\+\+|---)' \
|
| grep -vE '^(\+\+\+|---)' \
|
||||||
|
| sed -E 's/^[+-]//' \
|
||||||
|
| grep -vE '^[[:space:]]*#' \
|
||||||
| grep -hoE 'R-[A-Z]+-[0-9]+' \
|
| grep -hoE 'R-[A-Z]+-[0-9]+' \
|
||||||
| tr '\n' ' ' || true)"
|
| tr '\n' ' ' || true)"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user