Skip to content

feat: support bulk-export command#261

Merged
ohadedry merged 21 commits into
microsoft:mainfrom
ohadedry:support-bulk-export-command
Jul 23, 2026
Merged

feat: support bulk-export command#261
ohadedry merged 21 commits into
microsoft:mainfrom
ohadedry:support-bulk-export-command

Conversation

@ohadedry

@ohadedry ohadedry commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

✨ Description of new changes

This pull request introduces a new bulk-export command to the CLI, enabling bulk export of all supported items from a workspace or folder while preserving folder structure and item bindings. The implementation includes API integration, argument validation, error handling, and comprehensive documentation and examples. The changes also update command support configuration and the command index.

New Functionality:

  • Added a new bulk-export command that allows users to export all supported items from a workspace or folder in a single operation, preserving folder structure and item bindings. This includes both CLI logic and integration with the backend API. [1] [2] [3] [4] [5]

Documentation Updates:

  • Added a new documentation page bulk_export.md detailing usage, parameters, supported item types, output structure, and examples for the bulk-export command.
  • Updated the command index and navigation to include bulk-export. [1] [2]
  • Added example usage sections for bulk-exporting workspaces and folders in the relevant examples files. [1] [2]

Command Support and Configuration:

  • Registered the new bulkexport command in the CLI command enumeration and in the command support YAML, specifying supported elements (workspace, folder) and item types. [1] [2]

Internal Integration and Validation:

  • Integrated the bulk-export command into the main command handler and ensured argument validation, context checking, and user confirmation logic are in place. [1] [2] [3]
  • Improved error handling and user prompts for export preconditions, including checks for recursive export and non-empty output folders.

Minor Improvements:

  • Updated logging in the API client to include command context for improved traceability.
  • Added copyright headers to new files.

These changes collectively introduce and document the new bulk-export feature, making it easier for users to export entire workspaces or folders with structure and bindings preserved.# 📥 Pull Request

Copilot AI review requested due to automatic review settings July 6, 2026 10:51
@ohadedry
ohadedry requested a review from a team as a code owner July 6, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new bulk-export command to Fabric CLI (fab) to export all supported items from a workspace or folder in one operation, preserving folder structure and item bindings. It includes end-to-end command wiring (parser → command handler → API client), export utilities for writing bulk-export definition parts to storage, debug-log redaction improvements, and corresponding documentation + tests (including VCR recordings).

Changes:

  • Implemented bulk-export command flow (parsing, command dispatch, workspace/folder execution paths, API call, and export-to-storage utilities).
  • Extended export utilities and HTTP debug logging to better support bulk export payload shapes and reduce noisy/large debug logs.
  • Added docs pages/examples and comprehensive tests + recordings for the new command.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/test_utils/test_fab_cmd_bulk_export_utils.py Unit tests for bulk-export utility helpers (payload creation, summary output, path stripping/export).
tests/test_core/test_fab_logger.py Updates logger tests for new command-context-aware response logging/redaction.
tests/test_commands/test_bulk_export.py New command-level tests for bulk-export behavior and output structure.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_workspace_without_recursive_fail.yaml VCR recording for workspace bulk-export missing --recursive.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_workspace_folder_without_recursive_fail.yaml VCR recording for folder bulk-export missing --recursive.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_output_path_not_empty_warning.yaml VCR recording for warning behavior when output directory is non-empty.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_no_exportable_items_fail.yaml VCR recording for “no exportable items” failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_item_fail.yaml VCR recording for invalid target type (item path).
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_empty_workspace_fail.yaml VCR recording for empty workspace failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/test_bulk_export_empty_folder_fail.yaml VCR recording for empty folder failure case.
tests/test_commands/recordings/test_commands/test_bulk_export/class_setup.yaml VCR class setup recording for bulk-export command tests.
tests/test_commands/commands_parser.py Registers the new bulk-export parser into the command test harness.
src/fabric_cli/utils/fab_commands.py Adds bulk-export to the CLI command index text.
src/fabric_cli/utils/fab_cmd_export_utils.py Extends decode/export helpers to support definitionParts payload shape.
src/fabric_cli/utils/fab_cmd_bulk_export_utils.py New bulk-export utility module (payload, export writing, summary printing, path validation).
src/fabric_cli/parsers/fab_fs_parser.py Adds argparse registration for the bulk-export command.
src/fabric_cli/errors/bulk_export.py Defines bulk-export-specific error messages.
src/fabric_cli/core/fab_parser_setup.py Wires bulk-export parser into the global CLI parser setup.
src/fabric_cli/core/fab_logger.py Adds command context to response logging + JSON response redaction logic.
src/fabric_cli/core/fab_constant.py Adds COMMAND_FS_BULKEXPORT_DESCRIPTION.
src/fabric_cli/core/fab_config/command_support.yaml Registers bulk-export support matrix (workspace/folder + supported item types).
src/fabric_cli/core/fab_commands.py Adds FS_BULKEXPORT command enum value.
src/fabric_cli/commands/fs/fab_fs.py Hooks bulk_export_command into the fs command module.
src/fabric_cli/commands/fs/fab_fs_bulk_export.py New bulk-export command implementation and precondition checks.
src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py Workspace bulk-export execution and response filtering.
src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_folder.py Folder bulk-export execution and recursive item collection.
src/fabric_cli/commands/fs/bulk_export/init.py Package init for bulk-export submodule.
src/fabric_cli/client/fab_api_item.py Adds bulk_export_definitions() API wrapper.
src/fabric_cli/client/fab_api_client.py Passes command context into HTTP response debug logging.
mkdocs.yml Adds bulk-export page to MkDocs navigation.
docs/examples/workspace_examples.md Adds workspace bulk-export example section.
docs/examples/folder_examples.md Adds folder bulk-export example section.
docs/commands/index.md Adds bulk-export to the command index page.
docs/commands/fs/bulk_export.md New bulk-export command documentation page.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py
Comment thread src/fabric_cli/parsers/fab_fs_parser.py Outdated
Comment thread src/fabric_cli/parsers/fab_fs_parser.py Outdated
Comment thread src/fabric_cli/utils/fab_commands.py
Comment thread src/fabric_cli/core/fab_constant.py
Comment thread docs/commands/fs/bulk_export.md
Comment thread tests/test_commands/test_bulk_export.py
Comment thread docs/commands/fs/bulk_export.md
Comment thread docs/commands/fs/bulk_export.md
Comment thread docs/commands/fs/bulk_export.md
Comment thread docs/commands/fs/bulk_export.md Outdated
Comment thread docs/commands/fs/bulk_export.md
Comment thread docs/commands/index.md Outdated
Comment thread docs/examples/folder_examples.md Outdated
Comment thread src/fabric_cli/client/fab_api_item.py Outdated
Comment thread src/fabric_cli/parsers/fab_fs_parser.py
Comment thread src/fabric_cli/utils/fab_cmd_export_utils.py Outdated
Comment thread src/fabric_cli/utils/fab_commands.py Outdated
Comment thread src/fabric_cli/utils/fab_cmd_bulk_export_utils.py Outdated
Comment thread src/fabric_cli/utils/fab_cmd_bulk_export_utils.py Outdated
Copilot AI review requested due to automatic review settings July 8, 2026 10:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 7 comments.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py Outdated
Comment thread docs/commands/fs/bulk_export.md
Comment thread tests/test_commands/test_bulk_export.py Outdated
Comment thread tests/test_commands/test_bulk_export.py Outdated
Comment thread tests/test_commands/test_bulk_export.py Outdated
Comment thread tests/test_commands/test_bulk_export.py
Comment thread tests/test_commands/test_bulk_export.py
Copilot AI review requested due to automatic review settings July 8, 2026 12:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py Outdated
Comment thread src/fabric_cli/utils/fab_cmd_bulk_export_utils.py Outdated
Comment thread src/fabric_cli/utils/fab_cmd_bulk_export_utils.py Outdated
Comment thread tests/test_core/test_fab_logger.py
Comment thread docs/commands/fs/bulk_export.md
Copilot AI review requested due to automatic review settings July 8, 2026 12:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/fabric_cli/utils/fab_cmd_export_utils.py:63

  • The docstring still says this function exports from the 'parts' array, but the implementation now supports an arbitrary key via definition_parts (e.g., definitionParts for bulk-export). Updating the docstring avoids misleading future callers.
    """
    Export each 'payload' in the 'parts' array to a file named based on 'path' in 'parts'.
    The 'payload' content will be saved as the file's content.
    """

Comment thread tests/test_commands/commands_parser.py
Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py Outdated
Copilot AI review requested due to automatic review settings July 12, 2026 11:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 2 comments.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py
Copilot AI review requested due to automatic review settings July 12, 2026 11:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 4 comments.

Comment thread src/fabric_cli/commands/fs/fab_fs_bulk_export.py
Comment thread src/fabric_cli/utils/fab_cmd_export_utils.py
Comment thread tests/test_core/test_fab_logger.py
Comment thread tests/test_utils/test_fab_cmd_bulk_export_utils.py Outdated
Comment thread tests/test_utils/test_fab_cmd_bulk_export_utils.py Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 13:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 43 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

src/fabric_cli/utils/fab_cmd_bulk_export_utils.py:156

  • Path traversal validation uses a string startswith check on os.path.abspath(...), which is brittle (e.g., case-insensitive filesystems, different separators) and can be bypassed in edge cases. Prefer os.path.commonpath(...) (or pathlib.Path.resolve() + is_relative_to) to robustly ensure the resolved path stays under the export root.
    for part in item_def.get("definitionParts", []):
        part_path = part.get("path", "").lstrip("/")
        full_export_path = os.path.abspath(os.path.join(export_path, part_path))
        if not full_export_path.startswith(os.path.abspath(export_path) + os.sep):
            raise FabricCLIError(

src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py:93

  • This logic treats any workspace item missing from itemDefinitionsIndex as an "unsupported item". Per the bulk-export docs, items may also be omitted due to insufficient permissions, so folding "not returned by API" into "unsupported type" can make the summary misleading (and can incorrectly mark supported types as skipped due to unsupported types). Consider tracking a separate bucket for "not exported" (e.g., permission) vs truly unsupported types, and adjust the summary message accordingly.
    # find unsupported items that were not in the response index and add them to the unsupported_items_list
    supported_item_ids: set[str] = {item.id for item in supported_items_list}
    unsupported_item_ids: set[str] = {item.id for item in unsupported_items_list}
    for ws_item in ws_items:
        if (

Copilot AI review requested due to automatic review settings July 23, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 43 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py:93

  • Items that are not returned in itemDefinitionsIndex are currently appended to unsupported_items_list, but the API can omit items for reasons other than unsupported type (e.g., permission filtering as described in the bulk-export docs). This causes the summary to incorrectly report these as "Skipped … due to unsupported item types".

Consider only classifying items that appear in itemDefinitionsIndex as supported/unsupported (or track a separate "not exported" bucket with a different message) so the skip reason remains accurate.

    # find unsupported items that were not in the response index and add them to the unsupported_items_list
    supported_item_ids: set[str] = {item.id for item in supported_items_list}
    unsupported_item_ids: set[str] = {item.id for item in unsupported_items_list}
    for ws_item in ws_items:
        if (

src/fabric_cli/utils/fab_cmd_export_utils.py:52

  • decode_payload() attempts to recurse into nested_parts by calling decode_payload(part), but nested_parts is a list (see _encode_payload usage in fab_cmd_set_utils.py) and part itself does not have the definition/parts or definitionParts structure that decode_payload() expects. As a result, nested parts payloads are never decoded.
        # Recursively check for nested parts if applicable
        if (
            "nested_parts" in part
        ):  # Assuming 'nested_parts' is a key for potential nested structures
            decode_payload(part)

Copilot AI review requested due to automatic review settings July 23, 2026 08:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 44 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py:93

  • Items that are not present in the bulk-export response index are currently appended to unsupported_items_list. Per the command docs, a workspace export may omit items due to missing read/write permissions, so classifying all non-returned items as “unsupported” can inflate the skipped count and mislead users about why items weren’t exported. Consider only adding items that are actually unsupported by command support (i.e., check_command_support raises), and leave supported-but-not-returned items out of the unsupported bucket (or track them separately).
    # find unsupported items that were not in the response index and add them to the unsupported_items_list
    supported_item_ids: set[str] = {item.id for item in supported_items_list}
    unsupported_item_ids: set[str] = {item.id for item in unsupported_items_list}
    for ws_item in ws_items:
        if (

Comment thread src/fabric_cli/core/fab_logger_response.py
Comment thread docs/examples/workspace_examples.md Outdated
Comment thread docs/examples/folder_examples.md Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 09:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 44 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (4)

src/fabric_cli/utils/fab_cmd_bulk_export_utils.py:156

  • The path-traversal guard uses a plain string prefix check (startswith) on absolute paths. That can be bypass-prone on Windows (case-insensitive paths, different path normalizations) and can also behave unexpectedly when drives differ. Use os.path.commonpath (with a ValueError fallback) to reliably enforce that each exported part stays within the export root.
    for part in item_def.get("definitionParts", []):
        part_path = part.get("path", "").lstrip("/")
        full_export_path = os.path.abspath(os.path.join(export_path, part_path))
        if not full_export_path.startswith(os.path.abspath(export_path) + os.sep):
            raise FabricCLIError(

src/fabric_cli/core/fab_logger_response.py:34

  • _process_bulk_export only redacts definitionParts when all parts have the exact expected keys. If the service adds/removes keys (or returns a partial structure), payloads could be logged unredacted in debug mode. Since definitionParts can contain user content and can be very large, it’s safer to redact whenever definitionParts is present and is a list.
def _process_bulk_export(parsed_json: dict):
    if (
        "itemDefinitionsIndex" in parsed_json
        and "definitionParts" in parsed_json
        and isinstance(parsed_json["definitionParts"], list)
        and all(
            isinstance(part, dict) and _ITEM_DEFINITION_PART_KEYS <= part.keys()
            for part in parsed_json["definitionParts"]
        )
    ):
        return {
            k: ("redacted_from_log" if k == "definitionParts" else v)
            for k, v in parsed_json.items()
        }

src/fabric_cli/core/fab_logger_response.py:56

  • _process_export only redacts payloads when every part contains path/payload/payloadType. If the response shape changes (e.g., missing payloadType), payloads may be logged unredacted. Consider redacting the payload field opportunistically for any dict part that contains payload whenever definition.parts is a list.
def _process_export(parsed_json: dict):
    if (
        "definition" in parsed_json
        and "parts" in parsed_json["definition"]
        and isinstance(parsed_json["definition"]["parts"], list)
        and all(
            isinstance(part, dict) and _ITEM_DEFINITION_PART_KEYS <= part.keys()
            for part in parsed_json["definition"]["parts"]
        )
    ):
        definition = {
            k: v for k, v in parsed_json["definition"].items() if k != "parts"
        }
        definition["parts"] = [
            {k: ("redacted_from_log" if k == "payload" else v) for k, v in part.items()}
            for part in parsed_json["definition"]["parts"]
        ]
        return {"definition": definition}

src/fabric_cli/commands/fs/bulk_export/fab_fs_bulk_export_workspace.py:97

  • This loop unconditionally adds any workspace items missing from itemDefinitionsIndex to unsupported_items_list. For workspace bulk-export, items can be missing for reasons other than unsupported type (notably insufficient permissions, per the command docs), which will make the summary incorrectly claim they were skipped due to unsupported item types. Only classify missing items as unsupported when the local command-support check fails.
        if (
            ws_item.id not in supported_item_ids
            and ws_item.id not in unsupported_item_ids
        ):
            unsupported_items_list.append(ws_item)

Comment thread src/fabric_cli/utils/fab_cmd_bulk_export_utils.py
ayeshurun
ayeshurun previously approved these changes Jul 23, 2026
Comment thread src/fabric_cli/core/fab_logger_response.py
aviatco
aviatco previously approved these changes Jul 23, 2026
Copilot AI review requested due to automatic review settings July 23, 2026 12:44
@ohadedry
ohadedry dismissed stale reviews from aviatco and ayeshurun via 3e9988a July 23, 2026 12:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 44 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/fabric_cli/core/fab_logger_response.py:36

  • _process_bulk_export only redacts definitionParts when every entry has the full {path,payload,payloadType} key set. If the service returns a slightly different shape (or a single malformed entry), the function falls back to returning the original JSON and can log base64 payloads in debug logs, which is a potential data exposure / log-bloat issue.
def _process_bulk_export(parsed_json: dict):
    if (
        "itemDefinitionsIndex" in parsed_json
        and "definitionParts" in parsed_json
        and isinstance(parsed_json["definitionParts"], list)
        and all(
            isinstance(part, dict) and _ITEM_DEFINITION_PART_KEYS <= part.keys()
            for part in parsed_json["definitionParts"]
        )
    ):
        return {
            k: ("redacted_from_log" if k == "definitionParts" else v)
            for k, v in parsed_json.items()
        }

    return parsed_json

src/fabric_cli/core/fab_logger_response.py:58

  • _process_export redaction is skipped unless all parts have {path,payload,payloadType} keys. If the API ever omits a key on one part (or includes a non-dict entry), the current code returns the original JSON and logs raw payload values. Redacting payload on a best-effort basis is safer and still preserves useful debug context (paths, payloadType, etc.).
def _process_export(parsed_json: dict):
    if (
        "definition" in parsed_json
        and "parts" in parsed_json["definition"]
        and isinstance(parsed_json["definition"]["parts"], list)
        and all(
            isinstance(part, dict) and _ITEM_DEFINITION_PART_KEYS <= part.keys()
            for part in parsed_json["definition"]["parts"]
        )
    ):
        definition = {
            k: v for k, v in parsed_json["definition"].items() if k != "parts"
        }
        definition["parts"] = [
            {k: ("redacted_from_log" if k == "payload" else v) for k, v in part.items()}
            for part in parsed_json["definition"]["parts"]
        ]
        return {"definition": definition}

    return parsed_json

src/fabric_cli/utils/fab_cmd_bulk_export_utils.py:159

  • The path traversal guard uses abspath(...).startswith(abspath(export_path) + os.sep). This is not robust on case-insensitive filesystems and does not account for symlinks inside the export directory (a pre-existing symlink could still cause writes outside the intended export root). Using realpath + commonpath provides a safer, cross-platform containment check.
def _validate_definition_parts_paths_are_under_export_path(
    item_def: dict, export_path: str
) -> None:
    """Validate that all definition part paths concatenated with the export path are under the export path to prevent path traversal issues."""
    for part in item_def.get("definitionParts", []):
        part_path = part.get("path", "").lstrip("/")
        full_export_path = os.path.abspath(os.path.join(export_path, part_path))
        if not full_export_path.startswith(os.path.abspath(export_path) + os.sep):
            raise FabricCLIError(
                BulkExportErrors.path_mismatch_full_export_path(),
                fab_constant.ERROR_INVALID_DEFINITION_PAYLOAD,
            )

@ohadedry
ohadedry merged commit 09e53e5 into microsoft:main Jul 23, 2026
18 of 19 checks passed
@ohadedry
ohadedry deleted the support-bulk-export-command branch July 23, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants