tyop. update to latest cutarelease tool
This commit is contained in:
parent
ea39bf03a5
commit
c8fe8dac34
2 changed files with 153 additions and 52 deletions
|
@ -139,7 +139,7 @@ request handling. See the changelog for node-bunyan 0.3.0 for details.
|
||||||
## serializers
|
## serializers
|
||||||
|
|
||||||
Bunyan has a concept of **"serializers" to produce a JSON-able object from a
|
Bunyan has a concept of **"serializers" to produce a JSON-able object from a
|
||||||
JavaScript object**, so your can easily do the following:
|
JavaScript object**, so you can easily do the following:
|
||||||
|
|
||||||
log.info({req: <request object>}, "something about handling this request");
|
log.info({req: <request object>}, "something about handling this request");
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,14 @@ Conventions:
|
||||||
- XXX
|
- XXX
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version_info__ = (1, 0, 4)
|
__version_info__ = (1, 0, 6)
|
||||||
__version__ = '.'.join(map(str, __version_info__))
|
__version__ = '.'.join(map(str, __version_info__))
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from os.path import join, dirname, normpath, abspath, exists, basename, splitext
|
from os.path import join, dirname, normpath, abspath, exists, basename, splitext
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
from pprint import pprint
|
||||||
import re
|
import re
|
||||||
import codecs
|
import codecs
|
||||||
import logging
|
import logging
|
||||||
|
@ -31,7 +32,7 @@ import json
|
||||||
#---- globals and config
|
#---- globals and config
|
||||||
|
|
||||||
log = logging.getLogger("cutarelease")
|
log = logging.getLogger("cutarelease")
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -41,42 +42,44 @@ class Error(Exception):
|
||||||
|
|
||||||
def cutarelease(project_name, version_files, dry_run=False):
|
def cutarelease(project_name, version_files, dry_run=False):
|
||||||
"""Cut a release.
|
"""Cut a release.
|
||||||
|
|
||||||
@param project_name {str}
|
@param project_name {str}
|
||||||
@param version_files {list} List of paths to files holding the version
|
@param version_files {list} List of paths to files holding the version
|
||||||
info for this project.
|
info for this project.
|
||||||
|
|
||||||
If none are given it attempts to guess the version file:
|
If none are given it attempts to guess the version file:
|
||||||
package.json or VERSION.txt or VERSION or $project_name.py
|
package.json or VERSION.txt or VERSION or $project_name.py
|
||||||
or lib/$project_name.py or $project_name.js or lib/$project_name.js.
|
or lib/$project_name.py or $project_name.js or lib/$project_name.js.
|
||||||
|
|
||||||
The version file can be in one of the following forms:
|
The version file can be in one of the following forms:
|
||||||
|
|
||||||
- A .py file, in which case the file is expect to have a top-level
|
- A .py file, in which case the file is expect to have a top-level
|
||||||
global called "__version_info__" as follows. [1]
|
global called "__version_info__" as follows. [1]
|
||||||
|
|
||||||
__version_info__ = (0, 7, 6)
|
__version_info__ = (0, 7, 6)
|
||||||
|
|
||||||
Note that I typically follow that with the following to get a
|
Note that I typically follow that with the following to get a
|
||||||
string version attribute on my modules:
|
string version attribute on my modules:
|
||||||
|
|
||||||
__version__ = '.'.join(map(str, __version_info__))
|
__version__ = '.'.join(map(str, __version_info__))
|
||||||
|
|
||||||
- A .js file, in which case the file is expected to have a top-level
|
- A .js file, in which case the file is expected to have a top-level
|
||||||
global called "VERSION" as follows:
|
global called "VERSION" as follows:
|
||||||
|
|
||||||
ver VERSION = "1.2.3";
|
ver VERSION = "1.2.3";
|
||||||
|
|
||||||
- A "package.json" file, typical of a node.js npm-using project.
|
- A "package.json" file, typical of a node.js npm-using project.
|
||||||
The package.json file must have a "version" field.
|
The package.json file must have a "version" field.
|
||||||
|
|
||||||
- TODO: A simple version file whose only content is a "1.2.3"-style version
|
- TODO: A simple version file whose only content is a "1.2.3"-style version
|
||||||
string.
|
string.
|
||||||
|
|
||||||
[1]: This is a convention I tend to follow in my projects.
|
[1]: This is a convention I tend to follow in my projects.
|
||||||
Granted it might not be your cup of tea. I should add support for
|
Granted it might not be your cup of tea. I should add support for
|
||||||
just `__version__ = "1.2.3"`. I'm open to other suggestions too.
|
just `__version__ = "1.2.3"`. I'm open to other suggestions too.
|
||||||
"""
|
"""
|
||||||
|
dry_run_str = dry_run and " (dry-run)" or ""
|
||||||
|
|
||||||
if not version_files:
|
if not version_files:
|
||||||
log.info("guessing version file")
|
log.info("guessing version file")
|
||||||
candidates = [
|
candidates = [
|
||||||
|
@ -112,43 +115,37 @@ def cutarelease(project_name, version_files, dry_run=False):
|
||||||
if answer != "yes":
|
if answer != "yes":
|
||||||
log.info("user abort")
|
log.info("user abort")
|
||||||
return
|
return
|
||||||
log.info("cutting a %s release", version)
|
log.info("cutting a %s release%s", version, dry_run_str)
|
||||||
|
|
||||||
# Checks: Ensure there is a section in changes for this version.
|
# Checks: Ensure there is a section in changes for this version.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
changes_path = "CHANGES.md"
|
changes_path = "CHANGES.md"
|
||||||
if not exists(changes_path):
|
changes_txt, changes, nyr = parse_changelog(changes_path)
|
||||||
raise Error("'%s' not found" % changes_path)
|
#pprint(changes)
|
||||||
changes_txt = changes_txt_before = codecs.open(changes_path, 'r', 'utf-8').read()
|
top_ver = changes[0]["version"]
|
||||||
|
|
||||||
changes_parser = re.compile(r'^##\s+(?:.*?\s+)?v?(?P<ver>[\d\.abc]+)'
|
|
||||||
r'(?P<nyr>\s+\(not yet released\))?'
|
|
||||||
r'(?P<body>.*?)(?=^##|\Z)', re.M | re.S)
|
|
||||||
changes_sections = changes_parser.findall(changes_txt)
|
|
||||||
try:
|
|
||||||
top_ver = changes_sections[0][0]
|
|
||||||
except IndexError:
|
|
||||||
raise Error("unexpected error parsing `%s': parsed=%r" % (
|
|
||||||
changes_path, changes_sections))
|
|
||||||
if top_ver != version:
|
if top_ver != version:
|
||||||
raise Error("top section in `%s' is for "
|
raise Error("changelog '%s' top section says "
|
||||||
"version %r, expected version %r: aborting"
|
"version %r, expected version %r: aborting"
|
||||||
% (changes_path, top_ver, version))
|
% (changes_path, top_ver, version))
|
||||||
top_nyr = changes_sections[0][1].strip()
|
top_verline = changes[0]["verline"]
|
||||||
if not top_nyr:
|
if not top_verline.endswith(nyr):
|
||||||
answer = query_yes_no("\n* * *\n"
|
answer = query_yes_no("\n* * *\n"
|
||||||
"The top section in `%s' doesn't have the expected\n"
|
"The changelog '%s' top section doesn't have the expected\n"
|
||||||
"'(not yet released)' marker. Has this been released already?"
|
"'%s' marker. Has this been released already?"
|
||||||
% changes_path, default="yes")
|
% (changes_path, nyr), default="yes")
|
||||||
print "* * *"
|
print "* * *"
|
||||||
if answer != "no":
|
if answer != "no":
|
||||||
log.info("abort")
|
log.info("abort")
|
||||||
return
|
return
|
||||||
top_body = changes_sections[0][2]
|
top_body = changes[0]["body"]
|
||||||
if top_body.strip() == "(nothing yet)":
|
if top_body.strip() == "(nothing yet)":
|
||||||
raise Error("top section body is `(nothing yet)': it looks like "
|
raise Error("top section body is `(nothing yet)': it looks like "
|
||||||
"nothing has been added to this release")
|
"nothing has been added to this release")
|
||||||
|
|
||||||
# Commits to prepare release.
|
# Commits to prepare release.
|
||||||
|
changes_txt_before = changes_txt
|
||||||
changes_txt = changes_txt.replace(" (not yet released)", "", 1)
|
changes_txt = changes_txt.replace(" (not yet released)", "", 1)
|
||||||
if not dry_run and changes_txt != changes_txt_before:
|
if not dry_run and changes_txt != changes_txt_before:
|
||||||
log.info("prepare `%s' for release", changes_path)
|
log.info("prepare `%s' for release", changes_path)
|
||||||
|
@ -170,26 +167,34 @@ def cutarelease(project_name, version_files, dry_run=False):
|
||||||
answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
|
answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
|
||||||
print "* * *"
|
print "* * *"
|
||||||
if answer == "yes":
|
if answer == "yes":
|
||||||
run('npm publish')
|
if dry_run:
|
||||||
|
log.info("skipping npm publish (dry-run)")
|
||||||
|
else:
|
||||||
|
run('npm publish')
|
||||||
elif exists("setup.py"):
|
elif exists("setup.py"):
|
||||||
answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
|
answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
|
||||||
print "* * *"
|
print "* * *"
|
||||||
if answer == "yes":
|
if answer == "yes":
|
||||||
run("%spython setup.py sdist --formats zip upload"
|
if dry_run:
|
||||||
% _setup_command_prefix())
|
log.info("skipping pypi publish (dry-run)")
|
||||||
|
else:
|
||||||
|
run("%spython setup.py sdist --formats zip upload"
|
||||||
|
% _setup_command_prefix())
|
||||||
|
|
||||||
# Commits to prepare for future dev and push.
|
# Commits to prepare for future dev and push.
|
||||||
# - update changelog file
|
# - update changelog file
|
||||||
next_version_info = _get_next_version_info(version_info)
|
next_version_info = _get_next_version_info(version_info)
|
||||||
next_version = _version_from_version_info(next_version_info)
|
next_version = _version_from_version_info(next_version_info)
|
||||||
log.info("prepare for future dev (version %s)", next_version)
|
log.info("prepare for future dev (version %s)", next_version)
|
||||||
marker = "## %s %s\n" % (project_name, version)
|
marker = "## " + changes[0]["verline"]
|
||||||
|
if marker.endswith(nyr):
|
||||||
|
marker = marker[0:-len(nyr)]
|
||||||
if marker not in changes_txt:
|
if marker not in changes_txt:
|
||||||
raise Error("couldn't find `%s' marker in `%s' "
|
raise Error("couldn't find `%s' marker in `%s' "
|
||||||
"content: can't prep for subsequent dev" % (marker, changes_path))
|
"content: can't prep for subsequent dev" % (marker, changes_path))
|
||||||
changes_txt = changes_txt.replace("## %s %s\n" % (project_name, version),
|
next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr)
|
||||||
"## %s %s (not yet released)\n\n(nothing yet)\n\n## %s %s\n" % (
|
changes_txt = changes_txt.replace(marker + '\n',
|
||||||
project_name, next_version, project_name, version))
|
"%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker))
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
f = codecs.open(changes_path, 'w', 'utf-8')
|
f = codecs.open(changes_path, 'w', 'utf-8')
|
||||||
f.write(changes_txt)
|
f.write(changes_txt)
|
||||||
|
@ -240,6 +245,9 @@ def cutarelease(project_name, version_files, dry_run=False):
|
||||||
|
|
||||||
#---- internal support routines
|
#---- internal support routines
|
||||||
|
|
||||||
|
def _indent(s, indent=' '):
|
||||||
|
return indent + indent.join(s.splitlines(True))
|
||||||
|
|
||||||
def _tuple_from_version(version):
|
def _tuple_from_version(version):
|
||||||
def _intify(s):
|
def _intify(s):
|
||||||
try:
|
try:
|
||||||
|
@ -287,7 +295,7 @@ def _version_info_from_version(version):
|
||||||
|
|
||||||
def _parse_version_file(version_file):
|
def _parse_version_file(version_file):
|
||||||
"""Get version info from the given file. It can be any of:
|
"""Get version info from the given file. It can be any of:
|
||||||
|
|
||||||
Supported version file types (i.e. types of files from which we know
|
Supported version file types (i.e. types of files from which we know
|
||||||
how to parse the version string/number -- often by some convention):
|
how to parse the version string/number -- often by some convention):
|
||||||
- json: use the "version" key
|
- json: use the "version" key
|
||||||
|
@ -295,7 +303,7 @@ def _parse_version_file(version_file):
|
||||||
- python: Python script/module with `__version_info__ = (1, 2, 3)`
|
- python: Python script/module with `__version_info__ = (1, 2, 3)`
|
||||||
- version: a VERSION.txt or VERSION file where the whole contents are
|
- version: a VERSION.txt or VERSION file where the whole contents are
|
||||||
the version string
|
the version string
|
||||||
|
|
||||||
@param version_file {str} Can be a path or "type:path", where "type"
|
@param version_file {str} Can be a path or "type:path", where "type"
|
||||||
is one of the supported types.
|
is one of the supported types.
|
||||||
"""
|
"""
|
||||||
|
@ -310,11 +318,11 @@ def _parse_version_file(version_file):
|
||||||
}
|
}
|
||||||
if version_file_type in aliases:
|
if version_file_type in aliases:
|
||||||
version_file_type = aliases[version_file_type]
|
version_file_type = aliases[version_file_type]
|
||||||
|
|
||||||
f = codecs.open(version_file, 'r', 'utf-8')
|
f = codecs.open(version_file, 'r', 'utf-8')
|
||||||
content = f.read()
|
content = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
if not version_file_type:
|
if not version_file_type:
|
||||||
# Guess the type.
|
# Guess the type.
|
||||||
base = basename(version_file)
|
base = basename(version_file)
|
||||||
|
@ -328,7 +336,7 @@ def _parse_version_file(version_file):
|
||||||
elif content.startswith("#!"):
|
elif content.startswith("#!"):
|
||||||
shebang = content.splitlines(False)[0]
|
shebang = content.splitlines(False)[0]
|
||||||
shebang_bits = re.split(r'[/ \t]', shebang)
|
shebang_bits = re.split(r'[/ \t]', shebang)
|
||||||
for name, typ in {"python": "python", "node": "javascript"}.items():
|
for name, typ in {"python": "python", "node": "javascript"}.items():
|
||||||
if name in shebang_bits:
|
if name in shebang_bits:
|
||||||
version_file_type = typ
|
version_file_type = typ
|
||||||
break
|
break
|
||||||
|
@ -337,7 +345,7 @@ def _parse_version_file(version_file):
|
||||||
if not version_file_type:
|
if not version_file_type:
|
||||||
raise RuntimeError("can't extract version from '%s': no idea "
|
raise RuntimeError("can't extract version from '%s': no idea "
|
||||||
"what type of file it it" % version_file)
|
"what type of file it it" % version_file)
|
||||||
|
|
||||||
if version_file_type == "json":
|
if version_file_type == "json":
|
||||||
obj = json.loads(content)
|
obj = json.loads(content)
|
||||||
version_info = _version_info_from_version(obj["version"])
|
version_info = _version_info_from_version(obj["version"])
|
||||||
|
@ -355,6 +363,100 @@ def _parse_version_file(version_file):
|
||||||
return version_file_type, version_info
|
return version_file_type, version_info
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog(changes_path):
|
||||||
|
"""Parse the given changelog path and return `(content, parsed, nyr)`
|
||||||
|
where `nyr` is the ' (not yet released)' marker and `parsed` looks like:
|
||||||
|
|
||||||
|
[{'body': u'\n(nothing yet)\n\n',
|
||||||
|
'verline': u'restify 1.0.1 (not yet released)',
|
||||||
|
'version': u'1.0.1'}, # version is parsed out for top section only
|
||||||
|
{'body': u'...',
|
||||||
|
'verline': u'1.0.0'},
|
||||||
|
{'body': u'...',
|
||||||
|
'verline': u'1.0.0-rc2'},
|
||||||
|
{'body': u'...',
|
||||||
|
'verline': u'1.0.0-rc1'}]
|
||||||
|
|
||||||
|
A changelog (CHANGES.md) is expected to look like this:
|
||||||
|
|
||||||
|
# $project Changelog
|
||||||
|
|
||||||
|
## $next_version (not yet released)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## $version1
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## $version2
|
||||||
|
|
||||||
|
... and so on
|
||||||
|
|
||||||
|
The version lines are enforced as follows:
|
||||||
|
|
||||||
|
- The top entry should have a " (not yet released)" suffix. "Should"
|
||||||
|
because recovery from half-cutarelease failures is supported.
|
||||||
|
- A version string must be extractable from there, but it tries to
|
||||||
|
be loose (though strict "X.Y.Z" versioning is preferred). Allowed
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
## my project 1.0.1
|
||||||
|
## foo 1.2.3-rc2
|
||||||
|
|
||||||
|
Basically, (a) the " (not yet released)" is stripped, (b) the
|
||||||
|
last token is the version, and (c) that version must start with
|
||||||
|
a digit (sanity check).
|
||||||
|
"""
|
||||||
|
if not exists(changes_path):
|
||||||
|
raise Error("changelog file '%s' not found" % changes_path)
|
||||||
|
content = codecs.open(changes_path, 'r', 'utf-8').read()
|
||||||
|
|
||||||
|
parser = re.compile(
|
||||||
|
r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)',
|
||||||
|
re.M | re.S)
|
||||||
|
sections = parser.findall(content)
|
||||||
|
|
||||||
|
# Sanity checks on changelog format.
|
||||||
|
if not sections:
|
||||||
|
template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
|
||||||
|
raise Error("changelog '%s' must have at least one section, "
|
||||||
|
"suggestion:\n\n%s" % (changes_path, _indent(template)))
|
||||||
|
first_section_verline = sections[0][0]
|
||||||
|
nyr = ' (not yet released)'
|
||||||
|
#if not first_section_verline.endswith(nyr):
|
||||||
|
# eg = "## %s%s" % (first_section_verline, nyr)
|
||||||
|
# raise Error("changelog '%s' top section must end with %r, "
|
||||||
|
# "naive e.g.: '%s'" % (changes_path, nyr, eg))
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for i, section in enumerate(sections):
|
||||||
|
item = {
|
||||||
|
"verline": section[0],
|
||||||
|
"body": section[1]
|
||||||
|
}
|
||||||
|
if i == 0:
|
||||||
|
# We only bother to pull out 'version' for the top section.
|
||||||
|
verline = section[0]
|
||||||
|
if verline.endswith(nyr):
|
||||||
|
verline = verline[0:-len(nyr)]
|
||||||
|
version = verline.split()[-1]
|
||||||
|
try:
|
||||||
|
int(version[0])
|
||||||
|
except ValueError:
|
||||||
|
msg = ''
|
||||||
|
if version.endswith(')'):
|
||||||
|
msg = " (cutarelease is picky about the trailing %r " \
|
||||||
|
"on the top version line. Perhaps you misspelled " \
|
||||||
|
"that?)" % nyr
|
||||||
|
raise Error("changelog '%s' top section version '%s' is "
|
||||||
|
"invalid: first char isn't a number%s"
|
||||||
|
% (changes_path, version, msg))
|
||||||
|
item["version"] = version
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
return content, items, nyr
|
||||||
|
|
||||||
## {{{ http://code.activestate.com/recipes/577058/ (r2)
|
## {{{ http://code.activestate.com/recipes/577058/ (r2)
|
||||||
def query_yes_no(question, default="yes"):
|
def query_yes_no(question, default="yes"):
|
||||||
"""Ask a yes/no question via raw_input() and return their answer.
|
"""Ask a yes/no question via raw_input() and return their answer.
|
||||||
|
@ -457,11 +559,11 @@ def main(argv):
|
||||||
help='Do a dry-run', default=False)
|
help='Do a dry-run', default=False)
|
||||||
opts, args = parser.parse_args()
|
opts, args = parser.parse_args()
|
||||||
log.setLevel(opts.log_level)
|
log.setLevel(opts.log_level)
|
||||||
|
|
||||||
cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
|
cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
|
||||||
|
|
||||||
|
|
||||||
## {{{ http://code.activestate.com/recipes/577258/ (r5)
|
## {{{ http://code.activestate.com/recipes/577258/ (r5+)
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
retval = main(sys.argv)
|
retval = main(sys.argv)
|
||||||
|
@ -488,7 +590,6 @@ if __name__ == "__main__":
|
||||||
log.error(exc_info[0])
|
log.error(exc_info[0])
|
||||||
if not skip_it:
|
if not skip_it:
|
||||||
if log.isEnabledFor(logging.DEBUG):
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
print()
|
|
||||||
traceback.print_exception(*exc_info)
|
traceback.print_exception(*exc_info)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Reference in a new issue