1
0
mirror of synced 2025-12-20 10:28:40 -05:00
Files
docs/script/graphql/build-changelog.js
2020-11-23 08:17:57 -05:00

301 lines
11 KiB
JavaScript

const { diff, ChangeType } = require('@graphql-inspector/core')
const { loadSchema } = require('@graphql-tools/load')
const git = require('../../lib/git-utils')
const fs = require('fs')
const yaml = require('js-yaml')
// check for required PAT
if (!process.env.GITHUB_TOKEN) {
console.error('Error! You must have a GITHUB_TOKEN set in an .env file to run this script.')
process.exit(1)
}
// main()
async function main() {
// Load the previous schema from this repo
// TODO -- how to make sure that this script runs _before_ this artifact is updated?
// Maybe hook into the existing `update-files` script instead of being a stand-alone script.
const oldSchemaString = fs.readFileSync('data/graphql/schema.docs.graphql').toString()
// Load the latest schema from github/github
const tree = await git.getTree('github', 'github', 'heads/master')
const schemaFileBlob = tree.find(entry => entry.path.includes('config/schema.docs.graphql') && entry.type === 'blob')
const newSchemaBuffer = await git.getContentsForBlob('github', 'github', schemaFileBlob)
const previewsString = fs.readFileSync('data/graphql/graphql_previews.yml')
const previews = yaml.safeLoad(previewsString)
// TODO how to make sure to get these before the file is updated?
const oldUpcomingChangesString = fs.readFileSync('data/graphql/graphql_upcoming_changes_public.yml')
const oldUpcomingChanges = yaml.safeLoad(oldUpcomingChangesString).upcoming_changes
// TODO actually get different changes here
const newUpcomingChanges = oldUpcomingChanges
const changelogEntry = createChangelogEntry(oldSchemaString, newSchemaBuffer.toString(), previews, oldUpcomingChanges, newUpcomingChanges)
if (changelogEntry) {
prependDatedEntry(changelogEntry, 'lib/graphql/static/changelog.json')
}
}
/**
* Tag `changelogEntry` with `date: YYYY-mm-dd`, then prepend it to the JSON
* structure written to `targetPath`. (`changelogEntry` and that file are modified in place.)
* @param {object} changelogEntry
* @param {string} targetPath
* @return {void}
*/
function prependDatedEntry(changelogEntry, targetPath) {
// Build a `yyyy-mm-dd`-formatted date string
// and tag the changelog entry with it
const today = new Date()
const todayString = String(today.getFullYear()) + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0')
changelogEntry.date = todayString
const previousChangelogString = fs.readFileSync(targetPath)
const previousChangelog = JSON.parse(previousChangelogString)
// add a new entry to the changelog data
previousChangelog.unshift(changelogEntry)
// rewrite the updated changelog
fs.writeFileSync(targetPath, JSON.stringify(previousChangelog, null, 2))
}
/**
* Compare `oldSchemaString` to `newSchemaString`, and if there are any
* changes that warrant a changelog entry, return a changelog entry.
* Based on the parsed `previews`, identify changes that are under a preview.
* Otherwise, return null.
* @param {string} [oldSchemaString]
* @param {string} [newSchemaString]
* @param {Array<object>} [previews]
* @param {Array<object>} [oldUpcomingChanges]
* @param {Array<object>} [newUpcomingChanges]
* @return {object?}
*/
async function createChangelogEntry(oldSchemaString, newSchemaString, previews, oldUpcomingChanges, newUpcomingChanges) {
// Create schema objects out of the strings
const oldSchema = await loadSchema(oldSchemaString)
const newSchema = await loadSchema(newSchemaString)
// Generate changes between the two schemas
const changes = diff(oldSchema, newSchema)
const changesToReport = []
changes.forEach(function (change) {
if (CHANGES_TO_REPORT.includes(change.type)) {
changesToReport.push(change)
} else if (CHANGES_TO_IGNORE.includes(change.type)) {
// Do nothing
} else {
throw "This change type should be added to CHANGES_TO_REPORT or CHANGES_TO_IGNORE: " + change.type
}
})
const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(changesToReport, previews)
const addedUpcomingChanges = newUpcomingChanges.filter(function (change) {
// Manually check each of `newUpcomingChanges` for an equivalent entry
// in `oldUpcomingChanges`.
return !oldUpcomingChanges.find(function (oldChange) {
return (oldChange.location == change.location &&
oldChange.date == change.date &&
oldChange.description == change.description
)
})
})
// If there were any changes, create a changelog entry
if (schemaChangesToReport.length > 0 || previewChangesToReport.length > 0 || addedUpcomingChanges.length > 0) {
const changelogEntry = {
schemaChanges: [],
previewChanges: [],
upcomingChanges: [],
}
const schemaChange = {
title: 'The GraphQL schema includes these changes:',
// Replace single quotes which wrap field/argument/type names with backticks
changes: cleanMessagesFromChanges(schemaChangesToReport)
}
changelogEntry.schemaChanges.push(schemaChange)
for (const previewTitle in previewChangesToReport) {
let previewChanges = previewChangesToReport[previewTitle]
let cleanTitle = cleanPreviewTitle(previewTitle)
let entryTitle = "The [" + cleanTitle + "](/graphql/overview/schema-previews#" + previewAnchor(cleanTitle) + ") includes these changes:"
changelogEntry.previewChanges.push({
title: entryTitle,
changes: cleanMessagesFromChanges(previewChanges.changes),
})
}
if (addedUpcomingChanges.length > 0) {
changelogEntry.upcomingChanges.push({
title: "The following changes will be made to the schema:",
changes: addedUpcomingChanges.map(function (change) {
const location = change.location
const description = change.description
const date = change.date.split("T")[0]
return "On member `" + location + "`:" + description + " **Effective " + date + "**."
})
})
}
return changelogEntry
} else {
return null
}
}
/**
* Prepare the preview title from github/github source for the docs.
* (ported from build-changelog-from-markdown)
* @param {string} title
* @return {string}
*/
function cleanPreviewTitle(title) {
if (title == "UpdateRefsPreview") {
title = "Update refs preview"
} else if (title == "MergeInfoPreview") {
title = "Merge info preview"
} else if (!title.endsWith("preview")) {
title = title + " preview"
}
return title
}
/**
* Turn the given title into an HTML-ready anchor.
* (ported from https://github.com/github/graphql-docs/blob/master/lib/graphql_docs/update_internal_developer/change_log.rb#L281)
* @param {string} [previewTitle]
* @return {string}
*/
function previewAnchor(previewTitle) {
return previewTitle
.toLowerCase()
.replace(/ /g, '-')
.replace(/[^\w-]/g, '')
}
/**
* Turn changes from graphql-inspector into messages for the HTML changelog.
* @param {Array<object>} changes
* @return {Array<string>}
*/
function cleanMessagesFromChanges(changes) {
return changes.map(function (change) {
// replace single quotes around graphql names with backticks,
// to match previous behavior from graphql-schema-comparator
return change.message.replace(/'([a-zA-Z\. :!]+)'/g, '`$1`')
})
}
/**
* Split `changesToReport` into two parts,
* one for changes in the main schema,
* and another for changes that are under preview.
* (Ported from https://github.com/github/graphql-docs/blob/7e6a5ccbf13cc7d875fee65527b25bc49e886b41/lib/graphql_docs/update_internal_developer/change_log.rb#L230)
* @param {Array<object>} changesToReport
* @param {object} previews
* @return {object}
*/
function segmentPreviewChanges(changesToReport, previews) {
// Build a map of `{ path => previewTitle` }
// for easier lookup of change to preview
const pathToPreview = {}
previews.forEach(function (preview) {
preview.toggled_on.forEach(function (path) {
pathToPreview[path] = preview.title
})
})
const schemaChanges = []
const changesByPreview = {}
changesToReport.forEach(function (change) {
// For each change, see if its path _or_ one of its ancestors
// is covered by a preview. If it is, mark this change as belonging to a preview
const pathParts = change.path.split(".")
let testPath = null
let previewTitle = null
let previewChanges = null
while (pathParts.length > 0 && !previewTitle) {
testPath = pathParts.join(".")
previewTitle = pathToPreview[testPath]
// If that path didn't find a match, then we'll
// check the next ancestor.
pathParts.pop()
}
if (previewTitle) {
previewChanges = changesByPreview[previewTitle] || (changesByPreview[previewTitle] = {
title: previewTitle,
changes: []
})
previewChanges.changes.push(change)
} else {
schemaChanges.push(change)
}
})
return { schemaChangesToReport: schemaChanges, previewChangesToReport: changesByPreview }
}
const CHANGES_TO_REPORT = [
ChangeType.FieldArgumentDefaultChanged,
ChangeType.FieldArgumentTypeChanged,
ChangeType.EnumValueRemoved,
ChangeType.EnumValueAdded,
ChangeType.FieldRemoved,
ChangeType.FieldAdded,
ChangeType.FieldTypeChanged,
ChangeType.FieldArgumentAdded,
ChangeType.FieldArgumentRemoved,
ChangeType.ObjectTypeInterfaceAdded,
ChangeType.ObjectTypeInterfaceRemoved,
ChangeType.InputFieldRemoved,
ChangeType.InputFieldAdded,
ChangeType.InputFieldDefaultValueChanged,
ChangeType.InputFieldTypeChanged,
ChangeType.TypeRemoved,
ChangeType.TypeAdded,
ChangeType.TypeKindChanged,
ChangeType.UnionMemberRemoved,
ChangeType.UnionMemberAdded,
ChangeType.SchemaQueryTypeChanged,
ChangeType.SchemaMutationTypeChanged,
ChangeType.SchemaSubscriptionTypeChanged,
]
const CHANGES_TO_IGNORE = [
ChangeType.FieldArgumentDescriptionChanged,
ChangeType.DirectiveRemoved,
ChangeType.DirectiveAdded,
ChangeType.DirectiveDescriptionChanged,
ChangeType.DirectiveLocationAdded,
ChangeType.DirectiveLocationRemoved,
ChangeType.DirectiveArgumentAdded,
ChangeType.DirectiveArgumentRemoved,
ChangeType.DirectiveArgumentDescriptionChanged,
ChangeType.DirectiveArgumentDefaultValueChanged,
ChangeType.DirectiveArgumentTypeChanged,
ChangeType.EnumValueDescriptionChanged,
ChangeType.EnumValueDeprecationReasonChanged,
ChangeType.EnumValueDeprecationReasonAdded,
ChangeType.EnumValueDeprecationReasonRemoved,
ChangeType.FieldDescriptionChanged,
ChangeType.FieldDescriptionAdded,
ChangeType.FieldDescriptionRemoved,
ChangeType.FieldDeprecationAdded,
ChangeType.FieldDeprecationRemoved,
ChangeType.FieldDeprecationReasonChanged,
ChangeType.FieldDeprecationReasonAdded,
ChangeType.FieldDeprecationReasonRemoved,
ChangeType.InputFieldDescriptionAdded,
ChangeType.InputFieldDescriptionRemoved,
ChangeType.InputFieldDescriptionChanged,
ChangeType.TypeDescriptionChanged,
ChangeType.TypeDescriptionRemoved,
ChangeType.TypeDescriptionAdded,
]
module.exports = { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry }