This repository has been archived by the owner on Jul 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
backport.sh
executable file
·309 lines (242 loc) · 8.11 KB
/
backport.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#!/bin/bash
set -o errexit -o pipefail -o nounset
newline_at_eof() {
local file="$1"
if [ -s "${file}" ] && [ "$(tail -c1 "${file}"; echo x)" != $'\nx' ]
then
# ensure newline at the end of file
echo ''>> "${file}"
fi
}
debug() {
local outvar=$1
shift
echo "::debug::running: $*"
local stdout
stdout="$(mktemp)"
# shellcheck disable=SC2001
("$@" 2> >(sed -e 's/^/::debug::err:/') > "${stdout}")
local rc=$?
# shellcheck disable=SC2140
eval "${outvar}"="'$(cat "${stdout}")'"
newline_at_eof "${stdout}"
sed -e 's/^/::debug::out:/' "${stdout}"
rm "${stdout}"
bash -c 'echo -n' # force flushing stdout so that debug out/err are outputted before rc
echo "::debug::rc=${rc}"
return ${rc}
}
http_post() {
local url=$1
local json=$2
local output
output="$(mktemp)"
result=''
debug result curl -XPOST --fail -v -fsL \
--output "${output}" \
-w '{"http_code":%{http_code},"url_effective":"%{url_effective}"}' \
-H 'Accept: application/vnd.github.v3+json' \
-H "Authorization: Bearer ${INPUT_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${json}" \
"${url}"|| true
newline_at_eof "${output}"
sed -e 's/^/::debug::output:/' "${output}"
rm "${output}"
echo "::debug::result=${result}"
if [[ ! $(echo "${result}" |jq -r .http_code) =~ ^"2" ]]
then
local message
message=$(echo "${result}"| jq -r -s 'add | (.http_code|tostring) + ", effective url: " + .url_effective')
echo "::error::Error in HTTP POST to ${url} of \`${json}\`: ${message}"
exit 1
fi
}
fail() {
local message=$1
local error=${2:-}
echo "::error::${message} (${error})"
echo '::endgroup::'
local comment="${message}"
if [ -n "${error}" ]
then
comment+="\n\n<details><summary>Error</summary><pre>${error}</pre></details>"
fi
local comment_json
comment_json="$(jq -n -c --arg body "${comment}" '{"body": $body|gsub ("\\\\n";"\n")}')"
local comments_url
comments_url=$(jq --raw-output .pull_request._links.comments.href "${GITHUB_EVENT_PATH}")
http_post "${comments_url}" "${comment_json}"
exit 1
}
auth_header() {
local token=$1
echo -n "$(echo -n "x-access-token:${token}"|base64 --wrap=0)"
}
cherry_pick() {
local branch=$1
local repository=$2
local backport_branch=$3
local merge_sha=$4
output=''
test -d "${GITHUB_WORKSPACE}/.git" && debug output git -C "${GITHUB_WORKSPACE}" checkout -b "${branch}" -t "origin/${branch}" || debug output git clone -q --no-tags -b "${branch}" "${repository}" "${GITHUB_WORKSPACE}" || fail "Unable to clone from repository \`${repository}\` a branch named \`${branch}\`, this should not have happened" &&
(
cd "${GITHUB_WORKSPACE}"
local user_name
user_name="$(git --no-pager log --format=format:'%an' -n 1)"
local user_email
user_email="$(git --no-pager log --format=format:'%ae' -n 1)"
set +e
debug output git checkout -q -b "${backport_branch}" || fail "Unable to checkout branch named \`${branch}\`, you might need to create it or use a different label."
debug output git -c user.name="${user_name}" -c user.email="${user_email}" cherry-pick -x --mainline 1 "${merge_sha}" || fail "Unable to cherry-pick commit ${merge_sha} on top of branch \`${branch}\`.\n\nThis pull request needs to be backported manually." "${output}
$(git status)"
set -e
)
}
push() {
local backport_branch=$1
local auth
auth="$(auth_header "${INPUT_TOKEN}")"
(
cd "${GITHUB_WORKSPACE}"
local user_name
user_name="$(git --no-pager log --format=format:'%an' -n 1)"
local user_email
user_email="$(git --no-pager log --format=format:'%ae' -n 1)"
set +e
git -c user.name="${user_name}" -c user.email="${user_email}" -c "http.https://github.com.extraheader=Authorization: basic ${auth}" push -q --set-upstream origin "${backport_branch}" > /dev/null || fail "Unable to push the backported branch, did you try to backport the same PR twice without deleting the \`${backport_branch}\` branch?"
set -e
)
}
create_pull_request() {
local branch=$1
local backport_branch=$2
local title=$3
local number=$4
local pulls_url=$5
local pull_request_title="[Backport ${branch}] ${title}"
local pull_request_body="Backport of #${number}"
local pull_request="{\
\"title\": \"${pull_request_title}\", \
\"body\": \"${pull_request_body}\", \
\"head\": \"${backport_branch}\", \
\"base\": \"${branch}\" \
}"
http_post "${pulls_url}" "${pull_request}"
}
backport() {
local number=$1
local branch=$2
echo '::group::Performing backport'
echo "::debug::Backporting pull request #${number} to branch ${branch}"
local repository
repository=$(jq --raw-output .repository.clone_url "${GITHUB_EVENT_PATH}")
local backport_branch
backport_branch="backport/${number}-to-${branch}"
local merge_sha
merge_sha=$(jq --raw-output .pull_request.merge_commit_sha "${GITHUB_EVENT_PATH}")
cherry_pick "${branch}" "${repository}" "${backport_branch}" "${merge_sha}"
push "${backport_branch}"
local title
title=$(jq --raw-output .pull_request.title "${GITHUB_EVENT_PATH}")
local pulls_url
pulls_url=$(tmp=$(jq --raw-output .repository.pulls_url "${GITHUB_EVENT_PATH}"); echo "${tmp%{*}")
create_pull_request "${branch}" "${backport_branch}" "${title}" "${number}" "${pulls_url}"
echo '::endgroup::'
}
delete_branch() {
echo '::group::Deleting closed pull request branch'
local branch=$1
local refs_url
refs_url=$(tmp=$(jq --raw-output .pull_request.head.repo.git_refs_url "${GITHUB_EVENT_PATH}"); echo "${tmp%{*}")
local output
output="$(mktemp)"
debug status curl -XDELETE -v -fsL \
--fail \
--output "${output}" \
-w '%{http_code}' \
-H 'Accept: application/vnd.github.v3+json' \
-H "Authorization: Bearer ${INPUT_TOKEN}" \
"$refs_url/heads/$branch" || true
newline_at_eof "${output}"
sed -e 's/^/::debug::output:/' "${output}"
rm "${output}"
echo "::debug::status=${status}"
if [[ "${status}" == 204 || "${status}" == 422 ]]; then
echo 'Deleted'
else
echo 'Failed to delete branch'
fail "Unable to delete pull request branch '${branch}'. Please delete it manually."
fi
echo '::endgroup::'
}
check_token() {
echo '::group::Checking token'
if [[ -z ${INPUT_TOKEN+x} ]]; then
echo '::error::INPUT_TOKEN is was not provided, by default it should be set to {{ github.token }}'
echo '::endgroup::'
exit 1
fi
local output
output="$(mktemp)"
status=''
debug status curl -v -fsL \
--fail \
--output "${output}" \
-w '%{http_code}' \
-H "Authorization: Bearer ${INPUT_TOKEN}" \
"https://api.github.com/zen" || true
newline_at_eof "${output}"
sed -e 's/^/::debug::output:/' "${output}"
rm "${output}"
echo "::debug::status=${status}"
if [[ ${status} != 200 ]]
then
echo '::error::Provided INPUT_TOKEN is not valid according to the zen API'
echo '::endgroup::'
exit 1
fi
echo 'Token seems valid'
echo '::endgroup::'
}
main() {
echo '::group::Environment'
for e in $(printenv)
do
echo "::debug::${e}"
done
echo '::endgroup::'
local state
state=$(jq --raw-output .pull_request.state "${GITHUB_EVENT_PATH}")
local login
login=$(jq --raw-output .pull_request.user.login "${GITHUB_EVENT_PATH}")
local title
title=$(jq --raw-output .pull_request.title "${GITHUB_EVENT_PATH}")
local merged
merged=$(jq --raw-output .pull_request.merged "${GITHUB_EVENT_PATH}")
if [[ "$state" == "closed" && "$login" == "github-actions[bot]" && "$title" == '[Backport '* ]]; then
check_token
delete_branch "$(jq --raw-output .pull_request.head.ref "${GITHUB_EVENT_PATH}")"
return
fi
if [[ "$merged" != "true" ]]; then
return
fi
local number
number=$(jq --raw-output .number "${GITHUB_EVENT_PATH}")
local labels
labels=$(jq --raw-output .pull_request.labels[].name "${GITHUB_EVENT_PATH}")
local default_ifs="${IFS}"
IFS=$'\n'
for label in ${labels}; do
IFS="${default_ifs}"
# label needs to be `backport <name of the branch>`
if [[ "${label}" == 'backport '* ]]; then
local branch=${label#* }
check_token
backport "${number}" "${branch}"
fi
done
}
${__SOURCED__:+return}
main "$@"