Add support for scoped function deploys with --only functions:group1.fn2,functions:fn4 (#204)

This commit is contained in:
Stephanie Madison
2017-05-09 16:53:36 -07:00
committed by GitHub
parent 4c70d75c4b
commit c3d2673bf8
4 changed files with 156 additions and 6 deletions

View File

@@ -5,6 +5,7 @@ var _ = require('lodash');
var acquireRefs = require('../lib/acquireRefs');
var chalk = require('chalk');
var checkDupHostingKeys = require('../lib/checkDupHostingKeys');
var checkValidTargetFilters = require('../lib/checkValidTargetFilters');
var Command = require('../lib/command');
var deploy = require('../lib/deploy');
var logger = require('../lib/logger');
@@ -19,7 +20,10 @@ module.exports = new Command('deploy')
.description('deploy code and assets to your Firebase project')
.option('-p, --public <path>', 'override the Hosting public directory specified in firebase.json')
.option('-m, --message <message>', 'an optional message describing this deploy')
.option('--only <targets>', 'only deploy to specified, comma-separated targets (e.g. "hosting,storage")')
.option('--only <targets>', 'only deploy to specified, comma-separated targets (e.g. "hosting,storage"). For functions, ' +
'can specify filters with colons to scope function deploys to only those functions (e.g. "--only functions:func1,functions:func2"). ' +
'When filtering based on export groups (the exported module object keys), use dots to specify group names ' +
'(e.g. "--only functions:group1.subgroup1,functions:group2)"')
.option('--except <targets>', 'deploy to all targets except specified (e.g. "database")')
.before(requireConfig)
.before(function(options) {
@@ -38,16 +42,15 @@ module.exports = new Command('deploy')
});
})
.before(checkDupHostingKeys)
.before(checkValidTargetFilters)
.action(function(options) {
var targets = VALID_TARGETS.filter(function(t) {
return options.config.has(t);
});
if (options.only && options.except) {
return utils.reject('Cannot specify both --only and --except', {exit: 1});
}
if (options.only) {
targets = _.intersection(targets, options.only.split(','));
targets = _.intersection(targets, options.only.split(',').map(function(opt) {
return opt.split(':')[0];
}));
} else if (options.except) {
targets = _.difference(targets, options.except.split(','));
}

View File

@@ -0,0 +1,36 @@
'use strict';
var _ = require('lodash');
var RSVP = require('rsvp');
var FirebaseError = require('./error');
module.exports = function(options) {
function numFilters(targetTypes) {
return _.filter(options.only, function(opt) {
var optChunks = opt.split(':');
return _.includes(targetTypes, optChunks[0]) && optChunks.length > 1;
}).length;
}
function targetContainsFilter(targetTypes) {
return numFilters(targetTypes) > 1;
}
function targetDoesNotContainFilter(targetTypes) {
return numFilters(targetTypes) === 0;
}
return new RSVP.Promise(function(resolve, reject) {
if (!options.only) {
return resolve();
}
if (options.except) {
return reject(new FirebaseError('Cannot specify both --only and --except', {exit: 1}));
}
if (targetContainsFilter(['database', 'storage', 'hosting'])) {
return reject(new FirebaseError('Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions', {exit: 1}));
}
if (targetContainsFilter(['functions']) && targetDoesNotContainFilter(['functions'])) {
return reject(new FirebaseError('Cannot specify "--only functions" and "--only functions:<filter>" at the same time', {exit: 1}));
}
return resolve();
});
};

View File

@@ -49,6 +49,67 @@ module.exports = function(context, options, payload) {
});
}
function _functionMatchesGroup(functionName, groupChunks) {
return _.isEqual(groupChunks, functionName.split('-').slice(0, groupChunks.length));
}
function _getFilterGroups() {
if (!options.only) {
return [];
}
var opts;
return _.chain(options.only.split(','))
.filter(function(filter) {
opts = filter.split(':');
return opts[0] === 'functions' && opts[1];
}).map(function(filter) {
return filter.split(':')[1].split('.');
}).value();
}
function _getReleaseNames(uploadNames, existingNames, functionFilterGroups) {
if (functionFilterGroups.length === 0) {
return uploadNames;
}
var allFunctions = _.union(uploadNames, existingNames);
return _.filter(allFunctions, function(functionName) {
return _.some(_.map(functionFilterGroups, function(groupChunks) {
return _functionMatchesGroup(functionName, groupChunks);
}));
});
}
function logFilters(existingNames, releaseNames, functionFilterGroups) {
if (functionFilterGroups.length === 0) {
return;
}
logger.debug('> [functions] filtering triggers to: ' + JSON.stringify(releaseNames, null, 2));
track('Functions Deploy with Filter', '', releaseNames.length);
if (existingNames.length > 0) {
utils.logBullet(chalk.bold.cyan('functions: ') + 'current functions in project: ' + existingNames.join(', '));
}
if (releaseNames.length > 0) {
utils.logBullet(chalk.bold.cyan('functions: ') + 'uploading functions in project: ' + releaseNames.join(', '));
}
var allFunctions = _.union(releaseNames, existingNames);
var unmatchedFilters = _.chain(functionFilterGroups)
.filter(function(filterGroup) {
return !_.some(_.map(allFunctions, function(functionName) {
return _functionMatchesGroup(functionName, filterGroup);
}));
}).map(function(group) {
return group.join('-');
}).value();
if (unmatchedFilters.length > 0) {
utils.logWarning(chalk.bold.yellow('functions: ') + 'the following filters were specified but do not match any functions in the project: ' + unmatchedFilters.join(', '));
}
}
function _reportResults(successfulCalls, failedCalls) {
function logFailedOps(ops) {
_.forEach(ops, function(operation) {
@@ -194,9 +255,14 @@ module.exports = function(context, options, payload) {
};
var existingNames = _.map(existingFunctions, pluckName);
var functionFilterGroups = _getFilterGroups();
var releaseNames = _getReleaseNames(uploadedNames, existingNames, functionFilterGroups);
logFilters(existingNames, releaseNames, functionFilterGroups);
var addOps = _.chain(uploadedNames)
.difference(existingNames)
.intersection(releaseNames)
.map(function(functionName) {
var functionInfo = _.find(functionsInfo, {'name': functionName});
return _prepFunctionOp(functionInfo).then(function(functionTrigger) {
@@ -218,6 +284,7 @@ module.exports = function(context, options, payload) {
var updateOps = _.chain(uploadedNames)
.intersection(existingNames)
.intersection(releaseNames)
.map(function(functionName) {
var functionInfo = _.find(functionsInfo, {'name': functionName});
return _prepFunctionOp(functionInfo, functionName).then(function(functionTrigger) {
@@ -267,6 +334,8 @@ module.exports = function(context, options, payload) {
});
}).value();
// If not using function filters, then `deleteReleaseNames` should be equivalent to existingNames so that intersection is a noop
var deleteReleaseNames = functionFilterGroups.length > 0 ? releaseNames : existingNames;
var deleteOps = _.chain(existingFunctions)
.filter(function(o) {
@@ -274,6 +343,7 @@ module.exports = function(context, options, payload) {
}) // only delete functions uploaded via firebase-tools
.map(pluckName)
.difference(uploadedNames)
.intersection(deleteReleaseNames)
.map(function(functionName) {
utils.logBullet(chalk.bold.cyan('functions: ') + 'deleting function ' + chalk.bold(functionName) + '...');
_startTimer(functionName, 'delete');

View File

@@ -93,6 +93,17 @@ var testCreateUpdate = function() {
});
};
var testCreateUpdateWithFilter = function() {
fs.copySync(functionsSource, tmpDir + '/functions/index.js');
return new RSVP.Promise(function(resolve) {
exec(localFirebase + ' deploy --only functions:dbAction,functions:httpsAction', {'cwd': tmpDir}, function(err, stdout) {
console.log(stdout);
expect(err).to.be.null;
resolve(checkFunctionsListMatch(['dbAction', 'httpsAction']));
});
});
};
var testDelete = function() {
return new RSVP.Promise(function(resolve) {
exec('> functions/index.js &&' + localFirebase + ' deploy', {'cwd': tmpDir}, function(err, stdout) {
@@ -103,6 +114,27 @@ var testDelete = function() {
});
};
var testDeleteWithFilter = function() {
return new RSVP.Promise(function(resolve) {
exec('> functions/index.js &&' + localFirebase + ' deploy --only functions:dbAction', {'cwd': tmpDir}, function(err, stdout) {
console.log(stdout);
expect(err).to.be.null;
resolve(checkFunctionsListMatch(['httpsAction']));
});
});
};
var testUnknownFilter = function() {
return new RSVP.Promise(function(resolve) {
exec('> functions/index.js &&' + localFirebase + ' deploy --only functions:unknownFilter', {'cwd': tmpDir}, function(err, stdout) {
console.log(stdout);
expect(stdout).to.contain('the following filters were passed but don\'t match any functions in current project or currently being exported: unknownFilter');
expect(err).to.be.null;
resolve(checkFunctionsListMatch(['httpsAction']));
});
});
};
var waitForAck = function(uuid, testDescription) {
return Promise.race([
new Promise(function(resolve) {
@@ -204,6 +236,15 @@ var main = function() {
return testDelete();
}).then(function() {
console.log(chalk.green('\u2713 Test passed: deleting functions'));
return testCreateUpdateWithFilter();
}).then(function() {
console.log(chalk.green('\u2713 Test passed: creating functions with filters'));
return testDeleteWithFilter();
}).then(function() {
console.log(chalk.green('\u2713 Test passed: deleting functions with filters'));
return testUnknownFilter();
}).then(function() {
console.log(chalk.green('\u2713 Test passed: threw warning when passing filter with unknown identifier'));
}).catch(function(err) {
console.log(chalk.red('Error while running tests: '), err);
return RSVP.resolve();