1 """Automate the INFO.yaml update."""
3 Copyright 2021 Deutsche Telekom AG
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
19 from itertools import chain, zip_longest
20 from pathlib import Path
21 from typing import Dict, Iterator, List, Optional, Tuple
25 from ruamel.yaml import YAML
26 from ruamel.yaml.scalarstring import SingleQuotedScalarString
29 class CommitterActions(Enum):
30 """Committer Actions enum.
33 * Addition - will add the commiter with their info into
34 the committers list and the tsc information would be added
35 * Deletion - commiter will be deleted from the committers list
36 and the tsc information would be added
44 class CommitterChange:
45 """Class representing the change on the committers list which needs to be done."""
50 action: CommitterActions,
54 committer_id: str = None,
57 """Initialize the change object.
60 name (str): Committer name
61 action (CommitterActions): Action to be done
62 link (str): Link to the TSC confirmation
63 email (str, optional): Committer's e-mail. Needed only for addition.
65 company (str, optional): Committer's company name. Needed only for addition.
67 committer_id (str, optional): Committer's LF ID. Needed only for addition.
69 timezone (str, optional): Committer's timezone. Needed only for addition.
72 self._committer_name: str = name
73 self._action: CommitterActions = action
74 self._link: str = link
77 def action(self) -> CommitterActions:
78 """Enum representing an action which is going to be done by the change.
81 CommitterActions: One of the CommittersActions enum value.
87 def committer_name(self) -> str:
88 """Committer name property.
91 str: Name provided during the initialization.
94 return self._committer_name
97 def tsc_change(self) -> Dict[str, str]:
100 Dictionary which is going to be added into
101 INFO.yaml file 'tsc' section.
102 Values are different for Addition and Deletion
106 Dict[str, str]: TSC section entry
109 # Start ignoring PyLintBear
111 case CommitterActions.ADDITION:
112 return self.tsc_change_addition
113 case CommitterActions.DELETION:
114 return self.tsc_change_deletion
118 def tsc_change_addition(self) -> Dict[str, str]:
119 """Addition tsc section entry value.
121 Value which is going to be added into 'tsc' section
124 NotImplementedError: That method is not implemented yet
127 Dict[str, str]: TSC section value
129 raise NotImplementedError
132 def tsc_change_deletion(self) -> Dict[str, str]:
133 """Addition tsc section entry value.
135 Value which is going to be added into 'tsc' section
138 Dict[str, str]: TSC section value
141 "type": self.action.value,
142 "name": self.committer_name,
148 """YAML config class which corresponds the configuration YAML file needed to be provided by the user.
150 Required YAML config structure:
153 repos: # List of the repositories which are going to be udated.
154 # That tool is not smart enough to resolve some conflicts etc.
155 # Please be sure that it would be possible to push the change to the gerrit.
156 # Remember that commit-msg hook should be executed so add that script into .git/hooks dir
157 - path: abs_path_to_the_repo # Local path to the repository
158 branch: master # Branch which needs to be udated
159 committers: # List of the committers which are going to be edited
160 - name: Committer Name # The name of the committer which we would delete or add
161 action: Deletion|Addition # Addition or deletion action
162 link: https://link.to.the.tcs.confirmation # Link to the ONAP TSC action confirmation
163 commit: # Configure the commit message
164 message: # List of the commit message lines. That's optional
165 - "[INTEGRATION] My awesome first line!"
166 - "Even better second one!"
167 issue_id: INT-2008 # ONAP's JIRA Issue ID is required in the commit message
170 def __init__(self, yaml_file_path: Path) -> None:
171 """Initialize yaml config object.
174 yaml_file_path (Path): Path to the config file provided by the user
177 with yaml_file_path.open("r") as yaml_file:
178 self._yaml = YAML().load(yaml_file.read())
181 def repos_data(self) -> Iterator[Tuple[Path, str]]:
182 """Repositories information iterator.
184 Returns the generator with the tuples on which:
185 * first element is a path to the repo
186 * second element is a branch name which
187 is going to be used to prepare a change
191 Iterator[Tuple[Path, str]]: Tuples of repository data: repo local abs path and branch name
194 for repo_info in self._yaml["repos"]:
195 yield (Path(repo_info["path"]), repo_info["branch"])
198 def committers_changes(self) -> Iterator[CommitterChange]:
199 """Committer changes iterator.
201 Returns the generator with `CommitterChange` class instances
204 Iterator[CommitterChange]: Committer changes generator
207 for committer_change in self._yaml["committers"]:
208 # Start ignoring PyLintBear
209 match action := CommitterActions(committer_change["action"]):
210 case CommitterActions.ADDITION:
211 continue # TODO: Add addition support
212 case CommitterActions.DELETION:
213 yield CommitterChange(
214 name=committer_change["name"],
216 link=committer_change["link"],
221 def issue_id(self) -> str:
222 """Onap's Jira issue id.
224 That issue id would be used in the commit message.
227 str: ONAP's Jira issue ID
230 return self._yaml["commit"]["issue_id"]
233 def commit_msg(self) -> Optional[List[str]]:
234 """Commit message lines list.
236 Optional, if user didn't provide it in the config file
240 Optional[List[str]]: List of the commit message lines or None
243 return self._yaml["commit"].get("message")
247 """ONAP repo class."""
249 def __init__(self, git_repo_path: Path, git_repo_branch: str) -> None:
250 """Initialize the Onap repo class object.
252 During that method an attempt will be made to change the branch to the one specified by the user.
255 git_repo_path (Path): Repository local abstract path
256 git_repo_branch (str): Branch name
259 ValueError: Branch provided by the user doesn't exist
262 self._repo: git.Repo = git.Repo(git_repo_path)
263 self._branch: str = git_repo_branch
264 if self._repo.head.ref.name != self._branch:
265 for branch in self._repo.branches:
266 if branch.name == self._branch:
271 f"Branch {self._branch} doesn't exist in {self._repo.working_dir} repo"
275 def git(self) -> git.Repo:
276 """Git repository object.
279 git.Repo: Repository object.
285 def info_file_path_abs(self) -> Path:
286 """Absolute path to the repositories INFO.yaml file.
288 Concanenated repository working tree directory and INFO.yaml
291 Path: Repositories INFO.yaml file abs path
294 return Path(self._repo.working_tree_dir, "INFO.yaml")
296 def push_the_change(self, issue_id: str, commit_msg: List[str] = None) -> None:
297 """Push the change to the repository.
299 INFO.yaml file will be added to index and then the commit message has to be created.
300 If used doesn't provide commit message in the config file the default one will be used.
301 Commit command will look:
302 `git commit -m <First line> -m <Second line> ... -m <Last line> -m Issue-ID: <issue ID> -s`
304 `git push origin HEAD:refs/for/<branch defined by user>`
307 issue_id (str): ONAP's Jira issue ID
308 commit_msg (List[str], optional): Commit message lines. Defaults to None.
311 index = self.git.index
312 index.add(["INFO.yaml"])
314 commit_msg = ["Edit INFO.yaml file."]
315 commit_msg_with_m = list(
316 chain.from_iterable(zip_longest([], commit_msg, fillvalue="-m"))
318 self.git.git.execute(
324 "That change was done by automated integration tool to maintain commiters list in INFO.yaml",
326 f"Issue-ID: {issue_id}",
330 self.git.git.execute(["git", "push", "origin", f"HEAD:refs/for/{self._branch}"])
331 print(f"Pushed successfully to {self._repo} respository")
334 class InfoYamlLoader(YAML):
335 """Yaml loader class.
337 Contains the options which are same as used in the INFO.yaml file.
338 After making changes and save INFO.yaml file would have same format as before.
339 Several options are set:
341 * sequence dash indent - 4
342 * sequence item indent - 6
343 * explicit start (triple dashes at the file beginning '---')
344 * preserve quotes - keep the quotes for all strings loaded from the file.
345 It doesn't mean that all new strings would also have quotas.
346 To make new strings be stored with quotas ruamel.yaml.scalarstring.SingleQuotedScalarString
347 class needs to be used.
350 def __init__(self, *args, **kwargs) -> None:
351 """Initialize loader object."""
352 super().__init__(*args, **kwargs)
353 self.preserve_quotes = True
355 self.sequence_dash_offset = 4
356 self.sequence_indent = 6
357 self.explicit_start = True
361 """Class to store information about INFO.yaml file.
363 It's context manager class, so it's possible to use it by
365 with InfoTamlFile(Path(...)) as info_file:
368 It's recommended because at the end all changes are going to be
369 saved on the same path as provided by the user (INFO.yaml will
374 def __init__(self, info_yaml_file_path: Path) -> None:
375 """Initialize the object.
378 info_yaml_file_path (Path): Path to the INFO.yaml file
381 self._info_yaml_file_path: Path = info_yaml_file_path
382 self._yml = InfoYamlLoader()
383 with info_yaml_file_path.open("r") as info:
384 self._info = self._yml.load(info.read())
387 """Enter context manager."""
390 def __exit__(self, *_):
391 """Exit context manager.
393 File is going to be saved now.
396 with self._info_yaml_file_path.open("w") as info:
397 self._yml.dump(self._info, info)
399 def perform_committer_change(self, committer_change: CommitterChange) -> None:
400 """Perform the committer change action.
402 Depends on the action change the right method is going to be executed:
403 * delete_committer for Deletion.
404 For the addition action ValueError exception is going to be raised as
405 it's not supported yet
408 committer_change (CommitterChange): Committer change object
411 ValueError: Addition action called - not supported yet
414 match committer_change.action:
415 case CommitterActions.ADDITION:
416 raise ValueError("Addition action not supported")
417 case CommitterActions.DELETION:
418 self.delete_committer(committer_change.committer_name)
419 self.add_tsc_change(committer_change)
421 def delete_committer(self, name: str) -> None:
422 """Delete commiter action execution.
424 Based on the name commiter is going to be removed from the INFO.yaml 'committers' section.
427 name (str): Committer name to delete.
430 ValueError: Committer not found on the list
433 for index, committer in enumerate(self._info["committers"]):
434 if committer["name"] == name:
435 del self._info["committers"][index]
437 raise ValueError(f"Committer {name} is not on the committer list")
439 def add_tsc_change(self, committer_change: CommitterChange) -> None:
440 """Add Technical Steering Committee entry.
442 All actions need to be confirmed by the TSC. That entry proves that
443 TSC was informed and approved the change.
446 committer_change (CommitterChange): Committer change object.
449 self._info["tsc"]["changes"].append(
451 key: SingleQuotedScalarString(value)
452 for key, value in committer_change.tsc_change.items()
459 "--changes_yaml_file_path",
460 "changes_yaml_file_path",
462 type=click.Path(exists=True),
463 help="Path to the file where chages are described",
465 def update_infos(changes_yaml_file_path):
467 yaml_config = YamlConfig(Path(changes_yaml_file_path))
468 for repo, branch in yaml_config.repos_data:
469 onap_repo = OnapRepo(repo, branch)
470 with InfoYamlFile(onap_repo.info_file_path_abs) as info:
471 for committer_change in yaml_config.committers_changes:
472 info.perform_committer_change(committer_change)
473 onap_repo.push_the_change(yaml_config.issue_id, yaml_config.commit_msg)
476 if __name__ == "__main__":