Merge lp:~twom/launchpad/snap-release-intents into lp:launchpad
- snap-release-intents
- Merge into devel
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Merged at revision: | 18988 |
Proposed branch: | lp:~twom/launchpad/snap-release-intents |
Merge into: | lp:launchpad |
Diff against target: |
679 lines (+52/-351) 9 files modified
lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt (+0/-5) lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt (+0/-7) lib/lp/snappy/interfaces/snapbuild.py (+3/-0) lib/lp/snappy/interfaces/snapstoreclient.py (+0/-15) lib/lp/snappy/mail/snapbuild.py (+0/-28) lib/lp/snappy/model/snapbuildjob.py (+4/-21) lib/lp/snappy/model/snapstoreclient.py (+8/-41) lib/lp/snappy/tests/test_snapbuildjob.py (+2/-122) lib/lp/snappy/tests/test_snapstoreclient.py (+35/-112) |
To merge this branch: | bzr merge lp:~twom/launchpad/snap-release-intents |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+369028@code.launchpad.net |
Commit message
Use release intents to release snapbuilds to channels after building
Description of the change
Use the SCA support for 'release_intents' and pass the required parameters to produce a release to a channel after review stage.
Pass `channels` and `only_if_newer` to ensure that the build is only released if it is newer that the current existing build in the specified channels.
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === removed file 'lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt' |
2 | --- lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 2016-06-30 16:44:14 +0000 |
3 | +++ lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 1970-01-01 00:00:00 +0000 |
4 | @@ -1,5 +0,0 @@ |
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 | === removed file 'lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt' |
12 | --- lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 2016-06-30 16:44:14 +0000 |
13 | +++ lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 1970-01-01 00:00:00 +0000 |
14 | @@ -1,7 +0,0 @@ |
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/interfaces/snapbuild.py' |
24 | --- lib/lp/snappy/interfaces/snapbuild.py 2019-05-22 18:33:10 +0000 |
25 | +++ lib/lp/snappy/interfaces/snapbuild.py 2019-06-19 16:07:26 +0000 |
26 | @@ -111,6 +111,9 @@ |
27 | The last attempt to upload this snap build to the store failed. |
28 | """) |
29 | |
30 | + # This is an impossible state for new releases (2019-06-19), due |
31 | + # to the store handling releases for us, however historical tasks |
32 | + # can have this status, so it is maintained here. |
33 | FAILEDTORELEASE = Item(""" |
34 | Failed to release to channels |
35 | |
36 | |
37 | === modified file 'lib/lp/snappy/interfaces/snapstoreclient.py' |
38 | --- lib/lp/snappy/interfaces/snapstoreclient.py 2018-05-02 23:55:51 +0000 |
39 | +++ lib/lp/snappy/interfaces/snapstoreclient.py 2019-06-19 16:07:26 +0000 |
40 | @@ -13,7 +13,6 @@ |
41 | 'BadSearchResponse', |
42 | 'ISnapStoreClient', |
43 | 'NeedsRefreshResponse', |
44 | - 'ReleaseFailedResponse', |
45 | 'ScanFailedResponse', |
46 | 'SnapStoreError', |
47 | 'UnauthorizedUploadResponse', |
48 | @@ -75,10 +74,6 @@ |
49 | pass |
50 | |
51 | |
52 | -class ReleaseFailedResponse(SnapStoreError): |
53 | - pass |
54 | - |
55 | - |
56 | class ISnapStoreClient(Interface): |
57 | """Interface for the API provided by the snap store.""" |
58 | |
59 | @@ -150,13 +145,3 @@ |
60 | :return: A list of dictionaries, one per channel, each of which |
61 | contains at least "name" and "display_name" keys. |
62 | """ |
63 | - |
64 | - def release(snapbuild, revision): |
65 | - """Tell the store to release a snap build to specified channels. |
66 | - |
67 | - :param snapbuild: The `ISnapBuild` to release. |
68 | - :param revision: The revision returned by the store when uploading |
69 | - the build. |
70 | - :raises ReleaseFailedResponse: if the store failed to release the |
71 | - build. |
72 | - """ |
73 | |
74 | === modified file 'lib/lp/snappy/mail/snapbuild.py' |
75 | --- lib/lp/snappy/mail/snapbuild.py 2017-06-12 16:37:40 +0000 |
76 | +++ lib/lp/snappy/mail/snapbuild.py 2019-06-19 16:07:26 +0000 |
77 | @@ -88,34 +88,6 @@ |
78 | config.canonical.noreply_from_address, |
79 | "snap-build-upload-scan-failed", build) |
80 | |
81 | - @classmethod |
82 | - def forManualReview(cls, build): |
83 | - """Create a mailer for notifying about manual review. |
84 | - |
85 | - :param build: The relevant build. |
86 | - """ |
87 | - requester = build.requester |
88 | - recipients = {requester: RecipientReason.forBuildRequester(requester)} |
89 | - return cls( |
90 | - "%(snap_name)s held for manual review", |
91 | - "snapbuild-manualreview.txt", recipients, |
92 | - config.canonical.noreply_from_address, |
93 | - "snap-build-release-manual-review", build) |
94 | - |
95 | - @classmethod |
96 | - def forReleaseFailure(cls, build): |
97 | - """Create a mailer for notifying about store release failures. |
98 | - |
99 | - :param build: The relevant build. |
100 | - """ |
101 | - requester = build.requester |
102 | - recipients = {requester: RecipientReason.forBuildRequester(requester)} |
103 | - return cls( |
104 | - "Store release failed for %(snap_name)s", |
105 | - "snapbuild-releasefailed.txt", recipients, |
106 | - config.canonical.noreply_from_address, |
107 | - "snap-build-release-failed", build) |
108 | - |
109 | def __init__(self, subject, template_name, recipients, from_address, |
110 | notification_type, build): |
111 | super(SnapBuildMailer, self).__init__( |
112 | |
113 | === modified file 'lib/lp/snappy/model/snapbuildjob.py' |
114 | --- lib/lp/snappy/model/snapbuildjob.py 2018-12-18 18:10:39 +0000 |
115 | +++ lib/lp/snappy/model/snapbuildjob.py 2019-06-19 16:07:26 +0000 |
116 | @@ -58,7 +58,6 @@ |
117 | BadRefreshResponse, |
118 | BadScanStatusResponse, |
119 | ISnapStoreClient, |
120 | - ReleaseFailedResponse, |
121 | ScanFailedResponse, |
122 | SnapStoreError, |
123 | UnauthorizedUploadResponse, |
124 | @@ -169,10 +168,6 @@ |
125 | return oops_vars |
126 | |
127 | |
128 | -class ManualReview(SnapStoreError): |
129 | - pass |
130 | - |
131 | - |
132 | class RetryableSnapStoreError(SnapStoreError): |
133 | pass |
134 | |
135 | @@ -192,8 +187,6 @@ |
136 | user_error_types = ( |
137 | UnauthorizedUploadResponse, |
138 | ScanFailedResponse, |
139 | - ManualReview, |
140 | - ReleaseFailedResponse, |
141 | ) |
142 | |
143 | retry_error_types = (UploadNotScannedYetResponse, RetryableSnapStoreError) |
144 | @@ -341,16 +334,12 @@ |
145 | # We made progress, so reset attempt_count. |
146 | self.attempt_count = 1 |
147 | if self.store_url is None: |
148 | + # This is no longer strictly necessary as the store is handling |
149 | + # releases via the release intent, but we export various fields |
150 | + # via the api, so once this is called, we're done with |
151 | + # this task |
152 | self.store_url, self.store_revision = ( |
153 | client.checkStatus(self.status_url)) |
154 | - # We made progress, so reset attempt_count. |
155 | - self.attempt_count = 1 |
156 | - if self.snapbuild.snap.store_channels: |
157 | - if self.store_revision is None: |
158 | - raise ManualReview( |
159 | - "Package held for manual review on the store; " |
160 | - "cannot release it automatically.") |
161 | - client.release(self.snapbuild, self.store_revision) |
162 | self.error_message = None |
163 | except self.retry_error_types: |
164 | raise |
165 | @@ -373,12 +362,6 @@ |
166 | elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)): |
167 | mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild) |
168 | mailer.sendAll() |
169 | - elif isinstance(e, ManualReview): |
170 | - mailer = SnapBuildMailer.forManualReview(self.snapbuild) |
171 | - mailer.sendAll() |
172 | - elif isinstance(e, ReleaseFailedResponse): |
173 | - mailer = SnapBuildMailer.forReleaseFailure(self.snapbuild) |
174 | - mailer.sendAll() |
175 | # The normal job infrastructure will abort the transaction, but |
176 | # we want to commit instead: the only database changes we make |
177 | # are to this job's metadata and should be preserved. |
178 | |
179 | === modified file 'lib/lp/snappy/model/snapstoreclient.py' |
180 | --- lib/lp/snappy/model/snapstoreclient.py 2019-06-14 14:46:15 +0000 |
181 | +++ lib/lp/snappy/model/snapstoreclient.py 2019-06-19 16:07:26 +0000 |
182 | @@ -43,7 +43,6 @@ |
183 | BadSearchResponse, |
184 | ISnapStoreClient, |
185 | NeedsRefreshResponse, |
186 | - ReleaseFailedResponse, |
187 | ScanFailedResponse, |
188 | UnauthorizedUploadResponse, |
189 | UploadFailedResponse, |
190 | @@ -266,6 +265,14 @@ |
191 | "series": snap.store_series.name, |
192 | "built_at": snapbuild.date_started.isoformat(), |
193 | } |
194 | + # The security proxy is useless and breaks JSON serialisation. |
195 | + channels = removeSecurityProxy(snap.store_channels) |
196 | + if channels: |
197 | + # This will cause a release |
198 | + data.update({ |
199 | + "channels": channels, |
200 | + "only_if_newer": True, |
201 | + }) |
202 | # XXX cjwatson 2016-05-09: This should add timeline information, but |
203 | # that's currently difficult in jobs. |
204 | try: |
205 | @@ -346,8 +353,6 @@ |
206 | error_messages.append(error_detail) |
207 | raise ScanFailedResponse( |
208 | error_message, messages=error_messages) |
209 | - elif not response_data["can_release"]: |
210 | - return response_data["url"], None |
211 | else: |
212 | return response_data["url"], response_data["revision"] |
213 | except requests.HTTPError as e: |
214 | @@ -394,41 +399,3 @@ |
215 | if channels is None: |
216 | channels = _default_store_channels |
217 | return channels |
218 | - |
219 | - @classmethod |
220 | - def _release(cls, snap, revision): |
221 | - """Release a snap revision to specified channels.""" |
222 | - release_url = urlappend( |
223 | - config.snappy.store_url, "dev/api/snap-release/") |
224 | - data = { |
225 | - "name": snap.store_name, |
226 | - "revision": revision, |
227 | - # The security proxy is useless and breaks JSON serialisation. |
228 | - "channels": removeSecurityProxy(snap.store_channels), |
229 | - "series": snap.store_series.name, |
230 | - } |
231 | - # XXX cjwatson 2016-06-28: This should add timeline information, but |
232 | - # that's currently difficult in jobs. |
233 | - try: |
234 | - assert snap.store_secrets is not None |
235 | - urlfetch( |
236 | - release_url, method="POST", json=data, |
237 | - auth=MacaroonAuth( |
238 | - snap.store_secrets["root"], |
239 | - snap.store_secrets.get("discharge"))) |
240 | - except requests.HTTPError as e: |
241 | - if e.response.status_code == 401: |
242 | - if (e.response.headers.get("WWW-Authenticate") == |
243 | - "Macaroon needs_refresh=1"): |
244 | - raise NeedsRefreshResponse() |
245 | - raise cls._makeSnapStoreError(ReleaseFailedResponse, e) |
246 | - |
247 | - @classmethod |
248 | - def release(cls, snapbuild, revision): |
249 | - """See `ISnapStoreClient`.""" |
250 | - assert config.snappy.store_url is not None |
251 | - snap = snapbuild.snap |
252 | - assert snap.store_name is not None |
253 | - assert snap.store_series is not None |
254 | - assert snap.store_channels |
255 | - cls.refreshIfNecessary(snap, cls._release, snap, revision) |
256 | |
257 | === modified file 'lib/lp/snappy/tests/test_snapbuildjob.py' |
258 | --- lib/lp/snappy/tests/test_snapbuildjob.py 2019-05-22 14:57:45 +0000 |
259 | +++ lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-19 16:07:26 +0000 |
260 | @@ -35,7 +35,6 @@ |
261 | from lp.snappy.interfaces.snapstoreclient import ( |
262 | BadRefreshResponse, |
263 | ISnapStoreClient, |
264 | - ReleaseFailedResponse, |
265 | ScanFailedResponse, |
266 | UnauthorizedUploadResponse, |
267 | UploadFailedResponse, |
268 | @@ -65,7 +64,6 @@ |
269 | self.upload = FakeMethod() |
270 | self.checkStatus = FakeMethod() |
271 | self.listChannels = FakeMethod(result=[]) |
272 | - self.release = FakeMethod() |
273 | |
274 | |
275 | class TestSnapBuildJob(TestCaseWithFactory): |
276 | @@ -160,7 +158,6 @@ |
277 | JobRunner([job]).runAll() |
278 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
279 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
280 | - self.assertEqual([], client.release.calls) |
281 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
282 | self.assertEqual(self.store_url, job.store_url) |
283 | self.assertEqual(1, job.store_revision) |
284 | @@ -180,7 +177,6 @@ |
285 | JobRunner([job]).runAll() |
286 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
287 | self.assertEqual([], client.checkStatus.calls) |
288 | - self.assertEqual([], client.release.calls) |
289 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
290 | self.assertIsNone(job.store_url) |
291 | self.assertIsNone(job.store_revision) |
292 | @@ -206,7 +202,6 @@ |
293 | JobRunner([job]).runAll() |
294 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
295 | self.assertEqual([], client.checkStatus.calls) |
296 | - self.assertEqual([], client.release.calls) |
297 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
298 | self.assertIsNone(job.store_url) |
299 | self.assertIsNone(job.store_revision) |
300 | @@ -253,7 +248,6 @@ |
301 | JobRunner([job]).runAll() |
302 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
303 | self.assertEqual([], client.checkStatus.calls) |
304 | - self.assertEqual([], client.release.calls) |
305 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
306 | self.assertIsNone(job.store_url) |
307 | self.assertIsNone(job.store_revision) |
308 | @@ -272,7 +266,6 @@ |
309 | JobRunner([job]).runAll() |
310 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
311 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
312 | - self.assertEqual([], client.release.calls) |
313 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
314 | self.assertEqual(self.store_url, job.store_url) |
315 | self.assertEqual(1, job.store_revision) |
316 | @@ -298,7 +291,6 @@ |
317 | JobRunner([job]).runAll() |
318 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
319 | self.assertEqual([], client.checkStatus.calls) |
320 | - self.assertEqual([], client.release.calls) |
321 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
322 | self.assertIsNone(job.store_url) |
323 | self.assertIsNone(job.store_revision) |
324 | @@ -349,7 +341,6 @@ |
325 | JobRunner([job]).runAll() |
326 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
327 | self.assertEqual([], client.checkStatus.calls) |
328 | - self.assertEqual([], client.release.calls) |
329 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
330 | self.assertIsNone(job.store_url) |
331 | self.assertIsNone(job.store_revision) |
332 | @@ -399,7 +390,6 @@ |
333 | JobRunner([job]).runAll() |
334 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
335 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
336 | - self.assertEqual([], client.release.calls) |
337 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
338 | self.assertIsNone(job.store_url) |
339 | self.assertIsNone(job.store_revision) |
340 | @@ -418,7 +408,6 @@ |
341 | JobRunner([job]).runAll() |
342 | self.assertEqual([], client.upload.calls) |
343 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
344 | - self.assertEqual([], client.release.calls) |
345 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
346 | self.assertEqual(self.store_url, job.store_url) |
347 | self.assertEqual(1, job.store_revision) |
348 | @@ -448,7 +437,6 @@ |
349 | JobRunner([job]).runAll() |
350 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
351 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
352 | - self.assertEqual([], client.release.calls) |
353 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
354 | self.assertIsNone(job.store_url) |
355 | self.assertIsNone(job.store_revision) |
356 | @@ -497,7 +485,6 @@ |
357 | JobRunner([job]).runAll() |
358 | self.assertEqual([((snapbuild,), {})], client.upload.calls) |
359 | self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
360 | - self.assertEqual([((snapbuild, 1), {})], client.release.calls) |
361 | self.assertContentEqual([job], snapbuild.store_upload_jobs) |
362 | self.assertEqual(self.store_url, job.store_url) |
363 | self.assertEqual(1, job.store_revision) |
364 | @@ -505,110 +492,6 @@ |
365 | self.assertEqual([], pop_notifications()) |
366 | self.assertWebhookDeliveries(snapbuild, ["Pending", "Uploaded"]) |
367 | |
368 | - def test_run_release_manual_review_notifies(self): |
369 | - # A run configured to automatically release the package to certain |
370 | - # channels but that encounters the manual review state on upload |
371 | - # sends mail. |
372 | - requester = self.factory.makePerson(name="requester") |
373 | - requester_team = self.factory.makeTeam( |
374 | - owner=requester, name="requester-team", members=[requester]) |
375 | - snapbuild = self.makeSnapBuild( |
376 | - requester=requester_team, name="test-snap", owner=requester_team, |
377 | - store_channels=["stable", "edge"]) |
378 | - self.assertContentEqual([], snapbuild.store_upload_jobs) |
379 | - job = SnapStoreUploadJob.create(snapbuild) |
380 | - client = FakeSnapStoreClient() |
381 | - client.upload.result = self.status_url |
382 | - client.checkStatus.result = (self.store_url, None) |
383 | - self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
384 | - with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
385 | - JobRunner([job]).runAll() |
386 | - self.assertEqual([((snapbuild,), {})], client.upload.calls) |
387 | - self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
388 | - self.assertEqual([], client.release.calls) |
389 | - self.assertContentEqual([job], snapbuild.store_upload_jobs) |
390 | - self.assertEqual(self.store_url, job.store_url) |
391 | - self.assertIsNone(job.store_revision) |
392 | - self.assertEqual( |
393 | - "Package held for manual review on the store; " |
394 | - "cannot release it automatically.", |
395 | - job.error_message) |
396 | - [notification] = pop_notifications() |
397 | - self.assertEqual( |
398 | - config.canonical.noreply_from_address, notification["From"]) |
399 | - self.assertEqual( |
400 | - "Requester <%s>" % requester.preferredemail.email, |
401 | - notification["To"]) |
402 | - subject = notification["Subject"].replace("\n ", " ") |
403 | - self.assertEqual("test-snap held for manual review", subject) |
404 | - self.assertEqual( |
405 | - "Requester @requester-team", |
406 | - notification["X-Launchpad-Message-Rationale"]) |
407 | - self.assertEqual( |
408 | - requester_team.name, notification["X-Launchpad-Message-For"]) |
409 | - self.assertEqual( |
410 | - "snap-build-release-manual-review", |
411 | - notification["X-Launchpad-Notification-Type"]) |
412 | - body, footer = notification.get_payload(decode=True).split("\n-- \n") |
413 | - self.assertIn(self.store_url, body) |
414 | - self.assertEqual( |
415 | - "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n" |
416 | - "Your team Requester Team is the requester of the build.\n" % |
417 | - snapbuild.id, footer) |
418 | - self.assertWebhookDeliveries( |
419 | - snapbuild, ["Pending", "Failed to release to channels"]) |
420 | - |
421 | - def test_run_release_failure_notifies(self): |
422 | - # A run configured to automatically release the package to certain |
423 | - # channels but that fails to do so sends mail. |
424 | - requester = self.factory.makePerson(name="requester") |
425 | - requester_team = self.factory.makeTeam( |
426 | - owner=requester, name="requester-team", members=[requester]) |
427 | - snapbuild = self.makeSnapBuild( |
428 | - requester=requester_team, name="test-snap", owner=requester_team, |
429 | - store_channels=["stable", "edge"]) |
430 | - self.assertContentEqual([], snapbuild.store_upload_jobs) |
431 | - job = SnapStoreUploadJob.create(snapbuild) |
432 | - client = FakeSnapStoreClient() |
433 | - client.upload.result = self.status_url |
434 | - client.checkStatus.result = (self.store_url, 1) |
435 | - client.release.failure = ReleaseFailedResponse("Failed to publish") |
436 | - self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
437 | - with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
438 | - JobRunner([job]).runAll() |
439 | - self.assertEqual([((snapbuild,), {})], client.upload.calls) |
440 | - self.assertEqual([((self.status_url,), {})], client.checkStatus.calls) |
441 | - self.assertEqual([((snapbuild, 1), {})], client.release.calls) |
442 | - self.assertContentEqual([job], snapbuild.store_upload_jobs) |
443 | - self.assertEqual(self.store_url, job.store_url) |
444 | - self.assertEqual(1, job.store_revision) |
445 | - self.assertEqual("Failed to publish", job.error_message) |
446 | - [notification] = pop_notifications() |
447 | - self.assertEqual( |
448 | - config.canonical.noreply_from_address, notification["From"]) |
449 | - self.assertEqual( |
450 | - "Requester <%s>" % requester.preferredemail.email, |
451 | - notification["To"]) |
452 | - subject = notification["Subject"].replace("\n ", " ") |
453 | - self.assertEqual("Store release failed for test-snap", subject) |
454 | - self.assertEqual( |
455 | - "Requester @requester-team", |
456 | - notification["X-Launchpad-Message-Rationale"]) |
457 | - self.assertEqual( |
458 | - requester_team.name, notification["X-Launchpad-Message-For"]) |
459 | - self.assertEqual( |
460 | - "snap-build-release-failed", |
461 | - notification["X-Launchpad-Notification-Type"]) |
462 | - body, footer = notification.get_payload(decode=True).split("\n-- \n") |
463 | - self.assertIn("Failed to publish", body) |
464 | - self.assertIn(self.store_url, body) |
465 | - self.assertEqual( |
466 | - "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n" |
467 | - "Your team Requester Team is the requester of the build.\n" % |
468 | - snapbuild.id, footer) |
469 | - self.assertWebhookDeliveries( |
470 | - snapbuild, ["Pending", "Failed to release to channels"]) |
471 | - |
472 | def test_retry_delay(self): |
473 | # The job is retried every minute, unless it just made one of its |
474 | # first four attempts to poll the status endpoint, in which case the |
475 | @@ -655,15 +538,12 @@ |
476 | client = FakeSnapStoreClient() |
477 | client.upload.result = self.status_url |
478 | client.checkStatus.result = (self.store_url, 1) |
479 | - client.release.failure = UploadFailedResponse( |
480 | - "Proxy error", can_retry=True) |
481 | self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) |
482 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
483 | JobRunner([job]).runAll() |
484 | |
485 | previous_upload = client.upload.calls |
486 | previous_checkStatus = client.checkStatus.calls |
487 | - len_previous_release = len(client.release.calls) |
488 | |
489 | # Check we uploaded as expected |
490 | self.assertEqual(self.store_url, job.store_url) |
491 | @@ -676,10 +556,10 @@ |
492 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
493 | JobRunner([job]).runAll() |
494 | |
495 | - # We should not have called `upload`, but moved straight to `release` |
496 | + # Release is not called due to release intent in upload |
497 | + # but ensure that we have not called upload twice |
498 | self.assertEqual(previous_upload, client.upload.calls) |
499 | self.assertEqual(previous_checkStatus, client.checkStatus.calls) |
500 | - self.assertEqual(len_previous_release + 1, len(client.release.calls)) |
501 | self.assertIsNone(job.error_message) |
502 | |
503 | def test_with_snapbuild_metadata_as_none(self): |
504 | |
505 | === modified file 'lib/lp/snappy/tests/test_snapstoreclient.py' |
506 | --- lib/lp/snappy/tests/test_snapstoreclient.py 2019-06-14 14:49:25 +0000 |
507 | +++ lib/lp/snappy/tests/test_snapstoreclient.py 2019-06-19 16:07:26 +0000 |
508 | @@ -47,7 +47,6 @@ |
509 | BadScanStatusResponse, |
510 | BadSearchResponse, |
511 | ISnapStoreClient, |
512 | - ReleaseFailedResponse, |
513 | ScanFailedResponse, |
514 | UnauthorizedUploadResponse, |
515 | UploadFailedResponse, |
516 | @@ -282,20 +281,6 @@ |
517 | self.addCleanup( |
518 | getUtility(IMemcacheClient).delete, self.channels_memcache_key) |
519 | |
520 | - def _addSnapReleaseResponse(self): |
521 | - responses.add( |
522 | - "POST", "http://sca.example/dev/api/snap-release/", |
523 | - json={ |
524 | - "success": True, |
525 | - "channel_map": [ |
526 | - {"channel": "stable", "info": "specific", |
527 | - "version": "1.0", "revision": 1}, |
528 | - {"channel": "edge", "info": "specific", |
529 | - "version": "1.0", "revision": 1}, |
530 | - ], |
531 | - "opened_channels": ["stable", "edge"], |
532 | - }) |
533 | - |
534 | @responses.activate |
535 | def test_requestPackageUploadPermission(self): |
536 | snappy_series = self.factory.makeSnappySeries(name="rolling") |
537 | @@ -402,6 +387,41 @@ |
538 | ])) |
539 | |
540 | @responses.activate |
541 | + def test_upload_with_release_intent(self): |
542 | + snapbuild = self.makeUploadableSnapBuild() |
543 | + snapbuild.snap.store_channels = ['beta', 'edge'] |
544 | + transaction.commit() |
545 | + self._addUnscannedUploadResponse() |
546 | + self._addSnapPushResponse() |
547 | + with dbuser(config.ISnapStoreUploadJobSource.dbuser): |
548 | + self.assertEqual( |
549 | + "http://sca.example/dev/api/snaps/1/builds/1/status", |
550 | + self.client.upload(snapbuild)) |
551 | + requests = [call.request for call in responses.calls] |
552 | + self.assertThat(requests, MatchesListwise([ |
553 | + RequestMatches( |
554 | + url=Equals("http://updown.example/unscanned-upload/"), |
555 | + method=Equals("POST"), |
556 | + form_data={ |
557 | + "binary": MatchesStructure.byEquality( |
558 | + name="binary", filename="test-snap.snap", |
559 | + value="dummy snap content", |
560 | + type="application/octet-stream", |
561 | + )}), |
562 | + RequestMatches( |
563 | + url=Equals("http://sca.example/dev/api/snap-push/"), |
564 | + method=Equals("POST"), |
565 | + headers=ContainsDict( |
566 | + {"Content-Type": Equals("application/json")}), |
567 | + auth=("Macaroon", MacaroonsVerify(self.root_key)), |
568 | + json_data={ |
569 | + "name": "test-snap", "updown_id": 1, "series": "rolling", |
570 | + "built_at": snapbuild.date_started.isoformat(), |
571 | + "only_if_newer": True, "channels": ['beta', 'edge'], |
572 | + }), |
573 | + ])) |
574 | + |
575 | + @responses.activate |
576 | def test_upload_no_discharge(self): |
577 | root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest() |
578 | root_macaroon = Macaroon(key=root_key) |
579 | @@ -638,100 +658,3 @@ |
580 | self.assertContentEqual([], responses.calls) |
581 | self.assertIsNone( |
582 | getUtility(IMemcacheClient).get(self.channels_memcache_key)) |
583 | - |
584 | - @responses.activate |
585 | - def test_release(self): |
586 | - snap = self.factory.makeSnap( |
587 | - store_upload=True, |
588 | - store_series=self.factory.makeSnappySeries(name="rolling"), |
589 | - store_name="test-snap", |
590 | - store_secrets=self._make_store_secrets(), |
591 | - store_channels=["stable", "edge"]) |
592 | - snapbuild = self.factory.makeSnapBuild(snap=snap) |
593 | - self._addSnapReleaseResponse() |
594 | - self.client.release(snapbuild, 1) |
595 | - self.assertThat(responses.calls[-1].request, RequestMatches( |
596 | - url=Equals("http://sca.example/dev/api/snap-release/"), |
597 | - method=Equals("POST"), |
598 | - headers=ContainsDict({"Content-Type": Equals("application/json")}), |
599 | - auth=("Macaroon", MacaroonsVerify(self.root_key)), |
600 | - json_data={ |
601 | - "name": "test-snap", "revision": 1, |
602 | - "channels": ["stable", "edge"], "series": "rolling", |
603 | - })) |
604 | - |
605 | - @responses.activate |
606 | - def test_release_no_discharge(self): |
607 | - root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest() |
608 | - root_macaroon = Macaroon(key=root_key) |
609 | - snap = self.factory.makeSnap( |
610 | - store_upload=True, |
611 | - store_series=self.factory.makeSnappySeries(name="rolling"), |
612 | - store_name="test-snap", |
613 | - store_secrets={"root": root_macaroon.serialize()}, |
614 | - store_channels=["stable", "edge"]) |
615 | - snapbuild = self.factory.makeSnapBuild(snap=snap) |
616 | - self._addSnapReleaseResponse() |
617 | - self.client.release(snapbuild, 1) |
618 | - self.assertThat(responses.calls[-1].request, RequestMatches( |
619 | - url=Equals("http://sca.example/dev/api/snap-release/"), |
620 | - method=Equals("POST"), |
621 | - headers=ContainsDict({"Content-Type": Equals("application/json")}), |
622 | - auth=("Macaroon", MacaroonsVerify(root_key)), |
623 | - json_data={ |
624 | - "name": "test-snap", "revision": 1, |
625 | - "channels": ["stable", "edge"], "series": "rolling", |
626 | - })) |
627 | - |
628 | - @responses.activate |
629 | - def test_release_needs_discharge_macaroon_refresh(self): |
630 | - store_secrets = self._make_store_secrets() |
631 | - snap = self.factory.makeSnap( |
632 | - store_upload=True, |
633 | - store_series=self.factory.makeSnappySeries(name="rolling"), |
634 | - store_name="test-snap", store_secrets=store_secrets, |
635 | - store_channels=["stable", "edge"]) |
636 | - snapbuild = self.factory.makeSnapBuild(snap=snap) |
637 | - responses.add( |
638 | - "POST", "http://sca.example/dev/api/snap-release/", status=401, |
639 | - headers={"WWW-Authenticate": "Macaroon needs_refresh=1"}) |
640 | - self._addMacaroonRefreshResponse() |
641 | - self._addSnapReleaseResponse() |
642 | - self.client.release(snapbuild, 1) |
643 | - requests = [call.request for call in responses.calls] |
644 | - self.assertThat(requests, MatchesListwise([ |
645 | - MatchesStructure.byEquality(path_url="/dev/api/snap-release/"), |
646 | - MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"), |
647 | - MatchesStructure.byEquality(path_url="/dev/api/snap-release/"), |
648 | - ])) |
649 | - self.assertNotEqual( |
650 | - store_secrets["discharge"], snap.store_secrets["discharge"]) |
651 | - |
652 | - @responses.activate |
653 | - def test_release_error(self): |
654 | - snap = self.factory.makeSnap( |
655 | - store_upload=True, |
656 | - store_series=self.factory.makeSnappySeries(name="rolling"), |
657 | - store_name="test-snap", store_secrets=self._make_store_secrets(), |
658 | - store_channels=["stable", "edge"]) |
659 | - snapbuild = self.factory.makeSnapBuild(snap=snap) |
660 | - responses.add( |
661 | - "POST", "http://sca.example/dev/api/snap-release/", status=503, |
662 | - json={"error_list": [{"message": "Failed to publish"}]}) |
663 | - self.assertRaisesWithContent( |
664 | - ReleaseFailedResponse, "Failed to publish", |
665 | - self.client.release, snapbuild, 1) |
666 | - |
667 | - @responses.activate |
668 | - def test_release_404(self): |
669 | - snap = self.factory.makeSnap( |
670 | - store_upload=True, |
671 | - store_series=self.factory.makeSnappySeries(name="rolling"), |
672 | - store_name="test-snap", store_secrets=self._make_store_secrets(), |
673 | - store_channels=["stable", "edge"]) |
674 | - snapbuild = self.factory.makeSnapBuild(snap=snap) |
675 | - responses.add( |
676 | - "POST", "http://sca.example/dev/api/snap-release/", status=404) |
677 | - self.assertRaisesWithContent( |
678 | - ReleaseFailedResponse, b"404 Client Error: Not Found", |
679 | - self.client.release, snapbuild, 1) |
I think in general this looks OK, but I want to see a bit more effort to go through and root out vestiges of the old workflow.