├── .gitignore ├── .travis.yml ├── assets ├── helpers │ ├── askpass.sh │ ├── git.sh │ ├── utils.sh │ └── bitbucket.sh ├── in ├── check └── out ├── .editorconfig ├── scripts ├── install_git_lfs.sh └── test ├── Dockerfile ├── NOTICE.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .ignore 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: scripts/test 7 | -------------------------------------------------------------------------------- /assets/helpers/askpass.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Private keys with passphrases are not supported." >&2 3 | exit 1 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /scripts/install_git_lfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | _main() { 6 | local tmpdir 7 | tmpdir="$(mktemp -d git_lfs_install.XXXXXX)" 8 | 9 | cd "$tmpdir" 10 | curl -Lo git.tar.gz https://github.com/github/git-lfs/releases/download/v1.1.2/git-lfs-linux-amd64-1.1.2.tar.gz 11 | gunzip git.tar.gz 12 | tar xf git.tar 13 | mv git-lfs-1.1.2/git-lfs /usr/bin 14 | cd .. 15 | rm -rf "$tmpdir" 16 | git lfs install --skip-smudge 17 | } 18 | 19 | _main "$@" 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk --no-cache add \ 4 | bash=4.4.19-r1 \ 5 | ca-certificates=20190108-r0 \ 6 | curl=7.61.1-r3 \ 7 | git=2.15.4-r0 \ 8 | jq=1.5-r5 \ 9 | openssh-client=7.5_p1-r10 10 | 11 | # can't `git pull` unless we set these 12 | RUN git config --global user.email "git@localhost" && \ 13 | git config --global user.name "git" 14 | 15 | COPY scripts/install_git_lfs.sh install_git_lfs.sh 16 | RUN ./install_git_lfs.sh 17 | 18 | COPY assets /opt/resource 19 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016 Laurent Verbruggen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | set +e 6 | docker run --interactive --rm hadolint/hadolint < Dockerfile 7 | docker run --interactive --rm --volume "$PWD:/mnt" koalaman/shellcheck \ 8 | --color=always \ 9 | /mnt/assets/check \ 10 | /mnt/assets/helpers/askpass.sh \ 11 | /mnt/assets/helpers/bitbucket.sh \ 12 | /mnt/assets/helpers/git.sh \ 13 | /mnt/assets/helpers/utils.sh \ 14 | /mnt/assets/in \ 15 | /mnt/assets/out \ 16 | /mnt/install_git_lfs.sh \ 17 | /mnt/scripts/test 18 | set -e 19 | 20 | docker build --pull --rm --tag mm62/concourse-bitbucket-pullrequest-resource-test . 21 | docker rmi mm62/concourse-bitbucket-pullrequest-resource-test 22 | -------------------------------------------------------------------------------- /assets/helpers/git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | load_pubkey() { 4 | local private_key_path=$TMPDIR/git-resource-private-key 5 | 6 | (jq -r '.source.private_key // empty' < $1) > $private_key_path 7 | 8 | if [ -s $private_key_path ]; then 9 | chmod 0600 $private_key_path 10 | 11 | eval $(ssh-agent) >/dev/null 2>&1 12 | 13 | SSH_ASKPASS=$ASSETS/helpers/askpass.sh DISPLAY= ssh-add $private_key_path >/dev/null 14 | 15 | mkdir -p ~/.ssh 16 | cat > ~/.ssh/config < $HOME/.netrc 116 | fi 117 | 118 | if [ "$token" != "" ]; then 119 | git config --global --add http.extraHeader "Authorization: Bearer $token" 120 | TOKEN="-H \"Authorization: Bearer $token\"" 121 | fi 122 | } 123 | -------------------------------------------------------------------------------- /assets/helpers/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TMPDIR=${TMPDIR:-/tmp} 4 | 5 | hash() { 6 | sha=$(which sha256sum || which shasum) 7 | echo "$1" | $sha | awk '{ print $1 }' 8 | } 9 | 10 | contains_element() { 11 | local e 12 | for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done 13 | return 1 14 | } 15 | 16 | hide_password() { 17 | if ! echo "$1" | jq -c '.' > /dev/null 2> /dev/null; then 18 | echo "(invalid json: $1)>" 19 | exit 1 20 | fi 21 | 22 | local paths=$(echo "${1:-{\} }" | jq -c "paths") 23 | local query="" 24 | if [ -n "$paths" ]; then 25 | while read path; do 26 | local parts=$(echo "$path" | jq -r '.[]') 27 | local selection="" 28 | local found="" 29 | while read part; do 30 | selection+=".$part" 31 | if [ "$part" == "password" ]; then 32 | found="true" 33 | fi 34 | done <<< "$parts" 35 | 36 | if [ -n "$found" ]; then 37 | query+=" | jq -c '$selection = \"*******\"'" 38 | fi 39 | done <<< "$paths" 40 | fi 41 | 42 | local json="${1//\"/\\\"}" 43 | eval "echo \"$json\" $query" 44 | } 45 | 46 | log() { 47 | # $1: message 48 | # $2: json 49 | local message="$(date -u '+%F %T') - $1" 50 | if [ -n "$2" ]; then 51 | message+=" - $(hide_password "$2")" 52 | fi 53 | echo -e "$message" >&2 54 | } 55 | 56 | tmp_file() { 57 | echo "$TMPDIR/bitbucket-pullrequest-resource-$1" 58 | } 59 | 60 | tmp_file_unique() { 61 | mktemp "$TMPDIR/bitbucket-pullrequest-resource-$1.XXXXXX" 62 | } 63 | 64 | # 65 | # URI parsing function 66 | # 67 | # The function creates global variables with the parsed results. 68 | # It returns 0 if parsing was successful or non-zero otherwise. 69 | # 70 | # [schema://][user[:password]@]host[:port][/path][?[arg1=val1]...][#fragment] 71 | # 72 | # Reference: http://wp.vpalos.com/537/uri-parsing-using-bash-built-in-features/ 73 | # 74 | uri_parser() { 75 | # uri capture 76 | uri="$@" 77 | 78 | # safe escaping 79 | uri="${uri//\`/%60}" 80 | uri="${uri//\"/%22}" 81 | 82 | # top level parsing 83 | pattern='^(([a-z]{3,5})://)?((([^:\/]+)(:([^@\/]*))?@)?([^:\/?]+)(:([0-9]+))?)(\/[^?]*)?(\?[^#]*)?(#.*)?$' 84 | [[ "$uri" =~ $pattern ]] || return 1; 85 | 86 | # component extraction 87 | uri=${BASH_REMATCH[0]} 88 | uri_schema=${BASH_REMATCH[2]} 89 | uri_address=${BASH_REMATCH[3]} 90 | uri_user=${BASH_REMATCH[5]} 91 | uri_password=${BASH_REMATCH[7]} 92 | uri_host=${BASH_REMATCH[8]} 93 | uri_port=${BASH_REMATCH[10]} 94 | uri_path=${BASH_REMATCH[11]} 95 | uri_query=${BASH_REMATCH[12]} 96 | uri_fragment=${BASH_REMATCH[13]} 97 | 98 | # path parsing 99 | count=0 100 | path="$uri_path" 101 | pattern='^/+([^/]+)' 102 | while [[ $path =~ $pattern ]]; do 103 | eval "uri_parts[$count]=\"${BASH_REMATCH[1]}\"" 104 | path="${path:${#BASH_REMATCH[0]}}" 105 | count=$((count + 1)) 106 | done 107 | 108 | # query parsing 109 | count=0 110 | query="$uri_query" 111 | pattern='^[?&]+([^= ]+)(=([^&]*))?' 112 | while [[ $query =~ $pattern ]]; do 113 | eval "uri_args[$count]=\"${BASH_REMATCH[1]}\"" 114 | eval "uri_arg_${BASH_REMATCH[1]}=\"${BASH_REMATCH[3]}\"" 115 | query="${query:${#BASH_REMATCH[0]}}" 116 | count=$((count + 1)) 117 | done 118 | 119 | # return success 120 | return 0 121 | } 122 | 123 | date_from_epoch_seconds() { 124 | # Mac OS X: 125 | #date -r $1 126 | date -d @$1 127 | } 128 | 129 | # http://stackoverflow.com/questions/296536/how-to-urlencode-data-for-curl-command 130 | rawurlencode() { 131 | local string="${1}" 132 | local strlen=${#string} 133 | local encoded="" 134 | local pos c o 135 | 136 | for (( pos=0 ; pos /dev/null 2>&1; then 172 | killall ssh-agent > /dev/null 2>&1 173 | fi 174 | } 175 | 176 | trap cleanup EXIT 177 | -------------------------------------------------------------------------------- /assets/in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | ASSETS=$(cd "$(dirname "$0")" && pwd) 10 | source $ASSETS/helpers/git.sh 11 | source $ASSETS/helpers/utils.sh 12 | source $ASSETS/helpers/bitbucket.sh 13 | 14 | # for all temporary files in 'check' 15 | tmpfile() { 16 | tmp_file_unique "in-$1" 17 | } 18 | 19 | destination=$1 20 | 21 | if [ -z "$destination" ]; then 22 | echo "usage: $0 " >&2 23 | exit 1 24 | fi 25 | 26 | # for jq 27 | PATH=/usr/local/bin:$PATH 28 | 29 | payload=$(tmpfile request) 30 | 31 | cat > "$payload" <&0 32 | 33 | load_pubkey "$payload" 34 | configure_credentials "$payload" 35 | 36 | skip_ssl_verification=$(jq -r '.source.skip_ssl_verification // false' < ${payload}) 37 | uri=$(jq -r '.source.uri // ""' < "$payload") 38 | git_config_payload=$(jq -r '.source.git_config // []' < "$payload") 39 | commit_verification_key_ids=$(jq -r '(.source.commit_verification_key_ids // [])[]' < "$payload") 40 | commit_verification_keys=$(jq -r '(.source.commit_verification_keys // [])[]' < "$payload") 41 | gpg_keyserver=$(jq -r '.source.gpg_keyserver // "hkp://keys.gnupg.net/"' < "$payload") 42 | 43 | depth=$(jq -r '(.params.depth // 0)' < "$payload") 44 | submodules=$(jq -r '(.params.submodules // "all")' < "$payload") 45 | disable_git_lfs=$(jq -r '(.params.disable_git_lfs // false)' < "$payload") 46 | 47 | prq_id=$(jq -r '.version.id // ""' < "$payload") 48 | prq_date=$(jq -r '.version.date // ""' < "$payload") 49 | 50 | configure_git_ssl_verification "$skip_ssl_verification" 51 | configure_git_global "${git_config_payload}" 52 | 53 | if [ -z "$uri" ]; then 54 | log "invalid payload (missing uri):" "$(cat "$payload")" 55 | exit 1 56 | fi 57 | 58 | if [ -z "$prq_id" ]; then 59 | log "invalid payload (missing pull request id):" "$(cat "$payload")" 60 | exit 1 61 | fi 62 | 63 | depthflag="" 64 | if test "$depth" -gt 0 2> /dev/null; then 65 | depthflag="--depth $depth" 66 | fi 67 | 68 | branch="pull-requests/${prq_id}/merge" 69 | 70 | log "Cloning $uri in $destination" 71 | git clone $depthflag "$uri" "$destination" 72 | cd "$destination" 73 | 74 | git fetch $depthflag origin "+refs/${branch}:refs/remotes/origin/${branch}" 75 | git checkout -B $branch origin/$branch 76 | 77 | ref=$(git rev-parse HEAD) 78 | 79 | invalid_key() { 80 | echo "Invalid GPG key in: ${commit_verification_keys}" 81 | exit 2 82 | } 83 | 84 | commit_not_signed() { 85 | commit_id=$(git rev-parse ${ref}) 86 | echo "The commit ${commit_id} is not signed" 87 | exit 1 88 | } 89 | 90 | if [ ! -z "${commit_verification_keys}" ] || [ ! -z "${commit_verification_key_ids}" ] ; then 91 | if [ ! -z "${commit_verification_keys}" ]; then 92 | echo "${commit_verification_keys}" | gpg --batch --import || invalid_key "${commit_verification_keys}" 93 | fi 94 | if [ ! -z "${commit_verification_key_ids}" ]; then 95 | echo "${commit_verification_key_ids}" | \ 96 | xargs --no-run-if-empty -n1 gpg --batch --keyserver $gpg_keyserver --recv-keys 97 | fi 98 | git verify-commit $(git rev-list -n 1 $ref) || commit_not_signed 99 | fi 100 | 101 | if [ "$disable_git_lfs" != "true" ]; then 102 | git lfs fetch 103 | git lfs checkout 104 | fi 105 | 106 | git clean --force --force -d 107 | 108 | if [ "$submodules" == "all" ]; then 109 | git submodule update --init $depthflag --recursive 110 | elif [ "$submodules" != "none" ]; then 111 | submodules=$(echo $submodules | jq -r '(.[])') 112 | for submodule in $submodules; do 113 | git submodule update --init $depthflag --recursive $submodule 114 | done 115 | fi 116 | 117 | if [ "$disable_git_lfs" != "true" ]; then 118 | git submodule foreach "git lfs fetch && git lfs checkout" 119 | fi 120 | 121 | # calculate source and target commit 122 | source_commit=$(git rev-list --parents -1 $ref | awk '{print $3}') 123 | target_commit=$(git rev-list --parents -1 $ref | awk '{print $2}') 124 | 125 | if [ -z "$source_commit" ]; then 126 | log "Unable to determine source commit from merge commit $ref. Please verify depth configuration." 127 | exit 1 128 | fi 129 | 130 | if [ -z "$target_commit" ]; then 131 | log "Unable to determine target commit from merge commit $ref. Please verify depth configuration." 132 | exit 1 133 | fi 134 | 135 | # parse uri and retrieve host 136 | uri_parser "$uri" 137 | repo_host="${uri_schema}://${uri_address}" 138 | repo_host=${repo_host}$(getBasePathOfBitbucket) 139 | 140 | # determine repository name for calling REST api 141 | repo_name=$(basename "$uri" | sed "s/.git$//") 142 | repo_project=$(basename $(dirname "$uri")) 143 | 144 | # verify target branch of prq 145 | prq=$(bitbucket_pullrequest "$repo_host" "$repo_project" "$repo_name" "$prq_id" "" "$skip_ssl_verification") 146 | 147 | if [ "$prq" != "NO_SUCH_PULL_REQUEST" ] && \ 148 | [ "$prq" != "ALREADY_MERGED" ] && \ 149 | [ "$prq" != "DECLINED" ]; then 150 | branch=$(echo "$prq" | jq -r '.fromRef.displayId') 151 | fi 152 | 153 | 154 | # expose configuration of pull request that can be used in container 155 | git config --add pullrequest.id $prq_id 156 | git config --add pullrequest.source $source_commit 157 | git config --add pullrequest.target $target_commit 158 | git config --add pullrequest.merge $ref 159 | git config --add pullrequest.date "$prq_date" 160 | git config --add pullrequest.branch "$branch" 161 | 162 | jq -n "{ 163 | version: $(jq '.version' < "$payload"), 164 | metadata: $(pullrequest_metadata "$prq_id" "$uri" "$skip_ssl_verification") 165 | }" >&3 166 | -------------------------------------------------------------------------------- /assets/check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | ASSETS=$(cd "$(dirname "$0")" && pwd) 10 | source $ASSETS/helpers/git.sh 11 | source $ASSETS/helpers/utils.sh 12 | source $ASSETS/helpers/bitbucket.sh 13 | 14 | # for all temporary files in 'check' 15 | tmpfile() { 16 | tmp_file "check-$1" 17 | } 18 | 19 | # for jq 20 | PATH=/usr/local/bin:$PATH 21 | 22 | payload=$(tmpfile request) 23 | 24 | cat > "$payload" <&0 25 | 26 | log "Configuring git credentials" 27 | load_pubkey "$payload" 28 | 29 | configure_credentials "$payload" 30 | 31 | log "Parsing payload" 32 | uri=$(jq -r '.source.uri // ""' < "$payload") 33 | skip_ssl_verification=$(jq -r '.source.skip_ssl_verification // false' < ${payload}) 34 | git_config_payload=$(jq -r '.source.git_config // []' < "$payload") 35 | 36 | only_for_branch=$(jq -r '.source.only_for_branch // "."' < "$payload") 37 | only_without_conflicts=$(jq -r '.source.only_without_conflicts // "true"' < "$payload") 38 | only_when_mergeable=$(jq -r '.source.only_when_mergeable // "false"' < "$payload") 39 | only_when_asked=$(jq -r '.source.only_when_asked // "false"' < "$payload") 40 | sleep_between_fetches=$(jq -r '.source.sleep_between_fetches // "0"' < "$payload") 41 | sleep_between_fetches=$(echo "$sleep_between_fetches" | sed 's/[^0-9\.]//g') 42 | rebuild_when_target_changed=$(jq -r '.source.rebuild_when_target_changed // "false"' < "$payload") 43 | rebuild_phrase=$(jq -r '.source.rebuild_phrase // "test this please"' < "$payload") 44 | CURRENT_VERSION_DATE=$(jq -r '.version.date // "0"' < "$payload") 45 | 46 | configure_git_ssl_verification "$skip_ssl_verification" 47 | configure_git_global "${git_config_payload}" 48 | 49 | if [ -z "$uri" ]; then 50 | log "invalid payload (missing uri):" "$(cat $payload)" 51 | exit 1 52 | fi 53 | 54 | # if option 'rebuild_when_target_changed' is enabled take merge branch since commit will always change for changes on target branch 55 | prq_branch="from" 56 | if [ "$rebuild_when_target_changed" == "true" ]; then 57 | prq_branch="merge" 58 | fi 59 | 60 | # collect all pull requests from uri 61 | REMOTES=$(git ls-remote "$uri") 62 | set +e 63 | PULL_REQUESTS=$(echo "$REMOTES" | grep -E "/pull\\-requests/[0-9]+/${prq_branch}") 64 | set -e 65 | 66 | 67 | versions="[]" 68 | if [ -n "$PULL_REQUESTS" ]; then 69 | log "Calculating repository specifics" 70 | # determine repository name for calling REST api 71 | repo_name=$(basename "$uri" | sed "s/.git$//") 72 | repo_project=$(basename $(dirname "$uri")) 73 | 74 | # parse uri and retrieve host 75 | uri_parser "$uri" 76 | repo_host="${uri_schema}://${uri_address}" 77 | 78 | repo_host=${repo_host}$(getBasePathOfBitbucket) 79 | 80 | versions="[]" 81 | while read pull_request ; do 82 | sleep $sleep_between_fetches # throttle requests to avoid excessive load to server 83 | log "Verifying pull request" 84 | # determine hash and prq number from grep 85 | prq_number=$(echo "$pull_request" | sed -E "s/^.*\/pull-requests\/([0-9]+)\/.*$/\\1/") 86 | prq_hash=$(echo "$pull_request" | awk '{print $1}') 87 | 88 | # verify target branch of prq 89 | prq=$(bitbucket_pullrequest "$repo_host" "$repo_project" "$repo_name" "$prq_number" "" "$skip_ssl_verification") 90 | 91 | if [ "$prq" = "NO_SUCH_PULL_REQUEST" ]; then 92 | continue 93 | fi 94 | 95 | PULL_REQUEST_DATE=$(echo "$prq" | jq -r '.updatedDate') 96 | 97 | log "Pull request #${prq_number}" 98 | 99 | prq_to_branch=$(echo "$prq" | jq -r '.toRef.displayId') 100 | if [[ "$prq_to_branch" =~ $only_for_branch ]]; then 101 | 102 | if [ "$only_when_mergeable" == "true" -o "$only_without_conflicts" == "true" ]; then 103 | prq_merge=$(bitbucket_pullrequest_merge "$repo_host" "$repo_project" "$repo_name" "$prq_number" "" "$skip_ssl_verification") 104 | 105 | if [ "$prq_merge" = "ALREADY_MERGED" ] || [ "$prq_merge" = "DECLINED" ]; then 106 | continue 107 | fi 108 | 109 | # verify if prq has merge conflicts 110 | conflicted=$(echo "$prq_merge" | jq -r '.conflicted') 111 | if [ "$conflicted" == "true" -a "$only_without_conflicts" == "true" ]; then continue; fi 112 | 113 | # verify if prq is mergeable (e.g. enough approvals, tasks done, ...) 114 | mergeable=$(echo "$prq_merge" | jq -r '.canMerge') 115 | if [ "$mergeable" == "false" -a "$only_when_mergeable" == "true" ]; then continue; fi 116 | fi 117 | 118 | # edit timestamp to version to force new build when rebuild_phrase is included in comments 119 | skip_build=false 120 | comments=$(bitbucket_pullrequest_overview_comments "$repo_host" "$repo_project" "$repo_name" "$prq_number" "" "$skip_ssl_verification" | jq -c '.[]') 121 | if [ -n "$comments" ]; then 122 | while read -r comment; do 123 | text=$(echo "$comment" | jq -r '.text') 124 | 125 | # check for progress or finished messages => do not include in versions when available 126 | if bitbucket_pullrequest_comment_commit_match "$text" "$prq_hash"; then 127 | log "Skipping PRQ #$prq_number since already handled" 128 | skip_build=true 129 | break 130 | fi 131 | 132 | # edit timestamp to force new build when rebuild_phrase is included in comments 133 | if echo "$text" | grep -Ec "$rebuild_phrase" > /dev/null; then 134 | PULL_REQUEST_DATE=$(echo "$comment" | jq -r '.createdDate') 135 | break 136 | fi 137 | done <<< "$comments" 138 | fi 139 | 140 | if [ "$PULL_REQUEST_DATE" -lt "$CURRENT_VERSION_DATE" ]; then 141 | continue 142 | fi 143 | 144 | # add prq to versions 145 | if [ "$skip_build" == "false" ]; then 146 | versions+=" + [{ id: \"$prq_number\", hash: \"$prq_hash\", date: \"$PULL_REQUEST_DATE\" }]" 147 | fi 148 | fi 149 | done <<< "$PULL_REQUESTS" 150 | fi 151 | 152 | # On the first request return only the current version. 153 | if [ "$CURRENT_VERSION_DATE" -eq "0" ]; then 154 | jq -n "$versions | sort_by((.date | tonumber), (.id | tonumber), .hash) | .[-1:]" > /tmp/check_result 155 | else 156 | jq -n "$versions | sort_by((.date | tonumber), (.id | tonumber), .hash)" > /tmp/check_result 157 | fi 158 | 159 | cat /tmp/check_result >&3 160 | -------------------------------------------------------------------------------- /assets/out: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | ASSETS=$(cd "$(dirname "$0")" && pwd) 10 | # shellcheck source=helpers/git.sh 11 | source $ASSETS/helpers/git.sh 12 | # shellcheck source=helpers/utils.sh 13 | source $ASSETS/helpers/utils.sh 14 | # shellcheck source=helpers/bitbucket.sh 15 | source $ASSETS/helpers/bitbucket.sh 16 | 17 | # for all temporary files in 'out' 18 | tmpfile() { 19 | tmp_file_unique "out-$1" 20 | } 21 | 22 | source=$1 23 | 24 | if [ -z "$source" ]; then 25 | echo "usage: $0 " 26 | exit 1 27 | fi 28 | 29 | # for jq 30 | PATH=/usr/local/bin:$PATH 31 | 32 | payload=$(tmpfile request) 33 | 34 | cat > "$payload" <&0 35 | 36 | load_pubkey "$payload" 37 | configure_credentials "$payload" 38 | 39 | skip_ssl_verification=$(jq -r '.source.skip_ssl_verification // false' < ${payload}) 40 | uri=$(jq -r '.source.uri // ""' < "$payload") 41 | git_config_payload=$(jq -r '.source.git_config // []' < "$payload") 42 | rebuild_when_target_changed=$(jq -r '.source.rebuild_when_target_changed // "false"' < "$payload") 43 | rebuild_phrase=$(jq -r '.source.rebuild_phrase // "test this please"' < "$payload") 44 | create_comments=$(jq -r '.source.create_comments // "false"' < "$payload") 45 | 46 | path=$(jq -r '.params.path // ""' < "$payload") 47 | status=$(jq -r '.params.status // ""' < "$payload") 48 | additionnal_comment=$(jq -r '.params.comment // ""' < "$payload") 49 | additionnal_comment_file=$(jq -r '.params.commentFile // ""' < "$payload") 50 | 51 | configure_git_ssl_verification "$skip_ssl_verification" 52 | configure_git_global "${git_config_payload}" 53 | 54 | if [ -z "$uri" ]; then 55 | log "invalid payload (missing uri)" 56 | exit 1 57 | fi 58 | 59 | if [ -z "$path" ]; then 60 | log "invalid payload (missing path)" 61 | exit 1 62 | fi 63 | 64 | if [ -z "$status" ]; then 65 | log "invalid payload (missing status)" 66 | exit 1 67 | fi 68 | 69 | cd "$source" 70 | 71 | if [ -n "$additionnal_comment_file" ]; then 72 | additionnal_comment="$(<${additionnal_comment_file})" 73 | fi 74 | 75 | cd "$path" 76 | 77 | merge_commit=$(git rev-parse HEAD) 78 | ls_remote=$(git ls-remote "$uri") 79 | 80 | # collect prq id and branch name from git config stored in git config (during get step) 81 | # included cat to catch error 82 | prq_number=$(git config --get pullrequest.id | cat) 83 | branch=$(git config --get pullrequest.branch | cat) 84 | 85 | if [ -z "$prq_number" ]; then 86 | prqs=$(echo "$ls_remote" | grep -E "/pull\-requests/[0-9]+" | grep "$merge_commit" | cat) 87 | 88 | # search for prq from commits in git ls-remote that match current commit 89 | # if none match we can't return a version since we can't determine the correct prq 90 | prq_number=$(echo "$prqs" | sed -E "s/^.*\/pull-requests\/([0-9]+)\/.*$/\\1/") 91 | fi 92 | 93 | if [ -z "$prq_number" ]; then 94 | log "Can't determine pull request id for commit $merge_commit" 95 | exit 1 96 | fi 97 | 98 | # if option 'rebuild_when_target_changed' is enabled take merge branch since commit will always change for changes on target branch 99 | prq_branch="from" 100 | if [ "$rebuild_when_target_changed" == "true" ]; then 101 | prq_branch="merge" 102 | fi 103 | 104 | prq_hash=$(echo "$ls_remote" | grep -E "/pull\-requests/${prq_number}/${prq_branch}" | awk '{print $1}') 105 | 106 | if [ -z "$prq_hash" ]; then 107 | log "Failed to determine pull request hash from id $prq_number in \n$ls_remote" 108 | exit 1 109 | fi 110 | 111 | case "$status" in 112 | success) 113 | build_state="SUCCESSFUL" 114 | ;; 115 | failure) 116 | build_state="FAILED" 117 | ;; 118 | pending) 119 | build_state="INPROGRESS" 120 | ;; 121 | *) 122 | log "status '$status' is not supported -- only success, failure or pending" 123 | exit 1 124 | esac 125 | 126 | # set build status on commit in bitbucket, this has to be the latest source commit or the pull request won't pick it up 127 | source_commit=$(git rev-list --parents -1 $merge_commit | awk '{print $3}') 128 | target_commit=$(git rev-list --parents -1 $merge_commit | awk '{print $2}') 129 | 130 | # determine repository name for calling REST api 131 | repo_name=$(basename "$uri" | sed "s/.git$//") 132 | repo_project=$(basename $(dirname "$uri")) 133 | 134 | # parse uri and retrieve host 135 | uri_parser "$uri" 136 | repo_host="${uri_schema}://${uri_address}"$(getBasePathOfBitbucket) 137 | 138 | # include ATC_EXTERNAL_URL in build status key, different sources should have different builds 139 | build_key="$BUILD_TEAM_NAME-$BUILD_PIPELINE_NAME-$BUILD_JOB_NAME-$ATC_EXTERNAL_URL" 140 | build_name="Concourse $BUILD_TEAM_NAME: $BUILD_PIPELINE_NAME - $BUILD_JOB_NAME - #$BUILD_NAME" 141 | build_url="$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME" 142 | build_description="Concourse build for PRQ #$prq_number @ $ATC_EXTERNAL_URL" 143 | 144 | data=$(jq -cn "{ 145 | state: \"$build_state\", 146 | key: \"$build_key\", 147 | name: \"$build_name\", 148 | url: \"$build_url\", 149 | description: \"$build_description\" 150 | }") 151 | 152 | # set commit build status for source commit 153 | bitbucket_pullrequest_commit_status "$repo_host" "$source_commit" "$data" "" "" "$skip_ssl_verification" 154 | 155 | # use the pullrequest date stored in git config in get 156 | PULL_REQUEST_DATE=$(git config --get pullrequest.date | cat) 157 | 158 | # Add branch name to additional comment 159 | if [ -n "$additionnal_comment" ]; then 160 | additionnal_comment="${additionnal_comment//\[\[BRANCH\]\]/$branch}" 161 | fi 162 | 163 | # add comment to pull request to track if build was started/finished 164 | comment_message=$(bitbucket_pullrequest_progress_comment "$status" "$prq_hash" "$source_commit" "$target_commit", "$additionnal_comment") 165 | comments=$(bitbucket_pullrequest_overview_comments "$repo_host" "$repo_project" "$repo_name" "$prq_number" "" "$skip_ssl_verification" | jq -c '.[]') 166 | commented="" 167 | if [ -n "$comments" ]; then 168 | while read -r comment; do 169 | id=$(echo "$comment" | jq -r '.id') 170 | text=$(echo "$comment" | jq -r '.text') 171 | version=$(echo "$comment" | jq -r '.version') 172 | 173 | # check for progress messages => if pull request number matches then edit comment (instead of creating a new one) 174 | if bitbucket_pullrequest_progress_commit_match "$text" "$prq_hash" "Started" && [ "$create_comments" == "true" ]; then 175 | bitbucket_pullrequest_update_comment_status "$repo_host" "$repo_project" "$repo_name" "$prq_number" "$comment_message" "$id" "$version" "" "$skip_ssl_verification" >/dev/null 176 | commented=true 177 | break 178 | fi 179 | done <<< "$comments" 180 | fi 181 | 182 | if [ -z "$commented" ] && [ "$create_comments" == "true" ]; then 183 | bitbucket_pullrequest_add_comment_status "$repo_host" "$repo_project" "$repo_name" "$prq_number" "$comment_message" "" "$skip_ssl_verification" >/dev/null 184 | fi 185 | 186 | jq -n "{ 187 | version: { 188 | id: \"$prq_number\", 189 | hash: \"$prq_hash\", 190 | date: \"$PULL_REQUEST_DATE\" 191 | }, 192 | metadata: $(pullrequest_metadata "$prq_number" "$uri" "$skip_ssl_verification") 193 | }" >&3 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/mmb/concourse-bitbucket-pullrequest-resource.svg?branch=master)](https://travis-ci.org/mmb/concourse-bitbucket-pullrequest-resource) 2 | 3 | This resource is a fork of 4 | https://github.com/laurentverbruggen/concourse-bitbucket-pullrequest-resource. 5 | 6 | That resource is no longer maintained but this one will continue to be 7 | developed. 8 | 9 | # Concourse Bitbucket Pull Request Resource 10 | 11 | Tracks pull requests made to a Bitbucket repository. 12 | A status of pending, success, or failure will be set on the pull request, which must be explicitly defined in your pipeline. 13 | 14 | Currently only basic username/password authentication offers full functionality. 15 | Private key allows scanning for pull requests because this is pure git, but verifying merge status and setting status on pull requests is done through the Bitbucket REST Api for which SSL hasn't been tested yet. 16 | 17 | > This resource was made for Bitbucket Server and will probably not work for Bitbucket Cloud. 18 | 19 | ## Installing 20 | 21 | Use this resource by adding the following to the `resource_types` section of a pipeline config: 22 | 23 | ```yaml 24 | --- 25 | resource_types: 26 | - name: concourse-bitbucket-pullrequest 27 | type: docker-image 28 | source: 29 | repository: mm62/concourse-bitbucket-pullrequest-resource 30 | ``` 31 | 32 | See [concourse docs](https://concourse-ci.org/resource-types.html) for more details on adding `resource_types` to a pipeline config. 33 | 34 | ## Source Configuration 35 | 36 | * `uri`: *Required.* The location of the repository. 37 | 38 | * `private_key`: *Optional.* Private key to use when pulling/pushing. 39 | Example: 40 | ``` 41 | private_key: | 42 | -----BEGIN RSA PRIVATE KEY----- 43 | MIIEowIBAAKCAQEAtCS10/f7W7lkQaSgD/mVeaSOvSF9ql4hf/zfMwfVGgHWjj+W 44 | 45 | DWiJL+OFeg9kawcUL6hQ8JeXPhlImG6RTUffma9+iGQyyBMCGd1l 46 | -----END RSA PRIVATE KEY----- 47 | ``` 48 | 49 | * `username`: *Optional.* Username for HTTP(S) auth when pulling/pushing. 50 | This is needed when only HTTP/HTTPS protocol for git is available (which does not support private key auth) and auth is required. 51 | 52 | * `password`: *Optional.* Password for HTTP(S) auth when pulling/pushing. 53 | 54 | * `token`: *Optional.* Token for HTTP(S) auth when pulling/pushing. 55 | If you have configured a [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html), you can use it instead of your username and password. 56 | 57 | * `skip_ssl_verification`: *Optional.* Skips git ssl verification by exporting `GIT_SSL_NO_VERIFY=true`. 58 | 59 | * `git_config`: *Optional*. If specified as (list of pairs `name` and `value`) it will configure git global options, setting each name with each value. 60 | 61 | This can be useful to set options like `credential.helper` or similar. 62 | 63 | See the [`git-config(1)` manual page](https://www.kernel.org/pub/software/scm/git/docs/git-config.html) 64 | for more information and documentation of existing git options. 65 | 66 | * `only_for_branch`: *Optional.* If specified only pull requests which target those branches will be considered. 67 | It will accept a regular expression as determined by [egrep](http://linuxcommand.org/man_pages/egrep1.html). 68 | 69 | * `sleep_between_fetches`: *Optional (default: 0).* Number of seconds to sleep between checking pull requests, avoiding too frequent fetches for check method. 70 | 71 | * `only_without_conflicts`: *Optional (default: true).* If enabled only pull requests which are not in a conflicted state will be built. 72 | 73 | * `only_when_mergeable`: *Optional (default: false).* If enabled only pull requests which are mergeable (all tasks done, required number of approvers reached, ...) will be built. 74 | 75 | * `only_when_asked`: *Optional (default: false).* Only build pull request when explicitly asked for, using rebuild_phrase. 76 | 77 | * `rebuild_when_target_changed`: *Optional (default: false).* Rebuild pull requests when target branch changed instead of only when source branch changed. 78 | 79 | * `rebuild_phrase`: *Optional (default: test this please).* Regular expression as determined by [egrep](http://linuxcommand.org/man_pages/egrep1.html) will match all comments in pull request overview. 80 | If a match is found the pull request will be rebuilt. 81 | 82 | * `create_comments`: *Optional (default: false).* If true write comments with build status to pull requests. 83 | 84 | ## Behavior 85 | 86 | ### `check`: Search for pull requests to build. 87 | 88 | Check will return a version for every pull request that matches the criteria defined in source configuration. 89 | 90 | ### `in`: Clone the repository, at the given pull request merge ref 91 | 92 | Clones the repository to the destination, and locks it down to a given ref. 93 | 94 | ** IMPORTANT ** 95 | It is essential that you set the [version](https://concourse-ci.org/get-step.html#get-step-version) to `every` on the get step of your job configuration. 96 | It will allow you to build all versions instead of only the latest. 97 | 98 | Submodules are initialized and updated recursively. 99 | 100 | Note: the name of the branch from which the pull request has been created is stored in the special git config `pullrequest.branch` so that you can use it as reference in your pipeline. 101 | 102 | #### Parameters 103 | 104 | * `depth`: *Optional.* If a positive integer is given, *shallow* clone the repository using the `--depth` option. Using this flag voids your warranty. 105 | Some things will stop working unless we have the entire history. 106 | 107 | * `submodules`: *Optional.* If `none`, submodules will not be fetched. If specified as a list of paths, only the given paths will be fetched. If not specified, or if `all` is explicitly specified, all submodules are fetched. 108 | 109 | * `disable_git_lfs`: *Optional.* If `true`, will not fetch Git LFS files. 110 | 111 | #### GPG signature verification 112 | 113 | If `commit_verification_keys` or `commit_verification_key_ids` is specified in the source configuration, it will additionally verify that the resulting commit has been GPG signed by one of the specified keys. It will error if this is not the case. 114 | 115 | ### `out`: Update the status of a pull request 116 | 117 | Set the status message on specified pull request. 118 | 119 | #### Parameters 120 | 121 | * `path`: *Required.* The path of the repository to reference the pull request. 122 | 123 | * `status`: *Required.* The status of success, failure or pending. 124 | 125 | * [`on_success`](https://concourse-ci.org/on-success-step-hook.html) and [`on_failure`](https://concourse-ci.org/on-failure-step-hook.html) triggers may be useful for you when you wanted to reflect build result to the pull request (see the example below). 126 | 127 | * `comment`: *Optional.* A custom comment that you want added to the status message. 128 | Any occurence of `[[BRANCH]]` will be replace by the actual branch name form the 129 | pull request. 130 | 131 | * `commentFile`: *Optional.* The path to a file that contains a custom comment to 132 | add to the message. This allow the comment to be built by a previous task in the job. 133 | 134 | ## Example pipeline 135 | 136 | ```yaml 137 | resource_types: 138 | - name: concourse-bitbucket-pullrequest 139 | type: docker-image 140 | source: 141 | repository: mm62/concourse-bitbucket-pullrequest-resource 142 | 143 | resources: 144 | - name: pullrequest 145 | type: concourse-bitbucket-pullrequest 146 | source: 147 | username: {{bitbucket-username}} 148 | password: {{bitbucket-password}} 149 | uri: https://your-bitbucket.com/project/repo 150 | 151 | jobs: 152 | - name: test pull request 153 | plan: 154 | - get: pullrequest 155 | trigger: true 156 | version: every 157 | - put: pullrequest 158 | params: 159 | path: pullrequest 160 | status: pending 161 | - task: test 162 | config: 163 | platform: linux 164 | 165 | inputs: 166 | - name: pullrequest 167 | 168 | ... 169 | 170 | on_success: 171 | put: pullrequest 172 | params: 173 | path: pullrequest 174 | status: success 175 | on_failure: 176 | put: pullrequest 177 | params: 178 | path: pullrequest 179 | status: failure 180 | ``` 181 | -------------------------------------------------------------------------------- /assets/helpers/bitbucket.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ASSETS=$(cd "$(dirname "$0")" && pwd) 6 | source $ASSETS/helpers/utils.sh 7 | 8 | VALUES_LIMIT=100 9 | 10 | bitbucket_request() { 11 | # $1: host 12 | # $2: path 13 | # $3: query 14 | # $4: data 15 | # $5: url base path 16 | # $6: Skip SSL verification 17 | # $7: netrc file (default: $HOME/.netrc) 18 | # $8: HTTP method (default: POST for data, GET without data) 19 | # $9: recursive data for bitbucket paging 20 | 21 | local data="$4" 22 | local path=${5:-rest/api/1.0} 23 | local skip_ssl_verification=${6:-"false"} 24 | local netrc_file=${7:-$HOME/.netrc} 25 | local method="$8" 26 | local recursive=${9:-limit=${VALUES_LIMIT}} 27 | 28 | local request_url="${1}/${path}/${2}?${recursive}&${3}" 29 | local request_result=$(tmp_file_unique bitbucket-request) 30 | local request_data=$(tmp_file_unique bitbucket-request-data) 31 | 32 | # deletes the temp files 33 | request_result_cleanup() { 34 | rm -f "$request_result" 35 | rm -f "$request_data" 36 | } 37 | 38 | local extra_options="" 39 | if [ -n "$data" ]; then 40 | method=${method:-POST} 41 | jq '.' <<< "$data" > "$request_data" 42 | extra_options="-H \"Content-Type: application/json\" -d @\"$request_data\"" 43 | fi 44 | 45 | if [ -n "$method" ]; then 46 | extra_options+=" -X $method" 47 | fi 48 | 49 | if [ "$skip_ssl_verification" = "true" ]; then 50 | extra_options+=" -k" 51 | fi 52 | 53 | curl_cmd="curl -s --netrc-file \"$netrc_file\" $TOKEN $extra_options \"$request_url\" > \"$request_result\"" 54 | if ! eval $curl_cmd; then 55 | log "Bitbucket request $request_url failed" 56 | exit 1 57 | fi 58 | 59 | if ! jq -c '.' < "$request_result" > /dev/null 2> /dev/null; then 60 | log "Bitbucket request $request_url failed (invalid JSON): $(cat "$request_result")" 61 | exit 1 62 | fi 63 | 64 | if [ "$(jq -r '.isLastPage' < "$request_result")" == "false" ]; then 65 | local nextPage=$(jq -r '.nextPageStart' < "$request_result") 66 | local nextResult=$(bitbucket_request "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "start=${nextPage}&limit=${VALUES_LIMIT}") 67 | jq -c '.values' < "$request_result" | jq -c ". + $nextResult" 68 | elif [ "$(jq -c '.values' < "$request_result")" != "null" ]; then 69 | jq -c '.values' < "$request_result" 70 | elif [ "$(jq -c '.errors' < "$request_result")" == "null" ]; then 71 | jq '.' < "$request_result" 72 | elif grep -q NoSuchPullRequestException "$request_result"; then 73 | printf "NO_SUCH_PULL_REQUEST" 74 | return 75 | elif grep -q 'This pull request has already been merged' "$request_result"; then 76 | printf "ALREADY_MERGED" 77 | return 78 | elif grep -q 'This pull request has been declined and must be reopened before it can be merged' "$request_result"; then 79 | printf "DECLINED" 80 | return 81 | else 82 | log "Bitbucket request ($request_url) failed: $(cat $request_result)" 83 | exit 1 84 | fi 85 | 86 | # cleanup 87 | request_result_cleanup 88 | } 89 | 90 | bitbucket_pullrequest() { 91 | # $1: host 92 | # $2: project 93 | # $3: repository id 94 | # $4: pullrequest id 95 | # $5: netrc file (default: $HOME/.netrc) 96 | # $6: skip ssl verification 97 | log "Retrieving pull request #$4 for $2/$3" 98 | bitbucket_request "$1" "projects/$2/repos/$3/pull-requests/$4" "" "" "" "$6" "$5" 99 | } 100 | 101 | bitbucket_pullrequest_merge() { 102 | # $1: host 103 | # $2: project 104 | # $3: repository id 105 | # $4: pullrequest id 106 | # $5: netrc file (default: $HOME/.netrc) 107 | # $6: skip ssl verification 108 | log "Retrieving pull request merge status #$4 for $2/$3" 109 | bitbucket_request "$1" "projects/$2/repos/$3/pull-requests/$4/merge" "" "" "" "$6" "$5" 110 | } 111 | 112 | bitbucket_pullrequest_overview_comments() { 113 | # $1: host 114 | # $2: project 115 | # $3: repository id 116 | # $4: pullrequest id 117 | # $5: netrc file (default: $HOME/.netrc) 118 | # $6: skip ssl verification 119 | 120 | log "Retrieving pull request comments #$4 for $2/$3" 121 | set -o pipefail; bitbucket_request "$1" "projects/$2/repos/$3/pull-requests/$4/activities" "" "" "" "$6" "$5" | \ 122 | jq 'map(select(.action == "COMMENTED" and .commentAction == "ADDED" and .commentAnchor == null)) | 123 | sort_by(.createdDate) | reverse | 124 | map({ id: .comment.id, version: .comment.version, text: .comment.text, createdDate: .comment.createdDate })' 125 | } 126 | 127 | bitbucket_pullrequest_progress_msg_start() { 128 | # $1: pull request hash 129 | # $2: type 130 | local hash="$1" 131 | local type="$2" 132 | 133 | local build_url_job="$ATC_EXTERNAL_URL/teams/$(rawurlencode "$BUILD_TEAM_NAME")/pipelines/$(rawurlencode "$BUILD_PIPELINE_NAME")/jobs/$(rawurlencode "$BUILD_JOB_NAME")" 134 | echo "[*Build$type* at **[${BUILD_PIPELINE_NAME} > ${BUILD_JOB_NAME}]($build_url_job)** for $hash" 135 | } 136 | 137 | bitbucket_pullrequest_progress_commit_match() { 138 | # $1: pull request comment 139 | # $2: pull request hash 140 | # $3: type of build to match 141 | local comment="$1" 142 | local hash="$2" 143 | local type="$3" 144 | 145 | local msg=$(bitbucket_pullrequest_progress_msg_start "$hash" "$type") 146 | echo "$comment" | grep -Ec "^$(regex_escape "$msg")" > /dev/null 147 | } 148 | 149 | bitbucket_pullrequest_comment_commit_match() { 150 | # $1: pull request comment 151 | # $2: pull request hash 152 | local comment="$1" 153 | local hash="$2" 154 | 155 | local msg=")** for $hash into" 156 | echo "$comment" | grep -Ec "$(regex_escape "$msg")" > /dev/null 157 | } 158 | 159 | bitbucket_pullrequest_progress_comment() { 160 | # $1: status (success, failure or pending) 161 | # $2: hash of merge commit 162 | # $3: hash of source commit 163 | # $4: hash of target commit 164 | # $5: custom comment 165 | local hash="$2" 166 | 167 | local progress_msg_end="" 168 | local custom_comment="" 169 | 170 | if [ "$hash" == "$3" ]; then 171 | progress_msg_end+=" into $4]" 172 | else 173 | progress_msg_end="] $3 into $4" 174 | fi 175 | 176 | if [ -n "$5" ]; then 177 | custom_comment="\n\n$5" 178 | fi 179 | 180 | local build_url="$ATC_EXTERNAL_URL/teams/$(rawurlencode "$BUILD_TEAM_NAME")/pipelines/$(rawurlencode "$BUILD_PIPELINE_NAME")/jobs/$(rawurlencode "$BUILD_JOB_NAME")/builds/$(rawurlencode "$BUILD_NAME")" 181 | local build_result_pre=" \n\n **[" 182 | local build_result_post="]($build_url)** - Build #$BUILD_NAME" 183 | 184 | case "$1" in 185 | success) 186 | echo "$(bitbucket_pullrequest_progress_msg_start "$hash" "Finished")${progress_msg_end}${build_result_pre}✓ BUILD SUCCESS${build_result_post}${custom_comment}" ;; 187 | failure) 188 | echo "$(bitbucket_pullrequest_progress_msg_start "$hash" "Finished")${progress_msg_end}${build_result_pre}✕ BUILD FAILED${build_result_post}${custom_comment}" ;; 189 | pending) 190 | echo "$(bitbucket_pullrequest_progress_msg_start "$hash" "Started")${progress_msg_end}${build_result_pre}⌛ BUILD IN PROGRESS${build_result_post}${custom_comment}" ;; 191 | esac 192 | } 193 | 194 | bitbucket_pullrequest_commit_status() { 195 | # $1: host 196 | # $2: commit 197 | # $3: data 198 | # $5: netrc file (default: $HOME/.netrc) 199 | # $6: skip ssl verification 200 | log "Setting pull request status $2" 201 | bitbucket_request "$1" "commits/$2" "" "$3" "rest/build-status/1.0" "$6" "$5" 202 | } 203 | 204 | bitbucket_pullrequest_add_comment_status() { 205 | # $1: host 206 | # $2: project 207 | # $3: repository id 208 | # $4: pullrequest id 209 | # $5: comment 210 | # $6: netrc file (default: $HOME/.netrc) 211 | # $7: skip ssl verification 212 | log "Adding pull request comment for status on #$4 for $2/$3" 213 | bitbucket_request "$1" "projects/$2/repos/$3/pull-requests/$4/comments" "" "{\"text\": \"$5\" }" "" "$7" "$6" 214 | } 215 | 216 | bitbucket_pullrequest_update_comment_status() { 217 | # $1: host 218 | # $2: project 219 | # $3: repository id 220 | # $4: pullrequest id 221 | # $5: comment 222 | # $6: comment id 223 | # $7: comment version 224 | # $8: netrc file (default: $HOME/.netrc) 225 | # $9: skip ssl verification 226 | log "Updating pull request comment (id: $6) for status on #$4 for $2/$3" 227 | bitbucket_request "$1" "projects/$2/repos/$3/pull-requests/$4/comments/$6" "" "{\"text\": \"$5\", \"version\": \"$7\" }" "" "$9" "$8" "PUT" 228 | } 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------