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
|
---
title: Bump version numbers when releasing
author: Lars Wirzenius
version: git version
...
# Introduction
Bumper is a small utility for updating version numbers when making a
release of a software project. It updates the version number in the
various locations in which it is stored in the source tree, creates a
git tag for indicating the release, then updates the source tree again
with a version number that indicates an unreleased version.
# Assumptions about the source tree
Bumper makes several assumptions about the source tree, to keep things
simpler. Here's a list:
* The source tree is stored in git. No other version control systems
are supported, sorry, but that's just because git is the only thing
the author uses now.
* The project is in Python. Again, this is just because that's what
the author uses.
* The project contains exactly one file called `version.py`, and that
file only sets the variables `__version__` and `__version_info__`.
Bumper will overwrite that file so it sets the desired values, and
commits those changes to git.
* `setup.py` is assumed to get the version from `__version__` from the
file Bumper writes, in some suitable way. Likewise the rest of the
project.
* It's OK for Bumper to make several commits and a tag in the git
repository.
# Using Bumper
In the examples below, we'll use Bumper on a fairly typical Python
project called `foo`, and we'll make a release 3.2 of it.
SCENARIO release a project
We will use a PGP test key for signing tags.
GIVEN a PGP key with id AA8CD13C
The Foo project consists of a main program, which uses a little Python
package where all the real code is, and where the version number is
also stored.
GIVEN Python project foo, version controlled by git
AND a file foolib/version.py in foo containing
... "__version__ = '1.0'\n__version_info__ = (1, 0)\n"
AND project foo has Debian packaging
We run Bumper to update version numbers and tag a new release. We
first try with a version that's older than the current one.
WHEN user attempts to run "bumper 0.1" in the foo directory
THEN bumper exits with code 1
We now run Bumper properly, and it does its various things.
WHEN user runs "bumper 3.2" in the foo directory
Bumper creates a git tag for the release, and updates files in the tag
to have the right version number.
THEN git repository foo has tag foo-3.2, signed with AA8CD13C
AND in foo, tag foo-3.2, foolib/version.py contains
... "__version__ = "3.2"\n__version_info__ = (3, 2)\n"
AND in foo, tag foo-3.2, NEWS matches
... "^Version 3.2, released \\d\\d\\d\\d-\\d\\d-\\d\\d$"
AND in foo, tag foo-3.2, has Debian version 3.2-1
Further, Bumper updates the files in master to have a version number
with `+git` appended, so that any non-release builds (builds not from
the tag) won't report a version number that looks like a release.
AND file foolib/version.py in foo contains
... "__version__ = "3.2+git"\n__version_info__ = (3, 2, '+git')\n"
AND file debian/changelog in foo matches
... "foo \\(3\\.2\\+git-1\\) UNRELEASED;"
AND file NEWS in foo matches
... "Version 3\\.2\\+git, not yet released"
We can also make a second release.
WHEN user runs "bumper 3.4" in the foo directory
THEN git repository foo has tag foo-3.4, signed with AA8CD13C
AND in foo, tag foo-3.4, foolib/version.py contains
... "__version__ = "3.4"\n__version_info__ = (3, 4)\n"
AND in foo, tag foo-3.4, NEWS matches
... "^Version 3.4, released \\d\\d\\d\\d-\\d\\d-\\d\\d$"
AND in foo, tag foo-3.4, has Debian version 3.4-1
AND file foolib/version.py in foo contains
... "__version__ = "3.4+git"\n__version_info__ = (3, 4, '+git')\n"
AND file debian/changelog in foo matches
... "foo \\(3\\.4\\+git-1\\) UNRELEASED;"
AND file NEWS in foo matches
... "Version 3\\.4\\+git, not yet released"
# Appendix: Scenario step implementations
This chapter provides executable implementations of the various
scenario steps, making this manual an automated [yarn][] test suite
for Bumper.
[yarn]: http://liw.fi/cmdtest/
IMPLEMENTS GIVEN a PGP key with id (\S+)
import os, shutil, cliapp, yarnstep
keyid = yarnstep.get_next_match()
src = yarnstep.srcdir('dot-gnupg')
dst = yarnstep.datadir('HOME/.gnupg')
shutil.copytree(src, dst)
cliapp.runcmd(
['gpg', '--list-keys', keyid],
cwd=os.environ['HOME'])
# Configure git to use this key for signing.
cliapp.runcmd(['git', 'config', '--global', 'user.signingkey', keyid])
IMPLEMENTS GIVEN Python project (\S+), version controlled by git
import os, cliapp, yarnstep
project = yarnstep.get_next_match()
dirname = yarnstep.datadir(project)
yarnstep.write_file(os.path.join(dirname, 'foolib', '__init__.py'), '''
from .version import __version__, __version_info__
''')
yarnstep.write_file(os.path.join(dirname, 'setup.py'), '''
from distutils.core import setup
import foolib
setup(name='{project}', version=foolib.__version__)
'''.format(project=project))
yarnstep.write_file(os.path.join(dirname, 'NEWS'), '''
NEWS for {project}
==================
Version 0.0, not yet released
-----------------------------
'''.format(project=project))
cliapp.runcmd(['git', 'init', dirname])
cliapp.runcmd(['git', 'add', '.'], cwd=dirname)
cliapp.runcmd(['git', 'commit', '-mInitial'], cwd=dirname)
IMPLEMENTS GIVEN a file (\S+) in (\S+) containing "(.*)"
import os, cliapp, yarnstep
filename = yarnstep.get_next_match()
dirname = yarnstep.get_next_match_as_datadir_path()
data = yarnstep.get_next_match()
yarnstep.write_file(
os.path.join(dirname, filename),
yarnstep.unescape_backslashes(data))
cliapp.runcmd(['git', 'add', filename], cwd=dirname)
cliapp.runcmd(
['git', 'commit', '-m', 'Add {}'.format(filename)],
cwd=dirname)
IMPLEMENTS GIVEN project (\S+) has Debian packaging
import os, cliapp, yarnstep
project = yarnstep.get_next_match()
dirname = yarnstep.datadir(project)
os.mkdir(os.path.join(dirname, 'debian'))
cliapp.runcmd(
['dch', '--create', '-v', '1.0-1', '--package', project, ''],
cwd=dirname)
cliapp.runcmd(['git', 'add', 'debian'], cwd=dirname)
cliapp.runcmd(['git', 'commit', '-m', 'Add debian packaging'], cwd=dirname)
IMPLEMENTS WHEN user attempts to run "bumper (\S+)" in the (\S+) directory
import cliapp, yarnstep
version = yarnstep.get_next_match()
dirname = yarnstep.get_next_match_as_datadir_path()
bin = yarnstep.srcdir('bumper')
returncode, out, err = cliapp.runcmd_unchecked([bin, version], cwd=dirname)
yarnstep.write_file(yarnstep.datadir('bumper.exit'), str(returncode))
IMPLEMENTS THEN bumper exits with code (\d+)
import yarnstep
expected = yarnstep.get_next_match()
actual = yarnstep.cat(yarnstep.datadir('bumper.exit'))
print 'expected:', repr(expected)
print 'actual:', repr(actual)
assert expected == actual
IMPLEMENTS WHEN user runs "bumper (\S+)" in the (\S+) directory
import cliapp, yarnstep
version = yarnstep.get_next_match()
dirname = yarnstep.get_next_match_as_datadir_path()
bin = yarnstep.srcdir('bumper')
cliapp.runcmd([bin, version], cwd=dirname)
IMPLEMENTS THEN file (\S+) in (\S+) contains "(.*)"
import os, yarnstep
filename = yarnstep.get_next_match()
dirname = yarnstep.get_next_match_as_datadir_path()
wanted_data_escaped = yarnstep.get_next_match()
wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped)
actual_data = yarnstep.cat(os.path.join(dirname, filename))
print 'wanted_data:', repr(wanted_data)
print 'actual_data:', repr(actual_data)
assert wanted_data == actual_data
IMPLEMENTS THEN file (\S+) in (\S+) matches "(.*)"
import re, os, yarnstep
filename = yarnstep.get_next_match()
dirname = yarnstep.get_next_match_as_datadir_path()
wanted_data_escaped = yarnstep.get_next_match()
wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped)
actual_data = yarnstep.cat(os.path.join(dirname, filename))
print 'wanted_data:', repr(wanted_data)
print 'actual_data:', repr(actual_data)
assert re.search(wanted_data, actual_data)
IMPLEMENTS THEN in (\S+), tag (\S+), (\S+) contains "(.*)"
import cliapp, yarnstep
dirname = yarnstep.get_next_match_as_datadir_path()
tag = yarnstep.get_next_match()
filename = yarnstep.get_next_match()
wanted_data_escaped = yarnstep.get_next_match()
wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped)
actual_data = cliapp.runcmd(
['git', 'cat-file', 'blob', '{}:{}'.format(tag, filename)],
cwd=dirname)
print 'wanted_data:', repr(wanted_data)
print 'actual_data:', repr(actual_data)
assert wanted_data == actual_data
IMPLEMENTS THEN in (\S+), tag (\S+), (\S+) matches "(.*)"
import re, cliapp, yarnstep
dirname = yarnstep.get_next_match_as_datadir_path()
tag = yarnstep.get_next_match()
filename = yarnstep.get_next_match()
wanted_data_escaped = yarnstep.get_next_match()
wanted_data = yarnstep.unescape_backslashes(wanted_data_escaped)
actual_data = cliapp.runcmd(
['git', 'cat-file', 'blob', '{}:{}'.format(tag, filename)],
cwd=dirname)
print 'wanted_data:', repr(wanted_data)
print 'actual_data:', repr(actual_data)
assert re.search(wanted_data, actual_data, flags=re.M)
IMPLEMENTS THEN in (\S+), tag (\S+), has Debian version (\S+)
import os, cliapp, yarnstep
dirname = yarnstep.get_next_match_as_datadir_path()
tag = yarnstep.get_next_match()
version = yarnstep.get_next_match()
text = cliapp.runcmd(
['git', 'cat-file', 'blob', '{}:debian/changelog'.format(tag)],
cwd=dirname)
line1, _ = text.split('\n', 1)
print 'line1:', repr(line1)
assert '({})'.format(version) in line1
assert line1.split()[2] != 'UNRELEASED;'
IMPLEMENTS THEN git repository (\S+) has tag (\S+), signed with (\S+)
import subprocess, cliapp, yarnstep
dirname = yarnstep.get_next_match_as_datadir_path()
tagname = yarnstep.get_next_match()
keyid = yarnstep.get_next_match()
output = cliapp.runcmd(
['git', 'verify-tag', tagname],
cwd=dirname,
stderr=subprocess.STDOUT)
assert keyid in output
|