CommitFiles
This script gets content files as input from the context, commits the files in the correct folder and creates the pull request text.
python · XSOAR CI/CD
Source
import io import os from contextlib import redirect_stderr, redirect_stdout from os.path import exists from pathlib import Path import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from demisto_sdk.commands.common.constants import ENTITY_TYPE_TO_DIR from demisto_sdk.commands.common.tools import find_type from demisto_sdk.commands.split.ymlsplitter import YmlSplitter PR_TEMPLATE = ( "### Pull Request created in Cortex XSOAR\n" "**Created by:** {}\n" "**Pack:** {}\n" "**Branch:** {}\n" "**Link to incident in Cortex XSOAR:** {}\n\n" "{}\n\n" "---" ) file_path_to_sha: dict[str, str] = {} files_path = [] class ContentFile: # pragma: no cover path_to_file: str = "" file_name: str = "" content_type: str = "" file_text: str = "" entry_id: str = "" def __init__(self, pack_name=None, file=None): if not pack_name and not file: return # read the file from entry id file_object = demisto.getFilePath(file["EntryID"]) with open(file_object["path"]) as f: file_contents = f.read() self.file_text = file_contents self.file_name = file["Name"] self.entry_id = file["EntryID"] if self.file_name == "metadata.json": self.file_name = "pack_metadata.json" self.path_to_file = os.path.join("Packs", pack_name) self.content_type = "metadata" else: with open(file_object["name"], "w") as f: f.write(file_contents) file_type = find_type(file_object["name"]) os.remove(file_object["name"]) self.content_type = file_type.value if file_type else file_type folder = ENTITY_TYPE_TO_DIR.get(self.content_type, "") self.path_to_file = os.path.join("Packs", pack_name, folder) def get_file_sha(branch_name: str, content_file: ContentFile, get_files_command: str): full_path = os.path.join(content_file.path_to_file, content_file.file_name) sha = file_path_to_sha.get(full_path) if sha: return sha # try to get the file from branch status, list_files_res = execute_command( get_files_command, {"branch": branch_name, "path": content_file.path_to_file}, fail_on_error=False ) if status: for file in list_files_res: file_path_to_sha[file["path"]] = file["sha"] return file_path_to_sha.get(full_path) def escape_placeholders(content: str) -> str: """ Escapes XSOAR-style placeholders ${somecontext.value} to prevent context substitution. Args: content (str): The input string containing placeholders. Returns: str: The string with placeholders escaped. """ return re.sub(r"\$(\{[^}]+})", r"$\\\1", content) # Prepend a backslash to preserve the placeholder def commit_content_item( branch_name: str, content_file: ContentFile, new_files: List, modified_files: List, keep_placeholders_in_files: bool = False ): commit_args = { "commit_message": f"Added {content_file.file_name}", "path_to_file": f"{content_file.path_to_file}/{content_file.file_name}", "branch_name": branch_name, "file_text": content_file.file_text, } file_sha = get_file_sha(branch_name, content_file, "Github-list-files") # dont commit pack_metadata.json if already exists in the branch if file_sha and content_file.file_name == "pack_metadata.json": return elif file_sha: # update existing file commit_args["file_sha"] = file_sha commit_args["commit_message"] = f"Updated {content_file.file_name}" modified_files.append(content_file.file_name) else: # new file added if keep_placeholders_in_files: demisto.debug(f"Got {keep_placeholders_in_files=}, escaping placeholders with \\") commit_args["file_text"] = escape_placeholders(content_file.file_text) commit_args["placeholders_escaped"] = "True" new_files.append(content_file.file_name) status, commit_res = execute_command("Github-commit-file", commit_args, fail_on_error=False) if not status: raise DemistoException(commit_res) def commit_content_item_gitlab(branch_name: str, content_file: ContentFile, new_files: List, modified_files: List): commit_args = { "commit_message": f"Added {content_file.file_name}", "file_path": f"{content_file.path_to_file}/{content_file.file_name}", "branch": branch_name, "file_content": content_file.file_text, } status, commit_res = execute_command("gitlab-file-create", commit_args, fail_on_error=False) if isinstance(commit_res, dict): new_files.append(content_file.file_name) elif isinstance(commit_res, str) and "already exists" in commit_res: demisto.debug(f"The file {content_file.file_name} already exist, running update command") if content_file.file_name == "pack_metadata.json": return commit_args["commit_message"] = f"Updated {content_file.file_name}" modified_files.append(content_file.file_name) status, commit_res = execute_command("gitlab-file-update", commit_args, fail_on_error=False) if not status: raise DemistoException(commit_res) def does_file_exist(branch_name: str, content_file: ContentFile) -> bool: full_path = os.path.join(content_file.path_to_file, content_file.file_name) if full_path in files_path: return True # try to get the file from branch status, list_commit_res = execute_command( "bitbucket-commit-list", {"included_branches": branch_name, "file_path": full_path}, fail_on_error=False ) if list_commit_res and len(list_commit_res) > 0: files_path.append(full_path) return True return False def searched_file_path(branch_name: str, content_file: ContentFile) -> bool: full_path = Path(content_file.path_to_file, content_file.file_name) if str(full_path) in files_path: # the files list, check if the file already exists in the list return True # try to get the file from branch response = execute_command( "azure-devops-file-list", args={"branch_name": branch_name.split("/")[-1], "recursion_level": "Full"} ) files_set = {file["path"] for file in response.get("value", [])} for file in files_set: if str(full_path) in file: files_path.append(str(full_path)) return True return False def commit_content_item_bitbucket(branch_name: str, content_file: ContentFile, new_files: List, modified_files: List): commit_args = { "message": f"Added {content_file.file_name}", "file_name": f"{content_file.path_to_file}/{content_file.file_name}", "branch": f"{branch_name}", "file_content": f"{content_file.file_text}", } file_status = does_file_exist(branch_name, content_file) # dont commit pack_metadata.json if already exists in the branch if file_status and content_file.file_name == "pack_metadata.json": return elif file_status: # update existing file commit_args["message"] = f"Updated {content_file.file_name}" modified_files.append(content_file.file_name) else: # new file added new_files.append(content_file.file_name) try: demisto.executeCommand("bitbucket-commit-create", args=commit_args) except DemistoException as e: raise DemistoException(f"Failed to execute bitbucket-commit-create command. Error: {e}, {traceback.format_exc()}") def commit_content_item_azure_devops(branch_name: str, content_file: ContentFile, new_files: List, modified_files: List): file_args = { "commit_comment": f"{content_file.file_name} was added.", "file_path": f"{content_file.path_to_file}/{content_file.file_name}", "branch_name": f"{branch_name}", "file_content": f"{content_file.file_text}", } file_exists = searched_file_path(branch_name, content_file) # don't commit pack_metadata.json if already exists in the branch if file_exists and content_file.file_name == "pack_metadata.json": return response = demisto.executeCommand("azure-devops-branch-list", args={}) branches_list = response[0].get("Contents", {}).get("value", []) if response and isinstance(response, list) else [] for branch in branches_list: if branch["name"] == branch_name: branch_id = branch["objectId"] break else: raise DemistoException("Failed to find a corresponding branch id to the given branch name.") file_args["branch_id"] = branch_id if file_exists: # update existing file file_args["commit_comment"] = f"{content_file.file_name} was updated." modified_files.append(content_file.file_name) command_to_execute = "azure-devops-file-update" else: # new file added new_files.append(content_file.file_name) command_to_execute = "azure-devops-file-create" try: demisto.executeCommand(command_to_execute, args=file_args) except DemistoException as e: raise DemistoException( f"Failed to execute azure-devops-file-update or azure-devops-file-create commands. Error: {e}, " ) from e def split_yml_file(content_file: ContentFile): # pragma: no cover content_files = [] if content_file.content_type == "script": base_name = content_file.file_name.replace("automation-", "").replace(".yml", "") else: base_name = content_file.file_name.replace("integration-", "").replace(".yml", "") # create the yml file from entry id with open(content_file.file_name, "w") as f: f.write(content_file.file_text) output_capture = io.StringIO() # split the yml file yml_splitter = YmlSplitter( content_file.file_name, base_name=base_name, output=base_name, # pylint: disable=E1123 file_type=content_file.content_type, no_pipenv=True, no_code_formatting=True, no_logging=True, no_readme=True, ) script_type = yml_splitter.yml_data.get("type") if not script_type: script_type = yml_splitter.yml_data.get("script", {}).get("type") if script_type == "python": script_extention = "py" elif script_type == "javascript": script_extention = "js" elif script_type == "powershell": script_extention = "ps1" else: script_extention = "" demisto.debug(f"{script_type=} didn't match any condition. {script_extention=}") with redirect_stdout(output_capture), redirect_stderr(output_capture): yml_splitter.extract_to_package_format() yml_file_path = f"{base_name}/{base_name}.yml" script_file_path = f"{base_name}/{base_name}.{script_extention}" path_to_file = os.path.join(content_file.path_to_file, base_name) # read the py and yml files content with open(yml_file_path) as f: yml_txt = f.read() with open(script_file_path) as f: script_txt = f.read() # create the yml file yml_file = ContentFile() yml_file.file_text = yml_txt yml_file.file_name = f"{base_name}.yml" yml_file.path_to_file = path_to_file content_files.append(yml_file) # create the script file script_file = ContentFile() script_file.file_text = script_txt script_file.file_name = f"{base_name}.{script_extention}" script_file.path_to_file = path_to_file content_files.append(script_file) # create the description file description_file_path = f"{base_name}/{base_name}_description.md" if exists(description_file_path): with open(description_file_path) as f: description_txt = f.read() description_file = ContentFile() description_file.file_text = description_txt description_file.file_name = f"{base_name}_description.md" description_file.path_to_file = path_to_file content_files.append(description_file) return content_files def commit_git( git_integration: str, branch_name: str, content_file: ContentFile, new_files: List, modified_files: List, keep_placeholders_in_files: bool, ): if git_integration == "Gitlab": commit_content_item_gitlab(branch_name, content_file, new_files, modified_files) elif git_integration == "GitHub": commit_content_item(branch_name, content_file, new_files, modified_files, keep_placeholders_in_files) elif git_integration == "Bitbucket": commit_content_item_bitbucket(branch_name, content_file, new_files, modified_files) elif git_integration == "AzureDevOps": commit_content_item_azure_devops(branch_name, content_file, new_files, modified_files) else: raise DemistoException(f"Unexpected {git_integration=}. Possible values: Gitlab, GitHub, Bitbucket and AzureDevOps.") """ MAIN FUNCTION """ def main(): # pragma: no cover try: files = demisto.getArg("files") branch_name = demisto.getArg("branch") pack_name = demisto.getArg("pack") user = demisto.getArg("user") comment = demisto.getArg("comment") template = demisto.getArg("template") git_integration = demisto.getArg("git_integration") keep_placeholders_in_files = demisto.getArg("cicd_keep_placeholders_in_files") or False new_files: List[str] = [] modified_files: List[str] = [] if not template: template = PR_TEMPLATE if not comment: comment = "" username = user.get("username") if user.get("email"): username = f'{username} ({user.get("email")})' # commit the files from the input for file in files: if file.get("Unzipped"): continue # create ContentFile item content_file = ContentFile(pack_name=pack_name, file=file) if content_file.content_type in ("script", "integration"): # split automation file to yml and script files content_files = split_yml_file(content_file) for file_to_commit in content_files: commit_git( git_integration, branch_name, file_to_commit, new_files, modified_files, keep_placeholders_in_files ) else: commit_git(git_integration, branch_name, content_file, new_files, modified_files, keep_placeholders_in_files) incident_url = demisto.demistoUrls().get("investigation") # create the PR text pr_body = template.format(f"{username}\n", f"{pack_name}\n", f"{branch_name}\n", f"{incident_url}\n", f"{comment}\n") if new_files: pr_body = f"{pr_body}\n\n### New files\n- " pr_body = pr_body + "\n- ".join(new_files) if modified_files: pr_body = f"{pr_body}\n\n### Modified files\n- " pr_body = pr_body + "\n- ".join(modified_files) return_results(CommandResults(readable_output=pr_body, outputs_prefix="PR_text", outputs=pr_body)) except Exception as ex: return_error(f"Failed to execute CommitFiles script. Error: {ex}", error=ex) """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()
README
This script gets content files as input from the context, commits the files in the correct folder and creates the pull request text.
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Cortex XSOAR Version | 6.0.0 |
Used In
This script is used in the following playbooks and scripts.
- Pull Request Creation - Github
- Pull Request Creation - Bitbucket
Inputs
| Argument Name | Description |
|---|---|
| files | The files to commit. |
| branch | The branch name to commit. |
| git_integration | Which version control integration to use. |
| pack | The name of the pack. |
| user | The current user details from the command “getUsers”. |
| comment | Short description to add to the pull request text. |
| template | Pull request description template. |
Outputs
| Path | Description | Type |
|---|---|---|
| PR_text | The pull request text. | string |