Merge pull request #185 from ankostis/bumpver

feat(build): add `/bin/bumpver.py` script to facilitate new releases
This commit is contained in:
Matthew Planchard 2017-11-10 12:10:42 -06:00 committed by GitHub
commit 35f9740429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 308 additions and 6 deletions

37
bin/README.rst Normal file
View File

@ -0,0 +1,37 @@
====================
Build scripts folder
====================
Files:
======
- ``bumpver.py`` : Bump, commit and tag new project versions
- ``check_readme.sh`` : Check that README has no RsT-syntactic errors.
- ``commit-standalone.sh`` : Create an executable file and add it into `standalone` branch.
- ``gen-standalone.sh`` : Invoked by `commit-standalone.sh`.
- ``git-new-workdir`` : Invoked by `gen-standalone.sh`.
- ``package.sh`` : Build deployable artifact (wheel) in ``/dist/`` folder.
- ``README.rst`` : This file.
Release check-list:
===================
1. Update ``/CHANGES.rst`` (+ Title + Date) & ``/README.rst``.
2. Push to GitHub to run all TCs once more.
3. Bump version: commit & tag it with ``/bin/bumpver.py``.
Use ``--help``.
Read `PEP-440 <https://www.python.org/dev/peps/pep-0440/`_ to decide the version.
4. Push it in GitHub with ``--tag``.
5. Generate package *wheel* with ``/bin/package.sh``.
6. Upload to PyPi with ``twine upload -su <gpg-user> dist/*``:
7. Run ``commit-standalone.sh``.
8. Copy release notes from ``/CHANGES.rst`` in GitHub as new *"release"* page
on the new tag. Check syntactic differences between ``.md`` and ``.rst`` files.

260
bin/bumpver.py Normal file
View File

@ -0,0 +1,260 @@
#!/usr/bin/env python
#
# NEED POSIX (i.e. *Cygwin* on Windows).
"""
Script to bump, commit and tag new versions.
USAGE:
bumpver
bumpver [-n] [-f] [-c] [-a] [-t <message>] <new-ver>
Without <new-ver> prints version extracted from current file.
Don't add a 'v' prefix!
OPTIONS:
-a, --amend Amend current commit for setting the "chore(ver): ..." msg.
-f, --force Bump (and optionally) commit/tag if version exists/is same.
-n, --dry-run Do not write files - just pretend.
-c, --commit Commit afterwardswith a commit-message describing version bump.
-t, --tag=<msg> Adds a signed tag with the given message (commit implied).
- Pre-releases: when working on some verion
X.YbN # Beta release
X.YrcN or X.YcN # Release Candidate
X.Y # Final release
- Post-release:
X.YaN.postM # Post-release of an alpha release
X.YrcN.postM # Post-release of a release candidate
- Dev-release:
X.YaN.devM # Developmental release of an alpha release
X.Y.postN.devM # Developmental release of a post-release
EXAMPLE:
bumpver -t 'Mostly model changes' 1.6.2b0
"""
import os.path as osp
import sys
import re
import functools as fnt
import docopt
my_dir = osp.dirname(__file__)
VFILE = osp.join(my_dir, '..', 'pypiserver', '__init__.py')
VFILE_regex_v = re.compile(r'version *= *__version__ *= *"([^"]+)"')
VFILE_regex_d = re.compile(r'__updated__ *= *"([^"]+)"')
RFILE = osp.join(my_dir, '..', 'README.rst')
PYTEST_ARGS = [osp.join('tests', 'test_docs.py')]
class CmdException(Exception):
pass
@fnt.lru_cache()
def read_txtfile(fpath):
with open(fpath, 'rt', encoding='utf-8') as fp:
return fp.read()
def extract_file_regexes(fpath, regexes):
"""
:param regexes:
A sequence of regexes to "search", having a single capturing-group.
:return:
One groups per regex, or raise if any regex did not match.
"""
txt = read_txtfile(fpath)
matches = [regex.search(txt) for regex in regexes]
if not all(matches):
raise CmdException("Failed extracting current versions with: %s"
"\n matches: %s" %
(regexes, matches))
return [m.group(1) for m in matches]
def replace_substrings(files, subst_pairs):
for fpath in files:
txt = read_txtfile(fpath)
replacements = []
for old, new in subst_pairs:
replacements.append((old, new, txt.count(old)))
txt = txt.replace(old, new)
yield (txt, fpath, replacements)
def format_syscmd(cmd):
if isinstance(cmd, (list, tuple)):
cmd = ' '.join('"%s"' % s if ' ' in s else s
for s in cmd)
else:
assert isinstance(cmd, str), cmd
return cmd
def strip_ver2_commonprefix(ver1, ver2):
cprefix = osp.commonprefix([ver1, ver2])
if cprefix:
striplen = cprefix.rfind('.')
if striplen > 0:
striplen += 1
else:
striplen = len(cprefix)
ver2 = ver2[striplen:]
return ver2
def run_testcases():
import pytest
retcode = pytest.main(PYTEST_ARGS)
if retcode:
raise CmdException(
"Doc TCs failed(%s), probably version-bumping has failed!" % retcode)
def exec_cmd(cmd):
import subprocess as sbp
err = sbp.call(cmd, stderr=sbp.STDOUT)
if err:
raise CmdException("Failed(%i) on: %s" % (err, format_syscmd(cmd)))
def do_commit(new_ver, old_ver, dry_run, amend, ver_files):
import pathlib
#new_ver = strip_ver2_commonprefix(old_ver, new_ver)
cmt_msg = 'chore(ver): bump %s-->%s' % (old_ver, new_ver)
ver_files = [pathlib.Path(f).as_posix() for f in ver_files]
git_add = ['git', 'add'] + ver_files
git_cmt = ['git', 'commit', '-m', cmt_msg]
if amend:
git_cmt.append('--amend')
commands = [git_add, git_cmt]
for cmd in commands:
cmd_str = format_syscmd(cmd)
if dry_run:
yield "DRYRUN: %s" % cmd_str
else:
yield "EXEC: %s" % cmd_str
exec_cmd(cmd)
def do_tag(tag, tag_msg, dry_run, force):
cmd = ['git', 'tag', tag, '-s', '-m', tag_msg]
if force:
cmd.append('--force')
cmd_str = format_syscmd(cmd)
if dry_run:
yield "DRYRUN: %s" % cmd_str
else:
yield "EXEC: %s" % cmd_str
exec_cmd(cmd)
def bumpver(new_ver, dry_run=False, force=False, amend=False,
tag_name_or_commit=None):
"""
:param tag_name_or_commit:
if true, do `git commit`, if string, also `git tag` with that as msg.
"""
if amend:
## Restore previous version before extracting it.
cmd = 'git checkout HEAD~ --'.split()
cmd.append(VFILE)
cmd.append(RFILE)
exec_cmd(cmd)
regexes = [VFILE_regex_v, VFILE_regex_d]
old_ver, old_date = extract_file_regexes(VFILE, regexes)
if not new_ver:
yield old_ver
yield old_date
else:
if new_ver == old_ver:
msg = "Version '%s'already bumped"
if force:
msg += ", but --force effected."
yield msg % new_ver
else:
msg += "!\n Use of --force recommended."
raise CmdException(msg % new_ver)
from datetime import datetime
new_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S%z')
ver_files = [osp.normpath(f) for f in [VFILE, RFILE]]
subst_pairs = [(old_ver, new_ver), (old_date, new_date)]
for repl in replace_substrings(ver_files, subst_pairs):
new_txt, fpath, replacements = repl
if not dry_run:
with open(fpath, 'wt', encoding='utf-8') as fp:
fp.write(new_txt)
yield '%s: ' % fpath
for old, new, nrepl in replacements:
yield ' %i x (%24s --> %s)' % (nrepl, old, new)
yield "...now launching DocTCs..."
run_testcases()
if tag_name_or_commit is not None:
yield from do_commit(new_ver, old_ver, dry_run, amend, ver_files)
if isinstance(tag_name_or_commit, str):
tag = 'v%s' % new_ver
yield from do_tag(tag, tag_name_or_commit, dry_run, force)
def main(*args):
opts = docopt.docopt(__doc__, argv=args)
new_ver = opts['<new-ver>']
assert not new_ver or new_ver[0] != 'v', (
"Version '%s' must NOT start with `v`!" % new_ver)
commit = opts['--commit']
tag = opts['--tag']
if tag:
tag_name_or_commit = tag
elif commit:
tag_name_or_commit = True
else:
tag_name_or_commit = None
try:
for i in bumpver(new_ver,
opts['--dry-run'],
opts['--force'],
opts['--amend'],
tag_name_or_commit):
print(i)
except CmdException as ex:
sys.exit(str(ex))
except Exception as ex:
raise ex
if __name__ == '__main__':
main(*sys.argv[1:])

View File

@ -1,9 +1,9 @@
#! /bin/sh
##
#
## Create an executable file and add it into `standalone` branch
##
## Invoke it directly on the commmit "tagged" for a release.
## Invoke it with any arg to avoid committing into `standalone` branch.
#
# Invoke it directly on the commmit "tagged" for a release.
# Invoke it with any arg to avoid committing into `standalone` branch.
set -x

View File

@ -1,7 +1,7 @@
#! /bin/sh
##
#
## Create an executable zip file.
## Invoked by `commit-standalone.sh`.
# Invoked by `commit-standalone.sh`.
set -x

View File

@ -1,3 +1,7 @@
#! /bin/sh
#
##
my_dir=`dirname "$0"`
cd $my_dir/..

View File

@ -20,3 +20,4 @@ twine>=1.7,<=1.7.4
WebOb==0.9.6.1; python_version == '2.5'
BeautifulSoup==3.2.1; python_version == '2.5'
WebTest==1.4.3; python_version == '2.5'
docopt # For `/bin/bumpver.py`.