-
Notifications
You must be signed in to change notification settings - Fork 3
/
entrypoint.sh
executable file
·562 lines (469 loc) · 17.4 KB
/
entrypoint.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
#!/usr/bin/env bash
set -o errtrace
set -o nounset
set -o errexit
set -o pipefail
# shellcheck disable=SC2034
{
declare -r ERROR='\033[0;31m'
declare -r WARNING='\033[0;33m'
declare -r INFO='\033[0m'
declare -r DEBUG='\033[0m'
declare -r DEFAULT='\033[0m'
}
function feedback() {
color="${1:-DEFAULT}"
case "${1}" in
ERROR)
echo >&2 -e "${!color}${1}: ${2}${DEFAULT}"
;;
WARNING)
echo >&2 -e "${!color}${1}: ${2}${DEFAULT}"
;;
*)
if [[ "${1^^}" != "DEBUG" ]]; then
echo -e "${!color}${1}: ${2}${DEFAULT}"
elif [[ "${1^^}" == "DEBUG" && -n "${LOG_LEVEL:+x}" && "${LOG_LEVEL^^}" == "DEBUG" ]]; then
echo -e "${!color}${1}: ${2}${DEFAULT}"
fi
;;
esac
}
function setup_environment() {
# Set the preferred shell behavior
shopt -s globstar
# Set the default branch
export DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
# Set workspace to /goat for local runs
export DEFAULT_WORKSPACE="${DEFAULT_WORKSPACE:-/goat}"
# Set default values for autofix
export AUTO_FIX="true"
export CURRENT_LINT_ROUND=1
# Create variables for the various dictionary file paths
export GLOBAL_DICTIONARY="${GLOBAL_DICTIONARY:-/etc/opt/goat/seiso_global_dictionary.txt}"
export LINTER_CONFIG="${LINTER_CONFIG:-/etc/opt/goat/linters.json}"
# Identify the correct relative path to use
if [[ -d "${DEFAULT_WORKSPACE}/.git" ]]; then
# Local / default use
RELATIVE_PATH="${DEFAULT_WORKSPACE}"
elif [[ -n "${GITHUB_WORKSPACE:+x}" ]]; then
# GitHub Actions
RELATIVE_PATH="${GITHUB_WORKSPACE}"
elif [[ -n "${BITBUCKET_CLONE_DIR:+x}" ]]; then
# Bitbucket Pipelines
RELATIVE_PATH="${BITBUCKET_CLONE_DIR}"
elif [[ -d "/src/.git" ]]; then
# Pre-commit
RELATIVE_PATH="/src"
else
feedback ERROR "Unable to identify the right relative path to find the repo dictionary"
exit 1
fi
if [[ -r "${RELATIVE_PATH}/.github/etc/dictionary.txt" ]]; then
# GitHub
export REPO_DICTIONARY="${RELATIVE_PATH}/.github/etc/dictionary.txt"
elif [[ -r "${RELATIVE_PATH}/dictionary.txt" ]]; then
export REPO_DICTIONARY="${RELATIVE_PATH}/dictionary.txt"
else
feedback ERROR "Unable to find the dictionary file"
exit 1
fi
if [[ -n ${JSCPD_CONFIG:+x} ]]; then
feedback WARNING "JSCPD_CONFIG is set; not auto ignoring the goat submodule..."
else
# This should override the ignore in the config file and is primarily needed so that we can pass in the correct relative path while not excluding all of the
# goat on the goat itself (i.e. we are avoiding **/goat/** in the config file)
export INTERNAL_JSCPD_CONFIG="--config /etc/opt/goat/.jscpd.json --ignore \"**/.github/workflows/**,${RELATIVE_PATH}/goat/**\""
export JSCPD_CONFIG="${INTERNAL_JSCPD_CONFIG}"
feedback DEBUG "JSCPD_CONFIG was dynamically set to ${JSCPD_CONFIG}"
fi
#############
# IMPORTANT: If you are changing any INPUT_ variables here, make sure to also update:
# - README.md
# - Task/**/Taskfile.yml (vars)
# - action.yml
#############
if [[ ${INPUT_AUTO_FIX:-true} == "false" ]]; then
# Let INPUT_AUTO_FIX override the autofix value. This allows for disabling autofix locally.
AUTO_FIX="false"
fi
if [[ ${INPUT_DISABLE_MYPY:-} == "true" ]]; then
export VALIDATE_PYTHON_MYPY="false"
fi
if [[ -n ${INPUT_EXCLUDE:+x} ]]; then
export FILTER_REGEX_EXCLUDE="${INPUT_EXCLUDE}"
fi
# Default to info
INPUT_LOG_LEVEL=${INPUT_LOG_LEVEL:-INFO}
if [[ ${INPUT_LOG_LEVEL^^} =~ ^(ERROR|WARNING|INFO|DEBUG)$ ]]; then
export LOG_LEVEL="${INPUT_LOG_LEVEL}"
export ACTIONS_RUNNER_DEBUG="true"
fi
if [[ ${INPUT_DISABLE_CODE_REVIEW:-false} == "true" ]]; then
export DISABLE_CODE_REVIEW="true"
fi
feedback DEBUG "Looking in ${REPO_DICTIONARY} for the dictionary.txt"
feedback DEBUG "INPUT_AUTO_FIX is ${INPUT_AUTO_FIX:-not set}"
feedback DEBUG "AUTO_FIX is ${AUTO_FIX:-not set}"
feedback DEBUG "INPUT_DISABLE_MYPY is ${INPUT_DISABLE_MYPY:-not set}"
feedback DEBUG "VALIDATE_PYTHON_MYPY is ${VALIDATE_PYTHON_MYPY:-not set}"
feedback DEBUG "INPUT_EXCLUDE is ${INPUT_EXCLUDE:-not set}"
feedback DEBUG "FILTER_REGEX_EXCLUDE is ${FILTER_REGEX_EXCLUDE:-not set}"
feedback DEBUG "INPUT_LOG_LEVEL is ${INPUT_LOG_LEVEL:-not set}"
feedback DEBUG "LOG_LEVEL is ${LOG_LEVEL:-not set}"
feedback DEBUG "INPUT_DISABLE_CODE_REVIEW is ${INPUT_DISABLE_CODE_REVIEW:-not set}"
feedback DEBUG "DISABLE_CODE_REVIEW is ${DISABLE_CODE_REVIEW:-not set}"
# Sets up pyenv so that any linters ran via pipenv run can have an arbitrary python version
# More details in https://github.com/pyenv/pyenv/tree/7b713a88c40f39139e1df4ed0ceb764f73767dac#advanced-configuration
eval "$(pyenv init -)"
declare -a linter_failures
declare -a linter_successes
declare -a linter_skipped
}
function check_environment() {
# Check PRs for the main branch
local wrong_destination_branch="false"
if [[ -n ${BITBUCKET_PR_DESTINATION_BRANCH:+x} && "${BITBUCKET_PR_DESTINATION_BRANCH}" != "main" ]]; then
wrong_destination_branch="true"
elif [[ ${GITHUB_ACTIONS:-false} == "true" && -n ${GITHUB_BASE_REF:+x} ]]; then
mainline="${GITHUB_BASE_REF##*/}"
if [[ ${mainline} != "main" ]]; then
wrong_destination_branch="true"
fi
fi
if [[ "${wrong_destination_branch}" == "true" ]]; then
feedback WARNING "Base branch name is not main"
fi
# Ensure there is a repo dictionary
if [[ ! -r "${REPO_DICTIONARY}" ]]; then
feedback ERROR "Unable to read a repo dictionary at ${REPO_DICTIONARY}; does it exist?"
exit 1
fi
# Ensure dictionaries don't have overlap
overlap=$(comm -12 <(sort "${GLOBAL_DICTIONARY}" | tr '[:upper:]' '[:lower:]') \
<(sort "${REPO_DICTIONARY}" | tr '[:upper:]' '[:lower:]'))
if [[ "${overlap}" ]]; then
feedback WARNING "The following words are already in the global dictionary:
${overlap}"
feedback ERROR "Overlap was detected in the per-repo and global dictionaries"
exit 1
fi
# Ensure dictionaries are sorted
if ! sort -c "${REPO_DICTIONARY}" 2>/dev/null; then
feedback ERROR "The repo dictionary must be sorted"
exit 1
fi
}
function detect_kubernetes_file() {
# Seach for k8s-specific strings in files to determine which files to pass to kubeconform for linting
# Here a return of 0 indicates the function did not find a string match and exits with a success code,
# and 1 indicates the strings were found. This more aligns with a boolean-like response.
local file="$1"
if grep -q -v 'kustomize.config.k8s.io' "${file}" &&
grep -q -v "tekton" "${file}" &&
grep -q -E '^apiVersion:' "${file}" &&
grep -q -E '^kind:' "${file}"; then
return 1
fi
return 0
}
function detect_cloudformation_file() {
# Search for AWS Cloud Formation-related strings in files to determine which files to pass to cfn-lint
# Here a return of 0 indicates the function did not find a string match and exits with a success code,
# and 1 indicates the string was found. This more aligns with a boolean-like response.
local file="$1"
# Searches for a string specific to AWS CF templates
if grep -q 'AWSTemplateFormatVersion' "${file}" >/dev/null; then
return 1
fi
# Search for AWS, Alexa Skills Kit, or Custom Cloud Formation syntax within the file
if grep -q -E '(AWS|Alexa|Custom)::' "${file}" >/dev/null; then
return 1
fi
return 0
}
function get_files_matching_filetype() {
local filenames=("${@:3}")
local linter_name="$2"
local f_type="$1"
declare -a matching_files=()
for file in "${filenames[@]}"; do
filename=$(basename "${file}")
if [[ $filename == *"$f_type" ]]; then
if [ "$linter_name" == "cfn-lint" ]; then
if detect_cloudformation_file "${file}"; then
continue
fi
fi
if [ "$linter_name" == "kubeconform" ]; then
if detect_kubernetes_file "${file}"; then
continue
fi
fi
if [ "$linter_name" == "actionlint" ]; then
if [[ -n "${GITHUB_WORKSPACE:+x}" ]]; then
local action_path="${GITHUB_WORKSPACE:-.}/.github/workflows/"
if [[ "${file}" != "${action_path}"* ]]; then
continue
fi
else
# Skip actionlint if not running in a GitHub Action
continue
fi
fi
matching_files+=("${file}")
fi
done
echo "${matching_files[@]}"
}
function load_filetype_array() {
local val=$1
declare -a types
val=$(echo "$val" | jq -r '.[]')
while IFS= read -r filetype; do
types+=("$filetype")
done <<<"$val"
echo "${types[@]}"
}
function has_autofix() {
local lint_name=$1
while IFS= read -r line; do
name=$(echo "$line" | jq -r ".name")
if [[ "$name" == "$lint_name" ]]; then
if echo "$line" | jq -e '.autofix' > /dev/null; then
# If linter has an autofix exit true bit
return 1
else
return 0
fi
fi
done < <(jq -c '.[]' "${LINTER_CONFIG}")
}
function lint_files() {
# Turn the received string back into an object
local -n linter_array="$1"
local filetypes_to_lint=("${@:2}")
local linter_args="${linter_array[args]}"
local files_to_lint=""
local env_var_name="${linter_array[env]}"
if [ "${CURRENT_LINT_ROUND}" -eq 2 ] && ! has_autofix "${linter_array[name]}"; then
linter_args="${linter_array[autofix]}"
fi
if [[ -v "${env_var_name}" ]]; then
linter_args="${!env_var_name}"
if [[ "${env_var_name}" == "JSCPD_CONFIG" && "${linter_args}" == "${INTERNAL_JSCPD_CONFIG}" ]]; then
feedback DEBUG "Hit special case for JSCPD_CONFIG internal customization to allow dynamic ignores at runtime; not printing a warning"
else
feedback WARNING "The linter runtime for ${linter_array[name]} has been customized, which might have unwanted side effects. Use with caution."
fi
fi
for type in "${filetypes_to_lint[@]}"; do
if [[ $type == "all" ]]; then
cmd="${linter_array[name]} $linter_args $(printf '%q ' "${included[@]}")"
eval "$cmd" >>"${linter_array[logfile]}" 2>&1
return
fi
files_to_lint="$(get_files_matching_filetype "$type" "${linter_array[name]}" "${included[@]}")"
if [ "${#files_to_lint}" -eq 0 ]; then
return
fi
for file in "${files_to_lint[@]}"; do
if [[ "${linter_array[executor]+x}" ]]; then
cmd="${linter_array[executor]} ${linter_array[name]} $linter_args ${file}"
else
cmd="${linter_array[name]} $linter_args ${file}"
fi
echo "$cmd" >>"${linter_array[logfile]}"
eval "$cmd" >>"${linter_array[logfile]}" 2>&1
done
done
}
function seiso_lint() {
echo -e "\nRunning Seiso Linter\n--------------------------\n"
local skip_safe="false"
if [[ -n ${GITHUB_WORKSPACE:+x} ]]; then
# GitHub Actions
local safe_directory="${GITHUB_WORKSPACE}"
elif [[ -n ${BITBUCKET_CLONE_DIR:+x} ]]; then
# Bitbucket Pipelines
local safe_directory="${BITBUCKET_CLONE_DIR}"
else
feedback WARNING "Unable to identify a directory to set as safe, skipping that step..."
local skip_safe="true"
fi
if [[ "${skip_safe}" == "false" ]]; then
feedback INFO "Setting ${safe_directory} as safe directory"
git config --global --add safe.directory "${safe_directory}"
fi
# When run in a pipeline, move per-repo configurations into the right location at runtime so the goat finds them, overwriting the defaults.
# This will handle hidden and non-hidden files, as well as sym links
if [[ -d "${GITHUB_WORKSPACE:-.}/.github/linters" ]]; then
cp -p "${GITHUB_WORKSPACE:-.}/.github/linters/"* "${GITHUB_WORKSPACE:-.}/.github/linters/".* /etc/opt/goat/ 2>/dev/null || true
elif [[ -d "${BITBUCKET_CLONE_DIR:-.}/linters" ]]; then
cp -p "${BITBUCKET_CLONE_DIR:-.}/linters/"* "${BITBUCKET_CLONE_DIR:-.}/linters/".* /etc/opt/goat/ 2>/dev/null || true
fi
excluded=()
included=()
while read -r file; do
if [[ -n ${FILTER_REGEX_EXCLUDE:+x} && "${file}" =~ ${FILTER_REGEX_EXCLUDE} ]]; then
feedback INFO "${file} matched the exclusion regex of ${FILTER_REGEX_EXCLUDE}"
excluded+=("${file}")
continue
else
feedback DEBUG "${file} didn't match the exclusion regex of ${FILTER_REGEX_EXCLUDE:-not set}"
fi
included+=("${file}")
done < <(find "${RELATIVE_PATH}" \( -path "${RELATIVE_PATH}/.git" -prune \) -o \( -type f -print \))
declare -A pids
while read -r line; do
unset linter
declare -A linter
unset linter_filetypes
declare -a linter_filetypes
while IFS='=' read -r key value; do
if [[ $key == "filetype" ]]; then
linter_filetypes=("$(load_filetype_array "$value")")
continue
fi
value=$(echo "$value" | tr -d "'" | tr -d '"')
linter["$key"]=$value
done < <(echo "$line" | jq -r 'to_entries|map("\(.key)=\(.value|tojson)")|.[]')
linter[logfile]="/opt/goat/log/${linter[name]}.log"
if [[ -v VALIDATE_PYTHON_MYPY && "${VALIDATE_PYTHON_MYPY,,}" == "false" && "${linter[name]}" == "mypy" ]]; then
feedback WARNING "mypy linter has been disabled"
linter_skipped+=("${linter[name]}")
continue
fi
feedback INFO "===============================" >>"${linter[logfile]}"
feedback INFO "Running linter: ${linter[name]}"
feedback INFO "${linter[name]^^}" >>"${linter[logfile]}"
# The string "linter" gets dereferenced back into a variable on the receiving end
lint_files linter "${linter_filetypes[@]}" &
pid=$!
pids["$pid"]="${linter[name]}"
echo "-------------------------------" >>"${linter[logfile]}"
done < <(jq -c '.[]' "${LINTER_CONFIG}")
for p in "${!pids[@]}"; do
set +e
wait "$p"
exit_code=$?
set -e
if [ "$exit_code" -gt 0 ]; then
cat "/opt/goat/log/${pids[$p]}.log"
linter_failures+=("${pids[$p]}")
else
linter_successes+=("${pids[$p]}")
fi
done
}
function rerun_lint() {
local failed_linter="$1"
unset rerun_linter
declare -A rerun_linter
unset rerun_filetypes
declare -a rerun_filetypes
while IFS= read -r line; do
name=$(echo "$line" | jq -r ".name")
if [[ "$name" == "$failed_linter" ]]; then
feedback INFO "Linter $failed_linter found errors and has a fix option. Attempting fix."
while IFS='=' read -r key value; do
if [[ $key == "filetype" ]]; then
rerun_filetypes=("$(load_filetype_array "$value")")
continue
fi
value=$(echo "$value" | tr -d "'" | tr -d '"')
rerun_linter["$key"]=$value
done < <(echo "$line" | jq -r 'to_entries|map("\(.key)=\(.value|tojson)")|.[]')
rerun_linter[logfile]="/opt/goat/log/rerun_${rerun_linter[name]}.log"
fi
done < <(jq -c '.[]' "${LINTER_CONFIG}")
echo "===============================" >>"${rerun_linter[logfile]}"
echo "Re-running linter: ${rerun_linter[name]}"
echo "${rerun_linter[name]^^}" >>"${rerun_linter[logfile]}"
# The string "rerun_linter" gets dereferenced back into a variable on the receiving end
lint_files rerun_linter "${rerun_filetypes[@]}"
echo "-------------------------------" >>"${rerun_linter[logfile]}"
}
function initiate_code_review() {
if [[ -n ${GITHUB_ACTIONS:+x} && ${DISABLE_CODE_REVIEW:-false} != "true" ]]; then
# Run the Python script
python /opt/goat/bin/code_review.py
else
feedback DEBUG "Code review is disabled or not running in GitHub Actions"
fi
}
start=$(date +%s)
setup_environment
check_environment
seiso_lint
initiate_code_review
end=$(date +%s)
runtime=$((end - start))
echo -e "\nScanned ${#included[@]} files in ${runtime} seconds"
echo -e "Excluded ${#excluded[@]} files\n"
if [ -n "${linter_successes[*]}" ]; then
for success in "${linter_successes[@]}"; do
feedback INFO "$success completed successfully"
done
fi
declare -a rerun_linter_failures
declare -a rerun_linter_successes
failed_lint="false"
if [ -n "${linter_failures[*]}" ]; then
if [[ ${AUTO_FIX:-true} == "true" ]]; then
CURRENT_LINT_ROUND=2
declare -A rerun_pids
for failure in "${linter_failures[@]}"; do
if ! has_autofix "$failure"; then
rerun_lint "$failure" &
rerun_pid=$!
rerun_pids["$rerun_pid"]="$failure"
continue
fi
feedback ERROR "$failure found errors"
failed_lint="true"
done
for p in "${!rerun_pids[@]}"; do
set +e
wait "$p"
exit_code=$?
set -e
if [ "$exit_code" -gt 0 ]; then
rerun_linter_failures+=("${rerun_pids[$p]}")
else
rerun_linter_successes+=("${rerun_pids[$p]}")
fi
cat "/opt/goat/log/rerun_${rerun_pids[$p]}.log"
done
else
for failure in "${linter_failures[@]}"; do
feedback ERROR "$failure found errors"
done
failed_lint="true"
fi
fi
if [[ -n "${rerun_linter_successes[*]}" && -n $(git status -s) ]]; then
for success in "${rerun_linter_successes[@]}"; do
if [[ ${CI:-false} == "true" ]]; then
feedback ERROR "$success detected issues but they can be **automatically fixed**; run 'task lint' locally, commit, and push."
continue
fi
feedback ERROR "Autofix of $success errors completed successfully. Check it out and commit the changes."
done
failed_lint="true"
fi
if [[ -n "${rerun_linter_failures[*]}" ]]; then
for failure in "${rerun_linter_failures[@]}"; do
if [[ ${CI:-false} == "true" ]]; then
feedback ERROR "Attempts to autofix $failure errors were unsuccessful. Please correct manually."
continue
fi
feedback ERROR "Attempts to autofix $failure errors were unsuccessful. Your local directory might be dirty with partial fixes."
done
failed_lint="true"
fi
if [[ "$failed_lint" == "true" ]]; then
feedback ERROR "Linters found errors"
exit 1
fi
feedback INFO "Linters found no errors."