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.