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
This commit is contained in:
George Kalpakas 2021-10-29 14:17:12 +03:00 committed by Alex Rickabaugh
parent 3894cef78e
commit 80d8eb87bb
2 changed files with 255 additions and 0 deletions

View file

@ -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 = '<no 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 || '<no 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.
//

View file

@ -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), <no name> (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 \'<no name>\' 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 \'<no name>\' 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();
});
});