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.