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.