@jonit-dev/night-watch-cli 1.8.8-beta.1 → 1.8.8-beta.11
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 +981 -78
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +50 -0
- package/dist/commands/install.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/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +2 -0
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +27 -4
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/resolve.d.ts +26 -0
- package/dist/commands/resolve.d.ts.map +1 -0
- package/dist/commands/resolve.js +186 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/review.d.ts +5 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +18 -18
- 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/summary.d.ts +14 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +193 -0
- package/dist/commands/summary.js.map +1 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +14 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/scripts/night-watch-helpers.sh +10 -1
- package/dist/scripts/night-watch-merger-cron.sh +321 -0
- package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +23 -142
- package/dist/scripts/night-watch-qa-cron.sh +30 -4
- package/dist/scripts/test-helpers.bats +45 -0
- package/dist/templates/night-watch-pr-reviewer.md +2 -1
- package/dist/templates/pr-reviewer.md +2 -1
- package/dist/templates/slicer.md +54 -64
- package/dist/web/assets/index-CPQbZ1BL.css +1 -0
- package/dist/web/assets/index-CiRJZI4z.js +386 -0
- package/dist/web/assets/index-ZE5lOeJp.js +386 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch PR Resolver Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-pr-resolver-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_PR_RESOLVER_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_PR_RESOLVER_MAX_PRS_PER_RUN=0 - Max PRs to process per run (0 = unlimited)
|
|
14
|
+
# NW_PR_RESOLVER_PER_PR_TIMEOUT=600 - Per-PR AI timeout in seconds
|
|
15
|
+
# NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION=1 - Set to 1 to use AI for conflict resolution
|
|
16
|
+
# NW_PR_RESOLVER_AI_REVIEW_RESOLUTION=0 - Set to 1 to also address review comments
|
|
17
|
+
# NW_PR_RESOLVER_READY_LABEL=ready-to-merge - Label to add when PR is conflict-free
|
|
18
|
+
# NW_PR_RESOLVER_BRANCH_PATTERNS= - Comma-separated branch prefixes to filter (empty = all)
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
21
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
22
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
23
|
+
LOG_FILE="${LOG_DIR}/pr-resolver.log"
|
|
24
|
+
MAX_RUNTIME="${NW_PR_RESOLVER_MAX_RUNTIME:-3600}" # 1 hour
|
|
25
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
26
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
27
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
28
|
+
MAX_PRS_PER_RUN="${NW_PR_RESOLVER_MAX_PRS_PER_RUN:-0}"
|
|
29
|
+
PER_PR_TIMEOUT="${NW_PR_RESOLVER_PER_PR_TIMEOUT:-600}"
|
|
30
|
+
AI_CONFLICT_RESOLUTION="${NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION:-1}"
|
|
31
|
+
AI_REVIEW_RESOLUTION="${NW_PR_RESOLVER_AI_REVIEW_RESOLUTION:-0}"
|
|
32
|
+
READY_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
|
|
33
|
+
BRANCH_PATTERNS_RAW="${NW_PR_RESOLVER_BRANCH_PATTERNS:-}"
|
|
34
|
+
SCRIPT_START_TIME=$(date +%s)
|
|
35
|
+
|
|
36
|
+
# Normalize numeric settings to safe ranges
|
|
37
|
+
if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
|
|
38
|
+
MAX_PRS_PER_RUN="0"
|
|
39
|
+
fi
|
|
40
|
+
if ! [[ "${PER_PR_TIMEOUT}" =~ ^[0-9]+$ ]]; then
|
|
41
|
+
PER_PR_TIMEOUT="600"
|
|
42
|
+
fi
|
|
43
|
+
if [ "${MAX_PRS_PER_RUN}" -gt 100 ]; then
|
|
44
|
+
MAX_PRS_PER_RUN="100"
|
|
45
|
+
fi
|
|
46
|
+
if [ "${PER_PR_TIMEOUT}" -gt 3600 ]; then
|
|
47
|
+
PER_PR_TIMEOUT="3600"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
mkdir -p "${LOG_DIR}"
|
|
51
|
+
|
|
52
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
53
|
+
# shellcheck source=night-watch-helpers.sh
|
|
54
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
55
|
+
|
|
56
|
+
# Ensure provider CLI is on PATH (nvm, fnm, volta, common bin dirs)
|
|
57
|
+
if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
|
|
58
|
+
echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
|
|
59
|
+
exit 127
|
|
60
|
+
fi
|
|
61
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
62
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
63
|
+
# NOTE: Lock file path must match resolverLockPath() in src/utils/status-data.ts
|
|
64
|
+
LOCK_FILE="/tmp/night-watch-pr-resolver-${PROJECT_RUNTIME_KEY}.lock"
|
|
65
|
+
SCRIPT_TYPE="pr-resolver"
|
|
66
|
+
|
|
67
|
+
emit_result() {
|
|
68
|
+
local status="${1:?status required}"
|
|
69
|
+
local details="${2:-}"
|
|
70
|
+
if [ -n "${details}" ]; then
|
|
71
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
72
|
+
else
|
|
73
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
78
|
+
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
79
|
+
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
80
|
+
if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
|
|
81
|
+
arm_global_queue_cleanup
|
|
82
|
+
else
|
|
83
|
+
claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
# PR discovery: returns JSON array of open PRs with required fields
|
|
89
|
+
discover_open_prs() {
|
|
90
|
+
gh pr list --state open \
|
|
91
|
+
--json number,title,headRefName,mergeable,isDraft,labels \
|
|
92
|
+
2>/dev/null || echo "[]"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Check if a branch matches any configured branch prefix patterns.
|
|
96
|
+
# Returns 0 (match/pass) or 1 (no match, skip PR).
|
|
97
|
+
matches_branch_patterns() {
|
|
98
|
+
local branch="${1}"
|
|
99
|
+
if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
|
|
100
|
+
return 0 # No filter configured = match all
|
|
101
|
+
fi
|
|
102
|
+
IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
|
|
103
|
+
for pattern in "${patterns[@]}"; do
|
|
104
|
+
pattern="${pattern# }" # trim leading space
|
|
105
|
+
if [[ "${branch}" == ${pattern}* ]]; then
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Process a single PR: resolve conflicts and/or review comments, then label.
|
|
113
|
+
# Echoes "ready" if the PR ends up conflict-free, "conflicted" otherwise.
|
|
114
|
+
# Returns 0 on success, 1 on unrecoverable failure.
|
|
115
|
+
process_pr() {
|
|
116
|
+
local pr_number="${1:?pr_number required}"
|
|
117
|
+
local pr_branch="${2:?pr_branch required}"
|
|
118
|
+
local pr_title="${3:-}"
|
|
119
|
+
local worktree_dir="/tmp/nw-resolver-pr${pr_number}-$$"
|
|
120
|
+
|
|
121
|
+
log "INFO: Processing PR #${pr_number}: ${pr_title}" "branch=${pr_branch}"
|
|
122
|
+
|
|
123
|
+
# Inner cleanup for worktree created during this PR's processing
|
|
124
|
+
cleanup_pr_worktree() {
|
|
125
|
+
if git -C "${PROJECT_DIR}" worktree list --porcelain 2>/dev/null \
|
|
126
|
+
| grep -qF "worktree ${worktree_dir}"; then
|
|
127
|
+
git -C "${PROJECT_DIR}" worktree remove --force "${worktree_dir}" 2>/dev/null || true
|
|
128
|
+
fi
|
|
129
|
+
rm -rf "${worktree_dir}" 2>/dev/null || true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ── Determine default branch ─────────────────────────────────────────────
|
|
133
|
+
local default_branch
|
|
134
|
+
default_branch="${NW_DEFAULT_BRANCH:-}"
|
|
135
|
+
if [ -z "${default_branch}" ]; then
|
|
136
|
+
default_branch=$(detect_default_branch "${PROJECT_DIR}")
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# ── Check current mergeable status ──────────────────────────────────────
|
|
140
|
+
local mergeable
|
|
141
|
+
mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
|
|
142
|
+
|
|
143
|
+
if [ "${mergeable}" = "CONFLICTING" ]; then
|
|
144
|
+
log "INFO: PR #${pr_number} has conflicts, attempting resolution" "branch=${pr_branch}"
|
|
145
|
+
|
|
146
|
+
# Fetch the PR branch so we have an up-to-date ref
|
|
147
|
+
git -C "${PROJECT_DIR}" fetch --quiet origin "${pr_branch}" 2>/dev/null || true
|
|
148
|
+
|
|
149
|
+
# Create an isolated worktree on the PR branch
|
|
150
|
+
if ! prepare_branch_worktree "${PROJECT_DIR}" "${worktree_dir}" "${pr_branch}" "${default_branch}" "${LOG_FILE}"; then
|
|
151
|
+
log "WARN: Failed to create worktree for PR #${pr_number}" "branch=${pr_branch}"
|
|
152
|
+
cleanup_pr_worktree
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
local rebase_success=0
|
|
157
|
+
|
|
158
|
+
# Attempt a clean rebase first (no AI needed if it auto-resolves)
|
|
159
|
+
if git -C "${worktree_dir}" rebase "origin/${default_branch}" --quiet 2>/dev/null; then
|
|
160
|
+
rebase_success=1
|
|
161
|
+
log "INFO: PR #${pr_number} rebased cleanly (no conflicts)" "branch=${pr_branch}"
|
|
162
|
+
else
|
|
163
|
+
# Clean up the failed rebase state
|
|
164
|
+
git -C "${worktree_dir}" rebase --abort 2>/dev/null || true
|
|
165
|
+
|
|
166
|
+
if [ "${AI_CONFLICT_RESOLUTION}" = "1" ]; then
|
|
167
|
+
log "INFO: Invoking AI to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
168
|
+
|
|
169
|
+
local ai_prompt
|
|
170
|
+
ai_prompt="You are working in a git repository at ${worktree_dir}. \
|
|
171
|
+
Branch '${pr_branch}' has merge conflicts with '${default_branch}'. \
|
|
172
|
+
Please resolve the merge conflicts by: \
|
|
173
|
+
1) Running: git rebase origin/${default_branch} \
|
|
174
|
+
2) Resolving any conflict markers in the affected files \
|
|
175
|
+
3) Staging resolved files with: git add <files> \
|
|
176
|
+
4) Continuing the rebase with: git rebase --continue \
|
|
177
|
+
5) Finally pushing with: git push --force-with-lease origin ${pr_branch} \
|
|
178
|
+
Work exclusively in the directory: ${worktree_dir}"
|
|
179
|
+
|
|
180
|
+
local -a cmd_parts
|
|
181
|
+
mapfile -d '' -t cmd_parts < <(build_provider_cmd "${worktree_dir}" "${ai_prompt}")
|
|
182
|
+
|
|
183
|
+
if timeout "${PER_PR_TIMEOUT}" "${cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
|
|
184
|
+
rebase_success=1
|
|
185
|
+
log "INFO: AI resolved conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
186
|
+
else
|
|
187
|
+
log "WARN: AI failed to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
188
|
+
cleanup_pr_worktree
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
log "WARN: Skipping PR #${pr_number} — conflicts exist and AI resolution is disabled" "branch=${pr_branch}"
|
|
193
|
+
cleanup_pr_worktree
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
if [ "${rebase_success}" = "1" ]; then
|
|
199
|
+
# Safety: never force-push to the default branch
|
|
200
|
+
if [ "${pr_branch}" = "${default_branch}" ]; then
|
|
201
|
+
log "WARN: Refusing to force-push to default branch ${default_branch} for PR #${pr_number}"
|
|
202
|
+
cleanup_pr_worktree
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
# Push the rebased branch (AI may have already pushed; --force-with-lease is idempotent)
|
|
206
|
+
git -C "${worktree_dir}" push --force-with-lease origin "${pr_branch}" >> "${LOG_FILE}" 2>&1 || {
|
|
207
|
+
log "WARN: Push after rebase failed for PR #${pr_number}" "branch=${pr_branch}"
|
|
208
|
+
}
|
|
209
|
+
fi
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# ── Secondary: AI review comment resolution (opt-in) ────────────────────
|
|
213
|
+
if [ "${AI_REVIEW_RESOLUTION}" = "1" ]; then
|
|
214
|
+
local unresolved_count
|
|
215
|
+
unresolved_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
|
|
216
|
+
--jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
|
|
217
|
+
|
|
218
|
+
if [ "${unresolved_count}" -gt "0" ]; then
|
|
219
|
+
log "INFO: PR #${pr_number} has ${unresolved_count} change request(s), invoking AI" "branch=${pr_branch}"
|
|
220
|
+
|
|
221
|
+
local review_workdir="${worktree_dir}"
|
|
222
|
+
if [ ! -d "${review_workdir}" ]; then
|
|
223
|
+
review_workdir="${PROJECT_DIR}"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
local review_prompt
|
|
227
|
+
review_prompt="You are working in the git repository at ${review_workdir}. \
|
|
228
|
+
PR #${pr_number} on branch '${pr_branch}' has unresolved review comments requesting changes. \
|
|
229
|
+
Please: \
|
|
230
|
+
1) Run 'gh pr view ${pr_number} --comments' to read the review comments \
|
|
231
|
+
2) Implement the requested changes \
|
|
232
|
+
3) Commit the changes with a descriptive message \
|
|
233
|
+
4) Push with: git push origin ${pr_branch} \
|
|
234
|
+
Work in the directory: ${review_workdir}"
|
|
235
|
+
|
|
236
|
+
local -a review_cmd_parts
|
|
237
|
+
mapfile -d '' -t review_cmd_parts < <(build_provider_cmd "${review_workdir}" "${review_prompt}")
|
|
238
|
+
|
|
239
|
+
if timeout "${PER_PR_TIMEOUT}" "${review_cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
|
|
240
|
+
log "INFO: AI addressed review comments for PR #${pr_number}" "branch=${pr_branch}"
|
|
241
|
+
else
|
|
242
|
+
log "WARN: AI failed to address review comments for PR #${pr_number}" "branch=${pr_branch}"
|
|
243
|
+
fi
|
|
244
|
+
fi
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# ── Re-check mergeable status after processing ──────────────────────────
|
|
248
|
+
# Brief wait for GitHub to propagate the push and recompute mergeability
|
|
249
|
+
sleep 3
|
|
250
|
+
local final_mergeable
|
|
251
|
+
final_mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
|
|
252
|
+
|
|
253
|
+
# ── Labeling ─────────────────────────────────────────────────────────────
|
|
254
|
+
local result
|
|
255
|
+
if [ "${final_mergeable}" != "CONFLICTING" ]; then
|
|
256
|
+
# Ensure the ready label exists in the repo (idempotent)
|
|
257
|
+
gh label create "${READY_LABEL}" \
|
|
258
|
+
--color "0075ca" \
|
|
259
|
+
--description "PR is conflict-free and ready to merge" \
|
|
260
|
+
2>/dev/null || true
|
|
261
|
+
gh pr edit "${pr_number}" --add-label "${READY_LABEL}" 2>/dev/null || true
|
|
262
|
+
log "INFO: PR #${pr_number} marked as '${READY_LABEL}'" "branch=${pr_branch}"
|
|
263
|
+
result="ready"
|
|
264
|
+
else
|
|
265
|
+
gh pr edit "${pr_number}" --remove-label "${READY_LABEL}" 2>/dev/null || true
|
|
266
|
+
log "WARN: PR #${pr_number} still has conflicts after processing" "branch=${pr_branch}"
|
|
267
|
+
result="conflicted"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
cleanup_pr_worktree
|
|
271
|
+
echo "${result}"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# ── Validate provider ────────────────────────────────────────────────────────
|
|
275
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
276
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
277
|
+
exit 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
rotate_log
|
|
281
|
+
log_separator
|
|
282
|
+
log "RUN-START: pr-resolver invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
|
|
283
|
+
log "CONFIG: max_runtime=${MAX_RUNTIME}s max_prs=${MAX_PRS_PER_RUN} per_pr_timeout=${PER_PR_TIMEOUT}s ai_conflict=${AI_CONFLICT_RESOLUTION} ai_review=${AI_REVIEW_RESOLUTION} ready_label=${READY_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
|
|
284
|
+
|
|
285
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
286
|
+
emit_result "skip_locked"
|
|
287
|
+
exit 0
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
cd "${PROJECT_DIR}"
|
|
291
|
+
|
|
292
|
+
# ── Dry-run mode ────────────────────────────────────────────────────────────
|
|
293
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
294
|
+
echo "=== Dry Run: PR Resolver ==="
|
|
295
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
296
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
|
|
297
|
+
echo "Max PRs Per Run: ${MAX_PRS_PER_RUN}"
|
|
298
|
+
echo "Per-PR Timeout: ${PER_PR_TIMEOUT}s"
|
|
299
|
+
echo "AI Conflict Resolution: ${AI_CONFLICT_RESOLUTION}"
|
|
300
|
+
echo "AI Review Resolution: ${AI_REVIEW_RESOLUTION}"
|
|
301
|
+
echo "Ready Label: ${READY_LABEL}"
|
|
302
|
+
echo "Max Runtime: ${MAX_RUNTIME}s"
|
|
303
|
+
log "INFO: Dry run mode — exiting without processing"
|
|
304
|
+
emit_result "skip_dry_run"
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
send_telegram_status_message "Night Watch PR Resolver: started" "Project: ${PROJECT_NAME}
|
|
309
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
310
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW:-all}
|
|
311
|
+
Action: scanning open PRs for merge conflicts."
|
|
312
|
+
|
|
313
|
+
# ── Discover open PRs ────────────────────────────────────────────────────────
|
|
314
|
+
pr_json=$(discover_open_prs)
|
|
315
|
+
|
|
316
|
+
if [ -z "${pr_json}" ] || [ "${pr_json}" = "[]" ]; then
|
|
317
|
+
log "SKIP: No open PRs found"
|
|
318
|
+
send_telegram_status_message "Night Watch PR Resolver: nothing to do" "Project: ${PROJECT_NAME}
|
|
319
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
320
|
+
Result: no open PRs found."
|
|
321
|
+
emit_result "skip_no_open_prs"
|
|
322
|
+
exit 0
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
pr_count=$(printf '%s' "${pr_json}" | jq 'length' 2>/dev/null || echo "0")
|
|
326
|
+
log "INFO: Found ${pr_count} open PR(s) to evaluate"
|
|
327
|
+
|
|
328
|
+
# ── Main processing loop ─────────────────────────────────────────────────────
|
|
329
|
+
processed=0
|
|
330
|
+
conflicts_resolved=0
|
|
331
|
+
reviews_addressed=0
|
|
332
|
+
prs_ready=0
|
|
333
|
+
prs_failed=0
|
|
334
|
+
|
|
335
|
+
while IFS= read -r pr_line; do
|
|
336
|
+
[ -z "${pr_line}" ] && continue
|
|
337
|
+
|
|
338
|
+
pr_number=$(printf '%s' "${pr_line}" | jq -r '.number')
|
|
339
|
+
pr_branch=$(printf '%s' "${pr_line}" | jq -r '.headRefName')
|
|
340
|
+
pr_title=$(printf '%s' "${pr_line}" | jq -r '.title')
|
|
341
|
+
is_draft=$(printf '%s' "${pr_line}" | jq -r '.isDraft')
|
|
342
|
+
labels=$(printf '%s' "${pr_line}" | jq -r '[.labels[].name] | join(",")')
|
|
343
|
+
|
|
344
|
+
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
345
|
+
|
|
346
|
+
# Skip draft PRs
|
|
347
|
+
if [ "${is_draft}" = "true" ]; then
|
|
348
|
+
log "INFO: Skipping draft PR #${pr_number}" "branch=${pr_branch}"
|
|
349
|
+
continue
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# Skip PRs labelled skip-resolver
|
|
353
|
+
if [[ "${labels}" == *"skip-resolver"* ]]; then
|
|
354
|
+
log "INFO: Skipping PR #${pr_number} (skip-resolver label)" "branch=${pr_branch}"
|
|
355
|
+
continue
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Apply branch pattern filter
|
|
359
|
+
if ! matches_branch_patterns "${pr_branch}"; then
|
|
360
|
+
log "DEBUG: Skipping PR #${pr_number} — branch '${pr_branch}' does not match patterns" "patterns=${BRANCH_PATTERNS_RAW}"
|
|
361
|
+
continue
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Enforce max PRs per run
|
|
365
|
+
if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${processed}" -ge "${MAX_PRS_PER_RUN}" ]; then
|
|
366
|
+
log "INFO: Reached max PRs per run (${MAX_PRS_PER_RUN}), stopping"
|
|
367
|
+
break
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
# Enforce global timeout
|
|
371
|
+
elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
372
|
+
if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
|
|
373
|
+
log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
|
|
374
|
+
break
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
processed=$(( processed + 1 ))
|
|
378
|
+
|
|
379
|
+
result=""
|
|
380
|
+
if result=$(process_pr "${pr_number}" "${pr_branch}" "${pr_title}" 2>&1); then
|
|
381
|
+
# process_pr echoes "ready" or "conflicted" on the last line; extract it
|
|
382
|
+
last_line=$(printf '%s' "${result}" | tail -1)
|
|
383
|
+
if [ "${last_line}" = "ready" ]; then
|
|
384
|
+
prs_ready=$(( prs_ready + 1 ))
|
|
385
|
+
conflicts_resolved=$(( conflicts_resolved + 1 ))
|
|
386
|
+
fi
|
|
387
|
+
else
|
|
388
|
+
prs_failed=$(( prs_failed + 1 ))
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
done < <(printf '%s' "${pr_json}" | jq -c '.[]')
|
|
392
|
+
|
|
393
|
+
log "RUN-END: pr-resolver complete processed=${processed} conflicts_resolved=${conflicts_resolved} prs_ready=${prs_ready} prs_failed=${prs_failed}"
|
|
394
|
+
|
|
395
|
+
send_telegram_status_message "Night Watch PR Resolver: completed" "Project: ${PROJECT_NAME}
|
|
396
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
397
|
+
PRs processed: ${processed}
|
|
398
|
+
Conflicts resolved: ${conflicts_resolved}
|
|
399
|
+
PRs marked '${READY_LABEL}': ${prs_ready}
|
|
400
|
+
PRs failed: ${prs_failed}"
|
|
401
|
+
|
|
402
|
+
emit_result "success" "prs_processed=${processed}|conflicts_resolved=${conflicts_resolved}|reviews_addressed=${reviews_addressed}|prs_ready=${prs_ready}|prs_failed=${prs_failed}"
|
|
@@ -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}"
|
|
@@ -105,7 +101,9 @@ extract_review_score_from_text() {
|
|
|
105
101
|
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
106
102
|
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
107
103
|
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
108
|
-
if [ "${
|
|
104
|
+
if [ "${NW_QUEUE_INHERITED_SLOT:-0}" = "1" ]; then
|
|
105
|
+
:
|
|
106
|
+
elif [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
|
|
109
107
|
arm_global_queue_cleanup
|
|
110
108
|
else
|
|
111
109
|
claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
|
|
@@ -550,7 +548,7 @@ fi
|
|
|
550
548
|
rotate_log
|
|
551
549
|
log_separator
|
|
552
550
|
log "RUN-START: reviewer invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} worker=${WORKER_MODE} target_pr=${TARGET_PR:-all} parallel=${PARALLEL_ENABLED}"
|
|
553
|
-
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}"
|
|
554
552
|
|
|
555
553
|
if ! acquire_lock "${LOCK_FILE}"; then
|
|
556
554
|
emit_result "skip_locked"
|
|
@@ -664,7 +662,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
|
|
|
664
662
|
} | awk '!seen[$0]++'
|
|
665
663
|
)
|
|
666
664
|
LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
|
|
667
|
-
if [ -
|
|
665
|
+
if [ -z "${LATEST_SCORE}" ]; then
|
|
666
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has no review score yet — needs initial review"
|
|
667
|
+
NEEDS_WORK=1
|
|
668
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
669
|
+
elif [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
668
670
|
log "INFO: PR #${pr_number} (${pr_branch}) has review score ${LATEST_SCORE}/100 (threshold: ${MIN_REVIEW_SCORE})"
|
|
669
671
|
NEEDS_WORK=1
|
|
670
672
|
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
@@ -675,62 +677,7 @@ done < <(
|
|
|
675
677
|
)
|
|
676
678
|
|
|
677
679
|
if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
678
|
-
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}
|
|
679
|
-
|
|
680
|
-
# ── Auto-merge eligible PRs ───────────────────────────────
|
|
681
|
-
if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
|
|
682
|
-
AUTO_MERGE_METHOD="${NW_AUTO_MERGE_METHOD:-squash}"
|
|
683
|
-
AUTO_MERGED_COUNT=0
|
|
684
|
-
|
|
685
|
-
log "AUTO-MERGE: Checking for merge-ready PRs (method: ${AUTO_MERGE_METHOD})"
|
|
686
|
-
|
|
687
|
-
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
688
|
-
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
689
|
-
printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}" || continue
|
|
690
|
-
|
|
691
|
-
# Check CI status - must have ALL checks passing (not just "no failures")
|
|
692
|
-
# gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
|
|
693
|
-
if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
|
|
694
|
-
log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
|
|
695
|
-
continue
|
|
696
|
-
fi
|
|
697
|
-
|
|
698
|
-
# Check review score
|
|
699
|
-
PR_COMMENTS=$(
|
|
700
|
-
{
|
|
701
|
-
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
702
|
-
if [ -n "${REPO}" ]; then
|
|
703
|
-
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
704
|
-
fi
|
|
705
|
-
} | awk '!seen[$0]++'
|
|
706
|
-
)
|
|
707
|
-
PR_SCORE=$(extract_review_score_from_text "${PR_COMMENTS}")
|
|
708
|
-
|
|
709
|
-
# Skip PRs without a score or with score below threshold
|
|
710
|
-
[ -z "${PR_SCORE}" ] && continue
|
|
711
|
-
[ "${PR_SCORE}" -lt "${MIN_REVIEW_SCORE}" ] && continue
|
|
712
|
-
|
|
713
|
-
# PR is merge-ready
|
|
714
|
-
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${PR_SCORE}/100, CI passing"
|
|
715
|
-
|
|
716
|
-
# Dry-run mode: show what would be merged
|
|
717
|
-
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
718
|
-
log "AUTO-MERGE (dry-run): Would queue merge for PR #${pr_number} using ${AUTO_MERGE_METHOD}"
|
|
719
|
-
continue
|
|
720
|
-
fi
|
|
721
|
-
|
|
722
|
-
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
723
|
-
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
724
|
-
AUTO_MERGED_COUNT=$((AUTO_MERGED_COUNT + 1))
|
|
725
|
-
else
|
|
726
|
-
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
727
|
-
fi
|
|
728
|
-
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
729
|
-
|
|
730
|
-
if [ "${AUTO_MERGED_COUNT}" -gt 0 ]; then
|
|
731
|
-
log "AUTO-MERGE: Queued ${AUTO_MERGED_COUNT} PR(s) for merge"
|
|
732
|
-
fi
|
|
733
|
-
fi
|
|
680
|
+
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}"
|
|
734
681
|
|
|
735
682
|
if [ "${WORKER_MODE}" != "1" ]; then
|
|
736
683
|
send_telegram_status_message "🔍 Night Watch Reviewer: nothing to do" "Project: ${PROJECT_NAME}
|
|
@@ -780,10 +727,6 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
780
727
|
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
781
728
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
782
729
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
783
|
-
echo "Auto-merge: ${AUTO_MERGE}"
|
|
784
|
-
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
785
|
-
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
786
|
-
fi
|
|
787
730
|
echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
|
|
788
731
|
echo "Open PRs needing work:${PRS_NEEDING_WORK}"
|
|
789
732
|
echo "Default Branch: ${DEFAULT_BRANCH}"
|
|
@@ -814,6 +757,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
814
757
|
NW_TARGET_PR="${pr_number}" \
|
|
815
758
|
NW_REVIEWER_WORKER_MODE="1" \
|
|
816
759
|
NW_REVIEWER_PARALLEL="0" \
|
|
760
|
+
NW_QUEUE_INHERITED_SLOT="1" \
|
|
817
761
|
bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}" > "${worker_output}" 2>&1
|
|
818
762
|
) &
|
|
819
763
|
|
|
@@ -922,7 +866,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
922
866
|
cleanup_reviewer_worktrees
|
|
923
867
|
|
|
924
868
|
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
|
|
925
|
-
exit
|
|
869
|
+
exit "${EXIT_CODE}"
|
|
926
870
|
fi
|
|
927
871
|
|
|
928
872
|
REVIEW_RUN_TOKEN="${PROJECT_RUNTIME_KEY}-$$"
|
|
@@ -940,10 +884,6 @@ if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
|
940
884
|
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
941
885
|
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW}"
|
|
942
886
|
echo "Min Review Score: ${MIN_REVIEW_SCORE}"
|
|
943
|
-
echo "Auto-merge: ${AUTO_MERGE}"
|
|
944
|
-
if [ "${AUTO_MERGE}" = "1" ]; then
|
|
945
|
-
echo "Auto-merge Method: ${AUTO_MERGE_METHOD}"
|
|
946
|
-
fi
|
|
947
887
|
echo "Max Retries: ${REVIEWER_MAX_RETRIES}"
|
|
948
888
|
echo "Retry Delay: ${REVIEWER_RETRY_DELAY}s"
|
|
949
889
|
echo "Max PRs Per Run: ${REVIEWER_MAX_PRS_PER_RUN}"
|
|
@@ -1015,8 +955,16 @@ if [ -n "${TARGET_PR}" ]; then
|
|
|
1015
955
|
fi
|
|
1016
956
|
if [ -n "${TARGET_SCORE}" ]; then
|
|
1017
957
|
TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
|
|
958
|
+
TARGET_SCOPE_PROMPT+=$'- action: fix\n'
|
|
959
|
+
# Inject the latest review comment body for the fix prompt
|
|
960
|
+
REVIEW_BODY=$(gh api "repos/$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)/issues/${TARGET_PR}/comments" --jq '[.[] | select(.body | test("Overall Score|Score:.*[0-9]+/100"))] | last | .body // ""' 2>/dev/null || echo "")
|
|
961
|
+
if [ -n "${REVIEW_BODY}" ]; then
|
|
962
|
+
TRUNCATED_REVIEW=$(printf '%s' "${REVIEW_BODY}" | head -c 6000)
|
|
963
|
+
TARGET_SCOPE_PROMPT+=$'\n## Latest Review Feedback\n'"${TRUNCATED_REVIEW}"$'\n'
|
|
964
|
+
fi
|
|
1018
965
|
else
|
|
1019
966
|
TARGET_SCOPE_PROMPT+=$'- latest review score: not found\n'
|
|
967
|
+
TARGET_SCOPE_PROMPT+=$'- action: review\n'
|
|
1020
968
|
fi
|
|
1021
969
|
fi
|
|
1022
970
|
|
|
@@ -1201,7 +1149,8 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
1201
1149
|
fi
|
|
1202
1150
|
continue
|
|
1203
1151
|
fi
|
|
1204
|
-
log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; failing run."
|
|
1152
|
+
log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; labeling needs-human-review and failing run."
|
|
1153
|
+
gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
|
|
1205
1154
|
EXIT_CODE=1
|
|
1206
1155
|
break
|
|
1207
1156
|
fi
|
|
@@ -1241,78 +1190,10 @@ if [ "${EXIT_CODE}" -eq 0 ] && [ -n "${TARGET_PR}" ] && [ -n "${PR_BRANCH_HEAD_B
|
|
|
1241
1190
|
fi
|
|
1242
1191
|
fi
|
|
1243
1192
|
|
|
1244
|
-
# ── Auto-merge eligible PRs ─────────────────────────────────────────────────────
|
|
1245
|
-
# After the reviewer completes, check for PRs that are merge-ready and queue them
|
|
1246
|
-
# for auto-merge if enabled. Uses gh pr merge --auto to respect GitHub branch protection.
|
|
1247
1193
|
AUTO_MERGED_PRS=""
|
|
1248
1194
|
AUTO_MERGE_FAILED_PRS=""
|
|
1249
1195
|
|
|
1250
|
-
if [ "${AUTO_MERGE}" = "1" ] && [ ${EXIT_CODE} -eq 0 ]; then
|
|
1251
|
-
log "AUTO-MERGE: Checking for merge-ready PRs..."
|
|
1252
|
-
|
|
1253
|
-
while IFS=$'\t' read -r pr_number pr_branch; do
|
|
1254
|
-
if [ -z "${pr_number}" ] || [ -z "${pr_branch}" ]; then
|
|
1255
|
-
continue
|
|
1256
|
-
fi
|
|
1257
|
-
|
|
1258
|
-
if [ -n "${TARGET_PR}" ] && [ "${pr_number}" != "${TARGET_PR}" ]; then
|
|
1259
|
-
continue
|
|
1260
|
-
fi
|
|
1261
|
-
|
|
1262
|
-
# Only process PRs matching branch patterns
|
|
1263
|
-
if [ -z "${TARGET_PR}" ] && ! printf '%s\n' "${pr_branch}" | grep -Eq "${BRANCH_REGEX}"; then
|
|
1264
|
-
continue
|
|
1265
|
-
fi
|
|
1266
|
-
|
|
1267
|
-
# Check CI status - must have ALL checks passing (not just "no failures")
|
|
1268
|
-
# gh pr checks exits 0 if all pass, 8 if pending, non-zero otherwise
|
|
1269
|
-
if ! gh pr checks "${pr_number}" --required >/dev/null 2>&1; then
|
|
1270
|
-
log "AUTO-MERGE: PR #${pr_number} has pending or failed CI checks"
|
|
1271
|
-
continue
|
|
1272
|
-
fi
|
|
1273
|
-
|
|
1274
|
-
# Check review score - must have score >= threshold
|
|
1275
|
-
ALL_COMMENTS=$(
|
|
1276
|
-
{
|
|
1277
|
-
gh pr view "${pr_number}" --json comments --jq '.comments[].body' 2>/dev/null || true
|
|
1278
|
-
if [ -n "${REPO}" ]; then
|
|
1279
|
-
gh api "repos/${REPO}/issues/${pr_number}/comments" --jq '.[].body' 2>/dev/null || true
|
|
1280
|
-
fi
|
|
1281
|
-
} | awk '!seen[$0]++'
|
|
1282
|
-
)
|
|
1283
|
-
LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
|
|
1284
|
-
|
|
1285
|
-
# Skip PRs without a score
|
|
1286
|
-
if [ -z "${LATEST_SCORE}" ]; then
|
|
1287
|
-
continue
|
|
1288
|
-
fi
|
|
1289
|
-
|
|
1290
|
-
# Skip PRs with score below threshold
|
|
1291
|
-
if [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
1292
|
-
continue
|
|
1293
|
-
fi
|
|
1294
|
-
|
|
1295
|
-
# PR is merge-ready - queue for auto-merge
|
|
1296
|
-
log "AUTO-MERGE: PR #${pr_number} (${pr_branch}) — score ${LATEST_SCORE}/100, CI passing"
|
|
1297
|
-
|
|
1298
|
-
if gh pr merge "${pr_number}" --"${AUTO_MERGE_METHOD}" --auto --delete-branch 2>>"${LOG_FILE}"; then
|
|
1299
|
-
log "AUTO-MERGE: Successfully queued merge for PR #${pr_number}"
|
|
1300
|
-
if [ -z "${AUTO_MERGED_PRS}" ]; then
|
|
1301
|
-
AUTO_MERGED_PRS="#${pr_number}"
|
|
1302
|
-
else
|
|
1303
|
-
AUTO_MERGED_PRS="${AUTO_MERGED_PRS},#${pr_number}"
|
|
1304
|
-
fi
|
|
1305
|
-
else
|
|
1306
|
-
log "WARN: Auto-merge failed for PR #${pr_number}"
|
|
1307
|
-
if [ -z "${AUTO_MERGE_FAILED_PRS}" ]; then
|
|
1308
|
-
AUTO_MERGE_FAILED_PRS="#${pr_number}"
|
|
1309
|
-
else
|
|
1310
|
-
AUTO_MERGE_FAILED_PRS="${AUTO_MERGE_FAILED_PRS},#${pr_number}"
|
|
1311
|
-
fi
|
|
1312
|
-
fi
|
|
1313
|
-
done < <(gh pr list --state open --json number,headRefName --jq '.[] | [.number, .headRefName] | @tsv' 2>/dev/null || true)
|
|
1314
|
-
fi
|
|
1315
|
-
|
|
1316
1196
|
REVIEWER_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
1317
1197
|
log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${REVIEWER_TOTAL_ELAPSED}s prs=${PRS_NEEDING_WORK_CSV:-none} attempts=${ATTEMPTS_MADE}"
|
|
1318
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}"
|
|
1199
|
+
exit "${EXIT_CODE}"
|