Merge lp:~jml/testtools/automate-lp-release into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 198
Proposed branch: lp:~jml/testtools/automate-lp-release
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 303 lines (+239/-17)
4 files modified
Makefile (+2/-1)
NEWS (+3/-0)
_lp_release.py (+228/-0)
doc/hacking.rst (+6/-16)
To merge this branch: bzr merge lp:~jml/testtools/automate-lp-release
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+66640@code.launchpad.net

Description of the change

Automate the release process. Not sure what needs explaining. Have tested fairly rigorously against staging.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2011-06-30 16:57:12 +0000
3+++ Makefile 2011-07-01 17:18:32 +0000
4@@ -1,6 +1,6 @@
5 # See README for copyright and licensing details.
6
7-PYTHON=python3
8+PYTHON=python
9 SOURCES=$(shell find testtools -name "*.py")
10
11 check:
12@@ -22,6 +22,7 @@
13
14 release:
15 ./setup.py sdist upload --sign
16+ $(PYTHON) _lp_release.py
17
18 snapshot: prerelease
19 ./setup.py sdist
20
21=== modified file 'NEWS'
22--- NEWS 2011-06-30 16:57:12 +0000
23+++ NEWS 2011-07-01 17:18:32 +0000
24@@ -10,6 +10,9 @@
25 * All public matchers are now in ``testtools.matchers.__all__``.
26 (Jonathan Lange, #784859)
27
28+* Automated the Launchpad part of the release process.
29+ (Jonathan Lange, #623486)
30+
31 * New convenience assertions, ``assertIsNone`` and ``assertIsNotNone``.
32 (Christian Kampka)
33
34
35=== added file '_lp_release.py'
36--- _lp_release.py 1970-01-01 00:00:00 +0000
37+++ _lp_release.py 2011-07-01 17:18:32 +0000
38@@ -0,0 +1,228 @@
39+#!/usr/bin/python
40+
41+"""Release testtools on Launchpad.
42+
43+Steps:
44+ 1. Make sure all "Fix committed" bugs are assigned to 'next'
45+ 2. Rename 'next' to the new version
46+ 3. Release the milestone
47+ 4. Upload the tarball
48+ 5. Create a new 'next' milestone
49+ 6. Mark all "Fix committed" bugs in the milestone as "Fix released"
50+
51+Assumes that NEWS is in the same directory, that the release sections are
52+underlined with '~' and the subsections are underlined with '-'.
53+
54+Assumes that this file is in the top-level of a testtools tree that has
55+already had a tarball built and uploaded with 'python setup.py sdist upload
56+--sign'.
57+"""
58+
59+from datetime import datetime, timedelta, tzinfo
60+import logging
61+import os
62+import sys
63+
64+from launchpadlib.launchpad import Launchpad
65+from launchpadlib import uris
66+
67+
68+APP_NAME = 'testtools-lp-release'
69+CACHE_DIR = os.path.expanduser('~/.launchpadlib/cache')
70+SERVICE_ROOT = uris.LPNET_SERVICE_ROOT
71+
72+FIX_COMMITTED = u"Fix Committed"
73+FIX_RELEASED = u"Fix Released"
74+
75+# Launchpad file type for a tarball upload.
76+CODE_RELEASE_TARBALL = 'Code Release Tarball'
77+
78+PROJECT_NAME = 'testtools'
79+NEXT_MILESTONE_NAME = 'next'
80+
81+
82+class _UTC(tzinfo):
83+ """UTC"""
84+
85+ def utcoffset(self, dt):
86+ return timedelta(0)
87+
88+ def tzname(self, dt):
89+ return "UTC"
90+
91+ def dst(self, dt):
92+ return timedelta(0)
93+
94+UTC = _UTC()
95+
96+
97+def configure_logging():
98+ level = logging.INFO
99+ log = logging.getLogger(APP_NAME)
100+ log.setLevel(level)
101+ handler = logging.StreamHandler()
102+ handler.setLevel(level)
103+ formatter = logging.Formatter("%(levelname)s: %(message)s")
104+ handler.setFormatter(formatter)
105+ log.addHandler(handler)
106+ return log
107+LOG = configure_logging()
108+
109+
110+def get_path(relpath):
111+ """Get the absolute path for something relative to this file."""
112+ return os.path.abspath(os.path.join(os.path.dirname(__file__), relpath))
113+
114+
115+def assign_fix_committed_to_next(testtools, next_milestone):
116+ """Find all 'Fix Committed' and make sure they are in 'next'."""
117+ fixed_bugs = list(testtools.searchTasks(status=FIX_COMMITTED))
118+ for task in fixed_bugs:
119+ LOG.debug("%s" % (task.title,))
120+ if task.milestone != next_milestone:
121+ task.milestone = next_milestone
122+ LOG.info("Re-assigning %s" % (task.title,))
123+ task.lp_save()
124+
125+
126+def rename_milestone(next_milestone, new_name):
127+ """Rename 'next_milestone' to 'new_name'."""
128+ LOG.info("Renaming %s to %s" % (next_milestone.name, new_name))
129+ next_milestone.name = new_name
130+ next_milestone.lp_save()
131+
132+
133+def get_release_notes_and_changelog(news_path):
134+ release_notes = []
135+ changelog = []
136+ state = None
137+ last_line = None
138+
139+ def is_heading_marker(line, marker_char):
140+ return line and line == marker_char * len(line)
141+
142+ LOG.debug("Loading NEWS from %s" % (news_path,))
143+ with open(news_path, 'r') as news:
144+ for line in news:
145+ line = line.strip()
146+ if state is None:
147+ if is_heading_marker(line, '~'):
148+ milestone_name = last_line
149+ state = 'release-notes'
150+ else:
151+ last_line = line
152+ elif state == 'title':
153+ # The line after the title is a heading marker line, so we
154+ # ignore it and change state. That which follows are the
155+ # release notes.
156+ state = 'release-notes'
157+ elif state == 'release-notes':
158+ if is_heading_marker(line, '-'):
159+ state = 'changelog'
160+ # Last line in the release notes is actually the first
161+ # line of the changelog.
162+ changelog = [release_notes.pop(), line]
163+ else:
164+ release_notes.append(line)
165+ elif state == 'changelog':
166+ if is_heading_marker(line, '~'):
167+ # Last line in changelog is actually the first line of the
168+ # next section.
169+ changelog.pop()
170+ break
171+ else:
172+ changelog.append(line)
173+ else:
174+ raise ValueError("Couldn't parse NEWS")
175+
176+ release_notes = '\n'.join(release_notes).strip() + '\n'
177+ changelog = '\n'.join(changelog).strip() + '\n'
178+ return milestone_name, release_notes, changelog
179+
180+
181+def release_milestone(milestone, release_notes, changelog):
182+ date_released = datetime.now(tz=UTC)
183+ LOG.info(
184+ "Releasing milestone: %s, date %s" % (milestone.name, date_released))
185+ release = milestone.createProductRelease(
186+ date_released=date_released,
187+ changelog=changelog,
188+ release_notes=release_notes,
189+ )
190+ milestone.is_active = False
191+ milestone.lp_save()
192+ return release
193+
194+
195+def create_milestone(series, name):
196+ """Create a new milestone in the same series as 'release_milestone'."""
197+ LOG.info("Creating milestone %s in series %s" % (name, series.name))
198+ return series.newMilestone(name=name)
199+
200+
201+def close_fixed_bugs(milestone):
202+ tasks = list(milestone.searchTasks())
203+ for task in tasks:
204+ LOG.debug("Found %s" % (task.title,))
205+ if task.status == FIX_COMMITTED:
206+ LOG.info("Closing %s" % (task.title,))
207+ task.status = FIX_RELEASED
208+ else:
209+ LOG.warning(
210+ "Bug not fixed, removing from milestone: %s" % (task.title,))
211+ task.milestone = None
212+ task.lp_save()
213+
214+
215+def upload_tarball(release, tarball_path):
216+ with open(tarball_path) as tarball:
217+ tarball_content = tarball.read()
218+ sig_path = tarball_path + '.asc'
219+ with open(sig_path) as sig:
220+ sig_content = sig.read()
221+ tarball_name = os.path.basename(tarball_path)
222+ LOG.info("Uploading tarball: %s" % (tarball_path,))
223+ release.add_file(
224+ file_type=CODE_RELEASE_TARBALL,
225+ file_content=tarball_content, filename=tarball_name,
226+ signature_content=sig_content,
227+ signature_filename=sig_path,
228+ content_type="application/x-gzip; charset=binary")
229+
230+
231+def release_project(launchpad, project_name, next_milestone_name):
232+ testtools = launchpad.projects[project_name]
233+ next_milestone = testtools.getMilestone(name=next_milestone_name)
234+ release_name, release_notes, changelog = get_release_notes_and_changelog(
235+ get_path('NEWS'))
236+ LOG.info("Releasing %s %s" % (project_name, release_name))
237+ # Since reversing these operations is hard, and inspecting errors from
238+ # Launchpad is also difficult, do some looking before leaping.
239+ errors = []
240+ tarball_path = get_path('dist/%s-%s.tar.gz' % (project_name, release_name,))
241+ if not os.path.isfile(tarball_path):
242+ errors.append("%s does not exist" % (tarball_path,))
243+ if not os.path.isfile(tarball_path + '.asc'):
244+ errors.append("%s does not exist" % (tarball_path + '.asc',))
245+ if testtools.getMilestone(name=release_name):
246+ errors.append("Milestone %s exists on %s" % (release_name, project_name))
247+ if errors:
248+ for error in errors:
249+ LOG.error(error)
250+ return 1
251+ assign_fix_committed_to_next(testtools, next_milestone)
252+ rename_milestone(next_milestone, release_name)
253+ release = release_milestone(next_milestone, release_notes, changelog)
254+ upload_tarball(release, tarball_path)
255+ create_milestone(next_milestone.series_target, next_milestone_name)
256+ close_fixed_bugs(next_milestone)
257+ return 0
258+
259+
260+def main(args):
261+ launchpad = Launchpad.login_with(APP_NAME, SERVICE_ROOT, CACHE_DIR)
262+ return release_project(launchpad, PROJECT_NAME, NEXT_MILESTONE_NAME)
263+
264+
265+if __name__ == '__main__':
266+ sys.exit(main(sys.argv))
267
268=== modified file 'doc/hacking.rst'
269--- doc/hacking.rst 2011-06-12 00:37:20 +0000
270+++ doc/hacking.rst 2011-07-01 17:18:32 +0000
271@@ -129,26 +129,16 @@
272 which should not be replaced).
273 #. Commit the changes.
274 #. Tag the release, bzr tag testtools-X.Y.Z
275-#. Create a source distribution and upload to pypi ('make release').
276-#. Make sure all "Fix committed" bugs are in the 'next' milestone on
277- Launchpad
278-#. Rename the 'next' milestone on Launchpad to 'X.Y.Z'
279-#. Create a release on the newly-renamed 'X.Y.Z' milestone
280-
281- * Make the milestone inactive (this is the default)
282- * Set the release date to the current day
283-
284-#. Upload the tarball and asc file to Launchpad
285+#. Run 'make release', this:
286+ #. Creates a source distribution and uploads to PyPI
287+ #. Ensures all Fix Committed bugs are in the release milestone
288+ #. Makes a release on Launchpad and uploads the tarball
289+ #. Marks all the Fix Committed bugs as Fix Released
290+ #. Creates a new milestone
291 #. Merge the release branch testtools-X.Y.Z into trunk. Before the commit,
292 add a NEXT heading to the top of NEWS and bump the version in __init__.py.
293 Push trunk to Launchpad
294 #. If a new series has been created (e.g. 0.10.0), make the series on Launchpad.
295-#. Make a new milestone for the *next release*.
296-
297- #. During release we rename NEXT to $version.
298- #. We call new milestones NEXT.
299-
300-#. Set all bugs that were "Fix Committed" to "Fix Released"
301
302 .. _PEP 8: http://www.python.org/dev/peps/pep-0008/
303 .. _unittest: http://docs.python.org/library/unittest.html

Subscribers

People subscribed via source and target branches