master
sh 224 lines 7.13 KB
Raw
1 #!/usr/bin/env bash
2 # Common helpers for codacy-audit scripts.
3 #
4 # Token-safe by design: CODACY_TOKEN never reaches the
5 # assistant-visible stdout. Internal helpers that handle
6 # credential bytes have leading-underscore names; public
7 # wrappers read .env internally and emit only the response
8 # body.
9 #
10 # Sourced from the per-action scripts; not executed directly.
11
12 set -euo pipefail
13
14 # ANSI colors. Real ESC bytes via $'...' so the variables work
15 # uniformly with echo -e and printf. Color vars are referenced
16 # by sourcing scripts; shellcheck cannot see that.
17 # shellcheck disable=SC2034
18 CA_RED=$'\033[0;31m'
19 # shellcheck disable=SC2034
20 CA_GREEN=$'\033[0;32m'
21 # shellcheck disable=SC2034
22 CA_YELLOW=$'\033[1;33m'
23 # shellcheck disable=SC2034
24 CA_GRAY=$'\033[0;90m'
25 # shellcheck disable=SC2034
26 CA_CYAN=$'\033[0;36m'
27 # shellcheck disable=SC2034
28 CA_NC=$'\033[0m'
29
30 # Resolve this lib's path (zsh + bash compatible). The query-agent-events
31 # skill uses the same idiom; mirror it here so sourcing from either shell
32 # works without warnings.
33 if [ -n "${ZSH_VERSION-}" ]; then
34 eval '_codacyaudit_lib_self="${(%):-%x}"'
35 elif [ -n "${BASH_VERSION-}" ]; then
36 _codacyaudit_lib_self="${BASH_SOURCE[0]}"
37 else
38 _codacyaudit_lib_self="$0"
39 fi
40 _codacyaudit_lib_dir="$(cd "$(dirname "$_codacyaudit_lib_self")" && pwd)"
41
42 # Locate the repo root from this script's location.
43 codacyaudit_repo_root() {
44 git -C "$_codacyaudit_lib_dir" rev-parse --show-toplevel
45 }
46
47 # Source <repo-root>/.env. CODACY_TOKEN is required for token-gated
48 # endpoints (issue search across master, repo metadata, future write
49 # actions). Read-only PR-issue queries also work anonymously, but
50 # this skill drives them through the token wrapper for consistency
51 # and to exercise the no-leak self-test on every run.
52 codacyaudit_load_env() {
53 local root env
54 root="$(codacyaudit_repo_root)"
55 env="${root}/.env"
56 if [[ ! -f "${env}" || ! -r "${env}" ]]; then
57 echo -e "${CA_RED}[ERROR]${CA_NC} Missing ${env}. See <repo>/.agents/ENV.md for the setup guide." >&2
58 return 1
59 fi
60 set -a
61 # shellcheck disable=SC1090
62 source "${env}"
63 set +a
64
65 : "${CODACY_TOKEN:?CODACY_TOKEN is empty -- see <repo>/.agents/ENV.md to set it.}"
66 : "${CODACY_HOST:=https://api.codacy.com}"
67 : "${CODACY_PROVIDER:=gh}"
68 : "${CODACY_ORG:=netdata}"
69 : "${CODACY_REPO:=netdata}"
70
71 export CODACY_TOKEN CODACY_HOST CODACY_PROVIDER CODACY_ORG CODACY_REPO
72 }
73
74 # Audit artifacts go under .local/audits/codacy/ at the repo root.
75 # .local/ is gitignored -- see AGENTS.md for the convention.
76 codacyaudit_audit_dir() {
77 local root dir
78 root="$(codacyaudit_repo_root)"
79 dir="${root}/.local/audits/codacy"
80 mkdir -p "${dir}"
81 echo "${dir}"
82 }
83
84 # ---------------------------------------------------------------
85 # Token-safe HTTP wrappers.
86 #
87 # The internal helper handles the token bytes. Public wrappers
88 # call it and emit response body only on stdout. We never echo
89 # the curl command line (which would expose the token).
90
91 # _codacyaudit_run METHOD PATH [DATA]
92 # Returns the response body on stdout. HTTP non-2xx -> non-zero.
93 # stderr: minimal status line on error.
94 _codacyaudit_run() {
95 local method="$1"
96 local path="$2"
97 local data="${3:-}"
98 local url="${CODACY_HOST}${path}"
99
100 local -a curl_args=(
101 --silent --show-error --fail-with-body
102 --max-time 60
103 --request "$method"
104 --header "api-token: ${CODACY_TOKEN}"
105 --header 'Accept: application/json'
106 )
107 if [ -n "$data" ]; then
108 curl_args+=(--header 'Content-Type: application/json' --data-raw "$data")
109 fi
110
111 local body
112 if ! body="$(curl "${curl_args[@]}" "$url")"; then
113 echo -e "${CA_RED}[ERROR]${CA_NC} ${method} ${path} failed (see body below)" >&2
114 printf '%s\n' "$body" >&2
115 return 1
116 fi
117 printf '%s' "$body"
118 }
119
120 # Public GET. Stdout is the response body; the token never leaks.
121 codacyaudit_get() {
122 local path="$1"
123 _codacyaudit_run GET "$path"
124 }
125
126 # Public POST.
127 codacyaudit_post() {
128 local path="$1"
129 local data="$2"
130 _codacyaudit_run POST "$path" "$data"
131 }
132
133 # Paginated GET. Walks the v3 cursor protocol and concatenates
134 # `data[]` into a single JSON array on stdout.
135 #
136 # Codacy v3 pagination:
137 # request : ?cursor=<c>&limit=<n>
138 # response: { data: [...], pagination: { cursor, limit, total } }
139 codacyaudit_get_paged() {
140 local path_base="$1"
141 local limit="${2:-1000}"
142 local sep cursor=""
143 local first=true
144 local out='[]'
145
146 while :; do
147 if [[ "$path_base" == *'?'* ]]; then sep='&'; else sep='?'; fi
148 local path="${path_base}${sep}limit=${limit}"
149 if [ -n "$cursor" ]; then
150 path="${path}&cursor=${cursor}"
151 fi
152
153 local resp
154 resp="$(_codacyaudit_run GET "$path")" || return 1
155
156 # Append data[] to accumulator.
157 out="$(printf '%s\n%s' "$out" "$resp" \
158 | jq -sc '.[0] + (.[1].data // [])')"
159
160 cursor="$(printf '%s' "$resp" | jq -r '.pagination.cursor // ""')"
161 [ -z "$cursor" ] && break
162 $first || [ "$first" = "false" ] # keep loop simple
163 first=false
164 done
165
166 printf '%s' "$out"
167 }
168
169 # ---------------------------------------------------------------
170 # Convenience wrappers for the two endpoints this SOW ships.
171
172 # PR issues: GET /v3/analysis/organizations/<p>/<o>/repositories/<r>/pull-requests/<n>/issues
173 codacyaudit_pr_issues() {
174 local pr="$1"
175 if [[ ! "$pr" =~ ^[1-9][0-9]*$ ]]; then
176 echo -e "${CA_RED}[ERROR]${CA_NC} PR number must be a positive integer, got: '${pr}'" >&2
177 return 1
178 fi
179 codacyaudit_get_paged \
180 "/api/v3/analysis/organizations/${CODACY_PROVIDER}/${CODACY_ORG}/repositories/${CODACY_REPO}/pull-requests/${pr}/issues"
181 }
182
183 # Repo overview: GET /v3/organizations/<p>/<o>/repositories/<r>
184 codacyaudit_repo_info() {
185 codacyaudit_get \
186 "/api/v3/organizations/${CODACY_PROVIDER}/${CODACY_ORG}/repositories/${CODACY_REPO}"
187 }
188
189 # ---------------------------------------------------------------
190 # No-token-leak self-test.
191 #
192 # Drives every public wrapper with a sentinel CODACY_TOKEN and
193 # asserts the sentinel never appears on captured stdout. Run
194 # this after editing any wrapper.
195
196 codacyaudit_selftest_no_token_leak() {
197 local sentinel="deadbeef-1234-5678-9abc-def012345678"
198
199 local saved="${CODACY_TOKEN:-}"
200 CODACY_TOKEN="$sentinel"
201 export CODACY_TOKEN
202
203 # Drive each public wrapper. The expected outcome is HTTP
204 # 401 (sentinel is not a real token); we capture stdout and
205 # assert the sentinel does not appear there.
206 local out
207 out="$( {
208 codacyaudit_get "/api/v3/user" 2>/dev/null || true
209 codacyaudit_post "/api/v3/user" '{"noop":true}' 2>/dev/null || true
210 codacyaudit_pr_issues 22423 2>/dev/null || true
211 codacyaudit_repo_info 2>/dev/null || true
212 } )"
213
214 CODACY_TOKEN="$saved"
215 export CODACY_TOKEN
216
217 if printf '%s' "$out" | grep -q "$sentinel"; then
218 echo -e "${CA_RED}FAIL${CA_NC}: sentinel ${sentinel} appeared on captured stdout" >&2
219 return 1
220 fi
221
222 echo -e "${CA_GREEN}PASS${CA_NC}: codacyaudit_selftest_no_token_leak"
223 return 0
224 }