From d1ee3ddb5d8dcd3063afc6011b148bef5ac1bf6f Mon Sep 17 00:00:00 2001 From: Scott Prue Date: Wed, 4 Dec 2019 05:07:26 -0500 Subject: [PATCH] v0.9.0 * fix(core): fix an issue where "no command specified" error was shown erroneously * feat(core): add `serve` command for serving project matching branch * chore(tests): add initial tests for `serve` and deploy actions --- README.md | 37 +++++++++++++- bin/firebase-ci | 2 +- cmds/deploy.js | 2 +- cmds/index.js | 1 + cmds/serve.js | 31 ++++++++++++ package.json | 2 +- src/actions/serve.js | 82 ++++++++++++++++++++++++++++++++ test/unit/actions/deploy.spec.js | 23 +++++++++ test/unit/actions/serve.spec.js | 23 +++++++++ 9 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 cmds/serve.js create mode 100644 src/actions/serve.js create mode 100644 test/unit/actions/deploy.spec.js create mode 100644 test/unit/actions/serve.spec.js diff --git a/README.md b/README.md index ce53e00..4b0bac7 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ Advanced configuration of Firebase deployment is often necessary when deploying * [`copyVersion`](#createversion) - Copy version from `package.json` to `functions/package.json` * [`createConfig`](#createconfig) - Create a config file based on CI environment variables (defaults to `src/config.js`) -* [`deploy`](#deploy) - Deploy to Firebase (runs other actions by default) +* [`deploy`](#deploy) - Deploy to Firebase project matching branch name in `.firebaserc` (runs other `firebase-ci` actions by default unless `-s` is passed) +* [`serve`](#serve) - Serve a the Firebase project matching branch name in `.firebaserc` using `firebase serve` * [`mapEnv`](#mapenv) - Map environment variables from CI Environment to Firebase functions environment * [`project`](#project) - Output project name associated with CI environment (useful for commands that should be run for each environment) @@ -195,10 +196,12 @@ Options can be passed as flags or within an options object if calling action as **Options:** * [Simple mode](#simple-mode) * [Info](#info-option) +* [Only](#only-option) Deploy to Firebase. Following the API of `firebase-tools`, specific targets (i.e. `functions, hosting`) can be specified for deployment. #### Default + * Everything skipped on Pull Requests * Deployment goes to default project * If you have a `functions` folder, `npm install` will be run for you within your `functions` folder @@ -206,18 +209,27 @@ Deploy to Firebase. Following the API of `firebase-tools`, specific targets (i.e * [`mapEnv`](#mapenv) is called before deployment based on settings in `.firebaserc`, if you don't want this to happen, use simple mode. #### Simple Mode + Option: `--simple` Flag: `-s` Skip all `firebase-ci` actions and only run Firebase deployment #### Info Option + Option : `--info` Flag: `-i` Provide extra information from internal actions (including npm install of `firebase-tools`). -#### Skipping Deploying Functions +#### Only Option + +Option : `--only` +Flag: `-o` + +Firebase targets to include (passed directly to firebase-tools) + +##### Skipping Deploying Functions If you have a functions folder, your functions will automatically deploy as part of using `firebase-ci`. For skipping this functionality, you may use the only flag, similar to the API of `firebase-tools`. @@ -226,6 +238,27 @@ script: - $(npm bin)/firebase-ci deploy --only hosting ``` +### serve + +`firebase-ci serve` + +**Options:** +* [only](#only-option) + +Serve using to `firebase serve`. Following the API of `firebase-tools`, specific targets (i.e. `functions, hosting`) can be specified for serving. + +#### Default + +* Project alias matching branch name is served +* If there is no matching alias, `default` project is used + +#### Only Option + +Option : `--only` +Flag: `-o` + +Firebase targets to include (passed directly to firebase-tools) + ### mapEnv `firebase-ci mapEnv` diff --git a/bin/firebase-ci b/bin/firebase-ci index 4ab4e16..93e7dee 100755 --- a/bin/firebase-ci +++ b/bin/firebase-ci @@ -20,7 +20,7 @@ program.on('*', function(name) { program.parse(process.argv) -if (program.args.length < 1) { +if (program.rawArgs.length < 3) { console.log("No command specified. See 'firebase-ci --help':") // eslint-disable-line no-console program.outputHelp() process.exit(1) diff --git a/cmds/deploy.js b/cmds/deploy.js index 6451b7f..a601f5b 100644 --- a/cmds/deploy.js +++ b/cmds/deploy.js @@ -16,7 +16,7 @@ module.exports = function(program) { program .command('deploy') .description( - 'Deploy to Firebase only on build branches (master, stage, prod)' + 'Deploy to Firebase only on build branches with matching project settings in .firebaserc' ) .option('-d --debug', 'Enable extra logging') // taken by autocmdr .option('-i --info', 'Extra Info from installs') diff --git a/cmds/index.js b/cmds/index.js index 144e01f..ac8af40 100644 --- a/cmds/index.js +++ b/cmds/index.js @@ -12,6 +12,7 @@ module.exports = function(client) { client.mapEnv = loadCommand('mapEnv') client.run = loadCommand('run') client.project = loadCommand('project') + client.serve = loadCommand('serve') return client } diff --git a/cmds/serve.js b/cmds/serve.js new file mode 100644 index 0000000..f756fc2 --- /dev/null +++ b/cmds/serve.js @@ -0,0 +1,31 @@ +/* deploy commander component + * To use add require('../cmds/deploy.js')(program) to your commander.js based node executable before program.parse + */ +'use strict' +const serve = require('../lib/actions/serve').default + +/** + * @name serve + * Serve project using project matching associated branch + * @param {object} program - Commander program object + * @example Basic + * # make sure FIREBASE_TOKEN env variable is set + * firebase-ci deploy + */ +module.exports = function(program) { + program + .command('serve') + .description( + 'Use firebase serve to serve a project matching branch name settings in .firebaserc' + ) + .option('-d --debug', 'Enable extra logging') // taken by autocmdr + .option( + '-o --only ', + 'Only serve specified targets, comma-seperated (e.g "hosting, storage")' + ) + .action(opts => { + return serve(opts) + .then(() => process.exit(0)) + .catch(() => process.exit(1)) + }) +} diff --git a/package.json b/package.json index 94d585f..f6da58a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-ci", - "version": "0.8.0", + "version": "0.9.0", "description": "Simplified Firebase interaction for continuous integration including deploying hosting, functions, and database/storage rules.", "main": "lib/index.js", "bin": { diff --git a/src/actions/serve.js b/src/actions/serve.js new file mode 100644 index 0000000..75daf38 --- /dev/null +++ b/src/actions/serve.js @@ -0,0 +1,82 @@ +import { get } from 'lodash' +import chalk from 'chalk' +import { error, info, warn } from '../utils/logger' +import { getFile } from '../utils/files' +import { to } from '../utils/async' +import { runCommand } from '../utils/commands' +import { getProjectKey, getFallbackProjectKey } from '../utils/ci' + +const skipPrefix = 'Skipping firebase-ci serve' + +/** + * Serve specific project + * @param {object} opts - Settings for serving + * @returns {Promise} Resolves after serve is called + */ +export default async function serve(opts) { + // Load settings from .firebaserc + const settings = getFile('.firebaserc') + + // Get project from passed options, falling back to branch name + const fallbackProjectName = getFallbackProjectKey() + const projectKey = getProjectKey(opts) + + // Get project setting from settings file based on branchName falling back + // to fallbackProjectName + const projectName = get(settings, `projects.${projectKey}`) + const fallbackProjectSetting = get( + settings, + `projects.${fallbackProjectName}` + ) + + // Handle project option + if (!projectName) { + const nonProjectBranch = `${skipPrefix} - Project ${chalk.cyan( + projectKey + )} is not an alias, checking for fallback...` + warn(nonProjectBranch) + if (!fallbackProjectSetting) { + const nonFallbackBranch = `${skipPrefix} - Fallback Project: ${chalk.cyan( + fallbackProjectName + )} is a not an alias, exiting...` + warn(nonFallbackBranch) + return nonProjectBranch + } + return null + } + + info( + `Calling serve for project ${chalk.cyan(projectName)} (alias ${chalk.cyan( + projectKey + )})` + ) + + const serveArgs = ['serve', '-P', projectKey] + const onlyString = opts && opts.only ? `--only ${opts.only}` : '' + if (onlyString) { + serveArgs.push(onlyString) + } + + // Run command to set functions config + const [serveErr] = await to( + runCommand({ + command: 'firebase', + args: serveArgs + }) + ) + + // Handle errors running functions config + if (serveErr) { + const errMsg = `Error calling serve for ${chalk.cyan( + projectName + )} (alias ${chalk.cyan(projectKey)}) :` + error(errMsg, serveErr) + throw new Error(errMsg) + } + + info( + `Successfully called serve for project ${chalk.cyan( + projectName + )} (alias ${chalk.cyan(projectKey)})` + ) +} diff --git a/test/unit/actions/deploy.spec.js b/test/unit/actions/deploy.spec.js new file mode 100644 index 0000000..6797205 --- /dev/null +++ b/test/unit/actions/deploy.spec.js @@ -0,0 +1,23 @@ +import deploy from 'actions/deploy' + +describe('deploy action', () => { + let logSpy + beforeEach(() => { + logSpy = sinon.spy(console, 'log') + }) + + afterEach(() => { + logSpy && logSpy.restore() + }) + + it('exports a function to be used as a command', () => { + expect(deploy).to.be.an('function') + }) + + it('Logs error saying no config found for provided alias', async () => { + await deploy({ project: 'asdf' }) + // first time with: 'ℹ Skipping Firebase Deploy - Project asdf is not an alias, checking for fallback...' + // second time with: 'ℹ Skipping Firebase Deploy - Fallback Project: undefined is a not an alias, exiting...' + expect(logSpy).to.have.been.calledTwice + }) +}) diff --git a/test/unit/actions/serve.spec.js b/test/unit/actions/serve.spec.js new file mode 100644 index 0000000..95da5e5 --- /dev/null +++ b/test/unit/actions/serve.spec.js @@ -0,0 +1,23 @@ +import serve from 'actions/serve' + +describe('serve action', () => { + let logSpy + beforeEach(() => { + logSpy = sinon.spy(console, 'log') + }) + + afterEach(() => { + logSpy && logSpy.restore() + }) + + it('exports a function to be used as a command', () => { + expect(serve).to.be.an('function') + }) + + it('Logs error saying no config found for provided alias', async () => { + await serve({ project: 'asdf' }) + // first time with: '⚠ Warning: Skipping firebase-ci serve - Project asdf is not an alias, checking for fallback...' + // second time with: '⚠ Warning: Skipping firebase-ci serve - Fallback Project: undefined is a not an alias, exiting...' + expect(logSpy).to.have.been.calledTwice + }) +})