diff --git a/packages/cli-build/README.md b/packages/cli-build/README.md index 230f90c73..2d1fc4a53 100644 --- a/packages/cli-build/README.md +++ b/packages/cli-build/README.md @@ -86,24 +86,26 @@ Usage: $ percy build:approve [options] Arguments: - build-id Build ID to approve + build-id Build ID to approve Options: - --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env - var) - --access-key Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY - env var) + --username Username for authentication (can also be set via + BROWSERSTACK_USERNAME env var) + --access-key Access key for authentication (can also be set via + BROWSERSTACK_ACCESS_KEY env var) + --pass-if-previously-approved Does not exit with an error if the build has previous approvals Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -l, --labels Associates labels to the build (ex: --labels=dev,prod ) - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help Examples: $ percy build:approve $ percy build:approve --username username --access-key **key** + $ percy build:approve --pass-if-previously-approved ``` ### `percy build:unapprove` diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index afcd95495..25ac0f961 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -7,7 +7,20 @@ import { fetchCredentials, reviewCommandConfig } from './utils.js'; */ export const approve = command('approve', { description: 'Approve Percy builds', - ...reviewCommandConfig + ...reviewCommandConfig, + ...{ + flags: [ + ...reviewCommandConfig.flags, + { + name: 'pass-if-previously-approved', + description: 'Does not exit with an error if the build has previous approvals' + } + ], + examples: [ + ...reviewCommandConfig.examples, + '$0 --pass-if-previously-approved' + ] + } }, async ({ flags, args, percy, log, exit }) => { // Early return if Percy is disabled if (!percy) { @@ -38,6 +51,15 @@ export const approve = command('approve', { log.info(`Build ${args.buildId} approved successfully!`); log.info(`Approved by: ${approvedBy.user_name} (${approvedBy.user_email})`); } catch (error) { + if ( + flags.passIfPreviouslyApproved && + Array.isArray(error?.response?.body?.errors) && + error.response.body.errors.some(e => e.detail.toLowerCase().includes('approve action is already performed on this build')) + ) { + log.info(`Build ${args.buildId} is already approved: skipping approval`); + exit(0); + } + log.error(`Failed to approve build ${args.buildId}`); log.error(error); diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index fc91b363a..93f366789 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -309,4 +309,36 @@ describe('percy build:approve', () => { '[percy] Approved by: temp-username (unknown@example.com)' ]); }); + + it('logs an error when build approval fails with 409 Conflict', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => [409, { errors: [{ detail: 'approve action is already performed on this build' }] }]); + + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Failed to approve build 123', + '[percy] Error: approve action is already performed on this build', + '[percy] Error: Failed to approve the build' + ]); + }); + + it('does not error when build is previously approved and --pass-if-previously-approved flag is used', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => [409, { errors: [{ detail: 'approve action is already performed on this build' }] }]); + + await expectAsync(approve(['123', '--pass-if-previously-approved'])).toBeResolved(); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build 123...', + '[percy] Build 123 is already approved: skipping approval' + ]); + }); });