Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: explicit sequential execution; extra args param for run/deploy commands #557

Merged
merged 13 commits into from
Aug 23, 2024
11 changes: 8 additions & 3 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@
- [--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-)
- [--dispenser ](#--dispenser-)
- [-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)
Expand Down Expand Up @@ -916,13 +917,13 @@ 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


### -C, --command <command>
### -C, -c, --command <command>
Custom deploy command. If not provided, will load the deploy command from .algokit.toml file.


Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion 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,6 +184,20 @@ 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 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:

```sh
$ algokit project deploy testnet -- my_contract_name --some_contract_related_param
```

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

```sh
Expand Down
74 changes: 59 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)
- `-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:

```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,57 @@ 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 the execution order of commands 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:
- Projects are executed in the specified order
- Invalid project names are skipped
- Partial project lists: Specified projects run first, others follow

> Note: Explicit order always triggers sequential execution.

### Controlling Concurrency

You can control whether commands are executed concurrently or sequentially:

1. Use command-line options:

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:
```sh
$ algokit project run hello -s # or --sequential
$ algokit project run hello -c # or --concurrent
```

- 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.
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

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
49 changes: 49 additions & 0 deletions src/algokit/cli/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
59 changes: 52 additions & 7 deletions src/algokit/cli/project/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down 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 @@ -131,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(
Expand All @@ -148,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,
Expand Down Expand Up @@ -209,6 +245,11 @@ def convert(
"command",
],
)
@click.argument(
"extra_args",
nargs=-1,
required=False,
)
@click.pass_context
def deploy_command( # noqa: PLR0913
ctx: click.Context,
Expand All @@ -219,9 +260,11 @@ def deploy_command( # noqa: PLR0913
path: Path,
deployer_alias: str | None,
dispenser_alias: str | None,
project_names: 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(
Expand Down Expand Up @@ -272,6 +315,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 +325,5 @@ def deploy_command( # noqa: PLR0913
interactive=interactive,
deployer_alias=deployer_alias,
dispenser_alias=dispenser_alias,
extra_args=extra_args,
)
Loading