mirror of https://github.com/openclaw/openclaw.git
336 lines
7.1 KiB
Bash
336 lines
7.1 KiB
Bash
require_artifact() {
|
|
local path="$1"
|
|
if [ ! -s "$path" ]; then
|
|
echo "Missing required artifact: $path"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
path_is_docsish() {
|
|
local path="$1"
|
|
case "$path" in
|
|
CHANGELOG.md|AGENTS.md|CLAUDE.md|README*.md|docs/*|*.md|*.mdx|mintlify.json|docs.json)
|
|
return 0
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
path_is_testish() {
|
|
local path="$1"
|
|
case "$path" in
|
|
*__tests__/*|*.test.*|*.spec.*|test/*|tests/*)
|
|
return 0
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
path_is_maintainer_workflow_only() {
|
|
local path="$1"
|
|
case "$path" in
|
|
.agents/*|scripts/pr|scripts/pr-*|docs/subagent.md)
|
|
return 0
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
file_list_is_docsish_only() {
|
|
local files="$1"
|
|
local saw_any=false
|
|
local path
|
|
while IFS= read -r path; do
|
|
[ -n "$path" ] || continue
|
|
saw_any=true
|
|
if ! path_is_docsish "$path"; then
|
|
return 1
|
|
fi
|
|
done <<<"$files"
|
|
|
|
[ "$saw_any" = "true" ]
|
|
}
|
|
|
|
changelog_required_for_changed_files() {
|
|
local files="$1"
|
|
local saw_any=false
|
|
local path
|
|
while IFS= read -r path; do
|
|
[ -n "$path" ] || continue
|
|
saw_any=true
|
|
if path_is_docsish "$path" || path_is_testish "$path" || path_is_maintainer_workflow_only "$path"; then
|
|
continue
|
|
fi
|
|
return 0
|
|
done <<<"$files"
|
|
|
|
if [ "$saw_any" = "false" ]; then
|
|
return 1
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
print_review_stdout_summary() {
|
|
require_artifact .local/review.md
|
|
require_artifact .local/review.json
|
|
|
|
local recommendation
|
|
recommendation=$(jq -r '.recommendation // ""' .local/review.json)
|
|
local finding_count
|
|
finding_count=$(jq '[.findings[]?] | length' .local/review.json)
|
|
|
|
echo "review summary:"
|
|
echo "recommendation: $recommendation"
|
|
echo "findings: $finding_count"
|
|
cat .local/review.md
|
|
}
|
|
|
|
print_relevant_log_excerpt() {
|
|
local log_file="$1"
|
|
if [ ! -s "$log_file" ]; then
|
|
echo "(no output captured)"
|
|
return 0
|
|
fi
|
|
|
|
local filtered_log
|
|
filtered_log=$(mktemp)
|
|
if rg -n -i 'error|err|failed|fail|fatal|panic|exception|TypeError|ReferenceError|SyntaxError|ELIFECYCLE|ERR_' "$log_file" >"$filtered_log"; then
|
|
echo "Relevant log lines:"
|
|
tail -n 120 "$filtered_log"
|
|
else
|
|
echo "No focused error markers found; showing last 120 lines:"
|
|
tail -n 120 "$log_file"
|
|
fi
|
|
rm -f "$filtered_log"
|
|
}
|
|
|
|
print_unrelated_gate_failure_guidance() {
|
|
local label="$1"
|
|
case "$label" in
|
|
pnpm\ build*|pnpm\ check*|pnpm\ test*)
|
|
cat <<'EOF_GUIDANCE'
|
|
If this local gate failure already reproduces on latest origin/main and is clearly unrelated to the PR:
|
|
- treat it as baseline repo noise
|
|
- document it explicitly
|
|
- report the scoped verification that validates the PR itself
|
|
- do not use this to ignore plausibly related failures
|
|
EOF_GUIDANCE
|
|
;;
|
|
esac
|
|
}
|
|
|
|
run_quiet_logged() {
|
|
local label="$1"
|
|
local log_file="$2"
|
|
shift 2
|
|
|
|
mkdir -p .local
|
|
if "$@" >"$log_file" 2>&1; then
|
|
echo "$label passed"
|
|
return 0
|
|
fi
|
|
|
|
echo "$label failed (log: $log_file)"
|
|
print_relevant_log_excerpt "$log_file"
|
|
print_unrelated_gate_failure_guidance "$label"
|
|
return 1
|
|
}
|
|
|
|
bootstrap_deps_if_needed() {
|
|
if [ ! -x node_modules/.bin/vitest ]; then
|
|
run_quiet_logged "pnpm install --frozen-lockfile" ".local/bootstrap-install.log" pnpm install --frozen-lockfile
|
|
fi
|
|
}
|
|
|
|
wait_for_pr_head_sha() {
|
|
local pr="$1"
|
|
local expected_sha="$2"
|
|
local max_attempts="${3:-6}"
|
|
local sleep_seconds="${4:-2}"
|
|
|
|
local attempt
|
|
for attempt in $(seq 1 "$max_attempts"); do
|
|
local observed_sha
|
|
observed_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
|
|
if [ "$observed_sha" = "$expected_sha" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ "$attempt" -lt "$max_attempts" ]; then
|
|
sleep "$sleep_seconds"
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
is_author_email_merge_error() {
|
|
local msg="$1"
|
|
printf '%s\n' "$msg" | rg -qi 'author.?email|email.*associated|associated.*email|invalid.*email'
|
|
}
|
|
|
|
merge_author_email_candidates() {
|
|
local reviewer="$1"
|
|
local reviewer_id="$2"
|
|
|
|
local gh_email
|
|
gh_email=$(gh api user --jq '.email // ""' 2>/dev/null || true)
|
|
local git_email
|
|
git_email=$(git config user.email 2>/dev/null || true)
|
|
|
|
printf '%s\n' \
|
|
"$gh_email" \
|
|
"$git_email" \
|
|
"${reviewer_id}+${reviewer}@users.noreply.github.com" \
|
|
"${reviewer}@users.noreply.github.com" | awk 'NF && !seen[$0]++'
|
|
}
|
|
|
|
common_repo_root() {
|
|
if command -v repo_root >/dev/null 2>&1; then
|
|
repo_root
|
|
return
|
|
fi
|
|
|
|
local base_dir
|
|
base_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
git -C "$base_dir" rev-parse --show-toplevel
|
|
}
|
|
|
|
worktree_path_for_branch() {
|
|
local branch="$1"
|
|
local ref="refs/heads/$branch"
|
|
|
|
git worktree list --porcelain | awk -v ref="$ref" '
|
|
/^worktree / {
|
|
worktree=$2
|
|
next
|
|
}
|
|
/^branch / {
|
|
if ($2 == ref) {
|
|
print worktree
|
|
found=1
|
|
}
|
|
}
|
|
END {
|
|
if (!found) {
|
|
exit 1
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
worktree_is_registered() {
|
|
local path="$1"
|
|
git worktree list --porcelain | awk -v target="$path" '
|
|
/^worktree / {
|
|
if ($2 == target) {
|
|
found=1
|
|
}
|
|
}
|
|
END {
|
|
exit found ? 0 : 1
|
|
}
|
|
'
|
|
}
|
|
|
|
resolve_existing_dir_path() {
|
|
local path="$1"
|
|
if [ ! -d "$path" ]; then
|
|
return 1
|
|
fi
|
|
|
|
(
|
|
cd "$path" >/dev/null 2>&1 &&
|
|
pwd -P
|
|
)
|
|
}
|
|
|
|
is_repo_pr_worktree_dir() {
|
|
local path="$1"
|
|
local root
|
|
root=$(common_repo_root)
|
|
|
|
local worktrees_dir="$root/.worktrees"
|
|
local resolved_path
|
|
resolved_path=$(resolve_existing_dir_path "$path" 2>/dev/null || true)
|
|
if [ -z "$resolved_path" ]; then
|
|
return 1
|
|
fi
|
|
|
|
local resolved_worktrees_dir
|
|
resolved_worktrees_dir=$(resolve_existing_dir_path "$worktrees_dir" 2>/dev/null || true)
|
|
if [ -z "$resolved_worktrees_dir" ]; then
|
|
return 1
|
|
fi
|
|
|
|
case "$resolved_path" in
|
|
"$resolved_worktrees_dir"/pr-*)
|
|
return 0
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
remove_worktree_if_present() {
|
|
local path="$1"
|
|
if [ ! -e "$path" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if worktree_is_registered "$path"; then
|
|
git worktree remove "$path" --force >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if [ ! -e "$path" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if worktree_is_registered "$path"; then
|
|
echo "Warning: failed to remove registered worktree $path"
|
|
return 0
|
|
fi
|
|
|
|
if ! is_repo_pr_worktree_dir "$path"; then
|
|
echo "Warning: refusing to trash non-PR-worktree path $path"
|
|
return 0
|
|
fi
|
|
|
|
if command -v trash >/dev/null 2>&1; then
|
|
trash "$path" >/dev/null 2>&1 || {
|
|
echo "Warning: failed to trash orphaned worktree dir $path"
|
|
return 0
|
|
}
|
|
return 0
|
|
fi
|
|
|
|
echo "Warning: orphaned worktree dir remains and trash is unavailable: $path"
|
|
return 0
|
|
}
|
|
|
|
delete_local_branch_if_safe() {
|
|
local branch="$1"
|
|
local ref="refs/heads/$branch"
|
|
|
|
if ! git show-ref --verify --quiet "$ref"; then
|
|
return 0
|
|
fi
|
|
|
|
local branch_worktree=""
|
|
branch_worktree=$(worktree_path_for_branch "$branch" 2>/dev/null || true)
|
|
if [ -n "$branch_worktree" ]; then
|
|
echo "Skipping local branch delete for $branch; checked out in worktree $branch_worktree"
|
|
return 0
|
|
fi
|
|
|
|
if git branch -D "$branch" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if git update-ref -d "$ref" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
echo "Warning: failed to delete local branch $branch"
|
|
return 0
|
|
}
|