From b9c6500aac73aca1a701f6d145c06e3e89fb83c1 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 6 Aug 2026 14:36:52 +0800 Subject: [PATCH] feat(roborev): run full PR panel from agent hook --- config/shared/hooks/roborev-agent.sh | 88 +++++++++++++++++++- spec/roborev_hooks_spec.sh | 118 ++++++++++++++++++++++++++- 2 files changed, 201 insertions(+), 5 deletions(-) diff --git a/config/shared/hooks/roborev-agent.sh b/config/shared/hooks/roborev-agent.sh index 5455df1eb..b305a8c93 100755 --- a/config/shared/hooks/roborev-agent.sh +++ b/config/shared/hooks/roborev-agent.sh @@ -1,11 +1,95 @@ #!/usr/bin/env bash # Feed agent-hook events to the RoboRev daemon running on this host. +# Start the full PR panel independently so the normal quick CI comment remains +# fast while the complete panel produces a second comment for the same PR. +# shellcheck disable=SC2016 set -euo pipefail -ROBOREV_BIN="${HOME}/.local/bin/roborev" +ROBOREV_BIN="${ROBOREV_BIN:-${HOME}/.local/bin/roborev}" +GH_BIN="${GH_BIN:-gh}" +GIT_BIN="${GIT_BIN:-git}" +JQ_BIN="${JQ_BIN:-jq}" +SERVER_ADDR="${ROBOREV_SERVER_ADDR:-127.0.0.1:7373}" +STATE_DIR="${ROBOREV_AGENT_FULL_REVIEW_STATE_DIR:-${HOME}/.roborev/agent-full-reviews}" + +run_full_pr_review() { + local cwd="$1" repo_path repo_key config_file pr_json pr_number pr_state + local base_oid head_oid current_head merge_base lock_dir review_output comment_body + + repo_path="$($GIT_BIN -C "$cwd" rev-parse --show-toplevel 2>/dev/null)" || return 0 + repo_key="${repo_path//\//_}" + config_file="${repo_path}/.roborev.toml" + [ -f "$config_file" ] || return 0 + grep -Eq '^\[review\.panels\.full\][[:space:]]*$' "$config_file" || return 0 + pr_json="$(cd "$repo_path" && "$GH_BIN" pr view \ + --json number,state,baseRefOid,headRefOid 2>/dev/null)" || return 0 + pr_number="$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.number // empty')" + pr_state="$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.state // empty')" + base_oid="$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.baseRefOid // empty')" + head_oid="$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.headRefOid // empty')" + current_head="$($GIT_BIN -C "$repo_path" rev-parse HEAD)" + [[ $pr_number =~ ^[0-9]+$ ]] || return 0 + [ "$pr_state" = "OPEN" ] && [ "$current_head" = "$head_oid" ] || return 0 + [[ $base_oid =~ ^[0-9a-fA-F]{40}$ && $head_oid =~ ^[0-9a-fA-F]{40}$ ]] || return 0 + merge_base="$($GIT_BIN -C "$repo_path" merge-base "$base_oid" "$head_oid" 2>/dev/null)" || return 0 + [[ $merge_base =~ ^[0-9a-fA-F]{40}$ ]] || return 0 + + lock_dir="${STATE_DIR}/${repo_key}/${head_oid}.enqueued" + mkdir -p "$(dirname "$lock_dir")" + mkdir "$lock_dir" 2>/dev/null || return 0 + + if ! review_output="$($ROBOREV_BIN --server "$SERVER_ADDR" review --wait --quiet \ + --repo "$repo_path" --sha "${merge_base}..${head_oid}" --panel full)"; then + rmdir "$lock_dir" + return 1 + fi + + pr_json="$(cd "$repo_path" && "$GH_BIN" pr view "$pr_number" \ + --json state,headRefOid 2>/dev/null)" || return 0 + [ "$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.state // empty')" = "OPEN" ] || return 0 + [ "$(printf '%s' "$pr_json" | "$JQ_BIN" -r '.headRefOid // empty')" = "$head_oid" ] || return 0 + + comment_body="$(printf '## RoboRev Full Panel (`%s`)\n\n%s' "${head_oid:0:12}" "$review_output")" + if ! (cd "$repo_path" && "$GH_BIN" pr comment "$pr_number" --body "$comment_body") >/dev/null; then + rmdir "$lock_dir" + return 1 + fi +} + +launch_full_pr_review() { + local cwd="$1" log_file + if [ "${ROBOREV_AGENT_HOOK_SYNC:-0}" = "1" ]; then + run_full_pr_review "$cwd" + return + fi + mkdir -p "$STATE_DIR" + log_file="${STATE_DIR}/full-review.log" + nohup "$0" --full-pr-review "$cwd" >"$log_file" 2>&1 & +} + +if [ "${1:-}" = "--full-pr-review" ]; then + run_full_pr_review "${2:-}" + exit 0 +fi if [ ! -x "$ROBOREV_BIN" ]; then exit 0 fi -exec "$ROBOREV_BIN" --server "127.0.0.1:7373" agent-hook run +INPUT="$(cat)" +HOOK_OUTPUT="$(printf '%s' "$INPUT" | "$ROBOREV_BIN" --server "$SERVER_ADDR" agent-hook run)" || true + +if command -v "$JQ_BIN" >/dev/null 2>&1; then + EVENT_NAME="$(printf '%s' "$INPUT" | "$JQ_BIN" -r '.hook_event_name // .hookEventName // empty' 2>/dev/null || true)" + CWD="$(printf '%s' "$INPUT" | "$JQ_BIN" -r '.cwd // empty' 2>/dev/null || true)" + TOOL_COMMAND="$(printf '%s' "$INPUT" | "$JQ_BIN" -r '.tool_input.command // .tool_input.cmd // .toolInput.command // empty' 2>/dev/null || true)" + if [ -n "$CWD" ]; then + case "$EVENT_NAME:$TOOL_COMMAND" in + Stop:* | PostToolUse:*git\ push* | PostToolUse:*gh\ pr\ create*) + launch_full_pr_review "$CWD" || true + ;; + esac + fi +fi + +printf '%s\n' "$HOOK_OUTPUT" diff --git a/spec/roborev_hooks_spec.sh b/spec/roborev_hooks_spec.sh index af78d65e4..d972c03ca 100644 --- a/spec/roborev_hooks_spec.sh +++ b/spec/roborev_hooks_spec.sh @@ -1,9 +1,121 @@ #!/usr/bin/env bash +# shellcheck disable=SC2016,SC2329 Describe 'config/shared/hooks/roborev-agent.sh' -It 'uses the local RoboRev daemon' -When run grep -F '127.0.0.1:7373' "$PWD/config/shared/hooks/roborev-agent.sh" -The output should include '127.0.0.1:7373' +SCRIPT="$PWD/config/shared/hooks/roborev-agent.sh" + +setup_hook() { + TEMP_HOME=$(mktemp -d) + mkdir -p "$TEMP_HOME/bin" "$TEMP_HOME/repo" + CALLS="$TEMP_HOME/calls" + : >"$CALLS" + printf '[review.panels.full]\nmembers = ["opencode"]\n' >"$TEMP_HOME/repo/.roborev.toml" + + cat >"$TEMP_HOME/bin/roborev" <<'SH' +#!/usr/bin/env bash +if [[ " $* " == *" agent-hook run "* ]]; then + cat >/dev/null + printf '{"continue":true}\n' +elif [[ " $* " == *" review "* ]]; then + printf 'roborev %s\n' "$*" >>"$CALLS" + [ "${ROBOREV_REVIEW_FAIL:-0}" = "1" ] && exit 1 + printf 'No issues found.\n' +fi +SH + cat >"$TEMP_HOME/bin/git" <<'SH' +#!/usr/bin/env bash +if [[ " $* " == *" --show-toplevel "* ]]; then + printf '%s\n' "$REPO_PATH" +elif [[ " $* " == *" merge-base "* ]]; then + printf '%s\n' "$BASE_OID" +else + printf '%s\n' "$HEAD_OID" +fi +SH + cat >"$TEMP_HOME/bin/gh" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = "pr" ]; then + if [ "${2:-}" = "view" ]; then + printf '{"number":42,"state":"OPEN","baseRefOid":"%s","headRefOid":"%s"}\n' "$BASE_OID" "$HEAD_OID" + else + printf 'gh %s\n' "$*" >>"$CALLS" + fi +fi +SH + chmod +x "$TEMP_HOME/bin/roborev" "$TEMP_HOME/bin/git" "$TEMP_HOME/bin/gh" + + export CALLS + export REPO_PATH="$TEMP_HOME/repo" + export BASE_OID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + export HEAD_OID=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + export ROBOREV_BIN="$TEMP_HOME/bin/roborev" + export GIT_BIN="$TEMP_HOME/bin/git" + export GH_BIN="$TEMP_HOME/bin/gh" + export JQ_BIN=jq + export ROBOREV_AGENT_HOOK_SYNC=1 + export ROBOREV_AGENT_FULL_REVIEW_STATE_DIR="$TEMP_HOME/state" +} + +cleanup_hook() { + rm -rf "$TEMP_HOME" +} + +Before 'setup_hook' +After 'cleanup_hook' + +It 'uses the local RoboRev daemon and preserves hook output' +Data '{"session_id":"s1","hook_event_name":"PostToolUse","cwd":"/tmp","tool_input":{"command":"true"}}' +When run bash "$SCRIPT" +The status should be success +The output should eq '{"continue":true}' +End + +It 'starts one full panel after a pushed PR and posts a separate comment' +Data '{"session_id":"s1","hook_event_name":"PostToolUse","cwd":"__REPO__","tool_input":{"command":"git push origin HEAD"}}' +When run bash -c 'sed "s|__REPO__|$REPO_PATH|" | bash "$1"; printf "calls:\n"; cat "$CALLS"' _ "$SCRIPT" +The status should be success +The output should include '{"continue":true}' +The output should include '--sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --panel full' +The output should include 'gh pr comment 42 --body ## RoboRev Full Panel' +End + +It 'does not comment when the full panel fails' +export ROBOREV_REVIEW_FAIL=1 +Data '{"session_id":"s1","hook_event_name":"Stop","cwd":"__REPO__"}' +When run bash -c 'sed "s|__REPO__|$REPO_PATH|" | bash "$1" >/dev/null || true; grep -c -- "gh pr comment" "$CALLS" || true' _ "$SCRIPT" +The status should be success +The output should include '0' +End + +It 'preserves hook output when the full panel fails' +export ROBOREV_REVIEW_FAIL=1 +Data '{"session_id":"s1","hook_event_name":"Stop","cwd":"__REPO__"}' +When run bash -c 'sed "s|__REPO__|$REPO_PATH|" | bash "$1"' _ "$SCRIPT" +The status should be success +The output should eq '{"continue":true}' +End + +It 'runs the production background path' +unset ROBOREV_AGENT_HOOK_SYNC +Data '{"session_id":"s1","hook_event_name":"Stop","cwd":"__REPO__"}' +When run bash -c 'sed "s|__REPO__|$REPO_PATH|" | bash "$1" >/dev/null; for _ in {1..50}; do grep -q "gh pr comment" "$CALLS" && break; sleep 0.05; done; grep -c "gh pr comment" "$CALLS"' _ "$SCRIPT" +The status should be success +The output should eq '1' +End + +It 'deduplicates repeated Stop events for the same PR head' +When run bash -c 'payload="{\"session_id\":\"s1\",\"hook_event_name\":\"Stop\",\"cwd\":\"$REPO_PATH\"}"; printf "%s" "$payload" | bash "$1" >/dev/null; printf "%s" "$payload" | bash "$1"; grep -c -- "--panel full" "$CALLS"' _ "$SCRIPT" +The status should be success +The output should include '{"continue":true}' +The output should include '1' +End + +It 'skips repositories that do not define the full panel' +rm -f "$REPO_PATH/.roborev.toml" +Data '{"session_id":"s1","hook_event_name":"Stop","cwd":"__REPO__"}' +When run bash -c 'sed "s|__REPO__|$REPO_PATH|" | bash "$1" >/dev/null; grep -c -- "--panel full" "$CALLS" || true' _ "$SCRIPT" +The status should be success +The output should eq '0' End Describe 'config/factory/activate-settings.sh'