Skip to content

Commit

Permalink
feat: Added the changelog and release gulp tasks
Browse files Browse the repository at this point in the history
Usage: `gulp changelog` and `gulp release`
The release task includes the changelog task.

Fixes #62
  • Loading branch information
ocombe committed May 25, 2014
1 parent a3b5705 commit a0406e1
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 18 deletions.
205 changes: 205 additions & 0 deletions changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env node

var child = require('child_process');
var fs = require('fs');
var util = require('util');
var q = require('qq');

var GIT_LOG_CMD = 'git log --grep="%s" -E --format=%s %s..HEAD';
var GIT_TAG_CMD = 'git describe --tags --abbrev=0';

var HEADER_TPL = '<a name="%s"></a>\n# %s (%s)\n\n';
var LINK_ISSUE = '[#%s](https:/Famous/famous-angular/issues/%s)';
var LINK_COMMIT = '[%s](https:/Famous/famous-angular/commit/%s)';

var EMPTY_COMPONENT = '$$';


var warn = function() {
console.error('WARNING:', util.format.apply(null, arguments));
};


var parseRawCommit = function(raw) {
if (!raw) return null;

var lines = raw.split('\n');
var msg = {}, match;

msg.hash = lines.shift();
msg.subject = lines.shift();
msg.closes = [];
msg.breaks = [];

lines.forEach(function(line) {
match = line.match(/(?:Closes|Fixes)\s#(\d+)/);
if (match) msg.closes.push(parseInt(match[1]));
});

match = raw.match(/BREAKING CHANGE:([\s\S]*)/);
if (match) {
msg.breaking = match[1];
}


msg.body = lines.join('\n');
match = msg.subject.match(/^(\w*)\s?\:\s?(.*)$/);

if (!match || !match[1] || !match[2]) {
warn('Incorrect message: %s %s', msg.hash, msg.subject);
return null;
}

msg.type = match[1];
// msg.component = match[2];
msg.subject = match[2];

return msg;
};


var linkToIssue = function(issue) {
return util.format(LINK_ISSUE, issue, issue);
};


var linkToCommit = function(hash) {
return util.format(LINK_COMMIT, hash.substr(0, 8), hash);
};


var currentDate = function() {
var now = new Date();
var pad = function(i) {
return ('0' + i).substr(-2);
};

return util.format('%d-%s-%s', now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate()));
};


var printSection = function(stream, title, section, printCommitLinks) {
printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks;
var components = Object.getOwnPropertyNames(section).sort();

if (!components.length || section['$$'].length <= 0) return;

stream.write(util.format('\n## %s\n\n', title));

components.forEach(function(name) {
var prefix = '-';
var nested = section[name].length > 1;

if (name !== EMPTY_COMPONENT) {
if (nested) {
stream.write(util.format('- **%s:**\n', name));
prefix = ' -';
} else {
prefix = util.format('- **%s:**', name);
}
}

section[name].forEach(function(commit) {
if (printCommitLinks) {
stream.write(util.format('%s %s\n (%s', prefix, commit.subject, linkToCommit(commit.hash)));
if (commit.closes.length) {
stream.write(',\n ' + commit.closes.map(linkToIssue).join(', '));
}
stream.write(')\n');
} else {
stream.write(util.format('%s %s', prefix, commit.subject));
}
});
});

stream.write('\n');
};


var readGitLog = function(grep, from) {
var deferred = q.defer();

// TODO(vojta): if it's slow, use spawn and stream it instead
child.exec(util.format(GIT_LOG_CMD, grep, '%H%n%s%n%b%n==END==', from), function(code, stdout, stderr) {
var commits = [];

stdout.split('\n==END==\n').forEach(function(rawCommit) {
var commit = parseRawCommit(rawCommit);
if (commit) commits.push(commit);
});

deferred.resolve(commits);
});

return deferred.promise;
};


var writeChangelog = function(stream, commits, version) {
var sections = {
fix: {},
feat: {},
perf: {},
docs: {},
breaks: {}
};

sections.breaks[EMPTY_COMPONENT] = [];

commits.forEach(function(commit) {
var section = sections[commit.type];
var component = commit.component || EMPTY_COMPONENT;

if (section) {
section[component] = section[component] || [];
section[component].push(commit);
}

if (commit.breaking) {
sections.breaks[component] = sections.breaks[component] || [];
sections.breaks[component].push({
subject: util.format("due to %s,\n %s", linkToCommit(commit.hash), commit.breaking),
hash: commit.hash,
closes: []
});
};
});

stream.write(util.format(HEADER_TPL, version, version, currentDate()));
printSection(stream, 'Bug Fixes', sections.fix);
printSection(stream, 'Features', sections.feat);
printSection(stream, 'Performance Improvements', sections.perf);
printSection(stream, 'Documentation', sections.docs);
printSection(stream, 'Breaking Changes', sections.breaks, false);
}


var getPreviousTag = function() {
var deferred = q.defer();
child.exec(GIT_TAG_CMD, function(code, stdout, stderr) {
if (code) deferred.reject('Cannot get the previous tag.');
else deferred.resolve(stdout.replace('\n', ''));
});
return deferred.promise;
};


var generate = function(version, file) {
getPreviousTag().then(function(tag) {
// console.log('Reading git log since', tag);
readGitLog('^fix|^feat|^perf|^docs|BREAKING', tag).then(function(commits) {
// console.log('Parsed', commits.length, 'commits');
// console.log('Generating changelog to', file || 'stdout', '(', version, ')');
writeChangelog(file ? fs.createWriteStream(file) : process.stdout, commits, version);
});
});
};


// publish for testing
exports.parseRawCommit = parseRawCommit;

// hacky start if not run by jasmine :-D
if (process.argv.join('').indexOf('jasmine-node') === -1) {
generate(process.argv[2], process.argv[3]);
}
117 changes: 106 additions & 11 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
var SITE_DIR = 'famous-angular-docs/';
var EXAMPLES_DIR = 'famous-angular-examples/';

var EXPRESS_PORT = 4000;
var EXPRESS_DOCS_ROOT = __dirname + '/' + SITE_DIR + '_site';

var LIVERELOAD_PORT = 35729;

// Load plugins
var gulp = require('gulp'),
Expand All @@ -17,11 +12,9 @@ var gulp = require('gulp'),
concat = require('gulp-concat'),
notify = require('gulp-notify'),
cache = require('gulp-cache'),
livereload = require('gulp-livereload'),
server = livereload(),
gutil = require('gulp-util'),
pkg = require('./package.json'),
exec = require('gulp-exec');
exec = require('gulp-exec'),
pkg = require('./package.json');

// Clean
gulp.task('clean', function() {
Expand All @@ -31,7 +24,6 @@ gulp.task('clean', function() {

// Build for dist
gulp.task('build', ['clean'], function(event) {

var header = require('gulp-header');
var banner = ['/**',
' * <%= pkg.name %> - <%= pkg.description %>',
Expand Down Expand Up @@ -110,6 +102,7 @@ gulp.task('build-to-examples', ['clean'], function(event) {

// Watch
gulp.task('watch-examples', function(event) {
var livereload = require('gulp-livereload');
var server = livereload();
// Watch .js files
gulp.watch([
Expand All @@ -124,6 +117,108 @@ gulp.task('watch-examples', function(event) {
);
});

var promptBump = function(callback) {
var prompt = require('gulp-prompt');
var semver = require('semver');

return gulp.src('')
.pipe(prompt.prompt({
type: 'list',
name: 'bump',
message: 'What type of version bump would you like to do ? (current version is '+pkg.version+')',
choices: [
'patch ('+pkg.version+' --> '+semver.inc(pkg.version, 'patch')+')',
'minor ('+pkg.version+' --> '+semver.inc(pkg.version, 'minor')+')',
'major ('+pkg.version+' --> '+semver.inc(pkg.version, 'major')+')',
'none (exit)'
]
}, function(res){var newVer;
if(res.bump.match(/^patch/)) {
newVer = semver.inc(pkg.version, 'patch');
} else if(res.bump.match(/^minor/)) {
newVer = semver.inc(pkg.version, 'minor');
} else if(res.bump.match(/^major/)) {
newVer = semver.inc(pkg.version, 'major');
}
if(newVer && typeof callback === 'function') {
return callback(newVer);
} else {
return;
}
}));
}

var makeChangelog = function(newVer) {
var streamqueue = require('streamqueue');
var stream = streamqueue({ objectMode: true });

stream.queue(gulp.src('').pipe(exec('node ./changelog.js ' + newVer, { pipeStdout: true })));
stream.queue(gulp.src('CHANGELOG.md'));

return stream.done()
.pipe(concat('CHANGELOG.md'))
.pipe(gulp.dest('./'));
}

// Make changelog
gulp.task('changelog', function(event) {
var prompt = require('gulp-prompt');
var semver = require('semver');

return promptBump(makeChangelog);
})

gulp.task('release', ['docs'], function() { // docs task includes build task
var jeditor = require("gulp-json-editor");

return promptBump(function(newVer) {
var streamqueue = require('streamqueue');
var stream = streamqueue({ objectMode: true });

// make the changelog
stream.queue(makeChangelog(newVer));

// update the main project version number
stream.queue(
gulp.src('package.json')
.pipe(jeditor({
'version': newVer
}))
.pipe(gulp.dest("./"))
);

stream.queue(
gulp.src('bower.json')
.pipe(jeditor({
'version': newVer
}))
.pipe(gulp.dest("./"))
);

// update docs dependency
stream.queue(
gulp.src('famous-angular-docs/bower.json')
.pipe(jeditor(function(json) {
json.dependencies['famous-angular'] = newVer;
return json; // must return JSON object.
}))
.pipe(gulp.dest("famous-angular-docs"))
);

// update examples dependency
stream.queue(
gulp.src(EXAMPLES_DIR+'bower.json')
.pipe(jeditor(function(json) {
json.dependencies['famous-angular'] = newVer;
return json; // must return JSON object.
}))
.pipe(gulp.dest(EXAMPLES_DIR))
);

return stream.done();
});
})

// Default task
gulp.task('dev', function() {
var express = require('express');
Expand All @@ -132,4 +227,4 @@ gulp.task('dev', function() {
app.use(express.static(EXAMPLES_DIR + 'app/'));
app.listen(EXPRESS_PORT);
gulp.start('watch-examples');
});
});
Loading

0 comments on commit a0406e1

Please sign in to comment.