예제 #1
0
 def update_batou(self):
     """Update the version pins on batou as well"""
     deployment_repo = self.buildout.sources.get('deployment')
     if deployment_repo is None:
         logger.info('No deployment repository sources found!'
                     '\n'
                     'Batou can not be updated!')
         return
     # clone the repo
     with git_repo(deployment_repo, shallow=False) as repo:
         # get components/plone/versions/versions.cfg Buildout
         path = 'components/plone/versions/versions.cfg'
         plone_versions = '{0}/{1}'.format(repo.working_tree_dir, path)
         deployment_buildout = Buildout(
             sources_file=plone_versions,
             checkouts_file=plone_versions,
             versions_file=plone_versions)
         # update version pins
         for dist_name in self.versions:
             deployment_buildout.set_version(dist_name,
                                             self.versions[dist_name])
         # commit and push the repo
         repo.index.add([
             path,
         ])
         repo.index.commit(message=self.commit_message.encode('utf-8'))
         # push the changes
         repo.remote().push()
예제 #2
0
    def __init__(self, path="src", test=False, filter_distributions="", offline=False, branch="master"):
        self.path = path
        self.test = test
        self.offline = offline
        self.filters = filter_distributions
        self.branch = branch
        self.buildout = Buildout(sources_file="sources.cfg", checkouts_file="buildout.cfg")

        if self.offline and not self.test:
            logger.warn(
                "Offline operations means that no release can be done. " "Test option has been turned on as well."
            )
            self.test = True
예제 #3
0
    def __init__(
            self,
            path='src',
            test=False,
            filter_distributions='',
            offline=False,
            branch='master',
    ):
        self.path = path
        self.test = test
        self.offline = offline
        self.filters = filter_distributions
        self.branch = branch
        self.buildout = Buildout(
            sources_file='sources.cfg',
            checkouts_file='buildout.cfg',
        )

        if self.offline and not self.test:
            logger.warn(
                'Offline operations means that no release can be done. '
                'Test option has been turned on as well.')
            self.test = True
예제 #4
0
class FullRelease(object):
    """Releases all distributions that have changes and want to be released

    Does lots of QA before and after any release actually happens as well as
    another bunch of boring tasks worth automating.
    """

    #: system path where to look for distributions to be released
    path = 'src'

    #: if actual releases have to happen or only gathering an overview of
    #: what's pending to be released
    test = None

    #: if network will be used (only to be used together with test)
    offline = None

    #: only release the distributions that their name match with this string
    filters = None

    #: distributions that will be released
    distributions = []

    #: plone.releaser.buildout.Buildout instance to get distribution's info
    #: and save new versions
    buildout = None

    #: changelog for each released distribution
    changelogs = {}

    #: version for each released distribution
    versions = {}

    #: last tag for each released distribution (before the new release)
    last_tags = {}

    #: global commit message for zope and deployment repositories which lists
    #: all distributions released and their changelog
    commit_message = ''

    def __init__(
            self,
            path='src',
            test=False,
            filter_distributions='',
            offline=False,
            branch='master',
    ):
        self.path = path
        self.test = test
        self.offline = offline
        self.filters = filter_distributions
        self.branch = branch
        self.buildout = Buildout(
            sources_file='sources.cfg',
            checkouts_file='buildout.cfg',
        )

        if self.offline and not self.test:
            logger.warn(
                'Offline operations means that no release can be done. '
                'Test option has been turned on as well.')
            self.test = True

    def __call__(self):
        """Go through all distributions and release them if needed *and* wanted
        """
        self.get_all_distributions()
        self.filter_distros()
        if not self.offline:
            self.check_pending_local_changes()
        self.check_changes_to_be_released()
        self.ask_what_to_release()

        if not self.test and len(self.distributions) > 0:
            self.release_all()
            self._create_commit_message()
            self.update_buildout()
            # push cfg files so that jenkins gets them already
            push_cfg_files()
            self.update_batou()

    def get_all_distributions(self):
        """Get all distributions that are found in self.path"""
        for folder in sorted(os.listdir(self.path)):
            path = '{0}/{1}'.format(self.path, folder)
            if not os.path.isdir(path):
                continue

            try:
                Repo(path)
            except InvalidGitRepositoryError:
                continue

            self.distributions.append(path)

        logger.debug('Distributions: ')
        logger.debug('\n'.join(self.distributions))

    def filter_distros(self):
        if not self.filters:
            return

        tmp_list = []
        for f in self.filters:
            tmp_list += [d for d in self.distributions if d.find(f) != -1]
        # keep them sorted
        self.distributions = sorted(tmp_list)

    def check_pending_local_changes(self):
        """Check that the distributions do not have local changes"""
        logger.info('')
        msg = 'Check pending local changes'
        logger.info(msg)
        logger.info('-' * len(msg))
        clean_distributions = []
        for distribution_path in self.distributions:
            # nice to have: add some sort of progress bar like plone.releaser
            repo = Repo(distribution_path)

            dirty = False
            local_changes = False

            if repo.is_dirty():
                dirty = True

            if not is_branch_synced(repo, branch=self.branch):
                local_changes = True

            if dirty or local_changes:
                msg = '{0} has non-committed/unpushed changes, ' \
                      'it will not be released.'
                msg = msg.format(DISTRIBUTION.format(distribution_path))
                logger.info(msg)
                continue

            clean_distributions.append(distribution_path)

        # if nothing is about to be released, do not filter the distributions
        if not self.test:
            if len(self.distributions) != len(clean_distributions):
                if not ask('Do you want to continue?', default=True):
                    sys.exit()

            self.distributions = clean_distributions

        logger.debug('Distributions: ')
        logger.debug('\n'.join(self.distributions))

    def check_changes_to_be_released(self):
        """Check which distributions have changes that could need a release"""
        logger.info('')
        msg = 'Check changes to be released'
        logger.info(msg)
        logger.info('-' * len(msg))
        need_a_release = []
        for distribution_path in self.distributions:
            dist_name = distribution_path.split('/')[-1]
            logger.debug(DISTRIBUTION.format(distribution_path))
            repo = Repo(distribution_path)
            remote = repo.remote()

            latest_tag = get_latest_tag(repo, self.branch)
            if latest_tag not in repo.tags:
                # if there is no tag it definitely needs a release
                need_a_release.append(distribution_path)
                self.last_tags[dist_name] = latest_tag
                continue

            self.last_tags[dist_name] = latest_tag
            # get the commit where the latest tag is on
            tag = repo.tags[latest_tag]
            tag_sha = tag.commit.hexsha

            branch_sha = remote.refs[self.branch].commit.hexsha
            if tag_sha != branch_sha:
                # self.branch is ahead of the last tag: needs a release
                need_a_release.append(distribution_path)

        # if nothing is about to be released, do not filter the distributions
        if not self.test:
            self.distributions = need_a_release

    def ask_what_to_release(self):
        """Show changes both in CHANGES.rst and on git history

        For that checkout the repository, show both changes to see if
        everything worth writing in CHANGES.rst from git history is already
        there.
        """
        logger.info('')
        msg = 'What to release'
        logger.info(msg)
        logger.info('-' * len(msg))
        to_release = []
        for distribution_path in self.distributions:
            dist_name = distribution_path.split('/')[-1]
            repo = Repo(distribution_path)

            git_changes = get_compact_git_history(
                repo,
                self.last_tags[dist_name],
                self.branch,
            )
            cleaned_git_changes = filter_git_history(git_changes)

            # a git history without any meaningful commit should not be
            # released
            if cleaned_git_changes == '':
                continue

            logger.info(DISTRIBUTION.format(distribution_path))

            change_log_path = '{0}/CHANGES.rst'.format(repo.working_tree_dir)
            try:
                changes = self._grab_changelog(change_log_path)
            except IOError:
                logger.debug('Changelog not found, skipping.')
                continue
            self.changelogs[dist_name] = changes[2:]

            # nice to have: show them side-by-side
            logger.info('')
            logger.info(cleaned_git_changes)
            logger.info('')
            logger.info('')
            logger.info(''.join(changes))
            msg = '{0}: write the above git history on CHANGES.rst?'
            if self.test and ask(msg.format(dist_name)):
                changelog = UpdateDistChangelog(
                    distribution_path,
                    branch=self.branch,
                )
                changelog.write_changes(history=cleaned_git_changes)
            elif not self.test and \
                    ask('Is the change log ready for release?'):
                to_release.append(distribution_path)

        if not self.test:
            self.distributions = to_release

        logger.debug('Distributions: ')
        logger.debug('\n'.join(self.distributions))

    def release_all(self):
        """Release all distributions"""
        logger.info('')
        msg = 'Release!'
        logger.info(msg)
        logger.info('-' * len(msg))
        for distribution_path in self.distributions:
            logger.info(DISTRIBUTION.format(distribution_path))
            dist_name = distribution_path.split('/')[-1]
            repo = Repo(distribution_path)

            release = ReleaseDistribution(repo.working_tree_dir, self.branch)
            new_version = release()
            self.versions[dist_name] = new_version

            self.buildout.set_version(dist_name, new_version)

            # update the local repository
            update_branch(repo, self.branch)

    def _create_commit_message(self):
        msg = [
            'New releases:',
            '',
        ]
        changelogs = [
            '',
            'Changelogs:',
            '',
        ]
        for dist in sorted(self.versions.keys()):
            tmp_msg = '{0} {1}'.format(dist, self.versions[dist])
            msg.append(tmp_msg)

            changelogs.append(dist)
            changelogs.append('-' * len(dist))
            changelogs.append(''.join(self.changelogs[dist]))
            changelogs.append('')

        # There's no need to run CI when doing releases...
        ci_skip = [
            '[ci-skip]',
        ]

        self.commit_message = '\n'.join(msg + changelogs + ci_skip)

    def update_buildout(self):
        """Commit the changes on buildout"""
        msg = 'Update buildout'
        logger.info(msg)
        logger.info('-' * len(msg))

        repo = Repo(os.path.curdir)
        repo.git.add('versions.cfg')
        repo.git.commit(message=self.commit_message)
        # push the changes
        repo.remote().push()

    def update_batou(self):
        """Update the version pins on batou as well"""
        deployment_repo = self.buildout.sources.get('deployment')
        if deployment_repo is None:
            logger.info('No deployment repository sources found!'
                        '\n'
                        'Batou can not be updated!')
            return
        # clone the repo
        with git_repo(deployment_repo, shallow=False) as repo:
            # get components/plone/versions/versions.cfg Buildout
            path = 'components/plone/versions/versions.cfg'
            plone_versions = '{0}/{1}'.format(repo.working_tree_dir, path)
            deployment_buildout = Buildout(
                sources_file=plone_versions,
                checkouts_file=plone_versions,
                versions_file=plone_versions)
            # update version pins
            for dist_name in self.versions:
                deployment_buildout.set_version(dist_name,
                                                self.versions[dist_name])
            # commit and push the repo
            repo.index.add([
                path,
            ])
            repo.index.commit(message=self.commit_message.encode('utf-8'))
            # push the changes
            repo.remote().push()

    @staticmethod
    def _grab_changelog(changelog_path):
        unreleased_regex = re.compile(r' \(unreleased\)$')
        release = re.compile(r' \(\d+-\d+-\d+\)$')
        lines = []
        with open(changelog_path) as changelog:
            on_changelog = False
            for line in changelog:
                if unreleased_regex.search(line):
                    on_changelog = True

                if release.search(line):
                    break

                if on_changelog:
                    lines.append(line)
        return lines