-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrelease.sh
executable file
·452 lines (394 loc) · 12.4 KB
/
release.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
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
#!/usr/bin/env bash
set -eu
shopt -s inherit_errexit
readonly CLI_PREFIX="[release-me]"
readonly DESCRIPTION="Fast minimal release workflow script written in Bash with plugins and presets support"
readonly USAGE="${CLI_PREFIX} Usage: release-me [options]
${DESCRIPTION}
Options:
-d, --dry-run Dry run. Skip tag creation, only show logs (if exists).
-w, --workspace Use in workspace environment for publishing workspaces separately.
--use-version Use project version from manifest. Requires workspace and a valid manifest.
--verbose Verbose mode, shows more detailed logs.
--quiet Quiet mode, shows less logs than default behavior.
--plugins=* Plugins option for loading plugins.
--preset= Presets option for parsing commits.
--stable If project has a \`0.x\` version, it will bump to \`1.x\`.
--pre-release Mark project release as non-production ready.
-h, --help Show this help.
-v, --version Show version.
"
##############################
####### Root variables #######
##############################
READLINK=$(readlink -f -- "$0")
SCRIPT_DIR=$(dirname -- "${READLINK}")
CURRENT_DATE=$(date +'%Y-%m-%d')
IS_GIT_REPO=$(git rev-parse --is-inside-work-tree 2>/dev/null || printf "%s" "")
GIT_LOG_ENTRY_SEPARATOR='____'
GIT_LOG_COMMIT_SEPARATOR='START_OF_COMMIT'
GIT_LOG_FORMAT="${GIT_LOG_COMMIT_SEPARATOR}%n%h${GIT_LOG_ENTRY_SEPARATOR}%H${GIT_LOG_ENTRY_SEPARATOR}%s${GIT_LOG_ENTRY_SEPARATOR}%b"
GIT_LOG_PARSE_REGEX="(.*)${GIT_LOG_ENTRY_SEPARATOR}(.*)${GIT_LOG_ENTRY_SEPARATOR}(.*)(${GIT_LOG_ENTRY_SEPARATOR}(.*)?)"
GIT_REMOTE_ORIGIN=$(git remote get-url origin 2>/dev/null || printf "%s" "")
GIT_REPO_NAME=
if [[ "${GIT_REMOTE_ORIGIN}" == "git@"* ]]; then
GIT_REPO_NAME=$(git remote get-url origin | cut -d ':' -f 2 | sed s/.git//)
elif [[ "${GIT_REMOTE_ORIGIN}" == "https"* ]]; then
GIT_REPO_NAME=$(git remote get-url origin | cut -d ':' -f 2 | sed s/\\/\\/github.com\\///)
fi
IS_WORKSPACE=false
IS_USE_PKG_VERSION=false
IS_DRY_RUN=false
IS_QUIET=false
IS_VERBOSE=false
IS_STABLE_VERSION=false
PRE_RELEASE_VERSION=false
PLUGINS=("git")
PRESET="conventional-commits"
# set `verbose` on `CI`
if [[ -n "${CI:-}" ]]; then
IS_VERBOSE=true
fi
##############################
###### Helpers & Utils #######
##############################
# proper handling of lines counter
# as `grep -c '^'` fails if there nothing
# and `wc -l` if there 1 commit
# so this utility was made
glc() {
local COUNT=0
while read -r line; do
if [[ -n "${line}" ]]; then
COUNT=$((COUNT + 1))
fi
done <<<"$(cat /dev/stdin)"
printf "%s" "${COUNT}"
}
parse_options() {
while :; do
local KEY="${1-}"
case "${KEY}" in
-v | --version)
printf "%s" "${CLI_PREFIX} last version available at GitHub"
exit 0
;;
-h | -\? | --help)
printf "%s" "${USAGE}"
exit 0
;;
-w | --workspace)
IS_WORKSPACE=true
;;
--plugins=*)
local IFS=','
read -ra PLUGINS <<<"${KEY#*=}"
;;
--preset=*)
read -r PRESET <<<"${KEY#*=}"
;;
--stable)
IS_STABLE_VERSION=true
PRE_RELEASE_VERSION=false
;;
--pre-release)
# shellcheck disable=2034
PRE_RELEASE_VERSION=true
IS_STABLE_VERSION=false
;;
--use-version)
IS_USE_PKG_VERSION=true
;;
-d | --dry-run)
# shellcheck disable=2034
IS_DRY_RUN=true
;;
-q | --quiet)
IS_QUIET=true
IS_VERBOSE=false
;;
--verbose)
IS_VERBOSE=true
IS_QUIET=false
# export GIT_TRACE=1
;;
-?*)
printf "%s" "${CLI_PREFIX} Unknown option: ${KEY}"
exit 1
;;
?*)
printf "%s" "${CLI_PREFIX} Unknown argument: ${KEY}"
exit 1
;;
*)
break
;;
esac
shift
done
}
is_valid_commit_type() {
local key="$1"
shift
local arr=("$@")
for element in "${arr[@]}"; do
if [[ "${key}" == "${element}"* ]]; then
return 0
fi
done
return 1
}
##############################
##### Logging helpers #######
##############################
log() {
if ! ${IS_QUIET}; then
if [[ "${2-}" == "-q" ]]; then
printf "%b\n" "$1"
else
printf "%b\n" "${CLI_PREFIX} $1"
fi
fi
}
log_verbose() {
if ${IS_VERBOSE}; then
if [[ "${2-}" == "-q" ]]; then
printf "%b\n" "$1"
else
printf "%b\n" "${CLI_PREFIX} $1"
fi
fi
}
##############################
##### Early exit errors ######
##############################
if [[ "${IS_GIT_REPO}" != true ]]; then
log "Current directory is not a Git repository!"
exit 1
fi
parse_options "$@"
up_to_date() {
log "${CLI_PREFIX} $1" -q
printf "%s\n" "${CLI_PREFIX} Your project is up-to-date"
exit 0
}
if [[ "${PRESET}" != "" ]]; then
SOURCE_PRESET_FILE="${SCRIPT_DIR}/presets/${PRESET}.sh"
# shellcheck disable=SC1090 source=/dev/null
source "${SOURCE_PRESET_FILE}"
fi
##############################
##### Package variables ######
##############################
PKG_NAME=""
NEXT_VERSION=(0 0 0)
CURRENT_VERSION=(0 0 0)
parse_packages() {
if ! ${IS_WORKSPACE}; then
return 0
fi
if [[ -f "./package.json" ]]; then
PKG_NAME=$(awk -F': ' '/"name":/ {gsub(/[",]/, "", $2); print $2}' "./package.json")
if ${IS_USE_PKG_VERSION}; then
PKG_VERSION=$(awk -F': ' '/"version":/ {gsub(/[",]/, "", $2); print $2}' "./package.json")
fi
elif [[ -f "./Cargo.toml" ]]; then
PKG_NAME=$(sed -n 's/^name = "\(.*\)"/\1/p' "./Cargo.toml")
if ${IS_USE_PKG_VERSION}; then
PKG_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' "./Cargo.toml")
fi
elif [[ -f "./setup.py" ]]; then
PKG_NAME=$(sed -n "s/.*name=['\"]\([^'\"]*\)['\"].*/\1/p" "./setup.py")
if ${IS_USE_PKG_VERSION}; then
PKG_VERSION=$(sed -n 's/^ *version\s*=\s*["'\'']\([^"'\'']*\)["'\''].*/\1/p' "./setup.py")
fi
else
cat <<EOF
This project currently supports only Node.js, Rust and Python projects.
Please wait for updates to get support in other languages!
EOF
exit 1
fi
if ${IS_WORKSPACE} && [[ -z "${PKG_NAME}" ]]; then
cat <<EOF
This release aims to being workspace release
but missing name and could not be release
EOF
exit 1
fi
if ${IS_USE_PKG_VERSION}; then
local IFS='.'
read -ra NEXT_VERSION <<<"${PKG_VERSION}"
CURRENT_VERSION=("${NEXT_VERSION[@]}")
fi
log_verbose "Workspace mode is enabled"
log_verbose "Workspace project name: ${PKG_NAME}"
}
##############################
####### Git variables ########
##############################
GIT_LAST_PROJECT_TAG=""
GIT_LAST_PROJECT_TAG_VER=""
get_git_variables() {
GIT_TAGS_LIST=$(git for-each-ref --sort=creatordate --format '%(refname)' refs/tags)
if ${IS_WORKSPACE}; then
GIT_LAST_PROJECT_TAG=$(printf "%b" "${GIT_TAGS_LIST}" | grep "${PKG_NAME}" | tail -1 | cut -d '/' -f 3)
else
GIT_LAST_PROJECT_TAG=$(printf "%s" "${GIT_TAGS_LIST}" | tail -1 | cut -d '/' -f 3)
fi
if [[ ${GIT_LAST_PROJECT_TAG} != "" ]]; then
GIT_LAST_PROJECT_TAG_VER=$(printf "%s" "${GIT_LAST_PROJECT_TAG}" | rev | cut -d 'v' -f 1 | rev)
fi
if [[ -n "${GIT_LAST_PROJECT_TAG_VER}" ]]; then
if ! ${IS_USE_PKG_VERSION}; then
mapfile -d '.' -t NEXT_VERSION < <(printf '%s' "${GIT_LAST_PROJECT_TAG_VER}")
CURRENT_VERSION=("${NEXT_VERSION[@]}")
fi
if [[ "${IS_STABLE_VERSION}" == false && ${PRE_RELEASE_VERSION} == false && "${NEXT_VERSION[0]}" -gt 0 ]]; then
IS_STABLE_VERSION=true
fi
fi
}
##############################
######## Git handling ########
##############################
GIT_LOGS=""
get_git_commits() {
local IFS=
local GIT_LOGS_LENGTH=0
# Fast early-catch empty repository
if ! git rev-parse HEAD 1>/dev/null 2>&1; then
log "You have not committed yet
You have to commit your initial/first commit."
exit 1
fi
# Check if exists last tag and is not empty
if [[ -n "${GIT_LAST_PROJECT_TAG}" ]]; then
log_verbose "Last project tag [${GIT_LAST_PROJECT_TAG}] found"
# Get and cache commits variable, so later
# can be checked for commits length and avaibality
if ${IS_WORKSPACE}; then
GIT_LOGS=$(git log "${GIT_LAST_PROJECT_TAG}...HEAD" --grep "${PKG_NAME}" --pretty=format:"${GIT_LOG_FORMAT}" --reverse)
GIT_LOGS_LENGTH=$(git log "${GIT_LAST_PROJECT_TAG}...HEAD" --grep "${PKG_NAME}" --pretty=format:"%s" | glc -)
else
GIT_LOGS=$(git log "${GIT_LAST_PROJECT_TAG}...HEAD" --pretty=format:"${GIT_LOG_FORMAT}" --reverse)
GIT_LOGS_LENGTH=$(git rev-list --count "${GIT_LAST_PROJECT_TAG}...HEAD")
fi
else
log_verbose "Last project tag not found"
if ${IS_WORKSPACE}; then
GIT_LOGS=$(git log HEAD --grep "${PKG_NAME}" --pretty=format:"${GIT_LOG_FORMAT}" --reverse)
GIT_LOGS_LENGTH=$(git log HEAD --grep "${PKG_NAME}" --pretty=format:"%s" | glc -)
else
GIT_LOGS=$(git log HEAD --pretty=format:"${GIT_LOG_FORMAT}" --reverse)
GIT_LOGS_LENGTH=$(git rev-list --count HEAD)
fi
fi
if [[ ${GIT_LOGS_LENGTH} -eq 0 ]]; then
up_to_date "Your project has no new commits"
else
if [[ -n "${GIT_LAST_PROJECT_TAG}" ]]; then
log_verbose "Found ${GIT_LOGS_LENGTH} commits since last release"
else
log_verbose "Found ${GIT_LOGS_LENGTH} commits but did not found any release"
fi
fi
}
##############################
#### Git commits handling ####
##############################
NEXT_BUILD_VERSION=""
NEXT_RELEASE_VERSION=""
NEXT_RELEASE_TAG=""
RELEASE_BODY=""
PATCH_UPGRADED=false
MINOR_UPGRADED=false
MAJOR_UPGRADED=false
handle_git_commits() {
log_verbose "Analyzing commits...\n"
local IFS=
while read -r line; do
if [[ "${line}" == "${GIT_LOG_COMMIT_SEPARATOR}" ]]; then
read -r commit
if [[ "${commit}" =~ ${GIT_LOG_PARSE_REGEX} ]]; then
log_verbose "${BASH_REMATCH[3]}" "-q"
CHECKOUT_SHA=${BASH_REMATCH[1]}
preset_command=$(command -v parse_commit)
if [[ -n "${preset_command}" ]]; then
parse_commit BASH_REMATCH
fi
fi
fi
done <<<"${GIT_LOGS}"
log_verbose "" "-q"
log_verbose "Analyzed commits!"
log_verbose "Preparing changes diff..."
build_release
log_verbose "Prepared changes diff!"
log_verbose "Analyzing updates..."
if [[ "${IS_STABLE_VERSION}" == true && "${NEXT_VERSION[0]}" -eq 0 ]]; then
NEXT_VERSION=(1 0 0)
elif [[ ${MAJOR_UPGRADED} == true && "${NEXT_VERSION[0]}" -gt 0 ]]; then
NEXT_VERSION[0]=$((NEXT_VERSION[0] + 1))
NEXT_VERSION[1]=0
NEXT_VERSION[2]=0
elif ${MINOR_UPGRADED} || [[ ${MAJOR_UPGRADED} == true && "${NEXT_VERSION[0]}" -eq 0 ]]; then
NEXT_VERSION[1]=$((NEXT_VERSION[1] + 1))
NEXT_VERSION[2]=0
elif ${PATCH_UPGRADED}; then
NEXT_VERSION[2]=$((NEXT_VERSION[2] + 1))
fi
local IFS='.'
NEXT_BUILD_VERSION="${NEXT_VERSION[*]}"
CURRENT_BUILD_VERSION="${CURRENT_VERSION[*]}"
if [[ "${NEXT_BUILD_VERSION}" == "${CURRENT_BUILD_VERSION}" ]]; then
up_to_date "Your project has no incremental update"
else
log_verbose "Analyzing updates done!"
fi
CURRENT_RELEASE_TAG=""
if ${IS_WORKSPACE}; then
NEXT_RELEASE_VERSION="v${NEXT_BUILD_VERSION}"
NEXT_RELEASE_TAG="${PKG_NAME}-${NEXT_RELEASE_VERSION}"
CURRENT_RELEASE_TAG="${PKG_NAME}-v${CURRENT_BUILD_VERSION}"
else
NEXT_RELEASE_VERSION="v${NEXT_BUILD_VERSION}"
NEXT_RELEASE_TAG="${NEXT_RELEASE_VERSION}"
CURRENT_RELEASE_TAG="v${CURRENT_BUILD_VERSION}"
fi
if [[ -n "${GIT_LAST_PROJECT_TAG_VER}" ]]; then
RELEASE_DIFF_URL="https://github.com/${GIT_REPO_NAME}/compare/${CURRENT_RELEASE_TAG}...${NEXT_RELEASE_TAG}"
RELEASE_BODY_TITLE="[${NEXT_RELEASE_TAG}](${RELEASE_DIFF_URL}) (${CURRENT_DATE})"
else
RELEASE_BODY_TITLE="${NEXT_RELEASE_TAG} (${CURRENT_DATE})"
fi
RELEASE_BODY="# ${RELEASE_BODY_TITLE}\n${RELEASE_BODY}"
}
##############################
######## Handle push ########
##############################
handle_pushes() {
log_verbose "Applying changes..."
log_verbose "Found tag commit [${CHECKOUT_SHA}]"
for plugin in "${PLUGINS[@]}"; do
log_verbose "Loading plugin \`${plugin}\`..."
local SOURCE_PLUGIN_FILE="${SCRIPT_DIR}/plugins/${plugin}.sh"
# shellcheck disable=SC1090 source=/dev/null
source "${SOURCE_PLUGIN_FILE}"
release_command=$(command -v release)
if [[ -n "${release_command}" ]]; then
release
fi
unset release
log_verbose "Applied plugin ${plugin}!"
done
log_verbose "Applied changes!"
}
##############################
######### Initialize #########
##############################
parse_packages
get_git_variables
get_git_commits
handle_git_commits
handle_pushes