| 1 | #!/usr/bin/env bash |
| 2 | # Common helpers for pr-reviews scripts. |
| 3 | # Sourced from the per-action scripts; not executed directly. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | |
| 7 | # IMPORTANT: define with $'...' so the variables contain real ESC bytes, |
| 8 | # not the literal four-character string "\033". This way both `echo -e |
| 9 | # "${PR_RED}..."` and `printf '%s' "${PR_RED}..."` render correctly -- |
| 10 | # without forcing every printf format string to be the variable itself |
| 11 | # (which trips shellcheck SC2059) or %b (which adds inconsistency). |
| 12 | # |
| 13 | # Color vars are referenced by sourcing scripts; shellcheck cannot see that. |
| 14 | # shellcheck disable=SC2034 |
| 15 | PR_RED=$'\033[0;31m' |
| 16 | # shellcheck disable=SC2034 |
| 17 | PR_GREEN=$'\033[0;32m' |
| 18 | # shellcheck disable=SC2034 |
| 19 | PR_YELLOW=$'\033[1;33m' |
| 20 | # shellcheck disable=SC2034 |
| 21 | PR_GRAY=$'\033[0;90m' |
| 22 | # shellcheck disable=SC2034 |
| 23 | PR_NC=$'\033[0m' |
| 24 | |
| 25 | pr_repo_root() { |
| 26 | git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel |
| 27 | } |
| 28 | |
| 29 | # Owner/repo of the upstream remote (or origin if no upstream). |
| 30 | # Override with PR_REPO_SLUG=owner/repo for cross-repo work. |
| 31 | # Uses bash parameter expansion so repo names containing dots parse correctly. |
| 32 | # Returns empty if the URL is not a github.com remote (this skill only |
| 33 | # supports GitHub). |
| 34 | pr_repo_slug() { |
| 35 | if [[ -n "${PR_REPO_SLUG:-}" ]]; then |
| 36 | # Validate the override too so callers can't smuggle whitespace or |
| 37 | # shell metacharacters in via env. Allowed: owner/repo with |
| 38 | # alphanumerics, dot, underscore, hyphen. |
| 39 | if [[ ! "${PR_REPO_SLUG}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then |
| 40 | echo "" >&2 |
| 41 | return |
| 42 | fi |
| 43 | echo "${PR_REPO_SLUG}" |
| 44 | return |
| 45 | fi |
| 46 | local root url |
| 47 | root="$(pr_repo_root)" |
| 48 | url="$(git -C "${root}" config --get remote.upstream.url 2>/dev/null \ |
| 49 | || git -C "${root}" config --get remote.origin.url)" |
| 50 | # Match github.com only as a host -- a substring match would accept |
| 51 | # `notgithub.com`, `github.com.attacker.example.com`, etc. |
| 52 | # Accepted forms (covering all common gh / git remote outputs): |
| 53 | # git@github.com:owner/repo[.git] (SCP-style ssh) |
| 54 | # ssh://git@github.com/owner/repo[.git] (URL-style ssh) |
| 55 | # https://github.com/owner/repo[.git] (anonymous https) |
| 56 | # https://x-access-token:TOK@github.com/... (credentialed https, gh auth) |
| 57 | if [[ "${url}" != *@github.com:* \ |
| 58 | && "${url}" != *://github.com/* \ |
| 59 | && "${url}" != *@github.com/* ]]; then |
| 60 | echo "" |
| 61 | return |
| 62 | fi |
| 63 | url="${url%.git}" # strip trailing .git |
| 64 | url="${url#*github.com[:/]}" # strip everything up to and including github.com:/ |
| 65 | echo "${url}" |
| 66 | } |
| 67 | |
| 68 | # Audit/state directory for pr-reviews artifacts. |
| 69 | pr_audit_dir() { |
| 70 | local root dir |
| 71 | root="$(pr_repo_root)" |
| 72 | dir="${root}/.local/audits/pr-reviews" |
| 73 | mkdir -p "${dir}" |
| 74 | echo "${dir}" |
| 75 | } |
| 76 | |
| 77 | # Per-PR working directory. |
| 78 | pr_state_dir() { |
| 79 | local pr="${1:?usage: pr_state_dir <pr-number>}" |
| 80 | local dir |
| 81 | dir="$(pr_audit_dir)/pr-${pr}" |
| 82 | mkdir -p "${dir}" |
| 83 | echo "${dir}" |
| 84 | } |
| 85 | |
| 86 | # Verify gh is available and authenticated. Bail loudly otherwise. |
| 87 | pr_require_gh() { |
| 88 | if ! command -v gh >/dev/null; then |
| 89 | echo -e "${PR_RED}[ERROR]${PR_NC} 'gh' CLI not installed. https://cli.github.com/" >&2 |
| 90 | return 1 |
| 91 | fi |
| 92 | if ! gh auth status >/dev/null 2>&1; then |
| 93 | echo -e "${PR_RED}[ERROR]${PR_NC} 'gh' is not authenticated. Run 'gh auth login'." >&2 |
| 94 | return 1 |
| 95 | fi |
| 96 | } |
| 97 | |
| 98 | # Bot logins recognized by the skill. The first regex matches the AI reviewers |
| 99 | # the skill iterates with autonomously; the second matches CI/quality bots |
| 100 | # whose comments are informational (sonar quality gate, etc.). |
| 101 | PR_AI_BOT_RE='^(cubic-dev-ai|copilot|copilot-pull-request-reviewer|github-copilot)\[bot\]$' |
| 102 | PR_INFO_BOT_RE='^(sonarqubecloud|netdata-bot|github-actions|coderabbitai)\[bot\]$' |
| 103 | |
| 104 | # Classify a login -> "ai_bot" | "info_bot" | "human". |
| 105 | pr_classify_author() { |
| 106 | local login="$1" |
| 107 | if [[ "${login}" =~ ${PR_AI_BOT_RE} ]]; then |
| 108 | echo "ai_bot" |
| 109 | elif [[ "${login}" =~ ${PR_INFO_BOT_RE} ]]; then |
| 110 | echo "info_bot" |
| 111 | else |
| 112 | echo "human" |
| 113 | fi |
| 114 | } |
| 115 | |
| 116 | # Pretty timestamp in UTC. |
| 117 | pr_now_utc() { |
| 118 | date -u +%Y-%m-%dT%H:%M:%SZ |
| 119 | } |
| 120 | |
| 121 | # PR numbers are positive integers (GitHub assigns 1+). Reject anything |
| 122 | # else before interpolating into REST paths or `gh` arguments. Even though |
| 123 | # URL interpolation isn't a shell-injection vector, malformed input causes |
| 124 | # confusing API errors that look like permission/auth issues. |
| 125 | pr_require_numeric() { |
| 126 | local n="$1" name="${2:-PR}" |
| 127 | if [[ ! "${n}" =~ ^[1-9][0-9]*$ ]]; then |
| 128 | echo -e "${PR_RED}[ERROR]${PR_NC} ${name} must be a positive integer, got: '${n}'" >&2 |
| 129 | return 1 |
| 130 | fi |
| 131 | } |
| 132 | |
| 133 | # Resolve and validate the repo slug. Returns "owner/repo" on stdout or |
| 134 | # exits non-zero if no slug could be derived (no remotes configured, or |
| 135 | # the URL didn't match github.com). |
| 136 | pr_require_slug() { |
| 137 | local slug |
| 138 | slug="$(pr_repo_slug)" |
| 139 | if [[ -z "${slug}" || "${slug}" != */* ]]; then |
| 140 | echo -e "${PR_RED}[ERROR]${PR_NC} could not derive owner/repo from git remotes (got: '${slug}'). This skill only supports github.com remotes; set PR_REPO_SLUG=owner/repo or fix the remote URL." >&2 |
| 141 | return 1 |
| 142 | fi |
| 143 | printf '%s' "${slug}" |
| 144 | } |