diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 518e6ec747..df6e47c5c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,3 +45,6 @@ repos: - id: mypy exclude: tests/.*|demisto_sdk/commands/init/templates/.* language: system +- repo: local + hooks: + - id: generate-docs diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 8c0385ed29..2bdbba1961 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -96,3 +96,10 @@ entry: prevent-mypy-global-ignore language: python files: .*Packs/.*/(?:Integrations|Scripts)/.*.py$ + +- id: generate-docs + name: Generate Documentation for Changed Commands + description: Generates documentation for commands when a _setup.py file is modified. + entry: python pre_commit_generate_docs.py + language: python + files: ^.*_setup\.py$ diff --git a/demisto_sdk/scripts/generate_commands_docs/__init__.py b/demisto_sdk/scripts/generate_commands_docs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/demisto_sdk/scripts/generate_commands_docs/generate_commands_docs.py b/demisto_sdk/scripts/generate_commands_docs/generate_commands_docs.py new file mode 100644 index 0000000000..751d64d831 --- /dev/null +++ b/demisto_sdk/scripts/generate_commands_docs/generate_commands_docs.py @@ -0,0 +1,131 @@ +import inspect +import sys +from pathlib import Path +from typing import List + +from typer.main import get_command + +from demisto_sdk.__main__ import app + + +def extract_changed_commands(modified_files: List[str]) -> List[str]: + """Extract the command names from the list of modified files.""" + changed_commands = [] + for file in modified_files: + # Check if the modified file ends with '_setup.py' + if file.endswith("_setup.py"): + command_name = Path(file).stem.replace("_setup", "") + changed_commands.append(command_name) + return changed_commands + + +def get_sdk_command(command_name: str): + click_app = get_command(app) + command = click_app.commands.get(command_name) # type: ignore[attr-defined] + + if command is None: + return f"No README found for command: {command_name}" + return command + + +def get_command_description(command_name: str) -> str: + """Retrieve the description (docstring) for the command.""" + command = get_sdk_command(command_name) + if isinstance(command, str): + return command + + command_func = command.callback + return inspect.getdoc(command_func) or "No description provided" + + +def get_command_options(command_name: str) -> str: + """Generate the options section for the command.""" + command = get_sdk_command(command_name) + if isinstance(command, str): + return command + options_text = "### Options\n\n" + for param in command.params: + param_name = ( + f"--{param.name.replace('_', '-')}" + if param.param_type_name == "option" + else param.name + ) + options_text += ( + f"- **{param_name}**: {param.help or 'No description provided'}\n" + ) + if param.default is not None: + options_text += f" - Default: `{param.default}`\n" + options_text += "\n" + return options_text + + +def update_readme(command_name: str, description: str, options: str): + """Update the README.md file for the command with the given description and options.""" + command_doc_path = Path("demisto_sdk/commands") / command_name / "README.md" + + if not command_doc_path.exists(): + print(f"README.md not found for command: {command_name}") # noqa: T201 + return + + # Read the existing README.md + with command_doc_path.open("r") as f: + readme_content = f.read() + + # Update the Description section + description_start = readme_content.find("## Description") + if description_start != -1: + description_end = readme_content.find( + "##", description_start + len("## Description") + ) + if description_end == -1: + description_end = len(readme_content) + updated_readme = ( + readme_content[:description_start] + + f"## Description\n{description}\n\n" + + readme_content[description_end:] + ) + else: + updated_readme = f"{readme_content}\n## Description\n{description}\n\n" + + # Update the Options section + options_start = updated_readme.find("### Options") + if options_start != -1: + options_end = updated_readme.find("##", options_start + len("### Options")) + if options_end == -1: + options_end = len(updated_readme) + updated_readme = ( + updated_readme[:options_start] + options + updated_readme[options_end:] + ) + else: + updated_readme += "\n" + options + + # Write the updated content back into the README.md + with command_doc_path.open("w") as f: + f.write(updated_readme) + + print(f"Description and options section updated for command: {command_name}") # noqa: T201 + + +def generate_docs_for_command(command_name: str): + """Generate documentation for a specific command.""" + description = get_command_description(command_name) + options = get_command_options(command_name) + update_readme(command_name, description, options) + + +def main(): + if len(sys.argv) < 2: + print("Usage: python generate_docs.py ...") # noqa: T201 + sys.exit(1) + + # Receive the list of modified files from command-line arguments + modified_files = sys.argv[1:] + changed_commands = extract_changed_commands(modified_files) + + # Generate documentation for each modified command + for command_name in changed_commands: + generate_docs_for_command(command_name) + + +if __name__ == "__main__": + main() diff --git a/demisto_sdk/scripts/generate_commands_docs/pre_commit_generate_docs.py b/demisto_sdk/scripts/generate_commands_docs/pre_commit_generate_docs.py new file mode 100644 index 0000000000..301b65f31c --- /dev/null +++ b/demisto_sdk/scripts/generate_commands_docs/pre_commit_generate_docs.py @@ -0,0 +1,62 @@ +import os +import re +import subprocess +import sys +from pathlib import Path + +EXCLUDED_BRANCHES_REGEX = r"^(master|[0-9]+\.[0-9]+\.[0-9]+)$" + + +def get_current_branch(): + """Returns the current Git branch name.""" + result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True) + return result.stdout.strip() + + +def get_modified_files(): + """Returns a list of files modified in the current commit.""" + result = subprocess.run(["git", "diff", "--cached", "--name-only"], capture_output=True, text=True) + return result.stdout.splitlines() + + +def extract_changed_commands(modified_files): + """Extract command names from modified _setup.py files.""" + changed_commands = [] + for file in modified_files: + if file.endswith("_setup.py"): + command_name = Path(file).stem.replace("_setup", "") + changed_commands.append(command_name) + return changed_commands + + +def main(): + # Check if the branch should be excluded + current_branch = get_current_branch() + if re.match(EXCLUDED_BRANCHES_REGEX, current_branch): + print(f"Pre-commit hook skipped on branch '{current_branch}'") + sys.exit(0) + + # Get the list of modified files + modified_files = get_modified_files() + + # Filter for _setup.py files to determine which commands changed + changed_commands = extract_changed_commands(modified_files) + if not changed_commands: + print("No modified _setup.py files found. Skipping documentation generation.") + sys.exit(0) + + # Run the documentation generation script with all changed commands + print(f"Generating documentation for modified commands: {changed_commands}") + subprocess.run([sys.executable, "generate_docs.py", *changed_commands]) + + # Stage the newly generated or updated README files for each command + for command_name in changed_commands: + readme_file = Path("demisto-sdk/commands") / command_name / "README.md" + if readme_file.exists(): + subprocess.run(["git", "add", str(readme_file)]) + + print("Pre-commit hook completed successfully.") + + +if __name__ == "__main__": + main()