github.com/rliebz/tusk@v0.6.5-0.20240416035353-dd5a98e9a5fb/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 ```console 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 ##### Quiet 141 142 Sometimes you may not want to print the command-to-be-run at all. In that case, 143 the `quiet` clause can be used. This is comparable to the global `-q`/`--quiet` 144 command-line flag in that it silence's Tusk's logging without silencing the 145 command output: 146 147 ```yaml 148 tasks: 149 hello: 150 run: 151 command: 152 exec: curl http://example.com 153 quiet: true 154 ``` 155 156 This property can also be set for an entire task and is inherited by any 157 sub-task. In both of these cases the executed commands are not printed: 158 159 ```yaml 160 tasks: 161 quiet-parent: 162 quiet: true 163 run: 164 task: normal-child 165 normal-child: 166 run: curl http://example.com 167 168 normal-parent: 169 run: 170 task: quiet-child 171 quiet-child: 172 quiet: true 173 run: curl http://example.com 174 ``` 175 176 ##### Dir 177 178 The `dir` clause sets the working directory for a specific command: 179 180 ```yaml 181 tasks: 182 hello: 183 run: 184 command: 185 exec: echo "Hello from $PWD!" 186 dir: ./subdir 187 ``` 188 189 #### Set Environment 190 191 To set or unset environment variables, simply define a map of environment 192 variable names to their desired values: 193 194 ```yaml 195 tasks: 196 hello: 197 options: 198 proxy-url: 199 default: http://proxy.example.com 200 run: 201 - set-environment: 202 http_proxy: ${proxy-url} 203 https_proxy: ${proxy-url} 204 no_proxy: ~ 205 - command: curl http://example.com 206 ``` 207 208 Passing `~` or `null` to an environment variable will explicitly unset it, 209 while passing an empty string will set it to an empty string. 210 211 Environment variables once modified will persist until Tusk exits. 212 213 #### Sub-Tasks 214 215 Run can also execute previously-defined tasks: 216 217 ```yaml 218 tasks: 219 one: 220 run: echo "Inside one" 221 two: 222 run: 223 - task: one 224 - command: echo "Inside two" 225 ``` 226 227 For any arg or option that a sub-task defines, the parent task can pass a 228 value, which is treated the same way as passing by command-line would be. Args 229 are passed in as a list, while options are a map from flag name to value. 230 231 To pass values, use the long definition of a sub-task: 232 233 ```yaml 234 tasks: 235 greet: 236 args: 237 name: 238 usage: The person to greet 239 options: 240 greeting: 241 default: Hello 242 run: echo "${greeting}, ${person}!" 243 greet-myself: 244 run: 245 task: 246 name: greet 247 args: 248 - me 249 options: 250 greeting: Howdy 251 ``` 252 253 In cases where a sub-task may not be useful on its own, define it as private to 254 prevent it from being invoked directly from the command-line. For example: 255 256 ```yaml 257 tasks: 258 configure-environment: 259 private: true 260 run: 261 set-environment: { APP_ENV: dev } 262 serve: 263 run: 264 - task: configure-environment 265 - command: python main.py 266 ``` 267 268 ### When 269 270 For conditional execution, `when` clauses are available. 271 272 ```yaml 273 run: 274 when: 275 os: linux 276 command: echo "This is a linux machine" 277 ``` 278 279 In a `run` clause, any item with a true `when` clause will execute. There are 280 five different checks supported: 281 282 - `command` (list): Execute if any command runs with an exit code of `0`. 283 Commands will execute in the order defined and stop execution at the first 284 successful command. 285 - `exists` (list): Execute if any of the listed files exists. 286 - `not-exists` (list): Execute if any of the listed files doesn't exist. 287 - `os` (list): Execute if the operating system matches any one from the list. 288 - `environment` (map[string -> list]): Execute if the environment variable 289 matches any of the values it maps to. To check if a variable is not set, the 290 value should be `~` or `null`. 291 - `equal` (map[string -> list]): Execute if the given option equals any of the 292 values it maps to. 293 - `not-equal` (map[string -> list]): Execute if the given option is not equal to 294 any one of the values it maps to. 295 296 The `when` clause supports any number of different checks as a list, where each 297 check must pass individually for the clause to evaluate to true. Here is a more 298 complicated example of how `when` can be used: 299 300 ```yaml 301 tasks: 302 echo: 303 options: 304 cat: 305 usage: Cat a file 306 run: 307 - when: 308 os: 309 - linux 310 - darwin 311 command: echo "This is a unix machine" 312 - when: 313 - exists: my_file.txt 314 - equal: { cat: true } 315 - command: command -v cat 316 command: cat my_file.txt 317 ``` 318 319 #### Short Form 320 321 Because it's common to check if a boolean flag is set to true, `when` clauses 322 also accept strings as shorthand. Consider the following example, which checks 323 to see if some option `foo` has been set to `true`: 324 325 ```yaml 326 when: 327 equal: { foo: true } 328 ``` 329 330 This can be expressed more succinctly as the following: 331 332 ```yaml 333 when: foo 334 ``` 335 336 #### When Any/All Logic 337 338 A `when` clause takes a list of items, where each item can have multiple checks. 339 Each `when` item will pass if _any_ of the checks pass, while the whole clause 340 will only pass if _all_ of the items pass. For example: 341 342 ```yaml 343 tasks: 344 exists: 345 run: 346 - when: 347 # There is a single `when` item with two checks 348 exists: 349 - file_one.txt 350 - file_two.txt 351 command: echo "At least one file exists" 352 - when: 353 # There are two separate `when` items with one check each 354 - exists: file_one.txt 355 - exists: file_two.txt 356 command: echo "Both files exist" 357 ``` 358 359 These properties can be combined for more complicated logic: 360 361 ```yaml 362 tasks: 363 echo: 364 options: 365 verbose: 366 type: bool 367 ignore-os: 368 type: bool 369 run: 370 - when: 371 # (OS is linux OR darwin OR ignore OS is true) AND (verbose is true) 372 - os: 373 - linux 374 - darwin 375 equal: { ignore-os: true } 376 - equal: { verbose: true } 377 command: echo "This is a unix machine" 378 ``` 379 380 ### Args 381 382 Tasks may have args that are passed directly as inputs. Any arg that is defined 383 is required for the task to execute. 384 385 ```yaml 386 tasks: 387 greet: 388 args: 389 name: 390 usage: The person to greet 391 run: echo "Hello, ${name}!" 392 ``` 393 394 The task can be invoked as such: 395 396 ```console 397 $ tusk greet friend 398 Hello, friend! 399 ``` 400 401 #### Arg Types 402 403 Args can be of the types `string`, `integer`, `float`, or `boolean`. Args 404 without types specified are considered strings. 405 406 ```yaml 407 tasks: 408 add: 409 args: 410 a: 411 type: int 412 b: 413 type: int 414 run: echo $((${a} + ${b})) 415 ``` 416 417 #### Arg Values 418 419 Args can specify which values are considered valid: 420 421 ```yaml 422 tasks: 423 greet: 424 args: 425 name: 426 values: 427 - Abby 428 - Bobby 429 - Carl 430 ``` 431 432 Any value passed by command-line must be one of the listed values, or the 433 command will fail to execute. 434 435 ### Options 436 437 Tasks may have options that are passed as GNU-style flags. The following 438 configuration will provide `-n, --name` flags to the CLI and help documentation, 439 which will then be interpolated: 440 441 ```yaml 442 tasks: 443 greet: 444 options: 445 name: 446 usage: The person to greet 447 short: n 448 environment: GREET_NAME 449 default: World 450 run: echo "Hello, ${name}!" 451 ``` 452 453 The above configuration will evaluate the value of `name` in order of highest 454 priority: 455 456 1. The value passed by command line flags (`-n` or `--name`) 457 2. The value of the environment variable (`GREET_NAME`), if set 458 3. The value set in default 459 460 For short flag names, values can be combined such that `tusk foo -ab` is exactly 461 equivalent to `tusk foo -a -b`. 462 463 #### Option Types 464 465 Options can be of the types `string`, `integer`, `float`, or `boolean`, using 466 the zero-value of that type as the default if not set. Options without types 467 specified are considered strings. 468 469 For boolean values, the flag should be passed by command line without any 470 arugments. In the following example: 471 472 ```yaml 473 tasks: 474 greet: 475 options: 476 loud: 477 type: bool 478 run: 479 - when: 480 equal: { loud: true } 481 command: echo "HELLO!" 482 - when: 483 equal: { loud: false } 484 command: echo "Hello." 485 ``` 486 487 The flag should be passed as such: 488 489 ```bash 490 tusk greet --loud 491 ``` 492 493 This means that for an option that is true by default, the only way to disable 494 it is with the following syntax: 495 496 ```bash 497 tusk greet --loud=false 498 ``` 499 500 Of course, options can always be defined in the reverse manner to avoid this 501 issue: 502 503 ```yaml 504 options: 505 no-loud: 506 type: bool 507 ``` 508 509 #### Option Defaults 510 511 Much like `run` clauses accept a shorthand form, passing a string to `default` 512 is shorthand. The following options are exactly equivalent: 513 514 ```yaml 515 options: 516 short: 517 default: foo 518 long: 519 default: 520 - value: foo 521 ``` 522 523 A `default` clause can also register the `stdout` of a command as its value: 524 525 ```yaml 526 options: 527 os: 528 default: 529 command: uname -s 530 ``` 531 532 A `default` clause also accepts a list of possible values with a corresponding 533 `when` clause. The first `when` that evaluates to true will be used as the 534 default value, with an omitted `when` always considered true. 535 536 In this example, linux users will have the name `Linux User`, while the default 537 for all other OSes is `User`: 538 539 ```yaml 540 options: 541 name: 542 default: 543 - when: 544 os: linux 545 value: Linux User 546 - value: User 547 ``` 548 549 #### Option Values 550 551 Like args, an option can specify which values are considered valid: 552 553 ```yaml 554 options: 555 number: 556 default: zero 557 values: 558 - one 559 - two 560 - three 561 ``` 562 563 Any value passed by command-line flags or environment variables must be one of 564 the listed values. Default values, including commands, are excluded from this 565 requirement. 566 567 #### Required Options 568 569 Options may be required if there is no sane default value. For a required flag, 570 the task will not execute unless the flag is passed: 571 572 ```yaml 573 options: 574 file: 575 required: true 576 ``` 577 578 A required option cannot be private or have any default values. 579 580 #### Private Options 581 582 Sometimes it may be desirable to have a variable that cannot be directly 583 modified through command-line flags. In this case, use the `private` option: 584 585 ```yaml 586 options: 587 user: 588 private: true 589 default: 590 command: whoami 591 ``` 592 593 A private option will not accept environment variables or command line flags, 594 and it will not appear in the help documentation. 595 596 #### Shared Options 597 598 Options may also be defined at the root of the config file to be shared between 599 tasks: 600 601 ```yaml 602 options: 603 name: 604 usage: The person to greet 605 default: World 606 607 tasks: 608 hello: 609 run: echo "Hello, ${name}!" 610 goodbye: 611 run: echo "Goodbye, ${name}!" 612 ``` 613 614 Any shared variables referenced by a task will be exposed by command-line when 615 invoking that task. Shared variables referenced by a sub-task will be evaluated 616 as needed, but not exposed by command-line. 617 618 Tasks that define an argument or option with the same name as a shared task will 619 overwrite the value of the shared option for the length of that task, not 620 including sub-tasks. 621 622 ### Finally 623 624 The `finally` clause is run after a task's `run` logic has completed, whether or 625 not that task was successful. This can be useful for clean-up logic. A `finally` 626 clause has the same format as a `run` clause: 627 628 <!-- prettier-ignore-start --> 629 ```yaml 630 tasks: 631 hello: 632 run: 633 - echo "Hello" 634 - exit 1 # `run` clause stops here 635 - echo "Oops!" # Never prints 636 finally: 637 - echo "Goodbye" # Always prints 638 - task: cleanup 639 # ... 640 ``` 641 <!-- prettier-ignore-end --> 642 643 If the `finally` clause runs an unsuccessful command, it will terminate early 644 the same way that a `run` clause would. The exit code is still passed back to 645 the command line. However, if both the `run` clause and `finally` clause fail, 646 the exit code from the `run` clause takes precedence. 647 648 ### Include 649 650 In some cases it may be desirable to split the task definition into a separate 651 file. The `include` clause serves this purpose. At the top-level of a task, a 652 task may optionally be specified using just the `include` key, which maps to a 653 separate file where there task definition is stored. 654 655 For example, `tusk.yml` could be written like this: 656 657 ```yaml 658 tasks: 659 hello: 660 include: .tusk/hello.yml 661 ``` 662 663 With a `.tusk/hello.yml` that looks like this: 664 665 ```yaml 666 options: 667 name: 668 usage: The person to greet 669 default: World 670 run: echo "Hello, ${name}!" 671 ``` 672 673 It is invalid to split the configuration; if the `include` clause is used, no 674 other keys can be specified in the `tusk.yml`, and the full task must be 675 defined in the included file. 676 677 ### Interpreter 678 679 By default, any command run will default to using `sh -c` as its interpreter. 680 This can optionally be configured using the `interpreter` clause. 681 682 The interpreter is specified as an executable, which can either be an absolute 683 path or available on the user's PATH, followed by a series of optional 684 arguments: 685 686 ```yaml 687 interpreter: node -e 688 689 tasks: 690 hello: 691 run: console.log("Hello!") 692 ``` 693 694 The commands specified in individual tasks will be passed as the final 695 argument. The above example is effectively equivalent to the following: 696 697 ```sh 698 node -e 'console.log("Hello!")' 699 ``` 700 701 ### CLI Metadata 702 703 It is also possible to create a custom CLI tool for use outside of a project's 704 directory by using shell aliases: 705 706 ```bash 707 alias mycli="tusk -f /path/to/tusk.yml" 708 ``` 709 710 In that case, it may be useful to override the tool name and usage text that 711 are provided as part of the help documentation: 712 713 ```yaml 714 name: mycli 715 usage: A custom aliased command-line application 716 717 tasks: 718 # ... 719 ``` 720 721 The example above will produce the following help documentation: 722 723 ```console 724 $ tusk --help 725 mycli - A custom aliased command-line application 726 727 Usage: 728 mycli [global options] <task> [task options] 729 730 Tasks: 731 ... 732 ``` 733 734 ### Interpolation 735 736 The interpolation syntax for a variable `foo` is `${foo}`, meaning any instances 737 of `${foo}` in the configuration file will be replaced with the value of `foo` 738 during execution. 739 740 Interpolation is done on a task-by-task basis, meaning args and options defined 741 in one task will not interpolate to any other tasks. Shared options, on the 742 other hand, will only be evaluated once per execution. 743 744 The execution order is as followed: 745 746 1. Shared options are interpolated first, in the order defined by the config 747 file. The results of global interpolation are cached and not re-run. 748 2. The args for the current task being run are interpolated, in order. 749 3. The options for the current task being run are interpolated, in order. 750 4. For each call to a sub-task, the process is repeated, ignoring the task- 751 specific interpolations for parent tasks, using the cached shared options. 752 753 This means that options can reference other options or args: 754 755 ```yaml 756 options: 757 name: 758 default: World 759 greeting: 760 default: Hello, ${name} 761 762 tasks: 763 greet: 764 run: echo "${greeting}" 765 ``` 766 767 Because interpolation is not always desirable, as in the case of environment 768 variables, `$$` will escape to `$` and ignore interpolation. It is also 769 possible to use alternative syntax such as `$foo` to avoid interpolation as 770 well. The following two tasks will both use environment variables and not 771 attempt interpolation: 772 773 ```yaml 774 tasks: 775 one: 776 run: Hello, $${USER} 777 two: 778 run: Hello, $USER 779 ``` 780 781 Interpolation works by substituting the value in the `yaml` config file, then 782 parsing the file after interpolation. This means that variable values with 783 newlines or other characters that are relevant to the `yaml` spec or the `sh` 784 interpreter will need to be considered by the user. This can be as simple as 785 using quotes when appropriate.