-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
polyglot-release
executable file
·632 lines (573 loc) · 15.9 KB
/
polyglot-release
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
#!/bin/bash
set -e
# quiet output from pushd / popd / find
function pushd() {
command pushd "$@" >/dev/null
}
function popd() {
command popd >/dev/null
}
function find() {
command find "$@" 2>/dev/null
}
# Usage: 'run pre_release|release|post_release <language>'
function run() {
if [[ -d $2 ]]; then
pushd "$2"
eval "$1_$2"
popd
elif eval "is_monoglot_$2"; then
IS_CURRENT_LANGUAGE_POLYGLOT=
eval "$1_$2"
IS_CURRENT_LANGUAGE_POLYGLOT=true
fi
}
# Usage 'decorate "prefix" "suffix" "${TAGS{@}}"'
function decorate() {
local prefix=$1
local suffix=$2
local tags=()
for tag in "${@:3}" ; do
tags+=("${prefix}${tag}${suffix}")
done
echo "${tags[@]}"
}
SUPPORTED_LANGUAGES=()
SUPPORTED_LANGUAGES+=("c")
function is_monoglot_c() {
[[ -f VERSION ]]
}
function pre_release_c() {
if [[ ! -f VERSION ]]; then
echo "This looks like a C project, but there is no VERSION file"
exit 1
fi
}
function release_c() {
echo "$NEW_VERSION" >VERSION
}
function post_release_c() {
# noop
:
}
SUPPORTED_LANGUAGES+=("dotnet")
function is_monoglot_dotnet() {
[[ $(find ./*.sln -type f | wc -l) -gt 0 ]]
}
function pre_release_dotnet() {
check_for_tools "xmlstarlet"
if [[ ! -f "$(cs_project_file)" ]]; then
echo "This looks like a .Net project, but there is no $(cs_project_file) file"
exit 1
fi
}
function release_dotnet() {
xmlstarlet ed --pf --omit-decl --inplace --update /Project/PropertyGroup/VersionNumber --value "$NEW_VERSION" "$(cs_project_file)"
}
function post_release_dotnet() {
# noop
:
}
function cs_project_file() {
solution_file=$(realpath "$(find ./*.sln -type f)" --relative-to "$(pwd)")
project_name=${solution_file%.sln}
echo "$project_name/$project_name.csproj"
}
SUPPORTED_LANGUAGES+=("elixir")
function is_monoglot_elixir() {
[[ -f mix.exs ]]
}
function pre_release_elixir() {
check_for_tools "sed"
}
function release_elixir() {
sed -i".tmp" "s/version: \".*\"/version: \"$NEW_VERSION\"/" "mix.exs"
rm -rf mix.exs.tmp
}
function post_release_elixir() {
# noop
:
}
SUPPORTED_LANGUAGES+=("github-action")
function is_monoglot_github-action() {
[[ -f action.yaml ]]
}
function pre_release_github-action() {
# noop
:
}
function release_github-action() {
# noop, publishing github only uses git tags
:
}
function post_release_github-action() {
# noop
:
}
SUPPORTED_LANGUAGES+=("go")
function is_monoglot_go() {
[[ -f go.mod ]]
}
function pre_release_go() {
check_for_tools "go" "jq" "sed" "find"
if $IS_CURRENT_LANGUAGE_POLYGLOT; then
# Use an additional tag. See: https://go.dev/ref/mod#vcs-version
TAGS+=("go/v$NEW_VERSION");
fi
}
function release_go() {
local module_with_old_version
module_with_old_version="$(go mod edit -json | jq -r '.Module.Path' )"
local new_major_version
new_major_version="$(echo "$NEW_VERSION" | sed -E 's/^([0-9]+)\.[0-9]+\.[0-9]+$/\1/')"
# The sed below also captures 3-digit versions
local module_with_new_version
module_with_new_version="$(echo "$module_with_old_version" | sed -E "s/(.*)v[0-9]+(\.[0-9]+\.[0-9]+)?$/\1v$new_major_version/")"
go mod edit -module "$module_with_new_version"
find . -name '*.go' -exec sed -i".tmp" "s#$module_with_old_version#$module_with_new_version#g" {} \;
find . -name '*.go.tmp' -exec rm {} \;
}
function post_release_go() {
# noop
:
}
SUPPORTED_LANGUAGES+=("javascript")
function is_monoglot_javascript() {
[[ -f package.json ]]
}
function pre_release_javascript() {
check_for_tools "npm"
}
function release_javascript() {
npm version --no-git-tag-version "$NEW_VERSION" >/dev/null
}
function post_release_javascript() {
# noop
:
}
SUPPORTED_LANGUAGES+=("java")
function is_monoglot_java() {
[[ -f pom.xml ]]
}
function pre_release_java() {
check_for_tools "mvn"
}
function release_java() {
mvn --quiet versions:set -DnewVersion="$NEW_VERSION" 2>/dev/null
mvn --quiet versions:set-scm-tag -DnewTag="v$NEW_VERSION" 2>/dev/null
}
function post_release_java() {
new_version_template="\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}-SNAPSHOT"
mvn --quiet \
build-helper:parse-version \
versions:set -DnewVersion="$new_version_template" \
versions:set-scm-tag -DnewTag="HEAD" \
2>/dev/null
}
SUPPORTED_LANGUAGES+=("perl")
function is_monoglot_perl() {
[[ -f cpanfile ]]
}
function pre_release_perl() {
if [[ ! -f VERSION ]]; then
echo "This looks like a Perl project, but there is no VERSION file"
exit 1
fi
}
function release_perl() {
echo "$NEW_VERSION" >VERSION
}
function post_release_perl() {
# noop
:
}
# Note: Do not repeat this pattern for project specific needs
# Instead implement https://github.com/cucumber/polyglot-release/issues/67
SUPPORTED_LANGUAGES+=("polyglot-release")
function is_monoglot_polyglot-release() {
[[ -f polyglot-release ]]
}
function pre_release_polyglot-release() {
check_for_tools "sed"
if [[ ! -f README.md ]]; then
echo "This looks like the polyglot-release project, but there is no README.md file"
exit 1
fi
}
function release_polyglot-release() {
for file in polyglot-release README.md; do
sed -i".tmp" "s/^POLYGLOT_RELEASE_VERSION=.*$/POLYGLOT_RELEASE_VERSION=$NEW_VERSION/" "$file"
rm -f "polyglot-release.tmp"
done
}
function post_release_polyglot-release() {
sed -i".tmp" "s/^POLYGLOT_RELEASE_VERSION=.*$/POLYGLOT_RELEASE_VERSION=/" polyglot-release
rm -f "polyglot-release.tmp"
}
SUPPORTED_LANGUAGES+=("php")
function is_monoglot_php() {
[[ -f composer.json ]]
}
function pre_release_php() {
# noop
:
}
function release_php() {
# noop, composer relies on git tags
:
}
function post_release_php() {
# noop
:
}
SUPPORTED_LANGUAGES+=("python")
function is_monoglot_python() {
[[ -f pyproject.toml || -f setup.py ]]
}
function pre_release_python() {
check_for_tools "sed"
}
function release_python() {
if [[ -f pyproject.toml ]]; then
PROJECT_FILE=pyproject.toml
else
PROJECT_FILE=setup.py
fi
sed -i".tmp" \
-e "s/\(version *= *\"\)[0-9]*\.[0-9]*\.[0-9]*\(\"\)/\1$NEW_VERSION\2/" \
"$PROJECT_FILE"
rm -f "$PROJECT_FILE.tmp"
}
function post_release_python() {
# noop
:
}
SUPPORTED_LANGUAGES+=("ruby")
function is_monoglot_ruby() {
[[ $(find ./*.gemspec -type f | wc -l) -gt 0 ]]
}
function pre_release_ruby() {
if [[ ! -f VERSION ]]; then
echo "This looks like a Ruby project, but there is no VERSION file"
exit 1
fi
}
function release_ruby() {
echo "$NEW_VERSION" >VERSION
}
function post_release_ruby() {
# noop
:
}
function validate_new_version_argument() {
if [[ ! "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid MAJOR.MINOR.PATCH argument: $NEW_VERSION"
show_usage
exit 1
fi
if [ -n "$(git tag --list "v$NEW_VERSION")" ]; then
echo "Version $NEW_VERSION has already been released."
exit 1
fi
}
function check_for_tools() {
for tool in "$@"; do
if ! command -v "$tool" >/dev/null; then
echo "$tool is not installed!"
missing_tool="true"
fi
done
if [ -n "$missing_tool" ]; then
echo
echo "Please install the missing required tool(s)."
exit 1
fi
}
function do_update() {
if [ -z "$POLYGLOT_RELEASE_VERSION" ]; then
# Polyglot release was not released.
# Let's assume we're up to date.
return
fi
latest_version=$(latest_tag_in_git)
if [ "v$POLYGLOT_RELEASE_VERSION" == "$latest_version" ]; then
log "Already up to date"
return
fi
check_for_tools "curl"
current_script=${BASH_SOURCE[0]}
if [ ! -w "$current_script" ]; then
echo "You do not appear to have write permissions to $current_script. Try using sudo."
exit 1
fi
new_script=$(mktemp "$current_script".XXXXXX)
curl --silent --output "$new_script" "$POLYGLOT_RELEASE_UPDATE_LOCATION/$latest_version/polyglot-release"
replace_file "$current_script" "$new_script"
log "Updated to $latest_version"
}
function replace_file() {
current_script=$1
new_script=$2
if [[ $(uname -s) = "Darwin" ]]; then
uid=$(stat -f '%u' "$current_script")
gid=$(stat -f '%g' "$current_script")
permissions=$(stat -f '%p' "$current_script")
chown "$uid:$gid" "$new_script"
chmod "${permissions:3:7}" "$new_script"
else
chown --reference="$current_script" -- "$new_script"
chmod --reference="$current_script" -- "$new_script"
fi
sync # force filesystem to fully flush file contents to disk
mv -- "$new_script" "$current_script"
}
function check_up_to_date() {
if [ -z "$POLYGLOT_RELEASE_VERSION" ]; then
# Polyglot release was not released.
# Let's assume we're up to date.
return
fi
local latest_version
latest_version=$(latest_tag_in_git)
if [ "v$POLYGLOT_RELEASE_VERSION" == "$latest_version" ]; then
return
fi
echo "Please run polyglot-release --update"
echo " - current version: v$POLYGLOT_RELEASE_VERSION"
echo " - latest version: $latest_version"
exit 1
}
function latest_tag_in_git() {
git ls-remote --tags --sort='version:refname' "$POLYGLOT_RELEASE_GIT_REPO" |
grep -E "refs/tags/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$" |
cut -d '/' -f 3 |
tail -n1
}
function check_changelog_exists() {
if [[ ! -f CHANGELOG.md ]]; then
echo "Please create a CHANGELOG.md"
exit 1
fi
}
function check_in_git_root_directory() {
git_root=$(git rev-parse --show-toplevel)
pwd=$(realpath "$(pwd)")
if [[ "$git_root" != "$pwd" ]]; then
relative_path=$(realpath --relative-to="$(pwd)" "$git_root")
echo "You're not in the root directory of your git repo!"
echo
echo "Try this:"
echo " cd $relative_path"
exit 1
fi
}
function check_gpg_keys_configured() {
if [[ ! $(git config user.signingkey) ]]; then
echo "You do not have a user.signingkey configured in git!"
echo
echo "All commits need to be signed. Please see https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key"
exit 1
fi
key=$(git config user.signingkey)
if ! gpg --list-secret-keys | grep -q "$key"; then
echo "Your git user.signingkey ($key) was not found in your GPG keys."
echo
echo "To see all your GPG keys, use:"
echo " gpg --list-secret-keys"
echo
echo "All commits need to be signed. Please see https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key"
exit 1
fi
}
function check_git_tags_fetched() {
latest_version_from_changelog=$(changelog latest)
if [[ $latest_version_from_changelog == v* ]]; then
expected_tag="$latest_version_from_changelog"
else
expected_tag="v$latest_version_from_changelog"
fi
if [[ -z $(git tag --list "$expected_tag") ]]; then
echo "No git tag found for $expected_tag (found in CHANGELOG.md)!"
echo
echo "Do you need to run this?"
echo " git fetch --tags"
exit 1
fi
}
function check_git_index_clean() {
if ! git diff-index --quiet HEAD; then
echo "Git has uncommitted changes."
exit 1
fi
}
function check_git_even_with_origin() {
current_branch=$(git branch --show-current)
remote_tracking_branch=$(git config "branch.$current_branch.merge")
origin_head=$(
git ls-remote origin --heads "$remote_tracking_branch" |
head -n1 |
tr -s '[:space:]' ' ' |
cut -d ' ' -f 1
)
local_head=$(git rev-list HEAD | head -n1)
if [ "$origin_head" != "$local_head" ]; then
echo "This branch is not even with origin $remote_tracking_branch!"
echo
echo "Have a look at:"
echo " git status"
exit 1
fi
}
function show_usage() {
echo "Usage: polyglot-release [OPTIONS] MAJOR.MINOR.PATCH"
echo "OPTIONS:"
echo " --help shows this help"
echo " --update updates this script to the latest version"
echo " --version show the current version"
echo " --no-git-push do not push to git"
echo " --quiet don't log progress messages"
}
function log() {
if [[ -z $QUIET ]]; then
echo "$1"
fi
}
function format_commit_message() {
echo "$1
Created-by: polyglot-release v${POLYGLOT_RELEASE_VERSION:--develop}"
}
# Initialize global variables
IS_CURRENT_LANGUAGE_POLYGLOT=true
NEW_VERSION=
NO_GIT_PUSH=
POLYGLOT_RELEASE_VERSION=
POLYGLOT_RELEASE_GIT_REPO=${POLYGLOT_RELEASE_GIT_REPO:-https://github.com/cucumber/polyglot-release}
POLYGLOT_RELEASE_UPDATE_LOCATION=${POLYGLOT_RELEASE_UPDATE_LOCATION:-https://raw.githubusercontent.com/cucumber/polyglot-release}
QUIET=
TAGS=()
POSITIONAL_ARGS=()
RELEASE_DATE=${RELEASE_DATE:-$(date +%F)}
while [[ $# -gt 0 ]]; do
case $1 in
--no-git-push)
NO_GIT_PUSH="true"
shift # past argument
;;
--quiet)
QUIET="true"
shift # past argument
;;
-h | --help)
echo "polyglot-release: Makes a release to GitHub"
show_usage
exit 0
;;
--update)
do_update
exit 0
;;
--version)
echo "v${POLYGLOT_RELEASE_VERSION:--develop}"
exit 0
;;
--* | -*)
echo "Unknown option $1"
show_usage
exit 1
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
check_for_tools "git" "changelog" "gpg" "realpath"
check_up_to_date
check_in_git_root_directory
check_changelog_exists
check_gpg_keys_configured
check_git_tags_fetched
check_git_index_clean
check_git_even_with_origin
if [[ $# -ne 1 ]]; then
echo "Please specify a version to release. Use --help to show usage instructions."
echo
echo "To help you choose the next version, here are the unreleased changes:"
echo
changelog show unreleased
echo
exit 1
fi
NEW_VERSION=$1
NEW_VERSION_TAG="v$NEW_VERSION"
TAGS+=("$NEW_VERSION_TAG")
validate_new_version_argument
commit_before_release="$(git rev-parse HEAD)"
###
## pre release
###
for language in "${SUPPORTED_LANGUAGES[@]}"; do
run pre_release "$language"
done
###
## release
###
for language in "${SUPPORTED_LANGUAGES[@]}"; do
run release "$language"
done
changelog release "$NEW_VERSION" \
--release-date "$RELEASE_DATE" \
--tag-format "v%s" \
--output CHANGELOG.md
log "Package manager manifests and CHANGELOG.md updated for $NEW_VERSION"
log "Here's what changed:"
log "$(git -c color.diff=always diff)"
git commit --gpg-sign --quiet --all --message="$(format_commit_message "Prepare release $NEW_VERSION_TAG")"
for tag in "${TAGS[@]}"; do
git tag --sign --message "$(format_commit_message "$NEW_VERSION_TAG")" "$tag"
done
log "Files committed to to git and tagged $(decorate "'" "'" "${TAGS[@]}")"
###
## post release
###
for language in "${SUPPORTED_LANGUAGES[@]}"; do
run post_release "$language"
done
if [[ $(git status --porcelain) ]]; then
git commit --gpg-sign --quiet --all --message="$(format_commit_message "Prepare for the next development iteration")"
log "Post-release changes committed to to git"
fi
###
# push to github
##
local_branch=$(git rev-parse --abbrev-ref HEAD)
release_commit=$(git rev-list --max-count=1 "$NEW_VERSION_TAG")
release_branch="release/$NEW_VERSION_TAG"
if [[ -z $NO_GIT_PUSH ]]; then
# shellcheck disable=SC2046
git push --quiet --atomic origin "refs/heads/$local_branch" $(decorate "refs/tags/" "" "${TAGS[@]}") "$release_commit:refs/heads/$release_branch"
log "Tag(s) ${TAGS[*]} pushed to origin"
log "All commit(s) pushed to origin/$local_branch"
log "Release commit (tagged with $NEW_VERSION_TAG) pushed to origin/$release_branch"
if [[ -n $QUIET ]]; then
echo "$release_branch"
fi
else
log "You now need to eyeball these commits, then push manually:"
log
log "# push local commits and tags to $local_branch"
log "git push origin refs/heads/$local_branch $(decorate "refs/tags/" "" "${TAGS[@]}")"
log
log "# push to release branch"
log "git push origin $release_commit:refs/heads/$release_branch"
log
log
log "If things do not look quite right you can roll back the release:"
log
log "# reset to the commit before release started"
log "git reset --hard $commit_before_release"
log
log "# delete the git tag that was created"
log "git tag -d ${TAGS[*]}"
fi