grunt-build-control
Advanced tools
Comparing version 0.1.8 to 0.2.0
@@ -30,6 +30,16 @@ /* | ||
tests: [ | ||
'test/mock-repo' | ||
'test/mock' | ||
] | ||
}, | ||
watch: { | ||
tests: { | ||
files: ['tasks/**/*', 'test/**/*', '!**/test/mock/**'], | ||
tasks: 'test', | ||
options: { | ||
atBegin: true | ||
} | ||
} | ||
}, | ||
// Configuration to be run (and then tested). | ||
@@ -59,2 +69,4 @@ buildcontrol: { | ||
// Actually load this plugin's task(s). | ||
@@ -66,2 +78,3 @@ grunt.loadTasks('tasks'); | ||
grunt.loadNpmTasks('grunt-contrib-clean'); | ||
grunt.loadNpmTasks('grunt-contrib-watch'); | ||
grunt.loadNpmTasks('grunt-contrib-nodeunit'); | ||
@@ -68,0 +81,0 @@ grunt.loadNpmTasks('grunt-mocha-test'); |
{ | ||
"name": "grunt-build-control", | ||
"description": "Automate version control tasks for your project's built code. Keep built code in sync with source code, maintain multiple branches of built code, commit with automatic messages, and push to remote repositories.", | ||
"version": "0.1.8", | ||
"version": "0.2.0", | ||
"homepage": "https://github.com/robwierzbowski/grunt-build-control", | ||
@@ -39,3 +39,5 @@ "author": "Rob Wierzbowski <hello@robwierzbowski.com> (http://robwierzbowski)", | ||
"grunt-contrib-nodeunit": "~0.1.2", | ||
"grunt-mocha-test": "^0.12.1" | ||
"grunt-contrib-watch": "^0.6.1", | ||
"grunt-mocha-test": "^0.12.1", | ||
"lodash": "^2.4.1" | ||
}, | ||
@@ -42,0 +44,0 @@ "peerDependencies": { |
@@ -69,2 +69,10 @@ # grunt-build-control | ||
#### remoteBranch | ||
Type: `String` | ||
Default: `''` | ||
The remote branch to push to. Common usage would be for Heroku's `master` branch | ||
requirement. | ||
#### login | ||
@@ -71,0 +79,0 @@ Type: `String` |
@@ -28,2 +28,3 @@ /* | ||
remote: '../', | ||
remoteBranch: '', | ||
login: '', | ||
@@ -145,2 +146,11 @@ token: '', | ||
function verifyRepoBranchIsTracked() { | ||
// attempt to track a branch from origin | ||
// it may fail on times that the branch is already tracking another | ||
// remote. There is no problem when that happens, nor does it have any affect | ||
shelljs.exec('git branch --track ' + options.branch + ' origin/' + options.branch, {silent: true}); | ||
} | ||
// Initialize git repo if one doesn't exist | ||
@@ -259,4 +269,7 @@ function initGit () { | ||
function gitPush () { | ||
var branch = options.branch; | ||
if (options.remoteBranch) branch += ':' + options.remoteBranch; | ||
log.subhead('Pushing ' + options.branch + ' to ' + options.remote); | ||
execWrap('git push ' + remoteName + ' ' + options.branch, false, true); | ||
execWrap('git push ' + remoteName + ' ' + branch, false, true); | ||
@@ -274,2 +287,3 @@ if (options.tag) { | ||
assignTokens(); | ||
if (options.remote === '../') verifyRepoBranchIsTracked(); | ||
@@ -285,3 +299,3 @@ // Change working directory | ||
// Regex to test for remote url | ||
var remoteUrlRegex = new RegExp('.+[\\/:].+'); | ||
var remoteUrlRegex = new RegExp('[\/\\:]'); | ||
if(remoteUrlRegex.test(remoteName)) { | ||
@@ -293,3 +307,3 @@ initRemote(); | ||
localBranchExists = shelljs.exec('git show-ref --verify --quiet refs/heads/' + options.branch, {silent: true}).code === 0; | ||
remoteBranchExists = shelljs.exec('git ls-remote --exit-code ' + remoteName + ' ' + options.branch, {silent: true}).code === 0; | ||
remoteBranchExists = shelljs.exec('git ls-remote --exit-code ' + remoteName + ' ' + options.remoteBranch || options.branch, {silent: true}).code === 0; | ||
@@ -311,3 +325,3 @@ if (remoteBranchExists) { | ||
// Create local branch that tracks remote | ||
execWrap('git branch --track ' + options.branch + ' ' + remoteName + '/' + options.branch); | ||
execWrap('git branch --track ' + options.branch + ' ' + remoteName + '/' + (options.remoteBranch || options.branch)); | ||
} | ||
@@ -314,0 +328,0 @@ else if (!remoteBranchExists && !localBranchExists) { |
@@ -6,2 +6,6 @@ # Tests | ||
``` | ||
or | ||
```bash | ||
grunt watch:tests | ||
``` | ||
@@ -14,5 +18,6 @@ | ||
tests.js - contains tests to be executed | ||
mock-repo/ - [auto gen] testing area for any given scenario | ||
mock/ - [auto gen] testing area for any given scenario | ||
repo/ - repository to do tests on | ||
remote/ - [auto gen] "remote", imagine it as a github repo | ||
verify/ - [auto gen] `git clone remote verify` produces this folder | ||
validate/ - [auto gen] `git clone remote validate` produces this folder | ||
scenarios/ - different scenarios to be executed | ||
@@ -24,11 +29,6 @@ exampleA/ | ||
#### Notes | ||
All tests are executed with the relative path being: `test/mock-repo/` | ||
All tests are executed with the relative path being: `test/mock/` | ||
A quick little helper to watch and rerun tests (requires `npm install nodemon -g`) | ||
```bash | ||
nodemon -w test -w tasks/ -i test/mock-repo --exec 'grunt test' | ||
``` | ||
# Usage Example/Workflow | ||
@@ -47,5 +47,5 @@ Still confused? | ||
The test case can be found in "/test/tests.js", high level is: | ||
- it purges `mock-repo/` | ||
- it copies `scenarios/basic deployment/**` to `mock-repo/` | ||
- it changes working directory to `mock-repo/` | ||
- it purges `mock/` | ||
- it copies `scenarios/basic deployment/**` to `mock/` | ||
- it changes working directory to `mock/` | ||
- it executes the test case named `basic deployment` | ||
@@ -57,3 +57,3 @@ | ||
- which executes `grunt default` | ||
- which executes `git clone remote verify` | ||
- which executes `git clone remote validate` | ||
- it does validations | ||
@@ -60,0 +60,0 @@ ``` |
@@ -10,2 +10,3 @@ /*jshint -W030 */ | ||
var should = require('chai').should(); | ||
var _ = require('lodash'); | ||
@@ -20,6 +21,9 @@ | ||
* | ||
* - A scenario has a `gruntfile.js` configuration. | ||
* - Each build task will upload to a mock repo (folder name is `remote`) | ||
* - It then clones the remote to `verify`. Validations can be done in the `verify` folder | ||
* A Scenario can contain: | ||
* repo - the folder to contain the repository | ||
* repo/gruntfile.js - the gruntfile to be tested | ||
* remote - (optional) can contain a setup cloud repository | ||
* validate - (will be overwritten) it is cloned from remote (used to validate a push) | ||
* | ||
** | ||
* NOTE: this function DOES change the process's working directory to the `scenario` so that | ||
@@ -29,6 +33,7 @@ * validations are easier access. | ||
var execScenario = function(cb) { | ||
var mockRepoDir = path.normalize(__dirname + '/mock-repo'); | ||
var mockRepoDir = path.normalize(__dirname + '/mock'); | ||
var distDir = path.join(mockRepoDir, 'repo'); | ||
var remoteDir = path.join(mockRepoDir, 'remote'); | ||
var verifyDir = path.join(mockRepoDir, 'verify'); | ||
var verifyDir = path.join(mockRepoDir, 'validate'); | ||
@@ -52,3 +57,3 @@ | ||
childProcess.exec(GRUNT_EXEC, {cwd: mockRepoDir}, function(err, stdout, stderr) { | ||
childProcess.exec(GRUNT_EXEC, {cwd: distDir}, function(err, stdout, stderr) { | ||
next(err, {stdout: stdout, stderr: stderr}); | ||
@@ -61,3 +66,3 @@ }); | ||
fs.removeSync(verifyDir); // since we're cloning from `remote/` we'll just remove the folder if it exists | ||
childProcess.exec('git clone remote verify', {cwd: mockRepoDir}, function(err) { | ||
childProcess.exec('git clone remote validate', {cwd: mockRepoDir}, function(err) { | ||
if (err) throw new Error(err); | ||
@@ -71,3 +76,3 @@ next(err); | ||
// return results from executeGruntCommand | ||
cb(err, results[1]); | ||
cb(err, results[1].stdout, results[1].stderr); | ||
}); | ||
@@ -86,22 +91,33 @@ }; | ||
* Assumptions: | ||
* - each tests' current working directory has been set to `test/mock-repo` | ||
* - each tests' current working directory has been set to `test/mock` | ||
*/ | ||
describe('buildcontrol', function() { | ||
this.timeout(10000); | ||
this.timeout(6000); | ||
beforeEach(function(done) { | ||
var scenarioPath = this.currentTest.parent.title; | ||
// ensure that we reset to `test/` dir | ||
process.chdir(__dirname); | ||
// clean testing folder `test/mock-repo` | ||
fs.removeSync('mock-repo'); | ||
fs.ensureDirSync('mock-repo'); | ||
// clean testing folder `test/mock` | ||
fs.removeSync('mock'); | ||
fs.ensureDirSync('mock'); | ||
// copy scenario to `test/mock-repo` | ||
fs.copySync('scenarios/' + this.currentTest.parent.title, 'mock-repo'); | ||
try { | ||
// copy scenario to `test/mock` | ||
fs.copySync('scenarios/' + scenarioPath, 'mock'); | ||
// ensure all tests are are using the working directory: `test/mock-repo` | ||
process.chdir('mock-repo'); | ||
done(); | ||
// ensure all tests are are using the working directory: `test/mock` | ||
process.chdir('mock'); | ||
done(); | ||
} | ||
catch (err) { | ||
if (err && err.code === 'ENOENT') | ||
throw new Error('could not find scenario "' + scenarioPath + '" in test/scenarios/'); | ||
throw new Error(err); | ||
} | ||
}); | ||
@@ -113,3 +129,3 @@ | ||
it('should have pushed a file and had the correct commit in "verify" repo', function(done) { | ||
// the working directory is `test/mock-repo`. | ||
// the working directory is `test/mock`. | ||
var tasks = []; | ||
@@ -120,3 +136,3 @@ | ||
*/ | ||
// make `mock-repo` a actual repository | ||
// make `mock` a actual repository | ||
tasks.push(function git_init(next) { | ||
@@ -148,3 +164,3 @@ childProcess.exec('git init', next); | ||
tasks.push(function verify_file_exists(next) { | ||
fs.existsSync('verify/empty_file').should.be.true; | ||
fs.existsSync('validate/empty_file').should.be.true; | ||
next(); | ||
@@ -157,3 +173,3 @@ }); | ||
childProcess.exec('git log --pretty=oneline --no-color', {cwd: 'verify'}, function(err, stdout) { | ||
childProcess.exec('git log --pretty=oneline --no-color', {cwd: 'validate'}, function(err, stdout) { | ||
stdout.should.have.string('from commit ' + sha); | ||
@@ -168,5 +184,2 @@ next(); | ||
}); | ||
@@ -177,5 +190,5 @@ | ||
it('merge multiple repos', function(done) { | ||
execScenario(function(err, results) { | ||
execScenario(function(err, stdout, stderr) { | ||
should.not.exist(err); | ||
var numberFile = fs.readFileSync('verify/numbers.txt', {encoding: 'utf8'}); | ||
var numberFile = fs.readFileSync('validate/numbers.txt', {encoding: 'utf8'}); | ||
numberFile.should.be.eql('0 1 2\n'); | ||
@@ -195,3 +208,3 @@ done(); | ||
execScenario(function() { | ||
var numberFile = fs.readFileSync('verify/numbers.txt', {encoding: 'utf8'}); | ||
var numberFile = fs.readFileSync('validate/numbers.txt', {encoding: 'utf8'}); | ||
numberFile.should.be.eql('1 2 3 4\n'); | ||
@@ -203,6 +216,6 @@ next(); | ||
tasks.push(function(next) { | ||
fs.writeFileSync('dist/numbers.txt', '100 200'); | ||
fs.writeFileSync('repo/dist/numbers.txt', '100 200'); | ||
execScenario(function(err, results) { | ||
var numberFile = fs.readFileSync('verify/numbers.txt', {encoding: 'utf8'}); | ||
var numberFile = fs.readFileSync('validate/numbers.txt', {encoding: 'utf8'}); | ||
numberFile.should.be.eql('100 200'); | ||
@@ -214,3 +227,3 @@ next(); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'verify'}, function(err, stdout) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'validate'}, function(err, stdout) { | ||
stdout.should.have.string('simple deploy commit message'); | ||
@@ -226,5 +239,5 @@ next(); | ||
it('should not have <TOKEN> in the message', function(done) { | ||
execScenario(function(err, result) { | ||
execScenario(function(err, stdout) { | ||
should.not.exist(err); | ||
result.stdout.should.not.have.string('<TOKEN>'); | ||
stdout.should.not.have.string('<TOKEN>'); | ||
done(); | ||
@@ -242,7 +255,7 @@ }); | ||
tasks.push(function(next) { | ||
execScenario(function(err, results) { | ||
results.stdout.should.not.have.string('privateUsername'); | ||
results.stdout.should.not.have.string('1234567890abcdef'); | ||
results.stdout.should.have.string('github.com/pubUsername/temp.git'); | ||
results.stdout.should.have.string('<CREDENTIALS>'); | ||
execScenario(function(err, stdout) { | ||
stdout.should.not.have.string('privateUsername'); | ||
stdout.should.not.have.string('1234567890abcdef'); | ||
stdout.should.have.string('github.com/pubUsername/temp.git'); | ||
stdout.should.have.string('<CREDENTIALS>'); | ||
next(); | ||
@@ -266,3 +279,3 @@ }); | ||
tasks.push(function(next) { | ||
childProcess.exec('git remote -v', {cwd: 'dist'}, function(err, stdout) { | ||
childProcess.exec('git remote -v', {cwd: 'repo/dist'}, function(err, stdout) { | ||
stdout.should.have.string('https://privateUsername:1234567890abcdef@github.com/pubUsername/temp.git'); | ||
@@ -278,2 +291,251 @@ next(); | ||
describe('untracked branch in src repo', function() { | ||
it('should track a branch in ../ if it was untracked', function(done) { | ||
var tasks = []; | ||
tasks.push(function(next) { | ||
fs.removeSync('repo'); | ||
childProcess.exec('git clone remote repo', next); | ||
}); | ||
tasks.push(function(next) { | ||
fs.ensureDirSync('repo/build'); | ||
fs.writeFileSync('repo/build/hello.txt', 'hello world!'); | ||
next(); | ||
}); | ||
//tasks.push(function(next) { childProcess.exec('git branch --track build origin/build', {cwd: 'repo'}, next); }); | ||
tasks.push(function(next) { | ||
execScenario(function(err, stdout) { | ||
next(err); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git checkout build', {cwd: 'repo'}, function(err, stdout) { | ||
next(); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log', {cwd: 'repo'}, function(err, stdout) { | ||
stdout.should.have.string('a build commit'); | ||
next(); | ||
}); | ||
}); | ||
async.series(tasks, done); | ||
}); | ||
it('should not set tracking info it branch already exists', function(done) { | ||
var tasks = []; | ||
tasks.push(function(next) { | ||
fs.removeSync('repo'); | ||
childProcess.exec('git clone remote repo', next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git branch build', {cwd: 'repo'}, next); | ||
}); | ||
tasks.push(function(next) { | ||
fs.ensureDirSync('repo/build'); | ||
fs.writeFileSync('repo/build/hello.txt', 'hello world!'); | ||
next(); | ||
}); | ||
tasks.push(function(next) { | ||
execScenario(function(err, stdout) { | ||
next(err); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git branch -lvv', {cwd: 'repo'}, function(err, stdout) { | ||
stdout.should.not.have.string('origin/build'); | ||
next(); | ||
}); | ||
}); | ||
async.series(tasks, done); | ||
}); | ||
}); | ||
describe('remote urls', function() { | ||
function generateRemote(url, cb) { | ||
var tasks = []; | ||
// read template | ||
var gruntfile = fs.readFileSync('repo/gruntfile.js', {encoding: 'UTF8'}); | ||
// generate template | ||
gruntfile = _.template(gruntfile, {remoteURL: url}); | ||
// write generated gruntfile | ||
fs.writeFileSync('repo/gruntfile.js', gruntfile); | ||
// execute grunt command | ||
tasks.push(function(next) { | ||
//options | ||
GRUNT_EXEC += ' --no-color'; | ||
childProcess.exec(GRUNT_EXEC, {cwd: 'repo'}, function(err, stdout, stderr) { | ||
// mask error because remote paths may not exist | ||
next(null, {stdout: stdout, stderr: stderr}); | ||
}); | ||
}); | ||
// get remote url | ||
tasks.push(function(next) { | ||
childProcess.exec('git remote -v', {cwd: 'repo/dist'}, function(err, stdout) { | ||
next(err, stdout); | ||
}); | ||
}); | ||
// callback | ||
async.series(tasks, function(err, results) { | ||
cb(err, results[1]); | ||
}); | ||
} | ||
var shouldMatch = [ | ||
'/path/to/repo.git/', | ||
'path/to/repo.git/', | ||
'/path/to/repo', | ||
//'\\\\path\\\\to\\\\repo', // assuming works, there's a lot of escaping to be done | ||
'path/to/repo', | ||
'C:/user/repo', | ||
'file:///path/to/repo.git/', | ||
'git://git.com/~user/path/to/repo.git/', | ||
'http://git.com/path/to/repo.git/', | ||
'https://github.com/user/repo', | ||
'ssh://user@server/project.git', | ||
'user@server:project.git', | ||
'../' | ||
]; | ||
async.each(shouldMatch, function(url) { | ||
it('should have created remote for: ' + url, function(done) { | ||
generateRemote(url, function(err, remoteURL) { | ||
remoteURL.should.have.string(url); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
var shouldNotMatch = [ | ||
'origin', | ||
'weird$1+name', | ||
'remote_name', | ||
'remote_name_extended', | ||
'remote-name', | ||
'remote.test' | ||
]; | ||
async.each(shouldNotMatch, function(url) { | ||
it('should not have created remote for: ' + url, function(done) { | ||
generateRemote(url, function(err, remoteURL) { | ||
remoteURL.should.not.have.string(url); | ||
remoteURL.should.be.empty; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('push diff branches', function() { | ||
it('should push local:stage to stage:master and local:prod to prod:master', function(done) { | ||
var tasks = []; | ||
tasks.push(function(next) { | ||
execScenario(function(err, stdout) { | ||
fs.removeSync('validate'); // not needed because there's two diff remotes | ||
next(err); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
fs.removeSync('stage_validate'); | ||
childProcess.exec('git clone stage_remote stage_validate', next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'stage_validate'}, function(err, stdout) { | ||
stdout.should.have.string('first stage commit'); | ||
stdout.should.have.string('new stage commit'); | ||
next(); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
fs.removeSync('prod_validate'); | ||
childProcess.exec('git clone prod_remote prod_validate', next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'prod_validate'}, function(err, stdout) { | ||
stdout.should.have.string('first prod commit'); | ||
stdout.should.have.string('new prod commit'); | ||
next(); | ||
}); | ||
}); | ||
async.series(tasks, done); | ||
}); | ||
it('should do it multiple times', function(done) { | ||
var tasks = []; | ||
tasks.push(function(next) { | ||
execScenario(next); | ||
}); | ||
tasks.push(function(next) { | ||
fs.writeFileSync('repo/dist/empty_file', 'file not empty anymore'); | ||
next(); | ||
}); | ||
tasks.push(function(next) { | ||
execScenario(next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git clone stage_remote stage_validate', next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'stage_validate'}, function(err, stdout) { | ||
stdout.match(/new stage commit/g).should.be.length(2); | ||
next(); | ||
}); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git clone prod_remote prod_validate', next); | ||
}); | ||
tasks.push(function(next) { | ||
childProcess.exec('git log --pretty=oneline --abbrev-commit --no-color', {cwd: 'prod_validate'}, function(err, stdout) { | ||
stdout.match(/new prod commit/g).should.be.length(2); | ||
next(); | ||
}); | ||
}); | ||
async.series(tasks, done); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
132178
103
961
246
11