From 362fe10a38de62e89e2a916c175b995ecd4706ae Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 22 Nov 2021 17:24:40 -0600 Subject: [PATCH 1/9] refactor: swap custom sleep for kit/sleep --- src/client/deployStrategies/containerDeploy.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client/deployStrategies/containerDeploy.ts b/src/client/deployStrategies/containerDeploy.ts index 95e4cb56d5..ccb67db4ab 100644 --- a/src/client/deployStrategies/containerDeploy.ts +++ b/src/client/deployStrategies/containerDeploy.ts @@ -5,6 +5,8 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { readFileSync } from 'graceful-fs'; +import { sleep } from '@salesforce/kit'; + import { deployTypes } from '../toolingApi'; import { DeployError } from '../../errors'; import { @@ -106,7 +108,7 @@ export class ContainerDeploy extends BaseDeploy { let containerStatus: ContainerAsyncRequest; do { if (count > 0) { - await this.sleep(100); + await sleep(100); } containerStatus = (await this.connection.tooling.retrieve( ContainerDeploy.CONTAINER_ASYNC_REQUEST, @@ -117,10 +119,6 @@ export class ContainerDeploy extends BaseDeploy { return containerStatus; } - private sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - private buildSourceDeployResult(containerRequest: ContainerAsyncRequest): SourceDeployResult { const componentDeployment: ComponentDeployment = { component: this.component, From 15ed4b176d361adfcac40091da5c06f8f645158f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 22 Nov 2021 17:25:52 -0600 Subject: [PATCH 2/9] test: update nonSupportedTypes --- src/registry/nonSupportedTypes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registry/nonSupportedTypes.ts b/src/registry/nonSupportedTypes.ts index 12f931a8e9..20f2d97aeb 100644 --- a/src/registry/nonSupportedTypes.ts +++ b/src/registry/nonSupportedTypes.ts @@ -26,8 +26,6 @@ export const metadataTypes = [ // things that don't show up in describe so far 'PicklistValue', // only existed in v37, so it's hard to describe! - 'FieldRestrictionRule', // not in describe for devorg. ScratchDef might need feature 'EMPLOYEEEXPERIENCE' but it doesn't say that - 'AppointmentSchedulingPolicy', // not in describe? 'AppointmentAssignmentPolicy', // not in describe? 'WorkflowFlowAction', // not in describe 'AdvAcctForecastDimSource', // not in describe From 5527d6a53349c5cc1e91c1d430f9c064b0d95473 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Nov 2021 10:09:28 -0600 Subject: [PATCH 3/9] refactor: use test1 coverage report schema --- src/registry/types.ts | 27 ++++++++++++++------------- test/utils/getMissingTypes.ts | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/registry/types.ts b/src/registry/types.ts index 596a9123a7..22f15d4525 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -178,20 +178,23 @@ export const enum TransformerStrategy { NonDecomposed = 'nonDecomposed', } +interface Channel { + exposed: boolean; +} /** * Subset of an item from the Metadata Coverage Report */ export interface CoverageObjectType { - scratchDefinitions: { - professional: string; - group: string; - enterprise: string; - developer: string; + orgShapes: { + developer: { + features?: string[]; + settings?: Record>; + }; }; channels: { - metadataApi: boolean; - sourceTracking: boolean; - toolingApi: boolean; + metadataApi: Channel; + sourceTracking: Channel; + toolingApi: Channel; }; } @@ -202,9 +205,7 @@ export interface CoverageObject { types: { [key: string]: CoverageObjectType; }; - versions: { - selected: number; - max: number; - min: number; - }; + // only exists on the test1 instances flavor of coverage report + apiVersion: number; + release: string; } diff --git a/test/utils/getMissingTypes.ts b/test/utils/getMissingTypes.ts index 46c1f690ca..a4c78e0441 100644 --- a/test/utils/getMissingTypes.ts +++ b/test/utils/getMissingTypes.ts @@ -14,7 +14,7 @@ export const getMissingTypes = ( ): Array<[string, CoverageObjectType]> => { const metadataApiTypesFromCoverage = Object.entries(metadataCoverage.types).filter( ([key, value]) => - value.channels.metadataApi && // if it's not in the mdapi, we don't worry about the registry + value.channels.metadataApi.exposed && // if it's not in the mdapi, we don't worry about the registry !metadataTypes.includes(key) && // types we should ignore, see the imported file for explanations !key.endsWith('Settings') && // individual settings shouldn't be in the registry !hasUnsupportedFeatures(value) // we don't support these types From 1bc80569bb5ef777a0e5013739a0ab7db24d9298 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Nov 2021 10:10:18 -0600 Subject: [PATCH 4/9] feat: metadata type preview --- package.json | 1 + scripts/update-registry/preview.ts | 23 +++++++++++++++++++++++ scripts/update-registry/update2.ts | 2 +- src/registry/nonSupportedTypes.ts | 16 +++++----------- 4 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 scripts/update-registry/preview.ts diff --git a/package.json b/package.json index 33fa66ebde..695ca7f86f 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "local:install": "./scripts/localInstall.js install", "local:link": "./scripts/localInstall.js link", "local:unlink": "./scripts/localInstall.js unlink", + "metadata:preview": "npx ts-node scripts/update-registry/preview.ts", "prepack": "sf-prepack", "pretest": "sf-compile-test", "repl": "node --inspect ./scripts/repl.js", diff --git a/scripts/update-registry/preview.ts b/scripts/update-registry/preview.ts new file mode 100644 index 0000000000..2b9ca33826 --- /dev/null +++ b/scripts/update-registry/preview.ts @@ -0,0 +1,23 @@ +import { CoverageObject } from '../../src/registry/types'; +import got from 'got'; +import { getMissingTypes } from '../../test/utils/getMissingTypes'; +import { registry } from '../../src'; + +(async () => { + const currentApiVersion = ( + JSON.parse((await got(`https://mdcoverage.secure.force.com/services/apexrest/report`)).body) as { + versions: { selected: number }; + } + ).versions.selected; + + const nextCoverage = JSON.parse( + (await got(`https://na${currentApiVersion - 8}.test1.pc-rnd.salesforce.com/mdcoverage/api.jsp`)).body + ) as CoverageObject; + + const missingTypes = getMissingTypes(nextCoverage, registry).map((type) => type[0]); + + console.log(`There are ${missingTypes.length} new types for v${nextCoverage.apiVersion} not in the registry.`); + console.log( + `${missingTypes.map((t) => `${t} (${nextCoverage.types[t].orgShapes.developer.features.join(';')})`).join('\n')}` + ); +})(); diff --git a/scripts/update-registry/update2.ts b/scripts/update-registry/update2.ts index d8c52faa23..47db58692c 100644 --- a/scripts/update-registry/update2.ts +++ b/scripts/update-registry/update2.ts @@ -103,7 +103,7 @@ const getMissingTypesAsDescribeResult = (missingTypes: [string, CoverageObjectTy const updateProjectScratchDef = (missingTypes: [string, CoverageObjectType][]) => { const scratchDefSummary = deepmerge.all( - [{}].concat(missingTypes.map(([key, missingType]) => JSON.parse(missingType.scratchDefinitions.developer))) + [{}].concat(missingTypes.map(([key, missingType]) => missingType.orgShapes.developer)) ) as { features: string[]; }; diff --git a/src/registry/nonSupportedTypes.ts b/src/registry/nonSupportedTypes.ts index 20f2d97aeb..5f6b7f1b01 100644 --- a/src/registry/nonSupportedTypes.ts +++ b/src/registry/nonSupportedTypes.ts @@ -38,21 +38,15 @@ export const metadataTypes = [ ]; export const hasUnsupportedFeatures = (type: CoverageObjectType): boolean => { - if (!type.scratchDefinitions?.developer) { + if (!type.orgShapes?.developer) { return true; } - const scratchDef = JSON.parse(type.scratchDefinitions.developer) as { - features?: string[]; - settings?: { - [key: string]: unknown; - }; - }; + if ( - scratchDef.features && - scratchDef.features.length > 0 && - features.some((feature) => scratchDef.features.includes(feature)) + type.orgShapes.developer.features?.length && + features.some((feature) => type.orgShapes?.developer.features.includes(feature)) ) { return true; } - return scratchDef.settings && settings.some((setting) => scratchDef.settings[setting]); + return type.orgShapes?.developer.settings && settings.some((setting) => type.orgShapes?.developer.settings[setting]); }; From b181f1f1f5aa81a2f337fc4e926d21453dcf6721 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Nov 2021 12:28:01 -0600 Subject: [PATCH 5/9] feat: ci and slack integration --- .circleci/config.yml | 24 ++++++++++++++ scripts/update-registry/preview.ts | 51 ++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb9fd617bb..e903678ef7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,6 +13,16 @@ jobs: - run: yarn build - run: yarn test:registry + registry-check-preview: + description: Checks registry against next version of metadataCoverageReport, reporting results to slack + docker: + - image: node:lts + steps: + - checkout + - run: yarn install + - run: yarn build + - run: yarn metadata:preview + external-nut: description: Runs NUTs from other (external) repos by cloning them. Substitutes a dependency for the current pull request. For example, you're testing a PR to a library and want to test a plugin in another repo that uses the library. @@ -102,8 +112,22 @@ jobs: echo "Environment Variables:" env NODE_OPTIONS=--max-old-space-size=8192 yarn test:nuts + workflows: version: 2 + registry-check-preview: + triggers: + - schedule: + # weekly on Monday morning + cron: 30 3 * * 1 + filters: + branches: + only: + - main + jobs: + - registry-check-preview: + # required for slack webhook + context: salesforce-cli registry-check: triggers: - schedule: diff --git a/scripts/update-registry/preview.ts b/scripts/update-registry/preview.ts index 2b9ca33826..f5cdb90dab 100644 --- a/scripts/update-registry/preview.ts +++ b/scripts/update-registry/preview.ts @@ -17,7 +17,52 @@ import { registry } from '../../src'; const missingTypes = getMissingTypes(nextCoverage, registry).map((type) => type[0]); console.log(`There are ${missingTypes.length} new types for v${nextCoverage.apiVersion} not in the registry.`); - console.log( - `${missingTypes.map((t) => `${t} (${nextCoverage.types[t].orgShapes.developer.features.join(';')})`).join('\n')}` - ); + // console.log( + // `${missingTypes.map((t) => `${t} (${nextCoverage.types[t].orgShapes.developer.features.join(';')})`).join('\n')}` + // ); + + const typesByFeature = new Map(); + missingTypes.map((t) => { + const featureLabel = nextCoverage.types[t].orgShapes.developer.features?.join(' & ') ?? 'NO FEATURE REQUIRED'; + typesByFeature.set(featureLabel, [...(typesByFeature.get(featureLabel) ?? []), t]); + }); + console.log(typesByFeature); + const formattedTypes: string[] = []; + typesByFeature.forEach((types, feature) => { + formattedTypes.push(`*${feature}*\n - ${types.join(', ')}`); + }); + + const json = { + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: `v${nextCoverage.apiVersion} Metadata Preview`, + }, + }, + { + type: 'section', + text: { + type: 'plain_text', + text: `There are ${missingTypes.length} new types not in the registry, organized by required features (if any). Slack can show a max of 50.`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: formattedTypes.join('\n\n'), + }, + }, + ], + }; + // console.log(JSON.stringify(json, null, 2)); + try { + await got.post(process.env.DEFAULT_SLACK_WEBHOOK, { + json, + }); + } catch (e) { + console.error(e); + } })(); From 54e8f245f5c6aa70982f834b9d07f6feb560dd69 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Nov 2021 12:41:28 -0600 Subject: [PATCH 6/9] refactor: cleaner code --- scripts/update-registry/preview.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/update-registry/preview.ts b/scripts/update-registry/preview.ts index f5cdb90dab..a1f447646a 100644 --- a/scripts/update-registry/preview.ts +++ b/scripts/update-registry/preview.ts @@ -17,9 +17,6 @@ import { registry } from '../../src'; const missingTypes = getMissingTypes(nextCoverage, registry).map((type) => type[0]); console.log(`There are ${missingTypes.length} new types for v${nextCoverage.apiVersion} not in the registry.`); - // console.log( - // `${missingTypes.map((t) => `${t} (${nextCoverage.types[t].orgShapes.developer.features.join(';')})`).join('\n')}` - // ); const typesByFeature = new Map(); missingTypes.map((t) => { @@ -27,10 +24,7 @@ import { registry } from '../../src'; typesByFeature.set(featureLabel, [...(typesByFeature.get(featureLabel) ?? []), t]); }); console.log(typesByFeature); - const formattedTypes: string[] = []; - typesByFeature.forEach((types, feature) => { - formattedTypes.push(`*${feature}*\n - ${types.join(', ')}`); - }); + const formattedTypes = Array.from(typesByFeature, ([feature, types]) => `*${feature}*\n - ${types.join(', ')}`); const json = { blocks: [ @@ -45,7 +39,7 @@ import { registry } from '../../src'; type: 'section', text: { type: 'plain_text', - text: `There are ${missingTypes.length} new types not in the registry, organized by required features (if any). Slack can show a max of 50.`, + text: `There are ${missingTypes.length} new types not in the registry, organized by required features (if any).`, }, }, { From 0905ac40d979448ad44665b6dccd6693f9fd4083 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 24 Nov 2021 12:54:20 -0600 Subject: [PATCH 7/9] style: pr feedback --- scripts/update-registry/preview.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/update-registry/preview.ts b/scripts/update-registry/preview.ts index a1f447646a..b4f3e22740 100644 --- a/scripts/update-registry/preview.ts +++ b/scripts/update-registry/preview.ts @@ -51,7 +51,6 @@ import { registry } from '../../src'; }, ], }; - // console.log(JSON.stringify(json, null, 2)); try { await got.post(process.env.DEFAULT_SLACK_WEBHOOK, { json, From f98b9031b2e9dca6e749ca0551b19e9ecb19c3f7 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 24 Nov 2021 12:55:43 -0600 Subject: [PATCH 8/9] test: more registry validations --- test/registry/registryValidation.test.ts | 106 ++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/test/registry/registryValidation.test.ts b/test/registry/registryValidation.test.ts index f85f2776ab..177c0dacfd 100644 --- a/test/registry/registryValidation.test.ts +++ b/test/registry/registryValidation.test.ts @@ -7,7 +7,7 @@ import { expect } from 'chai'; import { MetadataRegistry } from '../../src'; import { registry as defaultRegistry } from '../../src/registry/registry'; -import { MetadataType } from '../../src/registry/types'; +import { MetadataType, TransformerStrategy, DecompositionStrategy } from '../../src/registry/types'; describe('Registry Validation', () => { const registry = defaultRegistry as MetadataRegistry; @@ -194,4 +194,108 @@ describe('Registry Validation', () => { }); }); }); + + describe('top level required properties', () => { + describe('all have names and directoryName', () => { + Object.entries(registry.types).forEach(([key, type]) => { + it(`${type.id} has a name`, () => { + expect(type.name).to.be.a('string'); + }); + it(`${type.id} has a directoryName`, () => { + expect(type.directoryName).to.be.a('string'); + }); + }); + }); + }); + + describe('valid strategies', () => { + const typesWithStrategies = Object.values(registry.types).filter((type) => type.strategies); + + // there isn't an enum for this in Types, to the known are hardcoded here + describe('valid, known adapters', () => { + typesWithStrategies.forEach((type) => { + it(`${type.id} has a valid adapter`, () => { + expect(['default', 'mixedContent', 'bundle', 'matchingContentFile', 'decomposed']).includes( + type.strategies.adapter + ); + }); + }); + }); + + describe('adapter = matchingContentFile => no other strategy properties', () => { + typesWithStrategies + .filter((t) => t.strategies.adapter === 'matchingContentFile') + .forEach((type) => { + it(`${type.id} has no other strategy properties`, () => { + expect(type.strategies.decomposition).to.be.undefined; + expect(type.strategies.recomposition).to.be.undefined; + expect(type.strategies.transformer).to.be.undefined; + }); + }); + }); + + describe('adapter = bundle => no other strategy properties', () => { + typesWithStrategies + .filter((t) => t.strategies.adapter === 'bundle') + .forEach((type) => { + it(`${type.id} has no other strategy properties`, () => { + expect(type.strategies.decomposition).to.be.undefined; + expect(type.strategies.recomposition).to.be.undefined; + expect(type.strategies.transformer).to.be.undefined; + }); + }); + }); + + describe('adapter = decomposed => has transformer and decomposition props', () => { + typesWithStrategies + .filter((t) => t.strategies.adapter === 'decomposed') + .forEach((type) => { + it(`${type.id} has expected properties`, () => { + expect(type.strategies.decomposition).to.be.a('string'); + expect( + [DecompositionStrategy.FolderPerType.valueOf(), DecompositionStrategy.TopLevel.valueOf()].includes( + type.strategies.decomposition + ) + ).to.be.true; + expect(type.strategies.transformer).to.be.a('string'); + expect( + [ + TransformerStrategy.Standard.valueOf(), + TransformerStrategy.Decomposed.valueOf(), + TransformerStrategy.StaticResource.valueOf(), + TransformerStrategy.NonDecomposed.valueOf(), + ].includes(type.strategies.transformer) + ).to.be.true; + expect(type.strategies.recomposition).to.be.undefined; + }); + }); + }); + it('no standard types specified in registry', () => { + expect(typesWithStrategies.filter((t) => t.strategies.transformer === 'standard')).to.have.length(0); + }); + describe('adapter = mixedContent => has no decomposition/recomposition props', () => { + typesWithStrategies + .filter((t) => t.strategies.adapter === 'mixedContent') + .forEach((type) => { + it(`${type.id} has expected properties`, () => { + expect(type.strategies.decomposition).to.be.undefined; + expect(type.strategies.recomposition).to.be.undefined; + type.strategies.transformer + ? expect(type.strategies.transformer).to.be.a('string') + : expect(type.strategies.transformer).to.be.undefined; + }); + }); + }); + }); + + describe('folders', () => { + const folderTypes = Object.values(registry.types).filter((type) => type.inFolder); + + folderTypes.forEach((type) => { + it(`${type.name} has a valid folderType in the registry`, () => { + expect(type.folderType).to.not.be.undefined; + expect(registry.types[type.folderType]).to.be.an('object'); + }); + }); + }); }); From 5da8ae98927183a393da7f25190103e8c09ff250 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 30 Nov 2021 14:09:03 -0600 Subject: [PATCH 9/9] fix: slack message formatting --- scripts/update-registry/preview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-registry/preview.ts b/scripts/update-registry/preview.ts index b4f3e22740..0914809b94 100644 --- a/scripts/update-registry/preview.ts +++ b/scripts/update-registry/preview.ts @@ -24,7 +24,7 @@ import { registry } from '../../src'; typesByFeature.set(featureLabel, [...(typesByFeature.get(featureLabel) ?? []), t]); }); console.log(typesByFeature); - const formattedTypes = Array.from(typesByFeature, ([feature, types]) => `*${feature}*\n - ${types.join(', ')}`); + const formattedTypes = Array.from(typesByFeature, ([feature, types]) => `*${feature}*\n - ${types.join('\n - ')}`); const json = { blocks: [