mirror of
https://github.com/zhigang1992/firebase-tools.git
synced 2026-01-12 22:47:24 +08:00
Add support for scoped function deploys with --only functions:group1.fn2,functions:fn4 (#204)
This commit is contained in:
committed by
GitHub
parent
4c70d75c4b
commit
c3d2673bf8
@@ -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(','));
|
||||
}
|
||||
|
||||
36
lib/checkValidTargetFilters.js
Normal file
36
lib/checkValidTargetFilters.js
Normal 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();
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user