| 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 | } |