| 1 | #!/usr/bin/env bash |
| 2 | # analyze-local.sh -- run codacy-analysis-cli locally on the working tree. |
| 3 | # |
| 4 | # Mirrors what Codacy CI would run on the same source. Useful BEFORE |
| 5 | # `git push` to catch findings in seconds, not minutes. |
| 6 | # |
| 7 | # Output: a JSON dump under <repo>/.local/audits/codacy/. |
| 8 | # stdout (last line): the dump path. |
| 9 | |
| 10 | set -euo pipefail |
| 11 | |
| 12 | usage() { |
| 13 | cat <<'EOF' |
| 14 | analyze-local.sh [options] |
| 15 | |
| 16 | Runs the official codacy-analysis-cli (https://github.com/codacy/codacy-analysis-cli) |
| 17 | on the current working tree and writes a JSON dump under |
| 18 | <repo>/.local/audits/codacy/. The cli respects the repo's .codacy.yml |
| 19 | exclude_paths. |
| 20 | |
| 21 | Options: |
| 22 | --tool <name> run a single tool (e.g. shellcheck, markdownlint). |
| 23 | Omit to run all tools applicable to changed files. |
| 24 | --directory <path> analyze a subpath (default: <repo-root>) |
| 25 | --format json|sarif output format (default: json) |
| 26 | --output PATH explicit dump path (default: auto under .local/audits/codacy/) |
| 27 | --runner docker|local installer to use (default: auto -- prefer local |
| 28 | binary, fall back to docker, fall back to npm) |
| 29 | -h, --help |
| 30 | |
| 31 | Required tools: docker (default) OR a local codacy-analysis-cli binary. |
| 32 | EOF |
| 33 | } |
| 34 | |
| 35 | TOOL= |
| 36 | SUBDIR= |
| 37 | FORMAT=json |
| 38 | OUTPUT= |
| 39 | RUNNER=auto |
| 40 | |
| 41 | while [ $# -gt 0 ]; do |
| 42 | case "$1" in |
| 43 | --tool) TOOL="$2"; shift 2 ;; |
| 44 | --directory) SUBDIR="$2"; shift 2 ;; |
| 45 | --format) FORMAT="$2"; shift 2 ;; |
| 46 | --output) OUTPUT="$2"; shift 2 ;; |
| 47 | --runner) RUNNER="$2"; shift 2 ;; |
| 48 | -h|--help) usage; exit 0 ;; |
| 49 | *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; |
| 50 | esac |
| 51 | done |
| 52 | |
| 53 | # shellcheck source=SCRIPTDIR/_lib.sh disable=SC1091 |
| 54 | source "$(cd "$(dirname "$0")" && pwd)/_lib.sh" |
| 55 | |
| 56 | # We do not require CODACY_TOKEN here -- the CLI runs without it |
| 57 | # for read-only local analysis. Skip env load to avoid forcing |
| 58 | # users without a token to set one just to run pre-push checks. |
| 59 | |
| 60 | repo_root="$(codacyaudit_repo_root)" |
| 61 | audit_dir="$(codacyaudit_audit_dir)" |
| 62 | |
| 63 | # Resolve target directory. |
| 64 | if [ -z "$SUBDIR" ]; then |
| 65 | SUBDIR="$repo_root" |
| 66 | else |
| 67 | case "$SUBDIR" in |
| 68 | /*) : ;; # absolute |
| 69 | *) SUBDIR="$(cd "$SUBDIR" && pwd)" ;; |
| 70 | esac |
| 71 | fi |
| 72 | |
| 73 | # Resolve output path. |
| 74 | if [ -z "$OUTPUT" ]; then |
| 75 | suffix="" |
| 76 | [ -n "$TOOL" ] && suffix="-${TOOL}" |
| 77 | OUTPUT="${audit_dir}/local${suffix}-$(date -u +%Y%m%dT%H%M%SZ).${FORMAT}" |
| 78 | fi |
| 79 | |
| 80 | # Pick a runner. |
| 81 | if [ "$RUNNER" = "auto" ]; then |
| 82 | if command -v codacy-analysis-cli >/dev/null 2>&1; then |
| 83 | RUNNER=local |
| 84 | elif command -v docker >/dev/null 2>&1; then |
| 85 | RUNNER=docker |
| 86 | else |
| 87 | echo -e "${CA_RED}[ERROR]${CA_NC} neither 'codacy-analysis-cli' nor 'docker' found in PATH." >&2 |
| 88 | echo "Install options:" >&2 |
| 89 | echo " - docker: https://docs.docker.com/get-docker/" >&2 |
| 90 | echo " - cli: https://github.com/codacy/codacy-analysis-cli#install" >&2 |
| 91 | exit 2 |
| 92 | fi |
| 93 | fi |
| 94 | |
| 95 | echo -e "${CA_GRAY}[analyze-local] runner=${RUNNER} format=${FORMAT} dir=${SUBDIR}${CA_NC}" >&2 |
| 96 | |
| 97 | case "$RUNNER" in |
| 98 | local) |
| 99 | # Local binary expects host paths. |
| 100 | local_args=(analyze --directory "$SUBDIR" --format "$FORMAT") |
| 101 | [ -n "$TOOL" ] && local_args+=(--tool "$TOOL") |
| 102 | if ! codacy-analysis-cli "${local_args[@]}" > "$OUTPUT" 2>/dev/null; then |
| 103 | echo -e "${CA_YELLOW}[analyze-local] cli returned non-zero (this is normal when findings are present)${CA_NC}" >&2 |
| 104 | fi |
| 105 | ;; |
| 106 | docker) |
| 107 | # Per https://github.com/codacy/codacy-analysis-cli the CLI |
| 108 | # spawns one child container per tool and needs: |
| 109 | # - the host docker socket (docker-in-docker) |
| 110 | # - CODACY_CODE pointing at the host path of the source |
| 111 | # - the source bind-mounted at the SAME path inside the |
| 112 | # CLI container so child containers can resolve it |
| 113 | cli_args=(analyze --directory "$SUBDIR" --format "$FORMAT") |
| 114 | [ -n "$TOOL" ] && cli_args+=(--tool "$TOOL") |
| 115 | if ! docker run --rm \ |
| 116 | --env CODACY_CODE="$SUBDIR" \ |
| 117 | --volume /var/run/docker.sock:/var/run/docker.sock \ |
| 118 | --volume "$SUBDIR":"$SUBDIR" \ |
| 119 | codacy/codacy-analysis-cli:latest \ |
| 120 | "${cli_args[@]}" > "$OUTPUT" 2>/dev/null; then |
| 121 | echo -e "${CA_YELLOW}[analyze-local] cli returned non-zero (this is normal when findings are present)${CA_NC}" >&2 |
| 122 | fi |
| 123 | ;; |
| 124 | *) |
| 125 | echo -e "${CA_RED}[ERROR]${CA_NC} unknown --runner '${RUNNER}'" >&2 |
| 126 | exit 2 |
| 127 | ;; |
| 128 | esac |
| 129 | |
| 130 | # Sanity check the output. |
| 131 | if [ ! -s "$OUTPUT" ]; then |
| 132 | echo -e "${CA_RED}[ERROR]${CA_NC} empty output at ${OUTPUT}; check the runner above" >&2 |
| 133 | exit 1 |
| 134 | fi |
| 135 | |
| 136 | # Quick summary if format=json. |
| 137 | if [ "$FORMAT" = "json" ] && jq -e . "$OUTPUT" >/dev/null 2>&1; then |
| 138 | n="$(jq 'if type=="array" then length elif type=="object" and has("issues") then (.issues|length) else 0 end' "$OUTPUT")" |
| 139 | echo -e "${CA_GREEN}[analyze-local]${CA_NC} wrote ${n} finding(s) to ${OUTPUT}" >&2 |
| 140 | else |
| 141 | echo -e "${CA_GREEN}[analyze-local]${CA_NC} wrote ${OUTPUT} (${FORMAT} format)" >&2 |
| 142 | fi |
| 143 | |
| 144 | # Last line on stdout: the path. Pipe-friendly. |
| 145 | echo "$OUTPUT" |