@metasession.co/devaudit-cli 0.3.26 → 0.3.28
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 +152 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/scripts/close-out-release.sh +10 -13
- package/sdlc/files/_common/scripts/evaluate-npm-audit.sh +11 -4
- package/sdlc/files/_common/scripts/evaluate-npm-audit.test.sh +23 -3
- package/sdlc/files/_common/scripts/markdown-table.sh +126 -0
- package/sdlc/files/_common/scripts/markdown-table.test.sh +36 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +15 -6
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +1 -1
- package/sdlc/files/ci/check-release-approval.yml.template +4 -0
- package/sdlc/files/ci/ci.yml.template +10 -2
- package/sdlc/files/ci/close-out-release.yml.template +3 -0
- package/sdlc/files/ci/compliance-evidence.yml.template +8 -5
- package/sdlc/files/ci/feature-e2e.yml.template +3 -2
- package/sdlc/files/ci/post-deploy-prod.yml.template +3 -0
- package/sdlc/files/ci/python/ci.yml.template +8 -5
- package/sdlc/files/ci/reconcile-deployment.yml.template +3 -0
- package/sdlc/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
4
4
|
"description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.8.2",
|
|
36
|
-
"@metasession.co/devaudit-plugin-sdk": "^0.3.
|
|
36
|
+
"@metasession.co/devaudit-plugin-sdk": "^0.3.28",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
|
|
35
35
|
set -euo pipefail
|
|
36
36
|
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
# shellcheck source=markdown-table.sh
|
|
39
|
+
source "$SCRIPT_DIR/markdown-table.sh"
|
|
40
|
+
|
|
37
41
|
REQ_ID="${1:-}"
|
|
38
42
|
RELEASE_PR=""
|
|
39
43
|
shift || true
|
|
@@ -89,21 +93,14 @@ find_release_ticket_file() {
|
|
|
89
93
|
update_rtm_status() {
|
|
90
94
|
local req_id="$1"
|
|
91
95
|
local target_status="$2"
|
|
92
|
-
local tmp_file
|
|
96
|
+
local tmp_file status_col
|
|
93
97
|
tmp_file="$(mktemp)"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
status_col="$(markdown_table_column_index_for_row \
|
|
99
|
+
"$RTM" "$req_id" Status REQ-ID/ID Status 2>/dev/null || true)"
|
|
100
|
+
if [ -n "$status_col" ]; then
|
|
101
|
+
awk -v req="$req_id" -v target_status="$target_status" -v statuscol="$status_col" '
|
|
102
|
+
BEGIN { FS="|"; OFS="|" }
|
|
97
103
|
/\\\|/ { gsub(/\\\|/, "\001", $0) }
|
|
98
|
-
{
|
|
99
|
-
cand=0; idseen=0
|
|
100
|
-
for (i=1; i<=NF; i++) {
|
|
101
|
-
c=$i; gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
|
|
102
|
-
if (c=="Status") cand=i
|
|
103
|
-
if (c=="ID" || c=="REQ-ID" || c=="REQ ID" || c ~ /^Requirement/) idseen=1
|
|
104
|
-
}
|
|
105
|
-
if (cand>0 && idseen) statuscol=cand
|
|
106
|
-
}
|
|
107
104
|
$0 ~ ("^\\| " req " ") && statuscol>0 {
|
|
108
105
|
cell=$statuscol
|
|
109
106
|
note=""
|
|
@@ -39,6 +39,7 @@ done
|
|
|
39
39
|
python3 - "$AUDIT_PATH" "$LOCK_PATH" "$EXCEPTIONS_PATH" "$OUTPUT_PATH" <<'PY'
|
|
40
40
|
import json
|
|
41
41
|
import os
|
|
42
|
+
import re
|
|
42
43
|
import sys
|
|
43
44
|
from datetime import date
|
|
44
45
|
|
|
@@ -144,8 +145,11 @@ for index, item in enumerate(exceptions, start=1):
|
|
|
144
145
|
fail(f"exception {index} approvedAt cannot be in the future")
|
|
145
146
|
if expires_at <= today:
|
|
146
147
|
fail(f"exception {index} is expired on {item['expiresAt']}")
|
|
147
|
-
if
|
|
148
|
-
fail(f"exception {index} remediationIssue must be
|
|
148
|
+
if re.fullmatch(r"https://github\.com/[^/]+/[^/]+/issues/[1-9][0-9]*", item["remediationIssue"]) is None:
|
|
149
|
+
fail(f"exception {index} remediationIssue must be an exact GitHub issue URL")
|
|
150
|
+
controls = item.get("compensatingControls")
|
|
151
|
+
if controls is not None and (not isinstance(controls, str) or not controls.strip()):
|
|
152
|
+
fail(f"exception {index} compensatingControls must be a non-empty string when present")
|
|
149
153
|
key = (item["advisoryId"], item["dependencyPath"])
|
|
150
154
|
if key in seen_exception_keys:
|
|
151
155
|
fail(f"duplicate exception for {item['advisoryId']} at {item['dependencyPath']}")
|
|
@@ -217,10 +221,13 @@ for finding_name, finding in audit["vulnerabilities"].items():
|
|
|
217
221
|
file=sys.stderr,
|
|
218
222
|
)
|
|
219
223
|
else:
|
|
220
|
-
|
|
224
|
+
acceptance = {
|
|
221
225
|
field: match[field]
|
|
222
226
|
for field in ("approvedAt", "expiresAt", "approvedBy", "reason", "remediationIssue")
|
|
223
|
-
}
|
|
227
|
+
}
|
|
228
|
+
if "compensatingControls" in match:
|
|
229
|
+
acceptance["compensatingControls"] = match["compensatingControls"]
|
|
230
|
+
accepted.append({**context, "acceptance": acceptance})
|
|
224
231
|
print(
|
|
225
232
|
f"Accepted temporary risk: {identifier} ({package}@{version}) "
|
|
226
233
|
f"until {match['expiresAt']} by {match['approvedBy']}; "
|
|
@@ -7,6 +7,9 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
7
7
|
HELPER="$SCRIPT_DIR/evaluate-npm-audit.sh"
|
|
8
8
|
WORK="$(mktemp -d)"
|
|
9
9
|
trap 'rm -rf "$WORK"' EXIT
|
|
10
|
+
APPROVED_AT="$(date -u -d 'yesterday' +%Y-%m-%d)"
|
|
11
|
+
EXPIRES_AT="$(date -u -d '+30 days' +%Y-%m-%d)"
|
|
12
|
+
EXPIRED_AT="$(date -u -d 'yesterday' +%Y-%m-%d)"
|
|
10
13
|
|
|
11
14
|
mkdir -p "$WORK/compliance/security"
|
|
12
15
|
|
|
@@ -66,11 +69,16 @@ write_exception() {
|
|
|
66
69
|
"expiresAt": "2026-12-31",
|
|
67
70
|
"approvedBy": "reviewer@example.test",
|
|
68
71
|
"reason": "Fixture acceptance while the introducing dependency awaits an upstream repair.",
|
|
69
|
-
"remediationIssue": "https://github.com/example/repo/issues/1"
|
|
72
|
+
"remediationIssue": "https://github.com/example/repo/issues/1",
|
|
73
|
+
"compensatingControls": "Monitor upstream releases and retain the unaffected root package."
|
|
70
74
|
}
|
|
71
75
|
]
|
|
72
76
|
}
|
|
73
77
|
JSON
|
|
78
|
+
jq --arg approved "$APPROVED_AT" --arg expires "$EXPIRES_AT" \
|
|
79
|
+
'.exceptions[0].approvedAt = $approved | .exceptions[0].expiresAt = $expires' \
|
|
80
|
+
"$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
81
|
+
mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
run_helper() {
|
|
@@ -107,7 +115,7 @@ jq -e '.summary.accepted == 0 and .summary.unresolved == 0' "$WORK/dependency-ri
|
|
|
107
115
|
write_audit
|
|
108
116
|
write_exception
|
|
109
117
|
expect_success
|
|
110
|
-
jq -e '.summary.accepted == 1 and .summary.unresolved == 0 and .accepted[0].acceptance.remediationIssue == "https://github.com/example/repo/issues/1"' \
|
|
118
|
+
jq -e '.summary.accepted == 1 and .summary.unresolved == 0 and .accepted[0].acceptance.remediationIssue == "https://github.com/example/repo/issues/1" and (.accepted[0].acceptance.compensatingControls | length > 0)' \
|
|
111
119
|
"$WORK/dependency-risk-evaluation.json" >/dev/null
|
|
112
120
|
|
|
113
121
|
jq '.exceptions[0].advisoryId = "GHSA-wrong"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
@@ -140,7 +148,7 @@ expect_failure
|
|
|
140
148
|
|
|
141
149
|
write_exception
|
|
142
150
|
write_audit
|
|
143
|
-
jq '.exceptions[0].expiresAt =
|
|
151
|
+
jq --arg expired "$EXPIRED_AT" '.exceptions[0].expiresAt = $expired' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
144
152
|
mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
|
|
145
153
|
expect_failure
|
|
146
154
|
|
|
@@ -150,6 +158,18 @@ jq '.exceptions[0].expiresAt = "not-a-date"' "$WORK/compliance/security/accepted
|
|
|
150
158
|
mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
|
|
151
159
|
expect_failure
|
|
152
160
|
|
|
161
|
+
write_exception
|
|
162
|
+
write_audit
|
|
163
|
+
jq '.exceptions[0].remediationIssue = "https://github.com/example/repo"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
164
|
+
mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
|
|
165
|
+
expect_failure
|
|
166
|
+
|
|
167
|
+
write_exception
|
|
168
|
+
write_audit
|
|
169
|
+
jq '.exceptions[0].compensatingControls = []' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
170
|
+
mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
|
|
171
|
+
expect_failure
|
|
172
|
+
|
|
153
173
|
write_exception
|
|
154
174
|
write_audit
|
|
155
175
|
jq 'del(.exceptions[0].approvedAt)' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Context-aware helpers for simple GitHub-flavoured Markdown tables.
|
|
3
|
+
|
|
4
|
+
markdown_table_cell() {
|
|
5
|
+
local file="$1"
|
|
6
|
+
local key_value="$2"
|
|
7
|
+
local target_header="$3"
|
|
8
|
+
shift 3
|
|
9
|
+
local required_headers=("$@")
|
|
10
|
+
|
|
11
|
+
[ -f "$file" ] || return 1
|
|
12
|
+
awk \
|
|
13
|
+
-v key_value="$key_value" \
|
|
14
|
+
-v target_header="$target_header" \
|
|
15
|
+
-v required_headers="$(IFS=,; printf '%s' "${required_headers[*]}")" '
|
|
16
|
+
function clean(value) {
|
|
17
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
18
|
+
gsub(/\001/, "\\|", value)
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
function split_row(line, cells, protected) {
|
|
22
|
+
protected=line
|
|
23
|
+
gsub(/\\\|/, "\001", protected)
|
|
24
|
+
return split(protected, cells, "|")
|
|
25
|
+
}
|
|
26
|
+
function header_matches(name, required, options, count, i) {
|
|
27
|
+
count=split(required, options, "/")
|
|
28
|
+
for (i=1; i<=count; i++) {
|
|
29
|
+
if (tolower(name) == tolower(options[i])) return 1
|
|
30
|
+
}
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
BEGIN {
|
|
34
|
+
required_count=split(required_headers, required, ",")
|
|
35
|
+
active=0
|
|
36
|
+
}
|
|
37
|
+
/^\|/ {
|
|
38
|
+
count=split_row($0, cells)
|
|
39
|
+
candidate_target=0
|
|
40
|
+
candidate_key=0
|
|
41
|
+
matched=0
|
|
42
|
+
for (i=2; i<count; i++) {
|
|
43
|
+
name=clean(cells[i])
|
|
44
|
+
if (tolower(name) == tolower(target_header)) candidate_target=i
|
|
45
|
+
for (r=1; r<=required_count; r++) {
|
|
46
|
+
if (header_matches(name, required[r])) matched++
|
|
47
|
+
}
|
|
48
|
+
if (header_matches(name, required[1])) candidate_key=i
|
|
49
|
+
}
|
|
50
|
+
if (candidate_target && candidate_key && matched >= required_count) {
|
|
51
|
+
active=1
|
|
52
|
+
target_index=candidate_target
|
|
53
|
+
key_index=candidate_key
|
|
54
|
+
next
|
|
55
|
+
}
|
|
56
|
+
if (active && key_index && target_index && clean(cells[key_index]) == key_value) {
|
|
57
|
+
print clean(cells[target_index])
|
|
58
|
+
found=1
|
|
59
|
+
exit 0
|
|
60
|
+
}
|
|
61
|
+
next
|
|
62
|
+
}
|
|
63
|
+
{ active=0; key_index=0; target_index=0 }
|
|
64
|
+
END { if (!found) exit 1 }
|
|
65
|
+
' "$file"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
markdown_table_column_index_for_row() {
|
|
69
|
+
local file="$1"
|
|
70
|
+
local key_value="$2"
|
|
71
|
+
local target_header="$3"
|
|
72
|
+
shift 3
|
|
73
|
+
local required_headers=("$@")
|
|
74
|
+
|
|
75
|
+
[ -f "$file" ] || return 1
|
|
76
|
+
awk \
|
|
77
|
+
-v key_value="$key_value" \
|
|
78
|
+
-v target_header="$target_header" \
|
|
79
|
+
-v required_headers="$(IFS=,; printf '%s' "${required_headers[*]}")" '
|
|
80
|
+
function clean(value) {
|
|
81
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
82
|
+
return value
|
|
83
|
+
}
|
|
84
|
+
function split_row(line, cells, protected) {
|
|
85
|
+
protected=line
|
|
86
|
+
gsub(/\\\|/, "\001", protected)
|
|
87
|
+
return split(protected, cells, "|")
|
|
88
|
+
}
|
|
89
|
+
function header_matches(name, required, options, count, i) {
|
|
90
|
+
count=split(required, options, "/")
|
|
91
|
+
for (i=1; i<=count; i++) {
|
|
92
|
+
if (tolower(name) == tolower(options[i])) return 1
|
|
93
|
+
}
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
BEGIN { required_count=split(required_headers, required, ","); active=0 }
|
|
97
|
+
/^\|/ {
|
|
98
|
+
count=split_row($0, cells)
|
|
99
|
+
candidate_target=0
|
|
100
|
+
candidate_key=0
|
|
101
|
+
matched=0
|
|
102
|
+
for (i=2; i<count; i++) {
|
|
103
|
+
name=clean(cells[i])
|
|
104
|
+
if (tolower(name) == tolower(target_header)) candidate_target=i
|
|
105
|
+
for (r=1; r<=required_count; r++) {
|
|
106
|
+
if (header_matches(name, required[r])) matched++
|
|
107
|
+
}
|
|
108
|
+
if (header_matches(name, required[1])) candidate_key=i
|
|
109
|
+
}
|
|
110
|
+
if (candidate_target && candidate_key && matched >= required_count) {
|
|
111
|
+
active=1
|
|
112
|
+
target_index=candidate_target
|
|
113
|
+
key_index=candidate_key
|
|
114
|
+
next
|
|
115
|
+
}
|
|
116
|
+
if (active && clean(cells[key_index]) == key_value) {
|
|
117
|
+
print target_index
|
|
118
|
+
found=1
|
|
119
|
+
exit 0
|
|
120
|
+
}
|
|
121
|
+
next
|
|
122
|
+
}
|
|
123
|
+
{ active=0 }
|
|
124
|
+
END { if (!found) exit 1 }
|
|
125
|
+
' "$file"
|
|
126
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=markdown-table.sh
|
|
6
|
+
source "$SCRIPT_DIR/markdown-table.sh"
|
|
7
|
+
|
|
8
|
+
WORK="$(mktemp -d)"
|
|
9
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
10
|
+
|
|
11
|
+
cat > "$WORK/RTM.md" <<'MARKDOWN'
|
|
12
|
+
| Status | Description |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Draft | This is not the requirements table |
|
|
15
|
+
|
|
16
|
+
| REQ-ID | Issue | Risk | Evidence | Status |
|
|
17
|
+
| --- | --- | --- | --- | --- |
|
|
18
|
+
| REQ-100 | #100 | HIGH | report.md | TESTED - PENDING SIGN-OFF (note \| retained) |
|
|
19
|
+
MARKDOWN
|
|
20
|
+
|
|
21
|
+
[ "$(markdown_table_cell "$WORK/RTM.md" REQ-100 Status REQ-ID/ID Status)" = \
|
|
22
|
+
'TESTED - PENDING SIGN-OFF (note \| retained)' ]
|
|
23
|
+
[ "$(markdown_table_cell "$WORK/RTM.md" REQ-100 Risk REQ-ID/ID Risk)" = "HIGH" ]
|
|
24
|
+
[ "$(markdown_table_column_index_for_row "$WORK/RTM.md" REQ-100 Status REQ-ID/ID Status)" = "5" ]
|
|
25
|
+
|
|
26
|
+
if markdown_table_cell "$WORK/RTM.md" REQ-404 Status REQ-ID/ID Status >/dev/null; then
|
|
27
|
+
echo "missing row unexpectedly matched" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if markdown_table_cell "$WORK/RTM.md" Draft Status REQ-ID/ID Status >/dev/null; then
|
|
32
|
+
echo "legend row unexpectedly matched" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "markdown table tests passed"
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
# shellcheck source=markdown-table.sh
|
|
17
|
+
source "$SCRIPT_DIR/markdown-table.sh"
|
|
18
|
+
|
|
15
19
|
BASE_BRANCH="${1:-origin/main}"
|
|
16
20
|
EXIT_CODE=0
|
|
17
21
|
|
|
@@ -53,6 +57,7 @@ find_first_match() {
|
|
|
53
57
|
local match=""
|
|
54
58
|
match=$(compgen -G "$pattern" | head -1 || true)
|
|
55
59
|
[ -n "$match" ] && printf '%s' "$match"
|
|
60
|
+
return 0
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
require_bundled_fields() {
|
|
@@ -81,7 +86,9 @@ for REQ in $REQUIREMENTS; do
|
|
|
81
86
|
# The `| $REQ ` pattern (leading pipe + trailing space) matches the
|
|
82
87
|
# markdown table row used in RTM, so a substring elsewhere doesn't
|
|
83
88
|
# accidentally satisfy it.
|
|
84
|
-
|
|
89
|
+
RTM_STATUS="$(markdown_table_cell \
|
|
90
|
+
compliance/RTM.md "$REQ" Status REQ-ID/ID Status 2>/dev/null || true)"
|
|
91
|
+
if [ -z "$RTM_STATUS" ]; then
|
|
85
92
|
echo " INFO: $REQ is referenced in commits but has no RTM row — skipping (forward-reference, not a tracked requirement for this release)"
|
|
86
93
|
continue
|
|
87
94
|
fi
|
|
@@ -187,12 +194,12 @@ for REQ in $REQUIREMENTS; do
|
|
|
187
194
|
# by a successor (e.g. REQ-030 superseded by REQ-031). Without this
|
|
188
195
|
# branch every PR with a commit referencing a SUPERSEDED REQ blocks
|
|
189
196
|
# CI (#232).
|
|
190
|
-
if
|
|
191
|
-
if
|
|
197
|
+
if [ -n "$RTM_STATUS" ]; then
|
|
198
|
+
if printf '%s' "$RTM_STATUS" | grep -q "TESTED - PENDING SIGN-OFF"; then
|
|
192
199
|
echo " OK: RTM status is TESTED - PENDING SIGN-OFF"
|
|
193
|
-
elif
|
|
200
|
+
elif printf '%s' "$RTM_STATUS" | grep -q "APPROVED"; then
|
|
194
201
|
echo " OK: RTM status is APPROVED"
|
|
195
|
-
elif
|
|
202
|
+
elif printf '%s' "$RTM_STATUS" | grep -q "SUPERSEDED"; then
|
|
196
203
|
echo " OK: RTM status is SUPERSEDED"
|
|
197
204
|
else
|
|
198
205
|
echo " WARNING: RTM entry exists but status is not TESTED - PENDING SIGN-OFF"
|
|
@@ -371,7 +378,9 @@ for REQ in $REQUIREMENTS; do
|
|
|
371
378
|
fi
|
|
372
379
|
|
|
373
380
|
# Check implementation-plan.md for MEDIUM/HIGH risk (check RTM for risk level)
|
|
374
|
-
|
|
381
|
+
RTM_RISK="$(markdown_table_cell \
|
|
382
|
+
compliance/RTM.md "$REQ" Risk REQ-ID/ID Risk 2>/dev/null || true)"
|
|
383
|
+
if printf '%s' "$RTM_RISK" | grep -qiE 'MEDIUM|HIGH'; then
|
|
375
384
|
if [ ! -f "compliance/evidence/$REQ/implementation-plan.md" ]; then
|
|
376
385
|
echo " ERROR: Implementation plan missing for MEDIUM/HIGH risk: compliance/evidence/$REQ/implementation-plan.md"
|
|
377
386
|
EXIT_CODE=1
|
|
@@ -341,7 +341,7 @@ Reached from Phase 0 for non-tracked change-types. The skill drives this end-to-
|
|
|
341
341
|
- **Post-merge-only CI (older generated workflows — `push: branches: [<integration>]` with no `pull_request:` trigger)** — say so explicitly in the LAST/NEXT sticky: _"no PR-time checks will fire; review + merge is the gate; CI runs post-merge on `$INTEGRATION_BRANCH`."_ Don't poll the PR for checks that won't arrive. The post-merge run (CI Pipeline + Compliance Evidence Upload on the integration branch) is the actual gate; address it via fix-forward if it fails.
|
|
342
342
|
|
|
343
343
|
Either way, never bypass a gate (no `--no-verify`, no `--admin` merge of a red required check); the only difference is **where** you wait for the gate to fire — before merge vs. after merge.
|
|
344
|
-
8. **Guide review -> merge.** A human still reviews the PR. Merge only after all required checks are terminal green on the current PR head SHA; queued, running, stale, cancelled, unexpectedly skipped, or failed checks are not green. There is no portal release approval, UAT four-eyes, Production gate, or standalone close-out on the normal lightweight path.
|
|
344
|
+
8. **Guide review -> merge.** A human still reviews the PR. Merge only after all required checks are terminal green on the current PR head SHA; queued, running, stale, cancelled, unexpectedly skipped, or failed checks are not green. If GitHub leaves a stale projection, use the repository's **Recover Required Checks** workflow: dry-run first, then apply only to rerun a resolvable completed Actions run on the same SHA. When GitHub never created the required suite, push an auditable empty commit to the PR branch and verify the new SHA; never fabricate status or bypass protection. There is no portal release approval, UAT four-eyes, Production gate, or standalone close-out on the normal lightweight path.
|
|
345
345
|
9. **Record the correct release meaning.** A housekeeping push produces at most a bare-date release (`vYYYY.MM.DD`) that is integration/history, not an active approval by default. It waits on `$INTEGRATION_BRANCH` until a later tracked release explicitly absorbs it through the required bundled-changes markdown/JSON manifest and portal lineage. Never infer that an unrelated hotfix or mandatory back-merge belongs to the only pending REQ. A submitted REQ bundle is frozen approval scope; changing it returns that REQ to evidence compilation and UAT resubmission. A doc-only push attaches documents to the existing `REQ-XXX` release.
|
|
346
346
|
10. **Standalone exception.** Only when housekeeping cannot reasonably wait for the next tracked release may the skill prepare a standalone promotion. It must state `Standalone housekeeping promotion` and why in the release PR, add `compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-vYYYY.MM.DD.json` with `schemaVersion: 1`, the exact derived bare-date version, `releaseMode: standalone_housekeeping`, and a specific reason. It must obtain terminal-green CI and PR review. The installed release-scope check validates the declaration before merge; post-deploy uploads it as release-ticket evidence and changes the portal row to standalone housekeeping. Use portal approval only if project policy explicitly opts in. If the installed workflow cannot implement that policy, halt and keep the change on the normal integration path.
|
|
347
347
|
|
|
@@ -22,6 +22,10 @@ on:
|
|
|
22
22
|
pull_request:
|
|
23
23
|
branches: [main]
|
|
24
24
|
workflow_dispatch:
|
|
25
|
+
inputs:
|
|
26
|
+
runner_label:
|
|
27
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
28
|
+
required: false
|
|
25
29
|
repository_dispatch:
|
|
26
30
|
types: [release-approved]
|
|
27
31
|
|
|
@@ -15,6 +15,10 @@ name: CI Pipeline
|
|
|
15
15
|
|
|
16
16
|
on:
|
|
17
17
|
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
runner_label:
|
|
20
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
21
|
+
required: false
|
|
18
22
|
pull_request:
|
|
19
23
|
branches: [{{INTEGRATION_BRANCH}}]
|
|
20
24
|
push:
|
|
@@ -178,7 +182,9 @@ jobs:
|
|
|
178
182
|
{{E2E_DEV_SERVER_STEP}}
|
|
179
183
|
|
|
180
184
|
- name: Wait for dev server
|
|
181
|
-
|
|
185
|
+
# The inner wait-on timeout is diagnostic; the outer deadline keeps a
|
|
186
|
+
# hung child from consuming the entire quality-gates job budget.
|
|
187
|
+
run: timeout --signal=TERM --kill-after=15s 150s npx wait-on http://localhost:3000 --timeout 120000
|
|
182
188
|
|
|
183
189
|
# Compute the set of e2e spec files added on this branch (relative
|
|
184
190
|
# to the merge base). The evidenceShot helper in the consumer's
|
|
@@ -352,8 +358,10 @@ jobs:
|
|
|
352
358
|
fi
|
|
353
359
|
chmod +x scripts/standalone-housekeeping-release.sh scripts/upload-evidence.sh 2>/dev/null || true
|
|
354
360
|
bash scripts/standalone-housekeeping-release.sh validate "$VERSION" "$DECLARATION"
|
|
361
|
+
# A bare-date version identifies the release, not a requirement. Use
|
|
362
|
+
# the portal's common evidence scope while preserving release ownership.
|
|
355
363
|
bash scripts/upload-evidence.sh \
|
|
356
|
-
{{PROJECT_SLUG}}
|
|
364
|
+
{{PROJECT_SLUG}} _compliance-docs release_ticket "$DECLARATION" \
|
|
357
365
|
--release "$VERSION" --create-release-if-missing \
|
|
358
366
|
--environment uat --category release_artifact --sdlc-stage 2 \
|
|
359
367
|
--git-sha ${{ github.sha }} --branch ${{ github.ref_name }}
|
|
@@ -28,6 +28,9 @@ on:
|
|
|
28
28
|
release_pr:
|
|
29
29
|
description: 'Release PR number or URL to backlink (optional)'
|
|
30
30
|
required: false
|
|
31
|
+
runner_label:
|
|
32
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
33
|
+
required: false
|
|
31
34
|
repository_dispatch:
|
|
32
35
|
types: [release-closed]
|
|
33
36
|
|
|
@@ -21,6 +21,10 @@ name: Compliance Evidence Upload
|
|
|
21
21
|
|
|
22
22
|
on:
|
|
23
23
|
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
runner_label:
|
|
26
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
27
|
+
required: false
|
|
24
28
|
push:
|
|
25
29
|
branches: [develop]
|
|
26
30
|
paths:
|
|
@@ -443,16 +447,15 @@ jobs:
|
|
|
443
447
|
DERIVED_RELEASE="${REQS[0]}"
|
|
444
448
|
fi
|
|
445
449
|
|
|
446
|
-
if [ "$PRIOR_EVENT" = "deployment_status" ] && [ "${#REQS[@]}" -eq 0 ]; then
|
|
447
|
-
echo "::error::Deployment-origin E2E evidence requires tagged or in-scope REQ attribution; refusing _compliance-docs fallback."
|
|
448
|
-
exit 1
|
|
449
|
-
fi
|
|
450
|
-
|
|
451
450
|
if [ "${#REQS[@]}" -eq 0 ]; then
|
|
452
451
|
if echo "$DERIVED_RELEASE" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'; then
|
|
453
452
|
echo "No tracked REQ was executed for standalone/integration housekeeping ${DERIVED_RELEASE}; preserving this as GitHub historical CI without portal approval evidence."
|
|
454
453
|
exit 0
|
|
455
454
|
fi
|
|
455
|
+
if [ "$PRIOR_EVENT" = "deployment_status" ]; then
|
|
456
|
+
echo "::error::Deployment-origin E2E evidence requires tagged or in-scope REQ attribution; refusing _compliance-docs fallback."
|
|
457
|
+
exit 1
|
|
458
|
+
fi
|
|
456
459
|
REQS=(_compliance-docs)
|
|
457
460
|
fi
|
|
458
461
|
# A bounded Playwright process exits the workflow with `failure`
|
|
@@ -61,7 +61,7 @@ jobs:
|
|
|
61
61
|
- uses: actions/checkout@v6
|
|
62
62
|
with:
|
|
63
63
|
fetch-depth: 0
|
|
64
|
-
- uses: actions/setup-node@
|
|
64
|
+
- uses: actions/setup-node@v6
|
|
65
65
|
with:
|
|
66
66
|
node-version: {{NODE_VERSION}}
|
|
67
67
|
- name: Install dependencies (skip if lockfile unchanged)
|
|
@@ -84,7 +84,8 @@ jobs:
|
|
|
84
84
|
{{E2E_DEV_SERVER_STEP}}
|
|
85
85
|
|
|
86
86
|
- name: Wait for dev server
|
|
87
|
-
|
|
87
|
+
# Keep a process-level bound in addition to wait-on's internal timer.
|
|
88
|
+
run: timeout --signal=TERM --kill-after=15s 150s npx wait-on http://localhost:3000 --timeout 120000
|
|
88
89
|
|
|
89
90
|
# Compute the set of e2e spec files added on this branch (relative
|
|
90
91
|
# to the merge base). The evidenceShot helper reads E2E_NEW_SPECS
|
|
@@ -29,6 +29,9 @@ on:
|
|
|
29
29
|
release:
|
|
30
30
|
description: 'Optional REQ-XXX / version to promote (blank = all in-scope from pending release tickets).'
|
|
31
31
|
required: false
|
|
32
|
+
runner_label:
|
|
33
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
34
|
+
required: false
|
|
32
35
|
deployment_status:
|
|
33
36
|
types: [created]
|
|
34
37
|
|
|
@@ -14,6 +14,10 @@ name: CI Pipeline
|
|
|
14
14
|
|
|
15
15
|
on:
|
|
16
16
|
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
runner_label:
|
|
19
|
+
description: 'Optional one-run runner label override. Blank uses repository CI_RUNNER_LABEL.'
|
|
20
|
+
required: false
|
|
17
21
|
pull_request:
|
|
18
22
|
branches: [{{INTEGRATION_BRANCH}}]
|
|
19
23
|
push:
|
|
@@ -311,11 +315,10 @@ jobs:
|
|
|
311
315
|
name: Upload Evidence
|
|
312
316
|
runs-on: {{RUNNER}}
|
|
313
317
|
needs: [quality-gates, register-release]
|
|
314
|
-
#
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
if: ${{ always() && !cancelled() && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
|
|
318
|
+
# Pull requests do not register or upload release evidence. Keeping this
|
|
319
|
+
# intentionally inapplicable job out of `always()` avoids a cancelled
|
|
320
|
+
# aggregate workflow when Register Release is correctly skipped (#500).
|
|
321
|
+
if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref_name == '{{INTEGRATION_BRANCH}}' && needs.register-release.result == 'success' }}
|
|
319
322
|
env:
|
|
320
323
|
DEVAUDIT_BASE_URL_VAR: ${{ vars.DEVAUDIT_BASE_URL }}
|
|
321
324
|
DEVAUDIT_API_KEY: ${{ secrets.DEVAUDIT_API_KEY }}
|
package/sdlc/package.json
CHANGED