Advanced Scripting Guide
This guide provides some tips and ideas for advanced scripting. It assumes familiarity with the scripting guide and the Brigadier API.
Promises and the async and await keywords
Brigade supports the various methods provided by the JavaScript language for
controlling the flow of asynchronous execution. This includes chaining together
promises as well as utilization of the async and await keywords.
Here is an example that uses a Promise chain to organize the execution of two jobs:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
function exec(event) {
    let j1 = new Job("j1", "alpine:3.14", event);
    j1.primaryContainer.command = ["echo"];
    j1.primaryContainer.arguments = ["hello " + event.payload];
    let j2 = new Job("j2", "alpine:3.14", event);
    j2.primaryContainer.command = ["echo"];
    j2.primaryContainer.arguments = ["goodbye " + event.payload];
    j1.run()
    .then(() => {
        return j2.run()
    })
    .then(() => {
        console.log("done");
    });
}
events.process();
In the example above, we use JavaScript Promise objects for chaining two
jobs, then printing done after the two jobs are run. Each Job.run() call
returns a Promise, and we call that Promise’s then() method.
Here’s what it looks like when the script is run:
$ brig event create --project promises --payload world --follow
Created event "882f832a-c156-4afc-9936-00d3b2d61083".
Waiting for event's worker to be RUNNING...
2021-10-04T22:33:22.078Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T22:33:22.502Z [job: j1] INFO: Creating job j1
2021-10-04T22:33:25.052Z [job: j2] INFO: Creating job j2
done
$ brig event logs --id 882f832a-c156-4afc-9936-00d3b2d61083 --job j1
hello world
$ brig event logs --id 882f832a-c156-4afc-9936-00d3b2d61083 --job j2
goodbye world
We can rewrite the example to use await and get the same result:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
async function exec(event) {
    let j1 = new Job("j1", "alpine:3.14", event);
    j1.primaryContainer.command = ["echo"];
    j1.primaryContainer.arguments = ["hello " + event.payload];
    let j2 = new Job("j2", "alpine:3.14", event);
    j2.primaryContainer.command = ["echo"];
    j2.primaryContainer.arguments = ["goodbye " + event.payload];
    await j1.run();
    await j2.run();
    console.log("done");
}
events.process();
The first thing to note about this example is that we are annotating our
exec() function with the async keyword. This tells the JavaScript runtime
that the function is an asynchronous handler.
The two await statements will cause the jobs to run synchronously.
The first job will run to completion, then the second job will run to
completion. Then the console.log function will execute.
Some people feel that using async/await makes code more readable. Others
prefer the Promise notation. Brigade will support either. The same patterns
shown above can also be used with Job.concurrent() and Job.sequence(), as
their run() methods return Promise objects as well.
Error Handling
Note that when errors occur, they are thrown as exceptions. To handle this
case, use try/catch blocks. These can be employed with either the promise
chain approach or the async/await approach.
Via Promise chaining:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
function exec(event) {
    let j1 = new Job("j1", "alpine:3.14", event);
    j1.primaryContainer.command = ["echo"];
    j1.primaryContainer.arguments = ["hello " + event.payload];
    // j2 is configured to fail
    let j2 = new Job("j2", "alpine:3.14", event);
    j2.primaryContainer.command = ["exit"];
    j2.primaryContainer.arguments = ["1"];
    j1.run()
    .then(() => {
        return j2.run()
    })
    .then(() => {
        console.log("done");
    })
    .catch(e => {
        console.log(`Caught Exception ${e}`);
    });
}
events.process();
Via async/await:
const { events, Job } = require("@brigadecore/brigadier");
events.on("brigade.sh/cli", "exec", exec);
async function exec(event) {
    let j1 = new Job("j1", "alpine:3.14", event);
    j1.primaryContainer.command = ["echo"];
    j1.primaryContainer.arguments = ["hello " + event.payload];
    // j2 is configured to fail
    let j2 = new Job("j2", "alpine:3.14", event);
    j2.primaryContainer.command = ["exit"];
    j2.primaryContainer.arguments = ["1"];
    try {
        await j1.run();
        await j2.run();
        console.log("done");
    } catch (e) {
        console.log(`Caught Exception ${e}`);
    }
}
events.process();
Looking at the async/await example, the second job (j2) will execute
exit 1, which will cause the container to exit with an error. When
await j2.run() is executed, it will throw an exception because j2 exited
with an error. In our catch block, we print the error message that we
receive.
If we run this, we’ll see something like this:
$ brig event create --project await --payload world --follow
Created event "69b5713f-b612-434f-9b52-9bcd57f044c5".
Waiting for event's worker to be RUNNING...
2021-10-04T22:45:45.808Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T22:45:46.235Z [job: j1] INFO: Creating job j1
2021-10-04T22:45:48.826Z [job: j2] INFO: Creating job j2
Caught Exception Error: Job "j2" failed
The line Caught Exception... shows the error that we received.
Note, however, we didn’t configure the worker to fail when we caught the
exception in the example above; we simply logged it. Although the j2 job
fails, the worker succeeds. We see this when looking at the event afterwards:
$ brig event get --id 69b5713f-b612-434f-9b52-9bcd57f044c5
ID                                  	PROJECT	SOURCE        	TYPE	AGE	WORKER PHASE
69b5713f-b612-434f-9b52-9bcd57f044c5	await  	brigade.sh/cli	exec	20s	SUCCEEDED
Event "69b5713f-b612-434f-9b52-9bcd57f044c5" jobs:
NAME	STARTED	ENDED	PHASE
j1  	16s    	13s  	SUCCEEDED
j2  	11s    	11s  	FAILED
This illustrates the following point: As the script-writer, catching exceptions from jobs (or other runnables) creates the opportunity to decide whether the workflow succeeds or fails. Perhaps we do wish to fail the worker immediately. Alternatively, perhaps we wish to execute conditional logic which would then ultimately dictate worker success.
Using Object-oriented JavaScript to Extend Job
JavaScript supports class-based, object-oriented programming. One example where
Brigade makes use of this is by providing some useful ways of working with the
Job class. The Job class can be extended to either preconfigure similar
jobs or to add extra functionality to a job.
The following example creates a MyJob class that extends Job and provides
some predefined fields:
const { events, Job } = require("@brigadecore/brigadier");
class MyJob extends Job {
  constructor(name, event) {
    super(name, "alpine:3.14", event);
    this.primaryContainer.command = ["echo"];
    this.primaryContainer.arguments = ["hello " + event.payload];
  }
}
events.on("brigade.sh/cli", "exec", async event => {
  const j1 = new MyJob("j1", event);
  const j2 = new MyJob("j2", event);
  await Job.sequence(j1, j2).run();
});
events.process();
In the example above, both j1 and j2 will have the same image and the same
command. They inherited these predefined settings from the MyJob class. Using
inheritence in this way can reduce boilerplate code.
The fields can be selectively overwritten, as well. We could, for example, override the command arguments for the second job without affecting the first:
const { events, Job } = require("@brigadecore/brigadier");
class MyJob extends Job {
  constructor(name, event) {
    super(name, "alpine:3.14", event);
    this.primaryContainer.command = ["echo"];
    this.primaryContainer.arguments = ["hello " + event.payload];
  }
}
events.on("brigade.sh/cli", "exec", async event => {
  const j1 = new MyJob("j1", event);
  const j2 = new MyJob("j2", event);
  j2.primaryContainer.arguments = ["goodbye " + event.payload];
  await Job.sequence(j1, j2).run();
});
events.process();
If we were to look at the output of these two jobs, we’d see something like this:
$ brig event create --project jobs --payload world --follow
Created event "c4906ec3-fec1-400f-8d8f-89fd6a379475".
Waiting for event's worker to be RUNNING...
2021-10-04T23:02:58.191Z INFO: brigade-worker version: 5c94a15-dirty
2021-10-04T23:02:58.545Z [job: j1] INFO: Creating job j1
2021-10-04T23:03:01.088Z [job: j2] INFO: Creating job j2
$ brig event logs --id c4906ec3-fec1-400f-8d8f-89fd6a379475 --job j1
hello world
$ brig event logs --id c4906ec3-fec1-400f-8d8f-89fd6a379475 --job j2
goodbye world
Job j2 has the different command, while j1 inherited the defaults from MyJob.