Skip to content

Commit

Permalink
feat: explicit sequential execution; extra args param for run/deploy …
Browse files Browse the repository at this point in the history
…commands
  • Loading branch information
aorumbayev committed Aug 18, 2024
1 parent 760455d commit 507f0c2
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 63 deletions.
29 changes: 27 additions & 2 deletions docs/features/project/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
63 changes: 48 additions & 15 deletions docs/features/project/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion src/algokit/cli/project/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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."""

Expand Down Expand Up @@ -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(
Expand All @@ -281,4 +289,5 @@ def deploy_command( # noqa: PLR0913
interactive=interactive,
deployer_alias=deployer_alias,
dispenser_alias=dispenser_alias,
extra_args=extra_args,
)
33 changes: 23 additions & 10 deletions src/algokit/cli/project/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
61 changes: 32 additions & 29 deletions src/algokit/core/project/run.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -213,60 +216,60 @@ 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.
Args:
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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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: ······················
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 507f0c2

Please sign in to comment.