From 507f0c26c5f56e0f0f5c91d4a8e99e456344a884 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sun, 18 Aug 2024 21:03:44 +0200 Subject: [PATCH 01/13] feat: explicit sequential execution; extra args param for run/deploy commands --- docs/features/project/deploy.md | 29 ++++++++- docs/features/project/run.md | 63 ++++++++++++++----- src/algokit/cli/project/deploy.py | 11 +++- src/algokit/cli/project/run.py | 33 +++++++--- src/algokit/core/project/run.py | 61 +++++++++--------- ...rom_workspace_execution_error.approved.txt | 2 +- ...mmand_from_workspace_filtered.approved.txt | 2 +- ...workspace_filtered_no_project.approved.txt | 2 +- ...om_workspace_resolution_error.approved.txt | 2 +- ..._workspace_sequential_success.approved.txt | 2 +- ...ommand_from_workspace_success.approved.txt | 2 +- 11 files changed, 146 insertions(+), 63 deletions(-) diff --git a/docs/features/project/deploy.md b/docs/features/project/deploy.md index b9eb31d4..06686825 100644 --- a/docs/features/project/deploy.md +++ b/docs/features/project/deploy.md @@ -7,7 +7,7 @@ Deploy your smart contracts effortlessly to various networks with the algokit pr ## Usage ```sh -$ algokit project deploy [OPTIONS] [ENVIRONMENT_NAME] +$ algokit project deploy [OPTIONS] [ENVIRONMENT_NAME] [EXTRA_ARGS] ``` This command deploys smart contracts from an AlgoKit compliant repository to the specified network. @@ -22,6 +22,7 @@ This command deploys smart contracts from an AlgoKit compliant repository to the - `-p, --project-name`: (Optional) Projects to execute the command on. Defaults to all projects found in the current directory. Option is mutually exclusive with `--command`. - `-h, --help`: Show this message and exit. +- `[EXTRA_ARGS]...`: Additional arguments to pass to the deploy command. For instance, `algokit project deploy -- {custom args}`. This will ensure that the extra arguments are passed to the deploy command specified in the `.algokit.toml` file or directly via `--command` option. ## Environment files @@ -183,12 +184,36 @@ Example: $ algokit project deploy testnet --ci ``` +## Passing Extra Arguments + +You can pass additional arguments to the deploy command. These extra arguments will be appended to the end of the deploy command specified in your `.algokit.toml` file or to the command specifified directly via `--command` option. Declare after `--` to mark distinction between arguments used by cli vs arguments to be passed as extras to the deploy command/script defined. + +Example: + +```sh +$ algokit project deploy testnet -- my_contract_name --some_contract_related_param +``` + +In this example, `my_contract_name` is an extra argument that can be utilized under custom deploy command invocation to filter the deployment to a specific contract. + ## Example of a Full Deployment ```sh -$ algokit project deploy testnet --custom-deploy-command="your-custom-command" +$ algokit project deploy testnet --custom-deploy-command="your-custom-command" --skip-tests ``` +This example shows how to deploy smart contracts to the testnet using a custom deploy command and passing an extra argument `--skip-tests`. + +// ... rest of the existing content ... + +```` + +## Example of a Full Deployment + +```sh +$ algokit project deploy testnet --custom-deploy-command="your-custom-command" +```` + This example shows how to deploy smart contracts to the testnet using a custom deploy command. This also assumes that .algokit.toml file is present in the current working directory, and .env.testnet file is present in the current working directory and contains the required environment variables for deploying to TestNet environment. ## Further Reading diff --git a/docs/features/project/run.md b/docs/features/project/run.md index 3c86e8bd..c16b1c03 100644 --- a/docs/features/project/run.md +++ b/docs/features/project/run.md @@ -10,6 +10,20 @@ $ algokit project run [OPTIONS] COMMAND [ARGS] This command executes a custom command defined in the `.algokit.toml` file of the current project or workspace. +### Options + +- `-l, --list`: List all projects associated with the workspace command. (Optional) +- `-p, --project-name`: Execute the command on specified projects. Defaults to all projects in the current directory. (Optional) +- `-t, --type`: Limit execution to specific project types if executing from workspace. (Optional) +- `-c, --concurrent / -s, --sequential`: Execute workspace commands concurrently (default) or sequentially. (Optional) +- `[ARGS]...`: Additional arguments to pass to the custom command. These will be appended to the end of the command specified in the `.algokit.toml` file. + +To get detailed help on the above options, execute: + +```bash +algokit project run {name_of_your_command} --help +``` + ### Workspace vs Standalone Projects AlgoKit supports two main types of project structures: Workspaces and Standalone Projects. This flexibility caters to the diverse needs of developers, whether managing multiple related projects or focusing on a single application. @@ -113,27 +127,46 @@ Executing `algokit project run hello` from the root of the workspace will concur Executing `algokit project run hello` from the root of `project_(a|b)` will execute `echo hello` in the `project_(a|b)` directory. -### Controlling order of execution +### Controlling Execution Order -To control order of execution, simply define the order for a particular command as follows: +Customize command execution in workspaces for precise control: -```yaml -# ... other non [project.run] related metadata -[project] -type = 'workspace' -projects_root_path = 'projects' +1. Define order in `.algokit.toml`: -[project.run] -hello = ['project_a', 'project_b'] -# ... other non [project.run] related metadata -``` + ```yaml + [project] + type = 'workspace' + projects_root_path = 'projects' + + [project.run] + hello = ['project_a', 'project_b'] + ``` + +2. Execution behavior: + + - Invalid project names are skipped + - Partial project lists: Specified projects run first, others follow -Now if project_a and project_b are both defined as standalone projects, the order of execution will be respected. Additional behaviour can be described as follows: +3. Control concurrency: + ```sh + $ algokit project run hello {-s|--sequential}/{-c|--concurrent} + ``` + - Default: Concurrent + - Sequential: Enforced when order is specified -- Providing invalid project names will skip the execution of the command for the invalid project names. -- If only a subset of projects declaring this command are specified, the order of execution will be respected for the subset of projects first before the rest of the projects are executed in non-deterministic order. +> Note: Explicit order always triggers sequential execution. + +### Passing Extra Arguments + +You can pass additional arguments to the custom command. These extra arguments will be appended to the end of the command specified in your `.algokit.toml` file. + +Example: + +```sh +$ algokit project run hello -- world +``` -> Please note, when enabling explicit order of execution all commands will always run sequentially. +In this example, if the `hello` command in `.algokit.toml` is defined as `echo "Hello"`, the actual command executed will be `echo "Hello" world`. ## Further Reading diff --git a/src/algokit/cli/project/deploy.py b/src/algokit/cli/project/deploy.py index 1c3d1692..7d756974 100644 --- a/src/algokit/cli/project/deploy.py +++ b/src/algokit/cli/project/deploy.py @@ -88,6 +88,7 @@ def _execute_deploy_command( # noqa: PLR0913 interactive: bool, deployer_alias: str | None, dispenser_alias: str | None, + extra_args: tuple[str], ) -> None: logger.debug(f"Deploying from project directory: {path}") logger.debug("Loading deploy command from project config") @@ -103,7 +104,7 @@ def _execute_deploy_command( # noqa: PLR0913 "and no generic command available." ) raise click.ClickException(msg) - resolved_command = resolve_command_path(config.command) + resolved_command = resolve_command_path(config.command + list(extra_args)) logger.info(f"Using deploy command: {' '.join(resolved_command)}") logger.info("Loading deployment environment variables...") config_dotenv = load_deploy_env_files(environment_name, path) @@ -209,6 +210,11 @@ def convert( "command", ], ) +@click.argument( + "extra_args", + nargs=-1, + type=click.UNPROCESSED, +) @click.pass_context def deploy_command( # noqa: PLR0913 ctx: click.Context, @@ -220,6 +226,7 @@ def deploy_command( # noqa: PLR0913 deployer_alias: str | None, dispenser_alias: str | None, project_names: tuple[str], + extra_args: tuple[str], ) -> None: """Deploy smart contracts from AlgoKit compliant repository.""" @@ -272,6 +279,7 @@ def deploy_command( # noqa: PLR0913 interactive=interactive, deployer_alias=deployer_alias, dispenser_alias=dispenser_alias, + extra_args=extra_args, ) else: _execute_deploy_command( @@ -281,4 +289,5 @@ def deploy_command( # noqa: PLR0913 interactive=interactive, deployer_alias=deployer_alias, dispenser_alias=dispenser_alias, + extra_args=extra_args, ) diff --git a/src/algokit/cli/project/run.py b/src/algokit/cli/project/run.py index 2d2cc966..d5880597 100644 --- a/src/algokit/cli/project/run.py +++ b/src/algokit/cli/project/run.py @@ -41,13 +41,14 @@ def _load_project_commands(project_dir: Path) -> dict[str, click.Command]: for custom_command in custom_commands: # Define the base command function - def base_command( + def base_command( # noqa: PLR0913 *, - args: list[str], custom_command: ProjectCommand | WorkspaceProjectCommand = custom_command, project_names: tuple[str] | None = None, list_projects: bool = False, project_type: str | None = None, + concurrent: bool = True, + extra_args: tuple[str] | None = None, ) -> None: """ Executes a base command function with optional parameters for listing projects or specifying project names. @@ -57,32 +58,35 @@ def base_command( within a workspace. Args: - args (list[str]): The command arguments to be passed to the custom command. + extra_args (list[str]): The command arguments to be passed to the custom command. custom_command (ProjectCommand | WorkspaceProjectCommand): The custom command to be executed. project_names (list[str] | None): Optional. A list of project names to execute the command on. list_projects (bool): Optional. A flag indicating whether to list projects associated with a workspace command. project_type (str | None): Optional. Only execute commands in projects of specified type. - + concurrent (bool): Whether to execute wokspace commands concurrently. Defaults to True. Returns: None """ - if args: - logger.warning("Ignoring unrecognized arguments: %s.", " ".join(args)) - if list_projects and isinstance(custom_command, WorkspaceProjectCommand): for command in custom_command.commands: cmds = " && ".join(" ".join(cmd) for cmd in command.commands) logger.info(f"ℹ️ Project: {command.project_name}, Command name: {command.name}, Command(s): {cmds}") # noqa: RUF001 return - run_command(command=custom_command) if isinstance( + run_command(command=custom_command, extra_args=extra_args) if isinstance( custom_command, ProjectCommand - ) else run_workspace_command(custom_command, list(project_names or []), project_type) + ) else run_workspace_command( + workspace_command=custom_command, + project_names=list(project_names or []), + project_type=project_type, + concurrent=concurrent, + extra_args=extra_args, + ) # Check if the command is a WorkspaceProjectCommand and conditionally decorate is_workspace_command = isinstance(custom_command, WorkspaceProjectCommand) - command = click.argument("args", nargs=-1, type=click.UNPROCESSED, required=False)(base_command) + command = click.argument("extra_args", nargs=-1, type=click.UNPROCESSED, required=False)(base_command) if is_workspace_command: command = click.option( "project_names", @@ -118,6 +122,15 @@ def base_command( default=None, help="Limit execution to specific project types if executing from workspace. (Optional)", )(command) + command = click.option( + "concurrent", + "--concurrent/--sequential", + "-c/-s", + help="Execute workspace commands concurrently", + default=True, + is_flag=True, + required=False, + )(command) # Apply the click.command decorator with common options command = click.command( diff --git a/src/algokit/core/project/run.py b/src/algokit/core/project/run.py index 60afb0bf..5d7b7802 100644 --- a/src/algokit/core/project/run.py +++ b/src/algokit/core/project/run.py @@ -1,7 +1,7 @@ import dataclasses import logging import os -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import ThreadPoolExecutor from pathlib import Path from typing import Any @@ -162,12 +162,13 @@ def _load_commands_from_workspace( return list(workspace_commands.values()) -def run_command(*, command: ProjectCommand, from_workspace: bool = False) -> None: +def run_command(*, command: ProjectCommand, from_workspace: bool = False, extra_args: tuple[str] | None = None) -> None: """Executes a specified project command. Args: command (ProjectCommand): The project command to be executed. from_workspace (bool): Indicates whether the command is being executed from a workspace context. + extra_args (tuple[str] | None): Optional; additional arguments to pass to the command. Raises: click.ClickException: If the command execution fails. @@ -186,6 +187,8 @@ def run_command(*, command: ProjectCommand, from_workspace: bool = False) -> Non for index, cmd in enumerate(command.commands): try: resolved_command = resolve_command_path(cmd) + if index == len(command.commands) - 1 and extra_args: + resolved_command.extend(extra_args) except click.ClickException as e: logger.error(f"'{command.name}' failed executing: '{' '.join(cmd)}'") raise e @@ -213,9 +216,12 @@ def run_command(*, command: ProjectCommand, from_workspace: bool = False) -> Non def run_workspace_command( + *, workspace_command: WorkspaceProjectCommand, project_names: list[str] | None = None, project_type: str | None = None, + concurrent: bool = True, + extra_args: tuple[str] | None = None, ) -> None: """Executes a workspace command, potentially limited to specified projects. @@ -223,50 +229,47 @@ def run_workspace_command( workspace_command (WorkspaceProjectCommand): The workspace command to be executed. project_names (list[str] | None): Optional; specifies a subset of projects to execute the command for. project_type (str | None): Optional; specifies a subset of project types to execute the command for. + concurrent (bool): Whether to execute commands concurrently. Defaults to True. + extra_args (tuple[str] | None): Optional; additional arguments to pass to the command. """ def _execute_command(cmd: ProjectCommand) -> None: """Helper function to execute a single project command within the workspace context.""" logger.info(f"⏳ {cmd.project_name}: '{cmd.name}' command in progress...") try: - run_command(command=cmd, from_workspace=True) + run_command(command=cmd, from_workspace=True, extra_args=extra_args) executed_commands = " && ".join(" ".join(command) for command in cmd.commands) logger.info(f"✅ {cmd.project_name}: '{executed_commands}' executed successfully.") except Exception as e: logger.error(f"❌ {cmd.project_name}: {e}") raise click.ClickException(f"failed to execute '{cmd.name}' command in '{cmd.project_name}'") from e - if workspace_command.execution_order: - logger.info("Detected execution order, running commands sequentially") - order_map = {name: i for i, name in enumerate(workspace_command.execution_order)} - sorted_commands = sorted( - workspace_command.commands, key=lambda c: order_map.get(c.project_name, len(order_map)) + def _filter_command(cmd: ProjectCommand) -> bool: + return (not project_names or cmd.project_name in project_names) and ( + not project_type or project_type == cmd.project_type ) - if project_names: - existing_projects = {cmd.project_name for cmd in workspace_command.commands} - missing_projects = set(project_names) - existing_projects - if missing_projects: - logger.warning(f"Missing projects: {', '.join(missing_projects)}. Proceeding with available ones.") - - for cmd in sorted_commands: - if ( - project_names - and cmd.project_name not in project_names - or (project_type and project_type != cmd.project_type) - ): - continue + is_sequential = workspace_command.execution_order or not concurrent + logger.info(f"Running commands {'sequentially' if is_sequential else 'concurrently'}.") + + filtered_commands = list(filter(_filter_command, workspace_command.commands)) + + if project_names: + existing_projects = {cmd.project_name for cmd in filtered_commands} + missing_projects = set(project_names) - existing_projects + if missing_projects: + logger.warning(f"Missing projects: {', '.join(missing_projects)}. Proceeding with available ones.") + + if is_sequential: + if workspace_command.execution_order: + order_map = {name: i for i, name in enumerate(workspace_command.execution_order)} + filtered_commands.sort(key=lambda c: order_map.get(c.project_name, len(order_map))) + + for cmd in filtered_commands: _execute_command(cmd) else: with ThreadPoolExecutor() as executor: - futures = { - executor.submit(_execute_command, cmd): cmd - for cmd in workspace_command.commands - if (not project_names or cmd.project_name in project_names) - and (not project_type or project_type == cmd.project_type) - } - for future in as_completed(futures): - future.result() + list(executor.map(_execute_command, filtered_commands)) def load_commands(project_dir: Path) -> list[ProjectCommand] | list[WorkspaceProjectCommand] | None: diff --git a/tests/project/run/test_run.test_run_command_from_workspace_execution_error.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_execution_error.approved.txt index 80f7b144..4818b954 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_execution_error.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_execution_error.approved.txt @@ -1,4 +1,4 @@ -Detected execution order, running commands sequentially +Running commands sequentially. ⏳ frontend_project: 'hello' command in progress... ERROR: ····················· project run 'hello' command output: ······················ diff --git a/tests/project/run/test_run.test_run_command_from_workspace_filtered.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_filtered.approved.txt index 1ab29407..952945b8 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_filtered.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_filtered.approved.txt @@ -1,3 +1,3 @@ -Detected execution order, running commands sequentially +Running commands sequentially. ⏳ contract_project: 'hello' command in progress... ✅ contract_project: 'command_a' executed successfully. diff --git a/tests/project/run/test_run.test_run_command_from_workspace_filtered_no_project.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_filtered_no_project.approved.txt index 43b76eb3..6d4ea120 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_filtered_no_project.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_filtered_no_project.approved.txt @@ -1,2 +1,2 @@ -Detected execution order, running commands sequentially +Running commands sequentially. WARNING: Missing projects: contract_project2. Proceeding with available ones. diff --git a/tests/project/run/test_run.test_run_command_from_workspace_resolution_error.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_resolution_error.approved.txt index 58e0dd5a..9de847c7 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_resolution_error.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_resolution_error.approved.txt @@ -1,4 +1,4 @@ -Detected execution order, running commands sequentially +Running commands sequentially. ⏳ frontend_project: 'hello' command in progress... ERROR: 'hello' failed executing: 'failthiscommand' ERROR: ❌ frontend_project: Failed to resolve command path, 'failthiscommand' wasn't found diff --git a/tests/project/run/test_run.test_run_command_from_workspace_sequential_success.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_sequential_success.approved.txt index af8edab5..0d02bf12 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_sequential_success.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_sequential_success.approved.txt @@ -1,4 +1,4 @@ -Detected execution order, running commands sequentially +Running commands sequentially. ⏳ contract_project: 'hello' command in progress... ✅ contract_project: 'command_a' executed successfully. ⏳ frontend_project: 'hello' command in progress... diff --git a/tests/project/run/test_run.test_run_command_from_workspace_success.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_success.approved.txt index 1ab29407..952945b8 100644 --- a/tests/project/run/test_run.test_run_command_from_workspace_success.approved.txt +++ b/tests/project/run/test_run.test_run_command_from_workspace_success.approved.txt @@ -1,3 +1,3 @@ -Detected execution order, running commands sequentially +Running commands sequentially. ⏳ contract_project: 'hello' command in progress... ✅ contract_project: 'command_a' executed successfully. From cc53b7ad34b92b28ceff24c3111cfebf4604603c Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sun, 18 Aug 2024 21:09:02 +0200 Subject: [PATCH 02/13] chore: regen docs --- docs/cli/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index bcceabdf..1ab42d48 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -132,6 +132,7 @@ - [-p, --project-name ](#-p---project-name--1) - [Arguments](#arguments-8) - [ENVIRONMENT_NAME](#environment_name) + - [EXTRA_ARGS](#extra_args) - [link](#link) - [Options](#options-22) - [-p, --project-name ](#-p---project-name--2) @@ -916,7 +917,7 @@ algokit project bootstrap poetry [OPTIONS] Deploy smart contracts from AlgoKit compliant repository. ```shell -algokit project deploy [OPTIONS] [ENVIRONMENT_NAME] +algokit project deploy [OPTIONS] [ENVIRONMENT_NAME] [EXTRA_ARGS]... ``` ### Options @@ -951,6 +952,10 @@ Specify the project directory. If not provided, current working directory will b ### ENVIRONMENT_NAME Optional argument + +### EXTRA_ARGS +Optional argument(s) + ### link Automatically invoke 'algokit generate client' on contract projects available in the workspace. From 0c4b4f3e28296ca588754dcb053618c9d4e6a66e Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 21 Aug 2024 00:47:26 +0200 Subject: [PATCH 03/13] refactor: addressing pr comments --- docs/features/project/run.md | 27 +++++++++++++++++++-------- src/algokit/cli/project/run.py | 16 ++++++++-------- src/algokit/core/project/run.py | 6 +++--- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/docs/features/project/run.md b/docs/features/project/run.md index c16b1c03..a6031e28 100644 --- a/docs/features/project/run.md +++ b/docs/features/project/run.md @@ -15,7 +15,7 @@ This command executes a custom command defined in the `.algokit.toml` file of th - `-l, --list`: List all projects associated with the workspace command. (Optional) - `-p, --project-name`: Execute the command on specified projects. Defaults to all projects in the current directory. (Optional) - `-t, --type`: Limit execution to specific project types if executing from workspace. (Optional) -- `-c, --concurrent / -s, --sequential`: Execute workspace commands concurrently (default) or sequentially. (Optional) +- `-s, --sequential`: Execute workspace commands sequentially, for cases where you do not have a preference on the execution order, but want to disable concurrency. (Optional, defaults to concurrent) - `[ARGS]...`: Additional arguments to pass to the custom command. These will be appended to the end of the command specified in the `.algokit.toml` file. To get detailed help on the above options, execute: @@ -129,7 +129,7 @@ Executing `algokit project run hello` from the root of `project_(a|b)` will exec ### Controlling Execution Order -Customize command execution in workspaces for precise control: +Customize the execution order of commands in workspaces for precise control: 1. Define order in `.algokit.toml`: @@ -143,18 +143,29 @@ Customize command execution in workspaces for precise control: ``` 2. Execution behavior: - + - Projects are executed in the specified order - Invalid project names are skipped - Partial project lists: Specified projects run first, others follow -3. Control concurrency: +> Note: Explicit order always triggers sequential execution. + +### Controlling Concurrency + +You can control whether commands are executed concurrently or sequentially: + +1. Use command-line options: + ```sh - $ algokit project run hello {-s|--sequential}/{-c|--concurrent} + $ algokit project run hello -s # or --sequential + $ algokit project run hello -c # or --concurrent ``` - - Default: Concurrent - - Sequential: Enforced when order is specified -> Note: Explicit order always triggers sequential execution. +2. Behavior: + - Default: Concurrent execution + - Sequential: Use `-s` or `--sequential` flag + - Concurrent: Use `-c` or `--concurrent` flag or omit the flag (defaults to concurrent) + +> Note: When an explicit order is specified in `.algokit.toml`, execution is always sequential regardless of these flags. ### Passing Extra Arguments diff --git a/src/algokit/cli/project/run.py b/src/algokit/cli/project/run.py index d5880597..da09e8a0 100644 --- a/src/algokit/cli/project/run.py +++ b/src/algokit/cli/project/run.py @@ -47,7 +47,7 @@ def base_command( # noqa: PLR0913 project_names: tuple[str] | None = None, list_projects: bool = False, project_type: str | None = None, - concurrent: bool = True, + sequential: bool = False, extra_args: tuple[str] | None = None, ) -> None: """ @@ -64,7 +64,7 @@ def base_command( # noqa: PLR0913 list_projects (bool): Optional. A flag indicating whether to list projects associated with a workspace command. project_type (str | None): Optional. Only execute commands in projects of specified type. - concurrent (bool): Whether to execute wokspace commands concurrently. Defaults to True. + sequential (bool): Whether to execute wokspace commands sequentially. Defaults to False. Returns: None """ @@ -80,7 +80,7 @@ def base_command( # noqa: PLR0913 workspace_command=custom_command, project_names=list(project_names or []), project_type=project_type, - concurrent=concurrent, + sequential=sequential, extra_args=extra_args, ) @@ -123,11 +123,11 @@ def base_command( # noqa: PLR0913 help="Limit execution to specific project types if executing from workspace. (Optional)", )(command) command = click.option( - "concurrent", - "--concurrent/--sequential", - "-c/-s", - help="Execute workspace commands concurrently", - default=True, + "sequential", + "--sequential/--concurrent", + "-s/-c", + help="Execute workspace commands sequentially. Defaults to concurrent.", + default=False, is_flag=True, required=False, )(command) diff --git a/src/algokit/core/project/run.py b/src/algokit/core/project/run.py index 5d7b7802..35fb1fa4 100644 --- a/src/algokit/core/project/run.py +++ b/src/algokit/core/project/run.py @@ -220,7 +220,7 @@ def run_workspace_command( workspace_command: WorkspaceProjectCommand, project_names: list[str] | None = None, project_type: str | None = None, - concurrent: bool = True, + sequential: bool = False, extra_args: tuple[str] | None = None, ) -> None: """Executes a workspace command, potentially limited to specified projects. @@ -229,7 +229,7 @@ def run_workspace_command( workspace_command (WorkspaceProjectCommand): The workspace command to be executed. project_names (list[str] | None): Optional; specifies a subset of projects to execute the command for. project_type (str | None): Optional; specifies a subset of project types to execute the command for. - concurrent (bool): Whether to execute commands concurrently. Defaults to True. + sequential (bool): Whether to execute commands sequentially. Defaults to False. extra_args (tuple[str] | None): Optional; additional arguments to pass to the command. """ @@ -249,7 +249,7 @@ def _filter_command(cmd: ProjectCommand) -> bool: not project_type or project_type == cmd.project_type ) - is_sequential = workspace_command.execution_order or not concurrent + is_sequential = workspace_command.execution_order or sequential logger.info(f"Running commands {'sequentially' if is_sequential else 'concurrently'}.") filtered_commands = list(filter(_filter_command, workspace_command.commands)) From c25f2ef90efa466348ba4c6805395d8b4f508f36 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 21 Aug 2024 15:44:38 +0200 Subject: [PATCH 04/13] docs: remove typos --- docs/features/project/deploy.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/features/project/deploy.md b/docs/features/project/deploy.md index 06686825..38c90a61 100644 --- a/docs/features/project/deploy.md +++ b/docs/features/project/deploy.md @@ -198,21 +198,9 @@ In this example, `my_contract_name` is an extra argument that can be utilized un ## Example of a Full Deployment -```sh -$ algokit project deploy testnet --custom-deploy-command="your-custom-command" --skip-tests -``` - -This example shows how to deploy smart contracts to the testnet using a custom deploy command and passing an extra argument `--skip-tests`. - -// ... rest of the existing content ... - -```` - -## Example of a Full Deployment - ```sh $ algokit project deploy testnet --custom-deploy-command="your-custom-command" -```` +``` This example shows how to deploy smart contracts to the testnet using a custom deploy command. This also assumes that .algokit.toml file is present in the current working directory, and .env.testnet file is present in the current working directory and contains the required environment variables for deploying to TestNet environment. From e0f4da6235227b340626f8816444d8476ca3c20a Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 21 Aug 2024 17:10:19 +0200 Subject: [PATCH 05/13] test: adding extra snapshot tests for extra args and sequential flag --- src/algokit/core/project/run.py | 4 + tests/project/deploy/test_deploy.py | 39 ++++++ ...y.test_deploy_with_extra_args.approved.txt | 10 ++ ...extra_args_and_custom_command.approved.txt | 12 ++ tests/project/run/test_run.py | 123 ++++++++++++++++++ ...om_standalone_with_extra_args.approved.txt | 6 + ...rom_workspace_with_extra_args.approved.txt | 3 + ...extra_args_and_project_filter.approved.txt | 3 + 8 files changed, 200 insertions(+) create mode 100644 tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt create mode 100644 tests/project/deploy/test_deploy.test_deploy_with_extra_args_and_custom_command.approved.txt create mode 100644 tests/project/run/test_run.test_run_command_from_standalone_with_extra_args.approved.txt create mode 100644 tests/project/run/test_run.test_run_command_from_workspace_with_extra_args.approved.txt create mode 100644 tests/project/run/test_run.test_run_command_from_workspace_with_extra_args_and_project_filter.approved.txt diff --git a/src/algokit/core/project/run.py b/src/algokit/core/project/run.py index 35fb1fa4..6e3de98d 100644 --- a/src/algokit/core/project/run.py +++ b/src/algokit/core/project/run.py @@ -211,6 +211,8 @@ def run_command(*, command: ProjectCommand, from_workspace: bool = False, extra_ if is_verbose: log_msg = f"Command Executed: '{' '.join(cmd)}'\nOutput: {result.output}\n" if index == len(command.commands) - 1: + if extra_args: + log_msg += f"Extra Args: '{' '.join(extra_args)}'\n" log_msg += f"✅ {command.project_name}: '{' '.join(cmd)}' executed successfully." logger.info(log_msg) @@ -239,6 +241,8 @@ def _execute_command(cmd: ProjectCommand) -> None: try: run_command(command=cmd, from_workspace=True, extra_args=extra_args) executed_commands = " && ".join(" ".join(command) for command in cmd.commands) + if extra_args: + executed_commands += f" {' '.join(extra_args)}" logger.info(f"✅ {cmd.project_name}: '{executed_commands}' executed successfully.") except Exception as e: logger.error(f"❌ {cmd.project_name}: {e}") diff --git a/tests/project/deploy/test_deploy.py b/tests/project/deploy/test_deploy.py index bc797d4f..42cfba40 100644 --- a/tests/project/deploy/test_deploy.py +++ b/tests/project/deploy/test_deploy.py @@ -468,3 +468,42 @@ def test_deploy_dispenser_alias( assert passed_env_vars[env_var_name] == from_private_key(dummy_account_pk) # type: ignore[no-untyped-call] verify(result.output, options=NamerFactory.with_parameters(alias)) + + +def test_deploy_with_extra_args(tmp_path_factory: TempPathFactory, proc_mock: ProcMock, which_mock: WhichMock) -> None: + config_with_deploy = """ +[project.deploy] +command = "command_a" + """.strip() + + cwd = tmp_path_factory.mktemp("cwd") + (cwd / ALGOKIT_CONFIG).write_text(config_with_deploy, encoding="utf-8") + (cwd / ".env").touch() + + cmd_resolved = which_mock.add("command_a") + proc_mock.set_output([cmd_resolved], ["command executed"]) + + extra_args = ["--arg1 value1 --arg2 value2"] + result = invoke(["project", "deploy", "localnet", "--", *extra_args], cwd=cwd) + + assert result.exit_code == 0 + assert proc_mock.called[0].command == [cmd_resolved, *extra_args] + verify(result.output) + + +def test_deploy_with_extra_args_and_custom_command( + tmp_path_factory: TempPathFactory, proc_mock: ProcMock, which_mock: WhichMock +) -> None: + cwd = tmp_path_factory.mktemp("cwd") + (cwd / ".env").touch() + + custom_command = "custom_command" + cmd_resolved = which_mock.add(custom_command) + proc_mock.set_output([cmd_resolved], ["custom command executed"]) + + extra_args = ["--custom-arg1 custom-value1 --custom-arg2 custom-value2"] + result = invoke(["project", "deploy", "localnet", "--command", custom_command, "--", *extra_args], cwd=cwd) + + assert result.exit_code == 0 + assert proc_mock.called[0].command == [cmd_resolved, *extra_args] + verify(result.output) diff --git a/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt b/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt new file mode 100644 index 00000000..c128019e --- /dev/null +++ b/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt @@ -0,0 +1,10 @@ +DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml +DEBUG: Deploying from project directory: {current_working_directory} +DEBUG: Loading deploy command from project config +DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml +Using deploy command: /bin/command_a --arg1 value1 --arg2 value2 +Loading deployment environment variables... +DEBUG: Using default environment config for algod and indexer for network localnet +Deploying smart contracts from AlgoKit compliant repository 🚀 +DEBUG: Running '/bin/command_a --arg1 value1 --arg2 value2' in '{current_working_directory}' +/bin/command_a: command executed diff --git a/tests/project/deploy/test_deploy.test_deploy_with_extra_args_and_custom_command.approved.txt b/tests/project/deploy/test_deploy.test_deploy_with_extra_args_and_custom_command.approved.txt new file mode 100644 index 00000000..26e687c1 --- /dev/null +++ b/tests/project/deploy/test_deploy.test_deploy_with_extra_args_and_custom_command.approved.txt @@ -0,0 +1,12 @@ +DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml +DEBUG: No .algokit.toml file found in the project directory. +DEBUG: Deploying from project directory: {current_working_directory} +DEBUG: Loading deploy command from project config +DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml +DEBUG: No .algokit.toml file found in the project directory. +Using deploy command: /bin/custom_command --custom-arg1 custom-value1 --custom-arg2 custom-value2 +Loading deployment environment variables... +DEBUG: Using default environment config for algod and indexer for network localnet +Deploying smart contracts from AlgoKit compliant repository 🚀 +DEBUG: Running '/bin/custom_command --custom-arg1 custom-value1 --custom-arg2 custom-value2' in '{current_working_directory}' +/bin/custom_command: custom command executed diff --git a/tests/project/run/test_run.py b/tests/project/run/test_run.py index 5c2d4640..00342fd9 100644 --- a/tests/project/run/test_run.py +++ b/tests/project/run/test_run.py @@ -477,3 +477,126 @@ def test_run_command_help_works_without_path_resolution( verify(_format_output(result.output)) assert invoke("project run hello", cwd=cwd).exit_code == 1 + + +def test_run_command_from_workspace_with_sequential_flag( + tmp_path_factory: TempPathFactory, which_mock: WhichMock, proc_mock: ProcMock +) -> None: + cwd = tmp_path_factory.mktemp("cwd") / "algokit_project" + projects = [] + for i in range(1, 6): + projects.append( + { + "dir": f"project{i}", + "type": "contract", + "name": f"contract_project_{i}", + "command": f"hello{i}", + "description": "Prints hello", + } + ) + _create_workspace_project( + workspace_dir=cwd, + projects=projects, + mock_command=True, + which_mock=which_mock, + proc_mock=proc_mock, + ) + + result = invoke("project run hello --sequential", cwd=cwd) + assert result.exit_code == 0 + order_of_execution = [line for line in result.output.split("\n") if line.startswith("✅")] + for i in range(5): + assert f"contract_project_{i + 1}" in order_of_execution[i] + + +def test_run_command_from_workspace_with_order_and_sequential_flag( + tmp_path_factory: TempPathFactory, which_mock: WhichMock, proc_mock: ProcMock +) -> None: + cwd = tmp_path_factory.mktemp("cwd") / "algokit_project" + projects = [] + for i in range(1, 6): + projects.append( + { + "dir": f"project{i}", + "type": "contract", + "name": f"contract_project_{i}", + "command": f"hello{i}", + "description": "Prints hello", + } + ) + _create_workspace_project( + workspace_dir=cwd, + projects=projects, + mock_command=True, + which_mock=which_mock, + proc_mock=proc_mock, + custom_project_order=["contract_project_4"], + ) + + result = invoke("project run hello --sequential", cwd=cwd) + assert result.exit_code == 0 + order_of_execution = [line for line in result.output.split("\n") if line.startswith("✅")] + assert "contract_project_4" in order_of_execution[0] + + +def test_run_command_from_standalone_with_extra_args( + tmp_path_factory: TempPathFactory, which_mock: WhichMock, proc_mock: ProcMock +) -> None: + """ + Verifies successful command execution within a standalone project with extra arguments. + """ + cwd = tmp_path_factory.mktemp("cwd") / "algokit_project" + cwd.mkdir() + + which_mock.add("echo") + proc_mock.set_output(["echo", "Hello", "extra", "args"], ["Hello extra args"]) + _create_project_config(cwd, "contract", "contract_project", "echo Hello", "Prints hello with extra args") + + result = invoke("project run hello -- extra args", cwd=cwd) + + assert result.exit_code == 0 + verify(_format_output(result.output)) + assert "Hello extra args" in result.output + + +def test_run_command_from_workspace_with_extra_args( + tmp_path_factory: TempPathFactory, which_mock: WhichMock, proc_mock: ProcMock +) -> None: + """ + Verifies successful command execution within a workspace project with extra arguments. + """ + cwd = tmp_path_factory.mktemp("cwd") / "algokit_project" + projects = [ + { + "dir": "project1", + "type": "contract", + "name": "contract_project", + "command": "echo Hello", + "description": "Prints hello with extra args", + }, + ] + _create_workspace_project( + workspace_dir=cwd, projects=projects, mock_command=True, which_mock=which_mock, proc_mock=proc_mock + ) + + which_mock.add("echo") + proc_mock.set_output(["echo", "Hello", "extra", "args"], ["Hello extra args"]) + + result = invoke("project run hello -- extra args", cwd=cwd) + + assert result.exit_code == 0 + verify(_format_output(result.output)) + assert "Hello extra args" in result.output + + +def test_run_command_from_workspace_with_extra_args_and_project_filter(cwd_with_workspace_sequential: Path) -> None: + """ + Verifies successful command execution within a workspace project with extra arguments and project filtering. + """ + result = invoke( + "project run hello --project-name 'contract_project' -- extra args", cwd=cwd_with_workspace_sequential + ) + + assert result.exit_code == 0 + verify(_format_output(result.output)) + assert "frontend_project" not in result.output diff --git a/tests/project/run/test_run.test_run_command_from_standalone_with_extra_args.approved.txt b/tests/project/run/test_run.test_run_command_from_standalone_with_extra_args.approved.txt new file mode 100644 index 00000000..9c491369 --- /dev/null +++ b/tests/project/run/test_run.test_run_command_from_standalone_with_extra_args.approved.txt @@ -0,0 +1,6 @@ +Running `hello` command in {current_working_directory}... +Command Executed: 'echo Hello' +Output: STDOUT +STDERR +Extra Args: 'extra args' +✅ contract_project: 'echo Hello' executed successfully. diff --git a/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args.approved.txt new file mode 100644 index 00000000..6776612b --- /dev/null +++ b/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args.approved.txt @@ -0,0 +1,3 @@ +Running commands sequentially. +⏳ contract_project: 'hello' command in progress... +✅ contract_project: 'echo Hello extra args' executed successfully. diff --git a/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args_and_project_filter.approved.txt b/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args_and_project_filter.approved.txt new file mode 100644 index 00000000..fb373b90 --- /dev/null +++ b/tests/project/run/test_run.test_run_command_from_workspace_with_extra_args_and_project_filter.approved.txt @@ -0,0 +1,3 @@ +Running commands sequentially. +⏳ contract_project: 'hello' command in progress... +✅ contract_project: 'command_a extra args' executed successfully. From 968cf8cf2f01681b568b272511ba9e87c6926b2b Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:18:56 +0200 Subject: [PATCH 06/13] refactor: minor improvements --- docs/features/project/deploy.md | 6 ++- src/algokit/cli/common/utils.py | 49 +++++++++++++++++ src/algokit/cli/project/deploy.py | 54 +++++++++++++++---- src/algokit/cli/project/run.py | 7 +-- src/algokit/core/project/__init__.py | 2 +- src/algokit/core/project/run.py | 12 +++-- tests/project/deploy/test_deploy.py | 7 +-- ...y.test_deploy_with_extra_args.approved.txt | 1 - 8 files changed, 114 insertions(+), 24 deletions(-) diff --git a/docs/features/project/deploy.md b/docs/features/project/deploy.md index 38c90a61..859938d6 100644 --- a/docs/features/project/deploy.md +++ b/docs/features/project/deploy.md @@ -186,7 +186,9 @@ $ algokit project deploy testnet --ci ## Passing Extra Arguments -You can pass additional arguments to the deploy command. These extra arguments will be appended to the end of the deploy command specified in your `.algokit.toml` file or to the command specifified directly via `--command` option. Declare after `--` to mark distinction between arguments used by cli vs arguments to be passed as extras to the deploy command/script defined. +You can pass additional arguments to the deploy command. These extra arguments will be appended to the end of the deploy command specified in your `.algokit.toml` file or to the command specified directly via `--command` option. + +To pass extra arguments, use `--` after the AlgoKit command and options to mark the distinction between arguments used by the CLI and arguments to be passed as extras to the deploy command/script. Example: @@ -194,7 +196,7 @@ Example: $ algokit project deploy testnet -- my_contract_name --some_contract_related_param ``` -In this example, `my_contract_name` is an extra argument that can be utilized under custom deploy command invocation to filter the deployment to a specific contract. +In this example, `my_contract_name` and `--some_contract_related_param` are extra arguments that can be utilized by the custom deploy command invocation, for instance, to filter the deployment to a specific contract or modify deployment behavior. ## Example of a Full Deployment diff --git a/src/algokit/cli/common/utils.py b/src/algokit/cli/common/utils.py index 55b21220..412c7004 100644 --- a/src/algokit/cli/common/utils.py +++ b/src/algokit/cli/common/utils.py @@ -2,10 +2,12 @@ import typing as t +from pathlib import Path import click from algokit.cli.common.constants import ExplorerEntityType +from algokit.core.utils import is_windows class MutuallyExclusiveOption(click.Option): @@ -101,3 +103,50 @@ def get_explorer_url(identifier: str | int, network: str, entity_type: ExplorerE """ return f"https://explore.algokit.io/{network}/{entity_type.value}/{identifier}" + + +def sanitize_extra_args(extra_args: t.Sequence[str]) -> tuple[str, ...]: + """ + Sanitizes and formats extra arguments for command execution across different OSes. + + Args: + extra_args (t.Sequence[str]): A sequence of extra arguments to sanitize. + + Returns: + tuple[str, ...]: A sanitized list of extra arguments. + + Examples: + >>> sanitize_extra_args(["arg1", "arg with spaces", "--flag=value"]) + ['arg1', 'arg with spaces', '--flag=value'] + >>> sanitize_extra_args(("--extra bla bla bla",)) + ['--extra', 'bla', 'bla', 'bla'] + >>> sanitize_extra_args(["--complex='quoted value'", "--multi word"]) + ["--complex='quoted value'", '--multi', 'word'] + >>> sanitize_extra_args([r"C:\\Program Files\\My App", "%PATH%"]) + ['C:\\Program Files\\My App', '%PATH%'] + """ + + lex = __import__("mslex" if is_windows() else "shlex") + + def sanitize_arg(arg: str) -> str: + # Normalize path separators + arg = str(Path(arg)) + + # Handle environment variables + if arg.startswith("%") and arg.endswith("%"): + return arg # Keep Windows-style env vars as-is + elif arg.startswith("$"): + return arg # Keep Unix-style env vars as-is + + # Escape special characters and handle Unicode + return lex.quote(arg) # type: ignore[no-any-return] + + sanitized_args: tuple[str, ...] = () + for arg in extra_args: + # Split the argument if it contains multiple space-separated values + split_args = lex.split(arg) + for split_arg in split_args: + sanitized_arg = sanitize_arg(split_arg) + sanitized_args += (sanitized_arg,) + + return sanitized_args diff --git a/src/algokit/cli/project/deploy.py b/src/algokit/cli/project/deploy.py index 7d756974..9d717063 100644 --- a/src/algokit/cli/project/deploy.py +++ b/src/algokit/cli/project/deploy.py @@ -6,7 +6,7 @@ import click from algosdk.mnemonic import from_private_key -from algokit.cli.common.utils import MutuallyExclusiveOption +from algokit.cli.common.utils import MutuallyExclusiveOption, sanitize_extra_args from algokit.core import proc from algokit.core.conf import ALGOKIT_CONFIG, get_algokit_config from algokit.core.project import ProjectType, get_project_configs @@ -88,7 +88,7 @@ def _execute_deploy_command( # noqa: PLR0913 interactive: bool, deployer_alias: str | None, dispenser_alias: str | None, - extra_args: tuple[str], + extra_args: tuple[str, ...], ) -> None: logger.debug(f"Deploying from project directory: {path}") logger.debug("Loading deploy command from project config") @@ -132,7 +132,7 @@ def _execute_deploy_command( # noqa: PLR0913 raise click.ClickException(f"Deployment command exited with error code = {result.exit_code}") -class CommandParamType(click.types.StringParamType): +class _CommandParamType(click.types.StringParamType): name = "command" def convert( @@ -149,12 +149,47 @@ def convert( raise click.BadParameter(str(ex), param=param, ctx=ctx) from ex -@click.command("deploy") -@click.argument("environment_name", default=None, required=False) +class _DeployCommand(click.Command): + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + # Join all args into a single string + full_command = " ".join(args) + + try: + separator_index = full_command.find("-- ") + if separator_index == -1: + raise ValueError("No separator found") + main_args = args[:separator_index] + extra_args = args[separator_index + 1 :] + except Exception: + main_args = args + extra_args = [] + + # Ensure we have at least one argument for environment_name if extra_args exist + if extra_args and len(main_args) == 0: + main_args.insert(0, "") + + # Reconstruct args list + args = main_args + (["--"] if extra_args else []) + extra_args + + return super().parse_args(ctx, args) + + +@click.command( + "deploy", + context_settings={"ignore_unknown_options": True}, + cls=_DeployCommand, +) +@click.argument( + "environment_name", + default=None, + required=False, + callback=lambda _, __, value: None if value == "" else value, +) @click.option( "--command", "-C", - type=CommandParamType(), + "-c", + type=_CommandParamType(), default=None, help=("Custom deploy command. If not provided, will load the deploy command " "from .algokit.toml file."), required=False, @@ -213,7 +248,7 @@ def convert( @click.argument( "extra_args", nargs=-1, - type=click.UNPROCESSED, + required=False, ) @click.pass_context def deploy_command( # noqa: PLR0913 @@ -225,10 +260,11 @@ def deploy_command( # noqa: PLR0913 path: Path, deployer_alias: str | None, dispenser_alias: str | None, - project_names: tuple[str], - extra_args: tuple[str], + project_names: tuple[str, ...], + extra_args: tuple[str, ...], ) -> None: """Deploy smart contracts from AlgoKit compliant repository.""" + extra_args = sanitize_extra_args(extra_args) if ctx.parent and ctx.parent.command.name == "algokit": click.secho( diff --git a/src/algokit/cli/project/run.py b/src/algokit/cli/project/run.py index da09e8a0..1ef8bf45 100644 --- a/src/algokit/cli/project/run.py +++ b/src/algokit/cli/project/run.py @@ -4,7 +4,7 @@ import click -from algokit.cli.common.utils import MutuallyExclusiveOption +from algokit.cli.common.utils import MutuallyExclusiveOption, sanitize_extra_args from algokit.core.project import ProjectType from algokit.core.project.run import ( ProjectCommand, @@ -48,7 +48,7 @@ def base_command( # noqa: PLR0913 list_projects: bool = False, project_type: str | None = None, sequential: bool = False, - extra_args: tuple[str] | None = None, + extra_args: tuple[str, ...] | None = None, ) -> None: """ Executes a base command function with optional parameters for listing projects or specifying project names. @@ -58,7 +58,7 @@ def base_command( # noqa: PLR0913 within a workspace. Args: - extra_args (list[str]): The command arguments to be passed to the custom command. + extra_args (tuple[str, ...]): The command arguments to be passed to the custom command. custom_command (ProjectCommand | WorkspaceProjectCommand): The custom command to be executed. project_names (list[str] | None): Optional. A list of project names to execute the command on. list_projects (bool): Optional. A flag indicating whether to list projects associated @@ -68,6 +68,7 @@ def base_command( # noqa: PLR0913 Returns: None """ + extra_args = sanitize_extra_args(extra_args or ()) if list_projects and isinstance(custom_command, WorkspaceProjectCommand): for command in custom_command.commands: cmds = " && ".join(" ".join(cmd) for cmd in command.commands) diff --git a/src/algokit/core/project/__init__.py b/src/algokit/core/project/__init__.py index 08de5a2a..cd3ee284 100644 --- a/src/algokit/core/project/__init__.py +++ b/src/algokit/core/project/__init__.py @@ -75,7 +75,7 @@ def get_project_configs( working directory is used. lookup_level (int): The number of levels to go up the directory to search for workspace projects project_type (str | None): The type of project to filter by. If None, all project types are returned. - project_names (list[str] | None): The names of the projects to filter by. If None, all projects are returned. + project_names (tuple[str, ...] | None): The names of the projects to filter by. If None, gets all projects. Returns: list[dict[str, Any] | None]: A list of dictionaries, each containing the configuration of an algokit project. diff --git a/src/algokit/core/project/run.py b/src/algokit/core/project/run.py index 6e3de98d..dfdd7bb9 100644 --- a/src/algokit/core/project/run.py +++ b/src/algokit/core/project/run.py @@ -162,13 +162,15 @@ def _load_commands_from_workspace( return list(workspace_commands.values()) -def run_command(*, command: ProjectCommand, from_workspace: bool = False, extra_args: tuple[str] | None = None) -> None: +def run_command( + *, command: ProjectCommand, from_workspace: bool = False, extra_args: tuple[str, ...] | None = None +) -> None: """Executes a specified project command. Args: command (ProjectCommand): The project command to be executed. from_workspace (bool): Indicates whether the command is being executed from a workspace context. - extra_args (tuple[str] | None): Optional; additional arguments to pass to the command. + extra_args (tuple[str, ...] | None): Optional; additional arguments to pass to the command. Raises: click.ClickException: If the command execution fails. @@ -223,7 +225,7 @@ def run_workspace_command( project_names: list[str] | None = None, project_type: str | None = None, sequential: bool = False, - extra_args: tuple[str] | None = None, + extra_args: tuple[str, ...] | None = None, ) -> None: """Executes a workspace command, potentially limited to specified projects. @@ -232,14 +234,14 @@ def run_workspace_command( project_names (list[str] | None): Optional; specifies a subset of projects to execute the command for. project_type (str | None): Optional; specifies a subset of project types to execute the command for. sequential (bool): Whether to execute commands sequentially. Defaults to False. - extra_args (tuple[str] | None): Optional; additional arguments to pass to the command. + extra_args (tuple[str, ...] | None): Optional; additional arguments to pass to the command. """ def _execute_command(cmd: ProjectCommand) -> None: """Helper function to execute a single project command within the workspace context.""" logger.info(f"⏳ {cmd.project_name}: '{cmd.name}' command in progress...") try: - run_command(command=cmd, from_workspace=True, extra_args=extra_args) + run_command(command=cmd, from_workspace=True, extra_args=extra_args or ()) executed_commands = " && ".join(" ".join(command) for command in cmd.commands) if extra_args: executed_commands += f" {' '.join(extra_args)}" diff --git a/tests/project/deploy/test_deploy.py b/tests/project/deploy/test_deploy.py index 42cfba40..795ff784 100644 --- a/tests/project/deploy/test_deploy.py +++ b/tests/project/deploy/test_deploy.py @@ -4,6 +4,7 @@ import pytest from _pytest.tmpdir import TempPathFactory +from algokit.cli.common.utils import sanitize_extra_args from algokit.core.conf import ALGOKIT_CONFIG from algokit.core.tasks.wallet import WALLET_ALIASES_KEYRING_USERNAME from algosdk.account import generate_account @@ -484,10 +485,10 @@ def test_deploy_with_extra_args(tmp_path_factory: TempPathFactory, proc_mock: Pr proc_mock.set_output([cmd_resolved], ["command executed"]) extra_args = ["--arg1 value1 --arg2 value2"] - result = invoke(["project", "deploy", "localnet", "--", *extra_args], cwd=cwd) + result = invoke(["project", "deploy", "--", *extra_args], cwd=cwd) assert result.exit_code == 0 - assert proc_mock.called[0].command == [cmd_resolved, *extra_args] + assert proc_mock.called[0].command == [cmd_resolved, *sanitize_extra_args(extra_args)] verify(result.output) @@ -505,5 +506,5 @@ def test_deploy_with_extra_args_and_custom_command( result = invoke(["project", "deploy", "localnet", "--command", custom_command, "--", *extra_args], cwd=cwd) assert result.exit_code == 0 - assert proc_mock.called[0].command == [cmd_resolved, *extra_args] + assert proc_mock.called[0].command == [cmd_resolved, *sanitize_extra_args(extra_args)] verify(result.output) diff --git a/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt b/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt index c128019e..b06334c1 100644 --- a/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt +++ b/tests/project/deploy/test_deploy.test_deploy_with_extra_args.approved.txt @@ -4,7 +4,6 @@ DEBUG: Loading deploy command from project config DEBUG: Attempting to load project config from {current_working_directory}/.algokit.toml Using deploy command: /bin/command_a --arg1 value1 --arg2 value2 Loading deployment environment variables... -DEBUG: Using default environment config for algod and indexer for network localnet Deploying smart contracts from AlgoKit compliant repository 🚀 DEBUG: Running '/bin/command_a --arg1 value1 --arg2 value2' in '{current_working_directory}' /bin/command_a: command executed From 98a7c4d7990a03f713a045107f4a97d1ffa2b75b Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:20:54 +0200 Subject: [PATCH 07/13] chore: regen docs --- docs/cli/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index 1ab42d48..2854e8bf 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -124,7 +124,7 @@ - [--interactive, --non-interactive, --ci](#--interactive---non-interactive---ci-1) - [deploy](#deploy) - [Options](#options-21) - - [-C, --command ](#-c---command-) + - [-C, -c, --command ](#-c--c---command-) - [--interactive, --non-interactive, --ci](#--interactive---non-interactive---ci-2) - [-P, --path ](#-p---path-) - [--deployer ](#--deployer-) @@ -923,7 +923,7 @@ algokit project deploy [OPTIONS] [ENVIRONMENT_NAME] [EXTRA_ARGS]... ### Options -### -C, --command +### -C, -c, --command Custom deploy command. If not provided, will load the deploy command from .algokit.toml file. From 03daa769b2e21533ca1861cebe1b1748d3eebfbc Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:27:28 +0200 Subject: [PATCH 08/13] chore: extra tests for binary mode --- tests/portability/test_pyinstaller_binary.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index b126d608..71d2b2e4 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -19,16 +19,18 @@ def command_str_to_list(command: str) -> list[str]: @pytest.mark.parametrize( ("command", "exit_codes"), [ - (command_str_to_list("--help"), [0]), - (command_str_to_list("doctor"), [0]), - (command_str_to_list("task vanity-address PY"), [0]), + (command_str_to_list("algokit --help"), [0]), + (command_str_to_list("algokit doctor"), [0]), + (command_str_to_list("algokit task vanity-address PY"), [0]), + (command_str_to_list("algokit -v init --name playground -t python --no-git --no-ide"), [0]), + (command_str_to_list("cd playground && algokit project run build -- hello_world"), [0]), ], ) def test_non_interactive_algokit_commands( command: list[str], exit_codes: list[int], tmp_path_factory: pytest.TempPathFactory ) -> None: cwd = tmp_path_factory.mktemp("cwd") - execution_result = subprocess.run([algokit, *command], capture_output=True, text=True, check=False, cwd=cwd) + execution_result = subprocess.run(command, capture_output=True, text=True, check=False, cwd=cwd) logger.info(f"Command {command} returned {execution_result.stdout}") # Parts of doctor will fail in CI on macOS and windows on github actions since docker isn't available by default From 27439165f1accb4bd2699aacaa3ee3039390f675 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:31:43 +0200 Subject: [PATCH 09/13] chore: testing ci --- tests/portability/test_pyinstaller_binary.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index 71d2b2e4..42bb4ed8 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -23,13 +23,19 @@ def command_str_to_list(command: str) -> list[str]: (command_str_to_list("algokit doctor"), [0]), (command_str_to_list("algokit task vanity-address PY"), [0]), (command_str_to_list("algokit -v init --name playground -t python --no-git --no-ide"), [0]), - (command_str_to_list("cd playground && algokit project run build -- hello_world"), [0]), + (command_str_to_list("algokit project run build -- hello_world"), [0]), ], ) def test_non_interactive_algokit_commands( command: list[str], exit_codes: list[int], tmp_path_factory: pytest.TempPathFactory ) -> None: cwd = tmp_path_factory.mktemp("cwd") + + # Create a 'playground' directory + if "build" in command: + cwd = cwd / "playground" + cwd.mkdir(exist_ok=True) + execution_result = subprocess.run(command, capture_output=True, text=True, check=False, cwd=cwd) logger.info(f"Command {command} returned {execution_result.stdout}") From 800e25e1910a58d969c38cf8063b84e49a892e8c Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:35:50 +0200 Subject: [PATCH 10/13] chore: testing ci --- tests/portability/test_pyinstaller_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index 42bb4ed8..06657ccf 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -22,7 +22,7 @@ def command_str_to_list(command: str) -> list[str]: (command_str_to_list("algokit --help"), [0]), (command_str_to_list("algokit doctor"), [0]), (command_str_to_list("algokit task vanity-address PY"), [0]), - (command_str_to_list("algokit -v init --name playground -t python --no-git --no-ide"), [0]), + (command_str_to_list("algokit -v init --name playground -t python --no-git --no-ide --defaults"), [0]), (command_str_to_list("algokit project run build -- hello_world"), [0]), ], ) From faa089290f47d5dc207741ab296d0aeb8eaf71c1 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:42:22 +0200 Subject: [PATCH 11/13] chore testing ci --- tests/portability/test_pyinstaller_binary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index 06657ccf..32a9d4c1 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -22,8 +22,8 @@ def command_str_to_list(command: str) -> list[str]: (command_str_to_list("algokit --help"), [0]), (command_str_to_list("algokit doctor"), [0]), (command_str_to_list("algokit task vanity-address PY"), [0]), - (command_str_to_list("algokit -v init --name playground -t python --no-git --no-ide --defaults"), [0]), - (command_str_to_list("algokit project run build -- hello_world"), [0]), + (command_str_to_list("algokit init --name playground -t python --no-git --no-ide --defaults"), [0]), + (command_str_to_list("algokit -v project run build"), [0]), ], ) def test_non_interactive_algokit_commands( From 2bc2f6aa9b683741b4c8f083c79bfbf242ac522e Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:51:55 +0200 Subject: [PATCH 12/13] chore: testing ci --- tests/portability/test_pyinstaller_binary.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index 32a9d4c1..3d2c30e7 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -22,8 +22,6 @@ def command_str_to_list(command: str) -> list[str]: (command_str_to_list("algokit --help"), [0]), (command_str_to_list("algokit doctor"), [0]), (command_str_to_list("algokit task vanity-address PY"), [0]), - (command_str_to_list("algokit init --name playground -t python --no-git --no-ide --defaults"), [0]), - (command_str_to_list("algokit -v project run build"), [0]), ], ) def test_non_interactive_algokit_commands( @@ -46,7 +44,25 @@ def test_non_interactive_algokit_commands( assert execution_result.returncode in exit_codes, f"Command {command} failed with {execution_result.stderr}" -def test_algokit_init( +def test_algokit_init_and_project_run(tmp_path_factory: pytest.TempPathFactory) -> None: + cwd = tmp_path_factory.mktemp("cwd") + + # Run algokit init + init_command = command_str_to_list("algokit init --name playground -t python --no-git --no-ide --defaults") + init_result = subprocess.run(init_command, capture_output=True, text=True, check=False, cwd=cwd) + logger.info(f"Command {init_command} returned {init_result.stdout}") + assert init_result.returncode == 0, f"Init command failed with {init_result.stderr}" + + # Run algokit project run build + build_cwd = cwd / "playground" + build_cwd.mkdir(exist_ok=True) + build_command = command_str_to_list("algokit -v project run build") + build_result = subprocess.run(build_command, capture_output=True, text=True, check=False, cwd=build_cwd) + logger.info(f"Command {build_command} returned {build_result.stdout}") + assert build_result.returncode == 0, f"Build command failed with {build_result.stderr}" + + +def test_algokit_init_with_template_url( dummy_algokit_template_with_python_task: dict[str, Path], ) -> None: # TODO: revisit to improve From dd7258bba41dc8fa0d9ec1a07afc8dc16904810a Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 23 Aug 2024 18:56:46 +0200 Subject: [PATCH 13/13] chore: testing ci --- tests/portability/test_pyinstaller_binary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/portability/test_pyinstaller_binary.py b/tests/portability/test_pyinstaller_binary.py index 3d2c30e7..7566a20e 100644 --- a/tests/portability/test_pyinstaller_binary.py +++ b/tests/portability/test_pyinstaller_binary.py @@ -56,7 +56,7 @@ def test_algokit_init_and_project_run(tmp_path_factory: pytest.TempPathFactory) # Run algokit project run build build_cwd = cwd / "playground" build_cwd.mkdir(exist_ok=True) - build_command = command_str_to_list("algokit -v project run build") + build_command = command_str_to_list("algokit -v project run build -- hello_world") build_result = subprocess.run(build_command, capture_output=True, text=True, check=False, cwd=build_cwd) logger.info(f"Command {build_command} returned {build_result.stdout}") assert build_result.returncode == 0, f"Build command failed with {build_result.stderr}"