diff --git a/src/publish.js b/src/publish.js index 89af37df9..61e592a17 100644 --- a/src/publish.js +++ b/src/publish.js @@ -14,6 +14,7 @@ const yargsOpenwhisk = require('./yargs-openwhisk.js'); const yargsFastly = require('./yargs-fastly.js'); +const yargsGithub = require('./yargs-github.js'); const { makeLogger } = require('./log-common.js'); module.exports = function strain() { @@ -28,6 +29,7 @@ module.exports = function strain() { builder: (yargs) => { yargsOpenwhisk(yargs); yargsFastly(yargs); + yargsGithub(yargs); yargs .option('dry-run', { alias: 'dryRun', @@ -46,6 +48,17 @@ module.exports = function strain() { type: 'string', default: 'https://adobeioruntime.net/api/v1/web/helix/default/publish', }) + .option('api-config-purge', { + alias: 'apiConfigPurge', + describe: 'API URL for helix bot config service', + type: 'string', + default: 'https://app.project-helix.io/config/purge', + }) + .option('update-bot-config', { + alias: 'updateBotConfig', + describe: 'Update the helix bot configuration on the affected content repositories.', + type: 'boolean', + }) .demandOption( 'fastly-auth', 'Authentication is required. You can pass the key via the HLX_FASTLY_AUTH environment variable, too', @@ -54,8 +67,20 @@ module.exports = function strain() { 'fastly-namespace', 'Fastly Service ID is required', ) + .check((args) => { + if (args.githubToken && args.updateBotConfig === undefined) { + // eslint-disable-next-line no-param-reassign + args.updateBotConfig = true; + } else if (args.updateBotConfig && !args.githubToken) { + return new Error('Github token is required in order to update bot config.\n' + + 'Provide one via --github-token or via the HLX_GITHUB_TOKEN environment variable.\n' + + 'You can use `hlx auth` to automatically obtain a new token.'); + } + return true; + }) .group(['wsk-auth', 'wsk-namespace', 'fastly-auth', 'fastly-namespace'], 'Deployment Options') .group(['wsk-host', 'dry-run'], 'Advanced Options') + .group(['github-token', 'update-bot-config'], 'Helix Bot Options') .help(); }, handler: async (argv) => { @@ -69,15 +94,23 @@ module.exports = function strain() { executor = executor || new PublishCommand(makeLogger(argv)); } - await executor + const cmd = executor .withWskAuth(argv.wskAuth) .withWskHost(argv.wskHost) .withWskNamespace(argv.wskNamespace) .withFastlyNamespace(argv.fastlyNamespace) .withFastlyAuth(argv.fastlyAuth) .withDryRun(argv.dryRun) - .withPublishAPI(argv.apiPublish) - .run(); + .withPublishAPI(argv.apiPublish); + + if (argv.remote) { + // only support updating the bot config for remote publish + cmd + .withGithubToken(argv.githubToken) + .withUpdateBotConfig(argv.updateBotConfig) + .withConfigPurgeAPI(argv.apiConfigPurge); + } + await cmd.run(); }, }; diff --git a/src/remotepublish.cmd.js b/src/remotepublish.cmd.js index 446721226..db73c28ad 100644 --- a/src/remotepublish.cmd.js +++ b/src/remotepublish.cmd.js @@ -29,6 +29,9 @@ class RemotePublishCommand extends AbstractCommand { this._fastly_auth = null; this._dryRun = false; this._publishAPI = 'https://adobeioruntime.net/api/v1/web/helix/default/publish'; + this._githubToken = ''; + this._updateBotConfig = false; + this._configPurgeAPI = 'https://app.project-helix.io/config/purge'; } tick(ticks = 1, message, name) { @@ -49,16 +52,14 @@ class RemotePublishCommand extends AbstractCommand { } progressBar() { - if (this._bar) { - return this._bar; + if (!this._bar) { + this._bar = new ProgressBar('Publishing [:bar] :action :etas', { + total: 24 + (this._updateBotConfig ? 2 : 0), + width: 50, + renderThrottle: 1, + stream: process.stdout, + }); } - this._bar = new ProgressBar('Publishing [:bar] :action :etas', { - total: 23, - width: 50, - renderThrottle: 1, - stream: process.stdout, - }); - return this._bar; } @@ -97,6 +98,21 @@ class RemotePublishCommand extends AbstractCommand { return this; } + withGithubToken(value) { + this._githubToken = value; + return this; + } + + withUpdateBotConfig(value) { + this._updateBotConfig = value; + return this; + } + + withConfigPurgeAPI(value) { + this._configPurgeAPI = value; + return this; + } + showNextStep(dryrun) { this.progressBar().terminate(); if (dryrun) { @@ -154,6 +170,7 @@ class RemotePublishCommand extends AbstractCommand { } servicePublish() { + this.tick(1, 'preparing service config for Helix', true); return request.post(this._publishAPI, { json: true, body: { @@ -163,10 +180,10 @@ class RemotePublishCommand extends AbstractCommand { version: this._version, }, }).then(() => { - this.tick(10, 'set service config up for Helix', true); + this.tick(9, 'set service config up for Helix', true); return true; }).catch((e) => { - this.tick(10, 'failed to set service config up for Helix', true); + this.tick(9, 'failed to set service config up for Helix', true); this.log.error(`Remote publish service failed ${e}`); throw new Error('Unable to setup service config'); }); @@ -192,21 +209,149 @@ class RemotePublishCommand extends AbstractCommand { }); } + async updateBotConfig() { + const repos = {}; + this.tick(1, 'updating helix-bot purge config', true); + this._strainsToPublish.forEach((strain) => { + const url = strain.content; + // todo: respect path + const urlString = `${url.protocol}://${url.host}/${url.owner}/${url.repo}.git#${url.ref}`; + if (!repos[urlString]) { + repos[urlString] = { + strains: [], + key: `${url.owner}/${url.repo}#${url.ref}`, + }; + } + repos[urlString].strains.push(strain.name); + }); + const response = await request.post(this._configPurgeAPI, { + json: true, + body: { + github_token: this._githubToken, + content_repositories: Object.keys(repos), + fastly_service_id: this._fastly_namespace, + fastly_token: this._fastly_auth, + }, + }); + + this._botStatus = { + repos, + response, + }; + this.tick(1, 'updated helix-bot purge config', true); + } + + showHelixBotResponse() { + if (!this._botStatus) { + return; + } + const { repos, response } = this._botStatus; + // create summary + const reposNoBot = []; + const reposUpdated = []; + const reposErrors = []; + Object.keys(repos).forEach((repoUrl) => { + const repo = repos[repoUrl]; + const info = response[repo.key]; + if (!info) { + this.log.error(`Internal error: ${repo.key} should be in the service response`); + reposErrors.push(repo); + return; + } + if (info.errors) { + this.log.error(`${repo.key} update failed: ${info.errors}`); + reposErrors.push(repo); + return; + } + if (!info.installation_id) { + reposNoBot.push(repo); + return; + } + if (!info.config || !info.config.caches) { + this.log.error(`Internal error: ${repo.key} status does not have configuration details.`); + reposErrors.push(repo); + return; + } + // find the fastlyId in the cache + const cacheInfo = info.config.caches + .find(cache => cache.fastlyServiceId === this._fastly_namespace); + if (!cacheInfo) { + this.log.error(`Internal error: ${repo.key} status does have a configuration entry for given fastly service id.`); + reposErrors.push(repo); + return; + } + if (cacheInfo.errors) { + this.log.error(`${repo.key} update failed for given fastly service id: ${cacheInfo.errors}`); + reposErrors.push(repo); + return; + } + reposUpdated.push(repo); + }); + + if (reposUpdated.length > 0) { + this.log.info(''); + this.log.info('Updated the purge-configuration of the following repositories:'); + reposUpdated.forEach((repo) => { + this.log.info(chalk`- {cyan ${repo.key}} {grey (${repo.strains.join(', ')})}`); + }); + } + + if (reposErrors.length > 0) { + this.log.info(''); + this.log.info('The purge-configuration of following repositories were not updated due to errors (see log for details):'); + reposErrors.forEach((repo) => { + this.log.info(chalk`- {cyan ${repo.key}} {grey (${repo.strains.join(', ')})}`); + }); + } + + if (reposNoBot.length > 0) { + this.log.info(''); + this.log.info('The following repositories are referenced by strains but don\'t have the helix-bot setup:'); + reposNoBot.forEach((repo) => { + this.log.info(chalk`- {cyan ${repo.key}} {grey (${repo.strains.join(', ')})}`); + }); + this.log.info(chalk`\nVisit {blue https://github.com/apps/helix-bot} to manage the helix bot installations.`); + } + } + async init() { await super.init(); this._fastly = fastly(this._fastly_auth, this._fastly_namespace); + + // gather all content repositories of the affected strains + this._strainsToPublish = this.config.strains.getByFilter((strain) => { + if (strain.isProxy()) { + this.log.debug(`ignoring proxy strain ${strain.name}`); + return false; + } + // skip the strains where we can't determine the action name + if (!strain.package) { + this.log.debug(`ignoring unaffected strain ${strain.name}`); + return false; + } + return true; + }); } async run() { await this.init(); + if (this._strainsToPublish.length === 0) { + this.log.warn(chalk`None of the strains contains {cyan package} information. Aborting command.`); + return; + } try { + this.tick(1, 'preparing fastly transaction', true); await this._fastly.transact(async (version) => { this._version = version; await this.servicePublish(); await this.serviceAddLogger(); await this.updateFastlySecrets(); }, !this._dryRun); + if (this._updateBotConfig) { + await this.updateBotConfig(); + } await this.purgeFastly(); + this.showHelixBotResponse(); this.showNextStep(this._dryRun); } catch (e) { const message = 'Error while running the Publish command'; diff --git a/src/yargs-github.js b/src/yargs-github.js new file mode 100644 index 000000000..48cc839a0 --- /dev/null +++ b/src/yargs-github.js @@ -0,0 +1,20 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +module.exports = function commonArgs(yargs) { + return yargs + .option('github-token', { + alias: 'githubToken', + describe: 'Github access token', + type: 'string', + }); +}; diff --git a/test/fixtures/all.env b/test/fixtures/all.env index b72b22298..49430c688 100644 --- a/test/fixtures/all.env +++ b/test/fixtures/all.env @@ -49,6 +49,9 @@ HLX_WSK_HOST = myruntime.net # publish HLX_REMOTE = false HLX_API_PUBLISH = foobar.api +HLX_GITHUB_TOKEN = github-token-foobar +HLX_UPDATE_BOT_CONFIG = true +HLX_API_CONFIG_PURGE = purge.api # perf HLX_JUNIT = some-results.xml diff --git a/test/fixtures/deployed.yaml b/test/fixtures/deployed.yaml index d04c2e9e9..1f61eda65 100644 --- a/test/fixtures/deployed.yaml +++ b/test/fixtures/deployed.yaml @@ -1,7 +1,23 @@ strains: - default: + - name: default content: ssh://git@github.com/adobe/helix-cli.git code: ssh://git@github.com/adobe/helix-cli.git static: ssh://git@github.com/adobe/helix-cli.git package: dirty url: https://www.project-helix.io/cli + + - name: api + content: ssh://git@github.com/adobe/helix-content.git + code: ssh://git@github.com/adobe/helix-cli.git + static: ssh://git@github.com/adobe/helix-cli.git + package: dirty + url: https://www.project-helix.io/api + + - name: non-deployed + content: ssh://git@github.com/adobe/helix-cli.git + code: ssh://git@github.com/adobe/helix-cli.git + static: ssh://git@github.com/adobe/helix-cli.git + url: https://www.project-helix.io/cli + + - name: proxy + origin: https://www.adobe.com diff --git a/test/fixtures/non-deployed.yaml b/test/fixtures/non-deployed.yaml new file mode 100644 index 000000000..db448d8a0 --- /dev/null +++ b/test/fixtures/non-deployed.yaml @@ -0,0 +1,21 @@ +strains: + - name: default + content: ssh://git@github.com/adobe/helix-cli.git + code: ssh://git@github.com/adobe/helix-cli.git + static: ssh://git@github.com/adobe/helix-cli.git + url: https://www.project-helix.io/cli + + - name: api + content: ssh://git@github.com/adobe/helix-content.git + code: ssh://git@github.com/adobe/helix-cli.git + static: ssh://git@github.com/adobe/helix-cli.git + url: https://www.project-helix.io/api + + - name: non-deployed + content: ssh://git@github.com/adobe/helix-cli.git + code: ssh://git@github.com/adobe/helix-cli.git + static: ssh://git@github.com/adobe/helix-cli.git + url: https://www.project-helix.io/cli + + - name: proxy + origin: https://www.adobe.com diff --git a/test/testPublishCli.js b/test/testPublishCli.js index 5af71573f..62b76f4a6 100644 --- a/test/testPublishCli.js +++ b/test/testPublishCli.js @@ -20,7 +20,7 @@ const dotenv = require('dotenv'); const path = require('path'); const { clearHelixEnv } = require('./utils.js'); const CLI = require('../src/cli.js'); -const PublishCommand = require('../src/publish.cmd'); +const RemotePublishCommand = require('../src/remotepublish.cmd.js'); describe('hlx publish', () => { // mocked command instance @@ -28,7 +28,7 @@ describe('hlx publish', () => { beforeEach(() => { clearHelixEnv(); - mockPublish = sinon.createStubInstance(PublishCommand); + mockPublish = sinon.createStubInstance(RemotePublishCommand); mockPublish.withWskHost.returnsThis(); mockPublish.withWskAuth.returnsThis(); mockPublish.withWskNamespace.returnsThis(); @@ -36,6 +36,9 @@ describe('hlx publish', () => { mockPublish.withFastlyAuth.returnsThis(); mockPublish.withDryRun.returnsThis(); mockPublish.withPublishAPI.returnsThis(); + mockPublish.withConfigPurgeAPI.returnsThis(); + mockPublish.withUpdateBotConfig.returnsThis(); + mockPublish.withGithubToken.returnsThis(); mockPublish.run.returnsThis(); }); @@ -69,6 +72,23 @@ describe('hlx publish', () => { sinon.assert.calledWith(mockPublish.withDryRun, true); }); + it('hlx publish can use env (remote)', () => { + dotenv.config({ path: path.resolve(__dirname, 'fixtures', 'all.env') }); + new CLI() + .withCommandExecutor('publish', mockPublish) + .run(['publish', '--remote']); + sinon.assert.calledWith(mockPublish.withWskHost, 'myruntime.net'); + sinon.assert.calledWith(mockPublish.withWskAuth, 'foobar'); + sinon.assert.calledWith(mockPublish.withWskNamespace, '1234'); + sinon.assert.calledWith(mockPublish.withFastlyNamespace, '1234'); + sinon.assert.calledWith(mockPublish.withFastlyAuth, 'foobar'); + sinon.assert.calledWith(mockPublish.withPublishAPI, 'foobar.api'); + sinon.assert.calledWith(mockPublish.withDryRun, true); + sinon.assert.calledWith(mockPublish.withConfigPurgeAPI, 'purge.api'); + sinon.assert.calledWith(mockPublish.withGithubToken, 'github-token-foobar'); + sinon.assert.calledWith(mockPublish.withUpdateBotConfig, true); + }); + it('hlx publish works with minimal arguments', () => { new CLI() .withCommandExecutor('publish', mockPublish) @@ -105,6 +125,44 @@ describe('hlx publish', () => { sinon.assert.calledWith(mockPublish.withFastlyNamespace, 'hlx'); // TODO !! sinon.assert.calledWith(mockPublish.withFastlyAuth, 'secret-key'); sinon.assert.calledWith(mockPublish.withPublishAPI, 'https://adobeioruntime.net/api/v1/web/helix/default/publish'); + sinon.assert.calledWith(mockPublish.withUpdateBotConfig, undefined); sinon.assert.calledOnce(mockPublish.run); }); + + it('hlx publish implicit bot config with github token', () => { + new CLI() + .withCommandExecutor('publish', mockPublish) + .run(['publish', + '--wsk-auth', 'secret-key', + '--wsk-namespace', 'hlx', + '--fastly-auth', 'secret-key', + '--fastly-namespace', 'hlx', + '--remote', 'true', + '--github-token', 'foobar', + ]); + + sinon.assert.calledWith(mockPublish.withUpdateBotConfig, true); + sinon.assert.calledWith(mockPublish.withGithubToken, 'foobar'); + sinon.assert.calledWith(mockPublish.withConfigPurgeAPI, 'https://app.project-helix.io/config/purge'); + sinon.assert.calledOnce(mockPublish.run); + }); + + it('hlx publish requires github token for update config', (done) => { + new CLI() + .withCommandExecutor('publish', mockPublish) + .onFail((err) => { + assert.ok(err.indexOf('required')); + done(); + }) + .run(['publish', + '--wsk-auth', 'secret-key', + '--wsk-namespace', 'hlx', + '--fastly-auth', 'secret-key', + '--fastly-namespace', 'hlx', + '--remote', 'true', + '--update-bot-config', + ]); + + assert.fail('publish w/o github token should fail.'); + }); }); diff --git a/test/testRemotePublishCmd.botconfig.js b/test/testRemotePublishCmd.botconfig.js new file mode 100644 index 000000000..80458f52e --- /dev/null +++ b/test/testRemotePublishCmd.botconfig.js @@ -0,0 +1,435 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +const nock = require('nock'); +const path = require('path'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const assert = require('assert'); +const { Logger } = require('@adobe/helix-shared'); + +const EXPECTED_BODY = { + content_repositories: [ + 'ssh://github.com/adobe/helix-cli.git#master', + 'ssh://github.com/adobe/helix-content.git#master', + ], + fastly_service_id: 'fake_name', + fastly_token: 'fake_auth', + github_token: 'github-token-foobar', +}; + + +describe('hlx publish --remote (default)', () => { + let scope; + let RemotePublishCommand; + let writeDictItem; + let purgeAll; + + beforeEach('Setting up Fake Server', function bef() { + this.timeout(5000); + writeDictItem = sinon.fake.resolves(true); + purgeAll = sinon.fake.resolves(true); + + RemotePublishCommand = proxyquire('../src/remotepublish.cmd', { + '@adobe/fastly-native-promises': () => ({ + transact: fn => fn(3), + writeDictItem, + purgeAll, + }), + }); + + // ensure to reset nock to avoid conflicts with PollyJS + nock.restore(); + nock.cleanAll(); + nock.activate(); + + scope = nock('https://adobeioruntime.net') + .post('/api/v1/web/helix/default/publish') + .reply(200, {}) + .post('/api/v1/web/helix/default/addlogger') + .reply(200, {}); + }); + + it('publishing update bot config', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + 'adobe/helix-cli#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'adobe', + repo: 'helix-cli', + installation_id: 863255, + id: 142561818, + config: { + version: '1.0', + branch: 'master', + caches: [ + { + fastlyServiceId: 'fake_name', + domains: [ + 'app-dev.project-helix.io', + 'app-tripod.project-helix.io', + ], + fastlyToken: '*', + }, + ], + }, + }, + 'adobe/helix-content#master': { + key: 'adobe/helix-content#master', + ref: 'master', + owner: 'adobe', + repo: 'helix-content', + installation_id: 863255, + id: 142561818, + config: { + version: '1.0', + branch: 'master', + caches: [ + { + fastlyServiceId: 'fake_name', + domains: [ + 'app-dev.project-helix.io', + 'app-tripod.project-helix.io', + ], + fastlyToken: '*', + }, + ], + }, + }, + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withConfigPurgeAPI('https://app.project-helix.io/config/purge') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.indexOf('adobe/helix-cli#master'); + assert.ok(idx0 > 0, 'update message'); + assert.ok(idx1 < 0, 'error message'); + assert.ok(idx2 < 0, 'bot setup message'); + assert.ok(idx3 > idx0); + scopeBot.done(); + }); + + it('publishing update bot config complains if no bot installed', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + 'adobe/helix-cli#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'tripodsan', + repo: 'hlxtest', + }, + 'adobe/helix-content#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'tripodsan', + repo: 'hlxtest', + }, + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.indexOf('adobe/helix-cli#master'); + assert.ok(idx0 < 0, 'update message'); + assert.ok(idx1 < 0, 'error message'); + assert.ok(idx2 > 0, 'bot setup message'); + assert.ok(idx3 > idx2); + scopeBot.done(); + }); + + it('publishing update bot config reports error if incomplete response', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.lastIndexOf('adobe/helix-cli#master'); + assert.ok(idx0 < 0, 'update message'); + assert.ok(idx1 > 0, 'error message'); + assert.ok(idx2 < 0, 'bot setup message'); + assert.ok(idx3 > idx1); + assert.ok(log.indexOf('Internal error: adobe/helix-cli#master should be in the service response') > 0); + scopeBot.done(); + }); + + it('publishing update bot config reports error if fastly service is not in response', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + 'adobe/helix-cli#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'tripodsan', + repo: 'hlxtest', + installation_id: 863255, + id: 142561818, + config: { + version: '1.0', + branch: 'master', + caches: [ + { + fastlyServiceId: 'anotherid', + fastlyToken: '*', + }, + ], + }, + }, + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.lastIndexOf('adobe/helix-cli#master'); + assert.ok(idx0 < 0, 'update message'); + assert.ok(idx1 > 0, 'error message'); + assert.ok(idx2 < 0, 'bot setup message'); + assert.ok(idx3 > idx1); + assert.ok(log.indexOf('Internal error: adobe/helix-cli#master status does have a configuration entry for given fastly service id.') > 0); + scopeBot.done(); + }); + + it('publishing update bot config reports error if fastly service has error', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + 'adobe/helix-cli#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'tripodsan', + repo: 'hlxtest', + installation_id: 863255, + id: 142561818, + }, + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.lastIndexOf('adobe/helix-cli#master'); + assert.ok(idx0 < 0, 'update message'); + assert.ok(idx1 > 0, 'error message'); + assert.ok(idx2 < 0, 'bot setup message'); + assert.ok(idx3 > idx1); + assert.ok(log.indexOf(' Internal error: adobe/helix-cli#master status does not have configuration details.') > 0); + scopeBot.done(); + }); + + it('publishing update bot config reports error if config is missing', async () => { + const scopeBot = nock('https://app.project-helix.io') + .post('/config/purge') + .reply((uri, requestBody) => { + assert.deepEqual(requestBody, EXPECTED_BODY); + return [ + 200, + JSON.stringify({ + 'adobe/helix-cli#master': { + key: 'adobe/helix-cli#master', + ref: 'master', + owner: 'tripodsan', + repo: 'hlxtest', + installation_id: 863255, + id: 142561818, + errors: [ + 'errors with config', + ], + }, + 'adobe/helix-content#master': { + key: 'adobe/helix-content#master', + ref: 'master', + owner: 'adobe', + repo: 'helix-content', + installation_id: 863255, + id: 142561818, + config: { + version: '1.0', + branch: 'master', + caches: [ + { + fastlyServiceId: 'fake_name', + errors: [ + '400 error with service', + ], + }, + ], + }, + }, + }), + { 'content-type': 'application/json' }, + ]; + }); + const logger = Logger.getTestLogger(); + const remote = await new RemotePublishCommand(logger) + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/deployed.yaml')) + .withUpdateBotConfig(true) + .withGithubToken('github-token-foobar') + .withDryRun(false); + await remote.run(); + + sinon.assert.calledTwice(writeDictItem); + sinon.assert.calledOnce(purgeAll); + + const log = await logger.getOutput(); + + const idx0 = log.indexOf('Updated the purge-configuration of the following repositories'); + const idx1 = log.indexOf('The purge-configuration of following repositories were not updated due to errors'); + const idx2 = log.indexOf('The following repositories are referenced by strains but don\'t have the helix-bot setup'); + const idx3 = log.lastIndexOf('adobe/helix-cli#master'); + assert.ok(idx0 < 0, 'update message'); + assert.ok(idx1 > 0, 'error message'); + assert.ok(idx2 < 0, 'bot setup message'); + assert.ok(idx3 > idx1); + assert.ok(log.indexOf('adobe/helix-cli#master update failed: errors with config') > 0); + assert.ok(log.indexOf('adobe/helix-content#master update failed for given fastly service id: 400 error with service') > 0); + scopeBot.done(); + }); + + afterEach(() => { + scope.done(); + nock.restore(); + }); +}); diff --git a/test/testRemotePublishCmd.js b/test/testRemotePublishCmd.js index ca0e65364..814becde2 100644 --- a/test/testRemotePublishCmd.js +++ b/test/testRemotePublishCmd.js @@ -18,12 +18,11 @@ const proxyquire = require('proxyquire'); const sinon = require('sinon'); describe('hlx publish --remote (default)', () => { - let scope; let RemotePublishCommand; let writeDictItem; let purgeAll; - before('Setting up Fake Server', function bef() { + beforeEach('Setting up Fake Server', function bef() { this.timeout(5000); writeDictItem = sinon.fake.resolves(true); purgeAll = sinon.fake.resolves(true); @@ -40,15 +39,15 @@ describe('hlx publish --remote (default)', () => { nock.restore(); nock.cleanAll(); nock.activate(); + }); - scope = nock('https://adobeioruntime.net') + it('publishing makes HTTP requests', async () => { + const scope = nock('https://adobeioruntime.net') .post('/api/v1/web/helix/default/publish') .reply(200, {}) .post('/api/v1/web/helix/default/addlogger') .reply(200, {}); - }); - it('publishing makes HTTP requests', async () => { const remote = await new RemotePublishCommand() .withWskAuth('fakeauth') .withWskNamespace('fakename') @@ -62,10 +61,27 @@ describe('hlx publish --remote (default)', () => { sinon.assert.calledTwice(writeDictItem); sinon.assert.calledOnce(purgeAll); - }); - after(() => { scope.done(); + }); + + it('publishing stops of no strains with package info', async () => { + const remote = await new RemotePublishCommand() + .withWskAuth('fakeauth') + .withWskNamespace('fakename') + .withFastlyAuth('fake_auth') + .withFastlyNamespace('fake_name') + .withWskHost('doesn.t.matter') + .withPublishAPI('https://adobeioruntime.net/api/v1/web/helix/default/publish') + .withConfigFile(path.resolve(__dirname, 'fixtures/non-deployed.yaml')) + .withDryRun(false); + await remote.run(); + + sinon.assert.notCalled(writeDictItem); + sinon.assert.notCalled(purgeAll); + }); + + afterEach(() => { nock.restore(); }); });