@metasession.co/devaudit-cli 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +94 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +55 -1
- package/sdlc/ai-rules/SDLC_RULES.md +27 -7
- package/sdlc/files/_common/0-project-setup.md +13 -7
- package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
- package/sdlc/files/_common/README_TEMPLATE.md +8 -8
- package/sdlc/files/_common/Test_Architecture.md +15 -15
- package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
- package/sdlc/files/_common/Test_Policy.md +2 -2
- package/sdlc/files/_common/Test_Strategy.md +10 -10
- package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
- package/sdlc/files/_common/governance/dpia.md.template +3 -3
- package/sdlc/files/_common/governance/incident-report.md.template +3 -3
- package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
- package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
- package/sdlc/files/_common/governance/risk-register.md.template +1 -1
- package/sdlc/files/_common/governance/ropa.md.template +2 -2
- package/sdlc/files/_common/joining-an-existing-project.md +4 -4
- package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
- package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
- package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
- package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
- package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +65 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +38 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +307 -58
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +128 -78
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
- package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
- package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
- package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
- package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
- package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
- package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
- package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +55 -1
- package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +169 -2
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +169 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
- package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
- package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
- package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
- package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +57 -3
- package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
- package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
- package/sdlc/files/ci/check-release-approval.yml.template +18 -28
- package/sdlc/files/ci/ci.yml.template +95 -35
- package/sdlc/files/ci/close-out-release.yml.template +7 -4
- package/sdlc/files/ci/compliance-evidence.yml.template +234 -49
- package/sdlc/files/ci/feature-e2e.yml.template +84 -1
- package/sdlc/files/ci/post-deploy-prod.yml.template +59 -4
- package/sdlc/files/ci/python/ci.yml.template +60 -12
- package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +44 -2
- package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +102 -20
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +3 -3
- package/sdlc/src/blueprints/5-deploy-main.raw.md +2 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# submit-bundle-manifest.sh — resolve a release row, then POST its bundle manifest.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/submit-bundle-manifest.sh <project-slug> <release-version> <manifest-path> [--reconcile-existing-ownership]
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
PROJECT_SLUG="${1:-}"
|
|
10
|
+
RELEASE_VERSION="${2:-}"
|
|
11
|
+
MANIFEST_PATH="${3:-}"
|
|
12
|
+
shift $(( $# >= 3 ? 3 : $# ))
|
|
13
|
+
|
|
14
|
+
RECONCILE_EXISTING_OWNERSHIP=false
|
|
15
|
+
while [ "$#" -gt 0 ]; do
|
|
16
|
+
case "$1" in
|
|
17
|
+
--reconcile-existing-ownership)
|
|
18
|
+
RECONCILE_EXISTING_OWNERSHIP=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown argument: $1" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
if [ -z "$PROJECT_SLUG" ] || [ -z "$RELEASE_VERSION" ] || [ -z "$MANIFEST_PATH" ]; then
|
|
29
|
+
echo "Usage: $0 <project-slug> <release-version> <manifest-path> [--reconcile-existing-ownership]" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
: "${DEVAUDIT_BASE_URL:?DEVAUDIT_BASE_URL must be set}"
|
|
34
|
+
: "${DEVAUDIT_API_KEY:?DEVAUDIT_API_KEY must be set}"
|
|
35
|
+
|
|
36
|
+
if [ ! -f "$MANIFEST_PATH" ]; then
|
|
37
|
+
echo "Error: manifest file not found: $MANIFEST_PATH" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
42
|
+
echo "Error: jq is required." >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
BASE_URL="${DEVAUDIT_BASE_URL%/}"
|
|
47
|
+
|
|
48
|
+
SCHEMA_VERSION="$(jq -r '.schemaVersion // empty' "$MANIFEST_PATH")"
|
|
49
|
+
if [ "$SCHEMA_VERSION" != "1" ]; then
|
|
50
|
+
echo "Error: manifest schemaVersion must be 1." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
MEMBER_COUNT="$(jq -r '(.members // []) | length' "$MANIFEST_PATH")"
|
|
55
|
+
WORK_ITEM_COUNT="$(jq -r '(.nonReleaseWorkItems // []) | length' "$MANIFEST_PATH")"
|
|
56
|
+
if [ "$MEMBER_COUNT" = "0" ] && [ "$WORK_ITEM_COUNT" = "0" ]; then
|
|
57
|
+
echo "Bundle manifest has no members or non-release work items; skipping submission."
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
RESOLVE_URL="${BASE_URL}/api/ci/releases/resolve?projectSlug=${PROJECT_SLUG}&versionPrefix=${RELEASE_VERSION}"
|
|
62
|
+
RESOLVE_RESPONSE="$(curl -sS -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" "$RESOLVE_URL")"
|
|
63
|
+
RELEASE_ID="$(printf '%s' "$RESOLVE_RESPONSE" | jq -r '.latest.id // empty')"
|
|
64
|
+
RESOLVED_VERSION="$(printf '%s' "$RESOLVE_RESPONSE" | jq -r '.latest.version // empty')"
|
|
65
|
+
|
|
66
|
+
if [ -z "$RELEASE_ID" ]; then
|
|
67
|
+
echo "Error: could not resolve release ${RELEASE_VERSION} for project ${PROJECT_SLUG}." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
if [ "$RESOLVED_VERSION" != "$RELEASE_VERSION" ]; then
|
|
71
|
+
echo "Error: resolve endpoint returned ${RESOLVED_VERSION} instead of exact release ${RELEASE_VERSION}." >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
PAYLOAD="$(cat "$MANIFEST_PATH")"
|
|
76
|
+
if [ "$RECONCILE_EXISTING_OWNERSHIP" = "true" ]; then
|
|
77
|
+
PAYLOAD="$(jq '. + { reconcileExistingOwnership: true }' "$MANIFEST_PATH")"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
POST_URL="${BASE_URL}/api/ci/releases/${RELEASE_ID}/bundle-manifest"
|
|
81
|
+
HTTP_BODY_FILE="$(mktemp)"
|
|
82
|
+
trap 'rm -f "$HTTP_BODY_FILE"' EXIT
|
|
83
|
+
HTTP_CODE="$(
|
|
84
|
+
curl -sS -o "$HTTP_BODY_FILE" -w "%{http_code}" \
|
|
85
|
+
-X POST \
|
|
86
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
--data "$PAYLOAD" \
|
|
89
|
+
"$POST_URL"
|
|
90
|
+
)"
|
|
91
|
+
|
|
92
|
+
if [ "$HTTP_CODE" != "201" ]; then
|
|
93
|
+
echo "Error: bundle manifest submission failed with HTTP ${HTTP_CODE}." >&2
|
|
94
|
+
sed 's/^/ /' "$HTTP_BODY_FILE" >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo "Submitted bundle manifest for ${RELEASE_VERSION} to release ${RELEASE_ID}."
|
|
99
|
+
echo " members: ${MEMBER_COUNT}"
|
|
100
|
+
echo " non-release work items: ${WORK_ITEM_COUNT}"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# submit-bundle-manifest.test.sh — Focused tests for bundle manifest submission.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
HELPER="$SCRIPT_DIR/submit-bundle-manifest.sh"
|
|
8
|
+
[ -x "$HELPER" ] || chmod +x "$HELPER"
|
|
9
|
+
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
WORK="$(mktemp -d)"
|
|
13
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
14
|
+
|
|
15
|
+
assert_contains() {
|
|
16
|
+
local desc="$1" needle="$2" file="$3"
|
|
17
|
+
if grep -Fq "$needle" "$file"; then
|
|
18
|
+
echo " PASS: $desc"
|
|
19
|
+
PASS=$((PASS + 1))
|
|
20
|
+
else
|
|
21
|
+
echo " FAIL: $desc"
|
|
22
|
+
echo " missing: $needle"
|
|
23
|
+
echo " file:"
|
|
24
|
+
sed 's/^/ /' "$file"
|
|
25
|
+
FAIL=$((FAIL + 1))
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
make_fixture() {
|
|
30
|
+
local dir="$1"
|
|
31
|
+
rm -rf "$dir"
|
|
32
|
+
mkdir -p "$dir/bin"
|
|
33
|
+
cd "$dir"
|
|
34
|
+
cat > bin/curl <<'EOF'
|
|
35
|
+
#!/usr/bin/env bash
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
LOG_FILE="${SUBMIT_TEST_LOG:?}"
|
|
38
|
+
URL=""
|
|
39
|
+
BODY_FILE=""
|
|
40
|
+
WRITE_OUT=""
|
|
41
|
+
METHOD="GET"
|
|
42
|
+
BODY=""
|
|
43
|
+
ARGS=("$@")
|
|
44
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
45
|
+
arg="${ARGS[$i]}"
|
|
46
|
+
case "$arg" in
|
|
47
|
+
-X) METHOD="${ARGS[$((i+1))]}" ;;
|
|
48
|
+
-o) BODY_FILE="${ARGS[$((i+1))]}" ;;
|
|
49
|
+
-w) WRITE_OUT="${ARGS[$((i+1))]}" ;;
|
|
50
|
+
--data) BODY="${ARGS[$((i+1))]}" ;;
|
|
51
|
+
http://*|https://*) URL="$arg" ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
echo "METHOD:${METHOD}" >> "$LOG_FILE"
|
|
55
|
+
echo "URL:${URL}" >> "$LOG_FILE"
|
|
56
|
+
if [ -n "$BODY" ]; then
|
|
57
|
+
echo "BODY:${BODY}" >> "$LOG_FILE"
|
|
58
|
+
fi
|
|
59
|
+
if [[ "$URL" == *"/api/ci/releases/resolve"* ]]; then
|
|
60
|
+
printf '{"latest":{"id":"rel_123","version":"REQ-042","status":"draft"}}'
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
if [[ "$URL" == *"/api/ci/releases/rel_123/bundle-manifest" ]]; then
|
|
64
|
+
if [ -n "$BODY_FILE" ]; then
|
|
65
|
+
printf '{"ok":true}' > "$BODY_FILE"
|
|
66
|
+
fi
|
|
67
|
+
if [ -n "$WRITE_OUT" ]; then
|
|
68
|
+
printf '201'
|
|
69
|
+
fi
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
printf '{"error":"unexpected url"}'
|
|
73
|
+
exit 1
|
|
74
|
+
EOF
|
|
75
|
+
chmod +x bin/curl
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
echo "=== submit-bundle-manifest.sh tests ==="
|
|
79
|
+
|
|
80
|
+
D1="$WORK/case1"
|
|
81
|
+
make_fixture "$D1"
|
|
82
|
+
cat > "$D1/manifest.json" <<'EOF'
|
|
83
|
+
{
|
|
84
|
+
"schemaVersion": 1,
|
|
85
|
+
"approvalRelease": { "version": "REQ-042" },
|
|
86
|
+
"coreRelease": { "version": "REQ-042" },
|
|
87
|
+
"members": [
|
|
88
|
+
{
|
|
89
|
+
"version": "REQ-041",
|
|
90
|
+
"role": "predecessor",
|
|
91
|
+
"relationship": "superseded"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"nonReleaseWorkItems": [
|
|
95
|
+
{
|
|
96
|
+
"kind": "housekeeping_commit",
|
|
97
|
+
"title": "docs: refresh release notes"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"manifestHash": "sha256:test"
|
|
101
|
+
}
|
|
102
|
+
EOF
|
|
103
|
+
LOG_FILE="$D1/submit.log"
|
|
104
|
+
touch "$LOG_FILE"
|
|
105
|
+
export PATH="$D1/bin:$PATH"
|
|
106
|
+
export SUBMIT_TEST_LOG="$LOG_FILE"
|
|
107
|
+
export DEVAUDIT_BASE_URL="https://devaudit.example.test"
|
|
108
|
+
export DEVAUDIT_API_KEY="mc_test_dummy"
|
|
109
|
+
|
|
110
|
+
bash "$HELPER" fixture-project REQ-042 "$D1/manifest.json" > "$D1/stdout.log" 2>&1
|
|
111
|
+
assert_contains "resolve endpoint called" "/api/ci/releases/resolve?projectSlug=fixture-project&versionPrefix=REQ-042" "$LOG_FILE"
|
|
112
|
+
assert_contains "bundle endpoint called" "/api/ci/releases/rel_123/bundle-manifest" "$LOG_FILE"
|
|
113
|
+
assert_contains "member payload submitted" "\"version\": \"REQ-041\"" "$LOG_FILE"
|
|
114
|
+
|
|
115
|
+
D2="$WORK/case2"
|
|
116
|
+
make_fixture "$D2"
|
|
117
|
+
cat > "$D2/manifest.json" <<'EOF'
|
|
118
|
+
{
|
|
119
|
+
"schemaVersion": 1,
|
|
120
|
+
"approvalRelease": { "version": "REQ-042" },
|
|
121
|
+
"coreRelease": { "version": "REQ-042" },
|
|
122
|
+
"members": [],
|
|
123
|
+
"nonReleaseWorkItems": []
|
|
124
|
+
}
|
|
125
|
+
EOF
|
|
126
|
+
LOG_FILE="$D2/submit.log"
|
|
127
|
+
touch "$LOG_FILE"
|
|
128
|
+
export PATH="$D2/bin:$PATH"
|
|
129
|
+
export SUBMIT_TEST_LOG="$LOG_FILE"
|
|
130
|
+
|
|
131
|
+
bash "$HELPER" fixture-project REQ-042 "$D2/manifest.json" > "$D2/stdout.log" 2>&1
|
|
132
|
+
assert_contains "empty manifest skips submission" "Bundle manifest has no members or non-release work items; skipping submission." "$D2/stdout.log"
|
|
133
|
+
|
|
134
|
+
echo ""
|
|
135
|
+
echo "=== submit-bundle-manifest.test.sh: $PASS passed, $FAIL failed ==="
|
|
136
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
# portal group evidence by test cycle per
|
|
48
48
|
# ISO/IEC/IEEE 29119-3. Optional — older
|
|
49
49
|
# portals ignore the field (no error).
|
|
50
|
+
# --evidence-scope <scope> Evidence ownership scope: release | stage |
|
|
51
|
+
# cycle | approval. Forwarded as
|
|
52
|
+
# `evidenceScope`; optional and ignored by
|
|
53
|
+
# older portals.
|
|
54
|
+
# --test-cycle-record-id <id> First-class portal cycle UUID. Requires
|
|
55
|
+
# `--evidence-scope cycle`. Forwarded as
|
|
56
|
+
# `testCycleRecordId`; preserved alongside
|
|
57
|
+
# legacy `--test-cycle` during dual-write.
|
|
50
58
|
#
|
|
51
59
|
# Required environment variables:
|
|
52
60
|
# DEVAUDIT_BASE_URL e.g. https://meta-comply-production.up.railway.app
|
|
@@ -98,6 +106,10 @@ CHANGE_TYPE=""
|
|
|
98
106
|
GATE_STATUS=""
|
|
99
107
|
SDLC_STAGE=""
|
|
100
108
|
TEST_CYCLE=""
|
|
109
|
+
EVIDENCE_SCOPE=""
|
|
110
|
+
TEST_CYCLE_RECORD_ID=""
|
|
111
|
+
SENTINEL_CONTENT=""
|
|
112
|
+
COMMIT_TIMESTAMP=""
|
|
101
113
|
# Repeatable `--meta-key key=value` accumulator. Each pair gets merged
|
|
102
114
|
# into the metadata JSON sent to the portal. Used by the screenshot
|
|
103
115
|
# upload loop to pass `origin=feature|regression` from the per-PNG
|
|
@@ -125,6 +137,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
125
137
|
--gate-status) GATE_STATUS="$2"; shift 2 ;;
|
|
126
138
|
--sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
|
|
127
139
|
--test-cycle) TEST_CYCLE="$2"; shift 2 ;;
|
|
140
|
+
--evidence-scope) EVIDENCE_SCOPE="$2"; shift 2 ;;
|
|
141
|
+
--test-cycle-record-id) TEST_CYCLE_RECORD_ID="$2"; shift 2 ;;
|
|
128
142
|
# --meta-key key=value (repeatable). Merged into the metadata JSON
|
|
129
143
|
# before posting. Validates the `key=value` shape; rejects bare
|
|
130
144
|
# keys without `=`.
|
|
@@ -152,6 +166,14 @@ if [ -n "$SDLC_STAGE" ] && ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
|
|
|
152
166
|
echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)"
|
|
153
167
|
exit 1
|
|
154
168
|
fi
|
|
169
|
+
if [ -n "$EVIDENCE_SCOPE" ] && ! [[ "$EVIDENCE_SCOPE" =~ ^(release|stage|cycle|approval)$ ]]; then
|
|
170
|
+
echo "Error: --evidence-scope must be one of: release, stage, cycle, approval"
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
if [ -n "$TEST_CYCLE_RECORD_ID" ] && [ "$EVIDENCE_SCOPE" != "cycle" ]; then
|
|
174
|
+
echo "Error: --test-cycle-record-id requires --evidence-scope cycle"
|
|
175
|
+
exit 1
|
|
176
|
+
fi
|
|
155
177
|
|
|
156
178
|
if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
|
|
157
179
|
echo "Error: DEVAUDIT_BASE_URL environment variable is required"
|
|
@@ -202,6 +224,28 @@ probe_base_url_drift() {
|
|
|
202
224
|
}
|
|
203
225
|
probe_base_url_drift
|
|
204
226
|
|
|
227
|
+
is_tracked_change_type() {
|
|
228
|
+
case "${1:-}" in
|
|
229
|
+
feat|fix|refactor|perf|compliance|revert) return 0 ;;
|
|
230
|
+
*) return 1 ;;
|
|
231
|
+
esac
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
resolve_sentinel_context() {
|
|
235
|
+
if ! is_tracked_change_type "$CHANGE_TYPE"; then
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
if [ -f ".sdlc-implementer-invoked" ]; then
|
|
239
|
+
SENTINEL_CONTENT=$(cat .sdlc-implementer-invoked)
|
|
240
|
+
fi
|
|
241
|
+
local git_target="HEAD"
|
|
242
|
+
if [ -n "$GIT_SHA" ]; then
|
|
243
|
+
git_target="$GIT_SHA"
|
|
244
|
+
fi
|
|
245
|
+
COMMIT_TIMESTAMP=$(git show -s --format=%cI "$git_target" 2>/dev/null || true)
|
|
246
|
+
}
|
|
247
|
+
resolve_sentinel_context
|
|
248
|
+
|
|
205
249
|
# --- Build metadata JSON ---
|
|
206
250
|
# Assemble entries first; only emit `{ ... }` if at least one field is
|
|
207
251
|
# set. Each entry is a `"key":"value"` JSON pair with the value
|
|
@@ -213,6 +257,7 @@ META_ENTRIES=()
|
|
|
213
257
|
[ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
|
|
214
258
|
[ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
|
|
215
259
|
[ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
|
|
260
|
+
[ -n "$COMMIT_TIMESTAMP" ] && META_ENTRIES+=("\"commitTimestamp\":\"$(json_escape "$COMMIT_TIMESTAMP")\"")
|
|
216
261
|
for KV in "${META_KEYS[@]}"; do
|
|
217
262
|
KEY="${KV%%=*}"
|
|
218
263
|
VAL="${KV#*=}"
|
|
@@ -337,8 +382,13 @@ upload_presigned() {
|
|
|
337
382
|
\"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
|
|
338
383
|
\"releaseTitle\": \"${RELEASE_TITLE}\",
|
|
339
384
|
\"releaseSummary\": \"${RELEASE_SUMMARY}\",
|
|
385
|
+
\"changeType\": \"${CHANGE_TYPE}\",
|
|
340
386
|
\"sdlcStage\": \"${SDLC_STAGE}\",
|
|
341
|
-
\"testCycleId\": \"${TEST_CYCLE}\"
|
|
387
|
+
\"testCycleId\": \"${TEST_CYCLE}\",
|
|
388
|
+
\"evidenceScope\": \"${EVIDENCE_SCOPE}\",
|
|
389
|
+
\"testCycleRecordId\": \"${TEST_CYCLE_RECORD_ID}\",
|
|
390
|
+
\"sentinelContent\": $(jq -Rn --arg v "$SENTINEL_CONTENT" '$v'),
|
|
391
|
+
\"commitTimestamp\": \"${COMMIT_TIMESTAMP}\"
|
|
342
392
|
}") || curl_exit=$?
|
|
343
393
|
curl_exit=${curl_exit:-0}
|
|
344
394
|
if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
@@ -503,6 +553,10 @@ for FILE in "${FILES[@]}"; do
|
|
|
503
553
|
[ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
|
|
504
554
|
[ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
|
|
505
555
|
[ -n "$TEST_CYCLE" ] && CURL_ARGS+=(-F "testCycleId=${TEST_CYCLE}")
|
|
556
|
+
[ -n "$EVIDENCE_SCOPE" ] && CURL_ARGS+=(-F "evidenceScope=${EVIDENCE_SCOPE}")
|
|
557
|
+
[ -n "$TEST_CYCLE_RECORD_ID" ] && CURL_ARGS+=(-F "testCycleRecordId=${TEST_CYCLE_RECORD_ID}")
|
|
558
|
+
[ -n "$SENTINEL_CONTENT" ] && CURL_ARGS+=(-F "sentinelContent=${SENTINEL_CONTENT}")
|
|
559
|
+
[ -n "$COMMIT_TIMESTAMP" ] && CURL_ARGS+=(-F "commitTimestamp=${COMMIT_TIMESTAMP}")
|
|
506
560
|
|
|
507
561
|
ATTEMPT=1
|
|
508
562
|
BACKOFF=$INITIAL_BACKOFF_SECONDS
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# upload-evidence.test.sh — Focused regression tests for sentinel propagation.
|
|
3
|
+
#
|
|
4
|
+
# Verifies tracked uploads automatically forward `.sdlc-implementer-invoked`
|
|
5
|
+
# content and a commit timestamp to the portal-facing upload contract.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
HELPER="$SCRIPT_DIR/upload-evidence.sh"
|
|
11
|
+
[ -x "$HELPER" ] || chmod +x "$HELPER"
|
|
12
|
+
|
|
13
|
+
PASS=0
|
|
14
|
+
FAIL=0
|
|
15
|
+
|
|
16
|
+
WORK=$(mktemp -d)
|
|
17
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
18
|
+
|
|
19
|
+
assert_contains() {
|
|
20
|
+
local desc="$1" needle="$2" file="$3"
|
|
21
|
+
if grep -Fq "$needle" "$file"; then
|
|
22
|
+
echo " PASS: $desc"
|
|
23
|
+
PASS=$((PASS + 1))
|
|
24
|
+
else
|
|
25
|
+
echo " FAIL: $desc"
|
|
26
|
+
echo " missing: $needle"
|
|
27
|
+
echo " file:"
|
|
28
|
+
sed 's/^/ /' "$file"
|
|
29
|
+
FAIL=$((FAIL + 1))
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
make_fixture() {
|
|
34
|
+
local dir="$1"
|
|
35
|
+
rm -rf "$dir"
|
|
36
|
+
mkdir -p "$dir/bin"
|
|
37
|
+
cd "$dir"
|
|
38
|
+
git init -q --initial-branch=main
|
|
39
|
+
git config user.email "test@example.com"
|
|
40
|
+
git config user.name "test"
|
|
41
|
+
echo "evidence" > evidence.txt
|
|
42
|
+
printf '%s\n' '[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' > .sdlc-implementer-invoked
|
|
43
|
+
git add evidence.txt .sdlc-implementer-invoked
|
|
44
|
+
git commit -q -m "feat: tracked upload fixture"
|
|
45
|
+
|
|
46
|
+
cat > bin/curl <<'EOF'
|
|
47
|
+
#!/usr/bin/env bash
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
LOG_FILE="${UPLOAD_TEST_LOG:?}"
|
|
50
|
+
URL=""
|
|
51
|
+
OUT_FILE=""
|
|
52
|
+
HDR_FILE=""
|
|
53
|
+
WRITE_OUT=""
|
|
54
|
+
ARGS=("$@")
|
|
55
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
56
|
+
arg="${ARGS[$i]}"
|
|
57
|
+
case "$arg" in
|
|
58
|
+
-o) OUT_FILE="${ARGS[$((i+1))]}" ;;
|
|
59
|
+
-D) HDR_FILE="${ARGS[$((i+1))]}" ;;
|
|
60
|
+
-w) WRITE_OUT="${ARGS[$((i+1))]}" ;;
|
|
61
|
+
http://*|https://*) URL="$arg" ;;
|
|
62
|
+
-F)
|
|
63
|
+
echo "FORM:${ARGS[$((i+1))]}" >> "$LOG_FILE"
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
echo "URL:${URL}" >> "$LOG_FILE"
|
|
68
|
+
if [[ "$URL" == *"/api/health" ]]; then
|
|
69
|
+
printf '200 '
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "$OUT_FILE" ]; then
|
|
73
|
+
printf '{"ok":true}' > "$OUT_FILE"
|
|
74
|
+
fi
|
|
75
|
+
if [ -n "$HDR_FILE" ]; then
|
|
76
|
+
: > "$HDR_FILE"
|
|
77
|
+
fi
|
|
78
|
+
if [ -n "$WRITE_OUT" ]; then
|
|
79
|
+
printf '201'
|
|
80
|
+
fi
|
|
81
|
+
EOF
|
|
82
|
+
chmod +x bin/curl
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
echo "=== upload-evidence.sh tests ==="
|
|
86
|
+
|
|
87
|
+
D1="$WORK/case1"
|
|
88
|
+
make_fixture "$D1"
|
|
89
|
+
LOG_FILE="$D1/upload.log"
|
|
90
|
+
touch "$LOG_FILE"
|
|
91
|
+
export PATH="$D1/bin:$PATH"
|
|
92
|
+
export UPLOAD_TEST_LOG="$LOG_FILE"
|
|
93
|
+
export DEVAUDIT_BASE_URL="https://devaudit.example.test"
|
|
94
|
+
export DEVAUDIT_API_KEY="mc_test_dummy"
|
|
95
|
+
|
|
96
|
+
bash "$HELPER" fixture-project REQ-091 test_report evidence.txt \
|
|
97
|
+
--release v2026.07.13 \
|
|
98
|
+
--environment uat \
|
|
99
|
+
--category test_report \
|
|
100
|
+
--change-type feat > "$D1/stdout.log" 2>&1
|
|
101
|
+
|
|
102
|
+
assert_contains "tracked upload forwards sentinelContent" \
|
|
103
|
+
'FORM:sentinelContent=[{"currentPhase":"3","initializedBy":"skill","status":"active"}]' \
|
|
104
|
+
"$LOG_FILE"
|
|
105
|
+
assert_contains "tracked upload forwards commitTimestamp field" \
|
|
106
|
+
'FORM:commitTimestamp=' \
|
|
107
|
+
"$LOG_FILE"
|
|
108
|
+
assert_contains "tracked upload forwards changeType" \
|
|
109
|
+
'FORM:changeType=feat' \
|
|
110
|
+
"$LOG_FILE"
|
|
111
|
+
assert_contains "tracked upload stamps commitTimestamp into metadata" \
|
|
112
|
+
'FORM:metadata={"commitTimestamp":"' \
|
|
113
|
+
"$LOG_FILE"
|
|
114
|
+
|
|
115
|
+
echo ""
|
|
116
|
+
echo "=== upload-evidence.test.sh: $PASS passed, $FAIL failed ==="
|
|
117
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
@@ -48,6 +48,27 @@ fi
|
|
|
48
48
|
echo "Requirements found in PR commits: $REQUIREMENTS"
|
|
49
49
|
echo ""
|
|
50
50
|
|
|
51
|
+
find_first_match() {
|
|
52
|
+
local pattern="$1"
|
|
53
|
+
local match=""
|
|
54
|
+
match=$(compgen -G "$pattern" | head -1 || true)
|
|
55
|
+
[ -n "$match" ] && printf '%s' "$match"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
require_bundled_fields() {
|
|
59
|
+
local file="$1"
|
|
60
|
+
local label="$2"
|
|
61
|
+
local missing=0
|
|
62
|
+
shift 2
|
|
63
|
+
for field in "$@"; do
|
|
64
|
+
if ! grep -qF "**${field}:**" "$file" 2>/dev/null; then
|
|
65
|
+
echo " ERROR: ${label} is missing bundled field '**${field}:**'"
|
|
66
|
+
missing=1
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
return $missing
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
for REQ in $REQUIREMENTS; do
|
|
52
73
|
echo "--- Checking $REQ ---"
|
|
53
74
|
|
|
@@ -149,9 +170,14 @@ for REQ in $REQUIREMENTS; do
|
|
|
149
170
|
fi
|
|
150
171
|
fi
|
|
151
172
|
|
|
152
|
-
# Check test-execution-summary.md exists
|
|
173
|
+
# Check test-execution-summary.md exists. For tracked REQs this is a
|
|
174
|
+
# hard requirement: the file is the per-release test report that
|
|
175
|
+
# satisfies the portal's Test Reports gate. Housekeeping flows never
|
|
176
|
+
# reach this loop because they have no REQ refs in the validated
|
|
177
|
+
# commits, so the stricter behavior remains scoped to tracked work.
|
|
153
178
|
if [ ! -f "compliance/evidence/$REQ/test-execution-summary.md" ]; then
|
|
154
|
-
echo "
|
|
179
|
+
echo " ERROR: Test execution summary missing: compliance/evidence/$REQ/test-execution-summary.md"
|
|
180
|
+
EXIT_CODE=1
|
|
155
181
|
else
|
|
156
182
|
echo " OK: test-execution-summary.md exists"
|
|
157
183
|
fi
|
|
@@ -193,16 +219,157 @@ for REQ in $REQUIREMENTS; do
|
|
|
193
219
|
compgen -G "$TICKET_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
194
220
|
compgen -G "$APPROVED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
195
221
|
compgen -G "$SUPERSEDED_PATTERN" > /dev/null 2>&1 && LOCATIONS=$((LOCATIONS+1))
|
|
222
|
+
TICKET_FILE=""
|
|
196
223
|
if [ "$LOCATIONS" -gt 1 ]; then
|
|
197
224
|
echo " ERROR: RELEASE-TICKET-${REQ} exists in more than one release directory (pending/approved/superseded). Remove the stale pending copy — it will break the evidence-completeness gate (#192)."
|
|
198
225
|
EXIT_CODE=1
|
|
199
226
|
elif [ "$LOCATIONS" -eq 1 ]; then
|
|
200
227
|
echo " OK: Release ticket exists"
|
|
228
|
+
TICKET_FILE=$(find_first_match "$TICKET_PATTERN" || true)
|
|
229
|
+
[ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$APPROVED_PATTERN" || true)
|
|
230
|
+
[ -z "$TICKET_FILE" ] && TICKET_FILE=$(find_first_match "$SUPERSEDED_PATTERN" || true)
|
|
201
231
|
else
|
|
202
232
|
echo " ERROR: Release ticket missing: compliance/pending-releases/RELEASE-TICKET-${REQ}.md"
|
|
203
233
|
EXIT_CODE=1
|
|
204
234
|
fi
|
|
205
235
|
|
|
236
|
+
BUNDLED_FILE=""
|
|
237
|
+
for BUNDLED_PATTERN in \
|
|
238
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${REQ}.md" \
|
|
239
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${REQ}.md" \
|
|
240
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${REQ}.md"; do
|
|
241
|
+
BUNDLED_FILE=$(find_first_match "$BUNDLED_PATTERN" || true)
|
|
242
|
+
[ -n "$BUNDLED_FILE" ] && break
|
|
243
|
+
done
|
|
244
|
+
if [ -n "$BUNDLED_FILE" ]; then
|
|
245
|
+
echo " OK: Bundled release evidence present: ${BUNDLED_FILE}"
|
|
246
|
+
BUNDLED_MANIFEST=""
|
|
247
|
+
for MANIFEST_PATTERN in \
|
|
248
|
+
"compliance/pending-releases/BUNDLED-CHANGES-${REQ}.json" \
|
|
249
|
+
"compliance/approved-releases/BUNDLED-CHANGES-${REQ}.json" \
|
|
250
|
+
"compliance/superseded-releases/BUNDLED-CHANGES-${REQ}.json"; do
|
|
251
|
+
BUNDLED_MANIFEST=$(find_first_match "$MANIFEST_PATTERN" || true)
|
|
252
|
+
[ -n "$BUNDLED_MANIFEST" ] && break
|
|
253
|
+
done
|
|
254
|
+
if [ -n "$BUNDLED_MANIFEST" ]; then
|
|
255
|
+
echo " OK: Bundled release manifest present: ${BUNDLED_MANIFEST}"
|
|
256
|
+
else
|
|
257
|
+
echo " ERROR: Bundled release evidence exists but BUNDLED-CHANGES-${REQ}.json is missing"
|
|
258
|
+
EXIT_CODE=1
|
|
259
|
+
fi
|
|
260
|
+
if grep -q '^## Bundled Changes' "$BUNDLED_FILE"; then
|
|
261
|
+
echo " OK: Bundled release evidence has canonical heading"
|
|
262
|
+
else
|
|
263
|
+
echo " ERROR: Bundled release evidence is missing '## Bundled Changes'"
|
|
264
|
+
EXIT_CODE=1
|
|
265
|
+
fi
|
|
266
|
+
if ! require_bundled_fields \
|
|
267
|
+
"$BUNDLED_FILE" \
|
|
268
|
+
"Bundled release evidence" \
|
|
269
|
+
"Core tracked release" \
|
|
270
|
+
"Bundle manifest" \
|
|
271
|
+
"Manifest hash" \
|
|
272
|
+
"Absorbed predecessor releases" \
|
|
273
|
+
"Absorbed non-release work" \
|
|
274
|
+
"Why bundled here" \
|
|
275
|
+
"Evidence impact" \
|
|
276
|
+
"Reviewer impact" \
|
|
277
|
+
"Security / risk impact" \
|
|
278
|
+
"Reference"; then
|
|
279
|
+
EXIT_CODE=1
|
|
280
|
+
else
|
|
281
|
+
echo " OK: Bundled release evidence carries the required structured fields"
|
|
282
|
+
fi
|
|
283
|
+
if [ -n "$TICKET_FILE" ] && grep -qE '^## (Bundled Changes|Absorbed Predecessor Releases)' "$TICKET_FILE"; then
|
|
284
|
+
echo " OK: Release ticket documents bundled release context"
|
|
285
|
+
else
|
|
286
|
+
echo " ERROR: Bundled release evidence exists but the release ticket is missing '## Bundled Changes' or '## Absorbed Predecessor Releases'"
|
|
287
|
+
EXIT_CODE=1
|
|
288
|
+
fi
|
|
289
|
+
if [ -n "$TICKET_FILE" ]; then
|
|
290
|
+
if ! require_bundled_fields \
|
|
291
|
+
"$TICKET_FILE" \
|
|
292
|
+
"Release ticket" \
|
|
293
|
+
"Core tracked release" \
|
|
294
|
+
"Absorbed predecessor releases" \
|
|
295
|
+
"Absorbed non-release work" \
|
|
296
|
+
"Why bundled here" \
|
|
297
|
+
"Evidence impact" \
|
|
298
|
+
"Reviewer impact" \
|
|
299
|
+
"Security / risk impact" \
|
|
300
|
+
"Reference"; then
|
|
301
|
+
EXIT_CODE=1
|
|
302
|
+
else
|
|
303
|
+
echo " OK: Release ticket carries the required bundled fields"
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/test-execution-summary.md" 2>/dev/null; then
|
|
307
|
+
echo " OK: test-execution-summary.md documents bundled release context"
|
|
308
|
+
else
|
|
309
|
+
echo " ERROR: Bundled release evidence exists but test-execution-summary.md is missing '## Bundled Release Context'"
|
|
310
|
+
EXIT_CODE=1
|
|
311
|
+
fi
|
|
312
|
+
if ! require_bundled_fields \
|
|
313
|
+
"compliance/evidence/$REQ/test-execution-summary.md" \
|
|
314
|
+
"test-execution-summary.md" \
|
|
315
|
+
"Core tracked release" \
|
|
316
|
+
"Absorbed predecessor releases" \
|
|
317
|
+
"Absorbed non-release work" \
|
|
318
|
+
"Why bundled here" \
|
|
319
|
+
"Evidence impact" \
|
|
320
|
+
"Reviewer impact" \
|
|
321
|
+
"Security / risk impact" \
|
|
322
|
+
"Reference"; then
|
|
323
|
+
EXIT_CODE=1
|
|
324
|
+
else
|
|
325
|
+
echo " OK: test-execution-summary.md carries the required bundled fields"
|
|
326
|
+
fi
|
|
327
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/security-summary.md" 2>/dev/null; then
|
|
328
|
+
echo " OK: security-summary.md documents bundled release context"
|
|
329
|
+
else
|
|
330
|
+
echo " ERROR: Bundled release evidence exists but security-summary.md is missing '## Bundled Release Context'"
|
|
331
|
+
EXIT_CODE=1
|
|
332
|
+
fi
|
|
333
|
+
if ! require_bundled_fields \
|
|
334
|
+
"compliance/evidence/$REQ/security-summary.md" \
|
|
335
|
+
"security-summary.md" \
|
|
336
|
+
"Core tracked release" \
|
|
337
|
+
"Absorbed predecessor releases" \
|
|
338
|
+
"Absorbed non-release work" \
|
|
339
|
+
"Why bundled here" \
|
|
340
|
+
"Evidence impact" \
|
|
341
|
+
"Reviewer impact" \
|
|
342
|
+
"Security / risk impact" \
|
|
343
|
+
"Reference"; then
|
|
344
|
+
EXIT_CODE=1
|
|
345
|
+
else
|
|
346
|
+
echo " OK: security-summary.md carries the required bundled fields"
|
|
347
|
+
fi
|
|
348
|
+
if [ -f "compliance/evidence/$REQ/ai-use-note.md" ]; then
|
|
349
|
+
if grep -q '^## Bundled Release Context' "compliance/evidence/$REQ/ai-use-note.md"; then
|
|
350
|
+
echo " OK: ai-use-note.md documents bundled release context"
|
|
351
|
+
else
|
|
352
|
+
echo " ERROR: Bundled release evidence exists but ai-use-note.md is missing '## Bundled Release Context'"
|
|
353
|
+
EXIT_CODE=1
|
|
354
|
+
fi
|
|
355
|
+
if ! require_bundled_fields \
|
|
356
|
+
"compliance/evidence/$REQ/ai-use-note.md" \
|
|
357
|
+
"ai-use-note.md" \
|
|
358
|
+
"Core tracked release" \
|
|
359
|
+
"Absorbed predecessor releases" \
|
|
360
|
+
"Absorbed non-release work" \
|
|
361
|
+
"Why bundled here" \
|
|
362
|
+
"Evidence impact" \
|
|
363
|
+
"Reviewer impact" \
|
|
364
|
+
"Security / risk impact" \
|
|
365
|
+
"Reference"; then
|
|
366
|
+
EXIT_CODE=1
|
|
367
|
+
else
|
|
368
|
+
echo " OK: ai-use-note.md carries the required bundled fields"
|
|
369
|
+
fi
|
|
370
|
+
fi
|
|
371
|
+
fi
|
|
372
|
+
|
|
206
373
|
# Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
|
|
207
374
|
if grep "$REQ" compliance/RTM.md 2>/dev/null | grep -qiE 'MEDIUM|HIGH'; then
|
|
208
375
|
if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then
|