Last active
May 7, 2021 13:16
-
-
Save gulrich1/91d284a2444af32a41c609735fdf3203 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
############################ | |
# Dependencies: jq, recode # | |
############################ | |
# PARAMETERS: | |
# 1 - project name | |
# 2 - pull request | |
######################## | |
# Environent variable: # | |
# GITHUB_TOKEN # | |
# SONAR_TOKEN # | |
######################## | |
################################################ | |
# Constants depending on the project or team: # | |
GITHUB_ORG=despegar # | |
SONAR_URL=http://jenkins-loyalty-cross-00:9000 # | |
################################################ | |
CURL_OPTS=-s | |
DEBUG=1 | |
BLOCKER_IMG="![severity: BLOCKER](https://sonarsource.github.io/sonar-github/severity-blocker.png)" | |
CRITICAL_IMG="![severity: CRITICAL](https://sonarsource.github.io/sonar-github/severity-critical.png)" | |
MAJOR_IMG="![severity: MAYOR](https://sonarsource.github.io/sonar-github/severity-major.png)" | |
MINOR_IMG="![severity: MINOR](https://sonarsource.github.io/sonar-github/severity-minor.png)" | |
INFO_IMG="![severity: INFO](https://sonarsource.github.io/sonar-github/severity-info.png)" | |
RULE_IMG="![details](https://sonarsource.github.io/sonar-github/rule.png)" | |
# Get last commit id for pull request | |
get_commit_id () { | |
TOKEN=$1 | |
ORG=$2 | |
REPO=$3 | |
PR=$4 | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$REPO/pulls/$PR | jq -r '.head.sha' | |
} | |
get_total_violations() { | |
TOKEN=$1 | |
PROJECT=$2 | |
PR=$3 | |
curl $CURL_OPTS -u "${TOKEN}:" --location --request POST \ | |
"$SONAR_URL/api/measures/component?component=$PROJECT&pullRequest=$PR&metricKeys=violations" \ | |
| jq -r '.component.measures[] | select (.metric== "violations") | .value' | |
} | |
get_violations_list() { | |
TOKEN=$1 | |
PROJECT=$2 | |
PR=$3 | |
curl $CURL_OPTS -u "${TOKEN}:" --location --request POST \ | |
"$SONAR_URL/api/issues/search?componentKeys=$PROJECT&ps=100&p=1&pullRequest=$PR&metricKeys=violations&resolved=false" \ | |
| jq ".issues | [.[] | {\ | |
id: .key, | |
severity: (if .severity == null then \"MINOR\" else .severity end),\ | |
rule: .rule,\ | |
line: .line,\ | |
component: .component,\ | |
message: .message,\ | |
type: .type\ | |
}]" | |
} | |
get_source_code_line() { | |
TOKEN=$1 | |
PROJECT=$2 | |
PR=$3 | |
LINE=$4 | |
FILE=$5 | |
curl $CURL_OPTS -u "${TOKEN}:" --location --request POST \ | |
"$SONAR_URL/api/sources/lines?pullRequest=$PR&from=$LINE&to=$LINE&key=$FILE" \ | |
| jq -r '.sources[0].code' | sed -e 's/<[^>]*>//g' | recode html..ascii | |
} | |
get_pull_request_diff() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
PR=$4 | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" -H "Accept: application/vnd.github.v3.diff" \ | |
"https://api.github.com/repos/$GITHUB_ORG/$PROJECT/pulls/$PR" | |
} | |
get_line_in_diff() { | |
PR=$1 | |
FILE=$(basename $2) # File name | |
SOURCE=$(echo $3 | sed 's/"/\\"/g') # Source code line | |
cat /tmp/${PR}.diff | awk "BEGIN{ found=0} /$FILE/{found=1} {if (found) print }" \ | |
| awk 'BEGIN{ found=0} /@@.*/{found=1} {if (found) print }' \ | |
| awk '{print} /diff --git.*/ {exit}' | nl -v 0 | grep "$SOURCE" | awk '{print $1}' | |
} | |
get_github_reviews() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
PR=$4 | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$PROJECT/pulls/$PR/comments \ | |
| jq "[.[] | { \ | |
rp_id: .pull_request_review_id,\ | |
id: .id,\ | |
path: .path,\ | |
original_position:\ | |
.original_position,\ | |
original_commit_id:\ | |
.original_commit_id,\ | |
body: .body\ | |
}]" | |
} | |
get_github_comments() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
PR=$4 | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$PROJECT/issues/$PR/comments | jq '[.[] | {id: .id, body: .body}]' | |
} | |
update_github_status_pending() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
COMMIT=$4 | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" -X POST -H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$PROJECT/statuses/$COMMIT \ | |
-d '{"state":"pending", "context": "Sonarqube"}' | |
} | |
update_github_status_error() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
COMMIT=$4 | |
ISSUES="$5" | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" -X POST -H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$PROJECT/statuses/$COMMIT \ | |
-d '{"state" : "error", "context": "Sonarqube", "description": "Sonarqube reported '$ISSUES' issues. Quality gate is ERROR"}' | |
} | |
update_github_status_success() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
COMMIT=$4 | |
ISSUES="$5" | |
curl $CURL_OPTS -H "Authorization: token $TOKEN" -X POST -H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$ORG/$PROJECT/statuses/$COMMIT \ | |
-d '{"state" : "success", "context": "Sonarqube", "description": "Sonarqube reported '$ISSUES' issues, no criticals or blockers"}' | |
} | |
get_qualitygate_status_from_sonar() { | |
TOKEN=$1 | |
PROJECT=$2 | |
PR=$3 | |
curl $CURL_OPTS -u "${TOKEN}:" --location \ | |
--request POST "$SONAR_URL/api/qualitygates/project_status?projectKey=$PROJECT&pullRequest=$PR" \ | |
| jq -r .projectStatus.status | |
} | |
get_qualitygate_error_from_sonar() { | |
TOKEN=$1 | |
PROJECT=$2 | |
PR=$3 | |
curl $CURL_OPTS -u "${TOKEN}:" --location \ | |
--request POST "$SONAR_URL/api/qualitygates/project_status?projectKey=$PROJECT&pullRequest=$PR" \ | |
| jq -r .projectStatus.status | |
} | |
add_github_comment_summary() { | |
TOKEN=$1 | |
ORG=$2 | |
PROJECT=$3 | |
PR=$4 | |
curl -s -H "Authorization: token $TOKEN" -X POST -d @/tmp/${PR}.sumary.json \ | |
"https://api.github.com/repos/$ORG/$PROJECT/issues/$PR/comments" | |
} | |
usage() { | |
echo "usage: sonar-github-integration.sh <project_name> <pull-request>" | |
echo "set this environment variables: GITHUB_TOKEN, SONAR_SONAR" | |
echo "edit variables SONAR_URL and GITHUB_ORG according to the project" | |
} | |
if [ "$#" -ne 2 ] || [ -z "$GITHUB_TOKEN" ] || [ -z "$SONAR_TOKEN" ]; then | |
usage | |
exit 1 | |
fi | |
PROJECT_NAME=$1 | |
PULL_REQUEST=$(echo $2 | sed 's/PR-\(.*\)/\1/') | |
COMMIT_ID=$(get_commit_id $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $PULL_REQUEST) | |
echo "commit_id: $COMMIT_ID" | |
if [ "$ACTION" == "PENDING" ]; then | |
update_github_status_pending $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $COMMIT_ID > /dev/null | |
exit 0 | |
fi | |
# Get number of violations | |
violations=$(get_total_violations $SONAR_TOKEN $PROJECT_NAME $PULL_REQUEST) | |
if [ -z "$violations" ] || [ "$violations" -eq 0 ]; then | |
echo "No violations found for PR: $PULL_REQUEST" | |
update_github_status_success $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $COMMIT_ID 0 | |
exit 0 | |
else | |
echo "Found $violations violations for PR: $PULL_REQUEST" | |
fi | |
list=$(get_violations_list $SONAR_TOKEN $PROJECT_NAME $PULL_REQUEST) | |
count=$(echo $list | jq length) | |
echo "Issues to process: $count" | |
get_pull_request_diff $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $PULL_REQUEST > /tmp/${PULL_REQUEST}.diff | |
# Create json with all coments to send to github | |
REVIEWS='[]' | |
ADDITIONAL_COMMENTS='[]' | |
declare -A issue_severity | |
if [ $count -gt 0 ]; then | |
count=$((count-1)) | |
for idx in $(seq 0 $count); | |
do | |
found=0 | |
if [ $DEBUG -eq 1 ]; then | |
echo "Processing issue: $idx" | |
fi | |
# Get issue properties | |
issue=$(echo $list | jq ".[$idx]") | |
id=$(echo $issue | jq -r .id) # line number | |
line=$(echo $issue | jq '.line // empty') # line number | |
severity=$(echo $issue | jq -r .severity) | |
rule=$(echo $issue | jq -r .rule) | |
component=$(echo $issue | jq -r .component) | |
message=$(echo $issue | jq -r .message | sed 's/\"/\\\"/g' | tr -d '\n') | |
type=$(echo $issue | jq -r .type) | |
file=$(echo $component | sed 's/.*:\(.*\)/\1/') | |
if [ -n "$line" ]; then | |
source=$(get_source_code_line $SONAR_TOKEN $PROJECT_NAME $PULL_REQUEST $line $component) | |
diff_position=$(get_line_in_diff $PULL_REQUEST "$file" "$source") | |
fi | |
url="$SONAR_URL/project/issues?id=$PROJECT_NAME&open=$id&pullRequest=$PULL_REQUEST&resolved=false&types=$type" | |
if [ -n "$diff_position" ]; then | |
#increment severity counter | |
((issue_severity["$severity"]++)) | |
found=1 | |
case $severity in | |
"BLOCKER") comment="$CRITICAL_IMG $message [$RULE_IMG]($url)" ;; | |
"CRITICAL") comment="$CRITICAL_IMG $message [$RULE_IMG]($url)" ;; | |
"MAYOR") comment="$MAYOR_IMG $message [$RULE_IMG]($url)" ;; | |
"MINOR") comment="$MINOR_IMG $message [$RULE_IMG]($url)" ;; | |
*) comment="- $INFO_IMG $message [$RULE_IMG]($url)" ;; | |
esac | |
REVIEWS=$(echo $REVIEWS | jq ". + [{\"commit_id\": \"$COMMIT_ID\", \"body\":\"$comment\", \"path\": \"$file\", \"position\": $diff_position}]") | |
else | |
if [ -n "$line" ]; then | |
filename="$(basename $file)#L${line}:" | |
else | |
filename="$(basename $file):" | |
fi | |
case $severity in | |
"BLOCKER") comment="- $CRITICAL_IMG $filename $message [$RULE_IMG]($url)\n" ;; | |
"CRITICAL") comment="- $CRITICAL_IMG $filename $message [$RULE_IMG]($url)\n" ;; | |
"MAYOR") comment="- $MAYOR_IMG $filename $message [$RULE_IMG]($url)\n" ;; | |
"MINOR") comment="- $MINOR_IMG $filename $message [$RULE_IMG]($url)\n" ;; | |
*) comment="- $INFO_IMG $filename $message [$RULE_IMG]($url)\n" ;; | |
esac | |
ADDITIONAL_COMMENTS=$(echo "$ADDITIONAL_COMMENTS" | jq --compact-output ". + [\"$comment\"]") | |
fi | |
if [ $DEBUG -eq 1 ]; then | |
echo -e "\tseverity: $severity" | |
echo -e "\tline: $line" | |
echo -e "\trule: $rule" | |
echo -e "\tcomponent: $component" | |
echo -e "\tmessage: $message" | |
echo -e "\ttype: $type" | |
echo -e "\tfile: $file" | |
echo -e "\tsource code: '$source'" | |
echo -e "\tdiff position: $diff_position" | |
echo -e "\turl: $url" | |
echo -e "\tfound: $found" | |
if [ $idx -ne $count ]; then | |
echo "" # new line | |
fi | |
fi | |
done | |
fi | |
# Save coments in file grouping by file and posiion | |
echo $REVIEWS | jq " . | group_by(.path, .position, .commit_id) | map({\ | |
path: .[0].path,\ | |
position: .[0].position,\ | |
commit_id: .[0].commit_id,\ | |
body: map(.body) | join(\"\n\")\ | |
})" > "/tmp/${PULL_REQUEST}.reviews.json" | |
#START Create comment summary in /tmp/$PULL_REQUEST.summary.json | |
global_comment="[]" | |
total=0 | |
for i in "${!issue_severity[@]}" | |
do | |
total=$((total+${issue_severity[$i]})) | |
done | |
global_comment=$(echo $global_comment | jq ". + [\"SonarQube analysis reported $total issues\n\n\"]") | |
for i in "${!issue_severity[@]}" | |
do | |
case $i in | |
"BLOCKER") comment="$CRITICAL_IMG ${issue_severity[$i]}" ;; | |
"CRITICAL") comment="$CRITICAL_IMG ${issue_severity[$i]}" ;; | |
"MAYOR") comment="$MAYOR_IMG ${issue_severity[$i]}" ;; | |
"MINOR") comment="$MINOR_IMG ${issue_severity[$i]}" ;; | |
*) comment="$INFO_IMG ${issue_severity[$i]}" ;; | |
esac | |
global_comment=$(echo $global_comment | jq ". + [\"- $comment ${i,,}\n\"]") | |
done | |
additionals=$(echo $ADDITIONAL_COMMENTS | jq -r length) | |
if [ "$additionals" -gt 0 ]; then | |
if [ "$additionals" -gt 1 ]; then | |
global_comment=$(echo $global_comment | jq ". + [\"\n**$additionals extra issues**\n\"]") | |
fi | |
global_comment=$(echo $global_comment | jq ". + [\"\nNote: The following issues were found on lines that were not modified in the pull request. Because these issues can't be reported as line comments, they are summarized here:\n\"]") | |
global_comment=$(echo $global_comment | jq ". + $ADDITIONAL_COMMENTS") | |
fi | |
echo $global_comment | jq '. | join("") | { body: .}' > "/tmp/${PULL_REQUEST}.sumary.json" | |
# End Create comment summary | |
get_github_reviews $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $PULL_REQUEST > /tmp/${PULL_REQUEST}.github.reviews.json | |
get_github_comments $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $PULL_REQUEST > /tmp/${PULL_REQUEST}.github.comments.json | |
# ****************************************** | |
# at this point we have the following files | |
# - /tmp/${PULL_REQUEST}.diff -> pull request diff | |
# - /tmp/${PULL_REQUEST}.reviews.json -> json with all reviews | |
# - /tmp/${PULL_REQUEST}.sumary.json -> json with comment summary | |
# - /tmp/${PULL_REQUEST}.github.reviews.json -> json with all reviews in github | |
# - /tmp/${PULL_REQUEST}.github.sumary.json -> json with comments in github | |
# check if reviews already exists | |
reviews=$(cat /tmp/${PULL_REQUEST}.reviews.json | jq '.') | |
count=$(echo $reviews | jq length) | |
if [ $count -gt 0 ]; then | |
count=$((count-1)) | |
for idx in $(seq 0 $count); | |
do | |
echo "Check if comment $idx exists in github" | |
review=$(echo $reviews | jq ".[$idx]") | |
position=$(echo $review | jq -r '.position') | |
path=$(echo $review | jq -r '.path') | |
body=$(echo $review | jq -r '.body') | |
q=$(cat "/tmp/${PULL_REQUEST}.github.reviews.json" | jq ".| [.[] | select (.original_position == $position) \ | |
| select(.path == \"$path\")] | length") | |
if [ "$q" -eq 0 ]; then | |
echo "Review not found in github" | |
curl -s -H "Authorization: token $GITHUB_TOKEN" \ | |
-X POST \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
https://api.github.com/repos/$GITHUB_ORG/$PROJECT_NAME/pulls/$PULL_REQUEST/comments \ | |
-d ''"$review"'' | |
else | |
body_comment=$(cat "/tmp/${PULL_REQUEST}.github.reviews.json" | jq -r ".| [.[] | select (.original_position == $position) \ | |
| select(.path == \"$path\")] | .[0].body") | |
if [ "$body" == "$body_comment" ]; then | |
echo "Review found in github, skiping" | |
else | |
echo "Review found in github, but body is diferent" | |
fi | |
fi | |
done | |
fi | |
# filter comment | |
echo "Check if summary exists in github:" | |
has_comment=$(cat /tmp/${PULL_REQUEST}.github.comments.json | jq '.| [.[] | select(.body | contains("SonarQube analysis reported"))] | length') | |
if [ "$has_comment" -gt 0 ]; then | |
echo "Found." | |
else | |
echo "Not found." | |
add_github_comment_summary $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $PULL_REQUEST | |
fi | |
status=$(get_qualitygate_status_from_sonar $SONAR_TOKEN $PROJECT_NAME $PULL_REQUEST) | |
if [ "$status" == 'ERROR' ]; then | |
update_github_status_error $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $COMMIT_ID "$violations" | |
else | |
update_github_status_success $GITHUB_TOKEN $GITHUB_ORG $PROJECT_NAME $COMMIT_ID "$violations" | |
fi | |
# Remove all temp files | |
rm "/tmp/${PULL_REQUEST}.reviews.json" | |
rm "/tmp/${PULL_REQUEST}.diff" | |
rm "/tmp/${PULL_REQUEST}.sumary.json" | |
rm "/tmp/${PULL_REQUEST}.github.comments.json" | |
rm "/tmp/${PULL_REQUEST}.github.reviews.json" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment