#!/usr/bin/env bash # # Robin installer โ€” adds the Robin code-review workflow to the current repo. # # From the root of the Git repository you want Robin to review: # curl -fsSL https://robinreview.dev/install.sh | bash # # It keeps one canonical .github/workflows/robin.yml. Historical Robin workflow # variants are archived outside the workflows directory so they cannot trigger, # then the companion skill is installed or updated globally. No sudo required. # Override the action ref with ROBIN_REF=v1. # set -euo pipefail REF="${ROBIN_REF:-main}" WORKFLOW_PATH=".github/workflows/robin.yml" WORKFLOW_DIR=".github/workflows" ARCHIVE_DIR=".github/robin-workflow-archive" info() { printf '\033[0;32m๐Ÿน %s\033[0m\n' "$1"; } warn() { printf '\033[0;33m๐Ÿน %s\033[0m\n' "$1"; } die() { printf '\033[0;31m๐Ÿน %s\033[0m\n' "$1" >&2; exit 1; } git rev-parse --is-inside-work-tree >/dev/null 2>&1 \ || die "Not a git repository. cd into your repo and run this again." cd "$(git rev-parse --show-toplevel)" is_robin_workflow() { grep -Eiq '^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*antongulin/(robin|universal-code-reviewer)(/\.github/workflows/review\.ya?ml)?@[^[:space:]#]+' "$1" } is_robin_source_repository() { [ -f package.json ] && grep -Eq '"name"[[:space:]]*:[[:space:]]*"robin-review"' package.json \ && [ -f action.yml ] \ && [ -f skills/robin/SKILL.md ] \ && [ -f .github/workflows/self-test.yml ] } # Treat CRLF and LF workflow copies as equivalent across Git configurations. files_equal() { cmp -s <(sed $'s/\r$//' "$1") <(sed $'s/\r$//' "$2") } # Preserve an existing modern Robin ref unless ROBIN_REF explicitly overrides it. if [ -z "${ROBIN_REF+x}" ]; then ref_source="" if [ -f "$WORKFLOW_PATH" ] && is_robin_workflow "$WORKFLOW_PATH"; then ref_source="$WORKFLOW_PATH" elif [ -d "$WORKFLOW_DIR" ]; then while IFS= read -r candidate; do if is_robin_workflow "$candidate"; then if [ -n "$ref_source" ]; then ref_source="" warn "Multiple Robin workflows found; using default ref ($REF)." break fi ref_source="$candidate" fi done < <(find "$WORKFLOW_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print) fi if [ -n "$ref_source" ]; then # Preserve refs only from modern Robin workflows. Legacy Universal Code Reviewer # refs (including obsolete v0 tags) intentionally migrate to the current default. existing_ref="$(sed -nE 's|^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*antongulin/robin/\.github/workflows/review\.ya?ml@([A-Za-z0-9._/-]+).*|\2|p' "$ref_source" | head -n 1)" if [ -n "$existing_ref" ] && [ "$existing_ref" != "v0" ]; then REF="$existing_ref"; fi fi fi case "$REF" in *[!A-Za-z0-9._/-]*|'') die "Invalid ROBIN_REF: $REF" ;; esac tmp_workflow="$(mktemp)" trap 'rm -f "$tmp_workflow"' EXIT # Quoted heredoc: keeps ${{ secrets.* }} literal for GitHub Actions. cat > "$tmp_workflow" <<'YAML' # Generated by robin-review. Re-run `npx robin-review` to check for updates. name: Robin on: pull_request: types: [opened, reopened, ready_for_review] issue_comment: types: [created] permissions: actions: read contents: read pull-requests: write jobs: review: uses: antongulin/robin/.github/workflows/review.yml@__REF__ secrets: LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} LLM_MODEL: ${{ secrets.LLM_MODEL }} YAML tmp_rendered="$(mktemp)" trap 'rm -f "$tmp_workflow" "$tmp_rendered"' EXIT sed "s|@__REF__|@${REF}|" "$tmp_workflow" > "$tmp_rendered" archive_workflow() { local source_path="$1" base_name destination suffix=1 mkdir -p "$ARCHIVE_DIR" base_name="$(basename "$source_path")" destination="$ARCHIVE_DIR/${base_name}.disabled" while [ -f "$destination" ] && ! files_equal "$source_path" "$destination"; do destination="$ARCHIVE_DIR/${base_name}.${suffix}.disabled" suffix=$((suffix + 1)) done if [ -f "$destination" ]; then rm -f "$source_path" info "Removed $source_path (identical copy already at $destination)." else mv "$source_path" "$destination" info "Archived $source_path โ†’ $destination (cannot trigger in this location)." fi ARCHIVED_WORKFLOW_COUNT=$((ARCHIVED_WORKFLOW_COUNT + 1)) } WORKFLOW_CHANGED=0 ARCHIVED_WORKFLOW_COUNT=0 SOURCE_REPOSITORY=0 if is_robin_source_repository; then SOURCE_REPOSITORY=1 warn "Robin source repository detected โ€” leaving its self-test workflows untouched." if [ -f "$WORKFLOW_PATH" ] && is_robin_workflow "$WORKFLOW_PATH"; then warn "$WORKFLOW_PATH is a redundant consumer workflow; remove it instead of committing it here." fi else if [ -f "$WORKFLOW_PATH" ] && ! is_robin_workflow "$WORKFLOW_PATH"; then die "$WORKFLOW_PATH exists but is not a Robin workflow. Move or rename it, then run again." fi canonical_current=0 if [ -f "$WORKFLOW_PATH" ] && files_equal "$WORKFLOW_PATH" "$tmp_rendered"; then canonical_current=1; fi if [ -d "$WORKFLOW_DIR" ]; then while IFS= read -r candidate; do if is_robin_workflow "$candidate"; then if [ "$candidate" = "$WORKFLOW_PATH" ] && [ "$canonical_current" -eq 1 ]; then continue; fi archive_workflow "$candidate" WORKFLOW_CHANGED=1 fi done < <(find "$WORKFLOW_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print) fi if [ "$canonical_current" -eq 1 ]; then info "$WORKFLOW_PATH is already current." else mkdir -p "$WORKFLOW_DIR" mv "$tmp_rendered" "$WORKFLOW_PATH" info "Created canonical $WORKFLOW_PATH (ref: ${REF})." WORKFLOW_CHANGED=1 fi fi # Install the companion chat skill so any coding agent can automatically drive Robin's # review loop during ordinary PR work. Uses the cross-platform skills CLI to # install for every detected agent, globally. Best-effort โ€” the GitHub Action works # without it. Set ROBIN_SKILL=0 to skip. install_skill() { [ "${ROBIN_SKILL:-1}" = "0" ] && return 0 if ! command -v npx >/dev/null 2>&1; then warn "Skipping companion skill โ€” Node.js/npx not found." warn "Install it later: npx skills add https://github.com/antongulin/robin --all --global" return 0 fi info "Installing the Robin chat skill for all coding agentsโ€ฆ" # --agent '*' = every supported agent, --global = user-level (available everywhere). if npx -y skills add https://github.com/antongulin/robin --skill robin --agent '*' --global --yes >/dev/null 2>&1; then info "Robin chat skill installed (all agents). It activates automatically for PR work." else warn "Couldn't auto-install the skill. Run: npx skills add https://github.com/antongulin/robin --all --global" fi } install_skill if [ "$SOURCE_REPOSITORY" -eq 1 ]; then cat <