github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/docs/spec.md (about) 1 ## The Spec 2 3 ### Tasks 4 5 The core of every `tusk.yml` file is a list of tasks. Tasks are declared at the 6 top level of the `tusk.yml` file and include a list of tasks. 7 8 For the following tasks: 9 10 ```yaml 11 tasks: 12 hello: 13 run: echo "Hello, world!" 14 goodbye: 15 run: echo "Goodbye, world!" 16 ``` 17 18 The commands can be run with no additional configuration: 19 20 ```text 21 $ tusk hello 22 Running: echo "Hello, world!" 23 Hello, world! 24 ``` 25 26 Tasks can be documented with a one-line `usage` string and a slightly longer 27 `description`. This information will be displayed in help messages: 28 29 ```yaml 30 tasks: 31 hello: 32 usage: Say hello to the world 33 description: | 34 This command will echo "Hello, world!" to the user. There's no 35 surprises here. 36 run: echo "Hello, world!" 37 goodbye: 38 run: echo "Goodbye, world!" 39 ``` 40 41 ### Run 42 43 The behavior of a task is defined in its `run` clause. A `run` clause can be 44 used for commands, sub-tasks, or setting environment variables. Although each 45 `run` item can only perform one of these actions, they can be run in succession 46 to handle complex scenarios. 47 48 In its simplest form, `run` can be given a string or list of strings to be 49 executed serially as shell commands: 50 51 ```yaml 52 tasks: 53 hello: 54 run: echo "Hello!" 55 ``` 56 57 This is a shorthand syntax for the following: 58 59 ```yaml 60 tasks: 61 hello: 62 run: 63 - command: 64 exec: echo "Hello!" 65 ``` 66 67 The `run` clause tasks a list of `run` items, which allow executing shell 68 commands with `command`, setting or unsetting environment variables with 69 `set-environment`, running other tasks with `task`, and controlling conditional 70 execution with `when`. 71 72 #### Command 73 74 The `command` clause is the most common thing to do during a `run`, so for 75 convenience, passing a string or single item will be correctly interpreted. 76 Here are several examples of equivalent `run` clauses: 77 78 ```yaml 79 run: echo "Hello!" 80 81 run: 82 - echo "Hello!" 83 84 run: 85 command: echo "Hello!" 86 87 run: 88 - command: echo "Hello!" 89 90 run: 91 - command: 92 exec: echo "Hello!" 93 ``` 94 95 While the interpreter cannot be set for an individual command, it is possible 96 to set them globally using [the interpreter clause](#interpreter). 97 98 ##### Exec 99 100 The `exec` clause contains the actual shell command to be performed. 101 102 If any of the run commands execute with a non-zero exit code, Tusk will 103 immediately exit with the same exit code without executing any other commands. 104 105 Each command in a `run` clause gets its own sub-shell, so things like declaring 106 functions and environment variables will not be available across separate run 107 commmands, although it is possible to run the `set-environment` clause or use a 108 multi-line shell command. 109 110 When using POSIX interpreters with multi-line scripts, it is recommend to run 111 `set -e` at the top of the script, to preserve the exit-on-error behavior. 112 113 ```yaml 114 tasks: 115 hello: 116 run: | 117 set -e 118 errcho() { 119 >&2 echo "$@" 120 } 121 errcho "Hello, world!" 122 errcho "Goodbye, world!" 123 ``` 124 125 ##### Print 126 127 Sometimes it may not be desirable to print the exact command run, for example, 128 if it's overly verbose or contains secrets. In that case, the `command` clause 129 can be passed a `print` string to use as an alternative: 130 131 ```yaml 132 tasks: 133 hello: 134 run: 135 command: 136 exec: echo "SECRET_VALUE" 137 print: echo "*****" 138 ``` 139 140 ##### Dir 141 142 The `dir` clause sets the working directory for a specific command: 143 144 ```yaml 145 tasks: 146 hello: 147 run: 148 command: 149 exec: echo "Hello from $PWD!" 150 dir: ./subdir 151 ``` 152 153 #### Set Environment 154 155 To set or unset environment variables, simply define a map of environment 156 variable names to their desired values: 157 158 ```yaml 159 tasks: 160 hello: 161 options: 162 proxy-url: 163 default: http://proxy.example.com 164 run: 165 - set-environment: 166 http_proxy: ${proxy-url} 167 https_proxy: ${proxy-url} 168 no_proxy: ~ 169 - command: curl http://example.com 170 ``` 171 172 Passing `~` or `null` to an environment variable will explicitly unset it, 173 while passing an empty string will set it to an empty string. 174 175 Environment variables once modified will persist until Tusk exits. 176 177 #### Sub-Tasks 178 179 Run can also execute previously-defined tasks: 180 181 ```yaml 182 tasks: 183 one: 184 run: echo "Inside one" 185 two: 186 run: 187 - task: one 188 - command: echo "Inside two" 189 ``` 190 191 For any arg or option that a sub-task defines, the parent task can pass a 192 value, which is treated the same way as passing by command-line would be. Args 193 are passed in as a list, while options are a map from flag name to value. 194 195 To pass values, use the long definition of a sub-task: 196 197 ```yaml 198 tasks: 199 greet: 200 args: 201 name: 202 usage: The person to greet 203 options: 204 greeting: 205 default: Hello 206 run: echo "${greeting}, ${person}!" 207 greet-myself: 208 run: 209 task: 210 name: greet 211 args: 212 - me 213 options: 214 greeting: Howdy 215 ``` 216 217 In cases where a sub-task may not be useful on its own, define it as private to 218 prevent it from being invoked directly from the command-line. For example: 219 220 ```yaml 221 tasks: 222 configure-environment: 223 private: true 224 run: 225 set-environment: {APP_ENV: dev} 226 serve: 227 run: 228 - task: configure-environment 229 - command: python main.py 230 ``` 231 232 ### When 233 234 For conditional execution, `when` clauses are available. 235 236 ```yaml 237 run: 238 when: 239 os: linux 240 command: echo "This is a linux machine" 241 ``` 242 243 In a `run` clause, any item with a true `when` clause will execute. There are 244 five different checks supported: 245 246 - `command` (list): Execute if any command runs with an exit code of `0`. 247 Commands will execute in the order defined and stop execution at the first 248 successful command. 249 - `exists` (list): Execute if any of the listed files exists. 250 - `not-exists` (list): Execute if any of the listed files doesn't exist. 251 - `os` (list): Execute if the operating system matches any one from the list. 252 - `environment` (map[string -> list]): Execute if the environment variable 253 matches any of the values it maps to. To check if a variable is not set, the 254 value should be `~` or `null`. 255 - `equal` (map[string -> list]): Execute if the given option equals any of the 256 values it maps to. 257 - `not-equal` (map[string -> list]): Execute if the given option is not equal to 258 any one of the values it maps to. 259 260 The `when` clause supports any number of different checks as a list, where each 261 check must pass individually for the clause to evaluate to true. Here is a more 262 complicated example of how `when` can be used: 263 264 ```yaml 265 tasks: 266 echo: 267 options: 268 cat: 269 usage: Cat a file 270 run: 271 - when: 272 os: 273 - linux 274 - darwin 275 command: echo "This is a unix machine" 276 - when: 277 - exists: my_file.txt 278 - equal: {cat: true} 279 - command: command -v cat 280 command: cat my_file.txt 281 ``` 282 283 #### Short Form 284 285 Because it's common to check if a boolean flag is set to true, `when` clauses 286 also accept strings as shorthand. Consider the following example, which checks 287 to see if some option `foo` has been set to `true`: 288 289 ```yaml 290 when: 291 equal: {foo: true} 292 ``` 293 294 This can be expressed more succinctly as the following: 295 296 ```yaml 297 when: foo 298 ``` 299 300 #### When Any/All Logic 301 302 A `when` clause takes a list of items, where each item can have multiple checks. 303 Each `when` item will pass if _any_ of the checks pass, while the whole clause 304 will only pass if _all_ of the items pass. For example: 305 306 ```yaml 307 tasks: 308 exists: 309 run: 310 - when: 311 # There is a single `when` item with two checks 312 exists: 313 - file_one.txt 314 - file_two.txt 315 command: echo "At least one file exists" 316 - when: 317 # There are two separate `when` items with one check each 318 - exists: file_one.txt 319 - exists: file_two.txt 320 command: echo "Both files exist" 321 ``` 322 323 These properties can be combined for more complicated logic: 324 325 ```yaml 326 tasks: 327 echo: 328 options: 329 verbose: 330 type: bool 331 ignore-os: 332 type: bool 333 run: 334 - when: 335 # (OS is linux OR darwin OR ignore OS is true) AND (verbose is true) 336 - os: 337 - linux 338 - darwin 339 equal: {ignore-os: true} 340 - equal: {verbose: true} 341 command: echo "This is a unix machine" 342 ``` 343 344 ### Args 345 346 Tasks may have args that are passed directly as inputs. Any arg that is defined 347 is required for the task to execute. 348 349 ```yaml 350 tasks: 351 greet: 352 args: 353 name: 354 usage: The person to greet 355 run: echo "Hello, ${name}!" 356 ``` 357 358 The task can be invoked as such: 359 360 ```text 361 $ tusk greet friend 362 Hello, friend! 363 ``` 364 365 #### Arg Values 366 367 Args can specify which values are considered valid: 368 369 ```yaml 370 tasks: 371 greet: 372 args: 373 name: 374 values: 375 - Abby 376 - Bobby 377 - Carl 378 ``` 379 380 Any value passed by command-line must be one of the listed values, or the 381 command will fail to execute. 382 383 ### Options 384 385 Tasks may have options that are passed as GNU-style flags. The following 386 configuration will provide `-n, --name` flags to the CLI and help documentation, 387 which will then be interpolated: 388 389 ```yaml 390 tasks: 391 greet: 392 options: 393 name: 394 usage: The person to greet 395 short: n 396 environment: GREET_NAME 397 default: World 398 run: echo "Hello, ${name}!" 399 ``` 400 401 The above configuration will evaluate the value of `name` in order of highest 402 priority: 403 404 1. The value passed by command line flags (`-n` or `--name`) 405 2. The value of the environment variable (`GREET_NAME`), if set 406 3. The value set in default 407 408 For short flag names, values can be combined such that `tusk foo -ab` is exactly 409 equivalent to `tusk foo -a -b`. 410 411 #### Option Types 412 413 Options can be of the types `string`, `integer`, `float`, or `boolean`, using 414 the zero-value of that type as the default if not set. Options without types 415 specified are considered strings. 416 417 For boolean values, the flag should be passed by command line without any 418 arugments. In the following example: 419 420 ```yaml 421 tasks: 422 greet: 423 options: 424 loud: 425 type: bool 426 run: 427 - when: 428 equal: {loud: true} 429 command: echo "HELLO!" 430 - when: 431 equal: {loud: false} 432 command: echo "Hello." 433 ``` 434 435 The flag should be passed as such: 436 437 ```bash 438 tusk greet --loud 439 ``` 440 441 This means that for an option that is true by default, the only way to disable 442 it is with the following syntax: 443 444 ```bash 445 tusk greet --loud=false 446 ``` 447 448 Of course, options can always be defined in the reverse manner to avoid this 449 issue: 450 451 ```yaml 452 options: 453 no-loud: 454 type: bool 455 ``` 456 457 #### Option Defaults 458 459 Much like `run` clauses accept a shorthand form, passing a string to `default` 460 is shorthand. The following options are exactly equivalent: 461 462 ```yaml 463 options: 464 short: 465 default: foo 466 long: 467 default: 468 - value: foo 469 ``` 470 471 A `default` clause can also register the `stdout` of a command as its value: 472 473 ```yaml 474 options: 475 os: 476 default: 477 command: uname -s 478 ``` 479 480 A `default` clause also accepts a list of possible values with a corresponding 481 `when` clause. The first `when` that evaluates to true will be used as the 482 default value, with an omitted `when` always considered true. 483 484 In this example, linux users will have the name `Linux User`, while the default 485 for all other OSes is `User`: 486 487 ```yaml 488 options: 489 name: 490 default: 491 - when: 492 os: linux 493 value: Linux User 494 - value: User 495 ``` 496 497 #### Option Values 498 499 Like args, an option can specify which values are considered valid: 500 501 ```yaml 502 options: 503 number: 504 default: zero 505 values: 506 - one 507 - two 508 - three 509 ``` 510 511 Any value passed by command-line flags or environment variables must be one of 512 the listed values. Default values, including commands, are excluded from this 513 requirement. 514 515 #### Required Options 516 517 Options may be required if there is no sane default value. For a required flag, 518 the task will not execute unless the flag is passed: 519 520 ```yaml 521 options: 522 file: 523 required: true 524 ``` 525 526 A required option cannot be private or have any default values. 527 528 #### Private Options 529 530 Sometimes it may be desirable to have a variable that cannot be directly 531 modified through command-line flags. In this case, use the `private` option: 532 533 ```yaml 534 options: 535 user: 536 private: true 537 default: 538 command: whoami 539 ``` 540 541 A private option will not accept environment variables or command line flags, 542 and it will not appear in the help documentation. 543 544 #### Shared Options 545 546 Options may also be defined at the root of the config file to be shared between 547 tasks: 548 549 ```yaml 550 options: 551 name: 552 usage: The person to greet 553 default: World 554 555 tasks: 556 hello: 557 run: echo "Hello, ${name}!" 558 goodbye: 559 run: echo "Goodbye, ${name}!" 560 ``` 561 562 Any shared variables referenced by a task will be exposed by command-line when 563 invoking that task. Shared variables referenced by a sub-task will be evaluated 564 as needed, but not exposed by command-line. 565 566 Tasks that define an argument or option with the same name as a shared task will 567 overwrite the value of the shared option for the length of that task, not 568 including sub-tasks. 569 570 ### Finally 571 572 The `finally` clause is run after a task's `run` logic has completed, whether or 573 not that task was successful. This can be useful for clean-up logic. A `finally` 574 clause has the same format as a `run` clause: 575 576 ```yaml 577 tasks: 578 hello: 579 run: 580 - echo "Hello" 581 - exit 1 # `run` clause stops here 582 - echo "Oops!" # Never prints 583 finally: 584 - echo "Goodbye" # Always prints 585 - task: cleanup 586 # ... 587 ``` 588 589 If the `finally` clause runs an unsuccessful command, it will terminate early 590 the same way that a `run` clause would. The exit code is still passed back to 591 the command line. However, if both the `run` clause and `finally` clause fail, 592 the exit code from the `run` clause takes precedence. 593 594 ### Include 595 596 In some cases it may be desirable to split the task definition into a separate 597 file. The `include` clause serves this purpose. At the top-level of a task, a 598 task may optionally be specified using just the `include` key, which maps to a 599 separate file where there task definition is stored. 600 601 For example, `tusk.yml` could be written like this: 602 603 ```yaml 604 tasks: 605 hello: 606 include: .tusk/hello.yml 607 ``` 608 609 With a `.tusk/hello.yml` that looks like this: 610 611 ```yaml 612 options: 613 name: 614 usage: The person to greet 615 default: World 616 run: echo "Hello, ${name}!" 617 ``` 618 619 It is invalid to split the configuration; if the `include` clause is used, no 620 other keys can be specified in the `tusk.yml`, and the full task must be 621 defined in the included file. 622 623 ### Interpreter 624 625 By default, any command run will default to using `sh -c` as its interpreter. 626 This can optionally be configured using the `interpreter` clause. 627 628 The interpreter is specified as an executable, which can either be an absolute 629 path or available on the user's PATH, followed by a series of optional 630 arguments: 631 632 ```yaml 633 interpreter: node -e 634 635 tasks: 636 hello: 637 run: console.log("Hello!") 638 ``` 639 640 The commands specified in individual tasks will be passed as the final 641 argument. The above example is effectively equivalent to the following: 642 643 ```sh 644 node -e 'console.log("Hello!")' 645 ``` 646 647 ### CLI Metadata 648 649 It is also possible to create a custom CLI tool for use outside of a project's 650 directory by using shell aliases: 651 652 ```bash 653 alias mycli="tusk -f /path/to/tusk.yml" 654 ``` 655 656 In that case, it may be useful to override the tool name and usage text that 657 are provided as part of the help documentation: 658 659 ```yaml 660 name: mycli 661 usage: A custom aliased command-line application 662 663 tasks: 664 ... 665 ``` 666 667 The example above will produce the following help documentation: 668 669 ```text 670 mycli - A custom aliased command-line application 671 672 Usage: 673 mycli [global options] <task> [task options] 674 675 Tasks: 676 ... 677 ``` 678 679 ### Interpolation 680 681 The interpolation syntax for a variable `foo` is `${foo}`, meaning any instances 682 of `${foo}` in the configuration file will be replaced with the value of `foo` 683 during execution. 684 685 Interpolation is done on a task-by-task basis, meaning args and options defined 686 in one task will not interpolate to any other tasks. Shared options, on the 687 other hand, will only be evaluated once per execution. 688 689 The execution order is as followed: 690 691 1. Shared options are interpolated first, in the order defined by the config 692 file. The results of global interpolation are cached and not re-run. 693 2. The args for the current task being run are interpolated, in order. 694 3. The options for the current task being run are interpolated, in order. 695 4. For each call to a sub-task, the process is repeated, ignoring the task- 696 specific interpolations for parent tasks, using the cached shared options. 697 698 This means that options can reference other options or args: 699 700 ```yaml 701 options: 702 name: 703 default: World 704 greeting: 705 default: Hello, ${name} 706 707 tasks: 708 greet: 709 run: echo "${greeting}" 710 ``` 711 712 Because interpolation is not always desirable, as in the case of environment 713 variables, `$$` will escape to `$` and ignore interpolation. It is also 714 possible to use alternative syntax such as `$foo` to avoid interpolation as 715 well. The following two tasks will both use environment variables and not 716 attempt interpolation: 717 718 ```yaml 719 tasks: 720 one: 721 run: Hello, $${USER} 722 two: 723 run: Hello, $USER 724 ``` 725 726 Interpolation works by substituting the value in the `yaml` config file, then 727 parsing the file after interpolation. This means that variable values with 728 newlines or other characters that are relevant to the `yaml` spec or the `sh` 729 interpreter will need to be considered by the user. This can be as simple as 730 using quotes when appropriate.