tyop. update to latest cutarelease tool

This commit is contained in:
Trent Mick 2012-03-16 11:19:54 -07:00
parent ea39bf03a5
commit c8fe8dac34
2 changed files with 153 additions and 52 deletions

View file

@ -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");

View file

@ -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: