diff --git a/.jshintrc b/.jshintrc index 6b4c1a9..f57a8ff 100644 --- a/.jshintrc +++ b/.jshintrc @@ -9,6 +9,5 @@ "undef": true, "boss": true, "eqnull": true, - "node": true, - "es5": true + "node": true } diff --git a/Gruntfile.js b/Gruntfile.js index e364a61..f8de3af 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,7 @@ * grunt-deployments * https://github.com/getdave/grunt-deployments * - * Copyright (c) 2013 David Smith + * Copyright (c) 2014 David Smith * Licensed under the MIT license. */ @@ -10,54 +10,96 @@ module.exports = function(grunt) { + // load all grunt tasks + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); // Project configuration. grunt.initConfig({ - db_fixture: grunt.file.readJSON('test/fixtures/test_db.json'), - + db_fixture: grunt.file.readJSON('test/fixtures/basic_config.json'), + vows: { + all: { + options: { + // String {spec|json|dot-matrix|xunit|tap} + // defaults to "dot-matrix" + reporter: "spec", + // String or RegExp which is + // matched against title to + // restrict which tests to run + // onlyRun: /helper/, + // Boolean, defaults to false + verbose: false, + // Boolean, defaults to false + silent: false, + // Colorize reporter output, + // boolean, defaults to true + colors: true, + // Run each test in its own + // vows process, defaults to + // false + isolate: false, + // String {plain|html|json|xml} + // defaults to none + coverage: "json" + }, + // String or array of strings + // determining which files to include. + // This option is grunt's "full" file format. + src: ["test/**/*_test.js"] + } + }, jshint: { - all: [ - 'Gruntfile.js', - 'tasks/*.js', - '<%= nodeunit.tests %>', - ], + gruntfile: { + src: 'Gruntfile.js' + }, + lib: { + src: ['tasks/*.js'] + }, + test: { + src: ['test/**/*.js'] + }, options: { jshintrc: '.jshintrc', }, }, + watch: { + gruntfile: { + files: '<%= jshint.gruntfile.src %>', + tasks: ['jshint:gruntfile'] + }, + lib: { + files: '<%= jshint.lib.src %>', + tasks: ['jshint:lib', 'vows'] + }, + test: { + files: '<%= jshint.test.src %>', + tasks: ['jshint:test', 'vows'] + } + }, + // Before generating any new files, remove any previously-created files. clean: { tests: ['tmp'], + backups: ['./backups'], }, deployments: { options: { backups_dir: '' }, - local: '<%= db_fixture.local %>', - develop: '<%= db_fixture.develop %>' - }, - - // Unit tests. - nodeunit: { - tests: ['test/*_test.js'], + local: '<%= db_fixture.local %>', // make sure you've created valid fixture DB creds + develop: '<%= db_fixture.develop %>' // make sure you've created valid fixture DB creds }, }); - // Actually load this plugin's task(s). +// Actually load this plugin's task(s). grunt.loadTasks('tasks'); - // These plugins provide necessary tasks. - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-nodeunit'); - // Whenever the "test" task is run, first clean the "tmp" dir, then run this // plugin's task(s), then test the result. - grunt.registerTask('test', ['clean', 'deployments', 'nodeunit']); + grunt.registerTask('test', ['clean', 'vows']); // By default, lint and run all tests. grunt.registerTask('default', ['jshint', 'test']); diff --git a/README.md b/README.md index ee15fc1..56920d1 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,12 @@ A string value that represents the default target for the tasks. You can easily ## Contributing -Contributions to this plugin are most welcome. This is very much a Alpha release and so if you find a problem please consider raising a pull request or creating a Issue which describes the problem you are having and proposes a solution. +Contributions to this plugin are most welcome. Pull requests are preferred but input on open Issues is also most agreeable! + +This is very much a Alpha release and so if you find a problem please consider raising a pull request or creating a Issue which describes the problem you are having and proposes a solution. + +### Branches and merge strategy +All pull requests should merged into the `develop` branch. __Please do not merge into the `master` branch__. In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). diff --git a/lib/dbDump.js b/lib/dbDump.js new file mode 100644 index 0000000..c0c54c2 --- /dev/null +++ b/lib/dbDump.js @@ -0,0 +1,81 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + + +/** + * Dumps a MYSQL database to a suitable backup location + */ +function dbDump(config, output_paths, noExec) { + + var cmd; + var ignoreTables; + var rtn; + + grunt.file.mkdir(output_paths.dir); + + // 1) Get array of tables to ignore, as defined in the config, and format it correctly + if( config.ignoreTables ) { + ignoreTables = '--ignore-table=' + config.database + "." + config.ignoreTables.join(' --ignore-table='+config.database+'.'); + } + + // 2) Compile MYSQL cmd via Lo-Dash template string + var tpl_mysqldump = grunt.template.process(tpls.mysqldump, { + data: { + user: config.user, + pass: config.pass, + database: config.database, + host: config.host, + port: config.port || 3306, + ignoreTables: ignoreTables || '' + } + }); + + // 3) Test whether MYSQL DB is local or whether requires remote access via SSH + + if (typeof config.ssh_host === "undefined") { // it's a local connection + grunt.log.writeln("Creating dump of local database"); + cmd = tpl_mysqldump; + + } else { // it's a remote connection + var tpl_ssh = grunt.template.process(tpls.ssh, { + data: { + host: config.ssh_host + } + }); + grunt.log.writeln("Creating dump of remote database"); + + cmd = tpl_ssh + " \\ " + tpl_mysqldump; + } + + if (!noExec) { + // Capture output... + var output = shell.exec(cmd, {silent: true}).output; + // TODO: Add test here to check whether we were able to connect + + // Write output to file using native Grunt methods + grunt.file.write( output_paths.file, output ); + } + + if ( grunt.file.exists(output_paths.file) ) { + + grunt.log.oklns("Database dump succesfully exported to: " + output_paths.file); + } else if (noExec) { + grunt.log.oklns("Running with 'noExec' option. Database dump would have otherwise been succesfully exported to: " + output_paths.file); + } else { + grunt.fail.warn("Unable to locate database dump .sql file at " + output_paths.file, 6); + } + + rtn = { + cmd: cmd, + output_file: output_paths.file + }; + + // Return for reference and test suite purposes + return rtn; + +} + +module.exports = dbDump; \ No newline at end of file diff --git a/lib/dbImport.js b/lib/dbImport.js new file mode 100644 index 0000000..82558a4 --- /dev/null +++ b/lib/dbImport.js @@ -0,0 +1,58 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + +/** + * Imports a .sql file into the DB provided + */ +function dbImport(config, src) { + + var cmd; + var rtn; + + // 1) Create cmd string from Lo-Dash template + var tpl_mysql = grunt.template.process(tpls.mysql, { + data: { + host: config.host, + user: config.user, + pass: config.pass, + database: config.database, + path: src, + port: config.port || 3306 + } + }); + + + // 2) Test whether target MYSQL DB is local or whether requires remote access via SSH + if (typeof config.ssh_host === "undefined") { // it's a local connection + grunt.log.writeln("Importing into local database"); + cmd = tpl_mysql + " < " + src; + } else { // it's a remote connection + var tpl_ssh = grunt.template.process(tpls.ssh, { + data: { + host: config.ssh_host + } + }); + + grunt.log.writeln("Importing DUMP into remote database"); + + cmd = tpl_ssh + " '" + tpl_mysql + "' < " + src; + } + + // Execute cmd + shell.exec(cmd); + + grunt.log.oklns("Database imported succesfully"); + + + rtn = { + cmd: cmd + }; + + // Return for reference and test suite purposes + return rtn; +} + +module.exports = dbImport; \ No newline at end of file diff --git a/lib/dbReplace.js b/lib/dbReplace.js new file mode 100644 index 0000000..41b2e58 --- /dev/null +++ b/lib/dbReplace.js @@ -0,0 +1,30 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + +function dbReplace(search,replace,output_file, noExec) { + + var cmd = grunt.template.process( tpls.search_replace, { + data: { + search: search, + replace: replace, + path: output_file + } + }); + + grunt.log.writeln("Replacing '" + search + "' with '" + replace + "' in the database."); + + // Execute cmd + if (!noExec) { + shell.exec(cmd); + } + + grunt.log.oklns("Database references succesfully updated."); + + // Return for reference and test suite purposes + return cmd; +} + +module.exports = dbReplace; \ No newline at end of file diff --git a/lib/generateBackupPaths.js b/lib/generateBackupPaths.js new file mode 100644 index 0000000..8a95e34 --- /dev/null +++ b/lib/generateBackupPaths.js @@ -0,0 +1,30 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + + +function generateBackupPaths(target, task_options) { + + var rtn = []; + + var backups_dir = task_options['backups_dir'] || "backups"; + + // Create suitable backup directory paths + rtn['dir'] = grunt.template.process(tpls.backup_path, { + data: { + backups_dir: backups_dir, + env: target, + date: grunt.template.today('yyyymmdd'), + time: grunt.template.today('HH-MM-ss'), + } + }); + + + rtn['file'] = rtn['dir'] + '/db_backup.sql'; + + return rtn; +} + +module.exports = generateBackupPaths; \ No newline at end of file diff --git a/lib/tpls.js b/lib/tpls.js new file mode 100644 index 0000000..3466e90 --- /dev/null +++ b/lib/tpls.js @@ -0,0 +1,20 @@ +/** + * Lo-Dash Template Helpers + * http://lodash.com/docs/#template + * https://github.com/gruntjs/grunt/wiki/grunt.template + */ +var tpls = { + + search_replace: "sed -i '' 's#<%= search %>#<%= replace %>#g' <%= path %>", + + backup_path: "<%= backups_dir %>/<%= env %>/<%= date %>/<%= time %>", + + mysql: "mysql -h <%= host %> -u <%= user %> -p<%= pass %> -P<%= port %> <%= database %>", + + ssh: "ssh <%= host %>", + + mysqldump: "mysqldump -h <%= host %> -u<%= user %> -p<%= pass %> -P<%= port %> <%= database %> <%= ignoreTables %>" +}; + + +module.exports = tpls; \ No newline at end of file diff --git a/package.json b/package.json index ffa1885..b434083 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,16 @@ "test": "grunt test" }, "dependencies": { - "shelljs": "~0.1.4" + "shelljs": "~0.2.6" }, "devDependencies": { - "grunt-contrib-jshint": "~0.1.1", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-nodeunit": "~0.1.2", - "grunt": "~0.4.1" + "grunt": "~0.4.1", + "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-clean": "~0.5.0", + "matchdep": "~0.1.2", + "grunt-vows": "~0.4.0", + "vows": "~0.7.0", + "grunt-contrib-watch": "~0.5.0" }, "peerDependencies": { "grunt": "~0.4.1" @@ -48,4 +51,4 @@ "mysql", "wordpress" ] -} \ No newline at end of file +} diff --git a/tasks/deployments.js b/tasks/deployments.js index f487b90..7c77d13 100644 --- a/tasks/deployments.js +++ b/tasks/deployments.js @@ -8,12 +8,19 @@ 'use strict'; +// Global var shell = require('shelljs'); -module.exports = function(grunt) { - +// Library modules +var tpls = require('../lib/tpls'); +var dbReplace = require('../lib/dbReplace'); +var dbDump = require('../lib/dbDump'); +var dbImport = require('../lib/dbImport'); +var generateBackupPaths = require('../lib/generateBackupPaths'); +// Only Grunt registration within this "exports" +module.exports = function(grunt) { /** * DB PUSH @@ -36,24 +43,24 @@ module.exports = function(grunt) { var local_options = grunt.config.get('deployments').local; // Generate required backup directories and paths - var local_backup_paths = generate_backup_paths("local", task_options); - var target_backup_paths = generate_backup_paths(target, task_options); + var local_backup_paths = generateBackupPaths("local", task_options); + var target_backup_paths = generateBackupPaths(target, task_options); grunt.log.subhead("Pushing database from 'Local' to '" + target_options.title + "'"); // Dump local DB - db_dump(local_options, local_backup_paths); + dbDump(local_options, local_backup_paths); // Search and Replace database refs - db_replace( local_options.url, target_options.url, local_backup_paths.file ); + dbReplace( local_options.url, target_options.url, local_backup_paths.file ); // Dump target DB - db_dump(target_options, target_backup_paths); + dbDump(target_options, target_backup_paths); // Import dump to target DB - db_import(target_options, local_backup_paths.file); + dbImport(target_options, local_backup_paths.file); grunt.log.subhead("Operations completed"); }); @@ -82,191 +89,26 @@ module.exports = function(grunt) { var local_options = grunt.config.get('deployments').local; // Generate required backup directories and paths - var local_backup_paths = generate_backup_paths("local", task_options); - var target_backup_paths = generate_backup_paths(target, task_options); + var local_backup_paths = generateBackupPaths("local", task_options); + var target_backup_paths = generateBackupPaths(target, task_options); // Start execution grunt.log.subhead("Pulling database from '" + target_options.title + "' into Local"); // Dump Target DB - db_dump(target_options, target_backup_paths ); + dbDump(target_options, target_backup_paths ); - db_replace(target_options.url,local_options.url,target_backup_paths.file); + dbReplace(target_options.url,local_options.url,target_backup_paths.file); // Backup Local DB - db_dump(local_options, local_backup_paths); + dbDump(local_options, local_backup_paths); // Import dump into Local - db_import(local_options,target_backup_paths.file); + dbImport(local_options,target_backup_paths.file); grunt.log.subhead("Operations completed"); }); - - - - function generate_backup_paths(target, task_options) { - - var rtn = []; - - var backups_dir = task_options['backups_dir'] || "backups"; - - // Create suitable backup directory paths - rtn['dir'] = grunt.template.process(tpls.backup_path, { - data: { - backups_dir: backups_dir, - env: target, - date: grunt.template.today('yyyymmdd'), - time: grunt.template.today('HH-MM-ss'), - } - }); - - - rtn['file'] = rtn['dir'] + '/db_backup.sql'; - - return rtn; - } - - - /** - * Imports a .sql file into the DB provided - */ - function db_import(config, src) { - - var cmd; - - // 1) Create cmd string from Lo-Dash template - var tpl_mysql = grunt.template.process(tpls.mysql, { - data: { - host: config.host, - user: config.user, - pass: config.pass, - database: config.database, - path: src, - port: config.port || 3306 - } - }); - - - // 2) Test whether target MYSQL DB is local or whether requires remote access via SSH - if (typeof config.ssh_host === "undefined") { // it's a local connection - grunt.log.writeln("Importing into local database"); - cmd = tpl_mysql + " < " + src; - } else { // it's a remote connection - var tpl_ssh = grunt.template.process(tpls.ssh, { - data: { - host: config.ssh_host - } - }); - - grunt.log.writeln("Importing DUMP into remote database"); - - cmd = tpl_ssh + " '" + tpl_mysql + "' < " + src; - } - - // Execute cmd - shell.exec(cmd); - - grunt.log.oklns("Database imported succesfully"); - } - - - - /** - * Dumps a MYSQL database to a suitable backup location - */ - function db_dump(config, output_paths) { - - var cmd; - - grunt.file.mkdir(output_paths.dir); - - // 1) Get array of tables to ignore, as defined in the config, and format it correctly - if( config.ignoreTables ) { - var ignoreTables = '--ignore-table=' + config.database + "." + config.ignoreTables.join(' --ignore-table='+config.database+'.'); - } - - // 2) Compile MYSQL cmd via Lo-Dash template string - var tpl_mysqldump = grunt.template.process(tpls.mysqldump, { - data: { - user: config.user, - pass: config.pass, - database: config.database, - host: config.host, - port: config.port || 3306, - ignoreTables: ignoreTables || '' - } - }); - - // 3) Test whether MYSQL DB is local or whether requires remote access via SSH - - if (typeof config.ssh_host === "undefined") { // it's a local connection - grunt.log.writeln("Creating DUMP of local database"); - cmd = tpl_mysqldump; - - } else { // it's a remote connection - var tpl_ssh = grunt.template.process(tpls.ssh, { - data: { - host: config.ssh_host - } - }); - grunt.log.writeln("Creating DUMP of remote database"); - - cmd = tpl_ssh + " \\ " + tpl_mysqldump; - } - - // Capture output... - var output = shell.exec(cmd, {silent: true}).output; - - // Write output to file using native Grunt methods - grunt.file.write( output_paths.file, output ); - - grunt.log.oklns("Database DUMP succesfully exported to: " + output_paths.file); - - } - - - function db_replace(search,replace,output_file) { - - var cmd = grunt.template.process(tpls.search_replace, { - data: { - search: search, - replace: replace, - path: output_file - } - }); - - grunt.log.writeln("Replacing '" + search + "' with '" + replace + "' in the database."); - // Execute cmd - shell.exec(cmd); - grunt.log.oklns("Database references succesfully updated."); - } - - - - - /** - * Lo-Dash Template Helpers - * http://lodash.com/docs/#template - * https://github.com/gruntjs/grunt/wiki/grunt.template - */ - var tpls = { - - backup_path: "<%= backups_dir %>/<%= env %>/<%= date %>/<%= time %>", - - search_replace: "sed -i '' 's#<%= search %>#<%= replace %>#g' <%= path %>", - - - mysqldump: "mysqldump -h <%= host %> -u<%= user %> -p<%= pass %> -P<%= port %> <%= database %> <%= ignoreTables %>", - - - mysql: "mysql -h <%= host %> -u <%= user %> -p<%= pass %> -P<%= port %> <%= database %>", - - ssh: "ssh <%= host %>", - }; - - - }; diff --git a/test/deployments_test.js b/test/deployments_test.js index 0632799..e69de29 100644 --- a/test/deployments_test.js +++ b/test/deployments_test.js @@ -1,48 +0,0 @@ -'use strict'; - -var grunt = require('grunt'); - -/* - ======== A Handy Little Nodeunit Reference ======== - https://github.com/caolan/nodeunit - - Test methods: - test.expect(numAssertions) - test.done() - Test assertions: - test.ok(value, [message]) - test.equal(actual, expected, [message]) - test.notEqual(actual, expected, [message]) - test.deepEqual(actual, expected, [message]) - test.notDeepEqual(actual, expected, [message]) - test.strictEqual(actual, expected, [message]) - test.notStrictEqual(actual, expected, [message]) - test.throws(block, [error], [message]) - test.doesNotThrow(block, [error], [message]) - test.ifError(value) -*/ - -exports.deployments = { - setUp: function(done) { - // setup here if necessary - done(); - }, - default_options: function(test) { - test.expect(1); - - var actual = grunt.file.read('tmp/default_options'); - var expected = grunt.file.read('test/expected/default_options'); - test.equal(actual, expected, 'should describe what the default behavior is.'); - - test.done(); - }, - custom_options: function(test) { - test.expect(1); - - var actual = grunt.file.read('tmp/custom_options'); - var expected = grunt.file.read('test/expected/custom_options'); - test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); - - test.done(); - }, -}; diff --git a/test/fixtures/basic_config.json b/test/fixtures/basic_config.json new file mode 100644 index 0000000..dab56ef --- /dev/null +++ b/test/fixtures/basic_config.json @@ -0,0 +1,19 @@ +{ + "local": { + "title": "Local", + "database": "deploy_test", + "user": "root", + "pass": "pass4burfield", + "host": "localhost", + "url": "www.deploytest.dev:8888" + }, + "develop": { + "title": "Development Server", + "database": "ddeploy_dev", + "user": "ddeploy_dev", + "pass": "test4test", + "host": "localhost", + "url": "deploytest.com.burfield-dev.com", + "ssh_host": "ddeploy@134.0.18.114" + } +} diff --git a/test/options_test.js b/test/options_test.js new file mode 100644 index 0000000..6eb0d56 --- /dev/null +++ b/test/options_test.js @@ -0,0 +1,20 @@ +/* 'use strict'; + +var vows = require("vows"), + assert = require("assert"), + mysqldumpwrapper = require('../lib/mysqldumpwrapper.js'); + + +exports.suite = vows.describe("Package options tests").addBatch({ + "The User option": { + topic: function() { + mysqldumpwrapper({ + + }); + }, + "errors if undefined or empty": function (topic) { + assert.throws(topic, Error); + } + } +}); + */ \ No newline at end of file diff --git a/test/package_test.js b/test/package_test.js new file mode 100644 index 0000000..0efc6bb --- /dev/null +++ b/test/package_test.js @@ -0,0 +1,56 @@ +'use strict'; + +var grunt = require('grunt'), + fs = require("fs"), + vows = require("vows"), + assert = require("assert"), + dbReplace = require('../lib/dbReplace'), + dbDump = require('../lib/dbDump'), + generateBackupPaths = require('../lib/generateBackupPaths'), + basic_config = grunt.file.readJSON('test/fixtures/basic_config.json'); + + + +exports.suite = vows.describe("Basic tests").addBatch({ + "The dbReplace task": { + topic: dbReplace("foo","bar","test-file.txt", true), + "is not null": function (topic) { + assert.isNotNull(topic); + }, + "command is composed correctly": function (topic) { + assert.equal(topic, "sed -i '' 's#foo#bar#g' test-file.txt"); + }, + // add test to check against a fixture MYSQL export file that a search and replace works as expected + } +}).addBatch({ + "The dbDump task": { + topic: dbDump( + basic_config.local, + generateBackupPaths("local",{}) + ), + "is not null": function (topic) { + assert.isNotNull(topic); + }, + "command is composed correctly using basic data": function (topic) { + assert.equal(topic.cmd.trim(), "mysqldump -h localhost -uroot -ppass4burfield -P3306 deploy_test"); + }, + "results in a .sql file that": { + topic: function (topic) { + fs.stat(topic.output_file, this.callback); + }, + "has contents": function (err, stat) { + assert.isNull(err); + assert.isNotZero(stat.size); + } + } + } +}); + + + + + + + + +