Merge ~ines-almeida/launchpad:webhook-patterns/update-models into launchpad:master
- Git
- lp:~ines-almeida/launchpad
- webhook-patterns/update-models
- Merge into master
Status: | Merged |
---|---|
Approved by: | Ines Almeida |
Approved revision: | 7b067caf1d31c625c447c9cd8932308b3388413d |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ines-almeida/launchpad:webhook-patterns/update-models |
Merge into: | launchpad:master |
Diff against target: |
543 lines (+224/-21) 9 files modified
lib/lp/code/interfaces/cibuild.py (+13/-1) lib/lp/code/model/cibuild.py (+51/-12) lib/lp/code/model/tests/test_cibuild.py (+53/-2) lib/lp/services/webhooks/browser.py (+1/-0) lib/lp/services/webhooks/interfaces.py (+27/-2) lib/lp/services/webhooks/model.py (+35/-3) lib/lp/services/webhooks/tests/test_model.py (+41/-1) lib/lp/services/webhooks/tests/test_webservice.py (+1/-0) lib/lp/testing/factory.py (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+446948@code.launchpad.net |
Commit message
Update Webhook and CIBuild models with the new fields for webhook pattern filtering
`git_ref_pattern` field and `check_
`git_refs` added to CIBuild model will be used to store the git refs that originate the builds
Description of the change
This is the preparation work needed to filter git repo webhooks: https:/
This MP adds 3 main things:
- `git_ref_pattern` field to Webhook model - to store the pattern used to filter webhooks
- `checkGitRefPat
- `git_refs` field to CIBuild model - to store the git refs that originate a given build, to later be used to check against the webhooks ref_pattern
The final functionality to actually filter webhooks according to the `ref_pattern` will be added in another MP.
Ines Almeida (ines-almeida) wrote : | # |
Ines Almeida (ines-almeida) wrote : | # |
> Will update `ref_pattern` to `git_ref_pattern` as I also just changed it in
> the DB MP. I will wait for the approval of that MP before making that change.
Done
Colin Watson (cjwatson) : | # |
Ines Almeida (ines-almeida) : | # |
Colin Watson (cjwatson) : | # |
Ines Almeida (ines-almeida) wrote : | # |
Addressed all comments and cleaned up commit tree. Will merge after checks pass
Preview Diff
1 | diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py |
2 | index bac6181..472b67e 100644 |
3 | --- a/lib/lp/code/interfaces/cibuild.py |
4 | +++ b/lib/lp/code/interfaces/cibuild.py |
5 | @@ -105,6 +105,15 @@ class ICIBuildView(IPackageBuildView, IPrivacy): |
6 | ) |
7 | ) |
8 | |
9 | + git_refs = exported( |
10 | + List( |
11 | + TextLine(), |
12 | + title=_("The git references that originated this CI Build."), |
13 | + required=False, |
14 | + readonly=True, |
15 | + ) |
16 | + ) |
17 | + |
18 | distro_arch_series = exported( |
19 | Reference( |
20 | IDistroArchSeries, |
21 | @@ -274,6 +283,7 @@ class ICIBuildSet(ISpecificBuildFarmJobSource): |
22 | distro_arch_series, |
23 | stages, |
24 | date_created=DEFAULT, |
25 | + git_refs=None, |
26 | ): |
27 | """Create an `ICIBuild`.""" |
28 | |
29 | @@ -285,7 +295,9 @@ class ICIBuildSet(ISpecificBuildFarmJobSource): |
30 | these Git commit IDs. |
31 | """ |
32 | |
33 | - def requestBuild(git_repository, commit_sha1, distro_arch_series, stages): |
34 | + def requestBuild( |
35 | + git_repository, commit_sha1, distro_arch_series, stages, git_refs=None |
36 | + ): |
37 | """Request a CI build. |
38 | |
39 | This checks that the architecture is allowed and that there isn't |
40 | diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py |
41 | index 1cf5a19..0634d50 100644 |
42 | --- a/lib/lp/code/model/cibuild.py |
43 | +++ b/lib/lp/code/model/cibuild.py |
44 | @@ -14,7 +14,16 @@ from operator import itemgetter |
45 | |
46 | from lazr.lifecycle.event import ObjectCreatedEvent |
47 | from storm.databases.postgres import JSON |
48 | -from storm.locals import Bool, DateTime, Desc, Int, Reference, Store, Unicode |
49 | +from storm.locals import ( |
50 | + Bool, |
51 | + DateTime, |
52 | + Desc, |
53 | + Int, |
54 | + List, |
55 | + Reference, |
56 | + Store, |
57 | + Unicode, |
58 | +) |
59 | from storm.store import EmptyResultSet |
60 | from zope.component import getUtility |
61 | from zope.event import notify |
62 | @@ -171,12 +180,14 @@ def determine_DASes_to_build(configuration, logger=None): |
63 | |
64 | |
65 | def get_all_commits_for_paths(git_repository, paths): |
66 | - return [ |
67 | - ref.commit_sha1 |
68 | - for ref in GitRef.findByReposAndPaths( |
69 | - [(git_repository, ref_path) for ref_path in paths] |
70 | - ).values() |
71 | - ] |
72 | + commits = {} |
73 | + for ref in GitRef.findByReposAndPaths( |
74 | + [(git_repository, ref_path) for ref_path in paths] |
75 | + ).values(): |
76 | + if ref.commit_sha1 not in commits: |
77 | + commits[ref.commit_sha1] = [] |
78 | + commits[ref.commit_sha1].append(ref.path) |
79 | + return commits |
80 | |
81 | |
82 | def parse_configuration(git_repository, blob): |
83 | @@ -204,6 +215,7 @@ class CIBuild(PackageBuildMixin, StormBase): |
84 | git_repository = Reference(git_repository_id, "GitRepository.id") |
85 | |
86 | commit_sha1 = Unicode(name="commit_sha1", allow_none=False) |
87 | + git_refs = List(name="git_refs", allow_none=True) |
88 | |
89 | distro_arch_series_id = Int(name="distro_arch_series", allow_none=False) |
90 | distro_arch_series = Reference( |
91 | @@ -261,12 +273,14 @@ class CIBuild(PackageBuildMixin, StormBase): |
92 | builder_constraints, |
93 | stages, |
94 | date_created=DEFAULT, |
95 | + git_refs=None, |
96 | ): |
97 | """Construct a `CIBuild`.""" |
98 | super().__init__() |
99 | self.build_farm_job = build_farm_job |
100 | self.git_repository = git_repository |
101 | self.commit_sha1 = commit_sha1 |
102 | + self.git_refs = git_refs |
103 | self.distro_arch_series = distro_arch_series |
104 | self.processor = processor |
105 | self.virtualized = virtualized |
106 | @@ -655,6 +669,7 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
107 | commit_sha1, |
108 | distro_arch_series, |
109 | stages, |
110 | + git_refs=None, |
111 | date_created=DEFAULT, |
112 | ): |
113 | """See `ICIBuildSet`.""" |
114 | @@ -674,6 +689,7 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
115 | ), |
116 | stages=stages, |
117 | date_created=date_created, |
118 | + git_refs=git_refs, |
119 | ) |
120 | store.add(cibuild) |
121 | store.flush() |
122 | @@ -712,7 +728,12 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
123 | ) is not None and self._isBuildableArchitectureAllowed(das) |
124 | |
125 | def requestBuild( |
126 | - self, git_repository, commit_sha1, distro_arch_series, stages |
127 | + self, |
128 | + git_repository, |
129 | + commit_sha1, |
130 | + distro_arch_series, |
131 | + stages, |
132 | + git_refs=None, |
133 | ): |
134 | """See `ICIBuildSet`.""" |
135 | pocket = PackagePublishingPocket.UPDATES |
136 | @@ -726,17 +747,32 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
137 | CIBuild.distro_arch_series == distro_arch_series, |
138 | ) |
139 | if not result.is_empty(): |
140 | + # We append the new git_refs to existing builds here to keep the |
141 | + # git_refs list up-to-date, and potentially filter git repository |
142 | + # webhooks by their git refs if the status of the build changes |
143 | + if git_refs: |
144 | + for cibuild in result: |
145 | + if cibuild.git_refs is None: |
146 | + cibuild.git_refs = [] |
147 | + cibuild.git_refs.extend(git_refs) |
148 | raise CIBuildAlreadyRequested |
149 | |
150 | build = self.new( |
151 | - git_repository, commit_sha1, distro_arch_series, stages |
152 | + git_repository, commit_sha1, distro_arch_series, stages, git_refs |
153 | ) |
154 | build.queueBuild() |
155 | notify(ObjectCreatedEvent(build)) |
156 | return build |
157 | |
158 | def _tryToRequestBuild( |
159 | - self, git_repository, commit_sha1, configuration, das, stages, logger |
160 | + self, |
161 | + git_repository, |
162 | + commit_sha1, |
163 | + configuration, |
164 | + das, |
165 | + stages, |
166 | + logger, |
167 | + git_refs=None, |
168 | ): |
169 | try: |
170 | if logger is not None: |
171 | @@ -746,7 +782,9 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
172 | das.distroseries.name, |
173 | das.architecturetag, |
174 | ) |
175 | - build = self.requestBuild(git_repository, commit_sha1, das, stages) |
176 | + build = self.requestBuild( |
177 | + git_repository, commit_sha1, das, stages, git_refs |
178 | + ) |
179 | # Create reports for each individual job in this build so that |
180 | # they show up as pending in the web UI. The job names |
181 | # generated here should match those generated by |
182 | @@ -778,7 +816,7 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
183 | # getCommits performs a web request! |
184 | commits = getUtility(IGitHostingClient).getCommits( |
185 | git_repository.getInternalPath(), |
186 | - commit_sha1s, |
187 | + list(commit_sha1s), |
188 | # XXX cjwatson 2022-01-19: We should also fetch |
189 | # debian/.launchpad.yaml (or perhaps make the path a property of |
190 | # the repository) once lpci and launchpad-buildd support using |
191 | @@ -814,6 +852,7 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): |
192 | das, |
193 | stages[das.architecturetag], |
194 | logger, |
195 | + git_refs=commit_sha1s.get(commit["sha1"]), |
196 | ) |
197 | |
198 | def getByID(self, build_id): |
199 | diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py |
200 | index c49024a..bc3fd83 100644 |
201 | --- a/lib/lp/code/model/tests/test_cibuild.py |
202 | +++ b/lib/lp/code/model/tests/test_cibuild.py |
203 | @@ -94,7 +94,7 @@ class TestGetAllCommitsForPaths(TestCaseWithFactory): |
204 | |
205 | rv = get_all_commits_for_paths(repository, ref_paths) |
206 | |
207 | - self.assertEqual([], rv) |
208 | + self.assertEqual({}, rv) |
209 | |
210 | def test_one_ref_one_path(self): |
211 | repository = self.factory.makeGitRepository() |
212 | @@ -104,7 +104,7 @@ class TestGetAllCommitsForPaths(TestCaseWithFactory): |
213 | rv = get_all_commits_for_paths(repository, ref_paths) |
214 | |
215 | self.assertEqual(1, len(rv)) |
216 | - self.assertEqual(ref.commit_sha1, rv[0]) |
217 | + self.assertIn(ref.commit_sha1, rv) |
218 | |
219 | def test_multiple_refs_and_paths(self): |
220 | repository = self.factory.makeGitRepository() |
221 | @@ -781,6 +781,56 @@ class TestCIBuildSet(TestCaseWithFactory): |
222 | self.assertIsNone(build_queue.builder_constraints) |
223 | self.assertEqual(BuildQueueStatus.WAITING, build_queue.status) |
224 | |
225 | + def test_requestCIBuild_with_git_refs(self): |
226 | + # requestBuild creates a new CIBuild with the given git_refs |
227 | + repository = self.factory.makeGitRepository() |
228 | + commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() |
229 | + das = self.factory.makeBuildableDistroArchSeries() |
230 | + stages = [[("build", 0)]] |
231 | + |
232 | + git_refs = ["ref/test", "ref/foo"] |
233 | + build = getUtility(ICIBuildSet).requestBuild( |
234 | + repository, commit_sha1, das, stages, git_refs |
235 | + ) |
236 | + |
237 | + self.assertTrue(ICIBuild.providedBy(build)) |
238 | + self.assertThat( |
239 | + build, |
240 | + MatchesStructure.byEquality( |
241 | + git_repository=repository, |
242 | + commit_sha1=commit_sha1, |
243 | + distro_arch_series=das, |
244 | + stages=stages, |
245 | + status=BuildStatus.NEEDSBUILD, |
246 | + ), |
247 | + ) |
248 | + store = Store.of(build) |
249 | + store.flush() |
250 | + build_queue = store.find( |
251 | + BuildQueue, |
252 | + BuildQueue._build_farm_job_id |
253 | + == removeSecurityProxy(build).build_farm_job_id, |
254 | + ).one() |
255 | + self.assertProvides(build_queue, IBuildQueue) |
256 | + self.assertTrue(build_queue.virtualized) |
257 | + self.assertIsNone(build_queue.builder_constraints) |
258 | + self.assertEqual(BuildQueueStatus.WAITING, build_queue.status) |
259 | + self.assertEqual(git_refs, build.git_refs) |
260 | + |
261 | + # Rescheduling a build for the same commit_sha1 raises an error, but |
262 | + # git_refs of the build are updated |
263 | + self.assertRaises( |
264 | + CIBuildAlreadyRequested, |
265 | + getUtility(ICIBuildSet).requestBuild, |
266 | + repository, |
267 | + commit_sha1, |
268 | + das, |
269 | + stages, |
270 | + ["ref/bar"], |
271 | + ) |
272 | + git_refs.append("ref/bar") |
273 | + self.assertEqual(git_refs, build.git_refs) |
274 | + |
275 | def test_requestBuild_score(self): |
276 | # CI builds have an initial queue score of 2600. |
277 | repository = self.factory.makeGitRepository() |
278 | @@ -1022,6 +1072,7 @@ class TestCIBuildSet(TestCaseWithFactory): |
279 | ) |
280 | ), |
281 | ) |
282 | + self.assertEqual(build.git_refs, ref_paths) |
283 | |
284 | def test_requestBuildsForRefs_multiple_architectures(self): |
285 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
286 | diff --git a/lib/lp/services/webhooks/browser.py b/lib/lp/services/webhooks/browser.py |
287 | index bb53917..991d7ef 100644 |
288 | --- a/lib/lp/services/webhooks/browser.py |
289 | +++ b/lib/lp/services/webhooks/browser.py |
290 | @@ -134,6 +134,7 @@ class WebhookAddView(LaunchpadFormView): |
291 | event_types=data["event_types"], |
292 | active=data["active"], |
293 | secret=data["secret"], |
294 | + git_ref_pattern=data.get("git_ref_pattern"), |
295 | ) |
296 | self.next_url = canonical_url(webhook) |
297 | |
298 | diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py |
299 | index b00d45f..9b32ba1 100644 |
300 | --- a/lib/lp/services/webhooks/interfaces.py |
301 | +++ b/lib/lp/services/webhooks/interfaces.py |
302 | @@ -176,6 +176,18 @@ class IWebhook(Interface): |
303 | ) |
304 | ) |
305 | |
306 | + git_ref_pattern = exported( |
307 | + TextLine( |
308 | + title=_("Git ref pattern"), |
309 | + required=False, |
310 | + description=_( |
311 | + "Pattern to match against git-ref/branch name of an event, " |
312 | + "to filter webhook triggers" |
313 | + ), |
314 | + max_length=200, |
315 | + ) |
316 | + ) |
317 | + |
318 | def getDelivery(id): |
319 | """Retrieve a delivery by ID, or None if it doesn't exist.""" |
320 | |
321 | @@ -197,7 +209,15 @@ class IWebhook(Interface): |
322 | |
323 | |
324 | class IWebhookSet(Interface): |
325 | - def new(target, registrant, delivery_url, event_types, active, secret): |
326 | + def new( |
327 | + target, |
328 | + registrant, |
329 | + delivery_url, |
330 | + event_types, |
331 | + active, |
332 | + secret, |
333 | + git_ref_pattern=None, |
334 | + ): |
335 | """Create a new webhook.""" |
336 | |
337 | def delete(hooks): |
338 | @@ -250,7 +270,12 @@ class IWebhookTarget(Interface): |
339 | ) |
340 | @operation_for_version("devel") |
341 | def newWebhook( |
342 | - registrant, delivery_url, event_types, active=True, secret=None |
343 | + registrant, |
344 | + delivery_url, |
345 | + event_types, |
346 | + active=True, |
347 | + secret=None, |
348 | + git_ref_pattern=None, |
349 | ): |
350 | """Create a new webhook.""" |
351 | |
352 | diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py |
353 | index bafd604..d5e1267 100644 |
354 | --- a/lib/lp/services/webhooks/model.py |
355 | +++ b/lib/lp/services/webhooks/model.py |
356 | @@ -12,6 +12,7 @@ import ipaddress |
357 | import re |
358 | import socket |
359 | from datetime import datetime, timedelta, timezone |
360 | +from fnmatch import fnmatch |
361 | from urllib.parse import urlsplit |
362 | |
363 | import iso8601 |
364 | @@ -66,6 +67,15 @@ def webhook_modified(webhook, event): |
365 | removeSecurityProxy(webhook).date_last_modified = UTC_NOW |
366 | |
367 | |
368 | +def check_webhook_git_ref_pattern(webhook: IWebhook, git_ref: str): |
369 | + """Check if a given git ref matches against the webhook's |
370 | + `git_ref_pattern` if it has one (only Git Repository webhooks can have |
371 | + a `git_ref_pattern` value)""" |
372 | + if not webhook.git_ref_pattern: |
373 | + return True |
374 | + return fnmatch(git_ref, webhook.git_ref_pattern) |
375 | + |
376 | + |
377 | @implementer(IWebhook) |
378 | class Webhook(StormBase): |
379 | """See `IWebhook`.""" |
380 | @@ -114,6 +124,8 @@ class Webhook(StormBase): |
381 | |
382 | json_data = JSON(name="json_data") |
383 | |
384 | + git_ref_pattern = Unicode(allow_none=True) |
385 | + |
386 | @property |
387 | def target(self): |
388 | if self.git_repository is not None: |
389 | @@ -192,7 +204,14 @@ class WebhookSet: |
390 | """See `IWebhookSet`.""" |
391 | |
392 | def new( |
393 | - self, target, registrant, delivery_url, event_types, active, secret |
394 | + self, |
395 | + target, |
396 | + registrant, |
397 | + delivery_url, |
398 | + event_types, |
399 | + active, |
400 | + secret, |
401 | + git_ref_pattern=None, |
402 | ): |
403 | from lp.charms.interfaces.charmrecipe import ICharmRecipe |
404 | from lp.code.interfaces.branch import IBranch |
405 | @@ -233,6 +252,7 @@ class WebhookSet: |
406 | hook.delivery_url = delivery_url |
407 | hook.active = active |
408 | hook.secret = secret |
409 | + hook.git_ref_pattern = git_ref_pattern |
410 | hook.event_types = event_types |
411 | IStore(Webhook).add(hook) |
412 | IStore(Webhook).flush() |
413 | @@ -346,10 +366,22 @@ class WebhookTargetMixin: |
414 | return self.valid_webhook_event_types |
415 | |
416 | def newWebhook( |
417 | - self, registrant, delivery_url, event_types, active=True, secret=None |
418 | + self, |
419 | + registrant, |
420 | + delivery_url, |
421 | + event_types, |
422 | + active=True, |
423 | + secret=None, |
424 | + git_ref_pattern=None, |
425 | ): |
426 | return getUtility(IWebhookSet).new( |
427 | - self, registrant, delivery_url, event_types, active, secret |
428 | + self, |
429 | + registrant, |
430 | + delivery_url, |
431 | + event_types, |
432 | + active, |
433 | + secret, |
434 | + git_ref_pattern, |
435 | ) |
436 | |
437 | |
438 | diff --git a/lib/lp/services/webhooks/tests/test_model.py b/lib/lp/services/webhooks/tests/test_model.py |
439 | index ede094c..7053899 100644 |
440 | --- a/lib/lp/services/webhooks/tests/test_model.py |
441 | +++ b/lib/lp/services/webhooks/tests/test_model.py |
442 | @@ -20,7 +20,11 @@ from lp.services.features.testing import FeatureFixture |
443 | from lp.services.webapp.authorization import check_permission |
444 | from lp.services.webapp.snapshot import notify_modified |
445 | from lp.services.webhooks.interfaces import IWebhookSet |
446 | -from lp.services.webhooks.model import WebhookJob, WebhookSet |
447 | +from lp.services.webhooks.model import ( |
448 | + WebhookJob, |
449 | + WebhookSet, |
450 | + check_webhook_git_ref_pattern, |
451 | +) |
452 | from lp.soyuz.interfaces.livefs import ( |
453 | LIVEFS_FEATURE_FLAG, |
454 | LIVEFS_WEBHOOKS_FEATURE_FLAG, |
455 | @@ -71,6 +75,40 @@ class TestWebhook(TestCaseWithFactory): |
456 | webhook.event_types = ["foo", [1]] |
457 | self.assertContentEqual([], webhook.event_types) |
458 | |
459 | + def test_check_webhook_git_ref_pattern(self): |
460 | + # See lib/lp/code/templates/gitrepository-permissions.pt for an |
461 | + # explanation of the wildcards logic |
462 | + git_ref = "refs/heads/foo-test" |
463 | + expected_results = { |
464 | + None: True, |
465 | + "": True, |
466 | + "*": True, |
467 | + "*foo*": True, |
468 | + "foo": False, |
469 | + "foo-test": False, # it needs to match the full git ref |
470 | + "foo*": False, |
471 | + "*foo": False, |
472 | + "*foo?test": True, |
473 | + "*foo[-_.]test": True, |
474 | + "*foo![-]test": False, |
475 | + "*bar*": False, |
476 | + "refs/heads/*": True, # this should match all branches (not tags) |
477 | + "refs/heads/foo*": True, |
478 | + "refs/heads/bar*": False, |
479 | + "refs/heads/*-test": True, |
480 | + } |
481 | + |
482 | + results = dict() |
483 | + webhook = self.factory.makeWebhook() |
484 | + with admin_logged_in(): |
485 | + for pattern in expected_results: |
486 | + webhook.git_ref_pattern = pattern |
487 | + results[pattern] = check_webhook_git_ref_pattern( |
488 | + webhook, git_ref |
489 | + ) |
490 | + |
491 | + self.assertEqual(expected_results, results) |
492 | + |
493 | |
494 | class TestWebhookPermissions(TestCaseWithFactory): |
495 | layer = DatabaseFunctionalLayer |
496 | @@ -112,6 +150,7 @@ class TestWebhookPermissions(TestCaseWithFactory): |
497 | "secret", |
498 | "setSecret", |
499 | "target", |
500 | + "git_ref_pattern", |
501 | }, |
502 | } |
503 | webhook = self.factory.makeWebhook() |
504 | @@ -128,6 +167,7 @@ class TestWebhookPermissions(TestCaseWithFactory): |
505 | "event_types", |
506 | "registrant_id", |
507 | "secret", |
508 | + "git_ref_pattern", |
509 | }, |
510 | } |
511 | webhook = self.factory.makeWebhook() |
512 | diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py |
513 | index 810a83d..f7d9d0b 100644 |
514 | --- a/lib/lp/services/webhooks/tests/test_webservice.py |
515 | +++ b/lib/lp/services/webhooks/tests/test_webservice.py |
516 | @@ -78,6 +78,7 @@ class TestWebhook(TestCaseWithFactory): |
517 | "self_link", |
518 | "target_link", |
519 | "web_link", |
520 | + "git_ref_pattern", |
521 | ), |
522 | ) |
523 | |
524 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
525 | index 11d337b..2ac51bc 100644 |
526 | --- a/lib/lp/testing/factory.py |
527 | +++ b/lib/lp/testing/factory.py |
528 | @@ -6163,6 +6163,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
529 | secret=None, |
530 | active=True, |
531 | event_types=None, |
532 | + git_ref_pattern=None, |
533 | ): |
534 | if target is None: |
535 | target = self.makeGitRepository() |
536 | @@ -6175,6 +6176,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
537 | event_types or [], |
538 | active, |
539 | secret, |
540 | + git_ref_pattern, |
541 | ) |
542 | |
543 | def makeSnap( |
Will update `ref_pattern` to `git_ref_pattern` as I also just changed it in the DB MP. I will wait for the approval of that MP before making that change.
This MP can still be reviewed with that in mind.
https:/ /code.launchpad .net/~ines- almeida/ launchpad/ +git/launchpad/ +merge/ 446950