| #!/usr/bin/env bash |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # no public APIs here |
| # SHELLDOC-IGNORE |
| |
| # This bug system provides github integration |
| |
| add_bugsystem github |
| |
| # personalities can override the following settings: |
| |
| # Web interface URL. |
| GITHUB_BASE_URL="https://github.com" |
| |
| # API interface URL. |
| GITHUB_API_URL="https://api.github.com" |
| |
| # user/repo |
| GITHUB_REPO="" |
| |
| # user settings |
| GITHUB_TOKEN="${GITHUB_TOKEN-}" |
| GITHUB_ISSUE="" |
| GITHUB_USE_EMOJI_VOTE=false |
| GITHUB_STATUS_RECOVERY_COUNTER=1 |
| GITHUB_STATUS_RECOVER_TOOL=false |
| GITHUB_WRITE_COMMENT=false |
| GITHUB_ANNOTATION_LIMIT=50 |
| GITHUB_STATUS_USE_HTMLREPORT=${GITHUB_STATUS_USE_HTMLREPORT:-true} |
| declare -a GITHUB_AUTH |
| |
| # private globals... |
| GITHUB_BRIDGED=false |
| |
| function github_usage |
| { |
| yetus_add_option "--github-annotation-limit=<int>" "The maximum number of annotations to send to GitHub (default: '${GITHUB_ANNOTATION_LIMIT}')" |
| yetus_add_option "--github-api-url=<url>" "The URL of the API for github (default: '${GITHUB_API_URL}')" |
| yetus_add_option "--github-base-url=<url>" "The URL of the github server (default:'${GITHUB_BASE_URL}')" |
| yetus_add_option "--github-repo=<repo>" "github repo to use (default:'${GITHUB_REPO}')" |
| yetus_add_option "--github-token=<token>" "The token to use to read/write to github" |
| yetus_add_option "--github-write-comment" "Write final report as github comment (default: '${GITHUB_WRITE_COMMENT}')" |
| yetus_add_option "--github-use-emoji-vote" "Whether to use emoji to represent the vote result on github [default: ${GITHUB_USE_EMOJI_VOTE}]" |
| yetus_add_option "--github-status-use-htmlreport=<bool>" "Use htmlout for Github Status 'Details' link [default: ${GITHUB_STATUS_USE_HTMLREPORT}]" |
| } |
| |
| function github_parse_args |
| { |
| declare i |
| |
| for i in "$@"; do |
| case ${i} in |
| --github-annotation-limit=*) |
| delete_parameter "${i}" |
| GITHUB_ANNOTATION_LIMIT=${i#*=} |
| ;; |
| --github-api-url=*) |
| delete_parameter "${i}" |
| GITHUB_API_URL=${i#*=} |
| ;; |
| --github-base-url=*) |
| delete_parameter "${i}" |
| GITHUB_BASE_URL=${i#*=} |
| ;; |
| --github-repo=*) |
| delete_parameter "${i}" |
| GITHUB_REPO=${i#*=} |
| ;; |
| --github-token=*) |
| delete_parameter "${i}" |
| GITHUB_TOKEN=${i#*=} |
| ;; |
| --github-write-comment) |
| delete_parameter "${i}" |
| GITHUB_WRITE_COMMENT=true |
| ;; |
| --github-use-emoji-vote) |
| delete_parameter "${i}" |
| GITHUB_USE_EMOJI_VOTE=true |
| ;; |
| --github-status-use-htmlreport=*) |
| delete_parameter "${i}" |
| GITHUB_STATUS_USE_HTMLREPORT=${i#*=} |
| ;; |
| esac |
| done |
| } |
| |
| |
| ## @description this gets called when JIRA thinks this |
| ## @description issue is just a pointer to github |
| ## @description WARNING: Called from JIRA plugin! |
| function github_jira_bridge |
| { |
| declare jsonloc=$1 |
| declare patchout=$2 |
| declare diffout=$3 |
| declare urlfromjira |
| |
| # shellcheck disable=SC2016 |
| urlfromjira=$("${AWK}" "match(\$0,\"${GITHUB_BASE_URL}/[^ ]*patch[ &\\\"]\"){url=substr(\$0,RSTART,RLENGTH-1)} |
| END{if (url) print url}" "${jsonloc}" ) |
| if [[ -z $urlfromjira ]]; then |
| # This is currently the expected path, as github pull requests are not common |
| return 1 |
| fi |
| |
| # we use this to prevent loops later on |
| GITHUB_BRIDGED=true |
| yetus_debug "github_jira_bridge: Checking url ${urlfromjira}" |
| github_breakup_url "${urlfromjira}" |
| github_locate_patch GH:"${GITHUB_ISSUE}" "${patchout}" "${diffout}" |
| } |
| |
| ## @description given a URL, break it up into github plugin globals |
| ## @description this will *override* any personality or yetus defaults |
| ## @description WARNING: Called from various robots! |
| ## @param url |
| function github_breakup_url |
| { |
| declare url=$1 |
| declare count |
| declare pos1 |
| declare pos2 |
| |
| if [[ "${url}" =~ \@ ]]; then |
| url=${url//:/\/} |
| url=${url//git@/https://} |
| fi |
| |
| url=${url%\.git} |
| |
| count=${url//[^\/]} |
| count=${#count} |
| if [[ ${count} -gt 4 ]]; then |
| ((pos2=count-3)) |
| ((pos1=pos2)) |
| |
| GITHUB_BASE_URL=$(echo "${url}" | cut "-f1-${pos2}" -d/) |
| |
| ((pos1=pos1+1)) |
| ((pos2=pos1+1)) |
| |
| GITHUB_REPO=$(echo "${url}" | cut "-f${pos1}-${pos2}" -d/) |
| |
| ((pos1=pos2+2)) |
| unset pos2 |
| |
| GITHUB_ISSUE=$(echo "${url}" | cut "-f${pos1}-${pos2}" -d/ | cut -f1 -d.) |
| else |
| GITHUB_BASE_URL=$(echo "${url}" | cut -f1-3 -d/) |
| GITHUB_REPO=$(echo "${url}" | cut -f4- -d/) |
| fi |
| } |
| |
| # @description guess the repo |
| function github_brute_force_repo_on_remote |
| { |
| declare remote=$1 |
| declare domain=${GITHUB_BASE_URL##*/} |
| declare repo |
| declare remoteurl |
| |
| remoteurl=$("${GIT}" remote get-url "${remote}") |
| if [[ ${remoteurl} =~ ${domain} ]]; then |
| # chop off (protocol)(hostname) |
| repo=${remoteurl#*"${domain}"} |
| # chop off / or : in the front |
| repo=${repo:1} |
| # chop off ending .git |
| GITHUB_REPO=${repo%%\.git} |
| fi |
| |
| } |
| |
| ## @description initialize github |
| function github_initialize |
| { |
| declare url |
| |
| if [[ -n "${GITHUB_TOKEN}" ]]; then |
| GITHUB_AUTH=(-H "Authorization: token ${GITHUB_TOKEN}") # pragma: allowlist secret |
| fi |
| |
| GITHUB_REPO=${GITHUB_REPO:-${GITHUB_REPO_DEFAULT}} |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| yetus_error "WARNING: --github-repo not autodetermined or provided. Brute forcing." |
| pushd "${BASEDIR}" >/dev/null || return 1 |
| github_brute_force_repo_on_remote origin |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| while read -r; do |
| github_brute_force_repo_on_remote "${REPLY}" |
| if [[ -n "${GITHUB_REPO}" ]]; then |
| break |
| fi |
| done < <("${GIT}" remote) |
| fi |
| popd >/dev/null|| return 1 |
| if [[ -n "${GITHUB_REPO}" ]]; then |
| yetus_error "WARNING: Brute force says ${GITHUB_BASE_URL}/${GITHUB_REPO}" |
| fi |
| fi |
| |
| if [[ ${GITHUB_STATUS_USE_HTMLREPORT} == true ]]; then |
| if ! verify_plugin_enabled htmlout; then |
| GITHUB_STATUS_USE_HTMLREPORT=false |
| yetus_error "WARNING: Disabling --github-status-use-htmlreport. htmlout plug-in is not enabled." |
| else |
| url=$(htmlout_reportname) |
| if [[ -z ${url} ]]; then |
| GITHUB_STATUS_USE_HTMLREPORT=false |
| yetus_error "WARNING: Disabling --github-status-use-htmlreport. Artifacts URL does not resolve/report is not relative." |
| fi |
| fi |
| fi |
| |
| # if the default branch hasn't been set yet, ask GitHub |
| if [[ -z "${PATCH_BRANCH_DEFAULT}" && -n "${GITHUB_REPO}" && "${OFFLINE}" == false ]]; then |
| if [[ ! -f "${PATCH_DIR}/github-repo.json" ]]; then |
| "${CURL}" --silent --fail \ |
| -H "Accept: application/vnd.github.v3.full+json" \ |
| "${GITHUB_AUTH[@]}" \ |
| --output "${PATCH_DIR}/github-repo.json" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}" \ |
| > /dev/null |
| fi |
| if [[ -f "${PATCH_DIR}/github-repo.json" ]]; then |
| PATCH_BRANCH_DEFAULT=$("${GREP}" default_branch "${PATCH_DIR}/github-repo.json" | head -1 | cut -d\" -f4) |
| fi |
| fi |
| |
| if [[ "${PROJECT_NAME}" == "unknown" ]]; then |
| PROJECT_NAME=${GITHUB_REPO##*/} |
| fi |
| } |
| |
| ## @description based upon a github PR, attempt to link back to JIRA |
| function github_find_jira_title |
| { |
| declare title |
| declare maybe |
| declare retval |
| |
| if [[ ! -f "${PATCH_DIR}/github-pull.json" ]]; then |
| return 1 |
| fi |
| |
| title=$(${GREP} title "${PATCH_DIR}/github-pull.json" \ |
| | cut -f4 -d\") |
| |
| # people typically do two types: JIRA-ISSUE: and [JIRA-ISSUE] |
| # JIRA_ISSUE_RE is pretty strict so we need to chop that stuff |
| # out first |
| |
| maybe=$(echo "${title}" | cut -f2 -d\[ | cut -f1 -d\]) |
| jira_determine_issue "${maybe}" |
| retval=$? |
| |
| if [[ ${retval} == 0 ]]; then |
| return 0 |
| fi |
| |
| maybe=$(echo "${title}" | cut -f1 -d:) |
| jira_determine_issue "${maybe}" |
| retval=$? |
| |
| if [[ ${retval} == 0 ]]; then |
| return 0 |
| fi |
| |
| return 1 |
| } |
| |
| function github_determine_issue |
| { |
| declare input=$1 |
| |
| if [[ ${input} =~ ^[0-9]+$ |
| && -n ${GITHUB_REPO} ]]; then |
| # shellcheck disable=SC2034 |
| ISSUE=${input} |
| if [[ -z ${GITHUB_ISSUE} ]]; then |
| GITHUB_ISSUE=${input} |
| fi |
| fi |
| |
| # if JIRA didn't call us, should we call it? |
| if [[ ${GITHUB_BRIDGED} == false ]]; then |
| if github_find_jira_title; then |
| return 0 |
| fi |
| fi |
| |
| if [[ -n ${GITHUB_ISSUE} ]]; then |
| return 0 |
| fi |
| |
| return 1 |
| } |
| |
| ## @description Try to guess the branch being tested using a variety of heuristics |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @return 0 on success, with PATCH_BRANCH updated appropriately |
| ## @return 1 on failure |
| function github_determine_branch |
| { |
| if [[ ! -f "${PATCH_DIR}/github-pull.json" ]]; then |
| return 1 |
| fi |
| |
| # shellcheck disable=SC2016 |
| PATCH_BRANCH=$("${AWK}" 'match($0,"\"ref\": \""){print $2}' "${PATCH_DIR}/github-pull.json"\ |
| | cut -f2 -d\"\ |
| | tail -1 ) |
| |
| yetus_debug "Github determine branch: starting with ${PATCH_BRANCH}" |
| |
| verify_valid_branch "${PATCH_BRANCH}" |
| } |
| |
| ## @description Generate patch and diff files locally via git when the |
| ## @description GitHub API fails (e.g., HTTP 406 on PRs with >300 files). |
| ## @description Fetches the PR head via refs/pull/N/head and the base branch, |
| ## @description then uses git format-patch and git diff to produce the output. |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param PR number |
| ## @param patch output file |
| ## @param diff output file |
| ## @param skip_patch (optional, default false) - skip format-patch generation |
| ## @return 0 on success |
| ## @return 1 on failure |
| function github_generate_local_diff |
| { |
| declare input=$1 |
| declare patchout=$2 |
| declare diffout=$3 |
| declare skip_patch=$4 |
| declare base_ref |
| declare base_sha |
| declare head_sha |
| declare merge_base |
| |
| yetus_debug "github: attempting local git fallback for PR #${input}" |
| |
| # shellcheck disable=SC2016 |
| base_ref=$("${AWK}" 'match($0,"\"ref\": \""){print $2}' "${PATCH_DIR}/github-pull.json" \ |
| | cut -f2 -d\" \ |
| | tail -1) |
| |
| if [[ -z "${base_ref}" ]]; then |
| yetus_debug "github: cannot determine base ref from PR JSON" |
| return 1 |
| fi |
| |
| pushd "${BASEDIR}" >/dev/null || return 1 |
| |
| if ! "${GIT}" fetch origin "refs/pull/${input}/head" >/dev/null 2>&1; then |
| yetus_debug "github: cannot fetch refs/pull/${input}/head" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| head_sha=$("${GIT}" rev-parse FETCH_HEAD 2>/dev/null) |
| if [[ -z "${head_sha}" ]]; then |
| yetus_debug "github: cannot resolve PR head SHA" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| if ! "${GIT}" fetch origin "${base_ref}" >/dev/null 2>&1; then |
| yetus_debug "github: cannot fetch base ref ${base_ref}" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| base_sha=$("${GIT}" rev-parse FETCH_HEAD 2>/dev/null) |
| if [[ -z "${base_sha}" ]]; then |
| yetus_debug "github: cannot resolve base branch SHA" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| merge_base=$("${GIT}" merge-base "${base_sha}" "${head_sha}" 2>/dev/null) |
| |
| if [[ -z "${merge_base}" ]]; then |
| if "${GIT}" rev-parse --is-shallow-repository 2>/dev/null | grep -q true; then |
| yetus_debug "github: shallow repo detected, deepening to find merge base" |
| "${GIT}" fetch --deepen=2147483647 origin "${base_ref}" >/dev/null 2>&1 |
| "${GIT}" fetch --deepen=2147483647 origin "refs/pull/${input}/head" >/dev/null 2>&1 |
| merge_base=$("${GIT}" merge-base "${base_sha}" "${head_sha}" 2>/dev/null) |
| fi |
| fi |
| |
| if [[ -z "${merge_base}" ]]; then |
| yetus_debug "github: cannot determine merge base" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| if [[ "${skip_patch}" != true ]]; then |
| echo " Generating local patch via git format-patch" |
| if ! "${GIT}" format-patch --stdout "${merge_base}..${head_sha}" > "${patchout}" 2>/dev/null; then |
| yetus_debug "github: git format-patch failed" |
| popd >/dev/null || true |
| return 1 |
| fi |
| fi |
| |
| echo " Generating local binary diff via git diff --binary" |
| if ! "${GIT}" diff --binary "${merge_base}..${head_sha}" > "${diffout}" 2>/dev/null; then |
| yetus_debug "github: git diff --binary failed" |
| popd >/dev/null || true |
| return 1 |
| fi |
| |
| popd >/dev/null || true |
| return 0 |
| } |
| |
| ## @description Given input = GH:##, download a patch to output. |
| ## @description Also sets GITHUB_ISSUE to the raw number. |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param input |
| ## @param output |
| ## @return 0 on success |
| ## @return 1 on failure |
| function github_locate_pr_patch |
| { |
| declare input=$1 |
| declare patchout=$2 |
| declare diffout=$3 |
| declare apiurl |
| declare diffgenerated=false |
| declare line |
| declare sha |
| declare foundhead=false |
| |
| input=${input#GH:} |
| |
| # https://github.com/your/repo/pull/## |
| if [[ ${input} =~ ^${GITHUB_BASE_URL}.*/pull/[0-9]+$ ]]; then |
| github_breakup_url "${input}.patch" |
| input=${GITHUB_ISSUE} |
| fi |
| |
| # https://github.com/your/repo/pulls/##.patch |
| if [[ ${input} =~ ^${GITHUB_BASE_URL}.*patch$ ]]; then |
| github_breakup_url "${input}" |
| input=${GITHUB_ISSUE} |
| fi |
| |
| # https://github.com/your/repo/pulls/##.diff |
| if [[ ${input} =~ ^${GITHUB_BASE_URL}.*diff$ ]]; then |
| github_breakup_url "${input}" |
| input=${GITHUB_ISSUE} |
| fi |
| |
| # if it isn't a number at this point, no idea |
| # how to process |
| if [[ ! ${input} =~ ^[0-9]+$ ]]; then |
| yetus_debug "github: ${input} is not a pull request #" |
| return 1 |
| fi |
| |
| # We pull both .patch and .diff versions. The diff is preferred |
| # (see dryrun_both_files / YETUS-983); .patch is kept as a fallback. |
| |
| apiurl="${GITHUB_API_URL}/repos/${GITHUB_REPO}/pulls/${input}" |
| |
| # shellcheck disable=SC2034 |
| PATCHURL="${GITHUB_BASE_URL}/${GITHUB_REPO}/pull/${input}.patch" |
| |
| echo "GITHUB PR #${input} is being downloaded from" |
| echo "${apiurl}" |
| |
| echo " JSON data at $(date)" |
| # Let's pull the PR JSON for later use |
| if ! "${CURL}" --silent --fail \ |
| -H "Accept: application/vnd.github.v3.full+json" \ |
| "${GITHUB_AUTH[@]}" \ |
| --output "${PATCH_DIR}/github-pull.json" \ |
| --location \ |
| "${apiurl}"; then |
| yetus_debug "github_locate_patch: cannot download json" |
| return 1 |
| fi |
| |
| echo " Patch data at $(date)" |
| |
| # the actual patch file |
| if ! "${CURL}" --silent --fail \ |
| -H "Accept: application/vnd.github.v3.patch" \ |
| --output "${patchout}" \ |
| --location \ |
| "${GITHUB_AUTH[@]}" \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/pulls/${input}"; then |
| yetus_debug "github_locate_patch: API patch download failed" |
| if ! github_generate_local_diff "${input}" "${patchout}" "${diffout}" false; then |
| yetus_debug "github_locate_patch: local git fallback also failed" |
| return 1 |
| fi |
| diffgenerated=true |
| fi |
| |
| if [[ "${diffgenerated}" != true ]]; then |
| if ! github_generate_local_diff "${input}" "${patchout}" "${diffout}" true; then |
| echo " Local binary diff failed, falling back to API diff at $(date)" |
| if ! "${CURL}" --silent --fail \ |
| -H "Accept: application/vnd.github.v3.diff" \ |
| --output "${diffout}" \ |
| --location \ |
| "${GITHUB_AUTH[@]}" \ |
| "${apiurl}"; then |
| yetus_debug "github_locate_patch: cannot download diff" |
| return 1 |
| fi |
| fi |
| fi |
| |
| if [[ -z "${GIT_BRANCH_SHA}" ]]; then |
| while read -r line; do |
| if [[ "${line}" =~ \"head\": ]]; then |
| foundhead=true |
| fi |
| if [[ "${foundhead}" == true ]]; then |
| if [[ "${line}" =~ \"sha\": ]]; then |
| sha=${line##* \"} |
| GIT_BRANCH_SHA=${sha%%\"*} |
| break |
| fi |
| fi |
| done < <(cat "${PATCH_DIR}/github-pull.json") |
| fi |
| |
| GITHUB_ISSUE=${input} |
| |
| # github will translate this to be #(xx) ! |
| add_footer_table "GITHUB PR" "${GITHUB_BASE_URL}/${GITHUB_REPO}/pull/${input}" |
| |
| return 0 |
| } |
| |
| |
| ## @description a wrapper for github_locate_pr_patch that |
| ## @description that takes a (likely checkout'ed) github commit |
| ## @description sha and turns into the the github pr |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param input |
| ## @param output |
| ## @return 0 on success |
| ## @return 1 on failure |
| function github_locate_sha_patch |
| { |
| declare input=$1 |
| declare patchout=$2 |
| declare diffout=$3 |
| declare gitsha |
| declare number |
| |
| gitsha=${input#GHSHA:} |
| |
| # locate the PR number via GitHub API v3 |
| #curl https://api.github.com/search/issues?q=sha:40a7af3377d8087779bf8ad66397947b7270737a\&type:pr\&repo:apache/yetus |
| |
| |
| # Let's pull the PR JSON for later use |
| if ! "${CURL}" --silent --fail \ |
| -H "Accept: application/vnd.github.v3.full+json" \ |
| "${GITHUB_AUTH[@]}" \ |
| --output "${PATCH_DIR}/github-search.json" \ |
| --location \ |
| "${GITHUB_API_URL}/search/issues?q=${gitsha}&type:pr&repo:${GITHUB_REPO}"; then |
| return 1 |
| fi |
| |
| # shellcheck disable=SC2016 |
| number=$("${GREP}" number "${PATCH_DIR}/github-search.json" | \ |
| head -1 | \ |
| "${AWK}" '{print $NF}') |
| number=${number//\s} |
| number=${number%,} |
| |
| github_locate_pr_patch "GH:${number}" "${patchout}" "${diffout}" |
| } |
| |
| |
| ## @description Handle the various ways to reference a github PR |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param input |
| ## @param output |
| ## @return 0 on success |
| ## @return 1 on failure |
| function github_locate_patch |
| { |
| declare input=$1 |
| declare patchout=$2 |
| declare diffout=$3 |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| yetus_debug "github_locate_patch: offline, skipping" |
| return 1 |
| fi |
| |
| case "${input}" in |
| GHSHA:*) |
| github_locate_sha_patch "${input}" "${patchout}" "${diffout}" |
| return $? |
| ;; |
| *) |
| github_locate_pr_patch "${input}" "${patchout}" "${diffout}" |
| return $? |
| ;; |
| esac |
| } |
| |
| ## @description Generate a Github Check Run ID |
| ## @stability evolving |
| ## @audience private |
| function github_start_checkrun |
| { |
| declare tempfile="${PATCH_DIR}/ghcheckrun.$$.${RANDOM}" |
| declare output="${PATCH_DIR}/ghcheckrun.json" |
| |
| if [[ -z "${GITHUB_SHA}" ]]; then |
| GITHUB_SHA=$("${GREP}" \"sha\" "${PATCH_DIR}/github-pull.json" 2>/dev/null \ |
| | head -1 \ |
| | cut -f4 -d\") |
| fi |
| |
| if [[ -z "${GITHUB_SHA}" ]]; then |
| return 0 |
| fi |
| |
| # don't need this under GHA |
| if [[ "${ROBOTTYPE}" == 'githubactions' ]]; then |
| return 0 |
| fi |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| return 0 |
| fi |
| |
| if [[ "${#GITHUB_AUTH[@]}" -eq 0 ]]; then |
| return 0 |
| fi |
| |
| { |
| printf "{" |
| echo "\"name\":\"Apache Yetus ${ROBOTTYPE}\"," |
| echo "\"head_sha\": \"${GITHUB_SHA}\"," |
| echo "\"details_url\": \"${BUILD_URL}${BUILD_URL_CONSOLE}\"," |
| echo "\"external_id\": \"${INSTANCE}\"," |
| echo "\"status\": \"in_progress\"," |
| echo "\"started_at\": \"${ISODATESTART}\"" |
| # external_id instance_id? |
| # status queued, in_progress, completed |
| # started_at ISO-8601 |
| # conclusion , required for status=completed, completed_at=value |
| # success, failure, neutral, cancelled, skipped, timed_out, action_required |
| # completed_at ISO-8601 |
| # output see github docs @ https://docs.github.com/en/rest/reference/checks#update-a-check-run |
| echo "}" |
| } > "${tempfile}" |
| |
| "${CURL}" --silent --fail -X POST \ |
| -H "Accept: application/vnd.github.antiope-preview+json" \ |
| -H "Content-Type: application/json" \ |
| "${GITHUB_AUTH[@]}" \ |
| -d @"${tempfile}" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/check-runs" \ |
| --output "${output}" 2>/dev/null |
| |
| GITHUB_CHECK_RUN_ID=$("${GREP}" \"id\" "${output}" 2>/dev/null \ |
| | head -1 \ |
| | cut -f2 -d: \ |
| | cut -f1 -d,) |
| GITHUB_CHECK_RUN_ID=${GITHUB_CHECK_RUN_ID// /} |
| } |
| |
| ## @description Generate a Github Check Run ID |
| ## @stability evolving |
| ## @audience private |
| function github_end_checkrun |
| { |
| declare result=$1 |
| declare tempfile="${PATCH_DIR}/ghcheckrun.$$.${RANDOM}" |
| declare output="${PATCH_DIR}/ghcheckrun-final.json" |
| declare conclusion |
| declare detailslink |
| |
| # don't need this under GHA |
| if [[ "${ROBOTTYPE}" == 'githubactions' ]]; then |
| return 0 |
| fi |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| return 0 |
| fi |
| |
| if [[ "${#GITHUB_AUTH[@]}" -eq 0 ]]; then |
| return 0 |
| fi |
| |
| if [[ "${result}" -eq 0 ]]; then |
| conclusion="success" |
| else |
| conclusion="failure" |
| fi |
| |
| finishdate=$(date +"%Y-%m-%dT%H:%M:%SZ") |
| |
| # link to the html report if possible |
| if [[ ${GITHUB_STATUS_USE_HTMLREPORT} == true ]]; then |
| detailslink=$(htmlout_reportname) |
| else |
| detailslink="${BUILD_URL}${BUILD_URL_CONSOLE}" |
| fi |
| |
| { |
| printf "{" |
| echo "\"conclusion\":\"${conclusion}\"," |
| echo "\"status\": \"completed\"," |
| echo "\"details_url\": \"${detailslink}\"," |
| echo "\"completed_at\": \"${finishdate}\"" |
| echo "}" |
| } > "${tempfile}" |
| |
| "${CURL}" --fail --silent -X PATCH \ |
| -H "Accept: application/vnd.github.antiope-preview+json" \ |
| -H "Content-Type: application/json" \ |
| "${GITHUB_AUTH[@]}" \ |
| -d @"${tempfile}" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/check-runs/${GITHUB_CHECK_RUN_ID}" \ |
| --output "${output}" 2>/dev/null |
| rm "${tempfile}" |
| } |
| |
| ## @description Write a Github Checks Annotation |
| ## @param filename |
| ## @param linenum |
| ## @param column |
| ## @param plugin |
| ## @param text |
| ## @stability evolving |
| ## @audience private |
| function github_linecomments |
| { |
| declare file=$1 |
| declare linenum=$2 |
| declare column=$3 |
| declare plugin=$4 |
| shift 4 |
| declare text=$* |
| declare tempfile="${PATCH_DIR}/ghcomment.$$.${RANDOM}" |
| declare header |
| declare -a linehandler |
| declare -a colhandler |
| |
| if [[ "${ROBOTTYPE}" == 'githubactions' ]]; then |
| if [[ -z "${column}" ]] || [[ "${column}" == 0 ]]; then |
| echo "::error file=${file},line=${linenum}::${plugin}:${text}" |
| else |
| echo "::error file=${file},line=${linenum},col=${column}::${plugin}:${text}" |
| fi |
| return 0 |
| fi |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| yetus_error "WARNING: Running offline, GitHub annotations skipped." |
| return 0 |
| fi |
| |
| if [[ "${#GITHUB_AUTH[@]}" -eq 0 ]]; then |
| return 0 |
| fi |
| |
| if [[ ${GITHUB_ANNOTATION_LIMIT} -eq 0 ]]; then |
| return 0 |
| fi |
| |
| ((GITHUB_ANNOTATION_LIMIT=GITHUB_ANNOTATION_LIMIT - 1)) |
| |
| if [[ ${GITHUB_ANNOTATION_LIMIT} -eq 0 ]]; then |
| yetus_error "WARNING: GitHub annotations limit reached." |
| return 0 |
| fi |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| yetus_error "ERROR: --github-repo is not defined." |
| return 1 |
| fi |
| |
| if [[ -z "${GITHUB_CHECK_RUN_ID}" ]]; then |
| if ! github_start_checkrun; then |
| yetus_error "ERROR: Cannot generate a Github Check Run ID" |
| return 1 |
| fi |
| fi |
| |
| linehandler=(\"start_line\": "${linenum},") |
| linehandler+=(\"end_line\": "${linenum},") |
| |
| if [[ -z "${column}" ]] || [[ "${column}" == 0 ]]; then |
| colhandler=() |
| else |
| colhandler=(\"start_column\": "${column},") |
| colhandler+=(\"end_column\": "${column},") |
| fi |
| |
| newtext=$(echo "${text[*]}" | "${SED}" -e 's,\\,\\\\,g' \ |
| -e 's,\",\\\",g' \ |
| -e 's,$,\\r\\n,g' \ |
| | tr -d '\n') |
| |
| if [[ "${ROBOTTYPE}" ]]; then |
| header="Apache Yetus(${ROBOTTYPE})" |
| else |
| header="Apache Yetus" |
| fi |
| |
| cat <<EOF > "${tempfile}" |
| { |
| "output": { |
| "title": "${header}", |
| "summary": "Precommit Problem", |
| "annotations" : [{ |
| "path": "${file}", |
| ${linehandler[@]} |
| ${colhandler[@]} |
| "annotation_level": "failure", |
| "message": "${plugin}: ${newtext}" |
| }] |
| } |
| } |
| EOF |
| |
| "${CURL}" --silent --fail -X PATCH \ |
| -H "Accept: application/vnd.github.antiope-preview+json" \ |
| -H "Content-Type: application/json" \ |
| "${GITHUB_AUTH[@]}" \ |
| --output "${PATCH_DIR}/github-check-annotation-response.json" \ |
| -d @"${tempfile}" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/check-runs/${GITHUB_CHECK_RUN_ID}" \ |
| 2>/dev/null |
| rm "${tempfile}" |
| } |
| |
| ## @description Write the contents of a file to github |
| ## @param filename |
| ## @stability stable |
| ## @audience public |
| function github_write_comment |
| { |
| declare -r commentfile=${1} |
| declare retval=0 |
| declare restfile="${PATCH_DIR}/ghcomment.$$" |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| yetus_error "WARNING: Running offline, GitHub comment skipped." |
| return 0 |
| fi |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| yetus_error "ERROR: --github-repo is not defined." |
| return 0 |
| fi |
| |
| { |
| printf "{\"body\":\"" |
| "${SED}" -e 's,\\,\\\\,g' \ |
| -e 's,\",\\\",g' \ |
| -e 's,$,\\r\\n,g' "${commentfile}" \ |
| | tr -d '\n' |
| echo "\"}" |
| } > "${restfile}" |
| |
| if [[ "${#GITHUB_AUTH[@]}" -lt 1 ]]; then |
| yetus_error "ERROR: No GitHub credentials defined." |
| return 0 |
| fi |
| |
| "${CURL}" --silent --fail -X POST \ |
| -H "Accept: application/vnd.github.v3.full+json" \ |
| -H "Content-Type: application/json" \ |
| "${GITHUB_AUTH[@]}" \ |
| -d @"${restfile}" \ |
| --output "${PATCH_DIR}/github-comment-write-response.json" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/issues/${GITHUB_ISSUE}/comments" \ |
| >/dev/null |
| |
| retval=$? |
| rm "${restfile}" |
| return ${retval} |
| } |
| |
| ## @description Print out the finished details to the Github PR |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param runresult |
| function github_finalreport_as_comment |
| { |
| declare result=$1 |
| declare i |
| declare commentfile=${PATCH_DIR}/gitcommentfile.$$ |
| declare comment |
| declare url |
| declare ela |
| declare subs |
| declare logfile |
| declare calctime |
| declare vote |
| declare emoji |
| |
| rm "${commentfile}" 2>/dev/null |
| |
| if [[ ${ROBOT} = "false" |
| || -z ${GITHUB_ISSUE} ]] ; then |
| return 0 |
| fi |
| |
| url=$(get_artifact_url) |
| |
| big_console_header "Adding comment to Github" |
| |
| if [[ ${result} == 0 ]]; then |
| echo ":confetti_ball: **+1 overall**" >> "${commentfile}" |
| else |
| echo ":broken_heart: **-1 overall**" >> "${commentfile}" |
| fi |
| printf '\n\n\n\n' >> "${commentfile}" |
| |
| i=0 |
| until [[ ${i} -ge ${#TP_HEADER[@]} ]]; do |
| printf '%s\n\n' "${TP_HEADER[i]}" >> "${commentfile}" |
| ((i=i+1)) |
| done |
| |
| { |
| printf '\n\n' |
| echo "| Vote | Subsystem | Runtime | Logfile | Comment |" |
| echo "|:----:|----------:|--------:|:--------:|:-------:|" |
| } >> "${commentfile}" |
| |
| i=0 |
| until [[ ${i} -ge ${#TP_VOTE_TABLE[@]} ]]; do |
| ourstring=$(echo "${TP_VOTE_TABLE[i]}" | tr -s ' ') |
| vote=$(echo "${ourstring}" | cut -f2 -d\| | tr -d ' ') |
| subs=$(echo "${ourstring}" | cut -f3 -d\|) |
| ela=$(echo "${ourstring}" | cut -f4 -d\|) |
| calctime=$(clock_display "${ela}") |
| logfile=$(echo "${ourstring}" | cut -f5 -d\| | tr -d ' ') |
| comment=$(echo "${ourstring}" | cut -f6 -d\|) |
| |
| if [[ "${vote}" = "H" ]]; then |
| echo "|||| _${comment}_ |" >> "${commentfile}" |
| ((i=i+1)) |
| continue |
| fi |
| |
| if [[ ${GITHUB_USE_EMOJI_VOTE} == true ]]; then |
| emoji="" |
| case ${vote} in |
| 1|"+1") |
| emoji="+1 :green_heart:" |
| ;; |
| -1) |
| emoji="-1 :x:" |
| ;; |
| 0) |
| emoji="+0 :ok:" |
| ;; |
| -0) |
| emoji="-0 :warning:" |
| ;; |
| H) |
| # this never gets called (see above) but this is here so others know the color is taken |
| emoji="" |
| ;; |
| *) |
| # usually this should not happen but let's keep the old vote result if it happens |
| emoji=${vote} |
| ;; |
| esac |
| else |
| emoji="${vote}" |
| fi |
| |
| if [[ -n "${logfile}" ]]; then |
| t1=${logfile/@@BASE@@/} |
| t2=$(echo "${logfile}" | "${SED}" -e "s,@@BASE@@,${url},g") |
| t2="[${t1}](${t2})" |
| else |
| t2="" |
| fi |
| |
| printf '| %s | %s | %s | %s | %s |\n' \ |
| "${emoji}" \ |
| "${subs}" \ |
| "${calctime}" \ |
| "${t2}" \ |
| "${comment}" \ |
| >> "${commentfile}" |
| |
| ((i=i+1)) |
| done |
| |
| if [[ ${#TP_TEST_TABLE[@]} -gt 0 ]]; then |
| { |
| printf '\n\n' |
| echo "| Reason | Tests |" |
| echo "|-------:|:------|" |
| } >> "${commentfile}" |
| i=0 |
| until [[ ${i} -ge ${#TP_TEST_TABLE[@]} ]]; do |
| echo "${TP_TEST_TABLE[i]}" >> "${commentfile}" |
| ((i=i+1)) |
| done |
| fi |
| |
| { |
| printf '\n\n' |
| echo "| Subsystem | Report/Notes |" |
| echo "|----------:|:-------------|" |
| } >> "${commentfile}" |
| |
| i=0 |
| until [[ $i -ge ${#TP_FOOTER_TABLE[@]} ]]; do |
| comment=$(echo "${TP_FOOTER_TABLE[i]}" | "${SED}" -e "s,@@BASE@@,${url},g") |
| printf '%s\n' "${comment}" >> "${commentfile}" |
| ((i=i+1)) |
| done |
| printf '\n\nThis message was automatically generated.\n\n' >> "${commentfile}" |
| |
| github_write_comment "${commentfile}" |
| } |
| |
| ## @description Write a github status |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param runresult |
| function github_status_write |
| { |
| declare filename=$1 |
| declare retval=0 |
| declare recoverydir="${PATCH_DIR}/github-status-retry/${GITHUB_REPO}/${GIT_BRANCH_SHA}" |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| return 0 |
| fi |
| |
| if [[ "${#GITHUB_AUTH[@]}" -lt 1 ]]; then |
| echo "Github Plugin: no credentials provided to write a status." |
| return 0 |
| fi |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| echo "GitHub Repo not defined." |
| return 0 |
| fi |
| |
| "${CURL}" --silent --fail -X POST \ |
| -H "Accept: application/vnd.github.v3.full+json" \ |
| -H "Content-Type: application/json" \ |
| "${GITHUB_AUTH[@]}" \ |
| -d @"${filename}" \ |
| --location \ |
| "${GITHUB_API_URL}/repos/${GITHUB_REPO}/statuses/${GIT_BRANCH_SHA}" \ |
| --output "${PATCH_DIR}/gitub-status-response-${GITHUB_STATUS_RECOVERY_COUNTER}.json" \ |
| 2>/dev/null |
| |
| retval=$? |
| if [[ ${retval} -gt 0 ]]; then |
| yetus_error "ERROR: Failed to write github status. Token expired or missing repo:status write?" |
| if [[ "${GITHUB_STATUS_RECOVER_TOOL}" == false ]]; then |
| mkdir -p "${recoverydir}" |
| cp -p "${tempfile}" "${recoverydir}/${GITHUB_STATUS_RECOVERY_COUNTER}.json" |
| ((GITHUB_STATUS_RECOVERY_COUNTER=GITHUB_STATUS_RECOVERY_COUNTER+1)) |
| echo "${RESULT}" > "${PATCH_DIR}/github-status-retry/finalresult.txt" |
| fi |
| fi |
| return ${retval} |
| } |
| |
| ## @description Write a github status |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param runresult |
| function github_status_recovery |
| { |
| declare filename |
| declare retval=0 |
| declare retrydir |
| declare ghr |
| |
| # get the first filename |
| filename=$(find "${PATCH_DIR}/github-status-retry" -type f -name '1.json' 2>/dev/null) |
| |
| if [[ -z "${filename}" ]]; then |
| echo "No retry directory found in ${PATCH_DIR}. Maybe it was successful?" |
| return 0 |
| fi |
| |
| big_console_header "Attempting GitHub Status Recovery" |
| |
| retrydir="${filename##*/github-status-retry/}" |
| ghr=$(echo "${retrydir}" | cut -f1-2 -d/) |
| GITHUB_REPO=${GITHUB_REPO:-${ghr}} |
| GIT_BRANCH_SHA=$(echo "${retrydir}" | cut -f3 -d/) |
| |
| github_initialize |
| |
| if [[ "${#GITHUB_AUTH[@]}" -lt 1 ]]; then |
| echo "Github Plugin: no credentials provided to write a status." |
| return 1 |
| fi |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| yetus_error "ERROR: --github-repo is not defined." |
| return 1 |
| fi |
| |
| if ! github_start_checkrun; then |
| yetus_error "ERROR: Cannot generate a Github Check Run ID" |
| return 1 |
| fi |
| |
| RESULT=$(cat "${PATCH_DIR}/github-status-retry/finalresult.txt") |
| |
| if [[ "${GITHUB_CHECK_ANNOTATIONS}" == true ]]; then |
| bugsystem_linecomments_trigger |
| fi |
| |
| while read -r; do |
| github_status_write "${REPLY}" |
| retval=$? |
| done < <(find "${PATCH_DIR}/github-status-retry" -type f -name '*.json') |
| |
| github_end_checkrun "${RESULT}" |
| |
| return ${retval} |
| } |
| |
| ## @description Print out the finished details to the Github PR |
| ## @audience private |
| ## @stability evolving |
| ## @replaceable no |
| ## @param runresult |
| function github_finalreport |
| { |
| declare result=$1 |
| declare tempfile="${PATCH_DIR}/ghstatus.$$.${RANDOM}" |
| declare warnings=false |
| declare foundlogs=false |
| declare url |
| declare logurl |
| declare comment |
| declare url |
| declare subs |
| declare logfile |
| declare vote |
| declare ourstring |
| declare -i i=0 |
| declare header |
| declare detailslink |
| |
| if [[ "${OFFLINE}" == true ]]; then |
| return 0 |
| fi |
| |
| big_console_header "Adding GitHub Statuses" |
| |
| if [[ "${GITHUB_WRITE_COMMENT}" == true ]]; then |
| github_finalreport_as_comment "${result}" |
| fi |
| |
| if [[ "${#GITHUB_AUTH[@]}" -lt 1 ]]; then |
| yetus_error "ERROR: no credentials provided to write a status." |
| return 0 |
| fi |
| |
| if [[ -z "${GIT_BRANCH_SHA}" ]]; then |
| yetus_error "ERROR: Unknown GIT_BRANCH_SHA defined. Skipping." |
| return 0 |
| fi |
| |
| if [[ -z "${GITHUB_REPO}" ]]; then |
| yetus_error "ERROR: --github-repo is not defined." |
| return 0 |
| fi |
| |
| github_end_checkrun "${result}" |
| |
| # Under GHA the per-sub-check detail is already in the PR comment and the |
| # step summary. Commit statuses encode severity in the context key, so a |
| # re-run that changes outcome never overwrites the old status (YETUS-1277). |
| if [[ "${ROBOTTYPE}" == 'githubactions' ]]; then |
| return 0 |
| fi |
| |
| if [[ "${ROBOTTYPE}" ]]; then |
| header="Apache Yetus(${ROBOTTYPE})" |
| else |
| header="Apache Yetus" |
| fi |
| |
| # Customize the message based upon type of passing: |
| # - if there were no warnings, then just put a single message |
| # - if there were warnings, but no logs, then just mention warnings |
| # - if there were warnings and logs, handle in the next section |
| if [[ "${result}" == 0 ]]; then |
| i=0 |
| until [[ ${i} -eq ${#TP_VOTE_TABLE[@]} ]]; do |
| ourstring=$(echo "${TP_VOTE_TABLE[i]}" | tr -s ' ') |
| vote=$(echo "${ourstring}" | cut -f2 -d\| | tr -d ' ') |
| |
| if [[ "${vote}" == "-0" ]]; then |
| warnings=true |
| logfile=$(echo "${ourstring}" | cut -f5 -d\| | tr -d ' ') |
| |
| if [[ -n "${logfile}" ]]; then |
| foundlogs=true |
| break |
| fi |
| fi |
| ((i=i+1)) |
| done |
| |
| # link to the html report if possible |
| if [[ ${GITHUB_STATUS_USE_HTMLREPORT} == true ]]; then |
| detailslink=$(htmlout_reportname) |
| else |
| detailslink="${BUILD_URL}${BUILD_URL_CONSOLE}" |
| fi |
| |
| # did not find any logs, so just give a simple success status |
| if [[ ${foundlogs} == false ]]; then |
| if [[ ${warnings} == false ]]; then |
| # build our REST post |
| { |
| echo "{\"state\": \"success\", " |
| echo "\"target_url\": \"${detailslink}\"," |
| echo "\"description\": \"passed\"," |
| echo "\"context\":\"${header}\"}" |
| } > "${tempfile}" |
| elif [[ ${warnings} == true ]]; then |
| # build our REST post |
| { |
| echo "{\"state\": \"success\", " |
| echo "\"target_url\": \"${detailslink}\"," |
| echo "\"description\": \"passed with warnings\"," |
| echo "\"context\":\"${header}\"}" |
| } > "${tempfile}" |
| fi |
| github_status_write "${tempfile}" |
| rm "${tempfile}" |
| return 0 |
| fi |
| fi |
| |
| # from here on, success w/logs or failure. |
| # give a separate status for each: |
| # - failure |
| # - failure w/log |
| # - success w/warning log |
| |
| url=$(get_artifact_url) |
| i=0 |
| until [[ ${i} -eq ${#TP_VOTE_TABLE[@]} ]]; do |
| ourstring=$(echo "${TP_VOTE_TABLE[i]}" | tr -s ' ') |
| vote=$(echo "${ourstring}" | cut -f2 -d\| | tr -d ' ') |
| subs=$(echo "${ourstring}" | cut -f3 -d\|) |
| logfile=$(echo "${ourstring}" | cut -f5 -d\| | tr -d ' ') |
| comment=$(echo "${ourstring}" | cut -f6 -d\|) |
| |
| if [[ "${vote}" = "H" ]]; then |
| ((i=i+1)) |
| continue |
| fi |
| |
| # GitHub needs more statuses to cover everything yetus does :( |
| case ${vote} in |
| -1) |
| status="error" |
| ;; |
| *) |
| status="success" |
| ;; |
| esac |
| |
| logurl=${BUILD_URL}${BUILD_URL_CONSOLE} |
| if [[ "${url}" =~ ^http ]]; then |
| if [[ -n "${logfile}" ]]; then |
| logurl=$(echo "${logfile}" | "${SED}" -e "s,@@BASE@@,${url},g") |
| fi |
| fi |
| |
| if [[ ${status} == "success" && -n "${logfile}" ]]; then |
| { |
| echo "{\"state\": \"${status}\", " |
| echo "\"target_url\": \"${logurl}\"," |
| echo "\"description\": \"${comment}\"," |
| echo "\"context\":\"${header} warning: ${subs}\"}" |
| } > "${tempfile}" |
| github_status_write "${tempfile}" |
| rm "${tempfile}" |
| elif [[ ${status} == "error" ]]; then |
| { |
| echo "{\"state\": \"${status}\", " |
| echo "\"target_url\": \"${logurl}\"," |
| echo "\"description\": \"${comment}\"," |
| echo "\"context\":\"${header} error: ${subs}\"}" |
| } > "${tempfile}" |
| github_status_write "${tempfile}" |
| rm "${tempfile}" |
| fi |
| ((i=i+1)) |
| done |
| } |