|
| 1 | +#!/bin/sh |
| 2 | +# release.sh - Make a release. |
| 3 | +# See -h for details. |
| 4 | +# 2021, Odin Kroeger |
| 5 | +# shellcheck disable=2015 |
| 6 | + |
| 7 | +set -Cefu |
| 8 | + |
| 9 | + |
| 10 | +# CONSTANTS |
| 11 | +# ========= |
| 12 | + |
| 13 | +SCRIPT_NAME="$(basename "$0")" && [ "$SCRIPT_NAME" ] || { |
| 14 | + printf '%s: failed to determine basename.\n' "$0" >&2 |
| 15 | + exit 69 |
| 16 | +} |
| 17 | +readonly SCRIPT_NAME |
| 18 | + |
| 19 | + |
| 20 | +REPO="$(git rev-parse --show-toplevel)" && [ "$REPO" ] || { |
| 21 | + printf '%s: failed to determine root directory of repository.\n' \ |
| 22 | + "$SCRIPT_NAME" >&2 |
| 23 | + exit 69 |
| 24 | +} |
| 25 | +readonly REPO |
| 26 | + |
| 27 | + |
| 28 | +# FUNCTIONS |
| 29 | +# ========= |
| 30 | + |
| 31 | +# shellcheck disable=2059 |
| 32 | +warn() ( |
| 33 | + exec >&2 |
| 34 | + printf '%s: ' "$SCRIPT_NAME" |
| 35 | + printf -- "$@" |
| 36 | + echo |
| 37 | +) |
| 38 | + |
| 39 | +panic() { |
| 40 | + __panic_status=69 |
| 41 | + OPTIND=1 OPTARG='' __panic_opt= |
| 42 | + while getopts s: __panic_opt |
| 43 | + do |
| 44 | + case $__panic_opt in |
| 45 | + (s) __panic_status="$OPTARG" ;; |
| 46 | + (*) return 70 |
| 47 | + esac |
| 48 | + done |
| 49 | + shift $((OPTIND - 1)) |
| 50 | + warn "${@-something went wrong.}" |
| 51 | + exit "$__panic_status" |
| 52 | +} |
| 53 | + |
| 54 | +cleanup() { |
| 55 | + __cleanup_status=$? |
| 56 | + set +e |
| 57 | + trap '' EXIT HUP INT TERM |
| 58 | + [ "${RELEASE-}" ] && [ -d "$RELEASE" ] && |
| 59 | + rm -rf "$RELEASE" |
| 60 | + [ "${RELEASES-}" ] && [ -d "$RELEASES" ] && |
| 61 | + rmdir "$RELEASES" 2>/dev/null |
| 62 | + if [ "${CLEANUP-}" ] |
| 63 | + then |
| 64 | + eval "$CLEANUP" |
| 65 | + unset CLEANUP |
| 66 | + fi |
| 67 | + kill -15 -$$ 2>/dev/null |
| 68 | + wait |
| 69 | + exit "$__cleanup_status" |
| 70 | +} |
| 71 | + |
| 72 | +int() { |
| 73 | + trap '' HUP INT TERM |
| 74 | + exit $((${1:?} + 128)) |
| 75 | +} |
| 76 | + |
| 77 | +catch() { |
| 78 | + SIG="${1:?}" |
| 79 | +} |
| 80 | + |
| 81 | +trapf() { |
| 82 | + [ $# -gt 1 ] || return 0 |
| 83 | + __trapf_func="$1" |
| 84 | + shift |
| 85 | + case $1 in |
| 86 | + (0) __trapf_cond=EXIT ;; |
| 87 | + (*) __trapf_cond="$(kill -l "$1")" && [ "$__trapf_cond" ] || |
| 88 | + panic -s 70 '%s: not a signal number.' "$1" |
| 89 | + esac |
| 90 | + # shellcheck disable=2064 |
| 91 | + trap "$__trapf_func $1" "$__trapf_cond" |
| 92 | + shift |
| 93 | + trapf "$__trapf_func" "$@" |
| 94 | + unset __trapf_func __trapf_cond |
| 95 | +} |
| 96 | + |
| 97 | + |
| 98 | +# DEFAULTS |
| 99 | +# ======= |
| 100 | + |
| 101 | +# FIXME |
| 102 | +MANIFEST="$REPO/Manifest" |
| 103 | + |
| 104 | +# FIXME |
| 105 | +RELEASES="$REPO/dist" |
| 106 | + |
| 107 | + |
| 108 | +# ARGUMENTS |
| 109 | +# ========= |
| 110 | + |
| 111 | +filter= |
| 112 | +OPTIND=1 OPTARG='' opt= |
| 113 | +while getopts m:f:d:h opt |
| 114 | +do |
| 115 | + case $opt in |
| 116 | + (f) filter="$OPTARG" ;; |
| 117 | + (m) MANIFEST="$OPTARG" ;; |
| 118 | + (d) RELEASES="$OPTARG" ;; |
| 119 | + (h) exec cat <<EOF |
| 120 | +$SCRIPT_NAME - Make a release |
| 121 | +
|
| 122 | +Synopsis: |
| 123 | + $SCRIPT_NAME [-d DIR] [-f FILTER] [-m MANIFEST] |
| 124 | + $SCRIPT_NAME -h |
| 125 | +
|
| 126 | +Options: |
| 127 | + -d DIR Save releases in DIR (default: ${RELEASES#"$REPO/"}). |
| 128 | + -f FILTER The Lua filter. Only needed if there is more |
| 129 | + than one Lua script in the Manifest. |
| 130 | + -m MANIDEST The Manifest file (default: ${MANIFEST#"$REPO/"}) |
| 131 | + -h Show this help screen. |
| 132 | +EOF |
| 133 | + ;; |
| 134 | + (*) exit 70 |
| 135 | + esac |
| 136 | +done |
| 137 | +shift $((OPTIND - 1)) |
| 138 | +[ $# -gt 0 ] && panic -s 64 'too many operands.' |
| 139 | + |
| 140 | +[ -f "$MANIFEST" ] || panic -s 66 '%s: no such file.' "$MANIFEST" |
| 141 | + |
| 142 | +if ! [ "$filter" ] |
| 143 | +then |
| 144 | + n=0 |
| 145 | + while read -r fname || [ "$fname" ] |
| 146 | + do |
| 147 | + case $fname in ('#'*|'') |
| 148 | + continue |
| 149 | + esac |
| 150 | + case $fname in (*.lua) |
| 151 | + filter="$fname" n=$((n + 1)) |
| 152 | + esac |
| 153 | + done <"$MANIFEST" |
| 154 | + case $n in |
| 155 | + (0) panic 'no Lua script in Manifest.' ;; |
| 156 | + (1) : ;; |
| 157 | + (*) panic 'too many Lua scripts in Manifest, use -f.' |
| 158 | + esac |
| 159 | + case $filter in |
| 160 | + (/*) : ;; |
| 161 | + (*) filter="$REPO/$filter" |
| 162 | + esac |
| 163 | +fi |
| 164 | + |
| 165 | +[ -f "$filter" ] || panic -s 66 '%s: no such file.' |
| 166 | + |
| 167 | + |
| 168 | +# INIT |
| 169 | +# ==== |
| 170 | + |
| 171 | +cd -P "$REPO" || exit 69 |
| 172 | +trap cleanup EXIT |
| 173 | +trapf int 1 2 15 |
| 174 | +mkdir -p "$RELEASES" || exit 69 |
| 175 | + |
| 176 | +# MAIN |
| 177 | +# ==== |
| 178 | + |
| 179 | +warn 'verifying branch ...' |
| 180 | + |
| 181 | +[ "$(git branch --show-current)" = main ] || |
| 182 | + panic 'not on "main" branch.' |
| 183 | + |
| 184 | +warn 'verifying version number ...' |
| 185 | + |
| 186 | +tag="$( git tag --sort=-version:refname | |
| 187 | + grep -E '^v' | |
| 188 | + sed 's/^v//; q;')" && |
| 189 | + [ "$tag" ] || |
| 190 | + panic 'failed to derive version from tag.' |
| 191 | + |
| 192 | +release="$(sed -n 's/-- *@release *//p;' "$filter")" && [ "$release" ] || |
| 193 | + panic '%s: failed to parse @release.' "${filter#"$REPO/"}" |
| 194 | + |
| 195 | +vers="$(sed -n "s/^ *VERSION *= *['\"]\([^'\"]*\)['\"].*/\1/p;" "$filter")" && |
| 196 | + [ "$vers" ] || |
| 197 | + panic '%s: failed to parse VERSION.' "${filter#"$REPO/"}" |
| 198 | + |
| 199 | +[ "$tag" = "$release" ] || |
| 200 | + panic -s 65 '%s: @release %s does not match tag v%s.' \ |
| 201 | + "${filter#"$REPO/"}" "$release" "$tag" |
| 202 | + |
| 203 | +[ "$tag" = "$vers" ] || |
| 204 | + panic -s 65 '%s: VERSION %s does not match tag v%s.' \ |
| 205 | + "${filter#"$REPO/"}" "$vers" "$tag" |
| 206 | + |
| 207 | +while read -r fname || [ "$fname" ] |
| 208 | +do |
| 209 | + case $fname in |
| 210 | + ('#'*|'') continue ;; |
| 211 | + (*[Rr][Ee][Aa][Dd][Mm][Ee]*) |
| 212 | + grep --fixed-strings --quiet "$tag" "$fname" || |
| 213 | + panic -s 65 '%s: does not reference v%s.' \ |
| 214 | + "${fname#"$REPO/"}" "$tag" |
| 215 | + esac |
| 216 | +done <"$MANIFEST" |
| 217 | + |
| 218 | +warn 'running tests ...' |
| 219 | + |
| 220 | +make test >/dev/null 2>&1 || |
| 221 | + panic 'at least one test failed.' |
| 222 | +make test -e SCRIPT="$filter" >/dev/null 2>&1 || |
| 223 | + panic 'at least one real-world test failed.' |
| 224 | + |
| 225 | +name="$(basename "$REPO")" && [ "$name" ] || |
| 226 | + panic '%s: failed to determine basename.' "$REPO" |
| 227 | + |
| 228 | +warn 'packing release ...' |
| 229 | + |
| 230 | +RELEASE= |
| 231 | +release="$RELEASES/$name-$tag" |
| 232 | +trapf catch 1 2 15 |
| 233 | +set +e |
| 234 | +mkdir "$release" && readonly RELEASE="$release" |
| 235 | +err=$? |
| 236 | +set -e |
| 237 | +trapf int 1 2 15 |
| 238 | +[ "${SIG-}" ] && int "$SIG" |
| 239 | +SIG= |
| 240 | +[ "$err" -eq 0 ] || exit 69 |
| 241 | +unset release |
| 242 | + |
| 243 | +lineno=0 |
| 244 | +# shellcheck disable=2094 |
| 245 | +while read -r fname || [ "$fname" ] |
| 246 | +do |
| 247 | + lineno=$((lineno + 1)) |
| 248 | + case $fname in ('#'*|'') |
| 249 | + continue |
| 250 | + esac |
| 251 | + case $fname in |
| 252 | + ("/$REPO"|"/$REPO/*") : ;; |
| 253 | + (/*) panic -s 65 '%s: line %d: %s: not within %s.' \ |
| 254 | + "$MANIFEST" "$lineno" "$fname" "$REPO" ;; |
| 255 | + (*) fname="$REPO/$fname" ;; |
| 256 | + esac |
| 257 | + [ -e "$fname" ] || |
| 258 | + panic -s 66 '%s: line %d: %s: no such file or directory.' \ |
| 259 | + "$MANIFEST" "$lineno" "$fname" |
| 260 | + dirname="$(dirname "$fname")" && [ "$dirname" ] || |
| 261 | + panic '%s: line %d: %s: failed to get directory.' \ |
| 262 | + "$MANIFEST" "$lineno" "$fname" |
| 263 | + mkdir -p "$RELEASE/${dirname#"$REPO"}" |
| 264 | + if [ -d "$fname" ] |
| 265 | + then cp -a "$fname/" "$RELEASE/${fname#"$REPO"}" |
| 266 | + else cp "$fname" "$RELEASE/${fname#"$REPO"}" |
| 267 | + fi |
| 268 | +done <"$MANIFEST" |
| 269 | + |
| 270 | +cd -P "$RELEASES" |
| 271 | + |
| 272 | +TAR="$RELEASES/$name-$tag.tgz" |
| 273 | +readonly TAR |
| 274 | +trapf catch 1 2 15 |
| 275 | +set +e |
| 276 | +: >"$TAR" && CLEANUP="rm -f \"\$TAR\"; ${CLEANUP-}" |
| 277 | +err=$? |
| 278 | +set -e |
| 279 | +trapf int 1 2 15 |
| 280 | +[ "${SIG-}" ] && int "$SIG" |
| 281 | +SIG= |
| 282 | +[ "$err" -eq 0 ] || exit 69 |
| 283 | +tar --create --gzip --file "$TAR" "$name-$tag" |
| 284 | + |
| 285 | +ZIP="$RELEASES/$name-$tag.zip" |
| 286 | +readonly ZIP |
| 287 | +trapf catch 1 2 15 |
| 288 | +set +e |
| 289 | +[ -e "$ZIP" ] && panic '%s: exists.' "$ZIP" |
| 290 | +zip --grow --recurse-paths --test --quiet "$ZIP" "$name-$tag" && |
| 291 | + CLEANUP="rm -f \"\$ZIP\"; ${CLEANUP-}" |
| 292 | +err=$? |
| 293 | +set -e |
| 294 | +trapf int 1 2 15 |
| 295 | +[ "${SIG-}" ] && int "$SIG" |
| 296 | +SIG= |
| 297 | +[ "$err" -eq 0 ] || exit 69 |
| 298 | + |
| 299 | +n=0 |
| 300 | +for file in "$TAR" "$ZIP" |
| 301 | +do |
| 302 | + n=$((n + 1)) signature="$file.sig" |
| 303 | + eval "FILE_$n=\"$signature\"" |
| 304 | + readonly "FILE_$n" |
| 305 | + trapf catch 1 2 15 |
| 306 | + set +e |
| 307 | + : >"$signature" && |
| 308 | + CLEANUP="rm -f \"\$FILE_$n\"; ${CLEANUP-}" |
| 309 | + err=$? |
| 310 | + set -e |
| 311 | + trapf int 1 2 15 |
| 312 | + [ "${SIG-}" ] && int "$SIG" |
| 313 | + SIG= |
| 314 | + [ "$err" -eq 0 ] || exit 69 |
| 315 | + gpg --detach-sign --output - "$file" >>"$signature" |
| 316 | +done |
| 317 | + |
| 318 | +warn 'pushing v%s to github ...' "$tag" |
| 319 | + |
| 320 | +git push origin "v$tag" |
| 321 | + |
| 322 | +warn 'drafting release ...' |
| 323 | + |
| 324 | +pre= |
| 325 | +case $tag in (*[a-z]*) |
| 326 | + pre=--prerelease ;; |
| 327 | +esac |
| 328 | + |
| 329 | +gh release create --draft $pre "$RELEASES/$name-$tag."* |
0 commit comments