master
sh 144 lines 5.14 KB
Raw
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 }