Merge lp:~cjwatson/launchpad/snap-channels-job into lp:launchpad
- snap-channels-job
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18143 |
Proposed branch: | lp:~cjwatson/launchpad/snap-channels-job |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/snap-channels-store-client |
Diff against target: |
358 lines (+196/-9) 5 files modified
lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt (+5/-0) lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt (+7/-0) lib/lp/snappy/mail/snapbuild.py (+33/-2) lib/lp/snappy/model/snapbuildjob.py (+30/-5) lib/lp/snappy/tests/test_snapbuildjob.py (+121/-2) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-channels-job |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Thomi Richards (community) | Approve | ||
Launchpad code reviewers | Pending | ||
Review via email: mp+298810@code.launchpad.net |
Commit message
Automatically release snap packages that have store_channels set.
Description of the change
Automatically release snap packages that have store_channels set.
To post a comment you must log in.
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt' |
2 | --- lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 1970-01-01 00:00:00 +0000 |
3 | +++ lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 2016-06-30 17:00:36 +0000 |
4 | @@ -0,0 +1,5 @@ |
5 | +This snap package could not be released automatically because it was held |
6 | +for manual review. Once it has been approved, you will need to release it |
7 | +manually from here: |
8 | + |
9 | + %(store_url)s |
10 | |
11 | === added file 'lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt' |
12 | --- lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 1970-01-01 00:00:00 +0000 |
13 | +++ lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 2016-06-30 17:00:36 +0000 |
14 | @@ -0,0 +1,7 @@ |
15 | +Launchpad asked the store to release this snap package, but it failed: |
16 | + |
17 | + %(store_error_message)s |
18 | + |
19 | +You can try to release it manually here: |
20 | + |
21 | + %(store_url)s |
22 | |
23 | === modified file 'lib/lp/snappy/mail/snapbuild.py' |
24 | --- lib/lp/snappy/mail/snapbuild.py 2016-06-21 14:51:06 +0000 |
25 | +++ lib/lp/snappy/mail/snapbuild.py 2016-06-30 17:00:36 +0000 |
26 | @@ -60,6 +60,34 @@ |
27 | config.canonical.noreply_from_address, |
28 | "snap-build-upload-scan-failed", build) |
29 | |
30 | + @classmethod |
31 | + def forManualReview(cls, build): |
32 | + """Create a mailer for notifying about manual review. |
33 | + |
34 | + :param build: The relevant build. |
35 | + """ |
36 | + requester = build.requester |
37 | + recipients = {requester: RecipientReason.forBuildRequester(requester)} |
38 | + return cls( |
39 | + "%(snap_name)s held for manual review", |
40 | + "snapbuild-manualreview.txt", recipients, |
41 | + config.canonical.noreply_from_address, |
42 | + "snap-build-release-manual-review", build) |
43 | + |
44 | + @classmethod |
45 | + def forReleaseFailure(cls, build): |
46 | + """Create a mailer for notifying about store release failures. |
47 | + |
48 | + :param build: The relevant build. |
49 | + """ |
50 | + requester = build.requester |
51 | + recipients = {requester: RecipientReason.forBuildRequester(requester)} |
52 | + return cls( |
53 | + "Store release failed for %(snap_name)s", |
54 | + "snapbuild-releasefailed.txt", recipients, |
55 | + config.canonical.noreply_from_address, |
56 | + "snap-build-release-failed", build) |
57 | + |
58 | def __init__(self, subject, template_name, recipients, from_address, |
59 | notification_type, build): |
60 | super(SnapBuildMailer, self).__init__( |
61 | @@ -77,10 +105,12 @@ |
62 | """See `BaseMailer`.""" |
63 | build = self.build |
64 | upload_job = build.store_upload_jobs.first() |
65 | - if upload_job is None or upload_job.error_message is None: |
66 | + if upload_job is None: |
67 | error_message = "" |
68 | + store_url = "" |
69 | else: |
70 | - error_message = upload_job.error_message |
71 | + error_message = upload_job.error_message or "" |
72 | + store_url = upload_job.store_url or "" |
73 | params = super(SnapBuildMailer, self)._getTemplateParams( |
74 | email, recipient) |
75 | params.update({ |
76 | @@ -100,6 +130,7 @@ |
77 | "snap_authorize_url": canonical_url( |
78 | build.snap, view_name="+authorize"), |
79 | "store_error_message": error_message, |
80 | + "store_url": store_url, |
81 | }) |
82 | if build.duration is not None: |
83 | duration_formatter = DurationFormatterAPI(build.duration) |
84 | |
85 | === modified file 'lib/lp/snappy/model/snapbuildjob.py' |
86 | --- lib/lp/snappy/model/snapbuildjob.py 2016-06-21 14:51:06 +0000 |
87 | +++ lib/lp/snappy/model/snapbuildjob.py 2016-06-30 17:00:36 +0000 |
88 | @@ -50,8 +50,10 @@ |
89 | ISnapStoreUploadJobSource, |
90 | ) |
91 | from lp.snappy.interfaces.snapstoreclient import ( |
92 | + BadReleaseResponse, |
93 | BadScanStatusResponse, |
94 | ISnapStoreClient, |
95 | + ReleaseFailedResponse, |
96 | ScanFailedResponse, |
97 | UnauthorizedUploadResponse, |
98 | UploadNotScannedYetResponse, |
99 | @@ -157,6 +159,10 @@ |
100 | return oops_vars |
101 | |
102 | |
103 | +class ManualReview(Exception): |
104 | + pass |
105 | + |
106 | + |
107 | @implementer(ISnapStoreUploadJob) |
108 | @provider(ISnapStoreUploadJobSource) |
109 | class SnapStoreUploadJob(SnapBuildJobDerived): |
110 | @@ -164,7 +170,12 @@ |
111 | |
112 | class_job_type = SnapBuildJobType.STORE_UPLOAD |
113 | |
114 | - user_error_types = (UnauthorizedUploadResponse, ScanFailedResponse) |
115 | + user_error_types = ( |
116 | + UnauthorizedUploadResponse, |
117 | + ScanFailedResponse, |
118 | + ManualReview, |
119 | + ReleaseFailedResponse, |
120 | + ) |
121 | |
122 | # XXX cjwatson 2016-05-04: identify transient upload failures and retry |
123 | retry_error_types = (UploadNotScannedYetResponse,) |
124 | @@ -207,14 +218,19 @@ |
125 | try: |
126 | if "status_url" not in self.metadata: |
127 | self.metadata["status_url"] = client.upload(self.snapbuild) |
128 | - self.store_url = client.checkStatus(self.metadata["status_url"]) |
129 | + if self.store_url is None: |
130 | + self.store_url, self.metadata["store_revision"] = ( |
131 | + client.checkStatus(self.metadata["status_url"])) |
132 | + if self.snapbuild.snap.store_channels: |
133 | + if self.metadata["store_revision"] is None: |
134 | + raise ManualReview( |
135 | + "Package held for manual review on the store; " |
136 | + "cannot release it automatically.") |
137 | + client.release(self.snapbuild, self.metadata["store_revision"]) |
138 | self.error_message = None |
139 | except self.retry_error_types: |
140 | raise |
141 | except Exception as e: |
142 | - # Abort work done so far, but make sure that we commit the error |
143 | - # message. |
144 | - transaction.abort() |
145 | self.error_message = str(e) |
146 | if isinstance(e, UnauthorizedUploadResponse): |
147 | mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild) |
148 | @@ -222,5 +238,14 @@ |
149 | elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)): |
150 | mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild) |
151 | mailer.sendAll() |
152 | + elif isinstance(e, ManualReview): |
153 | + mailer = SnapBuildMailer.forManualReview(self.snapbuild) |
154 | + mailer.sendAll() |
155 | + elif isinstance(e, (BadReleaseResponse, ReleaseFailedResponse)): |
156 | + mailer = SnapBuildMailer.forReleaseFailure(self.snapbuild) |
157 | + mailer.sendAll() |
158 | + # The normal job infrastructure will abort the transaction, but |
159 | + # we want to commit instead: the only database changes we make |
160 | + # are to this job's metadata and should be preserved. |
161 | transaction.commit() |
162 | raise |
163 | |
164 | === modified file 'lib/lp/snappy/tests/test_snapbuildjob.py' |
165 | --- lib/lp/snappy/tests/test_snapbuildjob.py 2016-06-21 14:51:06 +0000 |
166 | +++ lib/lp/snappy/tests/test_snapbuildjob.py 2016-06-30 17:00:36 +0000 |
167 | @@ -20,6 +20,7 @@ |
168 | ISnapStoreUploadJob, |
169 | ) |
170 | from lp.snappy.interfaces.snapstoreclient import ( |
171 | + BadReleaseResponse, |
172 | BadScanStatusResponse, |
173 | ISnapStoreClient, |
174 | UnauthorizedUploadResponse, |
175 | @@ -47,6 +48,7 @@ |
176 | def __init__(self): |
177 | self.upload = FakeMethod() |
178 | self.checkStatus = FakeMethod() |
179 | + self.release = FakeMethod() |
180 | |
181 | |
182 | class TestSnapBuildJob(TestCaseWithFactory): |
183 | @@ -95,12 +97,13 @@ |
184 | job = SnapStoreUploadJob.create(snapbuild) |
185 | client = FakeSnapStoreClient() |
186 | client.upload.result = self.status_url |
187 | - client.checkStatus.result = self.store_url |
188 | + client.checkStatus.result = (self.store_url, 1) |
189 | self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
190 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
191 | JobRunner([job]).runAll() |
192 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
193 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
194 | + self.assertEqual([], client.release.calls) |
195 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
196 | self.assertEqual(self.store_url, job.store_url) |
197 | self.assertIsNone(job.error_message) |
198 | @@ -118,6 +121,7 @@ |
199 | JobRunner([job]).runAll() |
200 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
201 | self.assertEqual([], client.checkStatus.calls) |
202 | + self.assertEqual([], client.release.calls) |
203 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
204 | self.assertIsNone(job.store_url) |
205 | self.assertEqual("An upload failure", job.error_message) |
206 | @@ -138,6 +142,7 @@ |
207 | JobRunner([job]).runAll() |
208 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
209 | self.assertEqual([], client.checkStatus.calls) |
210 | + self.assertEqual([], client.release.calls) |
211 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
212 | self.assertIsNone(job.store_url) |
213 | self.assertEqual("Authorization failed.", job.error_message) |
214 | @@ -178,6 +183,7 @@ |
215 | JobRunner([job]).runAll() |
216 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
217 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
218 | + self.assertEqual([], client.release.calls) |
219 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
220 | self.assertIsNone(job.store_url) |
221 | self.assertIsNone(job.error_message) |
222 | @@ -190,11 +196,12 @@ |
223 | client.upload.calls = [] |
224 | client.checkStatus.calls = [] |
225 | client.checkStatus.failure = None |
226 | - client.checkStatus.result = self.store_url |
227 | + client.checkStatus.result = (self.store_url, 1) |
228 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
229 | JobRunner([job]).runAll() |
230 | self.assertEqual([], client.upload.calls) |
231 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
232 | + self.assertEqual([], client.release.calls) |
233 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
234 | self.assertEqual(self.store_url, job.store_url) |
235 | self.assertIsNone(job.error_message) |
236 | @@ -216,6 +223,7 @@ |
237 | JobRunner([job]).runAll() |
238 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
239 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
240 | + self.assertEqual([], client.release.calls) |
241 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
242 | self.assertIsNone(job.store_url) |
243 | self.assertEqual("Scan failed.", job.error_message) |
244 | @@ -239,3 +247,114 @@ |
245 | self.assertEqual( |
246 | "http://launchpad.dev/~requester/+snap/test-snap/+build/%d\n" |
247 | "You are the requester of the build.\n" % snapbuild.id, footer) |
248 | + |
249 | + def test_run_release(self): |
250 | + # A run configured to automatically release the package to certain |
251 | + # channels does so. |
252 | + snapbuild = self.factory.makeSnapBuild( |
253 | + store_channels=["stable", "edge"]) |
254 | + self.assertContentEqual([], snapbuild.store_upload_jobs) |
255 | + job = SnapStoreUploadJob.create(snapbuild) |
256 | + client = FakeSnapStoreClient() |
257 | + client.upload.result = self.status_url |
258 | + client.checkStatus.result = (self.store_url, 1) |
259 | + self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
260 | + with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
261 | + JobRunner([job]).runAll() |
262 | + self.assertEqual([((snapbuild,), {})], client.upload.calls) |
263 | + self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
264 | + self.assertEqual([((snapbuild, 1), {})], client.release.calls) |
265 | + self.assertContentEqual([job], snapbuild.store_upload_jobs) |
266 | + self.assertEqual(self.store_url, job.store_url) |
267 | + self.assertIsNone(job.error_message) |
268 | + self.assertEqual([], pop_notifications()) |
269 | + |
270 | + def test_run_release_manual_review_notifies(self): |
271 | + # A run configured to automatically release the package to certain |
272 | + # channels but that encounters the manual review state on upload |
273 | + # sends mail. |
274 | + requester = self.factory.makePerson(name="requester") |
275 | + snapbuild = self.factory.makeSnapBuild( |
276 | + requester=requester, name="test-snap", owner=requester, |
277 | + store_channels=["stable", "edge"]) |
278 | + self.assertContentEqual([], snapbuild.store_upload_jobs) |
279 | + job = SnapStoreUploadJob.create(snapbuild) |
280 | + client = FakeSnapStoreClient() |
281 | + client.upload.result = self.status_url |
282 | + client.checkStatus.result = (self.store_url, None) |
283 | + self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
284 | + with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
285 | + JobRunner([job]).runAll() |
286 | + self.assertEqual([((snapbuild,), {})], client.upload.calls) |
287 | + self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
288 | + self.assertEqual([], client.release.calls) |
289 | + self.assertContentEqual([job], snapbuild.store_upload_jobs) |
290 | + self.assertEqual(self.store_url, job.store_url) |
291 | + self.assertEqual( |
292 | + "Package held for manual review on the store; " |
293 | + "cannot release it automatically.", |
294 | + job.error_message) |
295 | + [notification] = pop_notifications() |
296 | + self.assertEqual( |
297 | + config.canonical.noreply_from_address, notification["From"]) |
298 | + self.assertEqual( |
299 | + "Requester <%s>" % requester.preferredemail.email, |
300 | + notification["To"]) |
301 | + subject = notification["Subject"].replace("\n ", " ") |
302 | + self.assertEqual("test-snap held for manual review", subject) |
303 | + self.assertEqual( |
304 | + "Requester", notification["X-Launchpad-Message-Rationale"]) |
305 | + self.assertEqual( |
306 | + requester.name, notification["X-Launchpad-Message-For"]) |
307 | + self.assertEqual( |
308 | + "snap-build-release-manual-review", |
309 | + notification["X-Launchpad-Notification-Type"]) |
310 | + body, footer = notification.get_payload(decode=True).split("\n-- \n") |
311 | + self.assertIn(self.store_url, body) |
312 | + self.assertEqual( |
313 | + "http://launchpad.dev/~requester/+snap/test-snap/+build/%d\n" |
314 | + "You are the requester of the build.\n" % snapbuild.id, footer) |
315 | + |
316 | + def test_run_release_failure_notifies(self): |
317 | + # A run configured to automatically release the package to certain |
318 | + # channels but that fails to do so sends mail. |
319 | + requester = self.factory.makePerson(name="requester") |
320 | + snapbuild = self.factory.makeSnapBuild( |
321 | + requester=requester, name="test-snap", owner=requester, |
322 | + store_channels=["stable", "edge"]) |
323 | + self.assertContentEqual([], snapbuild.store_upload_jobs) |
324 | + job = SnapStoreUploadJob.create(snapbuild) |
325 | + client = FakeSnapStoreClient() |
326 | + client.upload.result = self.status_url |
327 | + client.checkStatus.result = (self.store_url, 1) |
328 | + client.release.failure = BadReleaseResponse("Failed to publish") |
329 | + self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
330 | + with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
331 | + JobRunner([job]).runAll() |
332 | + self.assertEqual([((snapbuild,), {})], client.upload.calls) |
333 | + self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
334 | + self.assertEqual([((snapbuild, 1), {})], client.release.calls) |
335 | + self.assertContentEqual([job], snapbuild.store_upload_jobs) |
336 | + self.assertEqual(self.store_url, job.store_url) |
337 | + self.assertEqual("Failed to publish", job.error_message) |
338 | + [notification] = pop_notifications() |
339 | + self.assertEqual( |
340 | + config.canonical.noreply_from_address, notification["From"]) |
341 | + self.assertEqual( |
342 | + "Requester <%s>" % requester.preferredemail.email, |
343 | + notification["To"]) |
344 | + subject = notification["Subject"].replace("\n ", " ") |
345 | + self.assertEqual("Store release failed for test-snap", subject) |
346 | + self.assertEqual( |
347 | + "Requester", notification["X-Launchpad-Message-Rationale"]) |
348 | + self.assertEqual( |
349 | + requester.name, notification["X-Launchpad-Message-For"]) |
350 | + self.assertEqual( |
351 | + "snap-build-release-failed", |
352 | + notification["X-Launchpad-Notification-Type"]) |
353 | + body, footer = notification.get_payload(decode=True).split("\n-- \n") |
354 | + self.assertIn("Failed to publish", body) |
355 | + self.assertIn(self.store_url, body) |
356 | + self.assertEqual( |
357 | + "http://launchpad.dev/~requester/+snap/test-snap/+build/%d\n" |
358 | + "You are the requester of the build.\n" % snapbuild.id, footer) |
+1