@jonit-dev/night-watch-cli 1.7.50 โ 1.7.52
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/cli.js +393 -229
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +6 -24
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -23
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +16 -4
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +6 -4
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/shared/env-builder.d.ts +5 -0
- package/dist/commands/shared/env-builder.d.ts.map +1 -1
- package/dist/commands/shared/env-builder.js +32 -0
- package/dist/commands/shared/env-builder.js.map +1 -1
- package/dist/commands/slice.d.ts +8 -0
- package/dist/commands/slice.d.ts.map +1 -1
- package/dist/commands/slice.js +90 -2
- package/dist/commands/slice.js.map +1 -1
- package/dist/scripts/night-watch-audit-cron.sh +17 -4
- package/dist/scripts/night-watch-cron.sh +19 -5
- package/dist/scripts/night-watch-helpers.sh +137 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +268 -5
- package/dist/scripts/night-watch-qa-cron.sh +427 -22
- package/dist/scripts/night-watch-slicer-cron.sh +14 -3
- package/dist/templates/audit.md +87 -0
- package/dist/templates/executor.md +67 -0
- package/dist/templates/night-watch-pr-reviewer.md +33 -0
- package/dist/templates/night-watch.config.json +31 -1
- package/dist/templates/night-watch.md +31 -0
- package/dist/templates/pr-reviewer.md +203 -0
- package/dist/templates/qa.md +157 -0
- package/dist/templates/slicer.md +234 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
12
|
# NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
|
|
13
13
|
# NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
|
|
14
|
-
# NW_QA_ARTIFACTS=both - Artifact mode (
|
|
14
|
+
# NW_QA_ARTIFACTS=both - Artifact mode (screenshot, video, both)
|
|
15
15
|
# NW_QA_AUTO_INSTALL_PLAYWRIGHT=1 - Auto-install Playwright browsers
|
|
16
16
|
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
17
17
|
|
|
@@ -22,6 +22,7 @@ LOG_FILE="${LOG_DIR}/night-watch-qa.log"
|
|
|
22
22
|
MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
|
|
23
23
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
24
24
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
25
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
25
26
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
26
27
|
SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
|
|
27
28
|
QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
|
|
@@ -53,6 +54,286 @@ emit_result() {
|
|
|
53
54
|
fi
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
decode_base64_value() {
|
|
58
|
+
local value="${1:-}"
|
|
59
|
+
if [ -z "${value}" ]; then
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
if printf '%s' "${value}" | base64 --decode >/dev/null 2>&1; then
|
|
63
|
+
printf '%s' "${value}" | base64 --decode
|
|
64
|
+
else
|
|
65
|
+
printf '%s' "${value}" | base64 -d 2>/dev/null || true
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
append_csv() {
|
|
70
|
+
local current="${1:-}"
|
|
71
|
+
local incoming="${2:-}"
|
|
72
|
+
if [ -z "${incoming}" ]; then
|
|
73
|
+
printf "%s" "${current}"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
if [ -z "${current}" ]; then
|
|
77
|
+
printf "%s" "${incoming}"
|
|
78
|
+
else
|
|
79
|
+
printf "%s,%s" "${current}" "${incoming}"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
csv_or_none() {
|
|
84
|
+
local value="${1:-}"
|
|
85
|
+
if [ -n "${value}" ]; then
|
|
86
|
+
printf "%s" "${value}"
|
|
87
|
+
else
|
|
88
|
+
printf "none"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe_qa_artifacts() {
|
|
93
|
+
local mode="${1:-both}"
|
|
94
|
+
case "${mode}" in
|
|
95
|
+
screenshot)
|
|
96
|
+
printf "screenshots only"
|
|
97
|
+
;;
|
|
98
|
+
video)
|
|
99
|
+
printf "videos only"
|
|
100
|
+
;;
|
|
101
|
+
both)
|
|
102
|
+
printf "screenshots + videos"
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
printf "custom (%s)" "${mode}"
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
normalize_qa_screenshot_url() {
|
|
111
|
+
local raw_url="${1:-}"
|
|
112
|
+
if [ -z "${raw_url}" ]; then
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if printf '%s' "${raw_url}" | grep -Eq '^https?://'; then
|
|
117
|
+
printf '%s' "${raw_url}"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^\.\./blob/'; then
|
|
122
|
+
printf 'https://github.com/%s/%s' "${REPO}" "${raw_url#../}"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -n "${REPO:-}" ] && printf '%s' "${raw_url}" | grep -q '^blob/'; then
|
|
127
|
+
printf 'https://github.com/%s/%s' "${REPO}" "${raw_url}"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
printf '%s' "${raw_url}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
extract_url_host() {
|
|
135
|
+
local raw_url="${1:-}"
|
|
136
|
+
if [ -z "${raw_url}" ]; then
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
printf '%s' "${raw_url}" | sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://##; s#/.*$##'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resolve_claude_model_hint() {
|
|
143
|
+
local sonnet="${ANTHROPIC_DEFAULT_SONNET_MODEL:-}"
|
|
144
|
+
local opus="${ANTHROPIC_DEFAULT_OPUS_MODEL:-}"
|
|
145
|
+
local native_model="${NW_CLAUDE_MODEL_ID:-}"
|
|
146
|
+
|
|
147
|
+
if [ -n "${sonnet}" ] && [ -n "${opus}" ]; then
|
|
148
|
+
if [ "${sonnet}" = "${opus}" ]; then
|
|
149
|
+
printf "%s" "${sonnet}"
|
|
150
|
+
else
|
|
151
|
+
printf "sonnet=%s, opus=%s" "${sonnet}" "${opus}"
|
|
152
|
+
fi
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
if [ -n "${sonnet}" ]; then
|
|
156
|
+
printf "%s" "${sonnet}"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
if [ -n "${opus}" ]; then
|
|
160
|
+
printf "%s" "${opus}"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
if [ -n "${native_model}" ]; then
|
|
164
|
+
printf "%s" "${native_model}"
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
printf "default"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resolve_provider_model_display() {
|
|
171
|
+
local provider_cmd="${1:?provider command required}"
|
|
172
|
+
local provider_label="${2:-}"
|
|
173
|
+
local label_trimmed=""
|
|
174
|
+
local model_hint=""
|
|
175
|
+
local endpoint_host=""
|
|
176
|
+
local details=""
|
|
177
|
+
|
|
178
|
+
label_trimmed=$(printf '%s' "${provider_label}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
179
|
+
|
|
180
|
+
case "${provider_cmd}" in
|
|
181
|
+
claude)
|
|
182
|
+
model_hint=$(resolve_claude_model_hint)
|
|
183
|
+
endpoint_host=$(extract_url_host "${ANTHROPIC_BASE_URL:-}")
|
|
184
|
+
details="${model_hint}"
|
|
185
|
+
if [ -n "${endpoint_host}" ]; then
|
|
186
|
+
details="${details} via ${endpoint_host}"
|
|
187
|
+
fi
|
|
188
|
+
if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Claude" ] && [ "${label_trimmed}" != "Claude (proxy)" ]; then
|
|
189
|
+
details="${label_trimmed}; ${details}"
|
|
190
|
+
fi
|
|
191
|
+
printf "%s (%s)" "${provider_cmd}" "${details}"
|
|
192
|
+
;;
|
|
193
|
+
codex)
|
|
194
|
+
if [ -n "${label_trimmed}" ] && [ "${label_trimmed}" != "Codex" ]; then
|
|
195
|
+
printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
|
|
196
|
+
else
|
|
197
|
+
printf "%s" "${provider_cmd}"
|
|
198
|
+
fi
|
|
199
|
+
;;
|
|
200
|
+
*)
|
|
201
|
+
if [ -n "${label_trimmed}" ]; then
|
|
202
|
+
printf "%s (%s)" "${provider_cmd}" "${label_trimmed}"
|
|
203
|
+
else
|
|
204
|
+
printf "%s" "${provider_cmd}"
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get_pr_comment_bodies_base64() {
|
|
211
|
+
local pr_number="${1:?PR number required}"
|
|
212
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[]?.body | @base64' 2>/dev/null || true
|
|
213
|
+
if [ -n "${REPO:-}" ]; then
|
|
214
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body | @base64' 2>/dev/null || true
|
|
215
|
+
fi
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get_latest_qa_comment_body() {
|
|
219
|
+
local pr_number="${1:?PR number required}"
|
|
220
|
+
local latest=""
|
|
221
|
+
local encoded=""
|
|
222
|
+
local decoded=""
|
|
223
|
+
|
|
224
|
+
while IFS= read -r encoded; do
|
|
225
|
+
[ -z "${encoded}" ] && continue
|
|
226
|
+
decoded=$(decode_base64_value "${encoded}")
|
|
227
|
+
if printf '%s' "${decoded}" | grep -q '<!-- night-watch-qa-marker -->'; then
|
|
228
|
+
latest="${decoded}"
|
|
229
|
+
fi
|
|
230
|
+
done < <(get_pr_comment_bodies_base64 "${pr_number}")
|
|
231
|
+
|
|
232
|
+
printf "%s" "${latest}"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get_qa_screenshot_links() {
|
|
236
|
+
local pr_number="${1:?PR number required}"
|
|
237
|
+
local qa_comment=""
|
|
238
|
+
|
|
239
|
+
qa_comment=$(get_latest_qa_comment_body "${pr_number}")
|
|
240
|
+
if [ -z "${qa_comment}" ]; then
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
printf '%s' "${qa_comment}" \
|
|
245
|
+
| { grep -Eo '!\[[^]]*\]\(([^)]*qa-artifacts/[^)]*)\)' || true; } \
|
|
246
|
+
| sed -E 's/^!\[[^]]*\]\(([^)]*)\)$/\1/' \
|
|
247
|
+
| while IFS= read -r raw_url; do
|
|
248
|
+
[ -z "${raw_url}" ] && continue
|
|
249
|
+
normalize_qa_screenshot_url "${raw_url}"
|
|
250
|
+
printf '\n'
|
|
251
|
+
done \
|
|
252
|
+
| awk 'NF && !seen[$0]++'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
classify_qa_comment_outcome() {
|
|
256
|
+
local pr_number="${1:?PR number required}"
|
|
257
|
+
local qa_comment=""
|
|
258
|
+
local status_lines=""
|
|
259
|
+
|
|
260
|
+
qa_comment=$(get_latest_qa_comment_body "${pr_number}")
|
|
261
|
+
if [ -z "${qa_comment}" ]; then
|
|
262
|
+
printf "unclassified"
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if printf '%s' "${qa_comment}" | grep -Eqi 'QA: No tests needed for this PR|No tests needed'; then
|
|
267
|
+
printf "no_tests_needed"
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
status_lines=$(printf '%s' "${qa_comment}" | grep -E '^- \*\*Status\*\*:' || true)
|
|
272
|
+
if [ -z "${status_lines}" ]; then
|
|
273
|
+
printf "unclassified"
|
|
274
|
+
return 0
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if printf '%s' "${status_lines}" | grep -Eqi 'failing|failed|error|timed out|timeout'; then
|
|
278
|
+
printf "issues_found"
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
if printf '%s' "${status_lines}" | grep -Eqi 'all passing'; then
|
|
283
|
+
printf "passing"
|
|
284
|
+
return 0
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
printf "unclassified"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pr_has_qa_generated_files() {
|
|
291
|
+
local pr_number="${1:?PR number required}"
|
|
292
|
+
gh pr view "${pr_number}" --json files --jq '.files[]?.path' 2>/dev/null \
|
|
293
|
+
| grep -Eq '^(qa-artifacts/|tests/.*/qa/)'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
provider_output_looks_invalid() {
|
|
297
|
+
local from_line="${1:-0}"
|
|
298
|
+
if [ ! -f "${LOG_FILE}" ]; then
|
|
299
|
+
return 1
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
tail -n "+$((from_line + 1))" "${LOG_FILE}" 2>/dev/null \
|
|
303
|
+
| grep -Eqi 'Unknown skill:|session is in a broken state|working directory .* no longer exists|Please restart this session'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
validate_qa_evidence() {
|
|
307
|
+
local pr_number="${1:?PR number required}"
|
|
308
|
+
local qa_comment=""
|
|
309
|
+
|
|
310
|
+
qa_comment=$(get_latest_qa_comment_body "${pr_number}")
|
|
311
|
+
if [ -z "${qa_comment}" ]; then
|
|
312
|
+
log "FAIL-QA-EVIDENCE: PR #${pr_number} has no QA marker comment (<!-- night-watch-qa-marker -->)"
|
|
313
|
+
return 1
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
if printf '%s' "${qa_comment}" | grep -Eqi 'QA: No tests needed for this PR|No tests needed'; then
|
|
317
|
+
return 0
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
if ! pr_has_qa_generated_files "${pr_number}"; then
|
|
321
|
+
log "FAIL-QA-EVIDENCE: PR #${pr_number} has QA marker comment but no qa-artifacts/ or tests/*/qa/ files"
|
|
322
|
+
return 1
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if [ "${QA_ARTIFACTS}" = "screenshot" ] || [ "${QA_ARTIFACTS}" = "both" ]; then
|
|
326
|
+
if printf '%s' "${qa_comment}" | grep -q '#### UI Tests (Playwright)'; then
|
|
327
|
+
if ! printf '%s' "${qa_comment}" | grep -Eq '!\[[^]]*\]\([^)]*qa-artifacts/[^)]*\)'; then
|
|
328
|
+
log "FAIL-QA-EVIDENCE: PR #${pr_number} reports UI tests but comment lacks screenshot links to qa-artifacts/"
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
return 0
|
|
335
|
+
}
|
|
336
|
+
|
|
56
337
|
# Validate provider
|
|
57
338
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
58
339
|
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
@@ -68,8 +349,13 @@ fi
|
|
|
68
349
|
|
|
69
350
|
cd "${PROJECT_DIR}"
|
|
70
351
|
|
|
352
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
353
|
+
QA_ARTIFACTS_DESC=$(describe_qa_artifacts "${QA_ARTIFACTS}")
|
|
354
|
+
|
|
71
355
|
send_telegram_status_message "๐งช Night Watch QA: started" "Project: ${PROJECT_NAME}
|
|
72
|
-
Provider: ${
|
|
356
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
357
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
358
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW}
|
|
73
359
|
Scanning open PRs for QA candidates."
|
|
74
360
|
|
|
75
361
|
# Convert comma-separated branch prefixes into a regex that matches branch starts.
|
|
@@ -101,7 +387,9 @@ OPEN_PRS=$(
|
|
|
101
387
|
if [ "${OPEN_PRS}" -eq 0 ]; then
|
|
102
388
|
log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
|
|
103
389
|
send_telegram_status_message "๐งช Night Watch QA: no matching PRs" "Project: ${PROJECT_NAME}
|
|
104
|
-
|
|
390
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
391
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW}
|
|
392
|
+
Result: 0 open PRs matched."
|
|
105
393
|
emit_result "skip_no_open_prs"
|
|
106
394
|
exit 0
|
|
107
395
|
fi
|
|
@@ -158,7 +446,9 @@ done < <(
|
|
|
158
446
|
if [ "${QA_NEEDED}" -eq 0 ]; then
|
|
159
447
|
log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
|
|
160
448
|
send_telegram_status_message "๐งช Night Watch QA: nothing to do" "Project: ${PROJECT_NAME}
|
|
161
|
-
|
|
449
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
450
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
451
|
+
Result: All matching PRs already have QA results."
|
|
162
452
|
emit_result "skip_all_qa_done"
|
|
163
453
|
exit 0
|
|
164
454
|
fi
|
|
@@ -181,10 +471,10 @@ cleanup_worktrees "${PROJECT_DIR}"
|
|
|
181
471
|
# Dry-run mode: print diagnostics and exit
|
|
182
472
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
183
473
|
echo "=== Dry Run: QA Runner ==="
|
|
184
|
-
echo "Provider: ${
|
|
474
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
185
475
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
186
476
|
echo "Skip Label: ${SKIP_LABEL}"
|
|
187
|
-
echo "QA Artifacts: ${QA_ARTIFACTS}"
|
|
477
|
+
echo "QA Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})"
|
|
188
478
|
echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
|
|
189
479
|
echo "Open PRs needing QA:${PRS_NEEDING_QA}"
|
|
190
480
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
@@ -194,17 +484,31 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
194
484
|
fi
|
|
195
485
|
|
|
196
486
|
EXIT_CODE=0
|
|
487
|
+
PROCESSED_PRS_CSV=""
|
|
488
|
+
PASSING_PRS_CSV=""
|
|
489
|
+
ISSUES_FOUND_PRS_CSV=""
|
|
490
|
+
NO_TESTS_PRS_CSV=""
|
|
491
|
+
UNCLASSIFIED_PRS_CSV=""
|
|
492
|
+
FAILED_AUTOMATION_PRS_CSV=""
|
|
493
|
+
FAILED_PR=""
|
|
494
|
+
FAILED_REASON="unknown"
|
|
495
|
+
QA_SCREENSHOT_SUMMARY=""
|
|
197
496
|
|
|
198
497
|
# Process each PR that needs QA
|
|
199
498
|
for pr_ref in ${PRS_NEEDING_QA}; do
|
|
200
499
|
pr_num="${pr_ref#\#}"
|
|
500
|
+
PROCESSED_PRS_CSV=$(append_csv "${PROCESSED_PRS_CSV}" "#${pr_num}")
|
|
201
501
|
send_telegram_status_message "๐งช Night Watch QA: processing PR #${pr_num}" "Project: ${PROJECT_NAME}
|
|
202
|
-
Provider: ${
|
|
203
|
-
Artifacts: ${QA_ARTIFACTS}
|
|
502
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
503
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
504
|
+
Action: generating QA tests and evidence."
|
|
204
505
|
|
|
205
506
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
206
507
|
if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
207
508
|
log "FAIL: Unable to create isolated QA worktree ${QA_WORKTREE_DIR} for PR #${pr_num}"
|
|
509
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
510
|
+
FAILED_PR="#${pr_num}"
|
|
511
|
+
FAILED_REASON="worktree_setup_failed"
|
|
208
512
|
EXIT_CODE=1
|
|
209
513
|
break
|
|
210
514
|
fi
|
|
@@ -212,21 +516,38 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
212
516
|
log "QA: Checking out PR #${pr_num} in worktree"
|
|
213
517
|
if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
|
|
214
518
|
log "WARN: Failed to checkout PR #${pr_num}, skipping"
|
|
519
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
520
|
+
FAILED_PR="#${pr_num}"
|
|
521
|
+
FAILED_REASON="checkout_failed"
|
|
215
522
|
EXIT_CODE=1
|
|
216
523
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
217
524
|
continue
|
|
218
525
|
fi
|
|
219
526
|
|
|
220
|
-
QA_PROMPT_PATH=$(
|
|
527
|
+
QA_PROMPT_PATH=$(resolve_instruction_path_with_fallback "${QA_WORKTREE_DIR}" "qa.md" "night-watch-qa.md" || true)
|
|
221
528
|
if [ -z "${QA_PROMPT_PATH}" ]; then
|
|
222
|
-
log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked instructions/, .claude/commands/, and bundled templates/"
|
|
529
|
+
log "FAIL: Missing QA prompt file for PR #${pr_num}. Checked qa.md/night-watch-qa.md in instructions/, .claude/commands/, and bundled templates/"
|
|
530
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
531
|
+
FAILED_PR="#${pr_num}"
|
|
532
|
+
FAILED_REASON="missing_prompt"
|
|
223
533
|
EXIT_CODE=1
|
|
224
534
|
break
|
|
225
535
|
fi
|
|
536
|
+
QA_PROMPT_BUNDLED_NAME="qa.md"
|
|
537
|
+
if [[ "${QA_PROMPT_PATH}" == */night-watch-qa.md ]]; then
|
|
538
|
+
QA_PROMPT_BUNDLED_NAME="night-watch-qa.md"
|
|
539
|
+
fi
|
|
540
|
+
QA_PROMPT_PATH=$(prefer_bundled_prompt_if_legacy_command "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}" "${QA_PROMPT_BUNDLED_NAME}")
|
|
226
541
|
QA_PROMPT=$(cat "${QA_PROMPT_PATH}")
|
|
227
542
|
QA_PROMPT_REF=$(instruction_ref_for_prompt "${QA_WORKTREE_DIR}" "${QA_PROMPT_PATH}")
|
|
228
543
|
log "QA: PR #${pr_num} โ using prompt from ${QA_PROMPT_REF}"
|
|
229
544
|
|
|
545
|
+
# Inject provider attribution requirement into the QA prompt.
|
|
546
|
+
QA_PROVIDER_LABEL="${NW_PROVIDER_LABEL:-${PROVIDER_CMD}}"
|
|
547
|
+
QA_PROMPT="${QA_PROMPT}"$'\n\n'"## QA Attribution (Required)"$'\n'"At the very end of each QA result comment you post, add this footer on its own line:"$'\n'"> ๐งช QA run by ${QA_PROVIDER_LABEL}"
|
|
548
|
+
|
|
549
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
550
|
+
PROVIDER_OK=0
|
|
230
551
|
case "${PROVIDER_CMD}" in
|
|
231
552
|
claude)
|
|
232
553
|
if (
|
|
@@ -235,14 +556,20 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
235
556
|
--dangerously-skip-permissions \
|
|
236
557
|
>> "${LOG_FILE}" 2>&1
|
|
237
558
|
); then
|
|
238
|
-
|
|
559
|
+
PROVIDER_OK=1
|
|
239
560
|
else
|
|
240
561
|
local_exit=$?
|
|
241
562
|
log "QA: PR #${pr_num} โ provider exited with code ${local_exit}"
|
|
242
563
|
if [ ${local_exit} -eq 124 ]; then
|
|
564
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
565
|
+
FAILED_PR="#${pr_num}"
|
|
566
|
+
FAILED_REASON="timeout"
|
|
243
567
|
EXIT_CODE=124
|
|
244
568
|
break
|
|
245
569
|
fi
|
|
570
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
571
|
+
FAILED_PR="#${pr_num}"
|
|
572
|
+
FAILED_REASON="provider_exit_${local_exit}"
|
|
246
573
|
EXIT_CODE=${local_exit}
|
|
247
574
|
fi
|
|
248
575
|
;;
|
|
@@ -254,14 +581,20 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
254
581
|
--prompt "${QA_PROMPT}" \
|
|
255
582
|
>> "${LOG_FILE}" 2>&1
|
|
256
583
|
); then
|
|
257
|
-
|
|
584
|
+
PROVIDER_OK=1
|
|
258
585
|
else
|
|
259
586
|
local_exit=$?
|
|
260
587
|
log "QA: PR #${pr_num} โ provider exited with code ${local_exit}"
|
|
261
588
|
if [ ${local_exit} -eq 124 ]; then
|
|
589
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
590
|
+
FAILED_PR="#${pr_num}"
|
|
591
|
+
FAILED_REASON="timeout"
|
|
262
592
|
EXIT_CODE=124
|
|
263
593
|
break
|
|
264
594
|
fi
|
|
595
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
596
|
+
FAILED_PR="#${pr_num}"
|
|
597
|
+
FAILED_REASON="provider_exit_${local_exit}"
|
|
265
598
|
EXIT_CODE=${local_exit}
|
|
266
599
|
fi
|
|
267
600
|
;;
|
|
@@ -271,39 +604,111 @@ Artifacts: ${QA_ARTIFACTS}"
|
|
|
271
604
|
;;
|
|
272
605
|
esac
|
|
273
606
|
|
|
607
|
+
if [ "${PROVIDER_OK}" -eq 1 ]; then
|
|
608
|
+
if provider_output_looks_invalid "${LOG_LINE_BEFORE}"; then
|
|
609
|
+
log "FAIL-QA-EVIDENCE: PR #${pr_num} provider output indicates an invalid automation run"
|
|
610
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
611
|
+
FAILED_PR="#${pr_num}"
|
|
612
|
+
FAILED_REASON="invalid_provider_output"
|
|
613
|
+
EXIT_CODE=1
|
|
614
|
+
elif ! validate_qa_evidence "${pr_num}"; then
|
|
615
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
616
|
+
FAILED_PR="#${pr_num}"
|
|
617
|
+
FAILED_REASON="qa_evidence_validation_failed"
|
|
618
|
+
EXIT_CODE=1
|
|
619
|
+
else
|
|
620
|
+
QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
|
|
621
|
+
case "${QA_OUTCOME}" in
|
|
622
|
+
passing)
|
|
623
|
+
PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
|
|
624
|
+
;;
|
|
625
|
+
issues_found)
|
|
626
|
+
ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
|
|
627
|
+
;;
|
|
628
|
+
no_tests_needed)
|
|
629
|
+
NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
|
|
630
|
+
;;
|
|
631
|
+
*)
|
|
632
|
+
UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
|
|
633
|
+
;;
|
|
634
|
+
esac
|
|
635
|
+
|
|
636
|
+
PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
|
|
637
|
+
if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
|
|
638
|
+
QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
log "QA: PR #${pr_num} โ provider completed with verifiable QA evidence"
|
|
642
|
+
fi
|
|
643
|
+
fi
|
|
644
|
+
|
|
274
645
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
275
646
|
done
|
|
276
647
|
|
|
277
648
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
278
649
|
|
|
650
|
+
FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
|
|
651
|
+
PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
|
|
652
|
+
ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
|
|
653
|
+
NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
|
|
654
|
+
UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
|
|
655
|
+
FAILED_AUTOMATION_PRS_SUMMARY=$(csv_or_none "${FAILED_AUTOMATION_PRS_CSV}")
|
|
656
|
+
FAILED_PR_SUMMARY=$(csv_or_none "${FAILED_PR}")
|
|
657
|
+
|
|
279
658
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
280
659
|
log "DONE: QA runner completed successfully"
|
|
281
|
-
|
|
282
|
-
|
|
660
|
+
TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
|
|
661
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
662
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
663
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
664
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
665
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
666
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
667
|
+
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
|
|
668
|
+
if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
|
|
669
|
+
TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
|
|
670
|
+
Screenshot links:
|
|
671
|
+
${QA_SCREENSHOT_SUMMARY}"
|
|
672
|
+
fi
|
|
673
|
+
send_telegram_status_message "๐งช Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
|
|
283
674
|
if [ -n "${REPO}" ]; then
|
|
284
|
-
emit_result "success_qa" "prs=${
|
|
675
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
|
|
285
676
|
else
|
|
286
|
-
emit_result "success_qa" "prs=${
|
|
677
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
|
|
287
678
|
fi
|
|
288
679
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
289
680
|
log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
|
|
290
681
|
send_telegram_status_message "๐งช Night Watch QA: timeout" "Project: ${PROJECT_NAME}
|
|
682
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
291
683
|
Timeout: ${MAX_RUNTIME}s
|
|
292
|
-
|
|
684
|
+
Failed PR: ${FAILED_PR_SUMMARY}
|
|
685
|
+
Failure reason: ${FAILED_REASON}
|
|
686
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
687
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
688
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
689
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
690
|
+
Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
293
691
|
if [ -n "${REPO}" ]; then
|
|
294
|
-
emit_result "timeout" "prs=${
|
|
692
|
+
emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
|
|
295
693
|
else
|
|
296
|
-
emit_result "timeout" "prs=${
|
|
694
|
+
emit_result "timeout" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
297
695
|
fi
|
|
298
696
|
else
|
|
299
697
|
log "FAIL: QA runner exited with code ${EXIT_CODE}"
|
|
300
698
|
send_telegram_status_message "๐งช Night Watch QA: failed" "Project: ${PROJECT_NAME}
|
|
699
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
301
700
|
Exit code: ${EXIT_CODE}
|
|
302
|
-
|
|
701
|
+
Failed PR: ${FAILED_PR_SUMMARY}
|
|
702
|
+
Failure reason: ${FAILED_REASON}
|
|
703
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
704
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
705
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
706
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
707
|
+
Failed automation: ${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
303
708
|
if [ -n "${REPO}" ]; then
|
|
304
|
-
emit_result "failure" "prs=${
|
|
709
|
+
emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}|repo=${REPO}"
|
|
305
710
|
else
|
|
306
|
-
emit_result "failure" "prs=${
|
|
711
|
+
emit_result "failure" "prs=${FINAL_PROCESSED_PRS_CSV}|failed_pr=${FAILED_PR_SUMMARY}|reason=${FAILED_REASON}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|failed_automation=${FAILED_AUTOMATION_PRS_SUMMARY}"
|
|
307
712
|
fi
|
|
308
713
|
fi
|
|
309
714
|
|
|
@@ -22,6 +22,7 @@ LOCK_FILE=""
|
|
|
22
22
|
MAX_RUNTIME="${NW_SLICER_MAX_RUNTIME:-600}" # 10 minutes
|
|
23
23
|
MAX_LOG_SIZE="524288" # 512 KB
|
|
24
24
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
25
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
25
26
|
|
|
26
27
|
# Ensure NVM / Node / Night Watch CLI are on PATH
|
|
27
28
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -34,6 +35,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
34
35
|
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
35
36
|
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
36
37
|
LOCK_FILE="/tmp/night-watch-slicer-${PROJECT_RUNTIME_KEY}.lock"
|
|
38
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
37
39
|
|
|
38
40
|
# Validate provider
|
|
39
41
|
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
@@ -55,14 +57,16 @@ trap cleanup_on_exit EXIT
|
|
|
55
57
|
|
|
56
58
|
log "START: Running roadmap slicer for ${PROJECT_DIR}"
|
|
57
59
|
send_telegram_status_message "๐ Night Watch Planner: started" "Project: ${PROJECT_NAME}
|
|
58
|
-
Provider: ${
|
|
59
|
-
|
|
60
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
61
|
+
Roadmap path: ${NW_ROADMAP_PATH:-ROADMAP.md}
|
|
62
|
+
Action: planning next roadmap item into a PRD."
|
|
60
63
|
|
|
61
64
|
# Dry-run mode: print diagnostics and exit
|
|
62
65
|
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
63
66
|
echo "=== Dry Run: Roadmap Slicer ==="
|
|
64
|
-
echo "Provider: ${
|
|
67
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
65
68
|
echo "Project Dir: ${PROJECT_DIR}"
|
|
69
|
+
echo "Roadmap Path: ${NW_ROADMAP_PATH:-ROADMAP.md}"
|
|
66
70
|
echo "Timeout: ${MAX_RUNTIME}s"
|
|
67
71
|
exit 0
|
|
68
72
|
fi
|
|
@@ -71,6 +75,9 @@ fi
|
|
|
71
75
|
CLI_BIN=""
|
|
72
76
|
if ! CLI_BIN=$(resolve_night_watch_cli); then
|
|
73
77
|
log "ERROR: Could not resolve night-watch CLI"
|
|
78
|
+
send_telegram_status_message "๐ Night Watch Planner: failed" "Project: ${PROJECT_NAME}
|
|
79
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
80
|
+
Failure reason: cli_not_found"
|
|
74
81
|
exit 1
|
|
75
82
|
fi
|
|
76
83
|
|
|
@@ -85,14 +92,18 @@ fi
|
|
|
85
92
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
86
93
|
log "DONE: Slicer completed successfully"
|
|
87
94
|
send_telegram_status_message "๐ Night Watch Planner: completed" "Project: ${PROJECT_NAME}
|
|
95
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
88
96
|
PRD planning run finished successfully."
|
|
89
97
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
90
98
|
log "TIMEOUT: Slicer killed after ${MAX_RUNTIME}s"
|
|
91
99
|
send_telegram_status_message "๐ Night Watch Planner: timeout" "Project: ${PROJECT_NAME}
|
|
100
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
92
101
|
Timeout: ${MAX_RUNTIME}s"
|
|
93
102
|
else
|
|
94
103
|
log "FAIL: Slicer exited with code ${EXIT_CODE}"
|
|
95
104
|
send_telegram_status_message "๐ Night Watch Planner: failed" "Project: ${PROJECT_NAME}
|
|
105
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
106
|
+
Failure reason: provider_exit_${EXIT_CODE}
|
|
96
107
|
Exit code: ${EXIT_CODE}"
|
|
97
108
|
fi
|
|
98
109
|
|