@jonit-dev/night-watch-cli 1.8.8-beta.9 → 1.8.10-beta.0
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 +1086 -519
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +10 -2
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- 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 +25 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +25 -5
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/merge.d.ts +26 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +159 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/review.d.ts +0 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +0 -13
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/slice.d.ts +1 -0
- package/dist/commands/slice.d.ts.map +1 -1
- package/dist/commands/slice.js +19 -19
- package/dist/commands/slice.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +54 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/scripts/night-watch-merger-cron.sh +321 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +1 -137
- package/dist/templates/slicer.md +54 -64
- package/dist/web/assets/index-B1BnOpiO.css +1 -0
- package/dist/web/assets/index-CPQbZ1BL.css +1 -0
- package/dist/web/assets/index-CiRJZI4z.js +386 -0
- package/dist/web/assets/index-DGpU39Cp.css +1 -0
- package/dist/web/assets/index-DcgNAi4A.js +386 -0
- package/dist/web/assets/index-SQlBKu_s.js +386 -0
- package/dist/web/assets/index-ZE5lOeJp.js +386 -0
- package/dist/web/assets/index-rfU713Zm.js +386 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch Merge Orchestrator Cron Runner
|
|
5
|
+
# Usage: night-watch-merger-cron.sh /path/to/project
|
|
6
|
+
#
|
|
7
|
+
# Scans all open PRs, filters eligible ones, and merges them in FIFO order
|
|
8
|
+
# (oldest PR first by creation date). Rebases remaining PRs after each merge.
|
|
9
|
+
#
|
|
10
|
+
# Required env vars (with defaults shown):
|
|
11
|
+
# NW_MERGER_MAX_RUNTIME=1800 - Maximum runtime in seconds (30 min)
|
|
12
|
+
# NW_MERGER_MERGE_METHOD=squash - Merge method: squash|merge|rebase
|
|
13
|
+
# NW_MERGER_MIN_REVIEW_SCORE=80 - Minimum review score threshold
|
|
14
|
+
# NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
|
|
15
|
+
# NW_MERGER_REBASE_BEFORE_MERGE=1 - Set to 1 to rebase before merging
|
|
16
|
+
# NW_MERGER_MAX_PRS_PER_RUN=0 - Max PRs to merge per run (0 = unlimited)
|
|
17
|
+
# NW_DRY_RUN=0 - Set to 1 for dry-run mode
|
|
18
|
+
|
|
19
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
20
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
21
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
22
|
+
LOG_FILE="${LOG_DIR}/merger.log"
|
|
23
|
+
MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-1800}"
|
|
24
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
25
|
+
MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
|
|
26
|
+
MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
|
|
27
|
+
REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
|
|
28
|
+
MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
|
|
29
|
+
BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
|
|
30
|
+
SCRIPT_START_TIME=$(date +%s)
|
|
31
|
+
DRY_RUN="${NW_DRY_RUN:-0}"
|
|
32
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
33
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
34
|
+
|
|
35
|
+
# Normalize numeric settings
|
|
36
|
+
if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
|
|
37
|
+
MAX_PRS_PER_RUN="0"
|
|
38
|
+
fi
|
|
39
|
+
if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
|
|
40
|
+
MIN_REVIEW_SCORE="80"
|
|
41
|
+
fi
|
|
42
|
+
# Clamp merge method to valid values
|
|
43
|
+
case "${MERGE_METHOD}" in
|
|
44
|
+
squash|merge|rebase) ;;
|
|
45
|
+
*) MERGE_METHOD="squash" ;;
|
|
46
|
+
esac
|
|
47
|
+
|
|
48
|
+
mkdir -p "${LOG_DIR}"
|
|
49
|
+
|
|
50
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
51
|
+
# shellcheck source=night-watch-helpers.sh
|
|
52
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
53
|
+
|
|
54
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
55
|
+
# NOTE: Lock file path must match mergerLockPath() in src/utils/status-data.ts
|
|
56
|
+
LOCK_FILE="/tmp/night-watch-merger-${PROJECT_RUNTIME_KEY}.lock"
|
|
57
|
+
SCRIPT_TYPE="merger"
|
|
58
|
+
|
|
59
|
+
MERGED_PRS=0
|
|
60
|
+
FAILED_PRS=0
|
|
61
|
+
MERGED_PR_LIST=""
|
|
62
|
+
|
|
63
|
+
emit_result() {
|
|
64
|
+
local status="${1:?status required}"
|
|
65
|
+
local details="${2:-}"
|
|
66
|
+
if [ -n "${details}" ]; then
|
|
67
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
68
|
+
else
|
|
69
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
74
|
+
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
75
|
+
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
76
|
+
if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
|
|
77
|
+
arm_global_queue_cleanup
|
|
78
|
+
else
|
|
79
|
+
claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
# Check if branch matches configured patterns
|
|
85
|
+
matches_branch_patterns() {
|
|
86
|
+
local branch="${1}"
|
|
87
|
+
if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
|
|
88
|
+
return 0 # No filter = match all
|
|
89
|
+
fi
|
|
90
|
+
IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
|
|
91
|
+
for pattern in "${patterns[@]}"; do
|
|
92
|
+
pattern="${pattern# }" # trim leading space
|
|
93
|
+
if [ -n "${pattern}" ] && [[ "${branch}" == "${pattern}"* ]]; then
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
done
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Get review score from PR labels/comments
|
|
101
|
+
get_review_score() {
|
|
102
|
+
local pr_number="${1}"
|
|
103
|
+
# Look for review score comment from night-watch
|
|
104
|
+
local score
|
|
105
|
+
score=$(gh pr view "${pr_number}" --json comments \
|
|
106
|
+
--jq '[.comments[].body | select(test("review score|score:? [0-9]+/100"; "i")) | capture("(?i)score:? *(?<s>[0-9]+)/100") | .s] | last | tonumber // -1' \
|
|
107
|
+
2>/dev/null || echo "-1")
|
|
108
|
+
echo "${score}"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Check if CI is passing for a PR (all checks must be complete and none failing)
|
|
112
|
+
ci_passing() {
|
|
113
|
+
local pr_number="${1}"
|
|
114
|
+
local checks_json
|
|
115
|
+
checks_json=$(gh pr checks "${pr_number}" --json name,state,conclusion 2>/dev/null || echo "[]")
|
|
116
|
+
# Fail if any checks have explicit failures
|
|
117
|
+
local fail_count
|
|
118
|
+
fail_count=$(echo "${checks_json}" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED" or .state == "FAILURE")] | length' 2>/dev/null || echo "999")
|
|
119
|
+
if [ "${fail_count}" != "0" ]; then
|
|
120
|
+
return 1
|
|
121
|
+
fi
|
|
122
|
+
# Fail if any checks are still pending/in-progress (not yet concluded)
|
|
123
|
+
local pending_count
|
|
124
|
+
pending_count=$(echo "${checks_json}" | jq '[.[] | select(.state == "PENDING" or .state == "IN_PROGRESS" or (.conclusion == null and .state != "SUCCESS"))] | length' 2>/dev/null || echo "999")
|
|
125
|
+
if [ "${pending_count}" != "0" ]; then
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Rebase a PR against its base branch
|
|
132
|
+
rebase_pr() {
|
|
133
|
+
local pr_number="${1}"
|
|
134
|
+
log "INFO: Rebasing PR #${pr_number} against base branch"
|
|
135
|
+
if [ "${DRY_RUN}" = "1" ]; then
|
|
136
|
+
log "INFO: [DRY RUN] Would rebase PR #${pr_number}"
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
gh pr update-branch --rebase "${pr_number}" 2>/dev/null
|
|
140
|
+
return $?
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
log() {
|
|
144
|
+
echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "${LOG_FILE}"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
rotate_log() {
|
|
148
|
+
if [ -f "${LOG_FILE}" ] && [ "$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0)" -ge "${MAX_LOG_SIZE}" ]; then
|
|
149
|
+
mv "${LOG_FILE}" "${LOG_FILE}.bak" 2>/dev/null || true
|
|
150
|
+
fi
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# ── Log rotation ──────────────────────────────────────────────────────────────
|
|
154
|
+
rotate_log
|
|
155
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
cd "${PROJECT_DIR}"
|
|
158
|
+
|
|
159
|
+
log "========================================"
|
|
160
|
+
log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
|
|
161
|
+
log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
|
|
162
|
+
log "========================================"
|
|
163
|
+
|
|
164
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
165
|
+
emit_result "skip_locked"
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# ── Dry-run mode ────────────────────────────────────────────────────────────
|
|
170
|
+
if [ "${DRY_RUN}" = "1" ]; then
|
|
171
|
+
echo "=== Dry Run: Merge Orchestrator ==="
|
|
172
|
+
echo "Merge Method: ${MERGE_METHOD}"
|
|
173
|
+
echo "Min Review Score: ${MIN_REVIEW_SCORE}/100"
|
|
174
|
+
echo "Rebase Before Merge: ${REBASE_BEFORE_MERGE}"
|
|
175
|
+
echo "Max PRs Per Run: ${MAX_PRS_PER_RUN} (0=unlimited)"
|
|
176
|
+
echo "Max Runtime: ${MAX_RUNTIME}s"
|
|
177
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
|
|
178
|
+
log "INFO: Dry run mode — exiting without processing"
|
|
179
|
+
emit_result "skip_dry_run"
|
|
180
|
+
exit 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Timeout watchdog
|
|
184
|
+
(
|
|
185
|
+
sleep "${MAX_RUNTIME}"
|
|
186
|
+
log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
|
|
187
|
+
kill -TERM $$ 2>/dev/null || true
|
|
188
|
+
) &
|
|
189
|
+
WATCHDOG_PID=$!
|
|
190
|
+
trap 'kill ${WATCHDOG_PID} 2>/dev/null || true; rm -f "${LOCK_FILE}"' EXIT
|
|
191
|
+
|
|
192
|
+
# Discover open PRs sorted by creation date (oldest first = FIFO)
|
|
193
|
+
log "INFO: Scanning open PRs..."
|
|
194
|
+
PR_LIST_JSON=$(gh pr list --state open \
|
|
195
|
+
--json number,headRefName,createdAt,isDraft \
|
|
196
|
+
--jq 'sort_by(.createdAt)' \
|
|
197
|
+
2>/dev/null || echo "[]")
|
|
198
|
+
|
|
199
|
+
PR_COUNT=$(echo "${PR_LIST_JSON}" | jq 'length')
|
|
200
|
+
log "INFO: Found ${PR_COUNT} open PRs"
|
|
201
|
+
|
|
202
|
+
if [ "${PR_COUNT}" = "0" ]; then
|
|
203
|
+
log "SKIP: No open PRs found. Exiting."
|
|
204
|
+
emit_result "skip_no_prs"
|
|
205
|
+
exit 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Process each PR in FIFO order
|
|
209
|
+
PROCESSED=0
|
|
210
|
+
while IFS= read -r pr_json; do
|
|
211
|
+
pr_number=$(echo "${pr_json}" | jq -r '.number')
|
|
212
|
+
pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
|
|
213
|
+
is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
|
|
214
|
+
|
|
215
|
+
# Skip drafts
|
|
216
|
+
if [ "${is_draft}" = "true" ]; then
|
|
217
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Skipping draft"
|
|
218
|
+
continue
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# Check branch pattern filter
|
|
222
|
+
if ! matches_branch_patterns "${pr_branch}"; then
|
|
223
|
+
log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
|
|
224
|
+
continue
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Check CI status
|
|
228
|
+
if ! ci_passing "${pr_number}"; then
|
|
229
|
+
log "INFO: PR #${pr_number} (${pr_branch}): CI not passing, skipping"
|
|
230
|
+
continue
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Check review score
|
|
234
|
+
if [ "${MIN_REVIEW_SCORE}" -gt "0" ]; then
|
|
235
|
+
score=$(get_review_score "${pr_number}")
|
|
236
|
+
if [ "${score}" -lt "0" ] || [ "${score}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
237
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), skipping"
|
|
238
|
+
continue
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Eligible for merge"
|
|
243
|
+
|
|
244
|
+
# Rebase before merge if configured
|
|
245
|
+
if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
|
|
246
|
+
if ! rebase_pr "${pr_number}"; then
|
|
247
|
+
log "WARN: PR #${pr_number}: Rebase failed, skipping"
|
|
248
|
+
FAILED_PRS=$((FAILED_PRS + 1))
|
|
249
|
+
continue
|
|
250
|
+
fi
|
|
251
|
+
log "INFO: PR #${pr_number}: Rebase successful"
|
|
252
|
+
|
|
253
|
+
# Poll CI until all checks complete after rebase (up to 5 minutes)
|
|
254
|
+
local ci_max_wait=300
|
|
255
|
+
local ci_waited=0
|
|
256
|
+
local ci_poll=15
|
|
257
|
+
while [ "${ci_waited}" -lt "${ci_max_wait}" ]; do
|
|
258
|
+
sleep "${ci_poll}"
|
|
259
|
+
ci_waited=$((ci_waited + ci_poll))
|
|
260
|
+
if ci_passing "${pr_number}"; then
|
|
261
|
+
break
|
|
262
|
+
fi
|
|
263
|
+
log "INFO: PR #${pr_number}: Waiting for CI after rebase (${ci_waited}s/${ci_max_wait}s)..."
|
|
264
|
+
done
|
|
265
|
+
if ! ci_passing "${pr_number}"; then
|
|
266
|
+
log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${ci_waited}s), skipping"
|
|
267
|
+
continue
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Merge the PR
|
|
272
|
+
log "INFO: Merging PR #${pr_number} with method: ${MERGE_METHOD}..."
|
|
273
|
+
if gh pr merge "${pr_number}" "--${MERGE_METHOD}" --delete-branch 2>&1 | tee -a "${LOG_FILE}"; then
|
|
274
|
+
log "INFO: PR #${pr_number}: Merged successfully"
|
|
275
|
+
MERGED_PRS=$((MERGED_PRS + 1))
|
|
276
|
+
MERGED_PR_LIST="${MERGED_PR_LIST}${pr_number},"
|
|
277
|
+
|
|
278
|
+
# Rebase remaining PRs after each successful merge
|
|
279
|
+
log "INFO: Rebasing remaining open PRs after merging #${pr_number}..."
|
|
280
|
+
REMAINING_JSON=$(gh pr list --state open \
|
|
281
|
+
--json number,headRefName \
|
|
282
|
+
2>/dev/null || echo "[]")
|
|
283
|
+
while IFS= read -r remaining_pr; do
|
|
284
|
+
remaining_number=$(echo "${remaining_pr}" | jq -r '.number')
|
|
285
|
+
remaining_branch=$(echo "${remaining_pr}" | jq -r '.headRefName')
|
|
286
|
+
if [ "${remaining_number}" != "${pr_number}" ]; then
|
|
287
|
+
log "INFO: Rebasing remaining PR #${remaining_number} (${remaining_branch})"
|
|
288
|
+
gh pr update-branch --rebase "${remaining_number}" 2>/dev/null || \
|
|
289
|
+
log "WARN: PR #${remaining_number}: Rebase failed (continuing)"
|
|
290
|
+
fi
|
|
291
|
+
done < <(echo "${REMAINING_JSON}" | jq -c '.[]')
|
|
292
|
+
else
|
|
293
|
+
log "WARN: PR #${pr_number}: Merge failed"
|
|
294
|
+
FAILED_PRS=$((FAILED_PRS + 1))
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
PROCESSED=$((PROCESSED + 1))
|
|
298
|
+
|
|
299
|
+
# Check max PRs per run limit
|
|
300
|
+
if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${PROCESSED}" -ge "${MAX_PRS_PER_RUN}" ]; then
|
|
301
|
+
log "INFO: Reached max PRs per run limit (${MAX_PRS_PER_RUN}). Stopping."
|
|
302
|
+
break
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
# Enforce global timeout
|
|
306
|
+
elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
307
|
+
if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
|
|
308
|
+
log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
|
|
309
|
+
break
|
|
310
|
+
fi
|
|
311
|
+
done < <(echo "${PR_LIST_JSON}" | jq -c '.[]')
|
|
312
|
+
|
|
313
|
+
# Trim trailing comma from PR list
|
|
314
|
+
MERGED_PR_LIST="${MERGED_PR_LIST%,}"
|
|
315
|
+
|
|
316
|
+
log "========================================"
|
|
317
|
+
log "RUN-END: merger complete merged=${MERGED_PRS} failed=${FAILED_PRS}"
|
|
318
|
+
log "========================================"
|
|
319
|
+
|
|
320
|
+
emit_result "success" "merged=${MERGED_PRS}|failed=${FAILED_PRS}|prs=${MERGED_PR_LIST}"
|
|
321
|
+
exit 0
|
|
@@ -10,8 +10,6 @@ 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
|
|
15
13
|
|
|
16
14
|
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
17
15
|
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
@@ -23,8 +21,6 @@ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
|
23
21
|
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
24
22
|
MIN_REVIEW_SCORE="${NW_MIN_REVIEW_SCORE:-80}"
|
|
25
23
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
26
|
-
AUTO_MERGE="${NW_AUTO_MERGE:-0}"
|
|
27
|
-
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
28
24
|
TARGET_PR="${NW_TARGET_PR:-}"
|
|
29
25
|
PARALLEL_ENABLED="${NW_REVIEWER_PARALLEL:-1}"
|
|
30
26
|
WORKER_MODE="${NW_REVIEWER_WORKER_MODE:-0}"
|
|
@@ -552,7 +548,7 @@ fi
|
|
|
552
548
|
rotate_log
|
|
553
549
|
log_separator
|
|
554
550
|
log "RUN-START: reviewer invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} worker=${WORKER_MODE} target_pr=${TARGET_PR:-all} parallel=${PARALLEL_ENABLED}"
|
|
555
|
-
log "CONFIG: max_runtime=${MAX_RUNTIME}s min_review_score=${MIN_REVIEW_SCORE}
|
|
551
|
+
log "CONFIG: max_runtime=${MAX_RUNTIME}s min_review_score=${MIN_REVIEW_SCORE} branch_patterns=${BRANCH_PATTERNS_RAW}"
|
|
556
552
|
|
|
557
553
|
if ! acquire_lock "${LOCK_FILE}"; then
|
|
558
554
|
emit_result "skip_locked"
|
|
@@ -683,61 +679,6 @@ done < <(
|
|
|
683
679
|
if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
684
680
|
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}"
|
|
685
681
|
|
|
686
|
-
# ── Auto-merge eligible PRs ───────────────────────────────
|
|
687
|
-
if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
|
|
688
|
-
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
689
|
-
AUTO_MERGED_COUNT=0
|
|
690
|
-
|
|
691
|
-
log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
|
|
692
|
-
|
|
693
|
-
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
694
|
-
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
695
|
-
printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
|
|
696
|
-
|
|
697
|
-
# Check CI status - must have ALL checks passing (not just "no failures")
|
|
698
|
-
# gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
|
|
699
|
-
if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
|
|
700
|
-
log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
|
|
701
|
-
continue
|
|
702
|
-
fi
|
|
703
|
-
|
|
704
|
-
# Check review score
|
|
705
|
-
PR_COMMENTS=$(
|
|
706
|
-
{
|
|
707
|
-
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
708
|
-
if [ -n "${REPO}" ]; then
|
|
709
|
-
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
710
|
-
fi
|
|
711
|
-
} | awk '!seen[$0]++'
|
|
712
|
-
)
|
|
713
|
-
PR_SCORE=$(extract_review_score_from_text "${PR_COMMENTS}")
|
|
714
|
-
|
|
715
|
-
# Skip PRs without a score or with score below threshold
|
|
716
|
-
[ -z "${PR_SCORE}" ] && continue
|
|
717
|
-
[ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
|
|
718
|
-
|
|
719
|
-
# PR is merge-ready
|
|
720
|
-
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
|
|
721
|
-
|
|
722
|
-
# Dry-run mode: show what would be merged
|
|
723
|
-
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
724
|
-
log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
|
|
725
|
-
continue
|
|
726
|
-
fi
|
|
727
|
-
|
|
728
|
-
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
729
|
-
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
730
|
-
AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
|
|
731
|
-
else
|
|
732
|
-
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
733
|
-
fi
|
|
734
|
-
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
735
|
-
|
|
736
|
-
if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
|
|
737
|
-
log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
|
|
738
|
-
fi
|
|
739
|
-
fi
|
|
740
|
-
|
|
741
682
|
if [ "${WORKER_MODE}" != "1" ]; then
|
|
742
683
|
send_telegram_status_message "🔍 Night Watch Reviewer: nothing to do" "Project: ${PROJECT_NAME}
|
|
743
684
|
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
@@ -786,10 +727,6 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
786
727
|
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
787
728
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
788
729
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
789
|
-
echo "Auto-merge: ${AUTO_MERGE}"
|
|
790
|
-
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
791
|
-
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
792
|
-
fi
|
|
793
730
|
echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
|
|
794
731
|
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
795
732
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
@@ -947,10 +884,6 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
947
884
|
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
948
885
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
949
886
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
950
|
-
echo "Auto-merge: ${AUTO_MERGE}"
|
|
951
|
-
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
952
|
-
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
953
|
-
fi
|
|
954
887
|
echo "Max Retries: ${REVIEWER_MAX_RETRIES}"
|
|
955
888
|
echo "Retry Delay: ${REVIEWER_RETRY_DELAY}s"
|
|
956
889
|
echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
|
|
@@ -1257,78 +1190,9 @@ if [ "${EXIT_CODE}" -eq 0 ] && [ -n "${TARGET_PR}" ] && [ -n "${PR_BRANCH_HEAD_B
|
|
|
1257
1190
|
fi
|
|
1258
1191
|
fi
|
|
1259
1192
|
|
|
1260
|
-
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
1261
|
-
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
1262
|
-
# for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
|
|
1263
1193
|
AUTO_MERGED_PRS=""
|
|
1264
1194
|
AUTO_MERGE_FAILED_PRS=""
|
|
1265
1195
|
|
|
1266
|
-
if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
|
|
1267
|
-
log "AUTO-MERGE: Checking for merge-ready PRs..."
|
|
1268
|
-
|
|
1269
|
-
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
1270
|
-
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
1271
|
-
continue
|
|
1272
|
-
fi
|
|
1273
|
-
|
|
1274
|
-
if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
|
|
1275
|
-
continue
|
|
1276
|
-
fi
|
|
1277
|
-
|
|
1278
|
-
# Only process PRs matching branch patterns
|
|
1279
|
-
if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
1280
|
-
continue
|
|
1281
|
-
fi
|
|
1282
|
-
|
|
1283
|
-
# Check CI status - must have ALL checks passing (not just "no failures")
|
|
1284
|
-
# gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
|
|
1285
|
-
if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
|
|
1286
|
-
log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
|
|
1287
|
-
continue
|
|
1288
|
-
fi
|
|
1289
|
-
|
|
1290
|
-
# Check review score - must have score >= threshold
|
|
1291
|
-
ALL_COMMENTS=$(
|
|
1292
|
-
{
|
|
1293
|
-
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
1294
|
-
if [ -n "${REPO}" ]; then
|
|
1295
|
-
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
1296
|
-
fi
|
|
1297
|
-
} | awk '!seen[$0]++'
|
|
1298
|
-
)
|
|
1299
|
-
LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
|
|
1300
|
-
|
|
1301
|
-
# Skip PRs without a score
|
|
1302
|
-
if [ -z "${LATEST_SCORE}" ]; then
|
|
1303
|
-
continue
|
|
1304
|
-
fi
|
|
1305
|
-
|
|
1306
|
-
# Skip PRs with score below threshold
|
|
1307
|
-
if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
1308
|
-
continue
|
|
1309
|
-
fi
|
|
1310
|
-
|
|
1311
|
-
# PR is merge-ready - queue for auto-merge
|
|
1312
|
-
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
|
|
1313
|
-
|
|
1314
|
-
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
1315
|
-
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
1316
|
-
if [ -z "${AUTO_MERGED_PRS}" ]; then
|
|
1317
|
-
AUTO_MERGED_PRS="#${pr_number}"
|
|
1318
|
-
else
|
|
1319
|
-
AUTO_MERGED_PRS="${AUTO_MERGED_PRS},#${pr_number}"
|
|
1320
|
-
fi
|
|
1321
|
-
else
|
|
1322
|
-
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
1323
|
-
if [ -z "${AUTO_MERGE_FAILED_PRS}" ]; then
|
|
1324
|
-
AUTO_MERGE_FAILED_PRS="#${pr_number}"
|
|
1325
|
-
else
|
|
1326
|
-
AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS},#${pr_number}"
|
|
1327
|
-
fi
|
|
1328
|
-
fi
|
|
1329
|
-
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
1330
|
-
fi
|
|
1331
|
-
|
|
1332
1196
|
REVIEWER_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
1333
1197
|
log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${REVIEWER_TOTAL_ELAPSED}s prs=${PRS_NEEDING_WORK_CSV:-none} attempts=${ATTEMPTS_MADE}"
|
|
1334
1198
|
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${ATTEMPTS_MADE}" "${FINAL_SCORE}" "${NO_CHANGES_NEEDED}" "${NO_CHANGES_PRS}"
|