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 (#557)

* feat: explicit sequential execution; extra args param for run/deploy commands

* chore: regen docs

* refactor: addressing pr comments

* docs: remove typos

* test: adding extra snapshot tests for extra args and sequential flag

* refactor: minor improvements

* chore: regen docs

* chore: extra tests for binary mode

* chore: testing ci

* chore: testing ci

* chore testing ci

* chore: testing ci

* chore: testing ci
  • Loading branch information
aorumbayev authored Aug 23, 2024
1 parent ec5155c commit 41f5c7b
Show file tree
Hide file tree
Showing 22 changed files with 479 additions and 78 deletions.
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

1 comment on commit 41f5c7b

@github-actions
Copy link

Choose a reason for hiding this comment

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

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit
   __init__.py15753%6–13, 17–24, 32–34
   __main__.py440%1–7
src/algokit/cli
   __init__.py47394%31–34
   codespace.py50982%28, 114, 137, 150–155
   completions.py108992%63–64, 83, 93–99
   dispenser.py121199%77
   doctor.py53394%146–148
   explore.py631576%35–40, 42–47, 85–92, 113
   generate.py70396%76–77, 155
   goal.py47198%79
   init.py3112492%497–498, 503–504, 507, 528, 531–533, 544, 548, 606, 632, 661, 694, 703–705, 708–713, 726, 745, 757–758
   localnet.py1523279%65, 86–113, 133–137, 170, 182, 197–207, 220, 271, 292–293
   task.py34391%25–28
src/algokit/cli/common
   utils.py37295%137, 139
src/algokit/cli/project
   bootstrap.py32197%33
   deploy.py1172083%47, 49, 102, 125, 147–149, 270, 277, 291–299, 302–311
   link.py891682%60, 65–66, 101–105, 115–120, 148–149, 218–219, 223
   list.py33585%21–23, 51–56
   run.py46296%38, 174
src/algokit/cli/tasks
   analyze.py81199%81
   assets.py821384%65–66, 72, 74–75, 105, 119, 125–126, 132, 134, 136–137
   ipfs.py51884%52, 80, 92, 94–95, 105–107
   mint.py1061586%51, 73, 100–103, 108, 113, 131–132, 158, 335–339
   send_transaction.py651085%52–53, 57, 89, 158, 170–174
   sign_transaction.py59886%21, 28–30, 71–72, 109, 123
   transfer.py39392%26, 90, 117
   utils.py1144660%29–37, 43–46, 78–79, 103–104, 128–136, 155–165, 212, 261–262, 282–293, 300–302, 324
   vanity_address.py561082%41, 45–48, 112, 114, 121–123
   wallet.py79495%21, 66, 136, 162
src/algokit/core
   codespace.py1756861%34–37, 41–44, 48–71, 111–112, 125–133, 191, 200–202, 210, 216–217, 229–236, 251–298, 311–313, 338–344, 348, 395
   conf.py57984%12, 24, 28, 36, 38, 73–75, 80
   dispenser.py2022687%91, 123–124, 141–149, 191–192, 198–200, 218–219, 259–260, 318, 332–334, 345–346, 356, 369, 384
   doctor.py65789%67–69, 92–94, 134
   generate.py50394%44, 85, 103
   goal.py65494%21, 36–37, 47
   init.py671085%53, 57–62, 70, 81, 88, 108–109
   log_handlers.py68790%50–51, 63, 112–116, 125
   proc.py45198%99
   sandbox.py2632391%32, 89–92, 97, 101–103, 153, 201–208, 219, 590, 606, 631, 639
   typed_client_generation.py1702088%62–64, 103–108, 132, 135–138, 156, 159–162, 229, 232–235
   utils.py1504073%50–51, 57–69, 125–131, 155, 158, 164–177, 206–208, 237–240, 262
src/algokit/core/compilers
   python.py28582%19–20, 25, 49–50
src/algokit/core/config_commands
   container_engine.py412149%24, 29–31, 47–76
   version_prompt.py921485%37–38, 68, 87–90, 108, 118–125, 148
src/algokit/core/project
   __init__.py53394%50, 86, 145
   bootstrap.py120893%47, 126–127, 149, 176, 207–209
   deploy.py69987%108–111, 120–122, 126, 131
   run.py1321390%83, 88, 97–98, 133–134, 138–139, 143, 147, 277–278, 293
src/algokit/core/tasks
   analyze.py93397%105–112, 187
   ipfs.py63789%58–64, 140, 144, 146, 152
   nfd.py491373%25, 31, 34–41, 70–72, 99–101
   vanity_address.py903462%49–50, 54, 59–75, 92–108, 128–131
   wallet.py71593%37, 129, 155–157
src/algokit/core/tasks/mint
   mint.py74988%123–133
   models.py921782%50, 52, 57, 71–74, 81–90
TOTAL475765786% 

Tests Skipped Failures Errors Time
499 0 💤 0 ❌ 0 🔥 35.012s ⏱️

Please sign in to comment.