Merge ~logrotate-charmers/charm-logrotated:bugs_1833095_1833093 into ~logrotate-charmers/charm-logrotated:master

Proposed by Diko Parvanov
Status: Merged
Approved by: Diko Parvanov
Approved revision: 6d391ef597fad5552bfdee132025c4aa603a21d9
Merge reported by: Diko Parvanov
Merged at revision: 6d391ef597fad5552bfdee132025c4aa603a21d9
Proposed branch: ~logrotate-charmers/charm-logrotated:bugs_1833095_1833093
Merge into: ~logrotate-charmers/charm-logrotated:master
Diff against target: 672 lines (+148/-131)
10 files modified
actions/actions.py (+12/-21)
dev/null (+0/-13)
lib/lib_cron.py (+38/-32)
lib/lib_logrotate.py (+17/-24)
reactive/logrotate.py (+20/-8)
tests/functional/conftest.py (+14/-9)
tests/functional/juju_tools.py (+14/-13)
tests/functional/test_logrotate.py (+5/-2)
tests/unit/conftest.py (+11/-0)
tests/unit/test_logrotate.py (+17/-9)
Reviewer Review Type Date Requested Status
Diko Parvanov Approve
Review via email: mp+369020@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Diko Parvanov (dparv) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/actions/__init__.py b/actions/__init__.py
2deleted file mode 100755
3index 9b088de..0000000
4--- a/actions/__init__.py
5+++ /dev/null
6@@ -1,13 +0,0 @@
7-# Copyright 2016 Canonical Ltd
8-#
9-# Licensed under the Apache License, Version 2.0 (the "License");
10-# you may not use this file except in compliance with the License.
11-# You may obtain a copy of the License at
12-#
13-# http://www.apache.org/licenses/LICENSE-2.0
14-#
15-# Unless required by applicable law or agreed to in writing, software
16-# distributed under the License is distributed on an "AS IS" BASIS,
17-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18-# See the License for the specific language governing permissions and
19-# limitations under the License.
20diff --git a/actions/actions.py b/actions/actions.py
21index aee6bb7..8178850 100755
22--- a/actions/actions.py
23+++ b/actions/actions.py
24@@ -1,45 +1,36 @@
25 #!/usr/bin/env python3
26-#
27-# Copyright 2016 Canonical Ltd
28-#
29-# Licensed under the Apache License, Version 2.0 (the "License");
30-# you may not use this file except in compliance with the License.
31-# You may obtain a copy of the License at
32-#
33-# http://www.apache.org/licenses/LICENSE-2.0
34-#
35-# Unless required by applicable law or agreed to in writing, software
36-# distributed under the License is distributed on an "AS IS" BASIS,
37-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38-# See the License for the specific language governing permissions and
39-# limitations under the License.
40+"""Actions module."""
41
42 import os
43 import sys
44
45-sys.path.insert(0, os.path.join(os.environ['CHARM_DIR'], 'lib'))
46-from charmhelpers.core import (
47- hookenv,
48- host,
49-)
50+from charmhelpers.core import hookenv
51
52-from lib_logrotate import LogrotateHelper
53 from lib_cron import CronHelper
54
55+from lib_logrotate import LogrotateHelper
56+
57+sys.path.insert(0, os.path.join(os.environ['CHARM_DIR'], 'lib'))
58+
59 hooks = hookenv.Hooks()
60 logrotate = LogrotateHelper()
61 cron = CronHelper()
62
63+
64 @hooks.hook("update-logrotate-files")
65 def update_logrotate_files():
66+ """Update the logrotate files."""
67 logrotate.read_config()
68 logrotate.modify_configs()
69
70+
71 @hooks.hook("update-cronjob")
72 def update_cronjob():
73+ """Update the cronjob file."""
74 cron.read_config()
75 cron.install_cronjob()
76
77+
78 if __name__ == "__main__":
79+ """Main function."""
80 hooks.execute(sys.argv)
81-
82diff --git a/lib/lib_cron.py b/lib/lib_cron.py
83index 15bcdaf..f560084 100644
84--- a/lib/lib_cron.py
85+++ b/lib/lib_cron.py
86@@ -1,24 +1,27 @@
87+"""Cron helper module."""
88 import os
89-import re
90+
91 from lib_logrotate import LogrotateHelper
92
93+
94 class CronHelper:
95 """Helper class for logrotate charm."""
96
97- @classmethod
98 def __init__(self):
99- """Init function"""
100- self.cronjob_check_paths = [ "hourly", "daily", "weekly", "monthly" ]
101+ """Init function."""
102+ self.cronjob_base_path = "/etc/cron."
103+ self.cronjob_etc_config = "/etc/logrotate_cronjob_config"
104+ self.cronjob_check_paths = ["hourly", "daily", "weekly", "monthly"]
105 self.cronjob_logrotate_cron_file = "charm-logrotate"
106
107-
108- @classmethod
109 def read_config(self):
110- """Config changed/install hooks dumps config out to disk,
111- Here we read that config to update the cronjob"""
112+ """Read the configuration from the file.
113
114- config_file = open("/etc/logrotate_cronjob_config", "r")
115- lines = config_file.read()
116+ Config changed/install hooks dumps config out to disk,
117+ Here we read that config to update the cronjob.
118+ """
119+ config_file = open(self.cronjob_etc_config, "r")
120+ lines = config_file.read()
121 lines = lines.split('\n')
122
123 if lines[0] == 'True':
124@@ -28,51 +31,54 @@ class CronHelper:
125
126 self.cronjob_frequency = int(self.cronjob_check_paths.index(lines[1]))
127
128-
129- @classmethod
130 def install_cronjob(self):
131- """If logrotate-cronjob config option is set to True
132- install cronjob. Otherwise cleanup"""
133+ """Install the cron job task.
134
135+ If logrotate-cronjob config option is set to True install cronjob,
136+ otherwise cleanup.
137+ """
138 clean_up_file = self.cronjob_frequency if self.cronjob_enabled else -1
139
140 if self.cronjob_enabled is True:
141 path_to_lib = os.path.realpath(__file__)
142- cron_file_path = "/etc/cron." + self.cronjob_check_paths[clean_up_file] \
143- + "/" + self.cronjob_logrotate_cron_file
144+ cron_file_path = self.cronjob_base_path\
145+ + self.cronjob_check_paths[clean_up_file]\
146+ + "/" + self.cronjob_logrotate_cron_file
147
148 # upgrade to template if logic increases
149 cron_file = open(cron_file_path, 'w')
150 cron_file.write("#!/bin/sh\n/usr/bin/python3 " + path_to_lib + "\n\n")
151 cron_file.close()
152- os.chmod(cron_file_path,700)
153-
154- self.cleanup_cronjob(clean_up_file)
155+ os.chmod(cron_file_path, 700)
156
157+ self.cleanup_cronjob(clean_up_file)
158
159- @classmethod
160- def cleanup_cronjob(self, frequency = -1):
161- """Cleanup previous config"""
162- for i in range(4):
163- if frequency != i:
164- path = "/etc/cron." + self.cronjob_check_paths[i] + "/" +\
165- self.cronjob_logrotate_cron_file
166+ def cleanup_cronjob(self, frequency=-1):
167+ """Cleanup previous config."""
168+ if frequency == -1:
169+ for check_path in self.cronjob_check_paths:
170+ path = self.cronjob_base_path \
171+ + check_path \
172+ + "/" \
173+ + self.cronjob_logrotate_cron_file
174 if os.path.exists(path):
175 os.remove(path)
176+ if os.path.exists(self.cronjob_etc_config):
177+ os.remove(self.cronjob_etc_config)
178
179- @classmethod
180 def update_logrotate_etc(self):
181- """Run logrotate update config"""
182+ """Run logrotate update config."""
183 logrotate = LogrotateHelper()
184 logrotate.read_config()
185 logrotate.modify_configs()
186
187
188 def main():
189- cronHelper = CronHelper()
190- cronHelper.read_config()
191- cronHelper.update_logrotate_etc()
192- cronHelper.install_cronjob()
193+ """Ran by cron."""
194+ cronhelper = CronHelper()
195+ cronhelper.read_config()
196+ cronhelper.update_logrotate_etc()
197+ cronhelper.install_cronjob()
198
199
200 if __name__ == '__main__':
201diff --git a/lib/lib_logrotate.py b/lib/lib_logrotate.py
202index d823020..67245cb 100644
203--- a/lib/lib_logrotate.py
204+++ b/lib/lib_logrotate.py
205@@ -1,6 +1,8 @@
206+"""Logrotate module."""
207 import os
208 import re
209
210+from charmhelpers.core import hookenv
211
212 LOGROTATE_DIR = "/etc/logrotate.d/"
213
214@@ -8,27 +10,24 @@ LOGROTATE_DIR = "/etc/logrotate.d/"
215 class LogrotateHelper:
216 """Helper class for logrotate charm."""
217
218- @classmethod
219 def __init__(self):
220- """Init function"""
221- pass
222+ """Init function."""
223+ self.retention = hookenv.config('logrotate-retention')
224
225- @classmethod
226 def read_config(self):
227- """Config changed/install hooks dumps config out to disk,
228- Here we read that config to update the cronjob"""
229+ """Read changes from disk.
230
231+ Config changed/install hooks dumps config out to disk,
232+ Here we read that config to update the cronjob
233+ """
234 config_file = open("/etc/logrotate_cronjob_config", "r")
235 lines = config_file.read()
236 lines = lines.split('\n')
237
238 self.retention = int(lines[2])
239
240-
241- @classmethod
242 def modify_configs(self):
243 """Modify the logrotate config files."""
244-
245 for config_file in os.listdir(LOGROTATE_DIR):
246 file_path = LOGROTATE_DIR + config_file
247
248@@ -44,11 +43,8 @@ class LogrotateHelper:
249 logrotate_file.write(mod_contents)
250 logrotate_file.close()
251
252-
253- @classmethod
254 def modify_content(self, content):
255- """Helper function to edit the content of a logrotate file."""
256-
257+ """Edit the content of a logrotate file."""
258 # Split the contents in a logrotate file in separate entries (if
259 # multiple are found in the file) and put in a list for further
260 # processing
261@@ -66,7 +62,7 @@ class LogrotateHelper:
262 # the rotate option to the appropriate value
263 results = []
264 for item in items:
265- count = self.calculate_count(item)
266+ count = self.calculate_count(item, self.retention)
267 rotate = 'rotate {}'.format(count)
268 result = re.sub(r'rotate \d+\.?[0-9]*', rotate, item)
269 results.append(result)
270@@ -75,10 +71,8 @@ class LogrotateHelper:
271
272 return results
273
274- @classmethod
275 def modify_header(self, content):
276- """Helper function to add Juju headers to the file."""
277-
278+ """Add Juju headers to the file."""
279 header = "# Configuration file maintained by Juju. Local changes may be overwritten"
280
281 split = content.split('\n')
282@@ -90,23 +84,22 @@ class LogrotateHelper:
283 return result
284
285 @classmethod
286- def calculate_count(self, item):
287+ def calculate_count(cls, item, retention):
288 """Calculate rotate based on rotation interval. Always round up."""
289-
290 # Fallback to default lowest retention - days
291 # better to keep the logs than lose them
292- count = self.retention
293+ count = retention
294 # Daily 1:1 to configuration retention period (in days)
295 if 'daily' in item:
296- count = self.retention
297+ count = retention
298 # Weekly rounding up, as weeks are 7 days
299 if 'weekly' in item:
300- count = int(round(self.retention/7))
301+ count = int(round(retention/7))
302 # Monthly default 30 days and round up because of 28/31 days months
303 if 'monthly' in item:
304- count = int(round(self.retention/30))
305+ count = int(round(retention/30))
306 # For every 360 days - add 1 year
307 if 'yearly' in item:
308- count = self.retention // 360 + 1 if self.retention > 360 else 1
309+ count = retention // 360 + 1 if retention > 360 else 1
310
311 return count
312diff --git a/reactive/logrotate.py b/reactive/logrotate.py
313index 4a8d5c1..1810707 100644
314--- a/reactive/logrotate.py
315+++ b/reactive/logrotate.py
316@@ -1,36 +1,48 @@
317-from lib_logrotate import LogrotateHelper
318-from lib_cron import CronHelper
319+"""Reactive charm hooks."""
320 from charmhelpers.core import hookenv
321+
322 from charms.reactive import set_flag, when, when_not
323
324+from lib_cron import CronHelper
325+
326+from lib_logrotate import LogrotateHelper
327+
328+
329+hooks = hookenv.Hooks()
330 logrotate = LogrotateHelper()
331 cron = CronHelper()
332
333+
334 @when_not('logrotate.installed')
335 def install_logrotate():
336- dump_config_to_disk();
337+ """Install the logrotate charm."""
338+ dump_config_to_disk()
339 logrotate.read_config()
340 cron.read_config()
341 logrotate.modify_configs()
342 hookenv.status_set('active', 'Unit is ready.')
343 set_flag('logrotate.installed')
344- cron.install_cronjob();
345+ cron.install_cronjob()
346+
347
348 @when('config.changed')
349 def config_changed():
350+ """Run when configuration changes."""
351 dump_config_to_disk()
352 cron.read_config()
353 logrotate.read_config()
354 hookenv.status_set('maintenance', 'Modifying configs.')
355 logrotate.modify_configs()
356 hookenv.status_set('active', 'Unit is ready.')
357- cron.install_cronjob();
358+ cron.install_cronjob()
359+
360
361 def dump_config_to_disk():
362+ """Dump configurations to disk."""
363 cronjob_enabled = hookenv.config('logrotate-cronjob')
364 cronjob_frequency = hookenv.config('logrotate-cronjob-frequency')
365 logrotate_retention = hookenv.config('logrotate-retention')
366 with open('/etc/logrotate_cronjob_config', 'w+') as cronjob_config_file:
367- cronjob_config_file.write(str(cronjob_enabled) + '\n')
368- cronjob_config_file.write(str(cronjob_frequency) + '\n')
369- cronjob_config_file.write(str(logrotate_retention) + '\n')
370+ cronjob_config_file.write(str(cronjob_enabled) + '\n')
371+ cronjob_config_file.write(str(cronjob_frequency) + '\n')
372+ cronjob_config_file.write(str(logrotate_retention) + '\n')
373diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
374index 56925ff..1b22cb8 100644
375--- a/tests/functional/conftest.py
376+++ b/tests/functional/conftest.py
377@@ -1,28 +1,32 @@
378 #!/usr/bin/python3
379-'''
380-Reusable pytest fixtures for functional testing
381+"""
382+Reusable pytest fixtures for functional testing.
383
384 Environment variables
385 ---------------------
386
387 test_preserve_model:
388 if set, the testing model won't be torn down at the end of the testing session
389-'''
390+"""
391
392 import asyncio
393 import os
394-import uuid
395-import pytest
396 import subprocess
397+import uuid
398
399 from juju.controller import Controller
400+
401 from juju_tools import JujuTools
402
403+import pytest
404+
405
406 @pytest.fixture(scope='module')
407 def event_loop():
408- '''Override the default pytest event loop to allow for fixtures using a
409- broader scope'''
410+ """Override the default pytest event loop.
411+
412+ Do this too allow for fixtures using a broader scope.
413+ """
414 loop = asyncio.get_event_loop_policy().new_event_loop()
415 asyncio.set_event_loop(loop)
416 loop.set_debug(True)
417@@ -33,7 +37,7 @@ def event_loop():
418
419 @pytest.fixture(scope='module')
420 async def controller():
421- '''Connect to the current controller'''
422+ """Connect to the current controller."""
423 _controller = Controller()
424 await _controller.connect_current()
425 yield _controller
426@@ -42,7 +46,7 @@ async def controller():
427
428 @pytest.fixture(scope='module')
429 async def model(controller):
430- '''This model lives only for the duration of the test'''
431+ """Live only for the duration of the test."""
432 model_name = "functest-{}".format(str(uuid.uuid4())[-12:])
433 _model = await controller.add_model(model_name,
434 cloud_name=os.getenv('PYTEST_CLOUD_NAME'),
435@@ -62,5 +66,6 @@ async def model(controller):
436
437 @pytest.fixture(scope='module')
438 async def jujutools(controller, model):
439+ """Juju tools."""
440 tools = JujuTools(controller, model)
441 return tools
442diff --git a/tests/functional/juju_tools.py b/tests/functional/juju_tools.py
443index 2fd501d..5e4a1cc 100644
444--- a/tests/functional/juju_tools.py
445+++ b/tests/functional/juju_tools.py
446@@ -1,22 +1,26 @@
447+"""Juju tools."""
448+import base64
449 import pickle
450+
451 import juju
452-import base64
453
454 # from juju.errors import JujuError
455
456
457 class JujuTools:
458+ """Juju tools."""
459+
460 def __init__(self, controller, model):
461+ """Init."""
462 self.controller = controller
463 self.model = model
464
465 async def run_command(self, cmd, target):
466- '''
467- Runs a command on a unit.
468+ """Run a command on a unit.
469
470 :param cmd: Command to be run
471 :param unit: Unit object or unit name string
472- '''
473+ """
474 unit = (
475 target
476 if isinstance(target, juju.unit.Unit)
477@@ -26,13 +30,12 @@ class JujuTools:
478 return action.results
479
480 async def remote_object(self, imports, remote_cmd, target):
481- '''
482- Runs command on target machine and returns a python object of the result
483+ """Run command on target machine and returns a python object of the result.
484
485 :param imports: Imports needed for the command to run
486 :param remote_cmd: The python command to execute
487 :param target: Unit object or unit name string
488- '''
489+ """
490 python3 = "python3 -c '{}'"
491 python_cmd = ('import pickle;'
492 'import base64;'
493@@ -44,12 +47,11 @@ class JujuTools:
494 return pickle.loads(base64.b64decode(bytes(results['Stdout'][2:-1], 'utf8')))
495
496 async def file_stat(self, path, target):
497- '''
498- Runs stat on a file
499+ """Run stat on a file.
500
501 :param path: File path
502 :param target: Unit object or unit name string
503- '''
504+ """
505 imports = 'import os;'
506 python_cmd = ('os.stat("{}")'
507 .format(path))
508@@ -57,12 +59,11 @@ class JujuTools:
509 return await self.remote_object(imports, python_cmd, target)
510
511 async def file_contents(self, path, target):
512- '''
513- Returns the contents of a file
514+ """Return the contents of a file.
515
516 :param path: File path
517 :param target: Unit object or unit name string
518- '''
519+ """
520 cmd = 'cat {}'.format(path)
521 result = await self.run_command(cmd, target)
522 return result['Stdout']
523diff --git a/tests/functional/test_logrotate.py b/tests/functional/test_logrotate.py
524index b4402be..100690f 100644
525--- a/tests/functional/test_logrotate.py
526+++ b/tests/functional/test_logrotate.py
527@@ -1,6 +1,8 @@
528 #!/usr/bin/python3.6
529+"""Main module for functional testing."""
530
531 import os
532+
533 import pytest
534
535 pytestmark = pytest.mark.asyncio
536@@ -16,7 +18,7 @@ SERIES = ['xenial',
537 @pytest.fixture(scope='module',
538 params=SERIES)
539 async def deploy_app(request, model):
540- '''Deploys the logrotate charm as a subordinate of ubuntu'''
541+ """Deploy the logrotate charm as a subordinate of ubuntu."""
542 release = request.param
543
544 await model.deploy(
545@@ -42,7 +44,7 @@ async def deploy_app(request, model):
546
547 @pytest.fixture(scope='module')
548 async def unit(deploy_app):
549- '''Returns the logrotate unit we've deployed'''
550+ """Return the logrotate unit we've deployed."""
551 return deploy_app.units.pop()
552
553 #########
554@@ -51,4 +53,5 @@ async def unit(deploy_app):
555
556
557 async def test_deploy(deploy_app):
558+ """Tst the deployment."""
559 assert deploy_app.status == 'active'
560diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
561index 534e654..8520709 100644
562--- a/tests/unit/conftest.py
563+++ b/tests/unit/conftest.py
564@@ -1,11 +1,15 @@
565 #!/usr/bin/python3
566+"""Configurations for tests."""
567+
568 import mock
569+
570 import pytest
571
572 # If layer options are used, add this to ${fixture}
573 # and import layer in logrotate
574 @pytest.fixture
575 def mock_layers(monkeypatch):
576+ """Layers mock."""
577 import sys
578 sys.modules['charms.layer'] = mock.Mock()
579 sys.modules['reactive'] = mock.Mock()
580@@ -21,8 +25,10 @@ def mock_layers(monkeypatch):
581
582 monkeypatch.setattr('lib_logrotate.layer.options', options)
583
584+
585 @pytest.fixture
586 def mock_hookenv_config(monkeypatch):
587+ """Hookenv mock."""
588 import yaml
589
590 def mock_config():
591@@ -39,17 +45,22 @@ def mock_hookenv_config(monkeypatch):
592
593 monkeypatch.setattr('lib_logrotate.hookenv.config', mock_config)
594
595+
596 @pytest.fixture
597 def mock_remote_unit(monkeypatch):
598+ """Remote unit mock."""
599 monkeypatch.setattr('lib_logrotate.hookenv.remote_unit', lambda: 'unit-mock/0')
600
601
602 @pytest.fixture
603 def mock_charm_dir(monkeypatch):
604+ """Charm dir mock."""
605 monkeypatch.setattr('lib_logrotate.hookenv.charm_dir', lambda: '/mock/charm/dir')
606
607+
608 @pytest.fixture
609 def logrotate(tmpdir, mock_hookenv_config, mock_charm_dir, monkeypatch):
610+ """Logrotate fixture."""
611 from lib_logrotate import LogrotateHelper
612 helper = LogrotateHelper
613
614diff --git a/tests/unit/test_logrotate.py b/tests/unit/test_logrotate.py
615index 1f0ed9b..b01c7b5 100644
616--- a/tests/unit/test_logrotate.py
617+++ b/tests/unit/test_logrotate.py
618@@ -1,37 +1,45 @@
619-from unittest.mock import patch
620+"""Main unit test module."""
621+
622
623 class TestLogrotateHelper():
624+ """Main test class."""
625+
626 def test_pytest(self):
627+ """Simple pytest."""
628 assert True
629
630-
631 def test_daily_retention_count(self, logrotate):
632+ """Test daily retention count."""
633 logrotate.retention = 90
634 contents = '/var/log/some.log {\n rotate 123\n daily\n}'
635- count = logrotate.calculate_count(contents)
636+ count = logrotate.calculate_count(contents, logrotate.retention)
637 assert count == 90
638
639 def test_weekly_retention_count(self, logrotate):
640+ """Test weekly retention count."""
641 logrotate.retention = 21
642 contents = '/var/log/some.log {\n rotate 123\n weekly\n}'
643- count = logrotate.calculate_count(contents)
644+ count = logrotate.calculate_count(contents, logrotate.retention)
645 assert count == 3
646
647 def test_monthly_retention_count(self, logrotate):
648+ """Test monthly retention count."""
649 logrotate.retention = 60
650 contents = '/var/log/some.log {\n rotate 123\n monthly\n}'
651- count = logrotate.calculate_count(contents)
652+ count = logrotate.calculate_count(contents, logrotate.retention)
653 assert count == 2
654
655 def test_yearly_retention_count(self, logrotate):
656+ """Test yearly retention count."""
657 logrotate.retention = 180
658 contents = '/var/log/some.log {\n rotate 123\n yearly\n}'
659- count = logrotate.calculate_count(contents)
660+ count = logrotate.calculate_count(contents, logrotate.retention)
661 assert count == 1
662
663 def test_modify_content(self, logrotate):
664+ """Test the modify_content method."""
665 logrotate.retention = 42
666- contents = '/var/log/some.log {\n rotate 123\n daily\n}\n/var/log/other.log {\n rotate 456\n weekly\n}'
667- mod_contents = logrotate.modify_content(contents)
668- expected_contents = '/var/log/some.log {\n rotate 42\n daily\n}\n\n/var/log/other.log {\n rotate 6\n weekly\n}\n'
669+ contents = '/log/some.log {\n rotate 123\n daily\n}\n/log/other.log {\n rotate 456\n weekly\n}'
670+ mod_contents = logrotate.modify_content(logrotate, contents)
671+ expected_contents = '/log/some.log {\n rotate 42\n daily\n}\n\n/log/other.log {\n rotate 6\n weekly\n}\n'
672 assert mod_contents == expected_contents

Subscribers

People subscribed via source and target branches

to all changes: