github.com/dmaizel/tests@v0.0.0-20210728163746-cae6a2d9cee8/.ci/ci-fast-return.sh (about)

     1  #!/usr/bin/env bash
     2  
     3  # Copyright (c) 2019-2020 Intel Corporation
     4  #
     5  # SPDX-License-Identifier: Apache-2.0
     6  #
     7  # Description: Perform CI 'fast path return' checks.
     8  # Returns: 0 for 'fast return OK', 1 for 'do not fastpath'.
     9  #
    10  # Checks for:
    11  #  - file name patterns from the YAML that will force the CI to run
    12  #  - file name patterns from the YAML to see if we can skip all files in the PR
    13  #  - If potentially skipping, looks for a 'force' label on the PR to force the CI
    14  #    to run anyway
    15  #  - Looks for a force label on the PR to always skip the CI.
    16  
    17  set -e
    18  
    19  [ -n "$DEBUG" ] && set -x
    20  
    21  script_name="${0##*/}"
    22  script_dir_base=".ci"
    23  script_gitname="${script_dir_base}/${script_name}"
    24  
    25  cidir=$(dirname "$0")
    26  source "${cidir}/lib.sh"
    27  source /etc/os-release || source /usr/lib/os-release
    28  
    29  # If no branch specified, compare against the main branch.
    30  # The 'branch' var is required by the get_pr() lib functions.
    31  branch=${branch:-main}
    32  
    33  # The YAML file containing our filename match patterns.
    34  yqfile_rootname="ci-fast-return.yaml"
    35  yqfile="${cidir}/${yqfile_rootname}"
    36  yqfile_gitname="${script_dir_base}/${yqfile_rootname}"
    37  
    38  # The YAML file containing our unit test patterns
    39  unit_yamlfile_rootname="ci-fast-return/test.yaml"
    40  unit_yamlfile="${cidir}/${unit_yamlfile_rootname}"
    41  unit_yamlfile_gitname="${script_dir_base}/${unit_yamlfile_rootname}"
    42  
    43  # Name of the label, that if set on a PR, will force the CI to be run anyway.
    44  force_label="force-ci"
    45  
    46  # Name of the label, that if set on a PR, will force the CI to not be run.
    47  skip_label="force-skip-ci"
    48  
    49  # The list of files to check is held in a global, to make writing the unit test
    50  # code easier - otherwise we would have to pass two multi-entry lists (the list
    51  # of expressions and the list of filenames) to a single test function, which is
    52  # tricky.
    53  filenames=""
    54  
    55  # We have a local info func, as many of our funcs return their answers via stdout,
    56  # and we don't want to either open code a redirect all over the code nor send to
    57  # the standard stdout (would corrupt our return strings) or stderr (some CIs would
    58  # take that as a failure case). So, use another file descriptor, mapped back to
    59  # stdout, that we set up previously.
    60  local_info() {
    61  	msg="$*"
    62  	info "$msg" >&5
    63  }
    64  
    65  # Read our patterns from the YAML file.
    66  # Arguments:
    67  # $1 - the yaml file path
    68  # $2 - the yaml (yq) path to the patterns
    69  #
    70  # Returns on stdout
    71  #  the patterns found, or "" if no patterns (it will translate the yq 'null' return
    72  #  to the empty string)
    73  #
    74  read_yaml() {
    75  	${cidir}/install_yq.sh 1>&5 2>&1
    76  
    77  	res=$(yq read "$1" "$2")
    78  	[ "$res" == "null" ] && res=""
    79  	echo $res
    80  	return 0
    81  }
    82  
    83  # Install jq package
    84  install_jq() {
    85  	local cmd="jq"
    86  	local package="$cmd"
    87  
    88  	command -v "$cmd" &>/dev/null && return 0 || true
    89  
    90  	case "$ID" in
    91  		centos|rhel)
    92  			sudo yum -y install "${package}" 1>&5 2>&1
    93  			;;
    94  		debian|ubuntu)
    95  			sudo apt-get -y install "${package}" 1>&5 2>&1
    96  			;;
    97  		opensuse-*|sles)
    98  			sudo zypper install -y "${package}" 1>&5 2>&1
    99  			;;
   100  		fedora)
   101  			sudo dnf -y install "${package}" 1>&5 2>&1
   102  			;;
   103  	esac
   104  }
   105  
   106  # Check if any files in ${filenames} match the egrep command line expressions
   107  # passed in as arguments. Note, the arguments must be formatted *as passed*
   108  # to egrep. Thus, a single expression can just be passed on its own, but multiple
   109  # expressions must individually be prefixed with '-e' or '--regexp='.
   110  # Returns on stdout any files that match the patterns.
   111  check_matches() {
   112  	egrep $@ <<< "$filenames" || true
   113  }
   114  
   115  # Check if any files in ${filenames} *DO NOT* match the egrep command line expressions
   116  # passed in as arguments. Note, the arguments must be formatted *as passed*
   117  # to egrep. Thus, a single expression can just be passed on its own, but multiple
   118  # expressions must individually be prefixed with '-e' or '--regexp='.
   119  # Returns on stdout any files that *DO NOT* match the patterns.
   120  check_not_matches() {
   121  	egrep -v $@ <<< "$filenames" || true
   122  }
   123  
   124  # Check if the specified label is set on a PR.
   125  #
   126  # Returns on stdout as string:
   127  #
   128  #  0 - Label not found.
   129  #  1 - Label found.
   130  is_label_set() {
   131  	local repo="${1:-}"
   132  	local pr="${2:-}"
   133  	local label="${3:-}"
   134  
   135  	if [ -z "$repo" ]; then
   136  		local_info "BUG: No repo specified"
   137  		echo "0"
   138  		return 0
   139  	fi
   140  
   141  	if [ -z "$pr" ]; then
   142  		local_info "BUG: No PR specified"
   143  		echo "0"
   144  		return 0
   145  	fi
   146  
   147  	if [ -z "$label" ]; then
   148  		local_info "BUG: No label to check specified"
   149  		echo "0"
   150  		return 0
   151  	fi
   152  
   153  	local_info "Checking labels for PR ${repo}/${pr}"
   154  
   155  	local testing_mode=0
   156  
   157  	# If this function was called by a shunit2 function (with the standard
   158  	# function name prefix), we're running as part of the unit tests
   159  	[ "${#FUNCNAME[@]}" -gt 1 ] && grep -q "^test" <<< "${FUNCNAME[1]}" && testing_mode=1
   160  
   161  	if [ "$testing_mode" -eq 1 ]
   162  	then
   163  		# Always fail
   164  		echo "1"
   165  		return 0
   166  	else
   167  		# Pull the label list for the PR
   168  		# Ideally we'd use a github auth token here so we don't get rate limited, but to do that we would
   169  		# have to expose the token into the CI scripts, which is then potentially a security hole.
   170  		local json=$(curl -sL "https://api.github.com/repos/${repo}/issues/${pr}/labels")
   171  
   172  		install_jq
   173  
   174  		# Pull the label list out
   175  		local labels=$(jq '.[].name' <<< $json)
   176  
   177  		# Check if we have the forcing label set
   178  		for x in $labels; do
   179  			# Strip off any surrounding '"'s
   180  			y=$(sed 's/"//g' <<< $x)
   181  
   182  			[ "$y" == "$label" ] && echo "1" && return 0
   183  		done
   184  
   185  		echo "0"
   186  		return 0
   187  	fi
   188  }
   189  
   190  
   191  # Check if any files changed by the PR either force the CI to run or if
   192  # we can skip all the files in the PR.
   193  #
   194  # Returns stdout string:
   195  #  0 - all files are fastpath - yes, we can skip.
   196  #  1 - at least one non-fastpath file found - cannot skip
   197  can_we_skip() {
   198  	# The branch is the baseline - ignore it.
   199  	if [ "$specific_branch" = "true" ]; then
   200  		local_info "Skip baseline branch"
   201  		echo "0"
   202  		return 0
   203  	fi
   204  
   205  	filenames=$(get_pr_changed_file_details_full || true)
   206  	# Strip off the leading status - just grab last column.
   207  	filenames=$(echo "$filenames"|awk '{print $NF}')
   208  
   209  	# no files were changed - I guess we can skip the CI then?
   210  	if [ -z "$filenames" ]; then
   211  		local_info "No files found"
   212  		echo "0"
   213  		return 0
   214  	fi
   215  
   216  	# Check to see if this file or any of its deps changed, as then we should
   217  	# run our own internal unit tests.
   218  	check_for_self_test
   219  
   220  	# Get our common patterns we check against all repos.
   221  	local common_skip=$(read_yaml ${yqfile} common.skip_patterns)
   222  	local common_check=$(read_yaml ${yqfile} common.check_patterns)
   223  
   224  	# Use just the repo name itself. The YAML does not like having the full
   225  	# repo path in it.
   226  	local repo="${kata_repo##*/}"
   227  
   228  	if [ -n "$repo" ]; then
   229  		# Get our repo specific patterns
   230  		local repo_skip=$(read_yaml ${yqfile} ${repo}.skip_patterns)
   231  		local repo_check=$(read_yaml ${yqfile} ${repo}.check_patterns)
   232  	else
   233  		local_info "No repo set, skipping repo specific patterns"
   234  	fi
   235  
   236  	local canskip_exprs=""
   237  	for x in $common_skip $repo_skip; do
   238  		# Build up the string of "-e exp1 -e exp2" arguments to later pass
   239  		# to the egrep based search.
   240  		canskip_exprs+=$(echo "-e $x ")
   241  	done
   242  
   243  	local mustcheck_exprs=""
   244  	for x in $common_check $repo_check; do
   245  		# Build up the string of "-e exp1 -e exp2" arguments to later pass
   246  		# to the egrep based search.
   247  		mustcheck_exprs+=$(echo "-e $x ")
   248  	done
   249  
   250  	local_info "Skip patterns: [$canskip_exprs]"
   251  	local_info "Check patterns: [$mustcheck_exprs]"
   252  
   253  	# do we have any patterns to check?
   254  	if [ -n "$mustcheck_exprs" ]; then
   255  		local need_checking=$(check_matches ${mustcheck_exprs[@]})
   256  	else
   257  		local_info "No force CI check patterns"
   258  		local need_checking=""
   259  	fi
   260  
   261  	# If we have any files that *must* be checked, then immediately return
   262  	if [ -n "$need_checking" ]; then
   263  		# stderr, so it does not get in our return value...
   264  		cat >&2 <<-EOT
   265  		INFO: Cannot fastpath skip CI.
   266  		INFO: Some files present must be CI checked.
   267  		INFO: Files to check are:
   268  
   269  		$need_checking
   270  
   271  EOT
   272  		echo "1"
   273  		return 0
   274  	else
   275  		local_info "No force check files found"
   276  	fi
   277  
   278  	# Now we have checked there are no 'must check' files, we can check to see
   279  	# if all files fall into the 'can skip' patterns.
   280  	# first, do we have any skip patterns to search for?
   281  	if [ -n "$canskip_exprs" ]; then
   282  		local non_skippable=$(check_not_matches ${canskip_exprs[@]} <<< "$filenames")
   283  	else
   284  		local_info "No skip CI check patterns"
   285  		# No patterns, set non-skippable list to all files then...
   286  		local non_skippable="$filenames"
   287  	fi
   288  
   289  	if [ -z "$non_skippable" ]; then
   290  		# stderr, so it does not get in our return value...
   291  		cat >&2 <<-EOT
   292  		INFO: No files to check in CI.
   293  		INFO: Fastpath short circuit returning from CI.
   294  		INFO: Files skipped are:
   295  
   296  		$filenames
   297  
   298  EOT
   299  		echo "0"
   300  		return 0
   301  	else
   302  		cat >&2 <<-EOT
   303  		INFO: Not all files skippable
   304  
   305  		$non_skippable
   306  
   307  EOT
   308  		echo "1"
   309  		return 0
   310  	fi
   311  }
   312  
   313  check_label() {
   314  	local label="${1:-}"
   315  
   316  	if [ -z "$label" ]; then
   317  		local_info "BUG: No label to check specified"
   318  		echo "0"
   319  		return 0
   320  	fi
   321  
   322  	if [ -z "$ghprbGhRepository" ]; then
   323  		local_info "No ghprbGhRepository set, skip label check"
   324  		echo "0"
   325  		return 0
   326  	fi
   327  
   328  	if [ -z "$ghprbPullId" ]; then
   329  		local_info "No ghprbPullId set, skip label check"
   330  		echo "0"
   331  		return 0
   332  	fi
   333  
   334  	repo="$ghprbGhRepository"
   335  	pr="$ghprbPullId"
   336  
   337  	local result=$(is_label_set "$repo" "$pr" "$label")
   338  	if [ "$result" -eq 1 ]; then
   339  		local_info "label '$label' found"
   340  		echo "1"
   341  		return 0
   342  	fi
   343  
   344  	local_info "label '$label' not found"
   345  	echo "0"
   346  	return 0
   347  
   348  }
   349  
   350  # Check if we have the 'magic label' that forces a CI run set on the PR
   351  # Returns on stdout as string:
   352  #  0 - No label found, could skip the CI
   353  #  1 - Label found - should run the CI
   354  check_force_label() {
   355  	local label="$force_label"
   356  
   357  	local result=$(check_label "$label")
   358  	if [ "$result" -eq 1 ]; then
   359  		local_info "Forcing CI"
   360  		echo "1"
   361  		return 0
   362  	fi
   363  
   364  	local_info "No forcing label found"
   365  	echo "0"
   366  	return 0
   367  }
   368  
   369  # Check if we have the 'magic label' that forces a CI run to be skipped
   370  # for a PR.
   371  #
   372  # Returns on stdout as string:
   373  #  0 - No label found.
   374  #  1 - Label found - should skip the CI
   375  check_skip_label() {
   376  	local label="$skip_label"
   377  
   378  	local result=$(check_label "$label")
   379  	if [ "$result" -eq 1 ]; then
   380  		local_info "Skipping CI"
   381  		echo "1"
   382  		return 0
   383  	fi
   384  
   385  	local_info "No CI skip label found"
   386  	echo "0"
   387  	return 0
   388  }
   389  
   390  # Unit tests. Check the YAML file reader functions work as expected.
   391  testYAMLreader() {
   392  	repos=()
   393  	patterns=()
   394  	results=()
   395  
   396  	# Check we can read 'common' patterns, and they act as we expect.
   397  	repos+=("common")
   398  	patterns+=("skip_patterns")
   399  	results+=("^CODEOWNERS$ .*\.md")
   400  
   401  	repos+=("common")
   402  	patterns+=("check_patterns")
   403  	results+=("check1 .*check2$")
   404  
   405  	# Check a pattern with no list does not fault.
   406  	repos+=("common")
   407  	patterns+=("empty_patterns")
   408  	results+=("")
   409  
   410  	# Check we do not fault looking for a pattern that is not defined.
   411  	repos+=("common")
   412  	patterns+=("undefined_patterns")
   413  	results+=("")
   414  
   415  	# Check we can read a named repo.
   416  	# Check single line expression entries work.
   417  	repos+=("documentation")
   418  	patterns+=("doc_single_entry")
   419  	results+=("single_pattern")
   420  
   421  	# Check we can handle multiple complex regexps.
   422  	repos+=("documentation")
   423  	patterns+=("doc_complex_patterns")
   424  	results+=("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$ ^#?([a-f0-9]{6}|[a-f0-9]{3})$")
   425  
   426  	# Check we do not fail if trying to assess a repo that is not defined.
   427  	repos+=("nonrepo")
   428  	patterns+=("nonrepo_pattern")
   429  	results+=("")
   430  
   431  	count=0
   432  	for x in ${repos[@]}; do
   433  		echo " $count: Testing $x:${patterns[$count]} == '${results[$count]}'"
   434  
   435  		res=$(read_yaml "$unit_yamlfile" "${x}.${patterns[$count]}")
   436  		assertEquals "${results[$count]}" "$res"
   437  
   438  		count=$(( count+1 ))
   439  	done
   440  }
   441  
   442  # Unit test: Check the pattern matcher functions work as expected.
   443  testPatternMatcher() {
   444  	filenames="fred
   445  john
   446  mark
   447  doc.md"
   448  
   449  	matches=$(check_matches '.*\.md')
   450  	assertEquals "doc.md" "$matches"
   451  
   452  	# Check it also works with `-e` multiple expressions
   453  	matches=$(check_matches '-e notaname -e .*\.md')
   454  	assertEquals "doc.md" "$matches"
   455  
   456  	matches=$(check_matches 'mark')
   457  	assertEquals "mark" "$matches"
   458  
   459  	matches=$(check_matches '^.*$')
   460  	assertEquals "$filenames" "$matches"
   461  
   462  	matches=$(check_not_matches '.*\.md')
   463  	assertEquals "fred
   464  john
   465  mark" "$matches"
   466  
   467  	# check with multi patterns
   468  	matches=$(check_not_matches '-e .*\.md -e john')
   469  	assertEquals "fred
   470  mark" "$matches"
   471  
   472  
   473  	matches=$(check_not_matches 'mark')
   474  	assertEquals "fred
   475  john
   476  doc.md" "$matches"
   477  
   478  	matches=$(check_not_matches '^.*$')
   479  	assertEquals "" "$matches"
   480  
   481  }
   482  
   483  testIsLabelSet() {
   484  	local result=""
   485  
   486  	result=$(is_label_set "" "" "")
   487  	assertEquals "0" "$result"
   488  
   489  	result=$(is_label_set "repo" "" "")
   490  	assertEquals "0" "$result"
   491  
   492  	result=$(is_label_set "repo" "pr" "")
   493  	assertEquals "0" "$result"
   494  
   495  	result=$(is_label_set "" "pr" "label")
   496  	assertEquals "0" "$result"
   497  
   498  	result=$(is_label_set "repo" "" "label")
   499  	assertEquals "0" "$result"
   500  
   501  	result=$(is_label_set "repo" "pr" "label")
   502  	assertEquals "1" "$result"
   503  }
   504  
   505  testCheckLabel() {
   506  	local result=""
   507  
   508  	result=$(check_label "")
   509  	assertEquals "0" "$result"
   510  
   511  	result=$(unset ghprbGhRepository; check_label "label")
   512  	assertEquals "0" "$result"
   513  
   514  	result=$(unset ghprbPullId; check_label "label")
   515  	assertEquals "0" "$result"
   516  
   517  	result=$(unset ghprbGhRepository ghprbPullId; check_label "label")
   518  	assertEquals "0" "$result"
   519  
   520  	# Pretend label not found
   521  	result=$(is_label_set() { echo "0"; return 0; }; \
   522  	                ghprbGhRepository="repo"; \
   523  	                ghprbPullId=123; \
   524  	                check_label "label")
   525  	assertEquals "0" "$result"
   526  
   527  	# Pretend label found
   528  	result=$(is_label_set() { echo "1"; return 0; }; \
   529  	                ghprbGhRepository="repo"; \
   530  	                ghprbPullId=123; \
   531  	                check_label "label")
   532  	assertEquals "1" "$result"
   533  }
   534  
   535  testCheckForceLabel() {
   536  	local result=""
   537  
   538  	result=$(unset ghprbGhRepository; check_force_label)
   539  	assertEquals "0" "$result"
   540  
   541  	result=$(unset ghprbPullId; check_force_label)
   542  	assertEquals "0" "$result"
   543  
   544  	result=$(unset ghprbGhRepository ghprbPullId; check_force_label)
   545  	assertEquals "0" "$result"
   546  
   547  	result=$(ghprbGhRepository="repo"; \
   548  		ghprbPullId=123; \
   549  		force_label=""; \
   550  		check_force_label)
   551  	assertEquals "0" "$result"
   552  
   553  	# Pretend label not found
   554  	result=$(is_label_set() { echo "0"; return 0; }; \
   555  	                ghprbGhRepository="repo"; \
   556  	                ghprbPullId=123; \
   557  			check_force_label)
   558  	assertEquals "0" "$result"
   559  
   560  	# Pretend label found
   561  	result=$(is_label_set() { echo "1"; return 0; }; \
   562  	                ghprbGhRepository="repo"; \
   563  	                ghprbPullId=123; \
   564  			check_force_label)
   565  	assertEquals "1" "$result"
   566  }
   567  
   568  testCheckSkipLabel() {
   569  	local result=""
   570  
   571  	result=$(unset ghprbGhRepository; check_skip_label)
   572  	assertEquals "0" "$result"
   573  
   574  	result=$(unset ghprbPullId; check_skip_label)
   575  	assertEquals "0" "$result"
   576  
   577  	result=$(unset ghprbGhRepository ghprbPullId; check_skip_label)
   578  	assertEquals "0" "$result"
   579  
   580  	result=$(ghprbGhRepository="repo"; \
   581  		ghprbPullId=123; \
   582  		skip_label=""; \
   583  		check_skip_label)
   584  	assertEquals "0" "$result"
   585  
   586  	# Pretend label not found
   587  	result=$(is_label_set() { echo "0"; return 0; }; \
   588  	                ghprbGhRepository="repo"; \
   589  	                ghprbPullId=123; \
   590  	                check_skip_label "label")
   591  	assertEquals "0" "$result"
   592  
   593  	# Pretend label found
   594  	result=$(is_label_set() { echo "1"; return 0; }; \
   595  	                ghprbGhRepository="repo"; \
   596  	                ghprbPullId=123; \
   597  	                check_skip_label "label")
   598  	assertEquals "1" "$result"
   599  }
   600  
   601  # Check if any of our own files have changed in this PR, and if so, run our own
   602  # unit tests...
   603  check_for_self_test() {
   604  	local ourfiles=""
   605  
   606  	ourfiles+="-e ^${script_gitname}$ "
   607  	ourfiles+="-e ^${yqfile_gitname}$ "
   608  	ourfiles+="-e ^${unit_yamlfile_gitname}$ "
   609  
   610  	res=$(check_matches $ourfiles)
   611  
   612  	if [ -n "$res" ]; then
   613  		local_info "file(s) [$res] modified - self running unit tests"
   614  		# Need to push the unit test output via the stdout mapped file descriptor
   615  		# so we don't corrupt our actual test function return values.
   616  		${cidir}/${script_name} test >&5
   617  	fi
   618  }
   619  
   620  # Run our self tests. Tests are written using the
   621  # github.com/kward/shunit2 library, and are encoded into functions starting
   622  # with the string 'test'.
   623  self_test() {
   624  	local shunit2_path="github.com/kward/shunit2"
   625  	local_info "Running self tests"
   626  
   627  	local_info "Go get unit test framework from ${shunit2_path}"
   628  	go get -d "${shunit2_path}" || true
   629  	local_info "Run the unit tests"
   630  	# Sourcing the `shunit2` file automatically runs the unit tests in this file.
   631  	. "${GOPATH}/src/${shunit2_path}/shunit2"
   632  	# shunit2 call does not return - it exits with its return code.
   633  }
   634  
   635  help()
   636  {
   637  	cat <<EOT
   638  Usage: ${script_name} [test]
   639  
   640  This script will check if the CI system needs to be run, according
   641  to the egrep patterns found in the file ${yqfile}
   642  
   643  Passing the argument 'test' to this script will cause it to only
   644  run its self tests.
   645  EOT
   646  
   647  	exit 0
   648  }
   649  
   650  main() {
   651  
   652  	# Some of our sub-funcs return their results on stdout, but we also want them to be
   653  	# able to log INFO messages. But, we don't want those going to stderr, as that may
   654  	# be seen by some CIs as an actual error. Create another file descriptor, mapped
   655  	# back to stdout, for us to send INFO messages to...
   656  	exec 5>&1
   657  
   658  	if [ "$1" == "test" ]; then
   659  		self_test
   660  		# self_test func does not return
   661  	fi
   662  
   663  	[ $# -gt 0 ] && help
   664  
   665  	local res=$(check_skip_label)
   666  	[ "$res" -eq 1 ] && exit 0
   667  
   668  	info "Checking for any changed files that will prevent CI fastpath return"
   669  	res=$(can_we_skip)
   670  
   671  	# If the file check says we can skip, check the labels to see if we are forcing the
   672  	# CI run anyway.
   673  	[ $res -eq 0 ] && res=$(check_force_label)
   674  	exit $res
   675  }
   676  
   677  main "$@"