From c68e3571c92083e6efdb120bdd841cbb449b7b80 Mon Sep 17 00:00:00 2001 From: Sander Blue Date: Mon, 5 Aug 2024 15:31:52 -0500 Subject: [PATCH] feat(automation): report when NerdGraph API endpoints change signatures (i.e. breaking changes) --- .github/workflows/graphql-schema.yml | 2 +- scripts/schema-diff-reporter.js | 21 ++++ scripts/schema-diff.js | 77 ------------- scripts/schema-differ.js | 159 +++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 78 deletions(-) create mode 100755 scripts/schema-diff-reporter.js delete mode 100755 scripts/schema-diff.js create mode 100644 scripts/schema-differ.js diff --git a/.github/workflows/graphql-schema.yml b/.github/workflows/graphql-schema.yml index ca553930b..4f6864ce0 100644 --- a/.github/workflows/graphql-schema.yml +++ b/.github/workflows/graphql-schema.yml @@ -57,7 +57,7 @@ jobs: id: schema-diff with: script: | - const script = require('./scripts/schema-diff.js') + const script = require('./scripts/schema-diff-reporter.js') await script({core}) - name: Send report to Slack diff --git a/scripts/schema-diff-reporter.js b/scripts/schema-diff-reporter.js new file mode 100755 index 000000000..157fb9ec9 --- /dev/null +++ b/scripts/schema-diff-reporter.js @@ -0,0 +1,21 @@ +module.exports = async ({ + core +}) => { + const diff = require('./schema-differ'); + + core.setOutput('hero_mention', diff.heroMention); + core.setOutput('total_api_mutations_count', diff.schemaMutations.length); + core.setOutput('client_mutations_count', diff.clientMutations.length); + core.setOutput('client_mutations_missing_count', diff.clientMutationsDiff.length); + + core.setOutput('new_api_mutations', diff.newApiMutationsMsg); + core.setOutput('client_mutations_missing', diff.clientMutationsDiffMsg); + + await core.summary + .addHeading('New Relic Client Go | NerdGraph API Report') + .addRaw('Client mutations:') + .addList(diff.clientMutations) + .addRaw('Client is missing the following mutations:') + .addList(diff.clientMutationsDiff) + .write() +} diff --git a/scripts/schema-diff.js b/scripts/schema-diff.js deleted file mode 100755 index 4e9fcc5fd..000000000 --- a/scripts/schema-diff.js +++ /dev/null @@ -1,77 +0,0 @@ -module.exports = async ({ - core -}) => { - const fs = require('fs'); - const yaml = require('yaml'); - - let tutoneConfig = null; - let schemaOld = null; - let schemaLatest = null; - let heroMention = ""; - - try { - const tutoneConfigFile = fs.readFileSync('.tutone.yml', 'utf8') - tutoneConfig = yaml.parse(tutoneConfigFile) - - const schemaFileOld = fs.readFileSync('schema-old.json', 'utf8'); - schemaOld = JSON.parse(schemaFileOld); - - const schemaFileLatest = fs.readFileSync('schema.json', 'utf8'); - schemaLatest = JSON.parse(schemaFileLatest); - } catch (err) { - console.error(err); - } - - // Check for any newly added mutations - const endpointsOld = schemaOld.mutationType.fields.map(field => field.name); - const endpointsLatest = schemaLatest.mutationType.fields.map(field => field.name); - const endpointsDiff = endpointsLatest.filter(x => !endpointsOld.includes(x)); - - // Get the mutations the client has implemented - const clientMutations = tutoneConfig.packages.map(pkg => { - if (!pkg.mutations) { - return null; - } - - if (!pkg.mutations.length) { - return null; - } - - return pkg.mutations.map(m => m.name) - }).flat().reduce((acc, i) => i ? [...acc, i] : acc, []); - - // Check to see which mutations the client is missing - const schemaMutations = schemaLatest.mutationType.fields.map(field => field.name); - const clientMutationsDiff = schemaMutations.filter(x => !clientMutations.includes(x)); - - console.log('Client Mutations:', clientMutations); - console.log('Client is still missing the following mutations:\n', clientMutationsDiff); - - let newApiMutationsMsg = 'No new mutations since last check'; - if (endpointsDiff.length > 0) { - heroMention = '@hero'; - newApiMutationsMsg = `'${endpointsDiff.join('\n')}'`; - } - - let clientMutationsDiffMsg = '' - if (clientMutationsDiff.length > 0) { - clientMutationsDiffMsg = `'${clientMutationsDiff.join('\n')}'`; - } - - - core.setOutput('hero_mention', heroMention); - core.setOutput('total_api_mutations_count', schemaMutations.length); - core.setOutput('client_mutations_count', clientMutations.length); - core.setOutput('client_mutations_missing_count', clientMutationsDiff.length); - - core.setOutput('new_api_mutations', newApiMutationsMsg); - core.setOutput('client_mutations_missing', clientMutationsDiffMsg); - - await core.summary - .addHeading('New Relic Client Go | NerdGraph API Report') - .addRaw('Client mutations:') - .addList(clientMutations) - .addRaw('Client is missing the following mutations:') - .addList(clientMutationsDiff) - .write() -} diff --git a/scripts/schema-differ.js b/scripts/schema-differ.js new file mode 100644 index 000000000..6bf4e7638 --- /dev/null +++ b/scripts/schema-differ.js @@ -0,0 +1,159 @@ +const fs = require('fs'); +const yaml = require('yaml'); + +// This name must match the alias we created in the #oac-automation-reports Slack channel. +const heroAliasName = '@oac-automation-watchers'; + +let tutoneConfig = null; +let schemaOld = null; +let schemaLatest = null; +let heroMention = ""; + +try { + const tutoneConfigFile = fs.readFileSync('.tutone.yml', 'utf8') + tutoneConfig = yaml.parse(tutoneConfigFile) + + const schemaFileOld = fs.readFileSync('schema-old.json', 'utf8'); + schemaOld = JSON.parse(schemaFileOld); + + const schemaFileLatest = fs.readFileSync('schema.json', 'utf8'); + schemaLatest = JSON.parse(schemaFileLatest); +} catch (err) { + console.error(err); +} + +// Check for any newly added mutations +const endpointsOld = schemaOld.mutationType.fields.map(field => field.name); +const endpointsLatest = schemaLatest.mutationType.fields.map(field => field.name); +const endpointsDiff = endpointsLatest.filter(x => !endpointsOld.includes(x)); + +// Get the mutations the client has implemented +const clientMutations = tutoneConfig.packages.map(pkg => { + if (!pkg.mutations) { + return null; + } + + if (!pkg.mutations.length) { + return null; + } + + return pkg.mutations.map(m => m.name) +}).flat().reduce((acc, i) => i ? [...acc, i] : acc, []); + +const clientEndpointsSchemaOld = schemaOld.mutationType.fields.filter(field => clientMutations.includes(field.name)); +const clientEndpointsSchemaNew = schemaLatest.mutationType.fields.filter(field => clientMutations.includes(field.name)); + +// Check for changes in the mutations' signatures +const changedEndpoints = clientEndpointsSchemaNew.reduce((arr, field) => { + const oldMatch = clientEndpointsSchemaOld.find(f => f.name === field.name); + if (!oldMatch) { + return [...arr]; + } + + if (!oldMatch.args?.length && !field.args?.length) { + return [...arr]; + } + + const differences = compareArrays(oldMatch.args, field.args); + if (differences.length) { + return [...arr, { + name: field.name, + diff: differences, + }]; + } + + return [...arr]; +}, []); + +console.log(''); +console.log('New changedEndpoints', JSON.stringify(changedEndpoints, null, 2)); +console.log(''); + +// Check to see which mutations the client is missing +const schemaMutations = schemaLatest.mutationType.fields.map(field => field.name); +const clientMutationsDiff = schemaMutations.filter(x => !clientMutations.includes(x)); + +console.log(''); +console.log('Client is still missing the following API mutations:\n', clientMutationsDiff); +console.log(''); + +let newApiMutationsMsg = 'No new mutations since last check'; +if (endpointsDiff.length > 0) { + heroMention = heroAliasName; + newApiMutationsMsg = `'${endpointsDiff.join('\n')}'`; +} + +let clientMutationsDiffMsg = '' +if (clientMutationsDiff.length > 0) { + clientMutationsDiffMsg = `'${clientMutationsDiff.join('\n')}'`; +} + +function compareObjects(obj1, obj2, path = '') { + let differences = []; + + for (let key in obj1) { + if (obj1.hasOwnProperty(key) && obj2.hasOwnProperty(key)) { + if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { + differences = differences.concat(compareObjects(obj1[key], obj2[key], `${path}.${key}`)); + } else if (obj1[key] !== obj2[key]) { + differences.push({ + old: obj1[key], + new: obj2[key], + property: `${path}.${key}`, + }); + } + } else { + differences.push({ + old: obj1[key], + new: obj2[key] ? obj2[key] : undefined, + property: `${path}.${key}`, + }); + } + } + + for (let key in obj2) { + if (obj2.hasOwnProperty(key) && !obj1.hasOwnProperty(key)) { + differences.push({ + old: undefined, + new: obj2[key], + property: `${path}.${key}`, + }); + } + } + + return differences; +} + +function compareArrays(arr1, arr2) { + let differences = []; + + for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) { + if (arr1[i] && arr2[i]) { + differences = differences.concat(compareObjects(arr1[i], arr2[i], `[${i}]`)); + } else if (arr1[i]) { + differences.push({ + property: `[${i}]`, + old: arr1[i], + new: null, + }); + } else if (arr2[i]) { + differences.push({ + property: `[${i}]`, + old: null, + new: arr2[i] + }); + } + } + + return differences; +} + +module.exports = { + heroMention, + schemaMutations, + clientMutations, + clientMutationsDiff, + newApiMutationsMsg, + clientMutationsDiffMsg, + changedEndpoints, +};