aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/release
blob: 6f1cde78f6df7eb766b63a693f658691f6093987 (plain) (blame)
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
#!/bin/sh
# release -- cut a release: bump ./VERSION, commit, tag. One command, so the
# two steps that must agree cannot be done separately.
#
#   contrib/release 0.1.2                    # bump + commit + tag v0.1.2
#   contrib/release 0.1.2 "What changed."    # ...with release notes in the tag
#   contrib/release --check                  # VERSION agrees with the newest tag?
#
# The mistake this exists to prevent: tagging vX.Y.Z while ./VERSION still
# says the previous number. VERSION is the floor contrib/version falls back to
# when there is no git -- a cgit snapshot, a Homebrew source build -- so a
# stale floor means those builds report a version that was already released.
# It happened for v0.1.1. Bumping and tagging as one step removes the step
# that was forgotten; the pre-push hook and `make release` both run --check,
# so a tag made by hand is caught before it escapes.
#
# After tagging, the script prints the push and `make release` commands rather
# than running them: pushing a tag is publishing, and publishing should be the
# thing you do on purpose.
set -eu

root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
cd "$root"

fail() { printf 'release: %s\n' "$*" >&2; exit 1; }

# The invariant, checkable on its own: after every release, ./VERSION equals
# the newest tag. It holds between releases too, because this script is the
# only thing that moves either.
if [ "${1:-}" = "--check" ]; then
    newest="$(git tag -l 'v[0-9]*' | sort -V | tail -1)"
    if [ -n "$newest" ] && [ "v$(cat VERSION)" != "$newest" ]; then
        fail "VERSION says $(cat VERSION) but the newest tag is $newest --
  the floor was not bumped when that tag was cut. Bring the file up by
  hand (echo ${newest#v} > VERSION, commit), and use contrib/release for
  the next one."
    fi
    exit 0
fi

[ $# -ge 1 ] || fail "usage: contrib/release X.Y.Z [notes] | contrib/release --check"
v="${1#v}"       # accept 0.1.2 and v0.1.2; the tag gets the v either way
notes="${2:-}"

# The shape first, so nothing below has to wonder.
printf '%s' "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$' \
    || fail "'$v' does not look like X.Y.Z (or X.Y.Z-rcN)"

# A release is cut from committed work on a branch, not from whatever happens
# to be lying around in the tree or a detached HEAD.
git symbolic-ref -q HEAD >/dev/null || fail "detached HEAD -- check out a branch"
git diff --quiet && git diff --cached --quiet \
    || fail "the tree is not clean -- commit or stash first"

git rev-parse -q --verify "refs/tags/v$v" >/dev/null \
    && fail "v$v already exists"

# Strictly newer than everything already out there: the floor, and the newest
# tag. sort -V, because 0.1.10 comes after 0.1.9 in versions and before it in
# the alphabet.
floor="$(cat VERSION)"
newest="$( { git tag -l 'v[0-9]*' | sed 's/^v//'; echo "$floor"; } | sort -V | tail -1)"
[ "$(printf '%s\n%s\n' "$newest" "$v" | sort -V | tail -1)" = "$v" ] && [ "$v" != "$newest" ] \
    || fail "$v is not newer than $newest"

# The cut itself: floor, commit, tag, in that order, so the tagged commit is
# the one whose VERSION file already says so.
printf '%s\n' "$v" > VERSION
git add VERSION
git commit -q -m "release: v$v" -- VERSION
if [ -n "$notes" ]; then
    git tag -a "v$v" -m "slosh $v" -m "$notes"
else
    git tag -a "v$v" -m "slosh $v"
fi

echo "release: v$v -- VERSION bumped, committed, tagged. To publish:"
echo "    git push origin master v$v"
echo "    make release REF=v$v      # the tarballs and zips, into dist/"
echo "    contrib/arch/README.md    # bump the PKGBUILD, contrib/arch-repo, upload"