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 "$@"