Automating With Node.js

Node JS Automation...
Author:  Jknoxvil

29 downloads 475 Views 6MB Size

Recommend Documents

Automating with PROFINETFull description

NODE JS ManualFull description

Node JS AutomationFull description

NodeJS Build-conf Presentation Slides

NodeJS Build-conf Presentation SlidesFull description

pengenalan-nodejsFull description

Automating Junos® with AnsibleFull description

Nodejs Tutorial

Descripción completa

Ebook para iniciantes NodejsDescrição completa

Livro que mostra como aplicar testes em nodejs

Automating With STEP7 in STL and SCLFull description

Automating Solidworks 2013 Using Macros

Automating Solidworks 2013 Using MacrosDescripción completa

Automating Solidworks 2013 Using Macros

Automating the Precision Trading System[1]

Automating Solidworks 2011Descripción completa

A Web crawler or spider crawls through the Web looking for pages to index, and when it locates a new page it passes the page on to an indexer. The indexer identifies links, keywords, and o…Full description

Automating With PROFINET Industrial Communication Based on Industrial Ethernet PDF Download Free Online

Replay


The good thing about this approach is that all versions will be pointing to the

same core bundle. So if a bug appears in one of them, you can deploy a fix for the template and it will fix them all in one go, as opposed to rebuilding a bundle for each game. The JSON will differentiate our games and will be the single source of truth. The team would come to an agreement on what needs to be dynamic. The final JSON structure was agreed as follows (for any Rock Paper Scissors game).

 app/assets/game.json { "id": 123 "id": 123, , "projectName": "projectName" : "rock-paper-scissors" "rock-paper-scissors", , "theme": "theme" : { "fontFamily": "fontFamily" : "Cabin" "Cabin", , "path": "path" : "http://cdn.op "http://cdn.opencanvas.co.uk/ encanvas.co.uk/automatingwithn automatingwithnodejs/assets/ro odejs/assets/rock-paper-scisso ck-paper-scissors/fire-water-e rs/fire-water-earth-cute" arth-cute", "customStyles": "customStyles" : [ "https://fonts.googleapis.com/ "https://fonts .googleapis.com/css?family=Cabi css?family=Cabin" n" ] }, "images": "images" : { "background": "background" : "background.png" "background.png", , "rock": "rock" : "rock.png" "rock.png", , "paper": "paper" : "paper.png" "paper.png", , "scissors": "scissors" : "scissors.png" }, "labels": "labels" : { "rock": "rock" : "rock" "rock", , "paper": "paper" : "paper" "paper", , "scissors": "scissors" : "scissors" }, "screens": "screens" : { "choice": "choice" : { "title": "title" : "Rock Paper Scissors", Scissors", "subtitle": "subtitle" : "Make your choice" }, "result": "result" : { "won": "won" : "you won!", won!", "lost": "lost" : "you lost!", lost!", "draw": "draw" : "it's a draw", draw", "replay": "replay" : "replay" "replay", , "feedback": "feedback" : { "won": "won" : "{player} beats {cpu}", {cpu}", "lost": "lost" : "{cpu} beats {player}", {player}", "draw": "draw" : "Nobody won this time" } } } }

Each game we create will have a unique identifier

id

. This is needed so we can

store data on each game and evaluate which has more engagement with players. Our

config.json

object also has a

theme

where we can pass the fonts we would like

to use in the game, the path to the game’s assets - such as the images (which are retrieved from the CDN used by our designers), and any custom CSS files we’d like to use. In our example, under

customStyles

, we are loading in a font we we want to

render. To avoid confusion, the images should remain consistent with their naming convention. Labels and the content of the various screens are declared here too. There are important advantages for this approach of using a JSON file to load things like styles, content and game paths - rather than hard-coding them. Firstly, Firstly, it makes the game easier to configure. Secondly, it means you can allow your non-technical colleagues to configure the game as well, whilst not worrying about them breaking it, because they will not need to touch the source code. Thirdly, Thirdly, the JSON configuration file acts as a contract detailing the parts of the game which the business wants to be customisable. Nobot - Build Tool

Finally we come on to the main tool we are building in the book that interacts with a Project allocation tool API, pulls in values, builds the game, and then deploys to the website’s releases directory. directory. The explanation of how this is built will follow this chapter. GitHub Repository Build Tool

https://github.com/smks/nobot High Level Overview

Check out this diagram which details how the repositories interact with one

another from a high level.

With an overview of every repository we need, and an understanding under standing of the flow, we can get to work on the planning of our build tool.

Build Tool Planning Our team now wants to focus on the directory structure of our build tool, and try to understand how it’s to be architected. As an overview, overview, we would have something along these lines drawn up on a whiteboard. ├─── repositories



└─── templates

└───src

├─── commands

├─── constants

├─── creators



└─── rock-paper-scis rock-paper-scissors sors

├─── helpers

└─── setup

repositories Our first directory will be one identified as

repositories

. In here we will clone all

of the repositories we need, as mentioned in the previous pr evious chapter. We want to clone all the templates under a subdirectory called c alled

templates

 . When

we release the template to the website, we clone the website repository too so we can copy it there. So our directory structure as agreed with the team will be like so: ├── │ │ │ └──

templates └── rock-paper-scis rock-paper-scissors sors └── template-2 └── template-3 website

We shouldn’t clone all of these repositories repos itories manually. manually. So at this point we agree agr ee to create a command to as

nobot

setup

the build tool. Our build tool is globally identified

, and will therefore have this command: command:

$ nobot setup cloning repositories...

src The

src

directory will contain contain all of our source code related to the build tool.

This is what we will have in this directory. directory. ├── ├── ├── ├── ├── └──

commands constants creators helpers nobot.js setup

commands In here will be a file for each command. What was initially agreed was to create a command that builds the game and deploys it to the website, with another that releases a new version of a template. So we would need three commands total. Set up repositories: nobot setup

Deploy a Game: nobot game

Deploy a Template: nobot template

One developer suggests that when creating a game, we should pass it the ticket ID. The API would then retrieve all of the information needed, and feed it into the game we are building. So we amend the command slightly s lightly.. nobot game

An issue raised about the template command comes up too. We should choose

the template we want to release. So we agree to provide an option to the template. nobot template --id="rock-paper-scissors"

If not provided as an option like above, or if the id does not match the available templates, the script will prompt the user to choose one that is available for release.

Please note: we could add more commands, but we are keeping to the basic idea of the build tool and building a game.

constants This directory will contain any constants that can be imported by other scripts whenever they need them. For example, when creating a log script, we want different levels of logging.

 src/constants/log-level.js const LOG_LEVELS = {

ERROR: 'error' ERROR: 'error', , WARNING: WARNING : 'warning' 'warning', , INFO: INFO : 'info' 'info', , SUCCESS: SUCCESS : 'success' }; module. module .exports = LOG_LEVELS  LOG_LEVELS; ;

creators The command

game

will delegate the creation of games to creators, rather than

containing all of the logic. This is because each template will have its own process of creation. The command script will use a switch case to choose the correct template, and then use a creator function, passing it the ID of the ticket

and the data from the API. switch (template) { case ROCK_PAPER_SCISSORS ROCK_PAPER_SCISSORS: :

createRockPaperScissors(ticketId, createRockPaperScissors(ticketId , data)  data); ; // our creator script break; // ... other templates

default: default : throw new Error Error( (`Could not find template ${template ${template}` }`) );

}

helpers Helpers are reusable functions that we can use anywhere. Here is a list of some of the helpers we should build: Create release build of a template. Create a deployment branch. Get the path of the repositories directory. Updating a template. These helper functions will be imported when needed, and will help us to avoid code repetition.

setup These scripts will deal with the process of cloning the repositories. If we needed to do some more setting up for the build tool, we would add it in here.

nobot.js This behaves as the umbrella/entry point into our CLI application. Take a concierge as an example, retrieving input from the user and directing them to the right door (our commands). This will be our focus in the next chapter, in which we will use a library called

commander

.

Finally, Finally, we have to talk about abo ut the dummy API I set up for this book, called Nira, which in your case would be something like Target Target Process or Jira. I thought it would be wise to create my own dependency rather than relying on another API that is constantly changing. I have used an endpoint contract similar to Jira’s. http://opencanvas.co.uk/nira/re http://opencanv as.co.uk/nira/rest/api/latest/t st/api/latest/ticket?authKey=N icket?authKey=NOBOT_123&ticket OBOT_123&ticketId=GS-100 Id=GS-100

The way this will work is that you make a simple GET request, and the API will respond with a JSON object with all the data - fetched from the requested ticket about a specific game. It is operating like a REST API. 1.

http://opencanvas.co.uk/nira/ http://opencan vas.co.uk/nira/rest/api/latest rest/api/latest/ticket /ticket

2.

authKey

is the API endpoint.

is to grant the the user access to the sensitive content. (Without (Without this, it

returns a 404) 3.

ticketId

is the unique identifier of the game you need to create.

I have set up five API calls in the backend for this book listed below. below. Each API call will return a JSON object with data associated with that game. Here is an example response: { id: 36235 36235, , template: "rock-paper-scissors" "rock-paper-scissors", , projectName: "fire-water-earth-cute" "fire-water-earth-cute", , font: "Cabin" "Cabin", , fontUrl: "https://fonts. "https://fonts.googleapis.com/ googleapis.com/css?family=Cabi css?family=Cabin" n", , assetsPath: "http://cdn.op "http://cdn.opencanvas.co.uk/ encanvas.co.uk/automatingwithn automatingwithnodejs/assets/ro odejs/assets/rock-paper-scisso ck-paper-scissors/fire-water-e rs/fire-water-earth-cute" arth-cute", labelFirstOption: "fire" "fire", , labelSecondOption: "water" "water", , labelThirdOption: "earth" "earth", , screenChoiceTitle: "Fire Water & Earth", Earth", screenChoiceSubtitle: "Choose your element", element", screenResultWon: "you won!", won!", screenResultLost: "you lost!", lost!", screenResultDraw: "it's a draw!", draw!", screenResultReplay: "replay" "replay", , screenResultFeedbackWon: "{player} beats {cpu}", {cpu}", screenResultFeedbackLost: "{cpu} beats {player}", {player}", screenResultFeedbackDraw: "Nobody won this time"

}

Games List Fire Water Earth Cute

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket? authKey=NOBOT_123&ticketId=GS-100

Fire Water Earth Fantasy

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket? authKey=NOBOT_123&ticketId=GS-101

Fire Water Earth Retro

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket? authKey=NOBOT_123&ticketId=GS-102

Rock Paper Scissors Doodle

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket? authKey=NOBOT_123&ticketId=GS-103

Rock Paper Scissors Modern

API call: http://opencanvas.co.uk/nira/rest/api/latest/ticket? authKey=NOBOT_123&ticketId=GS-104

This means that we will have to make a HTTP request in our build tool. This will be done using the imported

axios

  library.

Right, we have planned enough to know what we have to do. Let’s get to work!

Commander If you haven’t heard of commander, you should. It’s a great way to bootstrap your CLI application. I think it would be good to start with an overview of the entire script, after which we will make it together, step by step.

 src/nobot.js const nobot = require require( ('commander' 'commander') ); const { version } = require require( ('../package' '../package') ); // commands

const setup = require require( ('./commands/setup' './commands/setup') ); const game = require require( ('./commands/game' './commands/game') ); const template = require require( ('./commands/template' './commands/template') );

nobot .version version(version) (version); ; nobot .command command( ('setup' 'setup') ) .description description( ('clone repository dependencies') dependencies') .action action(setup) (setup); ; nobot .command command( ('game ') ') .description description( ('create and deploy a new game reskin') reskin') .action action(game) (game); ; nobot .command command( ('template' 'template') ) .description description( ('release core files of template') template') .option option( ('-i, --id, [id]', [id]', 'what template to release') release') .action action(template) (template); ; nobot .command command( ('*' '*') ) .action action(() (() => nobot nobot. .help help()) ()); ; nobot. nobot .parse parse( (process process. .argv argv) ); if (  (! !process process. .argv argv. .slice slice( (2). ).length length) ) {

nobot. nobot .help help() (); ; }

 src/nobot.js

What we do first, is create a new program called of commander. I extract the version key from

nobot

. This will be our instance

package.json

dynamically on the next

line. const nobot = require require( ('commander' 'commander') ); const { version } = require require( ('../package' '../package') );// e.g. 1.0.0

Next I require/import all of the commands which are found under the

commands

directory. directory. At present they would be b e empty JavaScript files. // src/commands/se src/commands/setup.js tup.js // src/commands/ga src/commands/game.js me.js // src/commands/te src/commands/template.js mplate.js // commands

const setup = require require( ('./commands/setup' './commands/setup') ); const game = require require( ('./commands/game' './commands/game') ); const template = require require( ('./commands/template' './commands/template') );

I pass the version ver sion number, e.g. instance

nobot

1.0.0

, to the version method on the the commander

. This will output the version in the CLI.

nobot .version version(version) (version); ;

Commander allows each command to have a command identifier (e.g.

setup

description to explain what that command does, and a function to call as the

), a

action to that command. Each of our commands are separate scripts that we import and pass to the

action

  function.

The first command we will declare is

setup

, as agreed in the meeting. This This

command will clone the external repositories we depend on: The templates repository, repository, and the website repository. repository. nobot .command command( ('setup' 'setup') ) .description description( ('clone repository dependencies') dependencies') .action action(setup) (setup); ;

Our next command is

game

. This command will be used to create and deploy a

new game. In the example below, below, you can see that it expects an option to be passed, enclosed in angled brackets number e.g.

GS-101



. This value would be the ticket ticket

, where all of the data related to the game will be fetched from

using the Nira API. Angle brackets signify that this is a mandatory value without which the command will not be executed. Alternatively, Alternatively, you can wrap it in square brackets, meaning it’s optional

[ticketId]

. When using square brackets, the

script would continue even if the optional value was not passed as an option. nobot .command command( ('game ') ') .description description( ('create and deploy a new game reskin') reskin') .action action(game) (game); ;

Next up is the

template

command. Each template template will use Semantic Versioning. Versioning.

We want to create a command that will fetch the latest version of the template and copy the bundled JavaScript and CSS to the So if I have version the command and

template

1.0.0

core

as the current version, and

directory of the website. 1.1.0

is the latest version,

will build build this and copy over the files

rock-paper-scissors.1.0.1.css

to our

nobot-website

  repository’s

core

rock-paper-scissors.1.0.1.js

  directory.

Semantic Versioning A versioning system that is made up of 3 components, X.Y.Z X.Y .Z where: X is the major version, Y is the minor version, and Z is the patch version. Semantic versioning is applied to projects. In our case, it would be a game template. When releasing your application: if you are fixing bugs without introducing breaking changes, you would increment the patch version (Z); If you are adding a feature that doesn’t have breaking changes, you would increment the minor version (Y); If you are making breaking changes, you would increment the major version (X). The ID of the template

rock-paper-scissors

can be passed as an option, and if it isn’t,

then we will prompt the user to choose from an array of supported templates. This will be demonstrated later on.

Please note: The argument

-i

has been made optional for demonstration demonstration

purposes. Another way to support options in your command is to use the

option

  function.

This function takes two parameters: the option format and its description. The

option format accepts a string with comma-separated flags, e.g. ‘-i, —id, [id]’. The description parameter accepts a string describing the option, e.g. ‘what template to release’. nobot .command command( ('template' 'template') ) .description description( ('release core files of template') template') .option option( ('-i, --id, [id]', [id]', 'what template to release') release') .action action(template) (template); ;

Now if the user types a command in the CLI other than the three stated above, then we want to capture that, and instead show the user what is actually available. So to do this, we use an asterisk to catch everything other than the three commands by doing this. It behaves like a regular expression. nobot .command command( ('*' '*') ) .action action(() (() => nobot nobot. .help help()) ()); ; nobot. nobot .parse parse( (process process. .argv argv) );

As a last catch, if the user types only

nobot

into the terminal and hits enter, enter, then

we want to also output the help list so they can understand what else has to be inputted. if (  (! !process process. .argv argv. .slice slice( (2). ).length length) ) {

nobot. nobot .help help() (); ; }

So there we have it, the first script in our build tool. This will be the main entry point into our build tool, and it will route us to the commands by typing them out into the CLI. You You can see commander provides a user friendly interface to try and help the user understand the app’s capabilities. This is actioned by invoking nobot.help

.

When you run it you will see an output like this: $ node src/nobot.js

Usage: nobot [options] [command]

  Options: -V, --version output the version number -h, --help output usage information

  Commands: setup game template [options] *

clone dependent repositories create and deploy a new game reskin release core files of template

Now let’s let’s make this script easier eas ier to use. At the moment, to use this script, we’d need to run

node [path to script]

object you can set called

 . We can do better. In your

bin

 . Running

npm link

package.json file

 , there is an

in the directory of the

will make a global alias for each property set in

bin

package.json

. But you don’t have to to

worry about doing that for this project, as it’s already taken care of by the script, which you can run by doing

npm run init

init.js

in the root of the nobot repository. repository.

"bin": {

"nobot": "nobot" : "./src/nobot.js" }

In here, I am declaring a global command called nobot, and pointing it to the file src/nobot.js

.

So now run

npm run init

. You You will see something like this amongst the output of

this script. /usr/local/bin/nobot /usr/local/bin/ nobot -> /usr/local/lib /usr/local/lib/node_modules/n /node_modules/nobot/src/nobot. obot/src/nobot.js js

Please note: This has been done on a Mac and will look different for a Windows machine. I am setting up an identifier called

nobot

neat. Let’s test it to see what happens.

and linking it to a JavaScript file. Pretty

$ nobot Usage: nobot [options] [command]

  Options: -V, --version output the version number -h, --help output usage information

  Commands: setup game template [options] *

clone all the templates and deployment website creates and deploys a new game reskin releases core of template

Splendid, we have an entry point into our application. Now let’s move on to API configuration, template locations and the deployment process.

Configuration Before we can build our setup command, we want to think about a configuration file that can hold specific details about what templates we are using, what repository are we deploying to, what base branch we branch off of: develop

master

,

? What is the API URL we are using to obtain the values we need to build

the game? All of this can be declared in a

config.json

file. This file is not included

in source control, because we would be committing sensitive data.  You might have noticed that you have a Please note: You project, alongside the

config.example.json

which you have run via

npm run init

config.json

file in your

file. This was done by the the

init.js

  script

in the last chapter. chapter.

If we look at the implementation used in nobot, we can see how it’s beneficial to have dynamic configuration rather than hard coding it all into our ou r scripts. You’ll You’ll need to change “https://github.com/smks/nobot “https://github.com/smks/nobot-website.git” -website.git” and “https://github.com/smks/nobot-template-rock-paper-scissors” “https://github.com/smks/nobot-t emplate-rock-paper-scissors” to your forked forke d repositories’ URLs. URLs. The second is the

api.authKey

, which as shown below, below, needs to

be “NOBOT_123”. This key permits you to retrieve data from the API. Without this key, key, the API will respond with a 404 page.

 config.json

You will need to make two changes in this file. The first is the URLs of the nobot repositories outlined in the initial part 2 chapter. chapter. The second is the

authKey

.

You are free to make API calls to Nira. { "api": { "api": "authKey": "authKey" : "NOBOT_123" "NOBOT_123", , "endpoint": "endpoint" : "http://opencan "http://opencanvas.co.uk/nira/ vas.co.uk/nira/rest/api/latest rest/api/latest/ticket" /ticket" }, "deploy": "deploy" : { "baseBranch": "baseBranch" : "master" "master", , "name": "name" : "website" "website", , "repo": "repo" : "https://githu "https://github.com/smks/nobo b.com/smks/nobot-website.git" t-website.git", , "coreDirectory": "coreDirectory" : "core" "core", , "releaseDirectory": "releaseDirectory" : "releases" }, "templates": "templates" : { "rock-paper-scissors": "rock-paper-scissors" : { "baseBranch": "baseBranch" : "master" "master", , "repo": "repo" : "https://githu "https://github.com/smks/nobo b.com/smks/nobot-template-rock t-template-rock-paper-scissors -paper-scissors" " } } }

So at the top we have an object that contains details about the API we are calling to retrieve the data. The

authKey

, in the case of Jira (at the time of writing), would

be Base64 encoded Basic Auth. We We have just set it as a GET parameter with the value “NOBOT_123” for simplicity. simplicity. "api": {

"authKey": "NOBOT_123" "authKey": "NOBOT_123", , "endpoint": "endpoint" : "http://openca "http://opencanvas.co.uk/nira nvas.co.uk/nira/rest/api/lates /rest/api/latest/ticket" t/ticket" }

Next we want to contain details about the deployment process. This is under the deploy

object. We We may choose to have our base branch as

master

to trial the build tool first, set it to a separate branch such as

or if we wanted

develop

 . The

name

is

used to specify a different name to the actual repository for convenience. This is the simple branching strategy we will be applying.

The

repo

is the repository repository we want to clone. When following along you would would

have forked your own. This would be changed so that you have permissions to do deployments.

coreDirectory

is when the command

template version, it will copy it to the Similarly to the running the

game

releaseDirectory

core

template

directory of the

is releasing a new website

  repository.

, all games will be released to this directory when

  command.

"deploy": {

"baseBranch": "master" "baseBranch": "master", , "name": "name" : "website" "website", , "repo": "repo" : "https://github. "https://github.com/smks/nobotcom/smks/nobot-website.git" website.git", , "coreDirectory": "coreDirectory" : "core" "core", , "releaseDirectory": "releaseDirectory" : "releases" }

Finally, Finally, we have a list of o f the templates that will be cloned. Only one template exists for this book, but b ut this would grow, as would your game template creations. "templates": {

"rock-paper-scissors": { "rock-paper-scissors": "baseBranch": "baseBranch" : "master" "master", , "repo": "repo" : "https://githu "https://github.com/smks/nobo b.com/smks/nobot-template-rock t-template-rock-paper-scissors -paper-scissors" " } }

Constants A single place to declare constants is good. Plus, it helps avoid the mystifying ‘Magic Numbers’ problem.

Magic Numbers A magic number is a direct usage of a number in code. Since it has the chances of changing at a later stage, it can be said that the number is hard to update. It isn’t recommended and is considered to be a breakage of one of the oldest rules of programming. When a user chooses a template, they can optionally cancel. We We are going to use a constant rather than hard-coding common constants.

-1

. We We create a file that can contain many

 src/constants/common.js const COMMON = {

JSON_WHITESPACE: 4, JSON_WHITESPACE: GAME_JSON: GAME_JSON : 'game.json' 'game.json', , NO_CHOICE_MADE: NO_CHOICE_MADE : -1 }; module. module .exports = COMMON  COMMON; ;

Rather than passing the same strings, such as

'error'

or

'info'

 , in many places,

we put them in constants so that if we change them, they get updated everywhere. Although we only have two constants objects, this would potentially grow as the features of the application increase.

 src/constants/log-level.js const LOG_LEVELS = {

ERROR: 'error' ERROR: 'error', , WARNING: WARNING : 'warning' 'warning', , INFO: INFO : 'info' 'info', , SUCCESS: SUCCESS : 'success'

}; module. module .exports = LOG_LEVELS  LOG_LEVELS; ;

When we want to use it in another script, we would import like so. const { ERROR  ERROR, , INFO  INFO, , SUCCESS  SUCCESS, , WARNING } = require require( ('./../../constants/log-level');

These log level constants will be used for our log helper demonstrated in the next chapter. For now we are getting it ready for use.

 src/constants/templates.js const TEMPLATES = {

ROCK_PAPER_SCISSORS: ROCK_PAPER_SCISSORS : 'rock-paper-scissors' }; module. module .exports = TEMPLATES  TEMPLATES; ;

Here is a place to declare all of our template constants. Simple and straightforward, and is used for our switch case when the template has been chosen. The

template

command will match match this constant to a creator. creator. Below is just

a example of what we will be creating later on. const { ROCK_PAPER_SCIS ROCK_PAPER_SCISSORS SORS } = require require( ('./../constants/templates'); //...

switch (template) { case ROCK_PAPER_SCISSORS ROCK_PAPER_SCISSORS: : // use creator

break;

// ...etc.

}

Helpers This chapter will output each of the helpers and explain their purpose. You You should keep following along with the code examples, as we will be using these helpers in our commands and creators.

 src/helpers/build-template.js

Our templates should follow a consistent build process. When I refer to the build process, I am talking about installing all of the node dependencies, and about the npm task that transpiles, minifies, and does everything else necessary to make the template ready for production. This helper will be needed for preparing the game for release, and building the core functionality. functionality. const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const buildTemplate = (templatePath) => {

cd(templatePath); cd(templatePath) ; exec( exec ('npm install') install'); exec( exec ('npm run build') build'); }; module. module .exports = buildTemplate  buildTemplate; ;

 src/helpers/create-deploy-branch.js

This helper is used to create a new branch for the website repository. It starts by switching to the base branch (this could be master or develop) and pulling in all of the latest commits. When these changes have been pulled through, it creates a new branch - this would be prefixed with the ticket number and a short description (e.g. the project name) so that it can be identified. const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { deploy deploy: : { baseBranch } } = require require( ('../../config' '../../config') ); const releasePath = require require( ('./get-release-path' './get-release-path') ); const createDeployBr createDeployBranch anch = (branchName) => {

cd(releasePath); cd(releasePath) ; exec( exec (`git checkout ${baseBranch ${baseBranch}` }`) ); exec( exec (`git pull origin ${baseBranch ${baseBranch}` }`) ); exec( exec (`git checkout -b ${branchName ${branchName}` }`) ); }; module. module .exports = createDeployBr createDeployBranch anch; ;

 src/helpers/deploy-game.js

This helper deals primarily with source control. Staging your project production build, committing it with a message, switching to the base branch, pulling the latest commits, and then merging your feature branch to the base branch. This happens on the website repository. const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { deploy deploy: : { baseBranch } } = require require( ('../../config' '../../config') ); const releasePath = require require( ('./get-release-path' './get-release-path') ); const log = require require( ('./log' './log') ); const { INFO } = require require( ('../constants/log-levels'); const deployGame = (branchName  (branchName, , projectName  projectName, , ticketId) => {

log(`changing to path ${releasePath log( ${releasePath}` }`, , INFO)  INFO); ; cd(releasePath) cd (releasePath); ; exec( exec (`git pull origin ${baseBranch ${baseBranch}` }`) ); log( log (`staging project ${projectName ${projectName}` }`, , INFO)  INFO); ; exec( exec (`git add ${projectName ${projectName}` }`) ); exec( exec (`git commit -m "${ticketId "${ticketId} } - ${projectName ${projectName} } release"`) release"`); log( log (`switching to base branch ${baseBranch ${baseBranch}` }`, , INFO)  INFO); ; exec( exec (`git checkout ${baseBranch ${baseBranch} } && git pull origin ${baseBranch ${baseBranch}` }`) ); log( log (`merging ${branchName ${branchName} } into ${baseBranch ${baseBranch}` }`, , INFO)  INFO); ; exec( exec (`git merge ${branchName ${branchName}` }`) ); exec( exec (`git push origin ${baseBranch ${baseBranch}` }`) ); exec( exec (`git branch -d ${branchName ${branchName}` }`) ); }; module. module .exports = deployGame  deployGame; ;

Just to clean up, we delete the feature branch we created.

 src/helpers/deploy-template.js

This helper is quite similar to the previous helper

 , and although there

deploy-game.js

are not many differences, I would prefer that deployment for template and game are not entwined just in case their process changes.

const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { deploy deploy: : { baseBranch } } = require require( ('../../config' '../../config') ); const websitePath = require require( ('./get-website-path' './get-website-path') ); const log = require require( ('./log' './log') ); const { INFO } = require require( ('../constants/log-levels'); const deployTemplate = (template  (template, , version) => { const branchName = `${ `${template template}-${ }-${version version}` }`; ;

log(`changing to path ${websitePath log( ${websitePath}` }`, , INFO)  INFO); ; cd(websitePath) cd (websitePath); ; exec( exec (`git pull origin ${baseBranch ${baseBranch}` }`) ); log( log (`staging template ${branchName ${branchName}` }`, , INFO)  INFO); ; exec( exec (`git checkout -b ${branchName ${branchName}` }`) ); exec( exec ('git add core/*') core/*'); exec( exec (`git commit -m "${template "${template}.${ }.${version version}"` }"`) ); log( log (`switching to base branch ${baseBranch ${baseBranch}` }`, , INFO)  INFO); ; exec( exec (`git checkout ${baseBranch ${baseBranch} } && git pull origin ${baseBranch ${baseBranch}` }`) ); log( log (`merging ${branchName ${branchName} } into ${baseBranch ${baseBranch}` }`, , INFO)  INFO); ; exec( exec (`git merge ${branchName ${branchName}` }`) ); exec( exec (`git push origin ${baseBranch ${baseBranch}` }`) ); exec( exec (`git branch -d ${branchName ${branchName}` }`) ); }; module. module .exports = deployTemplate  deployTemplate; ;

 src/helpers/get-deploy-core-path.js

Our path to release the core bundle files is returned from this helper. It saves us reconstructing the path in multiple places. const { join } = require require( ('path' 'path') ); const repositoryPath = require require( ('./get-repositories-path'); const { deploy deploy: : { name  name, , coreDirectory } } = require require( ('../../config' '../../config') );

module. module .exports = join join(repositoryPath (repositoryPath, , name  name, , coreDirectory)  coreDirectory); ;

 src/helpers/get-release-path.js

Our path to release the project implementation is returned from this helper. helper. It saves us reconstructing the path in multiple places. const { join } = require require( ('path' 'path') ); const repositoryPath = require require( ('./get-repositories-path'); const { deploy deploy: : { name  name, , releaseDirecto releaseDirectory ry } } = require require( ('../../config' '../../config') );

module. module .exports = join join(repositoryPath (repositoryPath, , name  name, , releaseDirecto releaseDirectory) ry); ;

 src/helpers/get-repositories-path.js

The repositories path that contains all of our external repositories. const { join } = require require( ('path' 'path') );

module. module .exports = join join(__dirname (__dirname, , '..' '..', , '..' '..', , 'repositories' 'repositories') );

 src/helpers/get-templates-path.js

The path that has the list of templates we currently support. const { join } = require require( ('path' 'path') ); const repositoryPath = require require( ('./get-repositories-path');

module. module .exports = join join(repositoryPath (repositoryPath, , 'templates' 'templates') );

 src/helpers/get-ticket-data.js

This is the helper that will make a HTTP request to our API. For that we make use of a library called

axios

, which deals with the underlying call. As you can

see, it’s importing data from our configuration to extract the authentication key and endpoint. The

axios

library conveniently returns us a promise.

const axios = require require( ('axios' 'axios') ); const { api api: : { authKey  authKey, , endpoint } } = require require( ('../../config' '../../config') ); const getTicketData = ticketId => axios axios( ({

url: url : endpoint  endpoint, ,

params: { params: authKey, authKey, ticketId } });    

module. module .exports = getTicketData  getTicketData; ;

 src/helpers/get-website-path.js

This is the path to the repository where we deploy our projects. const { join } = require require( ('path' 'path') ); const repositoryPath = require require( ('./get-repositories-path'); const { deploy deploy: : { name } } = require require( ('../../config' '../../config') );

module. module .exports = join join(repositoryPath (repositoryPath, , name)  name); ;

 src/helpers/log.js

This was demonstrated in one of the examples, and is a personal preference of mine for logging different levels with associated colours.

require('colors' require( 'colors') ); const {   ERROR, ERROR, WARNING  WARNING, , INFO  INFO, , SUCCESS } = require require( ('../constants/log-levels'); const log = (message  (message, , type) => { let colorMessage  colorMessage; ; switch (type) { case ERROR ERROR: :

 

 

 

 

 

colorMessage break; case WARNING WARNING: : colorMessage break; case INFO INFO: : colorMessage break; case SUCCESS SUCCESS: : colorMessage break; default: default : colorMessage

= `[${ `[${ERROR ERROR}] }] ${message ${message}` }`. .red red; ;

= `[${ `[${WARNING WARNING}] }] ${message ${message}` }`. .yellow yellow; ;

= `[${ `[${INFO INFO}] }] ${message ${message}` }`. .blue blue; ;

= `[${ `[${SUCCESS SUCCESS}] }] ${message ${message}` }`. .green green; ;

= message  message; ;

} console. console .log log(colorMessage) (colorMessage); ; }; module. module .exports = log  log; ;

 src/helpers/update-template.js

This helper is used to pull in any bug fixes or features from the latest version of the template. We We do this before running the const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { templates } = require require( ('../../config' '../../config') ); const updateTemplate = (template  (template, , templatePath) => {

cd(templatePath) cd (templatePath); ; const { baseBranch } = templates[tem templates[template] plate]; ; exec( exec (`git pull origin ${baseBranch ${baseBranch}` }`) ); }; module. module .exports = updateTemplate  updateTemplate; ;

  helper.

build-template.js

With our helpers, we can now proceed with other scripts.

Setup The setup command exists so that we can initialise the build tool. Because we need to retrieve templates and deploy projects to our games website, we need to pull in these repositories, so the build tool can do what it was born to do. First, we will clone the website under our

repositories

directory. directory. The following

script will deal with that process.

 src/setup/deployment.js

With the use of npm installed libraries, the native Node API, and some of our helpers, we can achieve the task of cloning our deployment repository. repository. In this case, it’s the Nobot Game Studios website. This script’s goal is to check if the repository exists, if it doesn’t, then we have to clone it. const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { existsSync } = require require( ('fs' 'fs') ); const { deploy deploy: : { name  name, , repo } } = require require( ('../../config' '../../config') ); const log = require require( ('../helpers/log' '../helpers/log') ); const repositoriesPa repositoriesPath th = require require( ('../helpers/get-repositories-path'); const websitePath = require require( ('../helpers/get-website-path'); const { INFO } = require require( ('../constants/log-levels'); const setupDeploymen setupDeployment t = () => { if (  (existsSync existsSync(websitePath)) (websitePath)) { return log log( (`Deployment Repository '${websitePath '${websitePath}' }' exists`, exists`, INFO)  INFO); ; } cd(repositoriesPath) cd (repositoriesPath); ; return exec exec( (`git clone ${repo ${repo} } --progress ${name ${name}` }`) ); };

module. module .exports = setupDeploymen setupDeployment t;

Perfect! We We now have a script that will clone our website, and now we want to clone all of the production ready templates. The build tool will then be able to pick up these templates for deployment when the script below is run.

 src/setup/templates.js

A similar similar thing is done with the templates, but we are looping over an object’s keys. For each, if they don’t exist already, we clone the repository. repository. This makes it work dynamically as new template repositories are introduced. const { cd  cd, , exec } = require require( ('shelljs' 'shelljs') ); const { existsSync } = require require( ('fs' 'fs') ); const { join } = require require( ('path' 'path') ); const log = require require( ('../helpers/log' '../helpers/log') ); const templatesPath = require require( ('../helpers/get-templates-path'); const { templates } = require require( ('../../config' '../../config') ); const setupTemplates = () => {

cd(templatesPath); cd(templatesPath) ; Object. Object .keys keys(templates). (templates).map map((template) ((template) => { const templatePath = join join(templatesPath (templatesPath, , template)  template); ; if (  (existsSync existsSync(templatePath)) (templatePath)) { return log log( (`Template ${template ${template} } exists`, exists`, 'info' 'info') ); } log( log (`Downloading ${template ${template}` }`, , 'info' 'info') ); const { baseBranch  baseBranch, , repo } = templates[templ templates[template] ate]; ; return exec exec( (`git clone ${repo ${repo} } --branch ${baseBranch ${baseBranch} } --progress ${template ${template}` }`) ); }); }; module. module .exports = setupTemplates  setupTemplates; ;

These two scripts will be invoked when we run the

setup

command of nobot.

Below is a pseudo example of what will happen. $ nobot setup cloning website... cloning templates...

Command - Setup We include the setup scripts shown in the previous chapter, and invoke them.

 src/commands/setup.js const setupDeploymen setupDeployment t = require require( ('../setup/deployment' '../setup/deployment') ); const setupTemplates = require require( ('../setup/templates' '../setup/templates') ); const setup = () => {

setupDeployment(); setupDeployment() ; setupTemplates() setupTemplates (); ; }; module. module .exports = setup  setup; ;

So just to explain, when we run: $ nobot setup // running setup command

It will call both the deployment and template setup scripts, so that all of our repositories are ready for the game release process.

Command - Template This command is a bit more involved. Let’s Let’s step through each code block, starting with the imported modules.

 src/commands/template.js const fse = require require( ('fs-extra' 'fs-extra') ); const { join } = require require( ('path' 'path') ); const templatesPath = require require( ('../helpers/get-templates-path'); const deployCorePath = require require( ('../helpers/get-deploy-core-path'); const buildTemplate = require require( ('../helpers/build-template'); const updateTemplate = require require( ('../helpers/update-template'); const log = require require( ('../helpers/log' '../helpers/log') ); const readlineSync = require require( ('readline-sync' 'readline-sync') ); const { SUCCESS  SUCCESS, , ERROR } = require require( ('../constants/log-levels'); const { NO_CHOICE_MADE } = require require( ('../constants/common' '../constants/common') ); const deployTemplate = require require( ('../helpers/deploy-template');

fs-extra

is being used to copy the template core files.

is being used to construct the path to the chosen template.

join

templatesPath, deployCorePath

are the helpers for getting the template and deploy

path. buildTemplate updateTemplate

is a helper to build the template. is a helper to update the template template before we build it to make

sure it’s the latest stable version. log

is our log log helper to log anything that went right/wrong.

readlineSync

is used to read input from the user. user.

SUCCESS, ERROR

are log level constants. constants. We We only need these two two in this case.

NO_CHOICE_MADE

is a constant that signifies the user cancelled cancelled a choice of

template. deployTemplate

is used to deploy the template template and merge to the base branch,

once we are ready to do so. Now to the main function, assigned to

template

 . The

passed as input from fr om the user. This is captured in

id

nobot.js

parameter is optionally  . We scan the directory

of templates to return an array (making sure to filter out anything NOT template related). If the user does not pass the nobot template

--id=rock-paper-scissors

option when calling the

command or they did did enter a template template but it doesn’t exist, we prompt

the user with the

readline-sync

library to choose from templates templates that ‘do’ exist.

const template = (  ({ { id }) => { let choice = id  id; ; const templates = fse fse. .readdirSync readdirSync(templatesPath). (templatesPath).filter filter(t (t => t.match match( (/\. \./ /) === null); if (choice === undefined || templates templates. .includes includes(choice) (choice) === false) { const index = readlineSync readlineSync. .keyInSelect keyInSelect(templates (templates, , 'choose template to release ') '); if (index === NO_CHOICE_MADE) {

log('template release cancelled', log( cancelled', ERROR)  ERROR); ; process. process .exit exit( (0); } choice = templates[index templates[index] ];

  }

By this point, we would have the choice from the user us er.. So we create a template path, and then update the template using the helper we created cre ated earlier. Following Following that, we build it. // get template path

const templatePath = join join(templatesPath (templatesPath, , choice)  choice); ; // update the template

updateTemplate(templatePath) updateTemplate (templatePath); ; // build the core files

buildTemplate(templatePath) buildTemplate (templatePath); ;

Now that the template has built the core cor e files, it’s just a case of copying them into the

core

directory of our website repository. repository.

const templateRelease templateReleaseSource Source = join join(templatePath (templatePath, , 'public' 'public', , 'core' 'core') ); const templateRelease templateReleaseDestination Destination = deployCorePath  deployCorePath; ; const templatePackage templatePackageJson Json = join join(templatePath (templatePath, , 'package.json' 'package.json') ); const { version } = require require(templatePackageJson) (templatePackageJson); ;

fse.copy fse. copy(templateReleaseSource (templateReleaseSource, , templateReleas templateReleaseDestination) eDestination) .then then(() (() => { deployTemplate(choice deployTemplate (choice, , version)  version); ; log( log ('released latest template version', version', SUCCESS)  SUCCESS); ; }) .catch catch(e (e => log log(e (e, , ERROR))  ERROR)); ; }; module. module .exports = template  template; ;

The

core

directory would over time have something like this. this.

- core -- template-1.0.0. template-1.0.0.css css -- template-1.0.0. template-1.0.0.js js -- template-1.0.1. template-1.0.1.css css -- template-1.0.1. template-1.0.1.js js -- template-1.0.2. template-1.0.2.css css -- template-1.0.2. template-1.0.2.js js -- template-2.0.0. template-2.0.0.css css -- template-2.0.0. template-2.0.0.js js

Command - Game As mentioned before, this command will be delegating each game creation to a creator. This command’s command’s responsibility is to pass the ticket information to the creator and nothing more. So let’s let’s take a look.

 src/commands/game.js

We start by importing the templates, the error log level constant, the helper created earlier to fetch the data from our API, our custom log function, and the creator function. require('colors' require( 'colors') ); const { ROCK_PAPER_SCIS ROCK_PAPER_SCISSORS SORS } = require require( ('../constants/templates'); const { ERROR } = require require( ('../constants/log-levels'); const getTicketData = require require( ('../helpers/get-ticket-data'); const log = require require( ('../helpers/log' '../helpers/log') ); // game creators

const createRockPape createRockPaperScissors rScissors = require require( ('../creators/rock-paper-scissors');

Our main function getTicketData

Because

game

receives the mandatory ticket ID parameter. parameter. The

helper will use this ticket ID to fetch the associated data from Nira. axios

uses a promise implementation, implementation, we return the

data

part of the

response object. The ticket determines the template to be used (which should be correctly decided by the product owner). If the template matches one of the cases in the switch statement, it calls the

relevant creator. Otherwise, we log an error. const game = (ticketId) => {

getTicketData(ticketId) getTicketData(ticketId) .then then(( (({ { data }) => { const { template } = data  data; ; switch (template) { case ROCK_PAPER_SCISSORS ROCK_PAPER_SCISSORS: :

createRockPaperScissors(ticketId, createRockPaperScissors(ticketId , data)  data); ; break; default: default : throw new Error Error( (`Could not find template ${template ${template}` }`) ); } }) .catch catch(e (e => log log(e (e, , ERROR))  ERROR)); ;

}; module. module .exports = game  game; ;

So this command simply fetches the data from Nira and pas passes ses it to the creator.

Creator - Rock Paper Scissors I’ve created a transformer. Its sole purpose is to take the values from the API, and transform them into our JSON configuration format. When I used Jira, there were custom fields set that had no semantic meaning when returned in JSON. I use the original configuration data from the template, so that any values that don’t get overridden by our API data remain as default.

src/creators/rock-paper-scissors/transform.js src/creators/rock-paper -scissors/transform.js const fse = require require( ('fs-extra' 'fs-extra') ); const path = require require( ('path' 'path') ); const templatesPath = require require( ('../../helpers/get-templates-path'); const { ROCK_PAPER_SCIS ROCK_PAPER_SCISSORS SORS } = require require( ('../../constants/templates'); const { GAME_JSON } = require require( ('../../constants/common'); const transform = (  ({ {

id, id ,   projectName, projectName,   font, font,   fontUrl, fontUrl,   assetsPath, assetsPath,   labelFirstOption, labelFirstOption,   labelSecondOption, labelSecondOption,   labelThirdOption, labelThirdOption,   screenChoiceTitle, screenChoiceTitle,   screenChoiceSubtitle, screenChoiceSubtitle,   screenResultWon, screenResultWon,   screenResultLost, screenResultLost,   screenResultDraw, screenResultDraw,   screenResultReplay, screenResultReplay,   screenResultFeedbackWon, screenResultFeedbackWon,   screenResultFeedbackLost, screenResultFeedbackLost,   screenResultFeedbackDraw }) => new Promise Promise((resolve ((resolve, , reject) => { try {

const originalTemplate originalTemplateConfigPath ConfigPath = path path. .join join( (

   

templatesPath, templatesPath, ROCK_PAPER_SCISSORS, ROCK_PAPER_SCISSORS , 'public', 'public' ,   GAME_JSON ); const originalTemplate originalTemplateConfig Config = fse fse. .readJsonSync readJsonSync(originalTemplateConfigPath) (originalTemplateConfigPath); const newConfig = originalTempla originalTemplateConfig teConfig; ; newConfig. newConfig .id = id  id; ; newConfig. newConfig .projectName = projectName  projectName; ; newConfig. newConfig .theme theme. .fontFamily = font  font; ; newConfig. newConfig .customStyles = [   fontUrl ]; newConfig. newConfig .theme theme. .path = assetsPath  assetsPath; ; newConfig. newConfig .labels labels. .rock = labelFirstOptio labelFirstOption n; newConfig. newConfig .labels labels. .paper = labelSecondOpt labelSecondOption ion; ; newConfig. newConfig .labels labels. .scissors = labelThirdOpti labelThirdOption on; ; newConfig. newConfig .screens screens. .choice choice. .title = screenChoiceTi screenChoiceTitle tle; ; newConfig. newConfig .screens screens. .choice choice. .subtitle = screenChoiceS screenChoiceSubtitle ubtitle; ; newConfig. newConfig .screens screens. .result result. .won = screenResultW screenResultWon on; ; newConfig. newConfig .screens screens. .result result. .lost = screenResultLos screenResultLost t; newConfig. newConfig .screens screens. .result result. .draw = screenResultDra screenResultDraw w; newConfig. newConfig .screens screens. .result result. .replay = screenResultRep screenResultReplay lay; ; newConfig. newConfig .screens screens. .result result. .feedback feedback. .won = screenResultFe screenResultFeedbackWon edbackWon; ; newConfig. newConfig .screens screens. .result result. .feedback feedback. .lost = screenResultF screenResultFeedbackLost eedbackLost; ; newConfig. newConfig .screens screens. .result result. .feedback feedback. .draw = screenResultF screenResultFeedbackDraw eedbackDraw; ; resolve(newConfig) resolve (newConfig); ; } catch (e) { reject(e) reject (e); ; } }); module. module .exports = transform  transform; ;

The transform process acts as a bridge or translator between the API and the build tool. Translating the data from one form to another form that the build tool will understand. The function returns back the new configuration object.

src/creators/rock-paper-scissors/index.js src/creators/rock-paper -scissors/index.js

Now onto the actual creation of the game. As usual, we include all of our necessary libraries and helpers. const fse = require require( ('fs-extra' 'fs-extra') ); const { join } = require require( ('path' 'path') ); const templatesPath = require require( ('../../helpers/get-templates-path'); const releasePath = require require( ('../../helpers/get-release-path'); const buildTemplate = require require( ('../../helpers/build-template'); const createDeployBr createDeployBranch anch = require require( ('../../helpers/create-deploy-branch'); const deployGame = require require( ('../../helpers/deploy-game'); const log = require require( ('../../helpers/log' '../../helpers/log') ); const { ROCK_PAPER_SCIS ROCK_PAPER_SCISSORS SORS } = require require( ('../../constants/templates'); const { INFO  INFO, , SUCCESS  SUCCESS, , ERROR } = require require( ('../../constants/log-levels'); const { JSON_WHITESPACE  JSON_WHITESPACE, , GAME_JSON } = require require( ('../../constants/common'); const transform = require require( ('./transform' './transform') );

We want to use our create deploy branch helper, but first we construct a branch name. This is composed of our ticket ID, followed by an underscore, and the name of the project. This keeps our branch both unique so it doesn’t conflict with other projects as well as being meaningful to anyone looking at it. const create = (ticketId  (ticketId, , ticketInformati ticketInformation) on) => { const { projectName } = ticketInformati ticketInformation on; ; // 1. create a branch for deployment repository 

const branchName = `${ `${ticketId ticketId}_${ }_${projectName projectName}` }`; ;

createDeployBranch(branchName) createDeployBranch (branchName); ;

Next we construct the path to our template we want to build. In I n this case it’s ‘Rock Paper Scissors’. This is passed to our

buildTemplate

  helper.

// 2. run npm & build production version of template

const templatePath = join join(templatesPath (templatesPath, , ROCK_PAPER_SCIS ROCK_PAPER_SCISSORS) SORS); ;

buildTemplate(templatePath) buildTemplate (templatePath); ;

Now that the template is built for production, we can make a copy of our template by grabbing the contents of

index.html

  and

game.json

.

Please note: The JSON has not yet been updated. The

ignoreCoreFiles

is a filter function for our copy function. This

only available with

fs-extra

and not the native

fs

copy

method is

module provided by Node.

// 3. create copy of template & update config values

const templateRelease templateReleaseSource Source = join join(templatePath (templatePath, , 'public' 'public') ); const templateRelease templateReleaseDestination Destination = join join(releasePath (releasePath, , projectName)  projectName); ; const ignoreCoreFiles = src => !src src. .match match( (/core/ /core/) );

It’s It’s now time to copy the files. As mentioned before, the good thing about the fs-extra

methods, is that they all use promises rather than callbacks, so we can

chain our calls like so. fse.copy fse. copy(templateReleaseSource (templateReleaseSource, , templateReleas templateReleaseDestination eDestination, , { filter filter: : ignoreCoreFile ignoreCoreFiles s }) .then then(() (() => transform transform(ticketInformation)) (ticketInformation)) .then then((newValues) ((newValues) => { const configFile = join join(templateReleaseDestination (templateReleaseDestination, GAME_JSON)  GAME_JSON); ; return fse fse. .writeJsonSync writeJsonSync(configFile (configFile, , newValues  newValues, , { spaces spaces: : JSON_WHITESPAC JSON_WHITESPACE E }); }) .then then(() (() => { log( log (`built ${templateReleaseDestination ${templateReleaseDestination}` }`, , SUCCESS)  SUCCESS); ; log( log (`deploying ${branchName ${branchName}` }`, , INFO)  INFO); ; deployGame(branchName deployGame (branchName, , projectName  projectName, , ticketId)  ticketId); ; }) .catch catch(e (e => log log(e (e, , ERROR))  ERROR)); ; }; module. module .exports = create  create; ;

1. We copy the

index.html

  and

game.json

from the template repository. repository. Passing

the filter function to ignore the subdirectory called

core

.

2. We pass the ticket information retrieved from the API to our transform function shown earlier, which transforms the ticket information into our game.json

  format.

3. The new transformed transformed JSON then gets written synchronously to our project in the

releases

directory of our website.

4. Finally, Finally, we have our modified changes in the website, website, all that we need to to do is stage, commit and merge the changes to our base branch. 5. We sigh with relief knowing it’ it’s merged before the deadline.

End to end And that’s the code side of it. Let’s see how it works end to end for each command. We We are going to start from cloning the nobot repository.

Please note: I clone

https://github.com/smks/nobot.git

, but this URL would be for your

own forked version. $ git clone https://github https://github.com/smks/nobot .com/smks/nobot.git .git Cloning into 'nobot'... remote: Counting objects: 375, done. remote: Compressing objects: 100% (65/65), done. remote: Total 375 (delta 44), reused 69 (delta 30), pack-reused 276 Receiving objects: 100% (375/375), 388.19 KiB | 342.00 KiB/s, done. Resolving deltas: 100% (158/158), done.

I change into the directory of the project and install all of my external node modules. $ cd nobot nobot git:(master) npm install added 256 packages in 3.026s

I run the command to create my new nobot

config.json

and create a global global alias alias named

.

$ npm run init > [email protected] init /Users/shaun/Workspace/nobot /Users/shaun/Workspace/nobot > node ./init.js up to date in 0.779s /usr/local/bin/nobot /usr/local/bin/ nobot -> /usr/local/lib /usr/local/lib/node_modules/n /node_modules/nobot/src/nobot. obot/src/nobot.js js /usr/local/lib/node_modules/nob /usr/local/lib/ node_modules/nobot ot -> /Users/shaun/W /Users/shaun/Workspace/nobot orkspace/nobot [success] created configuration file

Now in my

config.json

I update the

authCode

for our API so that we can receive

JSON from our endpoint (otherwise it would return a 404). If you haven’t done this already, already, then now is the time to shine. ha ve omitted commas for segments of JSON J SON for readability. Y Your our Please note: I have

actual

config.json

file has been structured as you would expect further down.

"authKey": "SECRET"

Changes to: "authKey": "NOBOT_123"

My deployment repository will remain the same, but you should have forked it and used your own. So your URL will be different. You You can fork the game template

rock-paper-scissors

as well if you want want to add more features to the game.

 config.json { "api": { "api": "authKey": "authKey" : "SECRET" "SECRET", , "endpoint": "endpoint" : "http://opencan "http://opencanvas.co.uk/nira/ vas.co.uk/nira/rest/api/latest rest/api/latest/ticket" /ticket" }, "deploy": "deploy" : { "baseBranch": "baseBranch" : "master" "master", , "name": "name" : "website" "website", , "repo": "repo" : "https://githu "https://github.com/smks/nobo b.com/smks/nobot-website.git" t-website.git", , "coreDirectory": "coreDirectory" : "core" "core", , "releaseDirectory": "releaseDirectory" : "releases" }, "templates": "templates" : { "rock-paper-scissors": "rock-paper-scissors" : { "baseBranch": "baseBranch" : "master" "master", , "repo": "repo" : "https://githu "https://github.com/smks/nobo b.com/smks/nobot-template-rock t-template-rock-paper-scissors -paper-scissors" " } } }

I run the

setup

command to clone our repositories.

$ nobot setup Cloning into 'website'... remote: Counting objects: 122, done. remote: Compressing objects: 100% (91/91), done. remote: Total 122 (delta 57), reused 90 (delta 28), pack-reused 0 Receiving objects: 100% (122/122), 123.07 KiB | 275.00 KiB/s, done. Resolving deltas: 100% (57/57), done. [info] Downloading rock-paper-sci rock-paper-scissors ssors Cloning into 'rock-paper-sci 'rock-paper-scissors'... ssors'...

remote: Counting objects: 100, done. remote: Compressing objects: 100% (73/73), done. remote: Total 100 (delta 37), reused 82 (delta 23), pack-reused 0 Receiving objects: 100% (100/100), 75.70 KiB | 332.00 KiB/s, done. Resolving deltas: 100% (37/37), done.

These should now exist under your

repositories

  directory.

Great, now one of my colleagues has applied a fix to the template. The problem was that when saving the score to local storage, the result was being saved across all games. We want it on a game by game basis. This means I need to use the

template

command to release the latest latest version. Here it is in action.

$ nobot template [1] rock-paper-sci rock-paper-scissors ssors [0] CANCEL choose template to release [1/0]: 1 From https://github. https://github.com/smks/nobot-t com/smks/nobot-template-rock-pa emplate-rock-paper-scissors per-scissors * branch master -> FETCH_HEAD a84a476..0b0bb14 a84a476..0b0bb1 4 master -> origin/master Updating a84a476..0b0bb a84a476..0b0bb14 14 Fast-forward  app/actions/save-score.js | 14 ++++++++++---app/assets/index.html app/assets/ind ex.html | 4 ++-app/helpers/get-score.js app/helpers/ge t-score.js | 8 +++++--package.json package.js on | 2 + 4 files changed, 18 insertions(+), 10 deletions(-) > [email protected]. [email protected] 3 install /Users/shaun/ /Users/shaun/Workspace/nobot Workspace/nobot/repositories/t /repositories/templates/rock-p emplates/rock-paperaperscissors/node_modules/fsevents > node install [fsevents] Success: "/Users/shaun/Wo "/Users/shaun/Workspace/nobot/r rkspace/nobot/repositories/tem epositories/templates/rock-pap plates/rock-papererscissors/node_modules/fsevents/ scissors/node_m odules/fsevents/lib/binding/Rel lib/binding/Release/node-v59-d ease/node-v59-darwin-x64/fse.n arwin-x64/fse.node" ode" is installed via remote added 969 packages in 9.13s > rock-paper-sc [email protected] [email protected] build /Users/shaun/W /Users/shaun/Workspace/nobot/ orkspace/nobot/repositories/te repositories/templates/rock-pa mplates/rock-paper-scissors per-scissors > brunch build --production 11:29:21 - info: compiled 24 files into 2 files, copied 2 in 3.7 sec [info] changing to path /Users/shaun/Workspace/nobot/repositories/ /Users/shaun/Workspace/nobot/repositories/website website [info] staging template rock-paper-scissors-1.1.0 rock-paper-scissors-1.1.0 Switched to a new branch 'rock-paper-scissors-1.1.0' 'rock-paper-scissors-1.1.0' [rock-paper-scissors-1.1.0 [rock-paper-sci ssors-1.1.0 8706d73] rock-paper-scis rock-paper-scissors.1.1.0 sors.1.1.0  2 files changed, 2 insertions(+) create mode 100644 core/rock-paper-scissors.1.1.0.css core/rock-paper-scissors.1.1.0.css  create mode 100644 core/rock-paper-scissors.1.1.0.js core/rock-paper-scissors.1.1.0.js [info] switching to base branch master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. From https://github. https://github.com/smks/nobot-w com/smks/nobot-website ebsite * branch master -> FETCH_HEAD Already up-to-date. [info] merging rock-paper-scissors-1.1.0 into master

Updating e9c394b..8706d e9c394b..8706d73 73 Fast-forward  core/rock-paper-scissors.1.1.0.css  core/rock-paper-scissors. 1.1.0.css | 1 +  core/rock-paper-scisso  core/ro ck-paper-scissors.1.1.0.js rs.1.1.0.js | 1 +  2 files changed, 2 insertions(+) create mode 100644 core/rock-paper-scissors.1.1.0.css core/rock-paper-scissors.1.1.0.css  create mode 100644 core/rock-paper-scissors.1.1.0.js core/rock-paper-scissors.1.1.0.js To https://github. https://github.com/smks/nobotcom/smks/nobot-website.git website.git e9c394b..8706d73 e9c394b..8706d7 3 master -> master Deleted branch rock-paper-sciss rock-paper-scissors-1.1.0 ors-1.1.0 (was 8706d73). [success] released latest template version

Brilliant! We We have the latest version vers ion of our template. Now I can build five games by running the command for each ticket. I will only show one example of this being built and deployed, as it will spit out similar output. $ nobot game GS-100 Already on 'master' Your branch is up-to-date with 'origin/master'. From https://github. https://github.com/smks/nobot-w com/smks/nobot-website ebsite * branch master -> FETCH_HEAD Already up-to-date. Switched to a new branch 'GS-100_fire-water-earth-cute' 'GS-100_fire-water-earth-cute' up to date in 2.899s > rock-paper-sc [email protected] [email protected] build /Users/shaun/W /Users/shaun/Workspace/nobot/ orkspace/nobot/repositories/te repositories/templates/rock-pa mplates/rock-paper-scissors per-scissors > brunch build --production 11:33:55 - info: compiled 24 files into 2 files, copied 2 in 2.3 sec [success] built /Users/shaun/Wo /Users/shaun/Workspace/nobot/r rkspace/nobot/repositories/web epositories/website/releases/f site/releases/fire-water-earth ire-water-earth-cute -cute [info] changing to path /Users/shaun/W /Users/shaun/Workspace/nobot/ orkspace/nobot/repositories/we repositories/website/releases bsite/releases [info] staging project fire-water-earth-cute [GS-100_fire-water-earth-cute [GS-100_fire-wa ter-earth-cute d7a804d] GS-100 - fire-water-earth-cute release  2 files changed, 69 insertions(+) create mode 100644 releases/fire-water-earth-cute/game.js releases/fire-water-earth-cute/game.json on create mode 100644 releases/fire-water-earth-cute/index.h releases/fire-water-earth-cute/index.html tml [info] switching to base branch master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. From https://github. https://github.com/smks/nobot-w com/smks/nobot-website ebsite * branch master -> FETCH_HEAD Already up-to-date. [info] merging GS-100_fire-wate GS-100_fire-water-earth-cute r-earth-cute into master Updating 8706d73..d7a80 8706d73..d7a804d 4d Fast-forward  releases/fire-water-earth-cute/g  releases/fire-wat er-earth-cute/game.json ame.json | 39 ++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++ + releases/fire-water-earth-cute releases/firewater-earth-cute/index.html /index.html | 30 ++++++++++++++++++++++++  2 files changed, 69 insertions(+) create mode 100644 releases/fire-water-earth-cute/game.js releases/fire-water-earth-cute/game.json on create mode 100644 releases/fire-water-earth-cute/index.h releases/fire-water-earth-cute/index.html tml To https://github. https://github.com/smks/nobotcom/smks/nobot-website.git website.git 8706d73..d7a804d 8706d73..d7a804 d master -> master Deleted branch GS-100_fire-wate GS-100_fire-water-earth-cute r-earth-cute (was d7a804d).

Our game has been built with the typing of the command and ticket ID, then… the hit of an enter button. I have set up a Cron job on the website server-side to

pull in the latest changes every minute. Here is the live URL. http://ngs.opencanvas.co.uk/

Please note: On the website, the

index.php

script scans the releases directory and

outputs tiles for each game that exists. So every time we deploy a new game, the game tile will be added once the Cron job has pulled in the latest changes from the repository. I repeat running the build tool for the remaining four implementations. nobot ... nobot ... nobot ... nobot ...

game GS-101 game GS-102 game GS-103 game GS-104

Now we would have five games in the lobby on the website. They should pop up as tiles on the main lobby page as demonstrated in the following screenshot.

When you click on one, it should open a modal containing the game in an iframe. You You should then be able to play the game we built with our tool.

Just to repeat, you can see the website here: http://ngs.opencanvas.co.uk/

Wrap up Well… there you have it. An implementation that may prove to save you a lot of time in the long run. I hope you find it useful! It doesn’t have to stop there though. As you saw in some of the examples in part 1, you could add more features such as email or SMS. Here are a few that come to mind: 1. If a new template template has been released, email email your team with with the update. 2. Set up a frontend UI UI that allows you to build a game and provide feedback. Link it with the build tool. 3. Create your own templates with with different functionality. functionality. 4. Set up a frontend UI UI that takes in in CSV files, files, so you can batch batch create games. 5. Set up a hook on Jira (if you use it commercially) whenever a ticket is is created and allow the hook to call an endpoint on your y our server. That way it’s it’s fully automated, without any manual intervention. 6. Create a shortened link after creation and post a comment on the Jira ticket with details on how to preview it. It’s It’s good to note that this is one approach to deployment, but there are perhaps per haps many better ways this can be done, such as continuous integration with Jenkins or Travis. It’s something you can adopt in your workflow work flow,, but it’s outside the scope of this book. If you have any suggestions or improvements, feel free to let me know by contacting me here: http://smks.co.uk/contact Alternatively, Alternatively, you can follow me on the following social networks.

GitHub - https://github.com/smks Twitter - https://twitter.com/shaunmstone Facebook - https://www.facebook.com/automatingwithnodejs YouTube - http://www.youtube.com/c/OpenCanvas Or connect with me on LinkedIn for business-related requests. LinkedIn - https://www.linkedin.com/in/shaunmstone Thank you for reading. If you enjoyed it, please feel free to leave an online review. review. I hope you can find a way to automate your workflow. Don’t do it too much though… we all still need jobs! Good luck!