s3:torture: call fault_setup() to get usage backtraces
[Samba/gebeck_regimport.git] / lib / testtools / scripts / _lp_release.py
blob20afd0199ed732606e2e56e24e9afbb6bbf81b93
1 #!/usr/bin/python
3 """Release testtools on Launchpad.
5 Steps:
6 1. Make sure all "Fix committed" bugs are assigned to 'next'
7 2. Rename 'next' to the new version
8 3. Release the milestone
9 4. Upload the tarball
10 5. Create a new 'next' milestone
11 6. Mark all "Fix committed" bugs in the milestone as "Fix released"
13 Assumes that NEWS is in the parent directory, that the release sections are
14 underlined with '~' and the subsections are underlined with '-'.
16 Assumes that this file is in the 'scripts' directory a testtools tree that has
17 already had a tarball built and uploaded with 'python setup.py sdist upload
18 --sign'.
19 """
21 from datetime import datetime, timedelta, tzinfo
22 import logging
23 import os
24 import sys
26 from launchpadlib.launchpad import Launchpad
27 from launchpadlib import uris
30 APP_NAME = 'testtools-lp-release'
31 CACHE_DIR = os.path.expanduser('~/.launchpadlib/cache')
32 SERVICE_ROOT = uris.LPNET_SERVICE_ROOT
34 FIX_COMMITTED = u"Fix Committed"
35 FIX_RELEASED = u"Fix Released"
37 # Launchpad file type for a tarball upload.
38 CODE_RELEASE_TARBALL = 'Code Release Tarball'
40 PROJECT_NAME = 'testtools'
41 NEXT_MILESTONE_NAME = 'next'
44 class _UTC(tzinfo):
45 """UTC"""
47 def utcoffset(self, dt):
48 return timedelta(0)
50 def tzname(self, dt):
51 return "UTC"
53 def dst(self, dt):
54 return timedelta(0)
56 UTC = _UTC()
59 def configure_logging():
60 level = logging.INFO
61 log = logging.getLogger(APP_NAME)
62 log.setLevel(level)
63 handler = logging.StreamHandler()
64 handler.setLevel(level)
65 formatter = logging.Formatter("%(levelname)s: %(message)s")
66 handler.setFormatter(formatter)
67 log.addHandler(handler)
68 return log
69 LOG = configure_logging()
72 def get_path(relpath):
73 """Get the absolute path for something relative to this file."""
74 return os.path.abspath(
75 os.path.join(
76 os.path.dirname(os.path.dirname(__file__)), relpath))
79 def assign_fix_committed_to_next(testtools, next_milestone):
80 """Find all 'Fix Committed' and make sure they are in 'next'."""
81 fixed_bugs = list(testtools.searchTasks(status=FIX_COMMITTED))
82 for task in fixed_bugs:
83 LOG.debug("%s" % (task.title,))
84 if task.milestone != next_milestone:
85 task.milestone = next_milestone
86 LOG.info("Re-assigning %s" % (task.title,))
87 task.lp_save()
90 def rename_milestone(next_milestone, new_name):
91 """Rename 'next_milestone' to 'new_name'."""
92 LOG.info("Renaming %s to %s" % (next_milestone.name, new_name))
93 next_milestone.name = new_name
94 next_milestone.lp_save()
97 def get_release_notes_and_changelog(news_path):
98 release_notes = []
99 changelog = []
100 state = None
101 last_line = None
103 def is_heading_marker(line, marker_char):
104 return line and line == marker_char * len(line)
106 LOG.debug("Loading NEWS from %s" % (news_path,))
107 with open(news_path, 'r') as news:
108 for line in news:
109 line = line.strip()
110 if state is None:
111 if is_heading_marker(line, '~'):
112 milestone_name = last_line
113 state = 'release-notes'
114 else:
115 last_line = line
116 elif state == 'title':
117 # The line after the title is a heading marker line, so we
118 # ignore it and change state. That which follows are the
119 # release notes.
120 state = 'release-notes'
121 elif state == 'release-notes':
122 if is_heading_marker(line, '-'):
123 state = 'changelog'
124 # Last line in the release notes is actually the first
125 # line of the changelog.
126 changelog = [release_notes.pop(), line]
127 else:
128 release_notes.append(line)
129 elif state == 'changelog':
130 if is_heading_marker(line, '~'):
131 # Last line in changelog is actually the first line of the
132 # next section.
133 changelog.pop()
134 break
135 else:
136 changelog.append(line)
137 else:
138 raise ValueError("Couldn't parse NEWS")
140 release_notes = '\n'.join(release_notes).strip() + '\n'
141 changelog = '\n'.join(changelog).strip() + '\n'
142 return milestone_name, release_notes, changelog
145 def release_milestone(milestone, release_notes, changelog):
146 date_released = datetime.now(tz=UTC)
147 LOG.info(
148 "Releasing milestone: %s, date %s" % (milestone.name, date_released))
149 release = milestone.createProductRelease(
150 date_released=date_released,
151 changelog=changelog,
152 release_notes=release_notes,
154 milestone.is_active = False
155 milestone.lp_save()
156 return release
159 def create_milestone(series, name):
160 """Create a new milestone in the same series as 'release_milestone'."""
161 LOG.info("Creating milestone %s in series %s" % (name, series.name))
162 return series.newMilestone(name=name)
165 def close_fixed_bugs(milestone):
166 tasks = list(milestone.searchTasks())
167 for task in tasks:
168 LOG.debug("Found %s" % (task.title,))
169 if task.status == FIX_COMMITTED:
170 LOG.info("Closing %s" % (task.title,))
171 task.status = FIX_RELEASED
172 else:
173 LOG.warning(
174 "Bug not fixed, removing from milestone: %s" % (task.title,))
175 task.milestone = None
176 task.lp_save()
179 def upload_tarball(release, tarball_path):
180 with open(tarball_path) as tarball:
181 tarball_content = tarball.read()
182 sig_path = tarball_path + '.asc'
183 with open(sig_path) as sig:
184 sig_content = sig.read()
185 tarball_name = os.path.basename(tarball_path)
186 LOG.info("Uploading tarball: %s" % (tarball_path,))
187 release.add_file(
188 file_type=CODE_RELEASE_TARBALL,
189 file_content=tarball_content, filename=tarball_name,
190 signature_content=sig_content,
191 signature_filename=sig_path,
192 content_type="application/x-gzip; charset=binary")
195 def release_project(launchpad, project_name, next_milestone_name):
196 testtools = launchpad.projects[project_name]
197 next_milestone = testtools.getMilestone(name=next_milestone_name)
198 release_name, release_notes, changelog = get_release_notes_and_changelog(
199 get_path('NEWS'))
200 LOG.info("Releasing %s %s" % (project_name, release_name))
201 # Since reversing these operations is hard, and inspecting errors from
202 # Launchpad is also difficult, do some looking before leaping.
203 errors = []
204 tarball_path = get_path('dist/%s-%s.tar.gz' % (project_name, release_name,))
205 if not os.path.isfile(tarball_path):
206 errors.append("%s does not exist" % (tarball_path,))
207 if not os.path.isfile(tarball_path + '.asc'):
208 errors.append("%s does not exist" % (tarball_path + '.asc',))
209 if testtools.getMilestone(name=release_name):
210 errors.append("Milestone %s exists on %s" % (release_name, project_name))
211 if errors:
212 for error in errors:
213 LOG.error(error)
214 return 1
215 assign_fix_committed_to_next(testtools, next_milestone)
216 rename_milestone(next_milestone, release_name)
217 release = release_milestone(next_milestone, release_notes, changelog)
218 upload_tarball(release, tarball_path)
219 create_milestone(next_milestone.series_target, next_milestone_name)
220 close_fixed_bugs(next_milestone)
221 return 0
224 def main(args):
225 launchpad = Launchpad.login_with(APP_NAME, SERVICE_ROOT, CACHE_DIR)
226 return release_project(launchpad, PROJECT_NAME, NEXT_MILESTONE_NAME)
229 if __name__ == '__main__':
230 sys.exit(main(sys.argv))