summaryrefslogtreecommitdiff
path: root/bumper.yarn
blob: e67d0fa6037808ebcb25fb79f8f7c2cfa112fc3d (plain)
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
---
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 may use any programming languages.

* In projects using Python, the source tree 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.

* In projects not using Python, the version number is stored in the
  file `version.txt` or `version.yaml`, and the build system is
  expected to get it from there. If the project version is 1.2, then
  `version.txt` would contain "1.2\n" and `version.yaml` would contain
  "version: 1.2\nversion_info: [1, 2]\n". If both files exist, the
  YAML file has precedence.

* Bumper will update all of `version.py`, `version.txt`, and
  `version.yaml` if they exist.

* Bumper will update NEWS and debian/changelog as well, if they exist
  to make sure the version number is correct. It will update them in
  one commit to have the release number, and in a subsequent commit
  with a non-release number.

* Bumper will make several commits and a release tag in the git
  repository. The release tag is signed and annotated and of the form
  `foo-x.y` for project foo, release x.y.


# Using Bumper

In the examples below, we'll use Bumper on a 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. This is in the
`dot-gnupg` directory in the source tree.

    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. It also has the `version.txt` and `version.yaml` files.

    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 a file version.txt in foo containing "1.0\n"
    AND a file version.yaml in foo containing 
    ... "version = 1.0\nversion_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, version.txt contains "3.2\n"
    AND in foo, tag foo-3.2, version.yaml contains 
    ... "version: '3.2'\nversion_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 version.txt in foo contains "3.2+git\n"
    AND file version.yaml in foo contains 
    ... "version: 3.2+git\nversion_info: [3, 2, +git]\n"
    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, version.txt contains "3.4\n"
    AND in foo, tag foo-3.4, version.yaml contains 
    ... "version: '3.4'\nversion_info: [3, 4]\n"
    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