+++ /dev/null
-#!/usr/bin/python
-
-"""Release testtools on Launchpad.
-
-Steps:
- 1. Make sure all "Fix committed" bugs are assigned to 'next'
- 2. Rename 'next' to the new version
- 3. Release the milestone
- 4. Upload the tarball
- 5. Create a new 'next' milestone
- 6. Mark all "Fix committed" bugs in the milestone as "Fix released"
-
-Assumes that NEWS is in the parent directory, that the release sections are
-underlined with '~' and the subsections are underlined with '-'.
-
-Assumes that this file is in the 'scripts' directory a testtools tree that has
-already had a tarball built and uploaded with 'python setup.py sdist upload
---sign'.
-"""
-
-from datetime import datetime, timedelta, tzinfo
-import logging
-import os
-import sys
-
-from launchpadlib.launchpad import Launchpad
-from launchpadlib import uris
-
-
-APP_NAME = 'testtools-lp-release'
-CACHE_DIR = os.path.expanduser('~/.launchpadlib/cache')
-SERVICE_ROOT = uris.LPNET_SERVICE_ROOT
-
-FIX_COMMITTED = u"Fix Committed"
-FIX_RELEASED = u"Fix Released"
-
-# Launchpad file type for a tarball upload.
-CODE_RELEASE_TARBALL = 'Code Release Tarball'
-
-PROJECT_NAME = 'testtools'
-NEXT_MILESTONE_NAME = 'next'
-
-
-class _UTC(tzinfo):
- """UTC"""
-
- def utcoffset(self, dt):
- return timedelta(0)
-
- def tzname(self, dt):
- return "UTC"
-
- def dst(self, dt):
- return timedelta(0)
-
-UTC = _UTC()
-
-
-def configure_logging():
- level = logging.INFO
- log = logging.getLogger(APP_NAME)
- log.setLevel(level)
- handler = logging.StreamHandler()
- handler.setLevel(level)
- formatter = logging.Formatter("%(levelname)s: %(message)s")
- handler.setFormatter(formatter)
- log.addHandler(handler)
- return log
-LOG = configure_logging()
-
-
-def get_path(relpath):
- """Get the absolute path for something relative to this file."""
- return os.path.abspath(
- os.path.join(
- os.path.dirname(os.path.dirname(__file__)), relpath))
-
-
-def assign_fix_committed_to_next(testtools, next_milestone):
- """Find all 'Fix Committed' and make sure they are in 'next'."""
- fixed_bugs = list(testtools.searchTasks(status=FIX_COMMITTED))
- for task in fixed_bugs:
- LOG.debug("%s" % (task.title,))
- if task.milestone != next_milestone:
- task.milestone = next_milestone
- LOG.info("Re-assigning %s" % (task.title,))
- task.lp_save()
-
-
-def rename_milestone(next_milestone, new_name):
- """Rename 'next_milestone' to 'new_name'."""
- LOG.info("Renaming %s to %s" % (next_milestone.name, new_name))
- next_milestone.name = new_name
- next_milestone.lp_save()
-
-
-def get_release_notes_and_changelog(news_path):
- release_notes = []
- changelog = []
- state = None
- last_line = None
-
- def is_heading_marker(line, marker_char):
- return line and line == marker_char * len(line)
-
- LOG.debug("Loading NEWS from %s" % (news_path,))
- with open(news_path, 'r') as news:
- for line in news:
- line = line.strip()
- if state is None:
- if is_heading_marker(line, '~'):
- milestone_name = last_line
- state = 'release-notes'
- else:
- last_line = line
- elif state == 'title':
- # The line after the title is a heading marker line, so we
- # ignore it and change state. That which follows are the
- # release notes.
- state = 'release-notes'
- elif state == 'release-notes':
- if is_heading_marker(line, '-'):
- state = 'changelog'
- # Last line in the release notes is actually the first
- # line of the changelog.
- changelog = [release_notes.pop(), line]
- else:
- release_notes.append(line)
- elif state == 'changelog':
- if is_heading_marker(line, '~'):
- # Last line in changelog is actually the first line of the
- # next section.
- changelog.pop()
- break
- else:
- changelog.append(line)
- else:
- raise ValueError("Couldn't parse NEWS")
-
- release_notes = '\n'.join(release_notes).strip() + '\n'
- changelog = '\n'.join(changelog).strip() + '\n'
- return milestone_name, release_notes, changelog
-
-
-def release_milestone(milestone, release_notes, changelog):
- date_released = datetime.now(tz=UTC)
- LOG.info(
- "Releasing milestone: %s, date %s" % (milestone.name, date_released))
- release = milestone.createProductRelease(
- date_released=date_released,
- changelog=changelog,
- release_notes=release_notes,
- )
- milestone.is_active = False
- milestone.lp_save()
- return release
-
-
-def create_milestone(series, name):
- """Create a new milestone in the same series as 'release_milestone'."""
- LOG.info("Creating milestone %s in series %s" % (name, series.name))
- return series.newMilestone(name=name)
-
-
-def close_fixed_bugs(milestone):
- tasks = list(milestone.searchTasks())
- for task in tasks:
- LOG.debug("Found %s" % (task.title,))
- if task.status == FIX_COMMITTED:
- LOG.info("Closing %s" % (task.title,))
- task.status = FIX_RELEASED
- else:
- LOG.warning(
- "Bug not fixed, removing from milestone: %s" % (task.title,))
- task.milestone = None
- task.lp_save()
-
-
-def upload_tarball(release, tarball_path):
- with open(tarball_path) as tarball:
- tarball_content = tarball.read()
- sig_path = tarball_path + '.asc'
- with open(sig_path) as sig:
- sig_content = sig.read()
- tarball_name = os.path.basename(tarball_path)
- LOG.info("Uploading tarball: %s" % (tarball_path,))
- release.add_file(
- file_type=CODE_RELEASE_TARBALL,
- file_content=tarball_content, filename=tarball_name,
- signature_content=sig_content,
- signature_filename=sig_path,
- content_type="application/x-gzip; charset=binary")
-
-
-def release_project(launchpad, project_name, next_milestone_name):
- testtools = launchpad.projects[project_name]
- next_milestone = testtools.getMilestone(name=next_milestone_name)
- release_name, release_notes, changelog = get_release_notes_and_changelog(
- get_path('NEWS'))
- LOG.info("Releasing %s %s" % (project_name, release_name))
- # Since reversing these operations is hard, and inspecting errors from
- # Launchpad is also difficult, do some looking before leaping.
- errors = []
- tarball_path = get_path('dist/%s-%s.tar.gz' % (project_name, release_name,))
- if not os.path.isfile(tarball_path):
- errors.append("%s does not exist" % (tarball_path,))
- if not os.path.isfile(tarball_path + '.asc'):
- errors.append("%s does not exist" % (tarball_path + '.asc',))
- if testtools.getMilestone(name=release_name):
- errors.append("Milestone %s exists on %s" % (release_name, project_name))
- if errors:
- for error in errors:
- LOG.error(error)
- return 1
- assign_fix_committed_to_next(testtools, next_milestone)
- rename_milestone(next_milestone, release_name)
- release = release_milestone(next_milestone, release_notes, changelog)
- upload_tarball(release, tarball_path)
- create_milestone(next_milestone.series_target, next_milestone_name)
- close_fixed_bugs(next_milestone)
- return 0
-
-
-def main(args):
- launchpad = Launchpad.login_with(APP_NAME, SERVICE_ROOT, CACHE_DIR)
- return release_project(launchpad, PROJECT_NAME, NEXT_MILESTONE_NAME)
-
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv))