@metasession.co/devaudit-cli 0.3.8 → 0.3.10
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/README.md +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/upload-evidence.sh +23 -3
- package/sdlc/files/_common/joining-an-existing-project.md +5 -4
- package/sdlc/files/_common/scripts/close-out-contract.test.sh +71 -0
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +48 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +85 -0
- package/sdlc/files/_common/scripts/prepare-release-pr.sh +99 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +593 -0
- package/sdlc/files/_common/scripts/validate-commits.sh +32 -7
- package/sdlc/files/_common/scripts/validate-commits.test.sh +143 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +39 -3
- package/sdlc/files/ci/ci.yml.template +2 -2
- package/sdlc/files/ci/compliance-evidence.yml.template +49 -4
- package/sdlc/files/ci/incident-export.yml.template +7 -3
- package/sdlc/files/ci/label-retention.yml.template +3 -1
- package/sdlc/files/ci/periodic-review.yml.template +5 -3
- package/sdlc/files/ci/post-deploy-prod.yml.template +6 -1
- package/sdlc/files/ci/quality-gates-provenance.yml.template +1 -1
- package/sdlc/files/stacks/node/hooks/pre-push +13 -13
- package/sdlc/package.json +1 -1
- package/sdlc/src/bin/devaudit-sdlc.js +490 -54
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @requirement REQ-007 - CLI upload script for CI pipelines
|
|
3
|
+
# @requirement REQ-020 - Extended with release/environment/category flags
|
|
4
|
+
# @requirement REQ-XXX - #224: posts to META-COMPLY's API instead of Supabase
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# ./scripts/upload-evidence.sh <project-slug> <requirement-id> <evidence-type> <file-or-directory>
|
|
8
|
+
#
|
|
9
|
+
# Optional flags:
|
|
10
|
+
# --git-sha <sha> Git commit SHA
|
|
11
|
+
# --ci-run-id <id> CI run identifier
|
|
12
|
+
# --branch <branch> Git branch name
|
|
13
|
+
# --release <version> Release version (e.g. v1.0.0). The route
|
|
14
|
+
# resolves it to a release UUID server-side.
|
|
15
|
+
# --create-release-if-missing Auto-create the release as 'draft' if absent
|
|
16
|
+
# --environment <env> Environment: uat or production
|
|
17
|
+
# --category <cat> Evidence category: ci_pipeline, local_dev,
|
|
18
|
+
# planning, test_report, security_scan,
|
|
19
|
+
# release_artifact
|
|
20
|
+
# --release-title <text> Human title for the release row (e.g. the
|
|
21
|
+
# release-ticket H1). Forwarded as
|
|
22
|
+
# `releaseTitle`; the portal no-clobbers
|
|
23
|
+
# existing non-null values.
|
|
24
|
+
# --release-summary <text> Reviewer-facing short description of what
|
|
25
|
+
# the release covers. Forwarded as
|
|
26
|
+
# `releaseSummary`; the portal no-clobbers
|
|
27
|
+
# existing non-null values.
|
|
28
|
+
# DevAudit-Installer#285.
|
|
29
|
+
# --change-type <type> Conventional-commit prefix (feat / fix /
|
|
30
|
+
# refactor / perf / chore / docs / ci /
|
|
31
|
+
# build / test / compliance / revert) for
|
|
32
|
+
# the release row. Unknown values are
|
|
33
|
+
# silently dropped server-side.
|
|
34
|
+
# --gate-status <status> `passed` / `failed` / `skipped`. Lets the
|
|
35
|
+
# portal distinguish a gate that ran-and-
|
|
36
|
+
# failed from one that never ran. Forwarded
|
|
37
|
+
# as `gateStatus`; unknown values are
|
|
38
|
+
# silently dropped server-side.
|
|
39
|
+
# DevAudit-Installer#96.
|
|
40
|
+
# --sdlc-stage <1-5> SDLC stage that produced this artefact:
|
|
41
|
+
# 1 plan, 2 implement/test, 3 compile-evidence,
|
|
42
|
+
# 4 submit-for-review, 5 deploy. Forwarded as
|
|
43
|
+
# `sdlcStage`; unknown to older portals (ignored
|
|
44
|
+
# server-side, no error).
|
|
45
|
+
# --test-cycle <id> Test cycle identifier (typically the CI run
|
|
46
|
+
# ID). Forwarded as `testCycleId`; lets the
|
|
47
|
+
# portal group evidence by test cycle per
|
|
48
|
+
# ISO/IEC/IEEE 29119-3. Optional — older
|
|
49
|
+
# portals ignore the field (no error).
|
|
50
|
+
#
|
|
51
|
+
# Required environment variables:
|
|
52
|
+
# DEVAUDIT_BASE_URL e.g. https://meta-comply-production.up.railway.app
|
|
53
|
+
# DEVAUDIT_API_KEY project-scoped API key (uploader role); format `mc_…`.
|
|
54
|
+
# Issue from: Project Settings → API Keys in
|
|
55
|
+
# META-COMPLY's web UI.
|
|
56
|
+
#
|
|
57
|
+
# Large-file uploads (>=25MB):
|
|
58
|
+
# Files above 25MB use a presigned R2 upload flow (3-step: request URL →
|
|
59
|
+
# PUT to R2 → notify portal) instead of multipart POST. This requires
|
|
60
|
+
# the Portal to be configured with R2 evidence storage
|
|
61
|
+
# (R2_EVIDENCE_BUCKET and related env vars). If R2 is not configured,
|
|
62
|
+
# the Portal returns a clear error which is surfaced to the operator.
|
|
63
|
+
# #298 — the Portal response body is included in the error output.
|
|
64
|
+
#
|
|
65
|
+
# Examples:
|
|
66
|
+
# ./scripts/upload-evidence.sh meta-ats REQ-001 screenshot \
|
|
67
|
+
# compliance/evidence/REQ-001/screenshots/
|
|
68
|
+
# ./scripts/upload-evidence.sh meta-ats _compliance-docs compliance_document \
|
|
69
|
+
# compliance/RTM.md --git-sha abc123
|
|
70
|
+
# ./scripts/upload-evidence.sh meta-ats REQ-001 e2e_result \
|
|
71
|
+
# playwright-report/ --release v1.0.0 --environment uat \
|
|
72
|
+
# --category ci_pipeline --create-release-if-missing
|
|
73
|
+
|
|
74
|
+
set -euo pipefail
|
|
75
|
+
|
|
76
|
+
# --- Parse arguments ---
|
|
77
|
+
if [ "$#" -lt 4 ]; then
|
|
78
|
+
echo "Usage: $0 <project-slug> <requirement-id> <evidence-type> <file-or-directory> [flags…]"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
PROJECT_SLUG="$1"
|
|
83
|
+
REQUIREMENT_ID="$2"
|
|
84
|
+
EVIDENCE_TYPE="$3"
|
|
85
|
+
FILE_PATH="$4"
|
|
86
|
+
shift 4
|
|
87
|
+
|
|
88
|
+
GIT_SHA=""
|
|
89
|
+
CI_RUN_ID=""
|
|
90
|
+
BRANCH=""
|
|
91
|
+
RELEASE_VERSION=""
|
|
92
|
+
CREATE_RELEASE_IF_MISSING=false
|
|
93
|
+
ENVIRONMENT=""
|
|
94
|
+
EVIDENCE_CATEGORY=""
|
|
95
|
+
RELEASE_TITLE=""
|
|
96
|
+
RELEASE_SUMMARY=""
|
|
97
|
+
CHANGE_TYPE=""
|
|
98
|
+
GATE_STATUS=""
|
|
99
|
+
SDLC_STAGE=""
|
|
100
|
+
TEST_CYCLE=""
|
|
101
|
+
# Repeatable `--meta-key key=value` accumulator. Each pair gets merged
|
|
102
|
+
# into the metadata JSON sent to the portal. Used by the screenshot
|
|
103
|
+
# upload loop to pass `origin=feature|regression` from the per-PNG
|
|
104
|
+
# sidecar JSON written by the evidenceShot helper.
|
|
105
|
+
META_KEYS=()
|
|
106
|
+
|
|
107
|
+
while [ "$#" -gt 0 ]; do
|
|
108
|
+
case "$1" in
|
|
109
|
+
--git-sha) GIT_SHA="$2"; shift 2 ;;
|
|
110
|
+
--ci-run-id) CI_RUN_ID="$2"; shift 2 ;;
|
|
111
|
+
--branch) BRANCH="$2"; shift 2 ;;
|
|
112
|
+
--release) RELEASE_VERSION="$2"; shift 2 ;;
|
|
113
|
+
--create-release-if-missing) CREATE_RELEASE_IF_MISSING=true; shift ;;
|
|
114
|
+
--environment) ENVIRONMENT="$2"; shift 2 ;;
|
|
115
|
+
--category) EVIDENCE_CATEGORY="$2"; shift 2 ;;
|
|
116
|
+
# Descriptive title + conventional-commit change type passed through to
|
|
117
|
+
# the portal's findOrCreateRelease no-clobber backfill. Both optional;
|
|
118
|
+
# unknown change-type values are dropped server-side, not 400'd.
|
|
119
|
+
--release-title) RELEASE_TITLE="$2"; shift 2 ;;
|
|
120
|
+
--release-summary) RELEASE_SUMMARY="$2"; shift 2 ;;
|
|
121
|
+
--change-type) CHANGE_TYPE="$2"; shift 2 ;;
|
|
122
|
+
# passed/failed/skipped — surfaces failed gates on the portal so
|
|
123
|
+
# ran-and-failed != never-ran. Unknown values dropped server-side.
|
|
124
|
+
# DevAudit-Installer#96.
|
|
125
|
+
--gate-status) GATE_STATUS="$2"; shift 2 ;;
|
|
126
|
+
--sdlc-stage) SDLC_STAGE="$2"; shift 2 ;;
|
|
127
|
+
--test-cycle) TEST_CYCLE="$2"; shift 2 ;;
|
|
128
|
+
# --meta-key key=value (repeatable). Merged into the metadata JSON
|
|
129
|
+
# before posting. Validates the `key=value` shape; rejects bare
|
|
130
|
+
# keys without `=`.
|
|
131
|
+
--meta-key)
|
|
132
|
+
if [[ "$2" != *=* ]]; then
|
|
133
|
+
echo "Error: --meta-key requires key=value (got: $2)"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
META_KEYS+=("$2")
|
|
137
|
+
shift 2
|
|
138
|
+
;;
|
|
139
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
140
|
+
esac
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
if [ -n "$ENVIRONMENT" ] && [ -z "$RELEASE_VERSION" ]; then
|
|
144
|
+
echo "Error: --environment requires --release (evidence without a release is orphaned)"
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
if [ -n "$RELEASE_VERSION" ] && [ -z "$EVIDENCE_CATEGORY" ]; then
|
|
148
|
+
echo "Error: --category is required when --release is specified (gate validation)"
|
|
149
|
+
exit 1
|
|
150
|
+
fi
|
|
151
|
+
if [ -n "$SDLC_STAGE" ] && ! [[ "$SDLC_STAGE" =~ ^[1-5]$ ]]; then
|
|
152
|
+
echo "Error: --sdlc-stage must be an integer 1-5 (got: $SDLC_STAGE)"
|
|
153
|
+
exit 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [ -z "${DEVAUDIT_BASE_URL:-}" ]; then
|
|
157
|
+
echo "Error: DEVAUDIT_BASE_URL environment variable is required"
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
if [ -z "${DEVAUDIT_API_KEY:-}" ]; then
|
|
161
|
+
echo "Error: DEVAUDIT_API_KEY environment variable is required (issue from"
|
|
162
|
+
echo " Project Settings → API Keys in META-COMPLY)"
|
|
163
|
+
exit 1
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Strip any trailing slash so we don't double-up later.
|
|
167
|
+
DEVAUDIT_BASE_URL="${DEVAUDIT_BASE_URL%/}"
|
|
168
|
+
|
|
169
|
+
# --- Base-URL drift check (devaudit-installer#143) ---
|
|
170
|
+
# When the portal moves host (e.g. devaudit.metasession.co → devaudit.ai)
|
|
171
|
+
# Cloudflare / the origin replies 301/302 with a Location header pointing
|
|
172
|
+
# at the new host. Every consumer's CI that still uses the old base URL
|
|
173
|
+
# fails every upload until DEVAUDIT_BASE_URL is rotated. We don't want
|
|
174
|
+
# the failure mode to be a silent "evidence didn't upload" — surface the
|
|
175
|
+
# drift loudly at the top of the run so the operator knows to rotate the
|
|
176
|
+
# secret. (We still upload via `curl -L` so the run itself succeeds; the
|
|
177
|
+
# warning is the nudge to fix the secret, not a hard stop.)
|
|
178
|
+
probe_base_url_drift() {
|
|
179
|
+
# `${var:-}` so `set -u` doesn't trip if curl isn't installed or the
|
|
180
|
+
# network is offline. Probe with -I (HEAD); fall back to GET if HEAD
|
|
181
|
+
# is rejected by the upstream proxy. 5s connect + 10s overall is
|
|
182
|
+
# plenty for a redirect-only probe — we never read a body.
|
|
183
|
+
local probe_url="${DEVAUDIT_BASE_URL}/api/health"
|
|
184
|
+
local resp
|
|
185
|
+
resp=$(curl -sI -o /dev/null --max-time 10 --connect-timeout 5 \
|
|
186
|
+
-w "%{http_code} %{redirect_url}" "$probe_url" 2>/dev/null || true)
|
|
187
|
+
local code="${resp%% *}"
|
|
188
|
+
local redirect_url="${resp#* }"
|
|
189
|
+
if [[ "$code" =~ ^30[1278]$ ]] && [ -n "$redirect_url" ] && [ "$redirect_url" != " " ]; then
|
|
190
|
+
local old_host new_host
|
|
191
|
+
old_host=$(printf '%s' "$DEVAUDIT_BASE_URL" | sed -E 's|^https?://([^/]+).*|\1|')
|
|
192
|
+
new_host=$(printf '%s' "$redirect_url" | sed -E 's|^https?://([^/]+).*|\1|')
|
|
193
|
+
if [ -n "$new_host" ] && [ "$old_host" != "$new_host" ]; then
|
|
194
|
+
echo "WARNING: DEVAUDIT_BASE_URL host '${old_host}' redirects to '${new_host}'."
|
|
195
|
+
echo " Rotate the DEVAUDIT_BASE_URL secret in your CI environment to"
|
|
196
|
+
echo " the new host to avoid silent breakage. (Uploads will still"
|
|
197
|
+
echo " succeed this run — curl follows the redirect — but the"
|
|
198
|
+
echo " underlying secret should be updated.)"
|
|
199
|
+
echo " Ref: https://github.com/metasession-dev/DevAudit-Installer/issues/143"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
probe_base_url_drift
|
|
204
|
+
|
|
205
|
+
# --- Build metadata JSON ---
|
|
206
|
+
# Assemble entries first; only emit `{ ... }` if at least one field is
|
|
207
|
+
# set. Each entry is a `"key":"value"` JSON pair with the value
|
|
208
|
+
# json-escaped (quotes + backslashes).
|
|
209
|
+
json_escape() {
|
|
210
|
+
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
211
|
+
}
|
|
212
|
+
META_ENTRIES=()
|
|
213
|
+
[ -n "$GIT_SHA" ] && META_ENTRIES+=("\"gitSha\":\"$(json_escape "$GIT_SHA")\"")
|
|
214
|
+
[ -n "$CI_RUN_ID" ] && META_ENTRIES+=("\"ciRunId\":\"$(json_escape "$CI_RUN_ID")\"")
|
|
215
|
+
[ -n "$BRANCH" ] && META_ENTRIES+=("\"branch\":\"$(json_escape "$BRANCH")\"")
|
|
216
|
+
for KV in "${META_KEYS[@]}"; do
|
|
217
|
+
KEY="${KV%%=*}"
|
|
218
|
+
VAL="${KV#*=}"
|
|
219
|
+
META_ENTRIES+=("\"$(json_escape "$KEY")\":\"$(json_escape "$VAL")\"")
|
|
220
|
+
done
|
|
221
|
+
if [ "${#META_ENTRIES[@]}" -gt 0 ]; then
|
|
222
|
+
IFS=','
|
|
223
|
+
METADATA="{${META_ENTRIES[*]}}"
|
|
224
|
+
unset IFS
|
|
225
|
+
else
|
|
226
|
+
METADATA="{}"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# --- Collect files ---
|
|
230
|
+
FILES=()
|
|
231
|
+
if [ -d "$FILE_PATH" ]; then
|
|
232
|
+
while IFS= read -r -d '' f; do
|
|
233
|
+
FILES+=("$f")
|
|
234
|
+
done < <(find "$FILE_PATH" -type f -print0)
|
|
235
|
+
else
|
|
236
|
+
FILES=("$FILE_PATH")
|
|
237
|
+
fi
|
|
238
|
+
if [ ${#FILES[@]} -eq 0 ]; then
|
|
239
|
+
echo "Error: No files found at $FILE_PATH"
|
|
240
|
+
exit 1
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# --- Upload each file via /api/evidence/upload ---
|
|
244
|
+
#
|
|
245
|
+
# Retry-on-429 / 5xx: consumer CI workflows fire 8-12 sequential uploads per
|
|
246
|
+
# requirement (test-scope, test-plan, implementation-plan, security-summary,
|
|
247
|
+
# test-execution-summary, ai-prompts, ai-use-note, uat-checklist, gates/*.json).
|
|
248
|
+
# That cadence trips DevAudit's per-IP rate limiter, marking otherwise-green
|
|
249
|
+
# CI runs as failed. Retry transiently with exponential backoff (1s → 16s),
|
|
250
|
+
# honouring Retry-After if the portal supplies it. Auth/validation errors
|
|
251
|
+
# (4xx other than 429) still fail fast — they won't fix themselves.
|
|
252
|
+
#
|
|
253
|
+
# Issue: devaudit#263.
|
|
254
|
+
SUCCEEDED=0
|
|
255
|
+
FAILED=0
|
|
256
|
+
# devaudit#133 — central stub guard. Any file still carrying the
|
|
257
|
+
# DevAudit starter banner ("STARTER TEMPLATE — REPLACE BEFORE
|
|
258
|
+
# COMMITTING" / "...BEFORE GOING TO PRODUCTION" — both phrasings)
|
|
259
|
+
# is skipped before the upload attempt so unedited placeholders
|
|
260
|
+
# can't flip a clause to COVERED off a stub. The check is binary-
|
|
261
|
+
# safe (-a) so it doesn't choke on PNGs or other non-text files.
|
|
262
|
+
SKIPPED=0
|
|
263
|
+
TOTAL_SIZE=0
|
|
264
|
+
UPLOAD_URL="${DEVAUDIT_BASE_URL}/api/evidence/upload"
|
|
265
|
+
MAX_ATTEMPTS=${UPLOAD_MAX_ATTEMPTS:-5}
|
|
266
|
+
INITIAL_BACKOFF_SECONDS=${UPLOAD_INITIAL_BACKOFF_SECONDS:-1}
|
|
267
|
+
UPLOAD_CONNECT_TIMEOUT_SECONDS=${UPLOAD_CONNECT_TIMEOUT_SECONDS:-10}
|
|
268
|
+
UPLOAD_MAX_TIME_SECONDS=${UPLOAD_MAX_TIME_SECONDS:-120}
|
|
269
|
+
# DevAudit-Installer#189 — files above this size use the presigned R2 URL
|
|
270
|
+
# upload flow (3-step: request URL → PUT to R2 → notify portal) instead of
|
|
271
|
+
# the multipart POST. Avoids the portal's FormData body parser limit.
|
|
272
|
+
# 25MB = 26214400 bytes.
|
|
273
|
+
PRESIGNED_THRESHOLD_BYTES=${PRESIGNED_THRESHOLD:-26214400}
|
|
274
|
+
PRESIGNED_MAX_ATTEMPTS=${PRESIGNED_MAX_ATTEMPTS:-3}
|
|
275
|
+
PRESIGNED_UPLOAD_MAX_TIME_SECONDS=${PRESIGNED_UPLOAD_MAX_TIME_SECONDS:-300}
|
|
276
|
+
|
|
277
|
+
is_unedited_starter_stub() {
|
|
278
|
+
# Match BOTH banner phrasings the SDLC has shipped (v0.1.36 changed
|
|
279
|
+
# the wording from "...GOING TO PRODUCTION" to "...COMMITTING").
|
|
280
|
+
# -a forces binary→text so we don't error on PNGs/PDFs; the regex
|
|
281
|
+
# won't match either of those formats by accident.
|
|
282
|
+
grep -aqE 'STARTER TEMPLATE.+REPLACE BEFORE' "$1"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# DevAudit-Installer#189 — Presigned R2 URL upload for large files (>25MB).
|
|
286
|
+
# 3-step flow: (1) request presigned URL from portal, (2) PUT file to R2,
|
|
287
|
+
# (3) notify portal that upload is complete. Each step retries on 429/5xx
|
|
288
|
+
# and connection errors. Returns 0 on success, 1 on failure.
|
|
289
|
+
upload_presigned() {
|
|
290
|
+
local file="$1"
|
|
291
|
+
local filename
|
|
292
|
+
filename=$(basename "$file")
|
|
293
|
+
local file_size
|
|
294
|
+
file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
|
|
295
|
+
|
|
296
|
+
# Derive a MIME type from the extension (best-effort).
|
|
297
|
+
local mime_type="application/octet-stream"
|
|
298
|
+
case "$filename" in
|
|
299
|
+
*.zip) mime_type="application/zip" ;;
|
|
300
|
+
*.json) mime_type="application/json" ;;
|
|
301
|
+
*.png) mime_type="image/png" ;;
|
|
302
|
+
*.pdf) mime_type="application/pdf" ;;
|
|
303
|
+
*.md) mime_type="text/markdown" ;;
|
|
304
|
+
*.html) mime_type="text/html" ;;
|
|
305
|
+
esac
|
|
306
|
+
|
|
307
|
+
local presign_url="${DEVAUDIT_BASE_URL}/api/evidence/upload-url"
|
|
308
|
+
local complete_url="${DEVAUDIT_BASE_URL}/api/evidence/upload-complete"
|
|
309
|
+
|
|
310
|
+
# --- Step 1: Request presigned upload URL ---
|
|
311
|
+
local attempt backoff http_code curl_exit resp_body upload_url evidence_id
|
|
312
|
+
attempt=1
|
|
313
|
+
backoff=$INITIAL_BACKOFF_SECONDS
|
|
314
|
+
upload_url=""
|
|
315
|
+
evidence_id=""
|
|
316
|
+
while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
|
|
317
|
+
resp_body=$(mktemp)
|
|
318
|
+
http_code=$(curl -s -o "$resp_body" -w "%{http_code}" \
|
|
319
|
+
-X POST -L --max-redirs 3 \
|
|
320
|
+
--connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
|
|
321
|
+
--max-time "$UPLOAD_MAX_TIME_SECONDS" \
|
|
322
|
+
"$presign_url" \
|
|
323
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
324
|
+
-H "Content-Type: application/json" \
|
|
325
|
+
-d "{
|
|
326
|
+
\"projectSlug\": \"${PROJECT_SLUG}\",
|
|
327
|
+
\"requirementId\": \"${REQUIREMENT_ID}\",
|
|
328
|
+
\"evidenceType\": \"${EVIDENCE_TYPE}\",
|
|
329
|
+
\"fileName\": \"${filename}\",
|
|
330
|
+
\"fileSizeBytes\": ${file_size},
|
|
331
|
+
\"mimeType\": \"${mime_type}\",
|
|
332
|
+
\"metadata\": ${METADATA},
|
|
333
|
+
\"releaseVersion\": \"${RELEASE_VERSION}\",
|
|
334
|
+
\"createReleaseIfMissing\": \"${CREATE_RELEASE_IF_MISSING}\",
|
|
335
|
+
\"releaseBranch\": \"${BRANCH}\",
|
|
336
|
+
\"environment\": \"${ENVIRONMENT}\",
|
|
337
|
+
\"evidenceCategory\": \"${EVIDENCE_CATEGORY}\",
|
|
338
|
+
\"releaseTitle\": \"${RELEASE_TITLE}\",
|
|
339
|
+
\"releaseSummary\": \"${RELEASE_SUMMARY}\",
|
|
340
|
+
\"sdlcStage\": \"${SDLC_STAGE}\",
|
|
341
|
+
\"testCycleId\": \"${TEST_CYCLE}\"
|
|
342
|
+
}") || curl_exit=$?
|
|
343
|
+
curl_exit=${curl_exit:-0}
|
|
344
|
+
if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
345
|
+
upload_url=$(jq -r '.uploadUrl // empty' "$resp_body" 2>/dev/null || true)
|
|
346
|
+
evidence_id=$(jq -r '.evidenceId // empty' "$resp_body" 2>/dev/null || true)
|
|
347
|
+
rm -f "$resp_body"
|
|
348
|
+
if [ -n "$upload_url" ] && [ -n "$evidence_id" ] && [ "$upload_url" != "null" ] && [ "$evidence_id" != "null" ]; then
|
|
349
|
+
break
|
|
350
|
+
fi
|
|
351
|
+
# Portal responded 2xx but didn't return presigned URL fields —
|
|
352
|
+
# presigned URL flow not configured. Fall back to multipart.
|
|
353
|
+
echo -n "(portal did not return presigned URL, falling back to multipart) "
|
|
354
|
+
return 255
|
|
355
|
+
fi
|
|
356
|
+
if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
|
|
357
|
+
if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
|
|
358
|
+
rm -f "$resp_body"
|
|
359
|
+
echo -n "(step 1: HTTP ${http_code}, retry in ${backoff}s) "
|
|
360
|
+
sleep "$backoff"
|
|
361
|
+
attempt=$((attempt + 1))
|
|
362
|
+
backoff=$((backoff * 2))
|
|
363
|
+
continue
|
|
364
|
+
fi
|
|
365
|
+
fi
|
|
366
|
+
# Non-retriable error (4xx other than 429) or exhausted retries.
|
|
367
|
+
# #298 — Surface the Portal response body so operators see WHY it
|
|
368
|
+
# failed (e.g. "Presigned uploads require R2 storage configuration")
|
|
369
|
+
# instead of just "HTTP 501". Include up to 500 chars of the body.
|
|
370
|
+
local err_excerpt=""
|
|
371
|
+
if [ -s "$resp_body" ]; then
|
|
372
|
+
err_excerpt=$(head -c 500 "$resp_body")
|
|
373
|
+
fi
|
|
374
|
+
rm -f "$resp_body"
|
|
375
|
+
echo -n "(step 1 failed: HTTP ${http_code}"
|
|
376
|
+
if [ -n "$err_excerpt" ]; then
|
|
377
|
+
echo -n " — ${err_excerpt}"
|
|
378
|
+
fi
|
|
379
|
+
echo ") "
|
|
380
|
+
return 1
|
|
381
|
+
done
|
|
382
|
+
|
|
383
|
+
if [ -z "$upload_url" ] || [ -z "$evidence_id" ]; then
|
|
384
|
+
echo -n "(step 1: no presigned URL after ${attempt} attempts) "
|
|
385
|
+
return 1
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# --- Step 2: Upload directly to R2 via presigned URL ---
|
|
389
|
+
attempt=1
|
|
390
|
+
backoff=$INITIAL_BACKOFF_SECONDS
|
|
391
|
+
while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
|
|
392
|
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
393
|
+
-X PUT \
|
|
394
|
+
-H "Content-Type: ${mime_type}" \
|
|
395
|
+
--connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
|
|
396
|
+
--max-time "$PRESIGNED_UPLOAD_MAX_TIME_SECONDS" \
|
|
397
|
+
--data-binary @"$file" \
|
|
398
|
+
"$upload_url") || curl_exit=$?
|
|
399
|
+
curl_exit=${curl_exit:-0}
|
|
400
|
+
if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
401
|
+
break
|
|
402
|
+
fi
|
|
403
|
+
if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
|
|
404
|
+
if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
|
|
405
|
+
echo -n "(step 2: HTTP ${http_code}, retry in ${backoff}s) "
|
|
406
|
+
sleep "$backoff"
|
|
407
|
+
attempt=$((attempt + 1))
|
|
408
|
+
backoff=$((backoff * 2))
|
|
409
|
+
continue
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
echo -n "(step 2 failed: HTTP ${http_code}) "
|
|
413
|
+
return 1
|
|
414
|
+
done
|
|
415
|
+
|
|
416
|
+
# --- Step 3: Notify portal that upload is complete ---
|
|
417
|
+
attempt=1
|
|
418
|
+
backoff=$INITIAL_BACKOFF_SECONDS
|
|
419
|
+
while [ "$attempt" -le "$PRESIGNED_MAX_ATTEMPTS" ]; do
|
|
420
|
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
421
|
+
-X POST -L --max-redirs 3 \
|
|
422
|
+
--connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS" \
|
|
423
|
+
--max-time "$UPLOAD_MAX_TIME_SECONDS" \
|
|
424
|
+
"$complete_url" \
|
|
425
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}" \
|
|
426
|
+
-H "Content-Type: application/json" \
|
|
427
|
+
-d "{\"evidenceId\": \"${evidence_id}\"}") || curl_exit=$?
|
|
428
|
+
curl_exit=${curl_exit:-0}
|
|
429
|
+
if [ "$curl_exit" -eq 0 ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
430
|
+
return 0
|
|
431
|
+
fi
|
|
432
|
+
if [ "$curl_exit" -ne 0 ] || [ "$http_code" = "429" ] || { [ "$http_code" -ge 500 ] && [ "$http_code" -lt 600 ]; }; then
|
|
433
|
+
if [ "$attempt" -lt "$PRESIGNED_MAX_ATTEMPTS" ]; then
|
|
434
|
+
echo -n "(step 3: HTTP ${http_code}, retry in ${backoff}s) "
|
|
435
|
+
sleep "$backoff"
|
|
436
|
+
attempt=$((attempt + 1))
|
|
437
|
+
backoff=$((backoff * 2))
|
|
438
|
+
continue
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
echo -n "(step 3 failed: HTTP ${http_code}) "
|
|
442
|
+
return 1
|
|
443
|
+
done
|
|
444
|
+
return 1
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for FILE in "${FILES[@]}"; do
|
|
448
|
+
FILENAME=$(basename "$FILE")
|
|
449
|
+
if is_unedited_starter_stub "$FILE"; then
|
|
450
|
+
echo "SKIPPED ${FILENAME} — unedited starter stub (replace the STARTER TEMPLATE banner to upload)"
|
|
451
|
+
SKIPPED=$((SKIPPED + 1))
|
|
452
|
+
continue
|
|
453
|
+
fi
|
|
454
|
+
FILE_SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
|
|
455
|
+
echo -n "Uploading ${FILENAME}... "
|
|
456
|
+
|
|
457
|
+
# DevAudit-Installer#189 — large files use the presigned R2 URL flow
|
|
458
|
+
# to bypass the portal's multipart body parser limit. If the portal
|
|
459
|
+
# doesn't support presigned URLs (returns 255), fall back to multipart.
|
|
460
|
+
if [ "$FILE_SIZE" -ge "$PRESIGNED_THRESHOLD_BYTES" ]; then
|
|
461
|
+
PRESIGNED_RESULT=0
|
|
462
|
+
upload_presigned "$FILE" || PRESIGNED_RESULT=$?
|
|
463
|
+
if [ "$PRESIGNED_RESULT" -eq 0 ]; then
|
|
464
|
+
echo "OK (${FILE_SIZE} bytes, presigned R2 upload)"
|
|
465
|
+
SUCCEEDED=$((SUCCEEDED + 1))
|
|
466
|
+
TOTAL_SIZE=$((TOTAL_SIZE + FILE_SIZE))
|
|
467
|
+
continue
|
|
468
|
+
elif [ "$PRESIGNED_RESULT" -ne 255 ]; then
|
|
469
|
+
echo "FAILED (presigned upload after ${PRESIGNED_MAX_ATTEMPTS} attempts)"
|
|
470
|
+
FAILED=$((FAILED + 1))
|
|
471
|
+
continue
|
|
472
|
+
fi
|
|
473
|
+
# Result 255 — portal doesn't support presigned URLs, fall through
|
|
474
|
+
# to the existing multipart flow.
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
# `-L` follows 3xx redirects (devaudit-installer#143). The portal host
|
|
478
|
+
# has moved before (devaudit.metasession.co → devaudit.ai); without -L
|
|
479
|
+
# every consumer's CI silently fails on a stale base URL. `--max-redirs 3`
|
|
480
|
+
# bounds the follow so a misconfigured redirect loop can't hang CI.
|
|
481
|
+
CURL_ARGS=(
|
|
482
|
+
-X POST -L --max-redirs 3
|
|
483
|
+
--connect-timeout "$UPLOAD_CONNECT_TIMEOUT_SECONDS"
|
|
484
|
+
--max-time "$UPLOAD_MAX_TIME_SECONDS"
|
|
485
|
+
"$UPLOAD_URL"
|
|
486
|
+
-H "Authorization: Bearer ${DEVAUDIT_API_KEY}"
|
|
487
|
+
-F "file=@${FILE}"
|
|
488
|
+
-F "projectSlug=${PROJECT_SLUG}"
|
|
489
|
+
-F "requirementId=${REQUIREMENT_ID}"
|
|
490
|
+
-F "evidenceType=${EVIDENCE_TYPE}"
|
|
491
|
+
-F "metadata=${METADATA}"
|
|
492
|
+
)
|
|
493
|
+
[ -n "$RELEASE_VERSION" ] && CURL_ARGS+=(-F "releaseVersion=${RELEASE_VERSION}")
|
|
494
|
+
if [ "$CREATE_RELEASE_IF_MISSING" = true ]; then
|
|
495
|
+
CURL_ARGS+=(-F "createReleaseIfMissing=true")
|
|
496
|
+
fi
|
|
497
|
+
[ -n "$BRANCH" ] && CURL_ARGS+=(-F "releaseBranch=${BRANCH}")
|
|
498
|
+
[ -n "$ENVIRONMENT" ] && CURL_ARGS+=(-F "environment=${ENVIRONMENT}")
|
|
499
|
+
[ -n "$EVIDENCE_CATEGORY" ] && CURL_ARGS+=(-F "evidenceCategory=${EVIDENCE_CATEGORY}")
|
|
500
|
+
[ -n "$RELEASE_TITLE" ] && CURL_ARGS+=(-F "releaseTitle=${RELEASE_TITLE}")
|
|
501
|
+
[ -n "$RELEASE_SUMMARY" ] && CURL_ARGS+=(-F "releaseSummary=${RELEASE_SUMMARY}")
|
|
502
|
+
[ -n "$CHANGE_TYPE" ] && CURL_ARGS+=(-F "changeType=${CHANGE_TYPE}")
|
|
503
|
+
[ -n "$GATE_STATUS" ] && CURL_ARGS+=(-F "gateStatus=${GATE_STATUS}")
|
|
504
|
+
[ -n "$SDLC_STAGE" ] && CURL_ARGS+=(-F "sdlcStage=${SDLC_STAGE}")
|
|
505
|
+
[ -n "$TEST_CYCLE" ] && CURL_ARGS+=(-F "testCycleId=${TEST_CYCLE}")
|
|
506
|
+
|
|
507
|
+
ATTEMPT=1
|
|
508
|
+
BACKOFF=$INITIAL_BACKOFF_SECONDS
|
|
509
|
+
HTTP_CODE=0
|
|
510
|
+
RESP_BODY_FILE=""
|
|
511
|
+
RESP_HEADERS_FILE=""
|
|
512
|
+
LAST_CURL_ERROR=""
|
|
513
|
+
while [ "$ATTEMPT" -le "$MAX_ATTEMPTS" ]; do
|
|
514
|
+
[ -n "$RESP_BODY_FILE" ] && rm -f "$RESP_BODY_FILE"
|
|
515
|
+
RESP_BODY_FILE=$(mktemp)
|
|
516
|
+
RESP_HEADERS_FILE=$(mktemp)
|
|
517
|
+
CURL_EXIT=0
|
|
518
|
+
HTTP_CODE=$(curl -s -o "$RESP_BODY_FILE" -D "$RESP_HEADERS_FILE" -w "%{http_code}" "${CURL_ARGS[@]}") || CURL_EXIT=$?
|
|
519
|
+
if [ "$CURL_EXIT" -ne 0 ]; then
|
|
520
|
+
LAST_CURL_ERROR="curl exit ${CURL_EXIT}"
|
|
521
|
+
if [ "$CURL_EXIT" -eq 28 ]; then
|
|
522
|
+
LAST_CURL_ERROR="${LAST_CURL_ERROR} (timed out after ${UPLOAD_MAX_TIME_SECONDS}s)"
|
|
523
|
+
fi
|
|
524
|
+
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
|
|
525
|
+
WAIT_SECONDS=$BACKOFF
|
|
526
|
+
echo -n "(${LAST_CURL_ERROR}, retry in ${WAIT_SECONDS}s) "
|
|
527
|
+
rm -f "$RESP_HEADERS_FILE"
|
|
528
|
+
sleep "$WAIT_SECONDS"
|
|
529
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
530
|
+
BACKOFF=$((BACKOFF * 2))
|
|
531
|
+
continue
|
|
532
|
+
fi
|
|
533
|
+
rm -f "$RESP_HEADERS_FILE"
|
|
534
|
+
break
|
|
535
|
+
fi
|
|
536
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
537
|
+
rm -f "$RESP_HEADERS_FILE"
|
|
538
|
+
break
|
|
539
|
+
fi
|
|
540
|
+
# Decide whether the failure is retriable (429 or 5xx).
|
|
541
|
+
if [ "$HTTP_CODE" = "429" ] || { [ "$HTTP_CODE" -ge 500 ] && [ "$HTTP_CODE" -lt 600 ]; }; then
|
|
542
|
+
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
|
|
543
|
+
# Honour Retry-After (seconds) if the portal supplied it; otherwise back off.
|
|
544
|
+
WAIT_SECONDS=$BACKOFF
|
|
545
|
+
RETRY_AFTER=$(grep -i '^retry-after:' "$RESP_HEADERS_FILE" 2>/dev/null | head -1 | sed 's/^[Rr]etry-[Aa]fter:[[:space:]]*//' | tr -d '\r')
|
|
546
|
+
if [[ "$RETRY_AFTER" =~ ^[0-9]+$ ]] && [ "$RETRY_AFTER" -gt 0 ]; then
|
|
547
|
+
WAIT_SECONDS=$RETRY_AFTER
|
|
548
|
+
fi
|
|
549
|
+
echo -n "(HTTP ${HTTP_CODE}, retry in ${WAIT_SECONDS}s) "
|
|
550
|
+
rm -f "$RESP_HEADERS_FILE"
|
|
551
|
+
sleep "$WAIT_SECONDS"
|
|
552
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
553
|
+
BACKOFF=$((BACKOFF * 2))
|
|
554
|
+
continue
|
|
555
|
+
fi
|
|
556
|
+
fi
|
|
557
|
+
rm -f "$RESP_HEADERS_FILE"
|
|
558
|
+
break
|
|
559
|
+
done
|
|
560
|
+
|
|
561
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
562
|
+
rm -f "$RESP_BODY_FILE"
|
|
563
|
+
if [ "$ATTEMPT" -gt 1 ]; then
|
|
564
|
+
echo "OK (${FILE_SIZE} bytes, attempt ${ATTEMPT}/${MAX_ATTEMPTS})"
|
|
565
|
+
else
|
|
566
|
+
echo "OK (${FILE_SIZE} bytes)"
|
|
567
|
+
fi
|
|
568
|
+
SUCCEEDED=$((SUCCEEDED + 1))
|
|
569
|
+
TOTAL_SIZE=$((TOTAL_SIZE + FILE_SIZE))
|
|
570
|
+
else
|
|
571
|
+
if [ -n "$LAST_CURL_ERROR" ]; then
|
|
572
|
+
echo "FAILED (${LAST_CURL_ERROR} after ${ATTEMPT} attempt(s))"
|
|
573
|
+
else
|
|
574
|
+
echo "FAILED (HTTP ${HTTP_CODE} after ${ATTEMPT} attempt(s))"
|
|
575
|
+
fi
|
|
576
|
+
if [ -s "$RESP_BODY_FILE" ]; then
|
|
577
|
+
echo " Response: $(head -c 500 "$RESP_BODY_FILE")"
|
|
578
|
+
fi
|
|
579
|
+
rm -f "$RESP_BODY_FILE"
|
|
580
|
+
FAILED=$((FAILED + 1))
|
|
581
|
+
fi
|
|
582
|
+
done
|
|
583
|
+
|
|
584
|
+
# --- Summary ---
|
|
585
|
+
echo ""
|
|
586
|
+
echo "=== Upload Summary ==="
|
|
587
|
+
echo "Files: ${SUCCEEDED} succeeded, ${FAILED} failed, ${SKIPPED} skipped (${#FILES[@]} total)"
|
|
588
|
+
echo "Total size: $((TOTAL_SIZE / 1024)) KB"
|
|
589
|
+
# Skipped stubs are intentional (devaudit#133) — they don't fail the
|
|
590
|
+
# run. Only true upload failures bump the exit code.
|
|
591
|
+
if [ "$FAILED" -gt 0 ]; then
|
|
592
|
+
exit 1
|
|
593
|
+
fi
|
|
@@ -37,6 +37,23 @@ fi
|
|
|
37
37
|
TOTAL=0
|
|
38
38
|
FAILED=0
|
|
39
39
|
|
|
40
|
+
ACTIVE_RELEASE_REQS=""
|
|
41
|
+
if [ -d compliance/pending-releases ]; then
|
|
42
|
+
ACTIVE_RELEASE_REQS=$(find compliance/pending-releases -maxdepth 1 -type f \
|
|
43
|
+
-name 'RELEASE-TICKET-REQ-*.md' -print 2>/dev/null \
|
|
44
|
+
| sed -E 's|.*/RELEASE-TICKET-(REQ-[0-9]+)\.md$|\1|' | sort -u || true)
|
|
45
|
+
fi
|
|
46
|
+
if [ -z "$ACTIVE_RELEASE_REQS" ] && [ -f compliance/RTM.md ]; then
|
|
47
|
+
ACTIVE_RELEASE_REQS=$(sed 's/\\|/ /g' compliance/RTM.md 2>/dev/null \
|
|
48
|
+
| grep -E '\|[[:space:]]+IN PROGRESS|\|[[:space:]]+TESTED - PENDING SIGN-OFF' \
|
|
49
|
+
| grep -oE '^\|[[:space:]]*REQ-[0-9]+' \
|
|
50
|
+
| grep -oE 'REQ-[0-9]+' | sort -u || true)
|
|
51
|
+
fi
|
|
52
|
+
ACTIVE_RELEASE_REQ_COUNT=0
|
|
53
|
+
if [ -n "$ACTIVE_RELEASE_REQS" ]; then
|
|
54
|
+
ACTIVE_RELEASE_REQ_COUNT=$(printf '%s\n' "$ACTIVE_RELEASE_REQS" | grep -c . || true)
|
|
55
|
+
fi
|
|
56
|
+
|
|
40
57
|
while IFS= read -r sha; do
|
|
41
58
|
TOTAL=$((TOTAL + 1))
|
|
42
59
|
SUBJECT=$(git log -1 --format='%s' "$sha")
|
|
@@ -66,13 +83,21 @@ while IFS= read -r sha; do
|
|
|
66
83
|
feat|fix|refactor|perf)
|
|
67
84
|
if ! echo "$SUBJECT" | grep -qP '\[REQ-\d{3,}\]' \
|
|
68
85
|
&& ! echo "$BODY" | grep -qiP 'Ref:\s*REQ-\d{3,}'; then
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
if [ "$ACTIVE_RELEASE_REQ_COUNT" -eq 1 ]; then
|
|
87
|
+
ACTIVE_REQ=$(printf '%s\n' "$ACTIVE_RELEASE_REQS" | head -1)
|
|
88
|
+
echo "WARNING [$SHORT]: '$TYPE' cites no requirement, but the branch has one active tracked release (${ACTIVE_REQ})."
|
|
89
|
+
echo " Treating the release ticket / RTM context as authoritative for this already-merged history."
|
|
90
|
+
echo " Add a follow-up compliance note if this was not an intentional recovery path."
|
|
91
|
+
WARN_COUNT=$((WARN_COUNT + 1))
|
|
92
|
+
else
|
|
93
|
+
echo "ERROR [$SHORT]: '$TYPE' is an implementation commit but cites no requirement."
|
|
94
|
+
echo " Add [REQ-XXX] to the subject or a 'Ref: REQ-XXX' trailer. Start work"
|
|
95
|
+
echo " from a requirement via the sdlc-implementer skill (it assigns the REQ"
|
|
96
|
+
echo " from the originating issue in Phase 1)."
|
|
97
|
+
FAILED=$((FAILED + 1))
|
|
98
|
+
EXIT_CODE=1
|
|
99
|
+
continue
|
|
100
|
+
fi
|
|
76
101
|
fi
|
|
77
102
|
|
|
78
103
|
# RTM provenance check (devaudit-installer#226): verify the REQ-XXX
|