@jonit-dev/night-watch-cli 1.7.8 → 1.7.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/dist/board/providers/github-projects.d.ts +9 -1
- package/dist/board/providers/github-projects.d.ts.map +1 -1
- package/dist/board/providers/github-projects.js +199 -60
- package/dist/board/providers/github-projects.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/board.d.ts.map +1 -1
- package/dist/commands/board.js +47 -7
- package/dist/commands/board.js.map +1 -1
- package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
- package/dist/commands/dashboard/tab-config.js +1 -0
- package/dist/commands/dashboard/tab-config.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +131 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +24 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/qa.d.ts +30 -0
- package/dist/commands/qa.d.ts.map +1 -0
- package/dist/commands/qa.js +159 -0
- package/dist/commands/qa.js.map +1 -0
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +10 -0
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +73 -28
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +3 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +155 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +20 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +40 -0
- package/dist/constants.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -2
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/notify.d.ts.map +1 -1
- package/dist/utils/notify.js +18 -0
- package/dist/utils/notify.js.map +1 -1
- package/dist/utils/status-data.d.ts +4 -0
- package/dist/utils/status-data.d.ts.map +1 -1
- package/dist/utils/status-data.js +13 -3
- package/dist/utils/status-data.js.map +1 -1
- package/package.json +1 -1
- package/scripts/night-watch-cron.sh +52 -2
- package/scripts/night-watch-helpers.sh +30 -2
- package/scripts/night-watch-pr-reviewer-cron.sh +70 -0
- package/scripts/night-watch-qa-cron.sh +269 -0
- package/templates/night-watch-qa.md +157 -0
- package/templates/night-watch.config.json +14 -1
- package/web/dist/assets/index-BPW-7_1C.js +380 -0
- package/web/dist/assets/index-DVqjjJEO.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C64sy08d.js +0 -360
- package/web/dist/assets/index-DzoZeo_Y.css +0 -1
|
@@ -74,6 +74,14 @@ ISSUE_BODY="" # board mode: issue body (PRD content)
|
|
|
74
74
|
ISSUE_TITLE_RAW="" # board mode: issue title
|
|
75
75
|
NW_CLI="" # board mode: resolved night-watch CLI binary
|
|
76
76
|
|
|
77
|
+
restore_issue_to_ready() {
|
|
78
|
+
local reason="${1:-Execution failed before implementation started.}"
|
|
79
|
+
if [ -n "${ISSUE_NUMBER}" ] && [ -n "${NW_CLI}" ]; then
|
|
80
|
+
"${NW_CLI}" board move-issue "${ISSUE_NUMBER}" --column "Ready" 2>>"${LOG_FILE}" || true
|
|
81
|
+
"${NW_CLI}" board comment "${ISSUE_NUMBER}" --body "${reason}" 2>>"${LOG_FILE}" || true
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
if [ "${NW_BOARD_ENABLED:-}" = "true" ]; then
|
|
78
86
|
# Board mode: discover next task from GitHub Projects board
|
|
79
87
|
NW_CLI=$(resolve_night_watch_cli 2>/dev/null || true)
|
|
@@ -267,6 +275,7 @@ fi
|
|
|
267
275
|
|
|
268
276
|
if ! prepare_branch_worktree "${PROJECT_DIR}" "${WORKTREE_DIR}" "${BRANCH_NAME}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
269
277
|
log "FAIL: Unable to create isolated worktree ${WORKTREE_DIR} for ${BRANCH_NAME}"
|
|
278
|
+
restore_issue_to_ready "Failed to prepare worktree for branch ${BRANCH_NAME}. Moved back to Ready for retry."
|
|
270
279
|
night_watch_history record "${PROJECT_DIR}" "${ELIGIBLE_PRD}" failure --exit-code 1 2>/dev/null || true
|
|
271
280
|
exit 1
|
|
272
281
|
fi
|
|
@@ -281,9 +290,14 @@ fi
|
|
|
281
290
|
BACKOFF_BASE=300 # 5 minutes in seconds
|
|
282
291
|
EXIT_CODE=0
|
|
283
292
|
ATTEMPT=0
|
|
293
|
+
RATE_LIMIT_FALLBACK_TRIGGERED=0
|
|
284
294
|
|
|
285
295
|
while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
286
296
|
EXIT_CODE=0
|
|
297
|
+
# Capture log position before this attempt so check_rate_limited only
|
|
298
|
+
# scans lines written by the current invocation (not leftover 429s from
|
|
299
|
+
# previous runs that would cause false-positive rate-limit retries).
|
|
300
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
287
301
|
|
|
288
302
|
case "${PROVIDER_CMD}" in
|
|
289
303
|
claude)
|
|
@@ -322,8 +336,14 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
322
336
|
break
|
|
323
337
|
fi
|
|
324
338
|
|
|
325
|
-
# Check if this was a rate limit (429) error
|
|
326
|
-
if check_rate_limited "${LOG_FILE}"; then
|
|
339
|
+
# Check if this was a rate limit (429) error (only in lines from this attempt)
|
|
340
|
+
if check_rate_limited "${LOG_FILE}" "${LOG_LINE_BEFORE}"; then
|
|
341
|
+
# If fallback is enabled, skip proxy retries and switch to native Claude immediately
|
|
342
|
+
if [ "${NW_FALLBACK_ON_RATE_LIMIT:-}" = "true" ] && [ "${PROVIDER_CMD}" = "claude" ]; then
|
|
343
|
+
log "RATE-LIMITED: Proxy quota exhausted — triggering native Claude fallback"
|
|
344
|
+
RATE_LIMIT_FALLBACK_TRIGGERED=1
|
|
345
|
+
break
|
|
346
|
+
fi
|
|
327
347
|
ATTEMPT=$((ATTEMPT + 1))
|
|
328
348
|
if [ "${ATTEMPT}" -ge "${MAX_RETRIES}" ]; then
|
|
329
349
|
log "RATE-LIMITED: All ${MAX_RETRIES} attempts exhausted for ${ELIGIBLE_PRD}"
|
|
@@ -340,6 +360,36 @@ while [ "${ATTEMPT}" -lt "${MAX_RETRIES}" ]; do
|
|
|
340
360
|
fi
|
|
341
361
|
done
|
|
342
362
|
|
|
363
|
+
# ── Native Claude fallback ────────────────────────────────────────────────────
|
|
364
|
+
# When the proxy returns 429 and fallbackOnRateLimit is enabled, re-run the
|
|
365
|
+
# same prompt with native Claude (OAuth), bypassing the proxy entirely.
|
|
366
|
+
if [ "${RATE_LIMIT_FALLBACK_TRIGGERED}" = "1" ]; then
|
|
367
|
+
FALLBACK_MODEL="${NW_CLAUDE_MODEL_ID:-claude-sonnet-4-6}"
|
|
368
|
+
log "RATE-LIMIT-FALLBACK: Running native Claude (${FALLBACK_MODEL})"
|
|
369
|
+
|
|
370
|
+
# Send immediate Telegram warning (fire-and-forget)
|
|
371
|
+
send_rate_limit_fallback_warning "${FALLBACK_MODEL}" "$(basename "${PROJECT_DIR}")"
|
|
372
|
+
|
|
373
|
+
LOG_LINE_BEFORE=$(wc -l < "${LOG_FILE}" 2>/dev/null || echo 0)
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
cd "${WORKTREE_DIR}" && \
|
|
377
|
+
unset ANTHROPIC_BASE_URL ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN \
|
|
378
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL ANTHROPIC_DEFAULT_OPUS_MODEL && \
|
|
379
|
+
timeout "${MAX_RUNTIME}" \
|
|
380
|
+
claude -p "${PROMPT}" \
|
|
381
|
+
--dangerously-skip-permissions \
|
|
382
|
+
--model "${FALLBACK_MODEL}" \
|
|
383
|
+
>> "${LOG_FILE}" 2>&1
|
|
384
|
+
); then
|
|
385
|
+
EXIT_CODE=0
|
|
386
|
+
else
|
|
387
|
+
EXIT_CODE=$?
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
log "RATE-LIMIT-FALLBACK: Native Claude exited with code ${EXIT_CODE}"
|
|
391
|
+
fi
|
|
392
|
+
|
|
343
393
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
344
394
|
OPEN_PR_COUNT=$(count_prs_for_branch open "${BRANCH_NAME}")
|
|
345
395
|
if [ "${OPEN_PR_COUNT}" -gt 0 ]; then
|
|
@@ -406,11 +406,34 @@ mark_prd_done() {
|
|
|
406
406
|
|
|
407
407
|
# ── Rate limit detection ────────────────────────────────────────────────────
|
|
408
408
|
|
|
409
|
-
# Check if the
|
|
409
|
+
# Check if the log contains a 429 rate limit error since a given line number.
|
|
410
|
+
# Usage: check_rate_limited <log_file> [start_line]
|
|
411
|
+
# When start_line is provided, only lines after that position are checked,
|
|
412
|
+
# preventing false positives from 429 errors in previous runs.
|
|
410
413
|
# Returns 0 if rate limited, 1 otherwise.
|
|
411
414
|
check_rate_limited() {
|
|
412
415
|
local log_file="${1:?log_file required}"
|
|
413
|
-
|
|
416
|
+
local start_line="${2:-0}"
|
|
417
|
+
if [ "${start_line}" -gt 0 ] 2>/dev/null; then
|
|
418
|
+
tail -n "+$((start_line + 1))" "${log_file}" 2>/dev/null | grep -q "429"
|
|
419
|
+
else
|
|
420
|
+
tail -20 "${log_file}" 2>/dev/null | grep -q "429"
|
|
421
|
+
fi
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Send an immediate Telegram warning when the rate-limit fallback is triggered.
|
|
425
|
+
# Uses NW_TELEGRAM_BOT_TOKEN and NW_TELEGRAM_CHAT_ID exported by the CLI runner.
|
|
426
|
+
# Falls back silently when credentials are absent.
|
|
427
|
+
# Usage: send_rate_limit_fallback_warning <model> <project_name>
|
|
428
|
+
send_rate_limit_fallback_warning() {
|
|
429
|
+
local model="${1:-native Claude}"
|
|
430
|
+
local project_name="${2:-unknown}"
|
|
431
|
+
if [ -z "${NW_TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${NW_TELEGRAM_CHAT_ID:-}" ]; then
|
|
432
|
+
return 0
|
|
433
|
+
fi
|
|
434
|
+
local msg="⚠️ Rate Limit Fallback%0A%0AProject: ${project_name}%0AProxy quota exhausted — falling back to native Claude (${model})"
|
|
435
|
+
curl -s -X POST "https://api.telegram.org/bot${NW_TELEGRAM_BOT_TOKEN}/sendMessage" \
|
|
436
|
+
-d "chat_id=${NW_TELEGRAM_CHAT_ID}&text=${msg}" > /dev/null 2>&1 || true
|
|
414
437
|
}
|
|
415
438
|
|
|
416
439
|
# ── Board mode issue discovery ────────────────────────────────────────────────
|
|
@@ -429,6 +452,11 @@ find_eligible_board_issue() {
|
|
|
429
452
|
if [ -z "${result}" ]; then
|
|
430
453
|
return 1
|
|
431
454
|
fi
|
|
455
|
+
# Require valid JSON with an issue number to avoid treating plain text output
|
|
456
|
+
# as a runnable board issue.
|
|
457
|
+
if ! printf '%s' "${result}" | jq -e '.number and (.number | type == "number")' >/dev/null 2>&1; then
|
|
458
|
+
return 1
|
|
459
|
+
fi
|
|
432
460
|
printf '%s' "${result}"
|
|
433
461
|
return 0
|
|
434
462
|
}
|
|
@@ -10,6 +10,8 @@ set -euo pipefail
|
|
|
10
10
|
# NW_REVIEWER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
|
|
11
11
|
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
12
|
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
13
|
+
# NW_AUTO_MERGE=0 - Set to 1 to enable auto-merge
|
|
14
|
+
# NW_AUTO_MERGE_METHOD=squash - Merge method: squash, merge, or rebase
|
|
13
15
|
|
|
14
16
|
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
15
17
|
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
@@ -20,6 +22,8 @@ MAX_LOG_SIZE="524288" # 512 KB
|
|
|
20
22
|
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
21
23
|
MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
|
|
22
24
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
25
|
+
AUTO_MERGE="${NW_AUTO_MERGE:-0}"
|
|
26
|
+
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
23
27
|
|
|
24
28
|
# Ensure NVM / Node / Claude are on PATH
|
|
25
29
|
export NVM_DIR="${HOME}/.nvm"
|
|
@@ -156,6 +160,10 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
156
160
|
echo "Provider: ${PROVIDER_CMD}"
|
|
157
161
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
158
162
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
163
|
+
echo "Auto-merge: ${AUTO_MERGE}"
|
|
164
|
+
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
165
|
+
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
166
|
+
fi
|
|
159
167
|
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
160
168
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
161
169
|
echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
|
|
@@ -204,6 +212,68 @@ esac
|
|
|
204
212
|
|
|
205
213
|
cleanup_worktrees "${PROJECT_DIR}"
|
|
206
214
|
|
|
215
|
+
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
216
|
+
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
217
|
+
# for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
|
|
218
|
+
AUTO_MERGED_PRS=""
|
|
219
|
+
AUTO_MERGE_FAILED_PRS=""
|
|
220
|
+
|
|
221
|
+
if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
|
|
222
|
+
log "AUTO-MERGE: Checking for merge-ready PRs..."
|
|
223
|
+
|
|
224
|
+
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
225
|
+
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
226
|
+
continue
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Only process PRs matching branch patterns
|
|
230
|
+
if ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# Check CI status - must have no failures
|
|
235
|
+
FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
|
|
236
|
+
if [ "${FAILED_CHECKS}" -gt 0 ]; then
|
|
237
|
+
continue
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Check review score - must have score >= threshold
|
|
241
|
+
ALL_COMMENTS=$(
|
|
242
|
+
{
|
|
243
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
244
|
+
if [ -n "${REPO}" ]; then
|
|
245
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
246
|
+
fi
|
|
247
|
+
} | sort -u
|
|
248
|
+
)
|
|
249
|
+
LATEST_SCORE=$(echo "${ALL_COMMENTS}" \
|
|
250
|
+
| grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
|
|
251
|
+
| tail -1 \
|
|
252
|
+
| grep -oP '\d+(?=/100)' || echo "")
|
|
253
|
+
|
|
254
|
+
# Skip PRs without a score
|
|
255
|
+
if [ -z "${LATEST_SCORE}" ]; then
|
|
256
|
+
continue
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# Skip PRs with score below threshold
|
|
260
|
+
if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
261
|
+
continue
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# PR is merge-ready - queue for auto-merge
|
|
265
|
+
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
|
|
266
|
+
|
|
267
|
+
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
268
|
+
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
269
|
+
AUTO_MERGED_PRS="${AUTO_MERGED_PRS} #${pr_number}"
|
|
270
|
+
else
|
|
271
|
+
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
272
|
+
AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS} #${pr_number}"
|
|
273
|
+
fi
|
|
274
|
+
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
275
|
+
fi
|
|
276
|
+
|
|
207
277
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
208
278
|
log "DONE: PR reviewer completed successfully"
|
|
209
279
|
emit_result "success_reviewed" "prs=${PRS_NEEDING_WORK_CSV}"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch QA Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-qa-cron.sh /path/to/project
|
|
6
|
+
#
|
|
7
|
+
# NOTE: This script expects environment variables to be set by the caller.
|
|
8
|
+
# The Node.js CLI will inject config values via environment variables.
|
|
9
|
+
# Required env vars (with defaults shown):
|
|
10
|
+
# NW_QA_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
|
|
11
|
+
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
|
+
# NW_BRANCH_PATTERNS=feat/,night-watch/ - Comma-separated branch prefixes to match
|
|
13
|
+
# NW_QA_SKIP_LABEL=skip-qa - Label to skip QA on a PR
|
|
14
|
+
# NW_QA_ARTIFACTS=both - Artifact mode (both, tests, report)
|
|
15
|
+
# NW_QA_AUTO_INSTALL_PLAYWRIGHT=1 - Auto-install Playwright browsers
|
|
16
|
+
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
17
|
+
|
|
18
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
19
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
20
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
21
|
+
LOG_FILE="${LOG_DIR}/night-watch-qa.log"
|
|
22
|
+
MAX_RUNTIME="${NW_QA_MAX_RUNTIME:-3600}" # 1 hour
|
|
23
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
24
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
25
|
+
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
26
|
+
SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
|
|
27
|
+
QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
|
|
28
|
+
QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
|
|
29
|
+
|
|
30
|
+
# Ensure NVM / Node / Claude are on PATH
|
|
31
|
+
export NVM_DIR="${HOME}/.nvm"
|
|
32
|
+
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
|
|
33
|
+
|
|
34
|
+
# NOTE: Environment variables should be set by the caller (Node.js CLI).
|
|
35
|
+
# The .env.night-watch sourcing has been removed - config is now injected via env vars.
|
|
36
|
+
|
|
37
|
+
mkdir -p "${LOG_DIR}"
|
|
38
|
+
|
|
39
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
40
|
+
# shellcheck source=night-watch-helpers.sh
|
|
41
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
42
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
43
|
+
# NOTE: Lock file path must match qaLockPath() in src/utils/status-data.ts
|
|
44
|
+
LOCK_FILE="/tmp/night-watch-qa-${PROJECT_RUNTIME_KEY}.lock"
|
|
45
|
+
|
|
46
|
+
emit_result() {
|
|
47
|
+
local status="${1:?status required}"
|
|
48
|
+
local details="${2:-}"
|
|
49
|
+
if [ -n "${details}" ]; then
|
|
50
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
51
|
+
else
|
|
52
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Validate provider
|
|
57
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
58
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
rotate_log
|
|
63
|
+
|
|
64
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
65
|
+
emit_result "skip_locked"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
cd "${PROJECT_DIR}"
|
|
70
|
+
|
|
71
|
+
# Convert comma-separated branch prefixes into a regex that matches branch starts.
|
|
72
|
+
BRANCH_REGEX=""
|
|
73
|
+
IFS=',' read -r -a BRANCH_PATTERNS <<< "${BRANCH_PATTERNS_RAW}"
|
|
74
|
+
for pattern in "${BRANCH_PATTERNS[@]}"; do
|
|
75
|
+
trimmed_pattern=$(printf '%s' "${pattern}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
76
|
+
if [ -n "${trimmed_pattern}" ]; then
|
|
77
|
+
BRANCH_REGEX="${BRANCH_REGEX}${BRANCH_REGEX:+|}^${trimmed_pattern}"
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
if [ -z "${BRANCH_REGEX}" ]; then
|
|
82
|
+
BRANCH_REGEX='^(feat/|night-watch/)'
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# List open PRs with their details for filtering
|
|
86
|
+
PR_JSON=$(gh pr list --state open --json number,headRefName,title,labels 2>/dev/null || echo "[]")
|
|
87
|
+
|
|
88
|
+
# Count PRs matching branch patterns
|
|
89
|
+
OPEN_PRS=$(
|
|
90
|
+
echo "${PR_JSON}" \
|
|
91
|
+
| jq -r '.[].headRefName' 2>/dev/null \
|
|
92
|
+
| { grep -E "${BRANCH_REGEX}" || true; } \
|
|
93
|
+
| wc -l \
|
|
94
|
+
| tr -d '[:space:]'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if [ "${OPEN_PRS}" -eq 0 ]; then
|
|
98
|
+
log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
|
|
99
|
+
emit_result "skip_no_open_prs"
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
104
|
+
|
|
105
|
+
# Collect PRs that need QA
|
|
106
|
+
PRS_NEEDING_QA=""
|
|
107
|
+
QA_NEEDED=0
|
|
108
|
+
|
|
109
|
+
while IFS=$'\t' read -r pr_number pr_branch pr_title pr_labels; do
|
|
110
|
+
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
111
|
+
continue
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Filter by branch pattern
|
|
115
|
+
if ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
116
|
+
continue
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Skip PRs with the skip label
|
|
120
|
+
if echo "${pr_labels}" | grep -q "${SKIP_LABEL}"; then
|
|
121
|
+
log "SKIP-QA: PR #${pr_number} (${pr_branch}) has '${SKIP_LABEL}' label"
|
|
122
|
+
continue
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Skip PRs with [skip-qa] in their title
|
|
126
|
+
if echo "${pr_title}" | grep -qi '\[skip-qa\]'; then
|
|
127
|
+
log "SKIP-QA: PR #${pr_number} (${pr_branch}) has [skip-qa] in title"
|
|
128
|
+
continue
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Skip PRs that already have a QA comment (idempotency)
|
|
132
|
+
ALL_COMMENTS=$(
|
|
133
|
+
{
|
|
134
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
135
|
+
if [ -n "${REPO}" ]; then
|
|
136
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
137
|
+
fi
|
|
138
|
+
} | sort -u
|
|
139
|
+
)
|
|
140
|
+
if echo "${ALL_COMMENTS}" | grep -q '<!-- night-watch-qa-marker -->'; then
|
|
141
|
+
log "SKIP-QA: PR #${pr_number} (${pr_branch}) already has QA comment"
|
|
142
|
+
continue
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
QA_NEEDED=1
|
|
146
|
+
PRS_NEEDING_QA="${PRS_NEEDING_QA} #${pr_number}"
|
|
147
|
+
done < <(
|
|
148
|
+
echo "${PR_JSON}" \
|
|
149
|
+
| jq -r '.[] | [.number, .headRefName, .title, ([.labels[].name] | join(","))] | @tsv' 2>/dev/null || true
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if [ "${QA_NEEDED}" -eq 0 ]; then
|
|
153
|
+
log "SKIP: All ${OPEN_PRS} open PR(s) matching patterns already have QA comments"
|
|
154
|
+
emit_result "skip_all_qa_done"
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
PRS_NEEDING_QA=$(echo "${PRS_NEEDING_QA}" \
|
|
159
|
+
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]][[:space:]]*/ /g' -e 's/[[:space:]]*$//')
|
|
160
|
+
PRS_NEEDING_QA_CSV="${PRS_NEEDING_QA// /,}"
|
|
161
|
+
|
|
162
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
163
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
164
|
+
else
|
|
165
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
166
|
+
fi
|
|
167
|
+
QA_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${PROJECT_NAME}-nw-qa-runner"
|
|
168
|
+
|
|
169
|
+
log "START: Found PR(s) needing QA:${PRS_NEEDING_QA}"
|
|
170
|
+
|
|
171
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
172
|
+
|
|
173
|
+
# Dry-run mode: print diagnostics and exit
|
|
174
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
175
|
+
echo "=== Dry Run: QA Runner ==="
|
|
176
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
177
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
178
|
+
echo "Skip Label: ${SKIP_LABEL}"
|
|
179
|
+
echo "QA Artifacts: ${QA_ARTIFACTS}"
|
|
180
|
+
echo "Auto-install Playwright: ${QA_AUTO_INSTALL_PLAYWRIGHT}"
|
|
181
|
+
echo "Open PRs needing QA:${PRS_NEEDING_QA}"
|
|
182
|
+
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
183
|
+
echo "QA Worktree: ${QA_WORKTREE_DIR}"
|
|
184
|
+
echo "Timeout: ${MAX_RUNTIME}s"
|
|
185
|
+
exit 0
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
EXIT_CODE=0
|
|
189
|
+
|
|
190
|
+
# Process each PR that needs QA
|
|
191
|
+
for pr_ref in ${PRS_NEEDING_QA}; do
|
|
192
|
+
pr_num="${pr_ref#\#}"
|
|
193
|
+
|
|
194
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
195
|
+
if ! prepare_detached_worktree "${PROJECT_DIR}" "${QA_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
196
|
+
log "FAIL: Unable to create isolated QA worktree ${QA_WORKTREE_DIR} for PR #${pr_num}"
|
|
197
|
+
EXIT_CODE=1
|
|
198
|
+
break
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
log "QA: Checking out PR #${pr_num} in worktree"
|
|
202
|
+
if ! (cd "${QA_WORKTREE_DIR}" && gh pr checkout "${pr_num}" >> "${LOG_FILE}" 2>&1); then
|
|
203
|
+
log "WARN: Failed to checkout PR #${pr_num}, skipping"
|
|
204
|
+
EXIT_CODE=1
|
|
205
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
206
|
+
continue
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
case "${PROVIDER_CMD}" in
|
|
210
|
+
claude)
|
|
211
|
+
if (
|
|
212
|
+
cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
213
|
+
claude -p "/night-watch-qa" \
|
|
214
|
+
--dangerously-skip-permissions \
|
|
215
|
+
>> "${LOG_FILE}" 2>&1
|
|
216
|
+
); then
|
|
217
|
+
log "QA: PR #${pr_num} — provider completed successfully"
|
|
218
|
+
else
|
|
219
|
+
local_exit=$?
|
|
220
|
+
log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
|
|
221
|
+
if [ ${local_exit} -eq 124 ]; then
|
|
222
|
+
EXIT_CODE=124
|
|
223
|
+
break
|
|
224
|
+
fi
|
|
225
|
+
EXIT_CODE=${local_exit}
|
|
226
|
+
fi
|
|
227
|
+
;;
|
|
228
|
+
codex)
|
|
229
|
+
if (
|
|
230
|
+
cd "${QA_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
231
|
+
codex --quiet \
|
|
232
|
+
--yolo \
|
|
233
|
+
--prompt "$(cat "${QA_WORKTREE_DIR}/.claude/commands/night-watch-qa.md")" \
|
|
234
|
+
>> "${LOG_FILE}" 2>&1
|
|
235
|
+
); then
|
|
236
|
+
log "QA: PR #${pr_num} — provider completed successfully"
|
|
237
|
+
else
|
|
238
|
+
local_exit=$?
|
|
239
|
+
log "QA: PR #${pr_num} — provider exited with code ${local_exit}"
|
|
240
|
+
if [ ${local_exit} -eq 124 ]; then
|
|
241
|
+
EXIT_CODE=124
|
|
242
|
+
break
|
|
243
|
+
fi
|
|
244
|
+
EXIT_CODE=${local_exit}
|
|
245
|
+
fi
|
|
246
|
+
;;
|
|
247
|
+
*)
|
|
248
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
249
|
+
exit 1
|
|
250
|
+
;;
|
|
251
|
+
esac
|
|
252
|
+
|
|
253
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
254
|
+
done
|
|
255
|
+
|
|
256
|
+
cleanup_worktrees "${PROJECT_DIR}"
|
|
257
|
+
|
|
258
|
+
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
259
|
+
log "DONE: QA runner completed successfully"
|
|
260
|
+
emit_result "success_qa" "prs=${PRS_NEEDING_QA_CSV}"
|
|
261
|
+
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
262
|
+
log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
|
|
263
|
+
emit_result "timeout" "prs=${PRS_NEEDING_QA_CSV}"
|
|
264
|
+
else
|
|
265
|
+
log "FAIL: QA runner exited with code ${EXIT_CODE}"
|
|
266
|
+
emit_result "failure" "prs=${PRS_NEEDING_QA_CSV}"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
exit "${EXIT_CODE}"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
You are the Night Watch QA agent. Your job is to analyze open PRs, generate appropriate tests for the changes, run them, and report results with visual evidence.
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
You are running inside a worktree checked out to a PR branch. Your goal is to:
|
|
6
|
+
1. Analyze what changed in this PR compared to the base branch
|
|
7
|
+
2. Determine if the changes are UI-related, API-related, or both
|
|
8
|
+
3. Generate appropriate tests (Playwright e2e for UI, integration tests for API)
|
|
9
|
+
4. Run the tests and capture artifacts (screenshots, videos for UI)
|
|
10
|
+
5. Commit the tests and artifacts, then comment on the PR with results
|
|
11
|
+
|
|
12
|
+
## Environment Variables Available
|
|
13
|
+
- `NW_QA_ARTIFACTS` — What to capture: "screenshot", "video", or "both" (default: "both")
|
|
14
|
+
- `NW_QA_AUTO_INSTALL_PLAYWRIGHT` — "1" to auto-install Playwright if missing
|
|
15
|
+
|
|
16
|
+
## Instructions
|
|
17
|
+
|
|
18
|
+
### Step 1: Analyze the PR diff
|
|
19
|
+
|
|
20
|
+
Get the diff against the base branch:
|
|
21
|
+
```
|
|
22
|
+
git diff origin/${DEFAULT_BRANCH}...HEAD --name-only
|
|
23
|
+
git diff origin/${DEFAULT_BRANCH}...HEAD --stat
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Read the changed files to understand what the PR introduces.
|
|
27
|
+
|
|
28
|
+
### Step 2: Classify and Decide
|
|
29
|
+
|
|
30
|
+
Based on the diff, determine:
|
|
31
|
+
- **UI changes**: New/modified components, pages, layouts, styles, client-side logic
|
|
32
|
+
- **API changes**: New/modified endpoints, controllers, services, middleware, database queries
|
|
33
|
+
- **Both**: PR touches both UI and API code
|
|
34
|
+
- **No tests needed**: Trivial changes (docs, config, comments only) — in this case, post a comment saying "QA: No tests needed for this PR" and stop
|
|
35
|
+
|
|
36
|
+
### Step 3: Prepare Test Infrastructure
|
|
37
|
+
|
|
38
|
+
**For UI tests (Playwright):**
|
|
39
|
+
1. Check if Playwright is available: `npx playwright --version`
|
|
40
|
+
2. If not available and `NW_QA_AUTO_INSTALL_PLAYWRIGHT=1`:
|
|
41
|
+
- Run `npm install -D @playwright/test` (or yarn/pnpm equivalent based on lockfile)
|
|
42
|
+
- Run `npx playwright install chromium`
|
|
43
|
+
3. If not available and auto-install is disabled, skip UI tests and note in the report
|
|
44
|
+
|
|
45
|
+
**For API tests:**
|
|
46
|
+
- Use the project's existing test framework (vitest, jest, or mocha — detect from package.json)
|
|
47
|
+
- If no test framework exists, use vitest
|
|
48
|
+
|
|
49
|
+
### Step 4: Generate Tests
|
|
50
|
+
|
|
51
|
+
**UI Tests (Playwright):**
|
|
52
|
+
- Create test files in `tests/e2e/qa/` (or the project's existing e2e directory)
|
|
53
|
+
- Test the specific feature/page changed in the PR
|
|
54
|
+
- Configure Playwright for artifacts based on `NW_QA_ARTIFACTS`:
|
|
55
|
+
- `"screenshot"`: `screenshot: 'on'` only
|
|
56
|
+
- `"video"`: `video: { mode: 'on', size: { width: 1280, height: 720 } }` only
|
|
57
|
+
- `"both"`: Both screenshot and video enabled
|
|
58
|
+
- Name test files with a `qa-` prefix: `qa-<feature-name>.spec.ts`
|
|
59
|
+
- Include at minimum: navigation to the feature, interaction with key elements, visual assertions
|
|
60
|
+
|
|
61
|
+
**API Tests:**
|
|
62
|
+
- Create test files in `tests/integration/qa/` (or the project's existing test directory)
|
|
63
|
+
- Test the specific endpoints changed in the PR
|
|
64
|
+
- Include: happy path, error cases, validation checks
|
|
65
|
+
- Name test files with a `qa-` prefix: `qa-<endpoint-name>.test.ts`
|
|
66
|
+
|
|
67
|
+
### Step 5: Run Tests
|
|
68
|
+
|
|
69
|
+
**UI Tests:**
|
|
70
|
+
```bash
|
|
71
|
+
npx playwright test tests/e2e/qa/ --reporter=list
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**API Tests:**
|
|
75
|
+
```bash
|
|
76
|
+
npx vitest run tests/integration/qa/ --reporter=verbose
|
|
77
|
+
# (or equivalent for the project's test runner)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Capture the test output for the report.
|
|
81
|
+
|
|
82
|
+
### Step 6: Collect Artifacts
|
|
83
|
+
|
|
84
|
+
Move Playwright artifacts (screenshots, videos) to `qa-artifacts/` in the project root:
|
|
85
|
+
```bash
|
|
86
|
+
mkdir -p qa-artifacts
|
|
87
|
+
# Copy from playwright-report/ or test-results/ to qa-artifacts/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Step 7: Commit and Push
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git add tests/e2e/qa/ tests/integration/qa/ qa-artifacts/ || true
|
|
94
|
+
git add -A tests/*/qa/ qa-artifacts/ || true
|
|
95
|
+
git commit -m "test(qa): add automated QA tests for PR changes
|
|
96
|
+
|
|
97
|
+
- Generated by Night Watch QA agent
|
|
98
|
+
- <UI tests: X passing, Y failing | No UI tests>
|
|
99
|
+
- <API tests: X passing, Y failing | No API tests>
|
|
100
|
+
- Artifacts: <screenshots, videos | screenshots | videos | none>
|
|
101
|
+
|
|
102
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"
|
|
103
|
+
git push origin HEAD
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 8: Comment on PR
|
|
107
|
+
|
|
108
|
+
Post a comment on the PR with results. Use the `<!-- night-watch-qa-marker -->` HTML comment for idempotency detection.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
gh pr comment <PR_NUMBER> --body "<!-- night-watch-qa-marker -->
|
|
112
|
+
## Night Watch QA Report
|
|
113
|
+
|
|
114
|
+
### Changes Classification
|
|
115
|
+
- **Type**: <UI | API | UI + API>
|
|
116
|
+
- **Files changed**: <count>
|
|
117
|
+
|
|
118
|
+
### Test Results
|
|
119
|
+
|
|
120
|
+
<If UI tests>
|
|
121
|
+
#### UI Tests (Playwright)
|
|
122
|
+
- **Status**: <All passing | X of Y failing>
|
|
123
|
+
- **Tests**: <count> test(s) in <count> file(s)
|
|
124
|
+
|
|
125
|
+
<If screenshots captured>
|
|
126
|
+
#### Screenshots
|
|
127
|
+
<For each screenshot>
|
|
128
|
+

|
|
129
|
+
</For>
|
|
130
|
+
</If>
|
|
131
|
+
|
|
132
|
+
<If video captured>
|
|
133
|
+
#### Video Recording
|
|
134
|
+
Video artifact committed to \`qa-artifacts/\` — view in the PR's file changes.
|
|
135
|
+
</If>
|
|
136
|
+
</If>
|
|
137
|
+
|
|
138
|
+
<If API tests>
|
|
139
|
+
#### API Tests
|
|
140
|
+
- **Status**: <All passing | X of Y failing>
|
|
141
|
+
- **Tests**: <count> test(s) in <count> file(s)
|
|
142
|
+
</If>
|
|
143
|
+
|
|
144
|
+
<If no tests generated>
|
|
145
|
+
**QA: No tests needed for this PR** — changes are trivial (docs, config, comments).
|
|
146
|
+
</If>
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
*Night Watch QA Agent*"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Important Rules
|
|
153
|
+
- Process each PR **once** per run. Do NOT loop or retry after pushing.
|
|
154
|
+
- Do NOT modify existing project tests — only add new files in `qa/` subdirectories.
|
|
155
|
+
- If tests fail, still commit and report — the failures are useful information.
|
|
156
|
+
- Keep test files self-contained and independent from each other.
|
|
157
|
+
- Follow the project's existing code style and conventions (check CLAUDE.md, package.json scripts, tsconfig).
|
|
@@ -13,5 +13,18 @@
|
|
|
13
13
|
"minReviewScore": 80,
|
|
14
14
|
"maxLogSize": 524288,
|
|
15
15
|
"cronSchedule": "0 0-21 * * *",
|
|
16
|
-
"reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *"
|
|
16
|
+
"reviewerSchedule": "0 0,3,6,9,12,15,18,21 * * *",
|
|
17
|
+
"autoMerge": false,
|
|
18
|
+
"autoMergeMethod": "squash",
|
|
19
|
+
"fallbackOnRateLimit": false,
|
|
20
|
+
"claudeModel": "sonnet",
|
|
21
|
+
"qa": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"schedule": "30 1,7,13,19 * * *",
|
|
24
|
+
"maxRuntime": 3600,
|
|
25
|
+
"branchPatterns": [],
|
|
26
|
+
"artifacts": "both",
|
|
27
|
+
"skipLabel": "skip-qa",
|
|
28
|
+
"autoInstallPlaywright": true
|
|
29
|
+
}
|
|
17
30
|
}
|