ExactTarget, Platform Team
Christopher McCulloh (@cmcculloh)
2013
Grunt automation for AppCenter.
Allow grunt to utilize Git for build management This is kind of like a miniature, built-in version of GitScripts
GitScripts = (g) ->
grunt = g;
{
A list of git commands available to grunt through the GitScripts object. You can use these in your GitScripts
methods like: grunt.config("shell.checkoutMaster", {command: "checkout master", stdout: true})
which would run the mapped command git checkout master
commands:
"checkout master": "git checkout master"
"checkout": "git checkout"
"merge": "git merge"
"hard reset": "git reset --hard"
"reset": "git reset"
"force delete": "git branch -D"
"delete": "git branch -d"
"create": "git checkout -b"
This is a hello world type function to test out gitscripts and also use as an example of how to write a gitscripts funciton.
Notice that instead of overwriting the grunt config shell object, we extend it with our command.
If you are going to need to run the same command multiple times, doing something different each time, you would push the variables to an array that the function then looped over executing the function as many times as necessary. This sort of thing can be seen in the "merge" example below...
The way this works is that there is a "shell" task registered with grunt. The shell task is basically just an object with methods on it. Grunt will iterate through the object running each of the functions in turn. So, each function name must be unique, and, each method must be a top-level method on the object (meaning you can't nest methods). If you are expecting to run the same method multiple times, you will need to add a random string to its name (or something)'.
hello: () ->
grunt.config("shell.echo_test", {command: "echo test", stdout: true})
This actually kicks off the shell task, running all of the commands that have been stored on the shell object
run: () ->
grunt.task.run("shell")
Empties the shell object so that if called again, nothing happens (there will be no methods to iterate over)
clear: () ->
grunt.config("shell", {})
checkoutMaster: () ->
grunt.config("shell.checkout_master",{command: this.commands["checkout master"], stdout: true})
checkout: (what) ->
grunt.config('shell.checkout', {command: this.commands['checkout'] + " " + what, stdout: true})
merge: (branchlist, flags) ->
if not flags
flags = ""
for i in branchlist
grunt.config("shell.merge" + i, {command: this.commands.merge + " " + flags + " origin/" + branchlist[i], stdout: true})
reset: () ->
grunt.config("shell.reset", {command: this.commands['hard reset'], stdout: true})
del: (branch, safe) ->
if safe is undefined
safe = true
if safe
grunt.config("shell.delete", {command:this.commands["delete"] + " " + branch, stdout: true})
else
grunt.config("shell.delete", {command:this.commands["force delete"] + " " + branch, stdout: true})
create: (branch, from) ->
if from is undefined
from = "master"
grunt.config("shell.create", {command:this.commands["create"] + " " + branch + " " + from, stdout: true})
deploy: () ->
grunt.config("shell.deploy", {command:"stackato update --no-prompt", stdout:true})
}
The actual Grunt configurations/tasks.
Define what you want returned in module.exports for require...
module.exports = (grunt) ->
Load the required npm tasks for grunt
grunt.loadNpmTasks('grunt-contrib')
grunt.loadNpmTasks('grunt-testem')
grunt.loadNpmTasks('grunt-shell')
The actual grunt project configuration options
grunt.initConfig({
pkg: grunt.file.readJSON('package.json')
watch:
files: ['grunt.js', 'public/javascripts/**/*.js']
tasks: 'lint testem'
jshint:
uses_defaults: ['public/javascripts/**/*.js']
options:
curly: false
eqeqeq: true
immed: true
latedef: true
newcap: true
noarg: true
sub: true
undef: true
boss: true
eqnull: true
browser: true
devel: true
laxcomma: true
globals:
define: true
require: true
$: true
_: true
io: true
google: true
tinymce: true
with_overrides:
options:
browser: false
node: true
globals:
__dirname: true
module: true
define: false
require: false
$: false
_: false
files:
src: ['Gruntfile.js', 'app.js', 'lib/*.js']
requirejs:
combine:
options:
appDir: 'public/'
baseUrl: 'vendor'
dir: 'public-optimized/'
optimize: 'uglify2'
optimizeCss: 'none'
mainConfigFile: 'public/javascripts/app/main.js'
generateSourceMaps: true
preserveLicenseComments: false
paths:
config: 'empty:'
modules: [
{ name: 'app/main' }
]
shell: {}
});
Helper to execute a file and continually display its output
grunt.exec_file = (file, args, options, done) ->
cmd = require('child_process').execFile(file, args, options);
cmd.stderr.on('data', (data) ->
grunt.log.write(data.toString())
)
cmd.stdout.on('data', (data) ->
grunt.log.write(data.toString())
)
cmd.on('exit', (error) ->
done(!error)
)
Helper to execute a command and continually display its output
grunt.exec_cmd = (cmd, args, options, done) ->
proc = require('child_process').exec(cmd, args, options)
proc.stderr.on('data', (data) ->
grunt.log.write(data.toString())
)
proc.stdout.on('data', (data) ->
grunt.log.write(data.toString())
)
proc.on('exit', (error) ->
done(!error)
)
Launch the project with an optional argument for Express environment
serverTask = () ->
done = this.async()
args = ''
useTrace = false
process.env.HOST = ''
process.env.NODE_ENV = ''
for i in this.args
Detect named environment flags and set the NODE_ENV var based on them (maps directly to config/dev.js or config/qa1s1.js or config/edge.js etc)
if this.args[i].match(/qa1|qa2|prod|dev|edge/)
process.env.NODE_ENV = this.args[i]
Since there are multiple qa environments, determine if the user paseed the "qa" flag to indicate they are in a "qa like" environment and use the global settings from the qa.js config file. Any config grabbed based on the NODE_ENV var set about will override this
if this.args[i].match(/^qa$/)
process.env.HOST = this.args[i]
Determine if the user wants to use tracegl (look for the trace
or tracegl
argument
if this.args[i] in ['tracegl', 'trace']
useTrace = true
args += "#{ this.args[i] } "
if useTrace
grunt.exec_cmd("node ~/tracegl #{ __dirname }/app.js -nolib -tgt:4000 #{ args }", this.args, {}, done)
else
pass -q to silence debug messages... utilize forever to restart node server when it crashes
grunt.exec_cmd('forever start ' + __dirname + '/app.js', this.args, {}, done);
#grunt.exec_file( __dirname + '/' + 'app.js', this.args, {}, done);
Register the task with grunt
grunt.registerTask('server', 'Runs the node.js app for this project', serverTask)
Stamp the project, run jshint to validate, then min/merge everything together
grunt.registerTask('default', ['stamp', 'jshint', 'requirejs'])
append abbreviated githash to the top of version.txt so you can always see exactly which build is being served
grunt.registerTask('stamp', 'Stamp', () ->
grunt.config("shell", {})
grunt.config('shell.stamp', {command: 'git log -1 --abbrev=7 --format=oneline | cat - version.txt > /tmp/out && mv /tmp/out version.txt'})
grunt.config('shell.time', {command: 'git log -1 --format="<br><b>%ci</b>:" | cat - version.txt > /tmp/out && mv /tmp/out version.txt'})
#grunt.config('shell.newline', {command: 'echo "<br>\n" | cat - version.txt > /tmp/out && mv /tmp/out version.txt', stdout: true});
grunt.task.run("shell")
)
grunt.registerTask('edge', 'Calls server with edge settings', () ->
grunt.task.run('server:https:qa:edge-local')
)
Debug task. Sets "devel" to true to not warn about console statements
grunt.registerTask('development', 'Development Task', () ->
grunt.config("jshint.options.devel", true)
grunt.config("requirejs.combine.options.optimize", "none")
grunt.task.run('default')
)
This will create a branch based on an arbitrary list of branches. This allows managing branches to be included in DEV, QA, Stage and Prod more easily.
Do not call this directly from command line. Instead, register a task that calls this after populating a "shellopts" grunt config:
grunt.registerTask('build-edge', 'Build edge', () -> grunt.config('shellopts', { branch: 'edge' #this indicates the branch that will be created once this task runs list: grunt.buildList['2013-06'].concat('edge-stackato-config') #specify the .net build you want to use for your buildList and then concat your stackato-config branch onto the end. You can specify multiple .net build by separating them with commas if you want... from: 'master' #starting point. You can specify any branch as a starting point. I almost always use master, but, this could be useful for a big project that wants to use another branch as its common ancestor } )
grunt.task.run('build-branch') )
grunt.registerTask('build-branch', 'Builds a branch from an arbitrary list', () ->
gitScripts = new GitScripts(grunt)
gitScripts.clear()
if grunt.config('shellopts.from') is undefined
gitScripts.checkoutMaster();
else
gitScripts.checkout(grunt.config('shellopts.from'))
gitScripts.del(grunt.config('shellopts.branch'), false)
gitScripts.create(grunt.config('shellopts.branch'), grunt.config('shellopts.from'))
gitScripts.merge(grunt.config('shellopts.list'), "--no-ff")
gitScripts.run()
grunt.task.run('default')
)
Ideas: make it so this list can be managed from the browser, even just the stackato-deploy part
grunt.buildList =
'2013-05': ['FTR---734---hide-nav-bar-on-page-load']
'2013-06': ['FTR---734---hide-nav-bar-on-page-load'
'BUG---793---edit-message-success-wrong']
'2013-07': ['FTR---734---hide-nav-bar-on-page-load'
'BUG---793---edit-message-success-wrong'
'FTR---812---support-reload-at-any-url']
'experimental': ['FTR---734---hide-nav-bar-on-page-load'
'BUG---793---edit-message-success-wrong'
'bootstrap-3'
'nginx-support']
Each build target is defined in this way. To create a new build target, just copy the below and edit accordingly.
You will need to make a stackato-config branch for any new build targets. This branch should NEVER be merged back into master because prod has its own stackato configs that would be overwritten by this. Which is why you can just run "build-qa2s2" and then merge the results of that into master (you need to use the build-new-master task instead)
grunt.registerTask('build-edge', 'Build edge', () ->
grunt.config('shellopts',
{
branch: 'edge' #this indicates the branch that will be created once this task runs
list: grunt.buildList['2013-06'].concat('edge-stackato-config') #specify the .net build you want to use for your buildList and then concat your stackato-config branch onto the end. You can specify multiple .net build by separating them with commas if you want...
from: 'master' #starting point. You can specify any branch as a starting point. I almost always use master, but, this could be useful for a big project that wants to use another branch as its common ancestor
}
)
grunt.task.run('build-branch')
)
When a build target is promoted to prod, you will want to merge it in with master. Instead of checking out master and doing this manually, just run 'build-new-master' and use that build target for the buildList. It will build the new version of master, which you can then just push up to the server.
NEVER EVER EVER RUN "grunt build edge" AND THEN MERGE THE RESULTS WITH MASTER, because it will result in your stackato-config branch being merged into master. This is bad because you want master to be build-target agnostic and be able to be deployed to DEV, QA or PROD. Merging a build into master will cause master to contain the stackato config settings for that build. BAD.
grunt.registerTask('build-new-master', 'Build New Master', () ->
grunt.config('shellopts',
{
branch: 'master'
list: grunt.buildList['2013-05']
from: 'master'
}
)
grunt.task.run('build-branch')
)
You can now put jshint options in package.json, which is nice as it exposes them to everything vs just grunt. http://www.jshint.com/blog/2013-08-02/npm/