@jonit-dev/night-watch-cli 1.7.34 → 1.7.36
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 +6 -0
- package/dist/scripts/night-watch-audit-cron.sh +168 -0
- package/dist/scripts/night-watch-cron.sh +484 -0
- package/dist/scripts/night-watch-helpers.sh +515 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +528 -0
- package/dist/scripts/night-watch-qa-cron.sh +281 -0
- package/dist/scripts/night-watch-slicer-cron.sh +90 -0
- package/dist/scripts/test-helpers.bats +77 -0
- package/dist/web/assets/index-BiJf9LFT.js +458 -0
- package/dist/web/assets/index-OpSgvsYu.css +1 -0
- package/dist/web/avatars/carlos.webp +0 -0
- package/dist/web/avatars/dev.webp +0 -0
- package/dist/web/avatars/maya.webp +0 -0
- package/dist/web/avatars/priya.webp +0 -0
- package/dist/web/index.html +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch PR Reviewer Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-pr-reviewer-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_REVIEWER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
|
|
11
|
+
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
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
|
|
15
|
+
|
|
16
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
17
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
18
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
19
|
+
LOG_FILE="${LOG_DIR}/reviewer.log"
|
|
20
|
+
MAX_RUNTIME="${NW_REVIEWER_MAX_RUNTIME:-3600}" # 1 hour
|
|
21
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
22
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
23
|
+
MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
|
|
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}"
|
|
27
|
+
TARGET_PR="${NW_TARGET_PR:-}"
|
|
28
|
+
PARALLEL_ENABLED="${NW_REVIEWER_PARALLEL:-1}"
|
|
29
|
+
WORKER_MODE="${NW_REVIEWER_WORKER_MODE:-0}"
|
|
30
|
+
|
|
31
|
+
# Ensure NVM / Node / Claude are on PATH
|
|
32
|
+
export NVM_DIR="${HOME}/.nvm"
|
|
33
|
+
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
|
|
34
|
+
|
|
35
|
+
# NOTE: Environment variables should be set by the caller (Node.js CLI).
|
|
36
|
+
# The .env.night-watch sourcing has been removed - config is now injected via env vars.
|
|
37
|
+
|
|
38
|
+
mkdir -p "${LOG_DIR}"
|
|
39
|
+
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
41
|
+
# shellcheck source=night-watch-helpers.sh
|
|
42
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
43
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
44
|
+
GLOBAL_LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}.lock"
|
|
45
|
+
if [ "${WORKER_MODE}" = "1" ] && [ -n "${TARGET_PR}" ]; then
|
|
46
|
+
LOCK_FILE="/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}-pr-${TARGET_PR}.lock"
|
|
47
|
+
else
|
|
48
|
+
# NOTE: Lock file path must match reviewerLockPath() in src/utils/status-data.ts
|
|
49
|
+
LOCK_FILE="${GLOBAL_LOCK_FILE}"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
emit_result() {
|
|
53
|
+
local status="${1:?status required}"
|
|
54
|
+
local details="${2:-}"
|
|
55
|
+
if [ -n "${details}" ]; then
|
|
56
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
57
|
+
else
|
|
58
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
emit_final_status() {
|
|
63
|
+
local exit_code="${1:?exit code required}"
|
|
64
|
+
local prs_csv="${2:-}"
|
|
65
|
+
local auto_merged="${3:-}"
|
|
66
|
+
local auto_merge_failed="${4:-}"
|
|
67
|
+
|
|
68
|
+
if [ "${exit_code}" -eq 0 ]; then
|
|
69
|
+
log "DONE: PR reviewer completed successfully"
|
|
70
|
+
emit_result "success_reviewed" "prs=${prs_csv}|auto_merged=${auto_merged}|auto_merge_failed=${auto_merge_failed}"
|
|
71
|
+
elif [ "${exit_code}" -eq 124 ]; then
|
|
72
|
+
log "TIMEOUT: PR reviewer killed after ${MAX_RUNTIME}s"
|
|
73
|
+
emit_result "timeout" "prs=${prs_csv}"
|
|
74
|
+
else
|
|
75
|
+
log "FAIL: PR reviewer exited with code ${exit_code}"
|
|
76
|
+
emit_result "failure" "prs=${prs_csv}"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
append_csv() {
|
|
81
|
+
local current="${1:-}"
|
|
82
|
+
local incoming="${2:-}"
|
|
83
|
+
if [ -z "${incoming}" ]; then
|
|
84
|
+
printf "%s" "${current}"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
if [ -z "${current}" ]; then
|
|
88
|
+
printf "%s" "${incoming}"
|
|
89
|
+
else
|
|
90
|
+
printf "%s,%s" "${current}" "${incoming}"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Validate provider
|
|
95
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
96
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
rotate_log
|
|
101
|
+
|
|
102
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
103
|
+
emit_result "skip_locked"
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
cd "${PROJECT_DIR}"
|
|
108
|
+
|
|
109
|
+
# Convert comma-separated branch prefixes into a regex that matches branch starts.
|
|
110
|
+
BRANCH_REGEX=""
|
|
111
|
+
IFS=',' read -r -a BRANCH_PATTERNS <<< "${BRANCH_PATTERNS_RAW}"
|
|
112
|
+
for pattern in "${BRANCH_PATTERNS[@]}"; do
|
|
113
|
+
trimmed_pattern=$(printf '%s' "${pattern}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
114
|
+
if [ -n "${trimmed_pattern}" ]; then
|
|
115
|
+
BRANCH_REGEX="${BRANCH_REGEX}${BRANCH_REGEX:+|}^${trimmed_pattern}"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
if [ -z "${BRANCH_REGEX}" ]; then
|
|
120
|
+
BRANCH_REGEX='^(feat/|night-watch/)'
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
124
|
+
OPEN_PRS=$(
|
|
125
|
+
if gh pr view "${TARGET_PR}" --json number >/dev/null 2>&1; then
|
|
126
|
+
echo "1"
|
|
127
|
+
else
|
|
128
|
+
echo "0"
|
|
129
|
+
fi
|
|
130
|
+
)
|
|
131
|
+
else
|
|
132
|
+
OPEN_PRS=$(
|
|
133
|
+
{ gh pr list --state open --json headRefName --jq '.[].headRefName' 2>/dev/null || true; } \
|
|
134
|
+
| { grep -E "${BRANCH_REGEX}" || true; } \
|
|
135
|
+
| wc -l \
|
|
136
|
+
| tr -d '[:space:]'
|
|
137
|
+
)
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
if [ "${OPEN_PRS}" -eq 0 ]; then
|
|
141
|
+
log "SKIP: No open PRs matching branch patterns (${BRANCH_PATTERNS_RAW})"
|
|
142
|
+
emit_result "skip_no_open_prs"
|
|
143
|
+
exit 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
NEEDS_WORK=0
|
|
147
|
+
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
148
|
+
PRS_NEEDING_WORK=""
|
|
149
|
+
|
|
150
|
+
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
151
|
+
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
152
|
+
continue
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
|
|
156
|
+
continue
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
160
|
+
continue
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Merge-conflict signal: this PR needs action even if CI and score look fine.
|
|
164
|
+
MERGE_STATE=$(gh pr view "${pr_number}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "")
|
|
165
|
+
if [ "${MERGE_STATE}" = "DIRTY" ] || [ "${MERGE_STATE}" = "CONFLICTING" ]; then
|
|
166
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has merge conflicts (${MERGE_STATE})"
|
|
167
|
+
NEEDS_WORK=1
|
|
168
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
169
|
+
continue
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
|
|
173
|
+
if [ "${FAILED_CHECKS}" -gt 0 ]; then
|
|
174
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has ${FAILED_CHECKS} failed CI check(s)"
|
|
175
|
+
NEEDS_WORK=1
|
|
176
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
177
|
+
continue
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
ALL_COMMENTS=$(
|
|
181
|
+
{
|
|
182
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
183
|
+
if [ -n "${REPO}" ]; then
|
|
184
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
185
|
+
fi
|
|
186
|
+
} | sort -u
|
|
187
|
+
)
|
|
188
|
+
LATEST_SCORE=$(echo "${ALL_COMMENTS}" \
|
|
189
|
+
| grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
|
|
190
|
+
| tail -1 \
|
|
191
|
+
| grep -oP '\d+(?=/100)' || echo "")
|
|
192
|
+
if [ -n "${LATEST_SCORE}" ] && [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
193
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has review score ${LATEST_SCORE}/100 (threshold: ${MIN_REVIEW_SCORE})"
|
|
194
|
+
NEEDS_WORK=1
|
|
195
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
196
|
+
fi
|
|
197
|
+
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
198
|
+
|
|
199
|
+
if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
200
|
+
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE} (or no score yet)"
|
|
201
|
+
|
|
202
|
+
# ── Auto-merge eligible PRs ───────────────────────────────
|
|
203
|
+
if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
|
|
204
|
+
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
205
|
+
AUTO_MERGED_COUNT=0
|
|
206
|
+
|
|
207
|
+
log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
|
|
208
|
+
|
|
209
|
+
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
210
|
+
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
211
|
+
printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
|
|
212
|
+
|
|
213
|
+
# Check CI status
|
|
214
|
+
FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
|
|
215
|
+
[ "${FAILED_CHECKS}" -gt 0 ] && continue
|
|
216
|
+
|
|
217
|
+
# Check review score
|
|
218
|
+
PR_COMMENTS=$(
|
|
219
|
+
{
|
|
220
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
221
|
+
if [ -n "${REPO}" ]; then
|
|
222
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
223
|
+
fi
|
|
224
|
+
} | sort -u
|
|
225
|
+
)
|
|
226
|
+
PR_SCORE=$(echo "${PR_COMMENTS}" \
|
|
227
|
+
| grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
|
|
228
|
+
| tail -1 \
|
|
229
|
+
| grep -oP '\d+(?=/100)' || echo "")
|
|
230
|
+
|
|
231
|
+
# Skip PRs without a score or with score below threshold
|
|
232
|
+
[ -z "${PR_SCORE}" ] && continue
|
|
233
|
+
[ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
|
|
234
|
+
|
|
235
|
+
# PR is merge-ready
|
|
236
|
+
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
|
|
237
|
+
|
|
238
|
+
# Dry-run mode: show what would be merged
|
|
239
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
240
|
+
log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
|
|
241
|
+
continue
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
245
|
+
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
246
|
+
AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
|
|
247
|
+
else
|
|
248
|
+
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
249
|
+
fi
|
|
250
|
+
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
251
|
+
|
|
252
|
+
if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
|
|
253
|
+
log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
emit_result "skip_all_passing"
|
|
258
|
+
exit 0
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
PRS_NEEDING_WORK=$(echo "${PRS_NEEDING_WORK}" \
|
|
262
|
+
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]][[:space:]]*/ /g' -e 's/[[:space:]]*$//')
|
|
263
|
+
PRS_NEEDING_WORK_CSV="${PRS_NEEDING_WORK// /,}"
|
|
264
|
+
|
|
265
|
+
if [ -n "${NW_DEFAULT_BRANCH:-}" ]; then
|
|
266
|
+
DEFAULT_BRANCH="${NW_DEFAULT_BRANCH}"
|
|
267
|
+
else
|
|
268
|
+
DEFAULT_BRANCH=$(detect_default_branch "${PROJECT_DIR}")
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
log "START: Found PR(s) needing work:${PRS_NEEDING_WORK}"
|
|
272
|
+
|
|
273
|
+
# Convert "#12 #34" into ["12", "34"] for worker fan-out.
|
|
274
|
+
PR_NUMBER_ARRAY=()
|
|
275
|
+
for pr_token in ${PRS_NEEDING_WORK}; do
|
|
276
|
+
PR_NUMBER_ARRAY+=("${pr_token#\#}")
|
|
277
|
+
done
|
|
278
|
+
|
|
279
|
+
if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED}" = "1" ] && [ "${#PR_NUMBER_ARRAY[@]}" -gt 1 ]; then
|
|
280
|
+
# Dry-run mode: print diagnostics and exit
|
|
281
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
282
|
+
echo "=== Dry Run: PR Reviewer ==="
|
|
283
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
284
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
285
|
+
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
286
|
+
echo "Auto-merge: ${AUTO_MERGE}"
|
|
287
|
+
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
288
|
+
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
289
|
+
fi
|
|
290
|
+
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
291
|
+
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
292
|
+
echo "Parallel Workers: ${#PR_NUMBER_ARRAY[@]}"
|
|
293
|
+
echo "Timeout: ${MAX_RUNTIME}s"
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
log "PARALLEL: Launching ${#PR_NUMBER_ARRAY[@]} reviewer worker(s)"
|
|
298
|
+
|
|
299
|
+
declare -a WORKER_PIDS=()
|
|
300
|
+
declare -a WORKER_PRS=()
|
|
301
|
+
declare -a WORKER_OUTPUTS=()
|
|
302
|
+
|
|
303
|
+
for pr_number in "${PR_NUMBER_ARRAY[@]}"; do
|
|
304
|
+
worker_output=$(mktemp "/tmp/night-watch-pr-reviewer-${PROJECT_RUNTIME_KEY}-pr-${pr_number}.XXXXXX")
|
|
305
|
+
WORKER_OUTPUTS+=("${worker_output}")
|
|
306
|
+
WORKER_PRS+=("${pr_number}")
|
|
307
|
+
|
|
308
|
+
(
|
|
309
|
+
NW_TARGET_PR="${pr_number}" \
|
|
310
|
+
NW_REVIEWER_WORKER_MODE="1" \
|
|
311
|
+
NW_REVIEWER_PARALLEL="0" \
|
|
312
|
+
bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}" > "${worker_output}" 2>&1
|
|
313
|
+
) &
|
|
314
|
+
|
|
315
|
+
worker_pid=$!
|
|
316
|
+
WORKER_PIDS+=("${worker_pid}")
|
|
317
|
+
log "PARALLEL: Worker PID ${worker_pid} started for PR #${pr_number}"
|
|
318
|
+
done
|
|
319
|
+
|
|
320
|
+
EXIT_CODE=0
|
|
321
|
+
AUTO_MERGED_PRS=""
|
|
322
|
+
AUTO_MERGE_FAILED_PRS=""
|
|
323
|
+
|
|
324
|
+
for idx in "${!WORKER_PIDS[@]}"; do
|
|
325
|
+
worker_pid="${WORKER_PIDS[$idx]}"
|
|
326
|
+
worker_pr="${WORKER_PRS[$idx]}"
|
|
327
|
+
worker_output="${WORKER_OUTPUTS[$idx]}"
|
|
328
|
+
|
|
329
|
+
worker_exit_code=0
|
|
330
|
+
if wait "${worker_pid}"; then
|
|
331
|
+
worker_exit_code=0
|
|
332
|
+
else
|
|
333
|
+
worker_exit_code=$?
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
if [ -f "${worker_output}" ] && [ -s "${worker_output}" ]; then
|
|
337
|
+
cat "${worker_output}" >> "${LOG_FILE}"
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
worker_result=$(grep -o 'NIGHT_WATCH_RESULT:.*' "${worker_output}" 2>/dev/null | tail -1 || true)
|
|
341
|
+
worker_status=$(printf '%s' "${worker_result}" | sed -n 's/^NIGHT_WATCH_RESULT:\([^|]*\).*$/\1/p')
|
|
342
|
+
worker_auto_merged=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merged=)[^|]+' || true)
|
|
343
|
+
worker_auto_merge_failed=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merge_failed=)[^|]+' || true)
|
|
344
|
+
|
|
345
|
+
AUTO_MERGED_PRS=$(append_csv "${AUTO_MERGED_PRS}" "${worker_auto_merged}")
|
|
346
|
+
AUTO_MERGE_FAILED_PRS=$(append_csv "${AUTO_MERGE_FAILED_PRS}" "${worker_auto_merge_failed}")
|
|
347
|
+
|
|
348
|
+
rm -f "${worker_output}"
|
|
349
|
+
|
|
350
|
+
if [ "${worker_status}" = "failure" ] || { [ -n "${worker_status}" ] && [ "${worker_status}" != "success_reviewed" ] && [ "${worker_status}" != "timeout" ] && [ "${worker_status#skip_}" = "${worker_status}" ]; }; then
|
|
351
|
+
if [ "${EXIT_CODE}" -eq 0 ] || [ "${EXIT_CODE}" -eq 124 ]; then
|
|
352
|
+
EXIT_CODE=1
|
|
353
|
+
fi
|
|
354
|
+
log "PARALLEL: Worker for PR #${worker_pr} reported status '${worker_status:-unknown}'"
|
|
355
|
+
elif [ "${worker_status}" = "timeout" ]; then
|
|
356
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
357
|
+
EXIT_CODE=124
|
|
358
|
+
fi
|
|
359
|
+
log "PARALLEL: Worker for PR #${worker_pr} timed out"
|
|
360
|
+
elif [ "${worker_exit_code}" -ne 0 ]; then
|
|
361
|
+
if [ "${worker_exit_code}" -eq 124 ]; then
|
|
362
|
+
if [ "${EXIT_CODE}" -eq 0 ]; then
|
|
363
|
+
EXIT_CODE=124
|
|
364
|
+
fi
|
|
365
|
+
elif [ "${EXIT_CODE}" -eq 0 ] || [ "${EXIT_CODE}" -eq 124 ]; then
|
|
366
|
+
EXIT_CODE="${worker_exit_code}"
|
|
367
|
+
fi
|
|
368
|
+
log "PARALLEL: Worker for PR #${worker_pr} exited with code ${worker_exit_code}"
|
|
369
|
+
else
|
|
370
|
+
log "PARALLEL: Worker for PR #${worker_pr} completed"
|
|
371
|
+
fi
|
|
372
|
+
done
|
|
373
|
+
|
|
374
|
+
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
|
|
375
|
+
exit 0
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
REVIEW_WORKTREE_BASENAME="${PROJECT_NAME}-nw-review-runner"
|
|
379
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
380
|
+
REVIEW_WORKTREE_BASENAME="${REVIEW_WORKTREE_BASENAME}-pr-${TARGET_PR}"
|
|
381
|
+
fi
|
|
382
|
+
REVIEW_WORKTREE_DIR="$(dirname "${PROJECT_DIR}")/${REVIEW_WORKTREE_BASENAME}"
|
|
383
|
+
|
|
384
|
+
cleanup_worktrees "${PROJECT_DIR}" "${REVIEW_WORKTREE_BASENAME}"
|
|
385
|
+
|
|
386
|
+
# Dry-run mode: print diagnostics and exit
|
|
387
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
388
|
+
echo "=== Dry Run: PR Reviewer ==="
|
|
389
|
+
echo "Provider: ${PROVIDER_CMD}"
|
|
390
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
391
|
+
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
392
|
+
echo "Auto-merge: ${AUTO_MERGE}"
|
|
393
|
+
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
394
|
+
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
395
|
+
fi
|
|
396
|
+
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
397
|
+
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
398
|
+
echo "Review Worktree: ${REVIEW_WORKTREE_DIR}"
|
|
399
|
+
echo "Target PR: ${TARGET_PR:-all}"
|
|
400
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
401
|
+
echo "Worker Mode: ${WORKER_MODE}"
|
|
402
|
+
fi
|
|
403
|
+
echo "Timeout: ${MAX_RUNTIME}s"
|
|
404
|
+
exit 0
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
if ! prepare_detached_worktree "${PROJECT_DIR}" "${REVIEW_WORKTREE_DIR}" "${DEFAULT_BRANCH}" "${LOG_FILE}"; then
|
|
408
|
+
log "FAIL: Unable to create isolated reviewer worktree ${REVIEW_WORKTREE_DIR}"
|
|
409
|
+
exit 1
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
EXIT_CODE=0
|
|
413
|
+
TARGET_SCOPE_PROMPT=""
|
|
414
|
+
if [ -n "${TARGET_PR}" ]; then
|
|
415
|
+
TARGET_SCOPE_PROMPT=$'\n\n## Target Scope\n- Only process PR #'"${TARGET_PR}"$'.\n- Ignore all other PRs.\n- If this PR no longer needs work, stop immediately.\n'
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
case "${PROVIDER_CMD}" in
|
|
419
|
+
claude)
|
|
420
|
+
CLAUDE_PROMPT="/night-watch-pr-reviewer${TARGET_SCOPE_PROMPT}"
|
|
421
|
+
if (
|
|
422
|
+
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
423
|
+
claude -p "${CLAUDE_PROMPT}" \
|
|
424
|
+
--dangerously-skip-permissions \
|
|
425
|
+
>> "${LOG_FILE}" 2>&1
|
|
426
|
+
); then
|
|
427
|
+
EXIT_CODE=0
|
|
428
|
+
else
|
|
429
|
+
EXIT_CODE=$?
|
|
430
|
+
fi
|
|
431
|
+
;;
|
|
432
|
+
codex)
|
|
433
|
+
CODEX_PROMPT="$(cat "${REVIEW_WORKTREE_DIR}/.claude/commands/night-watch-pr-reviewer.md")${TARGET_SCOPE_PROMPT}"
|
|
434
|
+
if (
|
|
435
|
+
cd "${REVIEW_WORKTREE_DIR}" && timeout "${MAX_RUNTIME}" \
|
|
436
|
+
codex --quiet \
|
|
437
|
+
--yolo \
|
|
438
|
+
--prompt "${CODEX_PROMPT}" \
|
|
439
|
+
>> "${LOG_FILE}" 2>&1
|
|
440
|
+
); then
|
|
441
|
+
EXIT_CODE=0
|
|
442
|
+
else
|
|
443
|
+
EXIT_CODE=$?
|
|
444
|
+
fi
|
|
445
|
+
;;
|
|
446
|
+
*)
|
|
447
|
+
log "ERROR: Unknown provider: ${PROVIDER_CMD}"
|
|
448
|
+
exit 1
|
|
449
|
+
;;
|
|
450
|
+
esac
|
|
451
|
+
|
|
452
|
+
cleanup_worktrees "${PROJECT_DIR}" "${REVIEW_WORKTREE_BASENAME}"
|
|
453
|
+
|
|
454
|
+
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
455
|
+
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
456
|
+
# for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
|
|
457
|
+
AUTO_MERGED_PRS=""
|
|
458
|
+
AUTO_MERGE_FAILED_PRS=""
|
|
459
|
+
|
|
460
|
+
if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
|
|
461
|
+
log "AUTO-MERGE: Checking for merge-ready PRs..."
|
|
462
|
+
|
|
463
|
+
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
464
|
+
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
465
|
+
continue
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
|
|
469
|
+
continue
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
# Only process PRs matching branch patterns
|
|
473
|
+
if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
474
|
+
continue
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
# Check CI status - must have no failures
|
|
478
|
+
FAILED_CHECKS=$(gh pr checks "${pr_number}" 2>/dev/null | grep -ci 'fail' || true)
|
|
479
|
+
if [ "${FAILED_CHECKS}" -gt 0 ]; then
|
|
480
|
+
continue
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
# Check review score - must have score >= threshold
|
|
484
|
+
ALL_COMMENTS=$(
|
|
485
|
+
{
|
|
486
|
+
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
487
|
+
if [ -n "${REPO}" ]; then
|
|
488
|
+
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
489
|
+
fi
|
|
490
|
+
} | sort -u
|
|
491
|
+
)
|
|
492
|
+
LATEST_SCORE=$(echo "${ALL_COMMENTS}" \
|
|
493
|
+
| grep -oP 'Overall Score:\*?\*?\s*(\d+)/100' \
|
|
494
|
+
| tail -1 \
|
|
495
|
+
| grep -oP '\d+(?=/100)' || echo "")
|
|
496
|
+
|
|
497
|
+
# Skip PRs without a score
|
|
498
|
+
if [ -z "${LATEST_SCORE}" ]; then
|
|
499
|
+
continue
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
# Skip PRs with score below threshold
|
|
503
|
+
if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
504
|
+
continue
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# PR is merge-ready - queue for auto-merge
|
|
508
|
+
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
|
|
509
|
+
|
|
510
|
+
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
511
|
+
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
512
|
+
if [ -z "${AUTO_MERGED_PRS}" ]; then
|
|
513
|
+
AUTO_MERGED_PRS="#${pr_number}"
|
|
514
|
+
else
|
|
515
|
+
AUTO_MERGED_PRS="${AUTO_MERGED_PRS},#${pr_number}"
|
|
516
|
+
fi
|
|
517
|
+
else
|
|
518
|
+
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
519
|
+
if [ -z "${AUTO_MERGE_FAILED_PRS}" ]; then
|
|
520
|
+
AUTO_MERGE_FAILED_PRS="#${pr_number}"
|
|
521
|
+
else
|
|
522
|
+
AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS},#${pr_number}"
|
|
523
|
+
fi
|
|
524
|
+
fi
|
|
525
|
+
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}"
|