@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,227 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# render-test-cycles.sh — Render release test cycles as markdown tables.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash scripts/render-test-cycles.sh path/to/release-journey.json
|
|
6
|
+
#
|
|
7
|
+
# Supported inputs:
|
|
8
|
+
# 1. First-class cycle payloads with `.cycles[]`
|
|
9
|
+
# 2. Legacy grouped-evidence payloads with one of:
|
|
10
|
+
# - `.legacyCycles[]`
|
|
11
|
+
# - `.testCycleGroups[]`
|
|
12
|
+
# - `.evidenceGroups[]`
|
|
13
|
+
#
|
|
14
|
+
# Output:
|
|
15
|
+
# Markdown suitable for inclusion under `## Test Cycles` in
|
|
16
|
+
# `compliance/evidence/REQ-XXX/test-execution-summary.md`.
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
INPUT_JSON="${1:-}"
|
|
21
|
+
|
|
22
|
+
if [ -z "$INPUT_JSON" ] || [ ! -f "$INPUT_JSON" ]; then
|
|
23
|
+
echo "Usage: $0 <release-journey.json>" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
28
|
+
echo "Error: jq is required." >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
stage_label() {
|
|
33
|
+
case "$1" in
|
|
34
|
+
1) printf '1 plan' ;;
|
|
35
|
+
2) printf '2 implement_test' ;;
|
|
36
|
+
3) printf '3 compile_evidence' ;;
|
|
37
|
+
4) printf '4 uat_review' ;;
|
|
38
|
+
5) printf '5 production' ;;
|
|
39
|
+
*) printf '%s unknown' "${1:-?}" ;;
|
|
40
|
+
esac
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
format_date() {
|
|
44
|
+
local value="$1"
|
|
45
|
+
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
|
46
|
+
printf 'Unknown'
|
|
47
|
+
else
|
|
48
|
+
printf '%s' "$value" | sed -E 's/T/ /; s/Z$//; s/[.][0-9]+$//'
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render_first_class_cycles() {
|
|
53
|
+
echo "| Source Release | SDLC Stage | Cycle | Kind | Outcome | Workflow / Run | Related Evidence | Incident / Remediation | Date |"
|
|
54
|
+
echo "| --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
55
|
+
|
|
56
|
+
declare -A COUNTS=()
|
|
57
|
+
local rows
|
|
58
|
+
rows="$(
|
|
59
|
+
jq -r '
|
|
60
|
+
(.cycles // [])
|
|
61
|
+
| sort_by(.sourceRelease // "", .sdlcStage // 0, .startedAt // "", .completedAt // "", .cycleKind // "")
|
|
62
|
+
| .[]
|
|
63
|
+
| {
|
|
64
|
+
sourceRelease: (.sourceRelease // "Unknown"),
|
|
65
|
+
stageCode: ((.sdlcStage // 0) | tostring),
|
|
66
|
+
kind: (.cycleKind // "unknown"),
|
|
67
|
+
outcome: (.outcome // "unknown"),
|
|
68
|
+
workflow: (
|
|
69
|
+
if (.workflowUrl // "") != "" then
|
|
70
|
+
"[" + ((.workflowName // "Workflow")) + "](" + .workflowUrl + ")"
|
|
71
|
+
else
|
|
72
|
+
(.workflowName // "Manual")
|
|
73
|
+
end
|
|
74
|
+
),
|
|
75
|
+
runMeta: (
|
|
76
|
+
[
|
|
77
|
+
(if (.externalRunId // "") != "" then "run " + .externalRunId else empty end),
|
|
78
|
+
(if (.externalRunAttempt // null) != null then "attempt " + (.externalRunAttempt | tostring) else empty end)
|
|
79
|
+
]
|
|
80
|
+
| map(select(length > 0))
|
|
81
|
+
| join(", ")
|
|
82
|
+
),
|
|
83
|
+
evidence: (
|
|
84
|
+
(.relatedEvidence // [])
|
|
85
|
+
| map(.displayName // .fileName // .name // .evidenceType // empty)
|
|
86
|
+
| map(select(length > 0))
|
|
87
|
+
| join(", ")
|
|
88
|
+
),
|
|
89
|
+
incident: (
|
|
90
|
+
[
|
|
91
|
+
(.incidentReference // empty),
|
|
92
|
+
(.remediationReference // empty),
|
|
93
|
+
(.outcomeReason // empty)
|
|
94
|
+
]
|
|
95
|
+
| map(select(length > 0))
|
|
96
|
+
| join("; ")
|
|
97
|
+
),
|
|
98
|
+
date: (.completedAt // .startedAt // "")
|
|
99
|
+
}
|
|
100
|
+
| @base64
|
|
101
|
+
' "$INPUT_JSON"
|
|
102
|
+
)"
|
|
103
|
+
|
|
104
|
+
if [ -z "$rows" ]; then
|
|
105
|
+
echo "| Unknown | Unknown | None | unknown | unknown | No cycle records returned | None | None | Unknown |"
|
|
106
|
+
echo
|
|
107
|
+
echo "**Final assessment:** No first-class cycle records were returned by the portal."
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
while IFS= read -r row_b64; do
|
|
112
|
+
[ -n "$row_b64" ] || continue
|
|
113
|
+
local source_release stage_code kind outcome workflow run_meta evidence incident date
|
|
114
|
+
source_release="$(printf '%s' "$row_b64" | base64 -d | jq -r '.sourceRelease')"
|
|
115
|
+
stage_code="$(printf '%s' "$row_b64" | base64 -d | jq -r '.stageCode')"
|
|
116
|
+
kind="$(printf '%s' "$row_b64" | base64 -d | jq -r '.kind')"
|
|
117
|
+
outcome="$(printf '%s' "$row_b64" | base64 -d | jq -r '.outcome')"
|
|
118
|
+
workflow="$(printf '%s' "$row_b64" | base64 -d | jq -r '.workflow')"
|
|
119
|
+
run_meta="$(printf '%s' "$row_b64" | base64 -d | jq -r '.runMeta')"
|
|
120
|
+
evidence="$(printf '%s' "$row_b64" | base64 -d | jq -r '.evidence')"
|
|
121
|
+
incident="$(printf '%s' "$row_b64" | base64 -d | jq -r '.incident')"
|
|
122
|
+
date="$(printf '%s' "$row_b64" | base64 -d | jq -r '.date')"
|
|
123
|
+
[ -n "$source_release" ] || continue
|
|
124
|
+
local group_key cycle_ordinal workflow_cell evidence_cell incident_cell
|
|
125
|
+
group_key="${source_release}|${stage_code}"
|
|
126
|
+
COUNTS["$group_key"]=$(( ${COUNTS["$group_key"]:-0} + 1 ))
|
|
127
|
+
cycle_ordinal="#${COUNTS[$group_key]}"
|
|
128
|
+
workflow_cell="$workflow"
|
|
129
|
+
if [ -n "$run_meta" ]; then
|
|
130
|
+
workflow_cell="${workflow_cell} (${run_meta})"
|
|
131
|
+
fi
|
|
132
|
+
evidence_cell="${evidence:-None}"
|
|
133
|
+
incident_cell="${incident:-None}"
|
|
134
|
+
printf '| %s | %s | %s | %s | %s | %s | %s | %s | %s |\n' \
|
|
135
|
+
"$source_release" \
|
|
136
|
+
"$(stage_label "$stage_code")" \
|
|
137
|
+
"$cycle_ordinal" \
|
|
138
|
+
"$kind" \
|
|
139
|
+
"$outcome" \
|
|
140
|
+
"$workflow_cell" \
|
|
141
|
+
"$evidence_cell" \
|
|
142
|
+
"$incident_cell" \
|
|
143
|
+
"$(format_date "$date")"
|
|
144
|
+
done <<<"$rows"
|
|
145
|
+
|
|
146
|
+
echo
|
|
147
|
+
echo "**Final assessment:** Stage-scoped cycle numbering is authoritative from first-class portal cycle records."
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render_legacy_cycles() {
|
|
151
|
+
echo "> Legacy portal fallback — first-class cycle records unavailable; grouped by uploaded evidence \`testCycleId\`."
|
|
152
|
+
echo
|
|
153
|
+
echo "| Source Release | SDLC Stage | Cycle | Kind | Outcome | Workflow / Run | Related Evidence | Incident / Remediation | Date |"
|
|
154
|
+
echo "| --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
155
|
+
|
|
156
|
+
local rows
|
|
157
|
+
rows="$(
|
|
158
|
+
jq -r '
|
|
159
|
+
(.legacyCycles // .testCycleGroups // .evidenceGroups // [])
|
|
160
|
+
| .[]
|
|
161
|
+
| {
|
|
162
|
+
sourceRelease: (.sourceRelease // "Legacy grouped evidence"),
|
|
163
|
+
stageLabel: (.sdlcStageLabel // "legacy grouping"),
|
|
164
|
+
cycleId: (.testCycleId // .cycleId // "unknown"),
|
|
165
|
+
kind: (.cycleKind // "legacy"),
|
|
166
|
+
outcome: (.outcome // "unknown"),
|
|
167
|
+
workflow: (
|
|
168
|
+
if (.workflowUrl // "") != "" then
|
|
169
|
+
"[" + ((.workflowName // "Workflow")) + "](" + .workflowUrl + ")"
|
|
170
|
+
else
|
|
171
|
+
(.workflowName // "Legacy grouping")
|
|
172
|
+
end
|
|
173
|
+
),
|
|
174
|
+
evidence: (
|
|
175
|
+
(.relatedEvidence // .evidence // [])
|
|
176
|
+
| map(.displayName // .fileName // .name // .evidenceType // .path // empty)
|
|
177
|
+
| map(select(length > 0))
|
|
178
|
+
| join(", ")
|
|
179
|
+
),
|
|
180
|
+
incident: (.incidentReference // .notes // "Legacy fallback"),
|
|
181
|
+
date: (.date // .createdAt // "")
|
|
182
|
+
}
|
|
183
|
+
| @base64
|
|
184
|
+
' "$INPUT_JSON"
|
|
185
|
+
)"
|
|
186
|
+
|
|
187
|
+
if [ -z "$rows" ]; then
|
|
188
|
+
echo "| Legacy grouped evidence | legacy grouping | unknown | legacy | unknown | Legacy grouping | None | Legacy fallback | Unknown |"
|
|
189
|
+
echo
|
|
190
|
+
echo "**Final assessment:** Legacy fallback used, but no grouped evidence records were returned."
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
while IFS= read -r row_b64; do
|
|
195
|
+
[ -n "$row_b64" ] || continue
|
|
196
|
+
local source_release stage_label_value cycle_id kind outcome workflow evidence incident date
|
|
197
|
+
source_release="$(printf '%s' "$row_b64" | base64 -d | jq -r '.sourceRelease')"
|
|
198
|
+
stage_label_value="$(printf '%s' "$row_b64" | base64 -d | jq -r '.stageLabel')"
|
|
199
|
+
cycle_id="$(printf '%s' "$row_b64" | base64 -d | jq -r '.cycleId')"
|
|
200
|
+
kind="$(printf '%s' "$row_b64" | base64 -d | jq -r '.kind')"
|
|
201
|
+
outcome="$(printf '%s' "$row_b64" | base64 -d | jq -r '.outcome')"
|
|
202
|
+
workflow="$(printf '%s' "$row_b64" | base64 -d | jq -r '.workflow')"
|
|
203
|
+
evidence="$(printf '%s' "$row_b64" | base64 -d | jq -r '.evidence')"
|
|
204
|
+
incident="$(printf '%s' "$row_b64" | base64 -d | jq -r '.incident')"
|
|
205
|
+
date="$(printf '%s' "$row_b64" | base64 -d | jq -r '.date')"
|
|
206
|
+
[ -n "$cycle_id" ] || continue
|
|
207
|
+
printf '| %s | %s | %s | %s | %s | %s | %s | %s | %s |\n' \
|
|
208
|
+
"$source_release" \
|
|
209
|
+
"$stage_label_value" \
|
|
210
|
+
"$cycle_id" \
|
|
211
|
+
"$kind" \
|
|
212
|
+
"$outcome" \
|
|
213
|
+
"$workflow" \
|
|
214
|
+
"${evidence:-None}" \
|
|
215
|
+
"${incident:-Legacy fallback}" \
|
|
216
|
+
"$(format_date "$date")"
|
|
217
|
+
done <<<"$rows"
|
|
218
|
+
|
|
219
|
+
echo
|
|
220
|
+
echo "**Final assessment:** Legacy grouping was used because the portal did not expose first-class cycle records."
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if jq -e '((.cycles // []) | length) > 0' "$INPUT_JSON" >/dev/null 2>&1; then
|
|
224
|
+
render_first_class_cycles
|
|
225
|
+
else
|
|
226
|
+
render_legacy_cycles
|
|
227
|
+
fi
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# render-test-cycles.test.sh — Tests for render-test-cycles.sh.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
HELPER="$SCRIPT_DIR/render-test-cycles.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"
|
|
17
|
+
local needle="$2"
|
|
18
|
+
local haystack="$3"
|
|
19
|
+
if printf '%s' "$haystack" | grep -Fq "$needle"; then
|
|
20
|
+
echo " PASS: $desc"
|
|
21
|
+
PASS=$((PASS + 1))
|
|
22
|
+
else
|
|
23
|
+
echo " FAIL: $desc"
|
|
24
|
+
echo " missing: $needle"
|
|
25
|
+
echo "$haystack" | sed 's/^/ /'
|
|
26
|
+
FAIL=$((FAIL + 1))
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
echo "=== render-test-cycles.sh tests ==="
|
|
31
|
+
|
|
32
|
+
cat > "$WORK/first-class.json" <<'EOF'
|
|
33
|
+
{
|
|
34
|
+
"cycles": [
|
|
35
|
+
{
|
|
36
|
+
"sourceRelease": "REQ-091",
|
|
37
|
+
"sdlcStage": 2,
|
|
38
|
+
"cycleKind": "quality_gate",
|
|
39
|
+
"outcome": "failed",
|
|
40
|
+
"workflowName": "Quality Gates",
|
|
41
|
+
"workflowUrl": "https://github.com/example/repo/actions/runs/100",
|
|
42
|
+
"externalRunId": "100",
|
|
43
|
+
"externalRunAttempt": 1,
|
|
44
|
+
"relatedEvidence": [{ "displayName": "quality-gates.json" }],
|
|
45
|
+
"incidentReference": "#501",
|
|
46
|
+
"startedAt": "2026-07-15T08:00:00Z",
|
|
47
|
+
"completedAt": "2026-07-15T08:05:00Z"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"sourceRelease": "REQ-091",
|
|
51
|
+
"sdlcStage": 2,
|
|
52
|
+
"cycleKind": "quality_gate",
|
|
53
|
+
"outcome": "passed",
|
|
54
|
+
"workflowName": "Quality Gates",
|
|
55
|
+
"workflowUrl": "https://github.com/example/repo/actions/runs/101",
|
|
56
|
+
"externalRunId": "101",
|
|
57
|
+
"externalRunAttempt": 2,
|
|
58
|
+
"relatedEvidence": [{ "displayName": "quality-gates-rerun.json" }],
|
|
59
|
+
"remediationReference": "PR #507",
|
|
60
|
+
"startedAt": "2026-07-15T09:00:00Z",
|
|
61
|
+
"completedAt": "2026-07-15T09:07:00Z"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"sourceRelease": "REQ-090",
|
|
65
|
+
"sdlcStage": 4,
|
|
66
|
+
"cycleKind": "uat",
|
|
67
|
+
"outcome": "passed",
|
|
68
|
+
"workflowName": "UAT Review",
|
|
69
|
+
"workflowUrl": "https://portal.example/releases/req-090",
|
|
70
|
+
"relatedEvidence": [{ "displayName": "uat-checklist.md" }],
|
|
71
|
+
"startedAt": "2026-07-15T10:00:00Z",
|
|
72
|
+
"completedAt": "2026-07-15T10:12:00Z"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
EOF
|
|
77
|
+
|
|
78
|
+
OUTPUT="$(bash "$HELPER" "$WORK/first-class.json")"
|
|
79
|
+
assert_contains "first-class heading rendered" "| Source Release | SDLC Stage | Cycle | Kind | Outcome | Workflow / Run | Related Evidence | Incident / Remediation | Date |" "$OUTPUT"
|
|
80
|
+
assert_contains "stage label rendered" "| REQ-091 | 2 implement_test | #1 | quality_gate | failed | [Quality Gates](https://github.com/example/repo/actions/runs/100) (run 100, attempt 1) | quality-gates.json | #501 | 2026-07-15 08:05:00 |" "$OUTPUT"
|
|
81
|
+
assert_contains "ordinal increments within source release + stage" "| REQ-091 | 2 implement_test | #2 | quality_gate | passed | [Quality Gates](https://github.com/example/repo/actions/runs/101) (run 101, attempt 2) | quality-gates-rerun.json | PR #507 | 2026-07-15 09:07:00 |" "$OUTPUT"
|
|
82
|
+
assert_contains "different release/stage restarts numbering" "| REQ-090 | 4 uat_review | #1 | uat | passed | [UAT Review](https://portal.example/releases/req-090) | uat-checklist.md | None | 2026-07-15 10:12:00 |" "$OUTPUT"
|
|
83
|
+
assert_contains "first-class final assessment rendered" "Stage-scoped cycle numbering is authoritative from first-class portal cycle records." "$OUTPUT"
|
|
84
|
+
|
|
85
|
+
cat > "$WORK/legacy.json" <<'EOF'
|
|
86
|
+
{
|
|
87
|
+
"legacyCycles": [
|
|
88
|
+
{
|
|
89
|
+
"testCycleId": "287654321",
|
|
90
|
+
"workflowName": "Legacy E2E",
|
|
91
|
+
"workflowUrl": "https://github.com/example/repo/actions/runs/287654321",
|
|
92
|
+
"relatedEvidence": [{ "fileName": "e2e-results.json" }, { "fileName": "screenshot.png" }],
|
|
93
|
+
"notes": "grouped from uploaded evidence only",
|
|
94
|
+
"date": "2026-07-14T18:30:00Z"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
EOF
|
|
99
|
+
|
|
100
|
+
OUTPUT="$(bash "$HELPER" "$WORK/legacy.json")"
|
|
101
|
+
assert_contains "legacy fallback note rendered" "Legacy portal fallback — first-class cycle records unavailable; grouped by uploaded evidence" "$OUTPUT"
|
|
102
|
+
assert_contains "legacy row rendered" "| Legacy grouped evidence | legacy grouping | 287654321 | legacy | unknown | [Legacy E2E](https://github.com/example/repo/actions/runs/287654321) | e2e-results.json, screenshot.png | grouped from uploaded evidence only | 2026-07-14 18:30:00 |" "$OUTPUT"
|
|
103
|
+
assert_contains "legacy final assessment rendered" "Legacy grouping was used because the portal did not expose first-class cycle records." "$OUTPUT"
|
|
104
|
+
|
|
105
|
+
echo
|
|
106
|
+
echo "=== render-test-cycles.test.sh: $PASS passed, $FAIL failed ==="
|
|
107
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# report-test-cycle.sh — Producer helper for first-class cycle lifecycle events.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/report-test-cycle.sh start|complete [flags...]
|
|
6
|
+
#
|
|
7
|
+
# Required flags:
|
|
8
|
+
# --project-slug <slug>
|
|
9
|
+
# --release <version>
|
|
10
|
+
# --sdlc-stage <1-5>
|
|
11
|
+
# --environment <ci|uat|production>
|
|
12
|
+
# --cycle-kind <kind>
|
|
13
|
+
# --idempotency-key <key>
|
|
14
|
+
#
|
|
15
|
+
# Optional flags:
|
|
16
|
+
# --provider <name> Defaults to github_actions
|
|
17
|
+
# --external-run-id <id>
|
|
18
|
+
# --external-run-attempt <n>
|
|
19
|
+
# --external-job-id <id>
|
|
20
|
+
# --commit-sha <sha>
|
|
21
|
+
# --branch <name>
|
|
22
|
+
# --workflow-name <name>
|
|
23
|
+
# --workflow-url <url>
|
|
24
|
+
# --started-at <iso8601> Defaults to current UTC on `start`
|
|
25
|
+
# --completed-at <iso8601> Defaults to current UTC on `complete`
|
|
26
|
+
# --outcome <value> Terminal outcome for `complete`
|
|
27
|
+
# --outcome-reason <text>
|
|
28
|
+
# --incident-reference <text>
|
|
29
|
+
# --output-file <path> Writes key=value outputs for callers
|
|
30
|
+
#
|
|
31
|
+
# Output keys:
|
|
32
|
+
# cycle_supported=true|false
|
|
33
|
+
# cycle_release_id=<uuid-if-resolved>
|
|
34
|
+
# cycle_release_version=<exact-version-if-resolved>
|
|
35
|
+
# cycle_record_id=<uuid-if-created-or-updated>
|
|
36
|
+
# cycle_idempotency_key=<input-key>
|
|
37
|
+
# cycle_started_at=<iso8601-if-known>
|
|
38
|
+
# cycle_completed_at=<iso8601-if-known>
|
|
39
|
+
# cycle_endpoint=<start|complete|reconcile-if-used>
|
|
40
|
+
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
|
|
43
|
+
usage() {
|
|
44
|
+
echo "Usage: $0 start|complete [flags...]"
|
|
45
|
+
exit 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[ "$#" -ge 1 ] || usage
|
|
49
|
+
MODE="$1"
|
|
50
|
+
shift
|
|
51
|
+
|
|
52
|
+
case "$MODE" in
|
|
53
|
+
start|complete) ;;
|
|
54
|
+
*) usage ;;
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
PROJECT_SLUG=""
|
|
58
|
+
RELEASE_VERSION=""
|
|
59
|
+
SDLC_STAGE=""
|
|
60
|
+
ENVIRONMENT=""
|
|
61
|
+
CYCLE_KIND=""
|
|
62
|
+
PROVIDER="github_actions"
|
|
63
|
+
EXTERNAL_RUN_ID=""
|
|
64
|
+
EXTERNAL_RUN_ATTEMPT=""
|
|
65
|
+
EXTERNAL_JOB_ID=""
|
|
66
|
+
COMMIT_SHA=""
|
|
67
|
+
BRANCH=""
|
|
68
|
+
WORKFLOW_NAME=""
|
|
69
|
+
WORKFLOW_URL=""
|
|
70
|
+
IDEMPOTENCY_KEY=""
|
|
71
|
+
STARTED_AT=""
|
|
72
|
+
COMPLETED_AT=""
|
|
73
|
+
OUTCOME=""
|
|
74
|
+
OUTCOME_REASON=""
|
|
75
|
+
INCIDENT_REFERENCE=""
|
|
76
|
+
OUTPUT_FILE=""
|
|
77
|
+
|
|
78
|
+
while [ "$#" -gt 0 ]; do
|
|
79
|
+
case "$1" in
|
|
80
|
+
--project-slug) PROJECT_SLUG="$2"; shift 2 ;;
|
|
81
|
+
--release) RELEASE_VERSION="$2"; shift 2 ;;
|
|
82
|
+
--sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
|
|
83
|
+
--environment) ENVIRONMENT="$2"; shift 2 ;;
|
|
84
|
+
--cycle-kind) CYCLE_KIND="$2"; shift 2 ;;
|
|
85
|
+
--provider) PROVIDER="$2"; shift 2 ;;
|
|
86
|
+
--external-run-id) EXTERNAL_RUN_ID="$2"; shift 2 ;;
|
|
87
|
+
--external-run-attempt) EXTERNAL_RUN_ATTEMPT="$2"; shift 2 ;;
|
|
88
|
+
--external-job-id) EXTERNAL_JOB_ID="$2"; shift 2 ;;
|
|
89
|
+
--commit-sha) COMMIT_SHA="$2"; shift 2 ;;
|
|
90
|
+
--branch) BRANCH="$2"; shift 2 ;;
|
|
91
|
+
--workflow-name) WORKFLOW_NAME="$2"; shift 2 ;;
|
|
92
|
+
--workflow-url) WORKFLOW_URL="$2"; shift 2 ;;
|
|
93
|
+
--idempotency-key) IDEMPOTENCY_KEY="$2"; shift 2 ;;
|
|
94
|
+
--started-at) STARTED_AT="$2"; shift 2 ;;
|
|
95
|
+
--completed-at) COMPLETED_AT="$2"; shift 2 ;;
|
|
96
|
+
--outcome) OUTCOME="$2"; shift 2 ;;
|
|
97
|
+
--outcome-reason) OUTCOME_REASON="$2"; shift 2 ;;
|
|
98
|
+
--incident-reference) INCIDENT_REFERENCE="$2"; shift 2 ;;
|
|
99
|
+
--output-file) OUTPUT_FILE="$2"; shift 2 ;;
|
|
100
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
require_arg() {
|
|
105
|
+
local value="$1" name="$2"
|
|
106
|
+
if [ -z "$value" ]; then
|
|
107
|
+
echo "Error: ${name} is required" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
require_arg "$PROJECT_SLUG" "--project-slug"
|
|
113
|
+
require_arg "$RELEASE_VERSION" "--release"
|
|
114
|
+
require_arg "$SDLC_STAGE" "--sdlc-stage"
|
|
115
|
+
require_arg "$ENVIRONMENT" "--environment"
|
|
116
|
+
require_arg "$CYCLE_KIND" "--cycle-kind"
|
|
117
|
+
require_arg "$IDEMPOTENCY_KEY" "--idempotency-key"
|
|
118
|
+
|
|
119
|
+
if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
|
|
120
|
+
echo "Error: DEVAUDIT_BASE_URL environment variable is required" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
if [ -z "${DEVAUDIT_API_KEY:-}" ]; then
|
|
124
|
+
echo "Error: DEVAUDIT_API_KEY environment variable is required" >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
|
|
129
|
+
echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)" >&2
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
case "$ENVIRONMENT" in
|
|
134
|
+
ci|uat|production) ;;
|
|
135
|
+
*)
|
|
136
|
+
echo "Error: --environment must be one of ci, uat, production" >&2
|
|
137
|
+
exit 1
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
|
|
141
|
+
if [ "$MODE" = "complete" ] && [ -z "$OUTCOME" ]; then
|
|
142
|
+
echo "Error: --outcome is required for complete" >&2
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
case "$OUTCOME" in
|
|
147
|
+
""|passed|failed|cancelled|skipped|timed_out|action_required|unknown) ;;
|
|
148
|
+
*)
|
|
149
|
+
echo "Error: --outcome must be a terminal cycle outcome" >&2
|
|
150
|
+
exit 1
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
|
|
154
|
+
iso_now() {
|
|
155
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
[ -n "$STARTED_AT" ] || STARTED_AT="$(iso_now)"
|
|
159
|
+
if [ "$MODE" = "complete" ] && [ -z "$COMPLETED_AT" ]; then
|
|
160
|
+
COMPLETED_AT="$(iso_now)"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
DEVAUDIT_BASE_URL="${DEVAUDIT_BASE_URL%/}"
|
|
164
|
+
MAX_TIME_SECONDS=${UPLOAD_MAX_TIME_SECONDS:-120}
|
|
165
|
+
CONNECT_TIMEOUT_SECONDS=${UPLOAD_CONNECT_TIMEOUT_SECONDS:-10}
|
|
166
|
+
|
|
167
|
+
write_output() {
|
|
168
|
+
local key="$1" value="$2"
|
|
169
|
+
if [ -n "$OUTPUT_FILE" ]; then
|
|
170
|
+
printf '%s=%s\n' "$key" "$value" >> "$OUTPUT_FILE"
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
emit_fallback() {
|
|
175
|
+
local release_id="${1:-}" resolved_version="${2:-}"
|
|
176
|
+
write_output cycle_supported false
|
|
177
|
+
write_output cycle_release_id "$release_id"
|
|
178
|
+
write_output cycle_release_version "$resolved_version"
|
|
179
|
+
write_output cycle_record_id ""
|
|
180
|
+
write_output cycle_idempotency_key "$IDEMPOTENCY_KEY"
|
|
181
|
+
write_output cycle_started_at "$STARTED_AT"
|
|
182
|
+
write_output cycle_completed_at "$COMPLETED_AT"
|
|
183
|
+
write_output cycle_endpoint "$MODE"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
uri_escape() {
|
|
187
|
+
jq -rn --arg v "$1" '$v|@uri'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resolve_release_exact() {
|
|
191
|
+
local encoded_release encoded_slug url body code latest_id latest_version
|
|
192
|
+
encoded_release=$(uri_escape "$RELEASE_VERSION")
|
|
193
|
+
encoded_slug=$(uri_escape "$PROJECT_SLUG")
|
|
194
|
+
url="${DEVAUDIT_BASE_URL}/api/ci/releases/resolve?projectSlug=${encoded_slug}&versionPrefix=${encoded_release}"
|
|
195
|
+
body=$(mktemp)
|
|
196
|
+
code=$(curl -sS -o "$body" -w "%{http_code}" \
|
|
197
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
198
|
+
--connect-timeout "$CONNECT_TIMEOUT_SECONDS" \
|
|
199
|
+
--max-time "$MAX_TIME_SECONDS" \
|
|
200
|
+
"$url")
|
|
201
|
+
if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then
|
|
202
|
+
echo "::warning::Cycle lifecycle skipped: could not resolve release ${RELEASE_VERSION} (HTTP ${code})." >&2
|
|
203
|
+
rm -f "$body"
|
|
204
|
+
emit_fallback "" ""
|
|
205
|
+
return 1
|
|
206
|
+
fi
|
|
207
|
+
latest_id=$(jq -r '.latest.id // empty' "$body" 2>/dev/null || true)
|
|
208
|
+
latest_version=$(jq -r '.latest.version // empty' "$body" 2>/dev/null || true)
|
|
209
|
+
rm -f "$body"
|
|
210
|
+
if [ -z "$latest_id" ] || [ "$latest_version" != "$RELEASE_VERSION" ]; then
|
|
211
|
+
echo "::warning::Cycle lifecycle skipped: release resolve for ${RELEASE_VERSION} was empty or ambiguous (latest=${latest_version:-none})." >&2
|
|
212
|
+
emit_fallback "$latest_id" "$latest_version"
|
|
213
|
+
return 1
|
|
214
|
+
fi
|
|
215
|
+
RELEASE_ID="$latest_id"
|
|
216
|
+
RESOLVED_RELEASE_VERSION="$latest_version"
|
|
217
|
+
write_output cycle_release_id "$RELEASE_ID"
|
|
218
|
+
write_output cycle_release_version "$RESOLVED_RELEASE_VERSION"
|
|
219
|
+
return 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
build_payload() {
|
|
223
|
+
local mode="$1"
|
|
224
|
+
if [ -n "$EXTERNAL_RUN_ATTEMPT" ]; then
|
|
225
|
+
EXTERNAL_RUN_ATTEMPT_JSON="$EXTERNAL_RUN_ATTEMPT"
|
|
226
|
+
else
|
|
227
|
+
EXTERNAL_RUN_ATTEMPT_JSON="null"
|
|
228
|
+
fi
|
|
229
|
+
jq -n \
|
|
230
|
+
--argjson schemaVersion 1 \
|
|
231
|
+
--arg mode "$mode" \
|
|
232
|
+
--arg sdlcStage "$SDLC_STAGE" \
|
|
233
|
+
--arg environment "$ENVIRONMENT" \
|
|
234
|
+
--arg cycleKind "$CYCLE_KIND" \
|
|
235
|
+
--arg provider "$PROVIDER" \
|
|
236
|
+
--arg externalRunId "$EXTERNAL_RUN_ID" \
|
|
237
|
+
--argjson externalRunAttempt "$EXTERNAL_RUN_ATTEMPT_JSON" \
|
|
238
|
+
--arg externalJobId "$EXTERNAL_JOB_ID" \
|
|
239
|
+
--arg idempotencyKey "$IDEMPOTENCY_KEY" \
|
|
240
|
+
--arg startedAt "$STARTED_AT" \
|
|
241
|
+
--arg completedAt "$COMPLETED_AT" \
|
|
242
|
+
--arg outcome "$OUTCOME" \
|
|
243
|
+
--arg outcomeReason "$OUTCOME_REASON" \
|
|
244
|
+
--arg commitSha "$COMMIT_SHA" \
|
|
245
|
+
--arg branch "$BRANCH" \
|
|
246
|
+
--arg workflowName "$WORKFLOW_NAME" \
|
|
247
|
+
--arg workflowUrl "$WORKFLOW_URL" \
|
|
248
|
+
--arg incidentReference "$INCIDENT_REFERENCE" \
|
|
249
|
+
'
|
|
250
|
+
{
|
|
251
|
+
schemaVersion: $schemaVersion,
|
|
252
|
+
sdlcStage: ($sdlcStage | tonumber),
|
|
253
|
+
environment: $environment,
|
|
254
|
+
cycleKind: $cycleKind,
|
|
255
|
+
provider: $provider,
|
|
256
|
+
externalRunId: ($externalRunId | if . == "" then null else . end),
|
|
257
|
+
externalRunAttempt: $externalRunAttempt,
|
|
258
|
+
externalJobId: ($externalJobId | if . == "" then null else . end),
|
|
259
|
+
idempotencyKey: $idempotencyKey,
|
|
260
|
+
startedAt: $startedAt,
|
|
261
|
+
commitSha: ($commitSha | if . == "" then null else . end),
|
|
262
|
+
branch: ($branch | if . == "" then null else . end),
|
|
263
|
+
workflowName: ($workflowName | if . == "" then null else . end),
|
|
264
|
+
workflowUrl: ($workflowUrl | if . == "" then null else . end),
|
|
265
|
+
incidentReference: ($incidentReference | if . == "" then null else . end)
|
|
266
|
+
}
|
|
267
|
+
+ (if $mode == "start"
|
|
268
|
+
then { outcome: "running" }
|
|
269
|
+
else {
|
|
270
|
+
completedAt: $completedAt,
|
|
271
|
+
outcome: $outcome,
|
|
272
|
+
outcomeReason: ($outcomeReason | if . == "" then null else . end)
|
|
273
|
+
}
|
|
274
|
+
end)
|
|
275
|
+
'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
post_cycle_event() {
|
|
279
|
+
local endpoint="$1" payload="$2" body code
|
|
280
|
+
body=$(mktemp)
|
|
281
|
+
code=$(curl -sS -o "$body" -w "%{http_code}" \
|
|
282
|
+
-X POST \
|
|
283
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
284
|
+
-H "Content-Type: application/json" \
|
|
285
|
+
--connect-timeout "$CONNECT_TIMEOUT_SECONDS" \
|
|
286
|
+
--max-time "$MAX_TIME_SECONDS" \
|
|
287
|
+
"${DEVAUDIT_BASE_URL}/api/ci/releases/${RELEASE_ID}/cycles/${endpoint}" \
|
|
288
|
+
-d "$payload")
|
|
289
|
+
CYCLE_HTTP_CODE="$code"
|
|
290
|
+
CYCLE_RESPONSE_BODY_FILE="$body"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
handle_unavailable_endpoint() {
|
|
294
|
+
case "$CYCLE_HTTP_CODE" in
|
|
295
|
+
404|405|501)
|
|
296
|
+
echo "::warning::Cycle lifecycle endpoint ${MODE} unavailable on this portal (HTTP ${CYCLE_HTTP_CODE}); continuing in legacy testCycleId mode." >&2
|
|
297
|
+
emit_fallback "$RELEASE_ID" "$RESOLVED_RELEASE_VERSION"
|
|
298
|
+
rm -f "$CYCLE_RESPONSE_BODY_FILE"
|
|
299
|
+
return 0
|
|
300
|
+
;;
|
|
301
|
+
esac
|
|
302
|
+
return 1
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if ! resolve_release_exact; then
|
|
306
|
+
exit 0
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
PAYLOAD="$(build_payload "$MODE")"
|
|
310
|
+
ENDPOINT="$MODE"
|
|
311
|
+
post_cycle_event "$ENDPOINT" "$PAYLOAD"
|
|
312
|
+
|
|
313
|
+
if [ "$MODE" = "complete" ] && [ "$CYCLE_HTTP_CODE" -eq 400 ]; then
|
|
314
|
+
if grep -q 'different terminal outcome' "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null; then
|
|
315
|
+
ENDPOINT="reconcile"
|
|
316
|
+
post_cycle_event "$ENDPOINT" "$PAYLOAD"
|
|
317
|
+
fi
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
if [ "$CYCLE_HTTP_CODE" -lt 200 ] || [ "$CYCLE_HTTP_CODE" -ge 300 ]; then
|
|
321
|
+
if handle_unavailable_endpoint; then
|
|
322
|
+
exit 0
|
|
323
|
+
fi
|
|
324
|
+
BODY_EXCERPT="$(head -c 500 "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null || true)"
|
|
325
|
+
rm -f "$CYCLE_RESPONSE_BODY_FILE"
|
|
326
|
+
echo "Error: cycle ${ENDPOINT} failed for ${RELEASE_VERSION} (HTTP ${CYCLE_HTTP_CODE})${BODY_EXCERPT:+ — ${BODY_EXCERPT}}" >&2
|
|
327
|
+
exit 1
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
CYCLE_RECORD_ID=$(jq -r '.id // empty' "$CYCLE_RESPONSE_BODY_FILE" 2>/dev/null || true)
|
|
331
|
+
rm -f "$CYCLE_RESPONSE_BODY_FILE"
|
|
332
|
+
if [ -z "$CYCLE_RECORD_ID" ]; then
|
|
333
|
+
echo "Error: cycle ${ENDPOINT} succeeded but response did not include an id" >&2
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
write_output cycle_supported true
|
|
338
|
+
write_output cycle_record_id "$CYCLE_RECORD_ID"
|
|
339
|
+
write_output cycle_idempotency_key "$IDEMPOTENCY_KEY"
|
|
340
|
+
write_output cycle_started_at "$STARTED_AT"
|
|
341
|
+
write_output cycle_completed_at "$COMPLETED_AT"
|
|
342
|
+
write_output cycle_endpoint "$ENDPOINT"
|
|
343
|
+
|
|
344
|
+
echo "Cycle ${ENDPOINT} recorded for ${RELEASE_VERSION}: ${CYCLE_RECORD_ID}"
|