@metasession.co/devaudit-cli 0.3.12 → 0.3.14

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.
Files changed (67) hide show
  1. package/dist/index.js +53 -4
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/upload-evidence.sh +55 -1
  5. package/sdlc/files/_common/0-project-setup.md +10 -4
  6. package/sdlc/files/_common/Periodic_Security_Review_Schedule.md +5 -5
  7. package/sdlc/files/_common/README_TEMPLATE.md +8 -8
  8. package/sdlc/files/_common/Test_Architecture.md +15 -15
  9. package/sdlc/files/_common/Test_Plan_TEMPLATE.md +13 -13
  10. package/sdlc/files/_common/Test_Policy.md +2 -2
  11. package/sdlc/files/_common/Test_Strategy.md +10 -10
  12. package/sdlc/files/_common/governance/ai-disclosure.md.template +3 -3
  13. package/sdlc/files/_common/governance/dpia.md.template +3 -3
  14. package/sdlc/files/_common/governance/incident-report.md.template +3 -3
  15. package/sdlc/files/_common/governance/nil-incident-report.md.template +1 -1
  16. package/sdlc/files/_common/governance/periodic-review.md.template +3 -3
  17. package/sdlc/files/_common/governance/risk-register.md.template +1 -1
  18. package/sdlc/files/_common/governance/ropa.md.template +2 -2
  19. package/sdlc/files/_common/joining-an-existing-project.md +4 -4
  20. package/sdlc/files/_common/scripts/check-host-deployment.sh +107 -0
  21. package/sdlc/files/_common/scripts/check-host-deployment.test.sh +119 -0
  22. package/sdlc/files/_common/scripts/check-release-pr-scope.sh +95 -0
  23. package/sdlc/files/_common/scripts/check-release-pr-scope.test.sh +137 -0
  24. package/sdlc/files/_common/scripts/close-out-release.sh +175 -54
  25. package/sdlc/files/_common/scripts/close-out-release.test.sh +90 -0
  26. package/sdlc/files/_common/scripts/extract-release-metadata.sh +27 -0
  27. package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +13 -1
  28. package/sdlc/files/_common/scripts/generate-bundled-changes.sh +311 -64
  29. package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +133 -82
  30. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.sh +18 -9
  31. package/sdlc/files/_common/scripts/generate-housekeeping-release-ticket.test.sh +66 -0
  32. package/sdlc/files/_common/scripts/generate-security-summary.sh +14 -5
  33. package/sdlc/files/_common/scripts/generate-security-summary.test.sh +57 -0
  34. package/sdlc/files/_common/scripts/prepare-release-pr.sh +44 -4
  35. package/sdlc/files/_common/scripts/render-test-cycles.sh +227 -0
  36. package/sdlc/files/_common/scripts/render-test-cycles.test.sh +107 -0
  37. package/sdlc/files/_common/scripts/report-release-check.sh +86 -0
  38. package/sdlc/files/_common/scripts/report-release-check.test.sh +97 -0
  39. package/sdlc/files/_common/scripts/report-test-cycle.sh +344 -0
  40. package/sdlc/files/_common/scripts/submit-bundle-manifest.sh +100 -0
  41. package/sdlc/files/_common/scripts/submit-bundle-manifest.test.sh +138 -0
  42. package/sdlc/files/_common/scripts/upload-evidence.sh +24 -0
  43. package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +16 -0
  44. package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +14 -0
  45. package/sdlc/files/_common/scripts/validate-test-summary.test.sh +4 -4
  46. package/sdlc/files/_common/skills/adr-author/SKILL.md +2 -2
  47. package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +2 -2
  48. package/sdlc/files/_common/skills/e2e-test-engineer/references/bootstrap.md +5 -5
  49. package/sdlc/files/_common/skills/governance-doc-author/references/incident-classification.md +1 -1
  50. package/sdlc/files/_common/skills/requirements-aligner/SKILL.md +4 -4
  51. package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +21 -2
  52. package/sdlc/files/_common/skills/sdlc-implementer/references/change-request-loop.md +1 -1
  53. package/sdlc/files/_common/skills/sdlc-implementer/references/compliance-constraints.md +7 -14
  54. package/sdlc/files/ci/ci.yml.template +119 -28
  55. package/sdlc/files/ci/close-out-release.yml.template +7 -4
  56. package/sdlc/files/ci/compliance-evidence.yml.template +230 -20
  57. package/sdlc/files/ci/feature-e2e.yml.template +84 -1
  58. package/sdlc/files/ci/post-deploy-prod.yml.template +189 -6
  59. package/sdlc/files/ci/python/ci.yml.template +60 -12
  60. package/sdlc/files/ci/quality-gates-provenance.yml.template +123 -28
  61. package/sdlc/package.json +1 -1
  62. package/sdlc/src/bin/devaudit-sdlc.js +44 -2
  63. package/sdlc/src/blueprints/1-plan-requirement.raw.md +6 -1
  64. package/sdlc/src/blueprints/2-implement-and-test.raw.md +1 -1
  65. package/sdlc/src/blueprints/3-compile-evidence.raw.md +34 -20
  66. package/sdlc/src/blueprints/4-submit-for-review.raw.md +2 -2
  67. 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,86 @@
1
+ #!/usr/bin/env bash
2
+ # Records authoritative required-check state independently of evidence upload.
3
+ set -euo pipefail
4
+
5
+ PROJECT_SLUG="" RELEASE_VERSION="" CHECK_KEY="" LABEL="" PROVIDER="github_actions"
6
+ STATUS="" REQUIRED="true" EXTERNAL_RUN_ID="" EXTERNAL_URL="" COMMIT_SHA="" BRANCH=""
7
+ STARTED_AT="" COMPLETED_AT="" DETAILS_JSON='{}'
8
+
9
+ while [ "$#" -gt 0 ]; do
10
+ case "$1" in
11
+ --project-slug) PROJECT_SLUG="$2"; shift 2 ;;
12
+ --release) RELEASE_VERSION="$2"; shift 2 ;;
13
+ --check-key) CHECK_KEY="$2"; shift 2 ;;
14
+ --label) LABEL="$2"; shift 2 ;;
15
+ --provider) PROVIDER="$2"; shift 2 ;;
16
+ --status) STATUS="$2"; shift 2 ;;
17
+ --required) REQUIRED="$2"; shift 2 ;;
18
+ --external-run-id) EXTERNAL_RUN_ID="$2"; shift 2 ;;
19
+ --external-url) EXTERNAL_URL="$2"; shift 2 ;;
20
+ --commit-sha) COMMIT_SHA="$2"; shift 2 ;;
21
+ --branch) BRANCH="$2"; shift 2 ;;
22
+ --started-at) STARTED_AT="$2"; shift 2 ;;
23
+ --completed-at) COMPLETED_AT="$2"; shift 2 ;;
24
+ --details-json) DETAILS_JSON="$2"; shift 2 ;;
25
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
26
+ esac
27
+ done
28
+
29
+ for value in PROJECT_SLUG RELEASE_VERSION CHECK_KEY LABEL STATUS; do
30
+ if [ -z "${!value}" ]; then echo "Error: ${value} is required" >&2; exit 1; fi
31
+ done
32
+ case "$STATUS" in
33
+ queued|running|successful|failed|cancelled|timed_out|skipped|action_required|unknown) ;;
34
+ *) echo "Error: invalid check status '$STATUS'" >&2; exit 1 ;;
35
+ esac
36
+ jq -e 'type == "object"' >/dev/null <<<"$DETAILS_JSON" || {
37
+ echo "Error: --details-json must be a JSON object" >&2; exit 1;
38
+ }
39
+
40
+ BASE="${DEVAUDIT_BASE_URL%/}"
41
+ if [ -z "$BASE" ] || [ -z "${DEVAUDIT_API_KEY:-}" ]; then
42
+ echo "::warning::Release check reporting skipped: DevAudit URL/API key missing" >&2
43
+ exit 0
44
+ fi
45
+ ENCODED_SLUG=$(jq -rn --arg v "$PROJECT_SLUG" '$v|@uri')
46
+ ENCODED_VERSION=$(jq -rn --arg v "$RELEASE_VERSION" '$v|@uri')
47
+ RESOLVE=$(curl -fsS \
48
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
49
+ "${BASE}/api/ci/releases/resolve?projectSlug=${ENCODED_SLUG}&versionPrefix=${ENCODED_VERSION}" || true)
50
+ RELEASE_ID=$(jq -r --arg version "$RELEASE_VERSION" \
51
+ 'if .latest.version == $version then .latest.id // empty else empty end' <<<"$RESOLVE" 2>/dev/null || true)
52
+ if [ -z "$RELEASE_ID" ]; then
53
+ echo "::warning::Release check reporting skipped: exact release ${RELEASE_VERSION} not found" >&2
54
+ exit 0
55
+ fi
56
+
57
+ PAYLOAD=$(jq -n \
58
+ --arg checkKey "$CHECK_KEY" --arg displayLabel "$LABEL" --arg provider "$PROVIDER" \
59
+ --arg status "$STATUS" --argjson required "$REQUIRED" \
60
+ --arg externalRunId "$EXTERNAL_RUN_ID" --arg externalUrl "$EXTERNAL_URL" \
61
+ --arg commitSha "$COMMIT_SHA" --arg branch "$BRANCH" \
62
+ --arg startedAt "$STARTED_AT" --arg completedAt "$COMPLETED_AT" \
63
+ --argjson details "$DETAILS_JSON" \
64
+ '{checkKey:$checkKey,label:$displayLabel,provider:$provider,status:$status,required:$required,
65
+ externalRunId:(if $externalRunId=="" then null else $externalRunId end),
66
+ externalUrl:(if $externalUrl=="" then null else $externalUrl end),
67
+ commitSha:(if $commitSha=="" then null else $commitSha end),
68
+ branch:(if $branch=="" then null else $branch end),
69
+ startedAt:(if $startedAt=="" then null else $startedAt end),
70
+ completedAt:(if $completedAt=="" then null else $completedAt end),details:$details}')
71
+
72
+ RESPONSE_FILE=$(mktemp)
73
+ trap 'rm -f "$RESPONSE_FILE"' EXIT
74
+ HTTP_CODE=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' \
75
+ -X POST "${BASE}/api/ci/releases/${RELEASE_ID}/checks" \
76
+ -H "Authorization: Bearer ${DEVAUDIT_API_KEY}" -H 'Content-Type: application/json' \
77
+ -d "$PAYLOAD")
78
+ if [ "$HTTP_CODE" = "404" ]; then
79
+ echo "::warning::Portal does not support first-class release checks yet" >&2
80
+ exit 0
81
+ fi
82
+ if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
83
+ cat "$RESPONSE_FILE" >&2 || true
84
+ echo "Error: release check API returned HTTP ${HTTP_CODE}" >&2
85
+ exit 1
86
+ fi
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Focused regression tests for authoritative release-check reporting.
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ HELPER="$SCRIPT_DIR/report-release-check.sh"
7
+ PASS=0
8
+ FAIL=0
9
+
10
+ ok() { echo " PASS: $1"; PASS=$((PASS + 1)); }
11
+ no() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
12
+
13
+ make_fixture() {
14
+ local dir="$1"
15
+ mkdir -p "$dir/bin"
16
+ cat > "$dir/bin/curl" <<'EOF'
17
+ #!/usr/bin/env bash
18
+ set -euo pipefail
19
+ URL=""
20
+ OUT=""
21
+ BODY=""
22
+ ARGS=("$@")
23
+ for ((i=0; i<${#ARGS[@]}; i++)); do
24
+ case "${ARGS[$i]}" in
25
+ -o) OUT="${ARGS[$((i+1))]}" ;;
26
+ -d) BODY="${ARGS[$((i+1))]}" ;;
27
+ http://*|https://*) URL="${ARGS[$i]}" ;;
28
+ esac
29
+ done
30
+ printf 'URL:%s\nBODY:%s\n' "$URL" "$BODY" >> "${CHECK_TEST_LOG:?}"
31
+ if [[ "$URL" == *'/api/ci/releases/resolve'* ]]; then
32
+ printf '%s' "${RESOLVE_BODY:-{}}"
33
+ exit 0
34
+ fi
35
+ if [ -n "$OUT" ]; then printf '%s' "${CHECK_BODY:-{}}" > "$OUT"; fi
36
+ printf '%s' "${CHECK_STATUS:-200}"
37
+ EOF
38
+ chmod +x "$dir/bin/curl"
39
+ }
40
+
41
+ run_helper() {
42
+ PATH="$1/bin:$PATH" \
43
+ CHECK_TEST_LOG="$1/curl.log" \
44
+ DEVAUDIT_BASE_URL="https://devaudit.example.test" \
45
+ DEVAUDIT_API_KEY="mc_test_dummy" \
46
+ "$HELPER" "${@:2}"
47
+ }
48
+
49
+ base_args=(
50
+ --project-slug fixture-app
51
+ --release REQ-100
52
+ --check-key quality-gates
53
+ --label "Quality Gates"
54
+ --status failed
55
+ --external-run-id 123
56
+ --commit-sha abc123
57
+ --details-json '{"artifactUpload":"successful"}'
58
+ )
59
+
60
+ echo "=== report-release-check.sh tests ==="
61
+
62
+ dir=$(mktemp -d)
63
+ make_fixture "$dir"
64
+ RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
65
+ CHECK_STATUS=201 run_helper "$dir" "${base_args[@]}"
66
+ if grep -q '/api/ci/releases/release-1/checks' "$dir/curl.log"; then ok "posts to exact release"; else no "missing check endpoint"; fi
67
+ if grep -q 'status.*failed' "$dir/curl.log" && grep -q 'artifactUpload.*successful' "$dir/curl.log"; then
68
+ ok "keeps execution outcome separate from upload detail"
69
+ else
70
+ no "incorrect check payload"
71
+ fi
72
+ rm -rf "$dir"
73
+
74
+ dir=$(mktemp -d)
75
+ make_fixture "$dir"
76
+ RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
77
+ CHECK_STATUS=404 run_helper "$dir" "${base_args[@]}" >/dev/null 2>&1 && ok "tolerates older portal" || no "404 should be tolerant"
78
+ rm -rf "$dir"
79
+
80
+ dir=$(mktemp -d)
81
+ make_fixture "$dir"
82
+ if RESOLVE_BODY='{"latest":{"id":"release-1","version":"REQ-100"}}' \
83
+ CHECK_STATUS=500 CHECK_BODY='{"error":"failed"}' run_helper "$dir" "${base_args[@]}" >/dev/null 2>&1; then
84
+ no "500 should fail"
85
+ else
86
+ ok "fails on portal error"
87
+ fi
88
+ rm -rf "$dir"
89
+
90
+ if "$HELPER" "${base_args[@]/failed/not-a-status}" >/dev/null 2>&1; then
91
+ no "invalid status should fail"
92
+ else
93
+ ok "rejects invalid status"
94
+ fi
95
+
96
+ echo "=== report-release-check.sh: $PASS passed, $FAIL failed ==="
97
+ [ "$FAIL" -eq 0 ]