Repository navigation
Expand file tree
/
Copy pathci-lint-diff
More file actions
executable file
·325 lines (306 loc) · 12.3 KB
/
Copy pathci-lint-diff
File metadata and controls
executable file
·325 lines (306 loc) · 12.3 KB
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
#!/bin/bash
# Given a file of warnings, reports only those that are in the diff for the
# current pull request. Works for Azure Pipelines, CircleCI, GitHub Actions,
# and Travis CI. Exit status is non-zero if any warnings are output.
# Example use:
#
# PLUME_SCRIPTS="/tmp/${USER:-$(id -un)}/plume-scripts"
# if [ -d "$PLUME_SCRIPTS" ] ; then
# git -C "$PLUME_SCRIPTS" pull -q > /dev/null 2>&1
# else
# git clone --depth=1 -q https://github.com/plume-lib/plume-scripts.git "$PLUME_SCRIPTS"
# fi
# (command-that-issues-warnings > /tmp/warnings.txt 2>&1) || true
# "$PLUME_SCRIPTS"/ci-lint-diff /tmp/warnings.txt
#
# If you get an "Empty diff" error, here are two possible reasons:
# * Your branch is identical to the base/upstream branch. In this case, the pull
# request is pointless.
# * Your clone is shallow and does not contain all the commits. You can fix that
# by pulling upstream.
#
# Requires the `jq` program to be installed, when used in Azure Pipelines jobs.
# Requires the scripts `set-git-range` and `set-ci-org-and-branch` in the
# same directory as this script.
# This script is a thin wrapper around `lint-diff.py`.
SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
SCRIPT_NAME="$(basename -- "$0")"
DEBUG=""
CI_LINT_DIFF_ERROR=""
# Prints the environment, one variable per line, with the value of each
# variable whose name suggests a credential replaced. A plain `env` dump would
# write secrets into the CI log, which is often readable by anyone who can see
# the job. Matching by name pattern rather than by an allowlist of names also
# covers secrets that this script does not know about; GitHub Actions, for
# example, defines $ACTIONS_RUNTIME_TOKEN in every job and does not mask it.
# `set-ci-org-and-branch` and `set-git-range` each contain a copy of the
# redaction code, because each of them unsets its functions before returning.
print_redacted_environment() {
# Print the environment with awk's ENVIRON array rather than with
# `env -0 | sort -z`: the `-0` and `-z` options are GNU extensions that
# BusyBox and BSD lack, so on those platforms this dump would be empty.
# Each variable becomes exactly one line, with newlines in its value shown as
# "\n"; a multi-line value would otherwise scramble the sort and slip its
# continuation lines past the redaction below. The escaping is done by
# concatenation because a backslash in `gsub`'s replacement text is not
# portable: BusyBox awk and GNU awk interpret it differently.
awk '
BEGIN {
for (name in ENVIRON) {
parts = split(ENVIRON[name], part, /\n/)
value = part[1]
for (i = 2; i <= parts; i++) {
value = value "\\n" part[i]
}
print name "=" value
}
}
' | sort | awk '
{
name = $0
sub(/=.*/, "", name)
if (toupper(name) ~ /TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|APIKEY|API_KEY|PRIVATE_KEY|_PAT$|^PAT$/) {
print name "=<redacted>"
next
}
print
}
'
}
if [ "$#" -eq 0 ]; then
echo "Usage: ${SCRIPT_NAME} [--debug] WARNINGS-FILE" >&2
exit 2
fi
if [ "$1" = "--debug" ]; then
DEBUG="--debug"
shift
fi
if [ "$#" -ne 1 ]; then
echo "Usage: ${SCRIPT_NAME} [--debug] WARNINGS-FILE" >&2
exit 2
fi
if ! [ -f "$1" ]; then
echo "File $1 does not exist" >&2
exit 2
fi
WARNINGSFILE=$1
# If there are no warnings, there is nothing to report, so don't compute the
# diff.
if [ ! -s "$WARNINGSFILE" ]; then
# Report on standard error, because `lint-diff.py` writes the warnings that
# this script exists to produce on standard output, and a client may test
# whether that output is empty.
echo "${SCRIPT_NAME}: $WARNINGSFILE is empty; nothing to report." >&2
exit 0
fi
set -e
if [ "$DEBUG" = "--debug" ]; then
print_redacted_environment
git log --graph | head -n 10000
fi
# `set-git-range` sets CI_COMMIT_RANGE and the other CI_* variables. Source
# it rather than `eval`ing the output of `git-changes`: `eval` discards the
# exit status, and it mangles a value that contains a shell metacharacter (a
# branch name may contain one).
# Set the variables that configure it, rather than letting the environment do
# so, so that this script behaves the same way in every job.
PLUME_SCRIPTS="${SCRIPT_DIR}"
CI_VERBOSE=""
CI_DEBUG=""
# "set -e" needs no adjustment here: a command in an `if` condition does not
# trigger it. That lets a failure be handled here rather than aborting this
# script, which would skip the diagnostics below.
# shellcheck source=set-git-range
if ! . "${SCRIPT_DIR}"/set-git-range; then
echo "set-git-range failed; it reported the reason on standard error, above." >&2
exit 2
fi
if [ -z "$CI_COMMIT_RANGE" ]; then
CI_LINT_DIFF_ERROR+="CI_COMMIT_RANGE is not set.\n"
else
if [ -z "$CI_COMMIT_RANGE_START" ]; then
CI_LINT_DIFF_ERROR+="CI_COMMIT_RANGE_START is not set.\n"
else
# Discard git's own "fatal:" message; the message below is friendlier.
if ! git cat-file -e "${CI_COMMIT_RANGE_START}^{commit}" 2> /dev/null; then
CI_LINT_DIFF_ERROR+="Commit CI_COMMIT_RANGE_START=$CI_COMMIT_RANGE_START not found; clone more deeply.\n"
fi
fi
if [ -z "$CI_COMMIT_RANGE_END" ]; then
CI_LINT_DIFF_ERROR+="CI_COMMIT_RANGE_END is not set.\n"
else
# Discard git's own "fatal:" message; the message below is friendlier.
if ! git cat-file -e "${CI_COMMIT_RANGE_END}^{commit}" 2> /dev/null; then
CI_LINT_DIFF_ERROR+="Commit CI_COMMIT_RANGE_END=$CI_COMMIT_RANGE_END not found; clone more deeply.\n"
fi
fi
fi
set +e
diff_file=$(mktemp "${TMPDIR:-/tmp}/ci-lint-diff-XXXXXX.txt") || exit 2
diff_err_file=""
cleanup() {
if [ "$DEBUG" = "--debug" ]; then
echo "${SCRIPT_NAME}: retaining $diff_file $diff_err_file" >&2
else
rm -f "$diff_file" "$diff_err_file"
fi
}
# Install the trap before creating the second file, so that the first file is
# removed even if the second `mktemp` fails.
trap cleanup EXIT
diff_err_file=$(mktemp "${TMPDIR:-/tmp}/ci-lint-diff-err-XXXXXX.txt") || exit 2
echo "${SCRIPT_NAME} running: git diff $CI_COMMIT_RANGE > $diff_file" >&2
# Keep stderr out of $diff_file, which is parsed as a diff by `lint-diff.py`.
git --no-pager diff --exit-code "$CI_COMMIT_RANGE" > "$diff_file" 2> "$diff_err_file"
git_status=$?
if [ $git_status -gt 1 ]; then
CI_LINT_DIFF_ERROR+="git diff exited with status $git_status\n"
echo "start of git diff stderr:"
cat "$diff_err_file"
echo "end of git diff stderr:"
git --no-pager branch -a
git --no-pager diff "$CI_COMMIT_RANGE"
elif [ ! -r "$diff_file" ]; then
CI_LINT_DIFF_ERROR+="$diff_file does not exist\n"
fi
# Only diagnose an empty diff if `git diff` actually succeeded. Otherwise
# the failure above is the real problem, and this advice is misleading.
if [ $git_status -le 1 ] && [ ! -s "$diff_file" ]; then
CI_LINT_DIFF_ERROR+="ERROR: Empty diff for $CI_COMMIT_RANGE.
Try pulling base branch (often main or master) into compare branch (often your feature branch).
Other causes of an empty diff include:
* The commit range is a no-op, such as a commit and its revert.
* HEAD is a merge commit and the merge took all its changes from its second argument.
* Your compare branch does not differ from your base branch.
* Your clone is shallow, so set-git-range could not determine the start of the
commit range (HEAD^1 does not exist) and used HEAD, making the range empty.
Clone more deeply.
"
fi
if [ -n "$CI_LINT_DIFF_ERROR" ]; then
sleep 1
echo
echo "*****"
echo "${SCRIPT_NAME} error:"
printf '%b\n' "$CI_LINT_DIFF_ERROR"
echo "*****"
echo
echo "${SCRIPT_NAME} will print diagnostic information, then exit."
echo
echo "---- values from set-git-range ----"
echo "CI_ORGANIZATION=$CI_ORGANIZATION"
echo "CI_BRANCH=$CI_BRANCH"
echo "CI_COMMIT_RANGE_START=$CI_COMMIT_RANGE_START"
echo "CI_COMMIT_RANGE_END=$CI_COMMIT_RANGE_END"
echo "CI_COMMIT_RANGE=$CI_COMMIT_RANGE"
echo "---- end of values from set-git-range ----"
echo
echo "---- stderr of git diff $CI_COMMIT_RANGE ----"
cat "$diff_err_file"
echo "---- end of stderr of git diff $CI_COMMIT_RANGE ----"
echo
# `set-git-range` under CI_DEBUG dumps the environment, so run it only under
# --debug, because CI logs are often public and the environment may contain
# access tokens besides the GITHUB_PAT and GH_TOKEN that it redacts.
# Run it in a separate process, so that recomputing the values cannot change
# the ones that the diagnostics above report. Unlike a repeated invocation of
# the old `git-changes`, this one is meaningful, because `set-git-range`
# computes its values from scratch rather than reusing what the environment
# holds. It must be sourced, so run a shell that sources it.
# Its diagnostics go to standard error; redirect them so that they appear
# between the two markers rather than somewhere above.
if [ "$DEBUG" = "--debug" ]; then
echo "---------------- start of ${SCRIPT_DIR}/set-git-range with CI_DEBUG"
CI_DEBUG="--debug" PLUME_SCRIPTS="${SCRIPT_DIR}" \
sh -c '. "${PLUME_SCRIPTS}"/set-git-range' 2>&1 || true
echo "---------------- end of ${SCRIPT_DIR}/set-git-range with CI_DEBUG"
else
echo "Re-run with --debug to also print the output of ${SCRIPT_DIR}/set-git-range with CI_DEBUG."
fi
echo
echo git rev-parse \$CI_COMMIT_RANGE_START
echo git rev-parse "$CI_COMMIT_RANGE_START"
git rev-parse "$CI_COMMIT_RANGE_START"
echo git rev-parse \$CI_COMMIT_RANGE_END
echo git rev-parse "$CI_COMMIT_RANGE_END"
git rev-parse "$CI_COMMIT_RANGE_END"
echo git rev-parse remotes/origin/\$CI_COMMIT_RANGE_START
echo git rev-parse "remotes/origin/$CI_COMMIT_RANGE_START"
git rev-parse "remotes/origin/$CI_COMMIT_RANGE_START" || true
echo git rev-parse remotes/origin/\$CI_COMMIT_RANGE_END
echo git rev-parse "remotes/origin/$CI_COMMIT_RANGE_END"
git rev-parse "remotes/origin/$CI_COMMIT_RANGE_END" || true
echo
# This fetch mutates the clone, to determine whether the clone is too shallow.
echo git fetch
git fetch
echo
echo git rev-parse "$CI_COMMIT_RANGE_START"
git rev-parse "$CI_COMMIT_RANGE_START"
echo git rev-parse "$CI_COMMIT_RANGE_END"
git rev-parse "$CI_COMMIT_RANGE_END"
echo git rev-parse "remotes/origin/$CI_COMMIT_RANGE_START"
git rev-parse "remotes/origin/$CI_COMMIT_RANGE_START" || true
echo git rev-parse "remotes/origin/$CI_COMMIT_RANGE_END"
git rev-parse "remotes/origin/$CI_COMMIT_RANGE_END" || true
echo
echo "Branches:"
git --no-pager branch -a
echo
echo "git remote show origin"
git remote show origin
echo
echo "Re-execution of diff command:"
echo git --no-pager diff --exit-code "$CI_COMMIT_RANGE"
git --no-pager diff --exit-code "$CI_COMMIT_RANGE"
echo
echo "Second re-execution of diff command:"
# On failure, skip just this section rather than exiting, so that the
# remaining diagnostics (and the repeated error message) are still printed.
diff_retry_file=$(mktemp "${TMPDIR:-/tmp}/ci-lint-diff-retry-XXXXXX.txt") || diff_retry_file=""
if [ -n "$diff_retry_file" ]; then
echo "git --no-pager diff $CI_COMMIT_RANGE > $diff_retry_file; cat $diff_retry_file"
git --no-pager diff "$CI_COMMIT_RANGE" > "$diff_retry_file"
cat "$diff_retry_file"
rm -f "$diff_retry_file"
else
echo "Skipped: could not create a temporary file in ${TMPDIR:-/tmp}."
fi
echo
echo "Endpoints of commit range ($CI_COMMIT_RANGE_START and $CI_COMMIT_RANGE_END):"
git --no-pager show "$CI_COMMIT_RANGE_START"
git --no-pager show "$CI_COMMIT_RANGE_END"
echo
echo "Current directory:"
pwd
echo
echo "Listing of ${TMPDIR:-/tmp}:"
ls -l "${TMPDIR:-/tmp}"
echo
# The environment is printed only under --debug, and even then with
# credentials redacted, because CI logs are often public and the environment
# may contain access tokens (such as GITHUB_PAT).
if [ "$DEBUG" = "--debug" ]; then
echo "Environment:"
print_redacted_environment
else
echo "Re-run with --debug to also print the environment."
fi
echo
echo "Now, repeating the error; it appears before and after diagnostics."
echo "*****"
echo "${SCRIPT_NAME} error:"
printf '%b\n' "$CI_LINT_DIFF_ERROR"
echo "*****"
exit 2
fi
if [ "$DEBUG" = "--debug" ]; then
echo "warnings file $WARNINGSFILE:"
cat "$WARNINGSFILE"
echo "end of warnings file $WARNINGSFILE."
echo "$diff_file file:"
cat "$diff_file"
echo "end of $diff_file file."
fi
set -e
"${SCRIPT_DIR}"/lint-diff.py $DEBUG --guess-strip "$diff_file" "$WARNINGSFILE"