Skip to content

Commit

Permalink
Merge 60409dd into adf7fdc
Browse files Browse the repository at this point in the history
  • Loading branch information
kgal-pan committed May 28, 2024
2 parents adf7fdc + 60409dd commit 0c182e7
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 80 deletions.
4 changes: 4 additions & 0 deletions .changelog/4303.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changes:
- description: Improved implementation of finding the commands' sections within the integration README.md
type: fix
pr_number: 4303
17 changes: 14 additions & 3 deletions demisto_sdk/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from demisto_sdk.commands.common.constants import (
DEMISTO_SDK_MARKETPLACE_XSOAR_DIST_DEV,
ENV_DEMISTO_SDK_MARKETPLACE,
INTEGRATIONS_README_FILE_NAME,
FileType,
MarketplaceVersions,
)
Expand Down Expand Up @@ -2231,6 +2232,13 @@ def init(ctx, **kwargs):
is_flag=True,
default=True,
)
@click.option(
"-f",
"--force",
help="Whether to force the generation of documentation (rather than update when it exists in version control)",
is_flag=True,
default=False,
)
@click.pass_context
@logging_setup_decorator
def generate_docs(ctx, **kwargs):
Expand Down Expand Up @@ -2302,21 +2310,23 @@ def _generate_docs_for_file(kwargs: Dict[str, Any]):
custom_image_path: str = kwargs.get("custom_image_path", "")
readme_template: str = kwargs.get("readme_template", "")
use_graph = kwargs.get("graph", True)
force = kwargs.get("force", False)

try:
if command:
if (
output_path
and (not Path(output_path, "README.md").is_file())
and (not Path(output_path, INTEGRATIONS_README_FILE_NAME).is_file())
or (not output_path)
and (
not Path(
os.path.dirname(os.path.realpath(input_path)), "README.md"
os.path.dirname(os.path.realpath(input_path)),
INTEGRATIONS_README_FILE_NAME,
).is_file()
)
):
raise Exception(
"[red]The `command` argument must be presented with existing `README.md` docs."
f"[red]The `command` argument must be presented with existing `{INTEGRATIONS_README_FILE_NAME}` docs."
)

file_type = find_type(kwargs.get("input", ""), ignore_sub_categories=True)
Expand Down Expand Up @@ -2356,6 +2366,7 @@ def _generate_docs_for_file(kwargs: Dict[str, Any]):
command=command,
old_version=old_version,
skip_breaking_changes=skip_breaking_changes,
force=force,
)
elif file_type == FileType.SCRIPT:
logger.info(f"Generating {file_type.value.lower()} documentation")
Expand Down
45 changes: 27 additions & 18 deletions demisto_sdk/commands/generate_docs/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
## generate-docs
# `generate-docs`

Generate a README file for an Integration, Script or a Playbook.
Generate a `README.md` file for an Integration, Script or a Playbook.

## Use Cases

**Use-Cases**
This command is used to create a documentation file for Cortex XSOAR content files.

**Arguments**:
## Arguments

* **-i, --input**
Path of the yml file.
Expand Down Expand Up @@ -35,32 +36,40 @@ Path of the old integration version yml file.
Skip generating of breaking changes section.
* **-ngr, --no-graph**
Whether to use the content graph or not.
* **-f, --force**
Whether to force the generation of documentation (rather than update when it exists in version control).

**Notes**

* If `command_permissions` wil not be given, a generic message regarding the need of permissions will be given.
* If no `output` given, the README.md file will be generated in the `input` file repository.
* If no `additionalinfo` is provided for a commonly-used parameter (for example, `API Key`), a matching default value
> [!NOTE]
>
> * If `command_permissions` wil not be given, a generic message regarding the need of permissions will be given.
>
> * If no `output` given, the `README.md` file will be generated in the `input` file repository.
>
> * If no `additionalinfo` is provided for a commonly-used parameter (for example, `API Key`), a matching default value
will be used, see the parameters and defaults in `default_additional_information.json`.
* In order to generate an **incident mirroring** section, make sure that the *isremotesyncin* and/or *isremotesyncout* parameters are set to true in the YML file. In addition, the following configuration parameters (if used) should be named as stated:
* incidents_fetch_query
* Mirroring tags - the available names are 'comment_tag', 'work_notes_tag' and 'file_tag'
* mirror_direction
* close_incident
* close_out - (opposite to close_incident)
>
> In order to generate an **incident mirroring** section, make sure that the `isremotesyncin` and/or `isremotesyncout` parameters are set to `true` in the YML file. In addition, the following configuration parameters (if used) should be named as stated:
>
> * `incidents_fetch_query`
> * Mirroring tags - the available names are `comment_tag`, `work_notes_tag` and `file_tag`
> * `mirror_direction`
> * `close_incident`
> * `close_out` - (opposite to `close_incident`)
>
> * If the Integration/Script/Playbook exists in version control, the version from the main branch (e.g. `master`) will be used to only render the modified sections (e.g. configuration, command) unless the `--force` flag is specified.
### Examples

```bash
demisto-sdk generate-docs -i Packs/MyPack/Integrations/MyInt/MyInt.yml -e Packs/MyPack/Integrations/MyInt/command_example.txt
```

This will generate a documentation for the MyInt integration using the command examples found in the .txt file in the MyInt integration.
This will generate a documentation for the `MyInt` integration using the command examples found in the .txt file in the `MyInt` integration.

```bash
demisto-sdk generate-docs -i Packs/MyPack/Integrations/MyInt/MyInt_v2.yml --old-version Packs/MyPack/Integrations/MyInt/MyInt.yml
```

This will generate a documentation for MyInt_v2 integration including a section about changes compared the MyInt integration.
This will generate a documentation for `MyInt_v2` integration including a section about changes compared the `MyInt` integration.
The command will automatically detect if the given integration is a v2 using the integration's display name and create the changes section.
If no '--old-version' is supplied a prompt will appear asking for the path to the old integration.
If no `--old-version` is supplied a prompt will appear asking for the path to the old integration.
117 changes: 72 additions & 45 deletions demisto_sdk/commands/generate_docs/generate_integration_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DOCS_COMMAND_SECTION_REGEX,
INTEGRATIONS_DIR,
INTEGRATIONS_README_FILE_NAME,
SCRIPT,
)
from demisto_sdk.commands.common.default_additional_info_loader import (
load_default_additional_info_dict,
Expand Down Expand Up @@ -216,9 +217,6 @@ def _update_conf_section(self):

doc_text_lines = self.output_doc.splitlines()

# We take the first and the second-to-last index of the old section
# and use the section range to replace it with the new section.
# Second-to-last index because the last element is an empty string
old_config_start_line = doc_text_lines.index(
CONFIGURATION_SECTION_STEPS.STEP_1.value
)
Expand All @@ -244,16 +242,12 @@ def _update_commands_section(self):
README.
"""

for i, modified_command in enumerate(
self.integration_diff.get_modified_commands()
):
command_sections = get_commands_sections(self.output_doc)

for modified_command in self.integration_diff.get_modified_commands():
try:
old_command_section, _ = generate_commands_section(
self.integration_diff.old_yaml_data,
{},
{},
modified_command,
)

start_line, end_line = command_sections[modified_command]

(
new_command_section,
Expand All @@ -280,30 +274,7 @@ def _update_commands_section(self):

doc_text_lines = self.output_doc.splitlines()

# We take the first and the second-to-last index of the old section
# and use the section range to replace it with the new section.
# Second-to-last index because the last element is an empty string
old_cmd_start_line = doc_text_lines.index(old_command_section[0])

# In cases when there are multiple identical context outputs
# in the second-to-last line, we need to find the relevant
# second-to-last line for the specific command we're replacing.
indices = [
i
for i, doc_line in enumerate(doc_text_lines)
if doc_line == old_command_section[-2]
]

if indices and len(indices) > 1:
old_cmd_end_line = doc_text_lines.index(
old_command_section[-2], indices[i]
)
else:
old_cmd_end_line = doc_text_lines.index(old_command_section[-2])

doc_text_lines[
old_cmd_start_line : old_cmd_end_line + 1
] = new_command_section
doc_text_lines[start_line:end_line] = new_command_section

self.output_doc = "\n".join(doc_text_lines)
except (ValueError, IndexError) as e:
Expand All @@ -318,11 +289,6 @@ def _get_sections_to_update(self) -> Tuple[bool, List[str], List[str]]:
self.integration_diff.get_added_commands(),
)

def _get_resource_path(self) -> str:
"""
Helper function to resolve the resource path.
"""

def _write_resource_to_tmp(self, resource_path: Path, content: str) -> Path:
"""
Helper function to write
Expand Down Expand Up @@ -455,6 +421,7 @@ def generate_integration_doc(
old_version: str = "",
skip_breaking_changes: bool = False,
is_contribution: bool = False,
force: bool = False,
):
"""
Generate integration documentation.
Expand Down Expand Up @@ -536,7 +503,7 @@ def generate_integration_doc(
# in source control:
# - An integration YAML.
# - An integration README.
elif update_mgr.can_update_docs():
elif not force and update_mgr.can_update_docs():
logger.info("Found existing integration, updating documentation...")
doc_text, update_errors = update_mgr.update_docs()

Expand Down Expand Up @@ -851,9 +818,7 @@ def generate_commands_section(
"After you successfully execute a command, a DBot message appears in the War Room with the command details.",
"",
]
commands = filter(
lambda cmd: not cmd.get("deprecated", False), yaml_data["script"]["commands"]
)
commands = get_integration_commands(yaml_data)
command_sections: list = []
if command:
# for specific command, return it only.
Expand Down Expand Up @@ -1298,3 +1263,65 @@ def add_access_data_of_type_credentials(
"Required": credentials_conf.get("required", ""),
}
)


def get_integration_commands(yaml_data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Helper function to return a list of integration commands.
Integration commands that are marked as deprecated will not be
returned.
Args:
- `yml_data` (``Dict[str, Any]``): The integration YAML as a dictionary.
Returns:
- `List[Dict[str, Any]]` of integration commands.
"""

return list(
filter(
lambda cmd: not cmd.get("deprecated", False), yaml_data[SCRIPT]["commands"]
)
)


def get_commands_sections(doc_text: str) -> Dict[str, Tuple[int, int]]:
"""
Helper function that takes the integration README text
and returns a map of the commands and the start, end lines.
Args:
- `doc_text` (``str``): The integration README.
Returns:
- `dict[str, tuple]` with the name of the command and the
start and end line of the command section within the README.
"""

command_start_section_pattern = r"^###\s+([a-z]+(-[a-z]+)*$)"

out = {}

# Here we iterate over the README line by line
# and find what lines the command sections are defined
for line_nr, line_text in enumerate(doc_text.splitlines()):
cmd_search = re.search(command_start_section_pattern, line_text)

if cmd_search:
out[cmd_search.group(1)] = line_nr

# We then transform the structure
# to include the end line as well
keys = list(out.keys())
values = list(out.values())

transformed = {}

# Iterate over the keys and values
for i in range(len(keys)):
if i < len(keys) - 1:
transformed[keys[i]] = (values[i], values[i + 1])
else:
transformed[keys[i]] = (values[i], len(doc_text.splitlines()))

return transformed
Loading

0 comments on commit 0c182e7

Please sign in to comment.