From 80d8eb87bb5e7680bec94ee57a7a2b4f44fec651 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Fri, 29 Oct 2021 14:17:12 +0300 Subject: [PATCH] build(docs-infra): validate computed deployments in `deploy-to-firebase` script (#43963) The `deploy-to-firebase` script might have to deploy a single AIO build to multiple projects/sites (potentially with small tweaks between each). This commit adds a step to validate the computed deployments to ensure they are compatible with each other (for example, that there is exactly one primary deployment that builds the app and sets the theme/mode and that all secondary deployments are compatible with the primary one). PR Close #43963 --- aio/scripts/deploy-to-firebase/index.js | 81 +++++++++ aio/scripts/deploy-to-firebase/index.spec.js | 174 +++++++++++++++++++ 2 files changed, 255 insertions(+) diff --git a/aio/scripts/deploy-to-firebase/index.js b/aio/scripts/deploy-to-firebase/index.js index 7b4f2d01e9d..3931bf7f993 100644 --- a/aio/scripts/deploy-to-firebase/index.js +++ b/aio/scripts/deploy-to-firebase/index.js @@ -21,6 +21,8 @@ module.exports = { computeMajorVersion, getLatestCommit, getMostRecentMinorBranch, + skipDeployment, + validateDeploymentsInfo, }; // Run @@ -30,6 +32,8 @@ if (require.main === module) { const deploymentsInfo = computeDeploymentsInfo(inputVars); const totalDeployments = deploymentsInfo.length; + validateDeploymentsInfo(deploymentsInfo); + console.log(`Deployments (${totalDeployments}): ${listDeployTargetNames(deploymentsInfo)}`); deploymentsInfo.forEach((deploymentInfo, idx) => { @@ -392,6 +396,83 @@ function testPwaScore({deployedUrl, minPwaScore}) { yarn(`test-pwa-score "${deployedUrl}" "${minPwaScore}"`); } +function validateDeploymentsInfo(deploymentsList) { + const knownTargetTypes = ['primary', 'secondary', 'skipped']; + const requiredPropertiesForSkipped = ['name', 'type', 'reason']; + const requiredPropertiesForNonSkipped = [ + 'name', 'type', 'deployEnv', 'projectId', 'siteId', 'deployedUrl', 'preDeployActions', + 'postDeployActions', + ]; + + const primaryTargets = deploymentsList.filter(({type}) => type === 'primary'); + const secondaryTargets = deploymentsList.filter(({type}) => type === 'secondary'); + const skippedTargets = deploymentsList.filter(({type}) => type === 'skipped'); + const otherTargets = deploymentsList.filter(({type}) => !knownTargetTypes.includes(type)); + + // Check that all targets have a known `type`. + if (otherTargets.length > 0) { + throw new Error( + `Expected all deploy targets to have a type of ${knownTargetTypes.join(' or ')}, but ` + + `found ${otherTargets.length} targets with an unknown type: ` + + otherTargets.map(({name = '', type}) => `${name} (type: ${type})`).join(', ')); + } + + // Check that all targets have the required properties. + for (const target of deploymentsList) { + const requiredProperties = (target.type === 'skipped') ? + requiredPropertiesForSkipped : requiredPropertiesForNonSkipped; + const missingProperties = requiredProperties.filter(prop => target[prop] === undefined); + + if (missingProperties.length > 0) { + throw new Error( + `Expected deploy target '${target.name || ''}' to have all required ` + + `properties, but it is missing '${missingProperties.join('\', \'')}'.`); + } + } + + // If there are skipped targets... + if (skippedTargets.length > 0) { + // ...check that exactly one target has been specified. + if (deploymentsList.length > 1) { + throw new Error( + `Expected a single skipped deploy target, but found ${deploymentsList.length} targets ` + + `in total: ${listDeployTargetNames(deploymentsList)}`); + } + + // There is only one skipped deploy target and it is valid (i.e. has all required properties). + return; + } + + // Check that exactly one primary target has been specified. + if (primaryTargets.length !== 1) { + throw new Error( + `Expected exactly one primary deploy target, but found ${primaryTargets.length}: ` + + listDeployTargetNames(primaryTargets)); + } + + const primaryTarget = primaryTargets[0]; + const primaryIndex = deploymentsList.indexOf(primaryTarget); + + // Check that the primary target is the first item in the list. + if (primaryIndex !== 0) { + throw new Error( + `Expected the primary target (${primaryTarget.name}) to be the first item in the deploy ` + + `target list, but it was found at index ${primaryIndex} (0-based): ` + + listDeployTargetNames(deploymentsList)); + } + + const nonMatchingSecondaryTargets = + secondaryTargets.filter(({deployEnv}) => deployEnv !== primaryTarget.deployEnv); + + // Check that all secondary targets (if any) match the primary target's `deployEnv`. + if (nonMatchingSecondaryTargets.length > 0) { + throw new Error( + 'Expected all secondary deploy targets to match the primary target\'s `deployEnv` ' + + `(${primaryTarget.deployEnv}), but ${nonMatchingSecondaryTargets.length} targets do not: ` + + nonMatchingSecondaryTargets.map(t => `${t.name} (deployEnv: ${t.deployEnv})`).join(', ')); + } +} + function yarn(cmd) { // Using `--silent` to ensure no secret env variables are printed. // diff --git a/aio/scripts/deploy-to-firebase/index.spec.js b/aio/scripts/deploy-to-firebase/index.spec.js index 1e0fe765694..f0f474b34fd 100644 --- a/aio/scripts/deploy-to-firebase/index.spec.js +++ b/aio/scripts/deploy-to-firebase/index.spec.js @@ -8,6 +8,8 @@ const { computeMajorVersion, getLatestCommit, getMostRecentMinorBranch, + skipDeployment, + validateDeploymentsInfo, } = require('./index'); @@ -29,6 +31,7 @@ describe('deploy-to-firebase:', () => { (typeof val === 'function') ? `function:${val.name}` : val; const getDeploymentsInfoFor = env => { const deploymentsInfo = computeDeploymentsInfo(computeInputVars(env)); + validateDeploymentsInfo(deploymentsInfo); return JSON.parse(JSON.stringify(deploymentsInfo, jsonFunctionReplacer)); }; @@ -448,3 +451,174 @@ describe('deploy-to-firebase:', () => { ' https://next-angular-io-site.web.app/'); }); }); + +describe('validateDeploymentsInfo()', () => { + const createTarget = (name, type) => ({ + name, + type, + deployEnv: 'deployEnv', + projectId: 'projectId', + siteId: 'siteId', + deployedUrl: 'deployedUrl', + preDeployActions: [], + postDeployActions: [], + }); + + it('should error if there are deploy targets with unknown types', () => { + const targets = [ + createTarget('target-1', 'primary'), + createTarget('target-2', 'tertiary'), + createTarget('target-3', 'secondary'), + createTarget(undefined, 'other'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected all deploy targets to have a type of primary or secondary or skipped, but ' + + 'found 2 targets with an unknown type: target-2 (type: tertiary), (type: other)'); + }); + + it('should error if there are non-skipped targets missing required properties', () => { + // With target missing `name`. + const targets1 = [ + createTarget('target-1', 'primary'), + createTarget(undefined, 'secondary'), + ]; + + expect(() => validateDeploymentsInfo(targets1)).toThrowError( + 'Expected deploy target \'\' to have all required properties, but it is missing ' + + '\'name\'.'); + + // With target missing multiple properties. + const targets2 = [ + createTarget('target-1', 'primary'), + { + ...createTarget('target-2', 'secondary'), + deployEnv: undefined, + postDeployActions: undefined, + }, + ]; + + expect(() => validateDeploymentsInfo(targets2)).toThrowError( + 'Expected deploy target \'target-2\' to have all required properties, but it is missing ' + + '\'deployEnv\', \'postDeployActions\'.'); + }); + + it('should error if there are skipped targets missing required properties', () => { + // With target missing `name`. + const targets1 = [ + createTarget('target-1', 'primary'), + {...skipDeployment('just because'), name: undefined}, + ]; + + expect(() => validateDeploymentsInfo(targets1)).toThrowError( + 'Expected deploy target \'\' to have all required properties, but it is missing ' + + '\'name\'.'); + + // With target missing `reason`. + const targets2 = [ + createTarget('target-1', 'primary'), + skipDeployment(undefined), + ]; + + expect(() => validateDeploymentsInfo(targets2)).toThrowError( + 'Expected deploy target \'skipped\' to have all required properties, but it is missing ' + + '\'reason\'.'); + }); + + it('should error if there are both skipped and non-skipped targets', () => { + const targets = [ + skipDeployment('just because'), + createTarget('target-2', 'secondary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected a single skipped deploy target, but found 2 targets in total: skipped, target-2'); + }); + + it('should error if there are multiple skipped targets', () => { + const targets = [ + skipDeployment('just because'), + skipDeployment('because why not'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected a single skipped deploy target, but found 2 targets in total: skipped, skipped'); + }); + + it('should error if there is no primary target', () => { + const targets = [ + createTarget('target-1', 'secondary'), + createTarget('target-2', 'secondary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected exactly one primary deploy target, but found 0: -'); + }); + + it('should error if there is more than one primary target', () => { + const targets = [ + createTarget('target-1', 'primary'), + createTarget('target-2', 'secondary'), + createTarget('target-3', 'primary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected exactly one primary deploy target, but found 2: target-1, target-3'); + }); + + it('should error if the primary target is not the first item in the list', () => { + const targets = [ + createTarget('target-1', 'secondary'), + createTarget('target-2', 'primary'), + createTarget('target-3', 'secondary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected the primary target (target-2) to be the first item in the deploy target list, ' + + 'but it was found at index 1 (0-based): target-1, target-2, target-3'); + }); + + it('should error if there are secondary targets with a different `deployEnv` than primary', + () => { + const targets = [ + {...createTarget('target-1', 'primary'), deployEnv: 'deploy-env-1'}, + {...createTarget('target-2', 'secondary'), deployEnv: 'deploy-env-1'}, + {...createTarget('target-3', 'secondary'), deployEnv: 'deploy-env-2'}, + {...createTarget('target-4', 'secondary'), deployEnv: 'deploy-env-1'}, + {...createTarget('target-5', 'secondary'), deployEnv: 'deploy-env-2'}, + {...createTarget('target-6', 'secondary'), deployEnv: 'deploy-env-3'}, + ]; + + expect(() => validateDeploymentsInfo(targets)).toThrowError( + 'Expected all secondary deploy targets to match the primary target\'s `deployEnv` ' + + '(deploy-env-1), but 3 targets do not: target-3 (deployEnv: deploy-env-2), target-5 ' + + '(deployEnv: deploy-env-2), target-6 (deployEnv: deploy-env-3)'); + }); + + it('should succeed with a valid skipped target', () => { + const targets = [ + skipDeployment('due to valid reasons'), + ]; + + expect(() => validateDeploymentsInfo(targets)).not.toThrow(); + }); + + it('should succeed with a valid non-skipped target', () => { + const targets = [ + createTarget('target-1', 'primary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).not.toThrow(); + }); + + it('should succeed with multiple valid non-skipped targets', () => { + const targets = [ + createTarget('target-1', 'primary'), + createTarget('target-2', 'secondary'), + createTarget('target-3', 'secondary'), + createTarget('target-4', 'secondary'), + ]; + + expect(() => validateDeploymentsInfo(targets)).not.toThrow(); + }); +});