@joshwycuff/merge-stack

> TODO: description


License
ISC
Install
npm install @joshwycuff/merge-stack@1.0.1

Documentation

Terrascript

A tool to augment CLI work in Node.js projects

npm version Actions Status

About

Terrascript is a tool to augment CLI work in Node.js projects by providing extensible functionality with plugins which is configurable via YAML configuration files.

Install

npm install @joshwycuff/terrascript --save-dev
yarn add @joshwycuff/terrascript --dev

Usage

CLI

Terrascript command-line arguments take the form:

terrascript TARGET_PATH COMMAND [ARGUMENTS...]

Terrascript should be run in the same directory as the top-level Terrascript yaml configuration file (terrascript.yml).

Concepts

Project

A Terrascript project consists of at least one directory containing a single terrascript.yml configuration file. A project can have multiple directories with each of them having 0 or 1 terrascript.yml files except for the top level which must have 1. Lower-level directories containing a terrascript.yml file are called sub-projects.

Spec

Once a terrascript.yml configuration file has been read and processed by Terrascript, internally, Terrascript conceptualizes the resulting object as a spec (short for specification).

Action

Actions are commands, functions, or tasks to be run. They can be as simple as a single CLI command or as complicated as a series of CLI commands, JavaScript functions, and plugin actions.

CLI commands can be run by Terrascript without codifying them in a terrascript.yml. Complicated or common tasks can be codified in a terrascript.yml.

Target

All actions must be performed against a target and any desired targets must be defined in the sub-project in which you want to perform actions against them. For example, common targets might be environments such as prod and dev.

Hook

Hooks exist to perform additional actions at certain points in the execution of Terrascript such as before anything at all has happened, before a target, before an action, or after everything has executed successfully or unsuccessfully.

Context

Context is an object that is passed around in Terrascript containing information about the current execution including the command-line inputs and specs among other things. It is possible to manipulate the context for the purpose of additional functionality, which is how some plugins work.

Inheritance

Sub-projects "inherit" configuration from parent projects but will also overwrite configuration that they share.

Configuration

Configuration file

A basic configuration file with empty keys looks like the following:

plugins:
subprojects:
config:
groups:
targets:
actions:
hooks:
aliases:

Terrascript plugins which extend the capabilities of Terrascript usually use additional keys.

Note that nothing is stopping you from storing additional information in other keys. This can even be useful when using template expressions.

Targets

A Terrascript command will not run if the TARGET_PATH does not resolve to a defined target in a configuration file (with one exception).

Here's a config file with a single target named dev:

targets:
  dev:

You can now run something like:

terrascript dev echo hello world
# hello world

You can also define multiple targets:

targets:
  dev:
  prod:

Now you can run commands for either target, or you can use glob patterns to run multiple targets:

terrascript '*' pwd
# /Users/somebody/project
# /Users/somebody/project
Groups

You can also specify groups of targets:

groups:
  both:
    - dev
    - prod
targets:
  dev:
  prod:
terrascript both pwd
# /Users/somebody/project
# /Users/somebody/project

Yeah, I know. These commands are not terribly useful yet. Hold on...

Config

You can use the config key to set environment variables.

config:
  env:
    A_VAR: A_VAL
targets:
  dev:
terrascript dev echo '$A_VAR'
# A_VAL

You can also override the top-level config with target-level configs.

config:
  env:
    A_VAR: A_VAL
targets:
  dev:
    config:
      env:
        A_VAR: overridden
terrascript dev echo '$A_VAR'
# overridden

Note that the single quotes are necessary so that the variable is not expanded before being passed into Terrascript.

Aliasing

If you have something in your config file that needs to be in several places, you can use aliasing. Alias definitions are found under the aliases key and must begin with "@".

config:
  env:
    VAR1: "@VAL"
    VAR2: "@VAL"
targets:
  dev:
aliases:
  VAL: stuffidontwanttorepeat
terrascript dev echo '$VAR1'
# stuffidontwanttorepeat

Aliasing is a simple way to make DRY config files. However, it is limited. Subprojects cannot "see" alias definitions of parent projects. You also can't combine or perform any logic with aliases. For anything that aliasing can't accomplish, you probably need to go with template expressions.

Actions

If you have a set of commands that you run often, you can create an action for them.

targets:
  dev:
actions:
  things:
    - echo thing 1
    - echo thing 2
terrascript dev things
# thing 1
# thing 2
Template Expressions

You can use template expressions to access various available variables within the current context or even to run arbitrary JavaScript code.

The main available variable is called context whose main fields look like:

interface Context {
  conf: IConfig;
  spec: ISpec;
  cmd: string;
  args: string[];
  target?: ITarget; // available in target hooks, action hooks, and actions
}

conf is the current config under which the given command/hook/script is being run.

spec is the yaml configuration (or specification) under which the given command/hook/script is being run.

cmd and args are the command and arguments from the CLI input.

target is the current target.

Also, for convenience, conf, spec, and target are made directly available in templates.

Template expressions are evaluated dynamically at runtime and can access inherited values.

name: project
target:
  dev:
scripts:
  things:
    - echo {{ spec.name }}
    - echo {{ target.name }}
    - echo {{ 1 + 1 }}
terrascript dev things
# project
# dev
# 2
Hooks

There are a number of special hooks that allow you to run actions before or after certain events. Here they are in the order in which they run:

  • beforeAll
  • beforeSubproject
  • beforeTarget
  • beforeAction
  • afterAction
  • afterTarget
  • afterSubproject
  • afterSuccess
  • afterFailure

Here's an example:

targets:
  dev:
  prod:
hooks:
  beforeTarget:
    - echo do a thing beforeTarget
terrascript "*" echo '{{ target.name }}'
# do a thing beforeTarget
# dev
# do a thing beforeTarget
# prod

Plugins can even add custom hooks. See the Terraform plugin for an example.

Modules

With modules, you can also run Javascript functions within actions. The function should take a single input which is the context object. Modules that you want to import should look like this:

// scripts/mymodule.js
module.exports = {
    func: (context) => {
        console.log('running func')
        console.log(`I'm in ${context.target.name}`)
    }
}

You can import Javascript modules and run their exported functions in actions like so:

targets:
  dev:
modules:
  mymodule: scripts/mymodule.js
actions:
  doathing:
    - mymodule.func
terrascript dev doathing
# running func
# I'm in dev

You can also modify the context.

// scripts/mymodule.js
module.exports = {
    func: (context) => {
        console.log('running func')
        console.log('adding an environment variable to the config')
        context.conf.env.A_VAR = 'A_VAL'
    }
}
targets:
  dev:
modules:
  mymodule: scripts/mymodule.js
scripts:
  doathing:
    - echo $A_VAR
    - mymodule.func
    - echo $A_VAR
terrascript dev doathing
#
# running func
# adding an environment variable to the config
# A_VAL
Subprojects

Let's say we have a project structure like this:

.
├── terrascript.yml
├── subproject1/
│   ├── terrascript.yml
│   └── ...
└── subproject2/
    ├── terrascript.yml
    └── ...

And the terrascript.yml files look like this:

# ./terrascript.yml
subprojects:
  subproject1: ./subproject1/
  subproject2: ./subproject2/
# ./subproject1/terrascript.yml
targets:
  dev:
  prod:
# ./subproject2/terrascript.yml
targets:
  dev:
  prod:

To run the dev target for just subproject1:

terrascript subproject1/dev echo '{{ spec.name }} {{ target.name }}'

To run the dev target for all subprojects:

terrascript dev echo '{{ spec.name }} {{ target.name }}'

To run all targets for all subprojects:

terrascript "*" echo '{{ spec.name }} {{ target.name }}'

Note that since the top-level terrascript.yml does not contain any targets, these commands are not run there. It simply passes the commands down to its subprojects.

Also note that glob patterns apply to both subprojects and targets.

Plugins

The optional plugins key contains a list of module names of plugins for Terrascript to import and include in the execution of Terrascript actions. Some default Terrascript plugins are included without needing them stated here. These include the plugins which implement the functionality for aliases, groups, template-expressions, and modules.

For more information, see the docs for plugins.