123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- #!/usr/bin/python3
- """
- Put into Public Domain, by Nicolas Sebrecht.
- Make a new release.
- """
- # TODO: announce: cc list on announce includes all testers
- # TODO: announce: remove empty sections
- # TODO: websitedoc up
- # TODO: website branch not including all changes!
- from os import system, path, rename
- from datetime import datetime
- from subprocess import check_call
- import shlex
- import time
- from email import utils
- from helpers import (
- MAILING_LIST, CACHEDIR, EDITOR, Git, OfflineimapInfo, Testers, User, run, goTo
- )
- __VERSION__ = "0.2"
- SPHINXBUILD = 'sphinx-build'
- DOCSDIR = 'docs'
- CHANGELOG_MAGIC = '{:toc}'
- WEBSITE_LATEST = "website/_data/latest.yml"
- CHANGELOG_EXCERPT = "{}/changelog.excerpt.md".format(CACHEDIR)
- CHANGELOG_EXCERPT_OLD = "{}.old".format(CHANGELOG_EXCERPT)
- CHANGELOG = "Changelog.md"
- ANNOUNCE_FILE = "{}/announce.txt".format(CACHEDIR)
- WEBSITE_LATEST_SKEL = """# DO NOT EDIT MANUALLY: it is generated by the release script.
- stable: v{stable}
- """
- CHANGELOG_SKEL = """
- ### OfflineIMAP v{version} ({date})
- #### Notes
- This release was tested by:
- {testersList}
- #### Authors
- {authorsList}
- #### Features
- #### Fixes
- #### Changes
- {commitsList}
- """
- END_MESSAGE = """
- Release is ready!
- Make your checks and push the changes for both offlineimap and the website.
- Announce template stands in '{announce}'.
- Command samples to do manually:
- - git push <remote> master next {new_version}
- - python setup.py sdist && twine upload dist/* && rm -rf dist MANIFEST
- - cd website
- - git checkout master
- - git merge {website_branch}
- - git push <remote> master
- - cd ..
- - git send-email {announce}
- ...and write a Twitter message.
- Have fun! ,-)
- """
- class State():
- def __init__(self):
- self.master = None
- self.next = None
- self.website = None
- self.tag = None
- def setTag(self, tag):
- self.tag = tag
- def save(self):
- self.master = Git.getRef('master')
- self.next = Git.getRef('next')
- def saveWebsite(self):
- Git.chdirToRepositoryTopLevel()
- goTo('website')
- self.website = Git.getRef('master')
- goTo('..')
- def restore(self):
- Git.chdirToRepositoryTopLevel()
- try:
- Git.checkout('-f')
- except:
- pass
- # Git.checkout('master')
- # Git.resetKeep(self.master)
- # Git.checkout('next')
- # Git.resetKeep(self.next)
- if self.tag is not None:
- Git.rmTag(self.tag)
- if self.website is not None:
- if goTo('website'):
- Git.checkout(self.website)
- goTo('..')
- class Changelog():
- def __init__(self):
- self.shouldUsePrevious = False
- def edit(self):
- return system("{} {}".format(EDITOR, CHANGELOG_EXCERPT))
- def update(self):
- # Insert excerpt to CHANGELOG.
- system("sed -i -e '/{}/ r {}' '{}'".format(
- CHANGELOG_MAGIC, CHANGELOG_EXCERPT, CHANGELOG
- )
- )
- # Remove trailing whitespaces.
- system("sed -i -r -e 's, +$,,' '{}'".format(CHANGELOG))
- def savePrevious(self):
- rename(CHANGELOG_EXCERPT, CHANGELOG_EXCERPT_OLD)
- def isPrevious(self):
- if path.isfile(CHANGELOG_EXCERPT_OLD):
- return True
- return False
- def showPrevious(self):
- output = run(shlex.split("cat '{}'".format(CHANGELOG_EXCERPT_OLD)))
- for line in output.splitlines():
- print((line.decode('utf-8'))) # Weird to have to decode bytes here.
- def usePrevious(self):
- rename(CHANGELOG_EXCERPT_OLD, CHANGELOG_EXCERPT)
- self.shouldUsePrevious = True
- def usingPrevious(self):
- return self.shouldUsePrevious
- def writeExcerpt(self, version, date,
- testersList, authorsList, commitsList):
- with open(CHANGELOG_EXCERPT, 'w+') as fd:
- fd.write(CHANGELOG_SKEL.format(
- version=version,
- date=date,
- testersList=testersList,
- authorsList=authorsList,
- commitsList=commitsList,
- ))
- def getSectionsContent(self):
- dict_Content = {}
- with open(CHANGELOG_EXCERPT, 'r') as fd:
- currentSection = None
- for line in fd:
- line = line.rstrip()
- if line == "#### Notes":
- currentSection = 'Notes'
- dict_Content['Notes'] = ""
- continue # Don't keep this title.
- elif line == "#### Authors":
- currentSection = 'Authors'
- dict_Content['Authors'] = ""
- continue # Don't keep this title.
- elif line == "#### Features":
- currentSection = 'Features'
- dict_Content['Features'] = ""
- continue # Don't keep this title.
- elif line == "#### Fixes":
- currentSection = 'Fixes'
- dict_Content['Fixes'] = ""
- continue # Don't keep this title.
- elif line == "#### Changes":
- currentSection = 'Changes'
- dict_Content['Changes'] = ""
- continue # Don't keep this title.
- elif line == "-- ":
- break # Stop extraction.
- if currentSection is not None:
- dict_Content[currentSection] += "{}\n".format(line)
- # TODO: cleanup empty sections.
- return dict_Content
- class Announce():
- def __init__(self, version):
- self.fd = open(ANNOUNCE_FILE, 'w')
- self.version = version
- def setHeaders(self, messageId, date):
- self.fd.write("Message-Id: {}\n".format(messageId))
- self.fd.write("Date: {}\n".format(date))
- self.fd.write("From: Nicolas Sebrecht <nicolas.s-dev@laposte.net>\n")
- self.fd.write("To: {}\n".format(MAILING_LIST))
- self.fd.write(
- "Subject: [ANNOUNCE] OfflineIMAP v{} released\n".format(self.version))
- self.fd.write("\n")
- self.fd.write("""
- OfflineIMAP v{version} is out.
- Downloads:
- http://github.com/OfflineIMAP/offlineimap/archive/v{version}.tar.gz
- http://github.com/OfflineIMAP/offlineimap/archive/v{version}.zip
- Pip:
- wget "https://raw.githubusercontent.com/OfflineIMAP/offlineimap/v{version}/requirements.txt" -O requirements.txt
- pip install -r ./requirements.txt --user git+https://github.com/OfflineIMAP/offlineimap.git@v{version}
- """.format(version=self.version)
- )
- def setContent(self, dict_Content):
- self.fd.write("\n")
- for section in ['Notes', 'Authors', 'Features', 'Fixes', 'Changes']:
- if section in dict_Content:
- if section != "Notes":
- self.fd.write("# {}\n".format(section))
- self.fd.write(dict_Content[section])
- self.fd.write("\n")
- # Signature.
- self.fd.write("-- \n")
- self.fd.write("Nicolas Sebrecht\n")
- def close(self):
- self.fd.close()
- class Website():
- def updateUploads(self):
- req = ("add new archive to uploads/ on the website? "
- "(warning: checksums will change if it already exists)")
- if User.yesNo(req, defaultToYes=True) is False:
- return False
- if check_call(shlex.split("./docs/build-uploads.sh")) != 0:
- return exit(5)
- return True
- def updateAPI(self):
- req = "update API of the website? (requires {})".format(SPHINXBUILD)
- if User.yesNo(req, defaultToYes=True) is False:
- return False
- try:
- if check_call(shlex.split("{} --version".format(SPHINXBUILD))) != 0:
- raise RuntimeError("{} not found".format(SPHINXBUILD))
- except:
- print(("""
- Oops! you don't have {} installed?"
- Cannot update the webite documentation..."
- You should install it and manually run:"
- $ cd {}"
- $ make websitedoc"
- Then, commit and push changes of the website.""".format(SPHINXBUILD, DOCSDIR)))
- User.pause()
- return False
- Git.chdirToRepositoryTopLevel()
- if not goTo('website'):
- User.pause()
- return False
- if not Git.isClean:
- print("There is WIP in the website repository: stashing")
- Git.stash('WIP during offlineimap API import')
- goTo('..')
- return True
- def buildLatest(self, version):
- Git.chdirToRepositoryTopLevel()
- with open(WEBSITE_LATEST, 'w') as fd:
- fd.write(WEBSITE_LATEST_SKEL.format(stable=version))
- def exportDocs(self):
- if not goTo(DOCSDIR):
- User.pause()
- return
- if check_call(shlex.split("make websitedoc")) != 0:
- print("error while calling 'make websitedoc'")
- exit(3)
- def createImportBranch(self, version):
- branchName = "import-v{}".format(version)
- Git.chdirToRepositoryTopLevel()
- if not goTo("website"):
- User.pause()
- return
- Git.checkout(branchName, create=True)
- Git.add('.')
- Git.commit("update for offlineimap v{}".format(version))
- User.pause(
- "website: branch '{}' is ready for a merge in master!".format(
- branchName
- )
- )
- goTo('..')
- return branchName
- class Release():
- def __init__(self):
- self.state = State()
- self.offlineimapInfo = OfflineimapInfo()
- self.testers = Testers()
- self.changelog = Changelog()
- self.websiteBranch = "NO_BRANCH_NAME_ERROR"
- def getVersion(self):
- return self.offlineimapInfo.getVersion()
- def prepare(self):
- if not Git.isClean():
- print("The git repository is not clean; aborting")
- exit(1)
- Git.makeCacheDir()
- Git.checkout('next')
- def requestVersion(self, currentVersion):
- User.request("going to make a new release after {}".format(currentVersion))
- def updateVersion(self):
- self.offlineimapInfo.editInit()
- def checkVersions(self, current, new):
- if new == current:
- print("version was not changed; stopping.")
- exit(1)
- def updateChangelog(self):
- if self.changelog.isPrevious():
- self.changelog.showPrevious()
- if User.yesNo("A previous Changelog excerpt was found. Use it?"):
- self.changelog.usePrevious()
- if not self.changelog.usingPrevious():
- date = datetime.now().strftime('%Y-%m-%d')
- testersList = ""
- testers = self.testers.getListOk()
- authorsList = ""
- authors = Git.getAuthorsList(currentVersion)
- for tester in testers:
- testersList += "- {}\n".format(tester.getName())
- for author in authors:
- authorsList += "- {} ({})\n".format(
- author.getName(), author.getCount()
- )
- commitsList = Git.getCommitsList(currentVersion)
- date = datetime.now().strftime('%Y-%m-%d')
- self.changelog.writeExcerpt(
- newVersion, date, testersList, authorsList, commitsList
- )
- self.changelog.edit()
- self.changelog.update()
- def writeAnnounce(self):
- announce = Announce(newVersion)
- messageId = utils.make_msgid('release.py', 'laposte.net')
- nowtuple = datetime.now().timetuple()
- nowtimestamp = time.mktime(nowtuple)
- date = utils.formatdate(nowtimestamp)
- announce.setHeaders(messageId, date)
- announce.setContent(self.changelog.getSectionsContent())
- announce.close()
- def make(self):
- Git.add('offlineimap/__init__.py')
- Git.add('Changelog.md')
- commitMsg = "v{}\n".format(newVersion)
- for tester in self.testers.getListOk():
- commitMsg = "{}\nTested-by: {} {}".format(
- commitMsg, tester.getName(), tester.getEmail()
- )
- Git.commit(commitMsg)
- self.state.setTag(newVersion)
- Git.tag(newVersion)
- Git.checkout('master')
- Git.mergeFF('next')
- Git.checkout('next')
- def updateWebsite(self, newVersion):
- self.state.saveWebsite()
- website = Website()
- website.buildLatest(newVersion)
- res_upload = website.updateUploads()
- res_api = website.updateAPI()
- if res_api:
- res_export = website.exportDocs()
- if True in [res_upload, res_api, res_export]:
- self.websiteBranch = website.createImportBranch(newVersion)
- def getWebsiteBranch(self):
- return self.websiteBranch
- def after(self):
- for protectedRun in [self.testers.reset, self.changelog.savePrevious]:
- try:
- protectedRun()
- except Exception as e:
- print(e)
- def restore(self):
- self.state.restore()
- if __name__ == '__main__':
- release = Release()
- Git.chdirToRepositoryTopLevel()
- try:
- release.prepare()
- currentVersion = release.getVersion()
- release.requestVersion(currentVersion)
- release.updateVersion()
- newVersion = release.getVersion()
- release.checkVersions(currentVersion, newVersion)
- release.updateChangelog()
- release.writeAnnounce()
- User.pause()
- release.make()
- release.updateWebsite(newVersion)
- release.after()
- websiteBranch = release.getWebsiteBranch()
- print((END_MESSAGE.format(
- announce=ANNOUNCE_FILE,
- new_version=newVersion,
- website_branch=websiteBranch)
- ))
- except Exception as e:
- release.restore()
- raise
|