From d258ab56d1067695223e256f5d4ea65b45e083ee Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 22 Oct 2025 08:32:11 -0700 Subject: [PATCH 1/7] Convert 6 files to TypeScript (#58003) --- ...permissions.js => metadata-permissions.ts} | 72 +++++--- ...{build-changelog.js => build-changelog.ts} | 170 +++++++++++------- src/graphql/tests/build-changelog.ts | 18 +- .../fixtures/updated-changelog-file.json | 6 +- ...st-examples.js => create-rest-examples.ts} | 0 ...st-examples.js => create-rest-examples.ts} | 87 +++++++-- ...-samples.js => get-rest-code-samples-2.ts} | 98 ++++++---- ...n-oneof.js => webhook-generation-oneof.ts} | 0 8 files changed, 302 insertions(+), 149 deletions(-) rename src/github-apps/tests/{metadata-permissions.js => metadata-permissions.ts} (87%) rename src/graphql/scripts/{build-changelog.js => build-changelog.ts} (71%) rename src/rest/fixtures/{create-rest-examples.js => create-rest-examples.ts} (100%) rename src/rest/scripts/utils/{create-rest-examples.js => create-rest-examples.ts} (86%) rename src/rest/tests/{get-rest-code-samples.js => get-rest-code-samples-2.ts} (90%) rename src/webhooks/tests/{webhook-generation-oneof.js => webhook-generation-oneof.ts} (100%) diff --git a/src/github-apps/tests/metadata-permissions.js b/src/github-apps/tests/metadata-permissions.ts similarity index 87% rename from src/github-apps/tests/metadata-permissions.js rename to src/github-apps/tests/metadata-permissions.ts index dcd6a3279da1..4dc7e088e213 100644 --- a/src/github-apps/tests/metadata-permissions.js +++ b/src/github-apps/tests/metadata-permissions.ts @@ -1,9 +1,38 @@ import { describe, expect, test } from 'vitest' import { shouldFilterMetadataPermission, calculateAdditionalPermissions } from '../scripts/sync' +type PermissionSet = Record + +interface Operation { + operationId: string + permissionSets: PermissionSet[] +} + +interface ProgAccessData { + userToServerRest: boolean + serverToServer: boolean + permissions: PermissionSet[] +} + +interface ActorResource { + title: string + visibility: string +} + +interface FilteredOperation { + operationId: string + permission: string + additionalPermissions: boolean +} + +interface MetadataPermission { + operationId: string + additionalPermissions: boolean +} + describe('metadata permissions filtering', () => { // Mock data structure representing operations with metadata permissions - const mockOperationsWithMetadata = [ + const mockOperationsWithMetadata: Operation[] = [ { operationId: 'repos/enable-automated-security-fixes', permissionSets: [{ metadata: 'read', administration: 'write' }], @@ -23,7 +52,7 @@ describe('metadata permissions filtering', () => { ] // Mock programmatic access data - const mockProgAccessData = { + const mockProgAccessData: Record = { 'repos/enable-automated-security-fixes': { userToServerRest: true, serverToServer: true, @@ -47,7 +76,7 @@ describe('metadata permissions filtering', () => { } // Mock actor resources - const mockProgActorResources = { + const mockProgActorResources: Record = { metadata: { title: 'Metadata', visibility: 'public', @@ -95,8 +124,8 @@ describe('metadata permissions filtering', () => { }) test('filters metadata operations with additional permissions', () => { - const filteredOperations = [] - const metadataPermissions = [] + const filteredOperations: FilteredOperation[] = [] + const metadataPermissions: MetadataPermission[] = [] for (const operation of mockOperationsWithMetadata) { const progData = mockProgAccessData[operation.operationId] @@ -137,15 +166,15 @@ describe('metadata permissions filtering', () => { // Should have other permissions from operations with additional permissions const adminPermission = filteredOperations.find((op) => op.permission === 'administration') expect(adminPermission).toBeDefined() - expect(adminPermission.operationId).toBe('repos/enable-automated-security-fixes') - expect(adminPermission.additionalPermissions).toBe(true) + expect(adminPermission!.operationId).toBe('repos/enable-automated-security-fixes') + expect(adminPermission!.additionalPermissions).toBe(true) const orgAdminPermission = filteredOperations.find( (op) => op.permission === 'organization_administration', ) expect(orgAdminPermission).toBeDefined() - expect(orgAdminPermission.operationId).toBe('orgs/update-webhook') - expect(orgAdminPermission.additionalPermissions).toBe(true) + expect(orgAdminPermission!.operationId).toBe('orgs/update-webhook') + expect(orgAdminPermission!.additionalPermissions).toBe(true) }) test('preserves non-metadata permissions regardless of additional permissions', () => { @@ -168,11 +197,11 @@ describe('metadata permissions filtering', () => { expect(shouldFilterMetadataPermission('metadata', [])).toBe(false) // Permission set with empty object (edge case) - const edgeCase1 = [{ metadata: 'read' }, {}] + const edgeCase1: Record[] = [{ metadata: 'read' }, {}] expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true) // Multiple permission sets with metadata in different sets - const edgeCase2 = [{ metadata: 'read' }, { admin: 'write' }] + const edgeCase2: Record[] = [{ metadata: 'read' }, { admin: 'write' }] expect(shouldFilterMetadataPermission('metadata', edgeCase2)).toBe(true) }) @@ -207,17 +236,23 @@ describe('metadata permissions filtering', () => { test('handles complex permission structures from real data', () => { // Multiple permission sets (should filter metadata) - const multiplePermissionSets = [{ metadata: 'read' }, { administration: 'write' }] + const multiplePermissionSets: Record[] = [ + { metadata: 'read' }, + { administration: 'write' }, + ] expect(shouldFilterMetadataPermission('metadata', multiplePermissionSets)).toBe(true) // Single permission set with multiple permissions (should filter metadata) - const multiplePermissionsInSet = [ + const multiplePermissionsInSet: Record[] = [ { metadata: 'read', contents: 'write', pull_requests: 'write' }, ] expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true) // Multiple permission sets where metadata is not in the first set - const metadataInSecondSet = [{ administration: 'write' }, { metadata: 'read' }] + const metadataInSecondSet: Record[] = [ + { administration: 'write' }, + { metadata: 'read' }, + ] expect(shouldFilterMetadataPermission('metadata', metadataInSecondSet)).toBe(true) }) @@ -250,7 +285,7 @@ describe('metadata permissions filtering', () => { }) test('verifies consistency with additional-permissions flag calculation', () => { - const testCases = [ + const testCases: Array<{ permissionSets: Record[]; expected: boolean }> = [ // Single permission, single set - no additional permissions { permissionSets: [{ metadata: 'read' }], expected: false }, @@ -283,12 +318,7 @@ describe('metadata permissions filtering', () => { // - DELETE /orgs/{org}/actions/permissions/repositories/{repository_id} // Because they have metadata + organization_administration permissions - const mockMutatingOperation = { - operationId: 'actions/set-selected-repositories-enabled-github-actions-organization', - permissionSets: [{ metadata: 'read', organization_administration: 'write' }], - } - - const progData = { + const progData: ProgAccessData = { userToServerRest: true, serverToServer: true, permissions: [{ metadata: 'read', organization_administration: 'write' }], diff --git a/src/graphql/scripts/build-changelog.js b/src/graphql/scripts/build-changelog.ts similarity index 71% rename from src/graphql/scripts/build-changelog.js rename to src/graphql/scripts/build-changelog.ts index 324b7b5a83f9..1a5b248c3585 100644 --- a/src/graphql/scripts/build-changelog.js +++ b/src/graphql/scripts/build-changelog.ts @@ -1,23 +1,74 @@ -import { diff, ChangeType } from '@graphql-inspector/core' +import { diff, ChangeType, Change } from '@graphql-inspector/core' import { loadSchema } from '@graphql-tools/load' import fs from 'fs' import { renderContent } from '@/content-render/index' +interface UpcomingChange { + location: string + date: string + description: string +} + +interface Preview { + title: string + toggled_on: string[] +} + +interface ChangelogSchemaChange { + title: string + changes: string[] +} + +interface ChangelogPreviewChange { + title: string + changes: string[] +} + +interface ChangelogUpcomingChange { + title: string + changes: string[] +} + +export interface ChangelogEntry { + date?: string + schemaChanges: ChangelogSchemaChange[] + previewChanges: ChangelogPreviewChange[] + upcomingChanges: ChangelogUpcomingChange[] +} + +interface PreviewChanges { + title: string + changes: Change[] +} + +interface SegmentedChanges { + schemaChangesToReport: Change[] + previewChangesToReport: Record +} + +interface IgnoredChangeType { + type: string + count: number +} + +interface IgnoredChangesSummary { + totalCount: number + typeCount: number + types: IgnoredChangeType[] +} + /** * 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} */ -export function prependDatedEntry(changelogEntry, targetPath) { +export function prependDatedEntry(changelogEntry: ChangelogEntry, targetPath: string): void { // Build a `yyyy-mm-dd`-formatted date string // and tag the changelog entry with it const todayString = new Date().toISOString().slice(0, 10) changelogEntry.date = todayString - const previousChangelogString = fs.readFileSync(targetPath) - const previousChangelog = JSON.parse(previousChangelogString) + const previousChangelogString = fs.readFileSync(targetPath, 'utf8') + const previousChangelog = JSON.parse(previousChangelogString) as ChangelogEntry[] // add a new entry to the changelog data previousChangelog.unshift(changelogEntry) // rewrite the updated changelog @@ -29,28 +80,23 @@ export function prependDatedEntry(changelogEntry, targetPath) { * 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} [previews] - * @param {Array} [oldUpcomingChanges] - * @param {Array} [newUpcomingChanges] - * @return {object?} */ export async function createChangelogEntry( - oldSchemaString, - newSchemaString, - previews, - oldUpcomingChanges, - newUpcomingChanges, -) { + oldSchemaString: string, + newSchemaString: string, + previews: Preview[], + oldUpcomingChanges: UpcomingChange[], + newUpcomingChanges: UpcomingChange[], +): Promise { // Create schema objects out of the strings - const oldSchema = await loadSchema(oldSchemaString, {}) - const newSchema = await loadSchema(newSchemaString, {}) + // Using 'as any' because loadSchema accepts string schema directly without requiring loaders + const oldSchema = await loadSchema(oldSchemaString, {} as any) + const newSchema = await loadSchema(newSchemaString, {} as any) // Generate changes between the two schemas const changes = await diff(oldSchema, newSchema) - const changesToReport = [] - const ignoredChanges = [] + const changesToReport: Change[] = [] + const ignoredChanges: Change[] = [] changes.forEach((change) => { if (CHANGES_TO_REPORT.includes(change.type)) { changesToReport.push(change) @@ -76,14 +122,14 @@ export async function createChangelogEntry( } // Store ignored changes for potential workflow outputs - createChangelogEntry.lastIgnoredChanges = ignoredChanges + ;(createChangelogEntry as any).lastIgnoredChanges = ignoredChanges const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges( changesToReport, previews, ) - const addedUpcomingChanges = newUpcomingChanges.filter(function (change) { + const addedUpcomingChanges = newUpcomingChanges.filter(function (change): boolean { // Manually check each of `newUpcomingChanges` for an equivalent entry // in `oldUpcomingChanges`. return !oldUpcomingChanges.find(function (oldChange) { @@ -98,10 +144,10 @@ export async function createChangelogEntry( // If there were any changes, create a changelog entry if ( schemaChangesToReport.length > 0 || - previewChangesToReport.length > 0 || + Object.keys(previewChangesToReport).length > 0 || addedUpcomingChanges.length > 0 ) { - const changelogEntry = { + const changelogEntry: ChangelogEntry = { schemaChanges: [], previewChanges: [], upcomingChanges: [], @@ -109,11 +155,11 @@ export async function createChangelogEntry( const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport) const renderedScheamChanges = await Promise.all( - cleanedSchemaChanges.map(async (change) => { + cleanedSchemaChanges.map(async (change): Promise => { return await renderContent(change) }), ) - const schemaChange = { + const schemaChange: ChangelogSchemaChange = { title: 'The GraphQL schema includes these changes:', // Replace single quotes which wrap field/argument/type names with backticks changes: renderedScheamChanges, @@ -124,7 +170,7 @@ export async function createChangelogEntry( const previewChanges = previewChangesToReport[previewTitle] const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes) const renderedPreviewChanges = await Promise.all( - cleanedPreviewChanges.map(async (change) => { + cleanedPreviewChanges.map(async (change): Promise => { return renderContent(change) }), ) @@ -146,10 +192,10 @@ export async function createChangelogEntry( const location = change.location const description = change.description const date = change.date.split('T')[0] - return 'On member `' + location + '`:' + description + ' **Effective ' + date + '**.' + return `On member \`${location}\`:${description} **Effective ${date}**.` }) const renderedUpcomingChanges = await Promise.all( - cleanedUpcomingChanges.map(async (change) => { + cleanedUpcomingChanges.map(async (change): Promise => { return await renderContent(change) }), ) @@ -167,10 +213,8 @@ export async function createChangelogEntry( /** * Prepare the preview title from github/github source for the docs. - * @param {string} title - * @return {string} */ -export function cleanPreviewTitle(title) { +export function cleanPreviewTitle(title: string): string { if (title === 'UpdateRefsPreview') { title = 'Update refs preview' } else if (title === 'MergeInfoPreview') { @@ -184,10 +228,8 @@ export function cleanPreviewTitle(title) { /** * Turn the given title into an HTML-ready anchor. * (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281) - * @param {string} [previewTitle] - * @return {string} */ -export function previewAnchor(previewTitle) { +export function previewAnchor(previewTitle: string): string { return previewTitle .toLowerCase() .replace(/ /g, '-') @@ -196,11 +238,9 @@ export function previewAnchor(previewTitle) { /** * Turn changes from graphql-inspector into messages for the HTML changelog. - * @param {Array} changes - * @return {Array} */ -export function cleanMessagesFromChanges(changes) { - return changes.map(function (change) { +export function cleanMessagesFromChanges(changes: Change[]): string[] { + return changes.map(function (change): string { // 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`') @@ -212,29 +252,29 @@ export function cleanMessagesFromChanges(changes) { * one for changes in the main schema, * and another for changes that are under preview. * (Ported from /graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L230) - * @param {Array} changesToReport - * @param {object} previews - * @return {object} */ -export function segmentPreviewChanges(changesToReport, previews) { +export function segmentPreviewChanges( + changesToReport: Change[], + previews: Preview[], +): SegmentedChanges { // Build a map of `{ path => previewTitle` } // for easier lookup of change to preview - const pathToPreview = {} - previews.forEach(function (preview) { + const pathToPreview: Record = {} + previews.forEach(function (preview): void { preview.toggled_on.forEach(function (path) { pathToPreview[path] = preview.title }) }) - const schemaChanges = [] - const changesByPreview = {} + const schemaChanges: Change[] = [] + const changesByPreview: Record = {} - changesToReport.forEach(function (change) { + changesToReport.forEach(function (change): void { // 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 + const pathParts = change.path?.split('.') || [] + let testPath: string | null = null + let previewTitle: string | null = null + let previewChanges: PreviewChanges | null = null while (pathParts.length > 0 && !previewTitle) { testPath = pathParts.join('.') previewTitle = pathToPreview[testPath] @@ -294,28 +334,28 @@ const CHANGES_TO_REPORT = [ /** * Get the ignored change types from the last changelog entry creation - * @returns {Array} Array of ignored change objects */ -export function getLastIgnoredChanges() { - return createChangelogEntry.lastIgnoredChanges || [] +export function getLastIgnoredChanges(): Change[] { + return (createChangelogEntry as any).lastIgnoredChanges || [] } /** * Get summary of ignored change types for workflow outputs - * @returns {Object} Summary with counts and types */ -export function getIgnoredChangesSummary() { +export function getIgnoredChangesSummary(): IgnoredChangesSummary | null { const ignored = getLastIgnoredChanges() if (ignored.length === 0) return null const types = [...new Set(ignored.map((change) => change.type))] - const summary = { + const summary: IgnoredChangesSummary = { totalCount: ignored.length, typeCount: types.length, - types: types.map((type) => ({ - type, - count: ignored.filter((change) => change.type === type).length, - })), + types: types.map( + (type): IgnoredChangeType => ({ + type, + count: ignored.filter((change) => change.type === type).length, + }), + ), } return summary diff --git a/src/graphql/tests/build-changelog.ts b/src/graphql/tests/build-changelog.ts index 2c050a05291b..cf6e34e0dc5b 100644 --- a/src/graphql/tests/build-changelog.ts +++ b/src/graphql/tests/build-changelog.ts @@ -11,6 +11,7 @@ import { prependDatedEntry, getLastIgnoredChanges, getIgnoredChangesSummary, + type ChangelogEntry, } from '../scripts/build-changelog' import readJsonFile from '@/frame/lib/read-json-file' @@ -30,10 +31,6 @@ interface UpcomingChange { date: string } -interface ChangelogEntry { - [key: string]: any -} - interface IgnoredChange { type: string [key: string]: any @@ -245,7 +242,11 @@ describe('updating the changelog file', () => { const testTargetPath = 'src/graphql/tests/fixtures/example-changelog.json' const previousContents = await fs.readFile(testTargetPath) - const exampleEntry: ChangelogEntry = { someStuff: true } + const exampleEntry: ChangelogEntry = { + schemaChanges: [], + previewChanges: [], + upcomingChanges: [], + } const expectedDate = '2020-11-20' MockDate.set(expectedDate) @@ -254,7 +255,12 @@ describe('updating the changelog file', () => { // reset the file: await fs.writeFile(testTargetPath, previousContents.toString()) - expect(exampleEntry).toEqual({ someStuff: true, date: expectedDate }) + expect(exampleEntry).toEqual({ + schemaChanges: [], + previewChanges: [], + upcomingChanges: [], + date: expectedDate, + }) expect(JSON.parse(newContents)).toEqual(expectedUpdatedChangelogFile) }) }) diff --git a/src/graphql/tests/fixtures/updated-changelog-file.json b/src/graphql/tests/fixtures/updated-changelog-file.json index 3e8ee6041032..59aef576d6ea 100644 --- a/src/graphql/tests/fixtures/updated-changelog-file.json +++ b/src/graphql/tests/fixtures/updated-changelog-file.json @@ -1,7 +1,9 @@ [ { - "someStuff": true, - "date": "2020-11-20" + "date": "2020-11-20", + "schemaChanges": [], + "previewChanges": [], + "upcomingChanges": [] }, { "previous_entry": "..." diff --git a/src/rest/fixtures/create-rest-examples.js b/src/rest/fixtures/create-rest-examples.ts similarity index 100% rename from src/rest/fixtures/create-rest-examples.js rename to src/rest/fixtures/create-rest-examples.ts diff --git a/src/rest/scripts/utils/create-rest-examples.js b/src/rest/scripts/utils/create-rest-examples.ts similarity index 86% rename from src/rest/scripts/utils/create-rest-examples.js rename to src/rest/scripts/utils/create-rest-examples.ts index 9273b52f1891..f1fdf3a73358 100644 --- a/src/rest/scripts/utils/create-rest-examples.js +++ b/src/rest/scripts/utils/create-rest-examples.ts @@ -5,11 +5,54 @@ const DEFAULT_EXAMPLE_DESCRIPTION = 'Example' const DEFAULT_EXAMPLE_KEY = 'default' const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json' +// OpenAPI operation structure is dynamic and complex +type Operation = any + +interface RequestExample { + key: string + request: { + contentType?: string + description: string + acceptHeader: string + bodyParameters?: any + parameters?: Record + } +} + +interface ResponseExample { + key: string + response: { + statusCode: string + contentType?: string + description: string + example?: any + schema?: any + } +} + +interface MergedExample { + key: string + request: { + contentType?: string + description: string + acceptHeader: string + bodyParameters?: any + parameters?: Record + } + response?: { + statusCode: string + contentType?: string + description: string + example?: any + schema?: any + } +} + // Retrieves request and response examples and attempts to // merge them to create matching request/response examples // The key used in the media type `examples` property is // used to match requests to responses. -export default async function getCodeSamples(operation) { +export default async function getCodeSamples(operation: Operation): Promise { const responseExamples = getResponseExamples(operation) const requestExamples = getRequestExamples(operation) @@ -18,7 +61,7 @@ export default async function getCodeSamples(operation) { // If there are multiple examples and if the request body // has the same description, add a number to the example if (mergedExamples.length > 1) { - const count = {} + const count: Record = {} mergedExamples.forEach((item) => { count[item.request.description] = (count[item.request.description] || 0) + 1 }) @@ -33,7 +76,7 @@ export default async function getCodeSamples(operation) { ' ' + (i + 1) + ': Status Code ' + - example.response.statusCode + example.response!.statusCode : example.request.description, }, })) @@ -44,7 +87,10 @@ export default async function getCodeSamples(operation) { return mergedExamples } -export function mergeExamples(requestExamples, responseExamples) { +export function mergeExamples( + requestExamples: RequestExample[], + responseExamples: ResponseExample[], +): MergedExample[] { // There is always at least one request example, but it won't create // a meaningful example unless it has a response example. if (requestExamples.length === 1 && responseExamples.length === 0) { @@ -97,14 +143,17 @@ export function mergeExamples(requestExamples, responseExamples) { // Iterates over the larger array or "target" (or if equal requests) to see // if there are any matches in the smaller array or "source" // (or if equal responses) that can be added to target array. If a request + // If a request // example and response example have matching keys they will be merged into // an example. If there is more than one key match, the first match will // be used. - return target.filter((targetEx) => { - const match = source.find((srcEx) => srcEx.key === targetEx.key) - if (match) return Object.assign(targetEx, match) - return false - }) + return target + .filter((targetEx) => { + const match = source.find((srcEx) => srcEx.key === targetEx.key) + if (match) return Object.assign(targetEx, match) + return false + }) + .map((ex) => ex as MergedExample) } /* @@ -124,8 +173,8 @@ export function mergeExamples(requestExamples, responseExamples) { } } */ -export function getRequestExamples(operation) { - const requestExamples = [] +export function getRequestExamples(operation: Operation): RequestExample[] { + const requestExamples: RequestExample[] = [] const parameterExamples = getParameterExamples(operation) // When no request body or parameters are defined, we create a generic @@ -160,7 +209,7 @@ export function getRequestExamples(operation) { // Requests can have multiple content types each with their own set of // examples. Object.keys(operation.requestBody.content).forEach((contentType) => { - let examples = {} + let examples: Record = {} // This is a fallback to allow using the `example` property in // the schema. If we start to enforce using examples vs. example using // a linter, we can remove the check for `example`. @@ -232,8 +281,8 @@ export function getRequestExamples(operation) { } } */ -export function getResponseExamples(operation) { - const responseExamples = [] +export function getResponseExamples(operation: Operation): ResponseExample[] { + const responseExamples: ResponseExample[] = [] Object.keys(operation.responses).forEach((statusCode) => { // We don't want to create examples for error codes // Error codes are displayed in the status table in the docs @@ -259,7 +308,7 @@ export function getResponseExamples(operation) { // Responses can have multiple content types each with their own set of // examples. Object.keys(content).forEach((contentType) => { - let examples = {} + let examples: Record = {} // This is a fallback to allow using the `example` property in // the schema. If we start to enforce using examples vs. example using // a linter, we can remove the check for `example`. @@ -332,13 +381,13 @@ export function getResponseExamples(operation) { } } */ -export function getParameterExamples(operation) { +export function getParameterExamples(operation: Operation): Record> { if (!operation.parameters) { return {} } - const parameters = operation.parameters.filter((param) => param.in === 'path') - const parameterExamples = {} - parameters.forEach((parameter) => { + const parameters = operation.parameters.filter((param: any) => param.in === 'path') + const parameterExamples: Record> = {} + parameters.forEach((parameter: any) => { const examples = parameter.examples // If there are no examples, create an example from the uppercase parameter // name, so that it is more visible that the value is fake data diff --git a/src/rest/tests/get-rest-code-samples.js b/src/rest/tests/get-rest-code-samples-2.ts similarity index 90% rename from src/rest/tests/get-rest-code-samples.js rename to src/rest/tests/get-rest-code-samples-2.ts index 58af9c82678e..7ad79ddf73aa 100644 --- a/src/rest/tests/get-rest-code-samples.js +++ b/src/rest/tests/get-rest-code-samples-2.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest' import { getShellExample, getGHExample, getJSExample } from '../components/get-rest-code-samples' +import type { Operation } from '../components/types' // Mock version data similar to what's used in the actual app const mockVersions = { @@ -27,14 +28,19 @@ const mockVersions = { } // Mock operation with standard authentication requirements -const standardOperation = { +const standardOperation: Operation = { verb: 'post', title: 'Create an issue', requestPath: '/repos/{owner}/{repo}/issues', serverUrl: 'https://api.github.com', category: 'issues', subcategory: 'issues', + descriptionHTML: '', + previews: [], + statusCodes: [], parameters: [], + bodyParameters: [], + codeExamples: [], progAccess: { userToServerRest: true, serverToServer: true, @@ -45,14 +51,19 @@ const standardOperation = { } // Mock operation with allowPermissionlessAccess (like revoke credentials) -const unauthenticatedOperation = { +const unauthenticatedOperation: Operation = { verb: 'post', title: 'Revoke a list of credentials', requestPath: '/credentials/revoke', serverUrl: 'https://api.github.com', category: 'credentials', subcategory: 'revoke', + descriptionHTML: '', + previews: [], + statusCodes: [], parameters: [], + bodyParameters: [], + codeExamples: [], progAccess: { userToServerRest: true, serverToServer: true, @@ -63,14 +74,19 @@ const unauthenticatedOperation = { } // Mock operation with basic auth (like OAuth apps) -const basicAuthOperation = { +const basicAuthOperation: Operation = { verb: 'post', title: 'Create an OAuth app', requestPath: '/orgs/{org}/oauth/apps', serverUrl: 'https://api.github.com', category: 'apps', subcategory: 'oauth-applications', + descriptionHTML: '', + previews: [], + statusCodes: [], parameters: [], + bodyParameters: [], + codeExamples: [], progAccess: { userToServerRest: true, serverToServer: false, @@ -81,14 +97,19 @@ const basicAuthOperation = { } // Mock operation for GHES manage API -const ghesManageOperation = { +const ghesManageOperation: Operation = { verb: 'post', title: 'Set maintenance mode', requestPath: '/setup/api/maintenance', serverUrl: 'https://HOSTNAME', category: 'enterprise-admin', subcategory: 'manage-ghes', + descriptionHTML: '', + previews: [], + statusCodes: [], parameters: [], + bodyParameters: [], + codeExamples: [], progAccess: { userToServerRest: true, serverToServer: false, @@ -98,7 +119,7 @@ const ghesManageOperation = { } // Mock code sample -const mockCodeSample = { +const mockCodeSample: any = { key: 'default', request: { contentType: 'application/json', @@ -117,7 +138,7 @@ const mockCodeSample = { }, } -const mockCodeSampleWithoutBody = { +const mockCodeSampleWithoutBody: any = { key: 'default', request: { contentType: 'application/json', @@ -141,7 +162,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -152,7 +173,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-cloud@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -163,7 +184,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-cloud@2024-01-01', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -174,7 +195,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-server@3.17', - mockVersions, + mockVersions as any, ) expect(result).toContain('-H "Authorization: Bearer "') @@ -185,7 +206,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'github-ae@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('-H "Authorization: Bearer "') @@ -198,7 +219,7 @@ describe('REST code samples authentication header handling', () => { standardOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('-H "Authorization: Bearer "') @@ -211,7 +232,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -225,7 +246,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-cloud@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -238,7 +259,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-server@3.17', - mockVersions, + mockVersions as any, ) expect(result).toContain('-H "Authorization: Bearer "') @@ -251,7 +272,7 @@ describe('REST code samples authentication header handling', () => { basicAuthOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('-u ":"') @@ -263,7 +284,7 @@ describe('REST code samples authentication header handling', () => { ghesManageOperation, mockCodeSample, 'enterprise-server@3.17', - mockVersions, + mockVersions as any, ) expect(result).toContain('-u "api_key:your-password"') @@ -273,10 +294,10 @@ describe('REST code samples authentication header handling', () => { test('handles GET requests without body parameters correctly', () => { const getOperation = { ...unauthenticatedOperation, verb: 'get' } const result = getShellExample( - getOperation, + getOperation as any, mockCodeSampleWithoutBody, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -291,7 +312,7 @@ describe('REST code samples authentication header handling', () => { standardOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('gh api') @@ -306,7 +327,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('gh api') @@ -322,7 +343,7 @@ describe('REST code samples authentication header handling', () => { basicAuthOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toBeUndefined() @@ -330,7 +351,12 @@ describe('REST code samples authentication header handling', () => { test('generates example for GHES with hostname parameter', () => { const ghesOp = { ...standardOperation, serverUrl: 'https://github.example.com' } - const result = getGHExample(ghesOp, mockCodeSample, 'enterprise-server@3.17', mockVersions) + const result = getGHExample( + ghesOp, + mockCodeSample, + 'enterprise-server@3.17', + mockVersions as any, + ) expect(result).toContain('--hostname HOSTNAME') }) @@ -342,7 +368,7 @@ describe('REST code samples authentication header handling', () => { standardOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain("auth: 'YOUR-TOKEN'") @@ -355,7 +381,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})') @@ -369,7 +395,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-cloud@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})') @@ -382,7 +408,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSample, 'enterprise-server@3.17', - mockVersions, + mockVersions as any, ) expect(result).toContain("auth: 'YOUR-TOKEN'") @@ -394,7 +420,7 @@ describe('REST code samples authentication header handling', () => { basicAuthOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('import { createOAuthAppAuth } from "@octokit/auth-oauth-app"') @@ -408,7 +434,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, mockCodeSampleWithoutBody, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).toContain('const octokit = new Octokit()') @@ -426,10 +452,10 @@ describe('REST code samples authentication header handling', () => { } const shellResult = getShellExample( - operationWithoutProgAccess, + operationWithoutProgAccess as any, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) // Should default to including authentication when progAccess is undefined @@ -453,7 +479,7 @@ describe('REST code samples authentication header handling', () => { operationWithoutProperty, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) // Should default to including authentication when property is missing @@ -473,7 +499,7 @@ describe('REST code samples authentication header handling', () => { unauthenticatedOperation, nullSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(result).not.toContain('-H "Authorization: Bearer "') @@ -490,7 +516,7 @@ describe('REST code samples authentication header handling', () => { mixedAuthOperation, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) // Should still use management console auth even for allowPermissionlessAccess operations @@ -509,7 +535,7 @@ describe('REST code samples authentication header handling', () => { enterpriseUnauthOp, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(enterpriseResult).toContain('-u "api_key:your-password"') @@ -528,7 +554,7 @@ describe('REST code samples authentication header handling', () => { basicAuthUnauthOp, mockCodeSample, 'free-pro-team@latest', - mockVersions, + mockVersions as any, ) expect(basicAuthResult).toContain('-u ":"') diff --git a/src/webhooks/tests/webhook-generation-oneof.js b/src/webhooks/tests/webhook-generation-oneof.ts similarity index 100% rename from src/webhooks/tests/webhook-generation-oneof.js rename to src/webhooks/tests/webhook-generation-oneof.ts From 5992ca9bf4494227e82b42e5366273e8f9cc199f Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:41:03 -0700 Subject: [PATCH 2/7] GraphQL schema update (#58123) Co-authored-by: heiskr <1221423+heiskr@users.noreply.github.com> --- src/graphql/data/fpt/changelog.json | 13 +++++++++++++ src/graphql/data/fpt/schema.docs.graphql | 5 +++++ src/graphql/data/fpt/schema.json | 11 +++++++++++ src/graphql/data/ghec/schema.docs.graphql | 5 +++++ src/graphql/data/ghec/schema.json | 11 +++++++++++ 5 files changed, 45 insertions(+) diff --git a/src/graphql/data/fpt/changelog.json b/src/graphql/data/fpt/changelog.json index ae24cf0eb6d8..1879fd41b8b6 100644 --- a/src/graphql/data/fpt/changelog.json +++ b/src/graphql/data/fpt/changelog.json @@ -1,4 +1,17 @@ [ + { + "schemaChanges": [ + { + "title": "The GraphQL schema includes these changes:", + "changes": [ + "

Argument query: String (with default value) added to field 'ProjectV2.items'

" + ] + } + ], + "previewChanges": [], + "upcomingChanges": [], + "date": "2025-10-22" + }, { "schemaChanges": [ { diff --git a/src/graphql/data/fpt/schema.docs.graphql b/src/graphql/data/fpt/schema.docs.graphql index abc6e5bd51b9..3ca2d516848e 100644 --- a/src/graphql/data/fpt/schema.docs.graphql +++ b/src/graphql/data/fpt/schema.docs.graphql @@ -37388,6 +37388,11 @@ type ProjectV2 implements Closable & Node & Updatable { Ordering options for project v2 items returned from the connection """ orderBy: ProjectV2ItemOrder = {field: POSITION, direction: ASC} + + """ + Search query for filtering items + """ + query: String = "" ): ProjectV2ItemConnection! """ diff --git a/src/graphql/data/fpt/schema.json b/src/graphql/data/fpt/schema.json index 0b87813706f6..53b75091035c 100644 --- a/src/graphql/data/fpt/schema.json +++ b/src/graphql/data/fpt/schema.json @@ -49821,6 +49821,17 @@ "kind": "input-objects", "href": "/graphql/reference/input-objects#projectv2itemorder" } + }, + { + "name": "query", + "defaultValue": "", + "description": "

Search query for filtering items.

", + "type": { + "name": "String", + "id": "string", + "kind": "scalars", + "href": "/graphql/reference/scalars#string" + } } ] }, diff --git a/src/graphql/data/ghec/schema.docs.graphql b/src/graphql/data/ghec/schema.docs.graphql index abc6e5bd51b9..3ca2d516848e 100644 --- a/src/graphql/data/ghec/schema.docs.graphql +++ b/src/graphql/data/ghec/schema.docs.graphql @@ -37388,6 +37388,11 @@ type ProjectV2 implements Closable & Node & Updatable { Ordering options for project v2 items returned from the connection """ orderBy: ProjectV2ItemOrder = {field: POSITION, direction: ASC} + + """ + Search query for filtering items + """ + query: String = "" ): ProjectV2ItemConnection! """ diff --git a/src/graphql/data/ghec/schema.json b/src/graphql/data/ghec/schema.json index 0b87813706f6..53b75091035c 100644 --- a/src/graphql/data/ghec/schema.json +++ b/src/graphql/data/ghec/schema.json @@ -49821,6 +49821,17 @@ "kind": "input-objects", "href": "/graphql/reference/input-objects#projectv2itemorder" } + }, + { + "name": "query", + "defaultValue": "", + "description": "

Search query for filtering items.

", + "type": { + "name": "String", + "id": "string", + "kind": "scalars", + "href": "/graphql/reference/scalars#string" + } } ] }, From ed861c45db6850e969f8fdce8442843a99765cb1 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 22 Oct 2025 09:41:43 -0700 Subject: [PATCH 3/7] Add license section to README (#58118) --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4efe56cceeb0..4cc512edbf2b 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,10 @@ Here are some resources to help you get started with open source contributions: * [Set up Git](https://docs.github.com/en/get-started/git-basics/set-up-git) * [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) * [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) + +## License + +This project is dual-licensed under: + +* **Creative Commons Attribution 4.0** - for documentation and content in the assets, content, and data folders (see [LICENSE](LICENSE)) +* **MIT License** - for code (see [LICENSE-CODE](LICENSE-CODE)) From 9dab326c73742238b78ef06b7910c297eeb5a1ae Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 22 Oct 2025 10:18:14 -0700 Subject: [PATCH 4/7] Add LLM-friendly alternate format hints to article pages (#57999) --- src/frame/components/DefaultLayout.tsx | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index 5c3d5bf2990c..cf96acfe6490 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -138,7 +138,36 @@ export const DefaultLayout = (props: Props) => { {page.introPlainText && } + + {/* LLM-friendly alternate formats - only for articles */} + {page.documentType === 'article' && ( + <> + + + + )} + + + {/* a11y */} Date: Wed, 22 Oct 2025 12:22:01 -0500 Subject: [PATCH 5/7] ERP: enterprise teams, apps and roles public preview access in rulesets (#57828) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com> --- .../enforcing-policies-for-code-governance.md | 2 ++ data/reusables/repositories/rulesets-bypass-step.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/content/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-code-governance.md b/content/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-code-governance.md index 11a1490c5cea..bff85851c1b9 100644 --- a/content/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-code-governance.md +++ b/content/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-code-governance.md @@ -62,6 +62,7 @@ If you select **Must match a given regex pattern restriction**, you can use regu You can grant certain roles, teams, or apps bypass permissions as well as the ability to approve bypass requests for your ruleset. The following are eligible for bypass access: +* Enterprise teams, enterprise apps, and enterprise roles ({% data variables.release-phases.public_preview %}) * Repository admins, organization owners, and enterprise owners * The maintain or write role, or deploy keys. @@ -118,6 +119,7 @@ You can create a push ruleset for private or internal repositories in your enter You can grant certain roles, teams, or apps bypass permissions as well as the ability to approve bypass requests for your ruleset. The following are eligible for bypass access: +* Enterprise teams, enterprise apps, and enterprise roles ({% data variables.release-phases.public_preview %}) * Repository admins, organization owners, and enterprise owners * The maintain or write role, or deploy keys diff --git a/data/reusables/repositories/rulesets-bypass-step.md b/data/reusables/repositories/rulesets-bypass-step.md index 42e024fb0f5f..545cd55d0139 100644 --- a/data/reusables/repositories/rulesets-bypass-step.md +++ b/data/reusables/repositories/rulesets-bypass-step.md @@ -2,7 +2,8 @@ You can grant certain roles, teams, or apps bypass permissions {% ifversion push * Repository admins, organization owners, and enterprise owners * The maintain or write role, or custom repository roles based on the write role -* Teams, excluding secret teams. See [AUTOTITLE](/organizations/organizing-members-into-teams/about-teams#team-visibility). +* Teams, excluding secret teams. See [AUTOTITLE](/organizations/organizing-members-into-teams/about-teams#team-visibility).{% ifversion ghec %} +* Enterprise teams, enterprise apps, and enterprise roles ({% data variables.release-phases.public_preview %}){% endif %} {%- ifversion ghes %} * Deploy keys {%- endif %} From b7fea98d2ce026f7a79a5d30566e4197994a01be Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 22 Oct 2025 10:32:27 -0700 Subject: [PATCH 6/7] Migrate process-schemas.js to TypeScript (#58008) --- src/graphql/scripts/utils/process-schemas.js | 455 ----------- src/graphql/scripts/utils/process-schemas.ts | 779 +++++++++++++++++++ src/graphql/scripts/utils/schema-helpers.ts | 27 +- 3 files changed, 795 insertions(+), 466 deletions(-) delete mode 100755 src/graphql/scripts/utils/process-schemas.js create mode 100755 src/graphql/scripts/utils/process-schemas.ts diff --git a/src/graphql/scripts/utils/process-schemas.js b/src/graphql/scripts/utils/process-schemas.js deleted file mode 100755 index ac0b0e11e24b..000000000000 --- a/src/graphql/scripts/utils/process-schemas.js +++ /dev/null @@ -1,455 +0,0 @@ -import { sortBy } from 'lodash-es' -import { parse, buildASTSchema } from 'graphql' -import helpers from './schema-helpers' -import fs from 'fs/promises' -import path from 'path' - -const externalScalarsJSON = JSON.parse( - await fs.readFile(path.join(process.cwd(), './src/graphql/lib/non-schema-scalars.json')), -) -const externalScalars = await Promise.all( - externalScalarsJSON.map(async (scalar) => { - scalar.description = await helpers.getDescription(scalar.description) - scalar.id = helpers.getId(scalar.name) - scalar.href = helpers.getFullLink('scalars', scalar.id) - return scalar - }), -) - -// select and format all the data from the schema that we need for the docs -// used in the build step -export default async function processSchemas(idl, previewsPerVersion) { - const schemaAST = parse(idl.toString()) - const schema = buildASTSchema(schemaAST) - - // list of objects is used when processing mutations - const objectsInSchema = schemaAST.definitions.filter((def) => def.kind === 'ObjectTypeDefinition') - - const data = {} - - data.queries = [] - data.mutations = [] - data.objects = [] - data.interfaces = [] - data.enums = [] - data.unions = [] - data.inputObjects = [] - data.scalars = [] - - await Promise.all( - schemaAST.definitions.map(async (def) => { - // QUERIES - if (def.name.value === 'Query') { - await Promise.all( - def.fields.map(async (field) => { - const query = {} - const queryArgs = [] - - query.name = field.name.value - query.type = helpers.getType(field) - query.kind = helpers.getTypeKind(query.type, schema) - query.id = helpers.getId(query.type) - query.href = helpers.getFullLink(query.kind, query.id) - query.description = await helpers.getDescription(field.description.value) - query.isDeprecated = helpers.getDeprecationStatus(field.directives, query.name) - query.deprecationReason = await helpers.getDeprecationReason(field.directives, query) - query.preview = await helpers.getPreview(field.directives, query, previewsPerVersion) - - await Promise.all( - field.arguments.map(async (arg) => { - const queryArg = {} - queryArg.name = arg.name.value - queryArg.defaultValue = arg.defaultValue ? arg.defaultValue.value : undefined - queryArg.type = helpers.getType(arg) - queryArg.id = helpers.getId(queryArg.type) - queryArg.kind = helpers.getTypeKind(queryArg.type, schema) - queryArg.href = helpers.getFullLink(queryArg.kind, queryArg.id) - queryArg.description = await helpers.getDescription(arg.description.value) - queryArg.isDeprecated = helpers.getDeprecationStatus(arg.directives, queryArg.name) - queryArg.deprecationReason = await helpers.getDeprecationReason( - arg.directives, - queryArg, - ) - queryArg.preview = await helpers.getPreview( - arg.directives, - queryArg, - previewsPerVersion, - ) - queryArgs.push(queryArg) - }), - ) - - query.args = sortBy(queryArgs, 'name') - data.queries.push(query) - }), - ) - - return - } - - // MUTATIONS - if (def.name.value === 'Mutation') { - await Promise.all( - def.fields.map(async (field) => { - const mutation = {} - const inputFields = [] - const returnFields = [] - - mutation.name = field.name.value - mutation.kind = helpers.getKind(def.name.value) - mutation.id = helpers.getId(mutation.name) - mutation.href = helpers.getFullLink('mutations', mutation.id) - mutation.description = await helpers.getDescription(field.description.value) - mutation.isDeprecated = helpers.getDeprecationStatus(field.directives, mutation.name) - mutation.deprecationReason = await helpers.getDeprecationReason( - field.directives, - mutation, - ) - mutation.preview = await helpers.getPreview( - field.directives, - mutation, - previewsPerVersion, - ) - - // there is only ever one input field argument, but loop anyway - await Promise.all( - field.arguments.map(async (field) => { - const inputField = {} - inputField.name = field.name.value - inputField.type = helpers.getType(field) - inputField.id = helpers.getId(inputField.type) - inputField.kind = helpers.getTypeKind(inputField.type, schema) - inputField.href = helpers.getFullLink(inputField.kind, inputField.id) - inputFields.push(inputField) - }), - ) - - mutation.inputFields = sortBy(inputFields, 'name') - - // get return fields - // first get the payload, then find payload object's fields. these are the mutation's return fields. - const returnType = helpers.getType(field) - const mutationReturnFields = objectsInSchema.find( - (obj) => obj.name.value === returnType, - ) - - if (!mutationReturnFields) console.log(`no return fields found for ${returnType}`) - - await Promise.all( - mutationReturnFields.fields.map(async (field) => { - const returnField = {} - returnField.name = field.name.value - returnField.type = helpers.getType(field) - returnField.id = helpers.getId(returnField.type) - returnField.kind = helpers.getTypeKind(returnField.type, schema) - returnField.href = helpers.getFullLink(returnField.kind, returnField.id) - returnField.description = await helpers.getDescription(field.description.value) - returnField.isDeprecated = helpers.getDeprecationStatus( - field.directives, - returnField.name, - ) - returnField.deprecationReason = await helpers.getDeprecationReason( - field.directives, - returnField, - ) - returnField.preview = await helpers.getPreview( - field.directives, - returnField, - previewsPerVersion, - ) - returnFields.push(returnField) - }), - ) - - mutation.returnFields = sortBy(returnFields, 'name') - - data.mutations.push(mutation) - }), - ) - return - } - - // OBJECTS - if (def.kind === 'ObjectTypeDefinition') { - // objects ending with 'Payload' are only used to derive mutation values - // they are not included in the objects docs - if (def.name.value.endsWith('Payload')) return - - const object = {} - const objectImplements = [] - const objectFields = [] - - object.name = def.name.value - object.kind = helpers.getKind(def.kind) - object.id = helpers.getId(object.name) - object.href = helpers.getFullLink('objects', object.id) - object.description = await helpers.getDescription(def.description.value) - object.isDeprecated = helpers.getDeprecationStatus(def.directives, object.name) - object.deprecationReason = await helpers.getDeprecationReason(def.directives, object) - object.preview = await helpers.getPreview(def.directives, object, previewsPerVersion) - - // an object's interfaces render in the `Implements` section - // interfaces do not have directives so they cannot be under preview/deprecated - if (def.interfaces.length) { - await Promise.all( - def.interfaces.map(async (graphqlInterface) => { - const objectInterface = {} - objectInterface.name = graphqlInterface.name.value - objectInterface.id = helpers.getId(objectInterface.name) - objectInterface.href = helpers.getFullLink('interfaces', objectInterface.id) - objectImplements.push(objectInterface) - }), - ) - } - - // an object's fields render in the `Fields` section - if (def.fields.length) { - await Promise.all( - def.fields.map(async (field) => { - const objectField = {} - - objectField.name = field.name.value - objectField.description = field.description - ? await helpers.getDescription(field.description.value) - : '' - objectField.type = helpers.getType(field) - objectField.id = helpers.getId(objectField.type) - objectField.kind = helpers.getTypeKind(objectField.type, schema) - objectField.href = helpers.getFullLink(objectField.kind, objectField.id) - objectField.arguments = await helpers.getArguments(field.arguments, schema) - objectField.isDeprecated = helpers.getDeprecationStatus(field.directives) - objectField.deprecationReason = await helpers.getDeprecationReason( - field.directives, - objectField, - ) - objectField.preview = await helpers.getPreview( - field.directives, - objectField, - previewsPerVersion, - ) - - objectFields.push(objectField) - }), - ) - } - - if (objectImplements.length) object.implements = sortBy(objectImplements, 'name') - if (objectFields.length) object.fields = sortBy(objectFields, 'name') - - data.objects.push(object) - return - } - - // INTERFACES - if (def.kind === 'InterfaceTypeDefinition') { - const graphqlInterface = {} - const interfaceFields = [] - - graphqlInterface.name = def.name.value - graphqlInterface.kind = helpers.getKind(def.kind) - graphqlInterface.id = helpers.getId(graphqlInterface.name) - graphqlInterface.href = helpers.getFullLink('interfaces', graphqlInterface.id) - graphqlInterface.description = await helpers.getDescription(def.description.value) - graphqlInterface.isDeprecated = helpers.getDeprecationStatus(def.directives) - graphqlInterface.deprecationReason = await helpers.getDeprecationReason( - def.directives, - graphqlInterface, - ) - graphqlInterface.preview = await helpers.getPreview( - def.directives, - graphqlInterface, - previewsPerVersion, - ) - - // an interface's fields render in the "Fields" section - if (def.fields.length) { - await Promise.all( - def.fields.map(async (field) => { - const interfaceField = {} - - interfaceField.name = field.name.value - interfaceField.description = field.description - ? await helpers.getDescription(field.description.value) - : '' - interfaceField.type = helpers.getType(field) - interfaceField.id = helpers.getId(interfaceField.type) - interfaceField.kind = helpers.getTypeKind(interfaceField.type, schema) - interfaceField.href = helpers.getFullLink(interfaceField.kind, interfaceField.id) - interfaceField.arguments = await helpers.getArguments(field.arguments, schema) - interfaceField.isDeprecated = helpers.getDeprecationStatus(field.directives) - interfaceField.deprecationReason = await helpers.getDeprecationReason( - field.directives, - interfaceField, - ) - interfaceField.preview = await helpers.getPreview( - field.directives, - interfaceField, - previewsPerVersion, - ) - - interfaceFields.push(interfaceField) - }), - ) - } - - graphqlInterface.fields = sortBy(interfaceFields, 'name') - - data.interfaces.push(graphqlInterface) - return - } - - // ENUMS - if (def.kind === 'EnumTypeDefinition') { - const graphqlEnum = {} - const enumValues = [] - - graphqlEnum.name = def.name.value - graphqlEnum.kind = helpers.getKind(def.kind) - graphqlEnum.id = helpers.getId(graphqlEnum.name) - graphqlEnum.href = helpers.getFullLink('enums', graphqlEnum.id) - graphqlEnum.description = await helpers.getDescription(def.description.value) - graphqlEnum.isDeprecated = helpers.getDeprecationStatus(def.directives) - graphqlEnum.deprecationReason = await helpers.getDeprecationReason( - def.directives, - graphqlEnum, - ) - graphqlEnum.preview = await helpers.getPreview( - def.directives, - graphqlEnum, - previewsPerVersion, - ) - - await Promise.all( - def.values.map(async (value) => { - const enumValue = {} - enumValue.name = value.name.value - enumValue.description = await helpers.getDescription(value.description.value) - enumValues.push(enumValue) - }), - ) - - graphqlEnum.values = sortBy(enumValues, 'name') - - data.enums.push(graphqlEnum) - return - } - - // UNIONS - if (def.kind === 'UnionTypeDefinition') { - const union = {} - const possibleTypes = [] - - union.name = def.name.value - union.kind = helpers.getKind(def.kind) - union.id = helpers.getId(union.name) - union.href = helpers.getFullLink('unions', union.id) - union.description = await helpers.getDescription(def.description.value) - union.isDeprecated = helpers.getDeprecationStatus(def.directives) - union.deprecationReason = await helpers.getDeprecationReason(def.directives, union) - union.preview = await helpers.getPreview(def.directives, union, previewsPerVersion) - - // union types do not have directives so cannot be under preview/deprecated - await Promise.all( - def.types.map(async (type) => { - const possibleType = {} - possibleType.name = type.name.value - possibleType.id = helpers.getId(possibleType.name) - possibleType.href = helpers.getFullLink('objects', possibleType.id) - possibleTypes.push(possibleType) - }), - ) - - union.possibleTypes = sortBy(possibleTypes, 'name') - - data.unions.push(union) - return - } - - // INPUT OBJECTS - // NOTE: input objects ending with `Input` are NOT included in the v4 input objects sidebar - // but they are still present in the docs (e.g., https://developer.github.com/v4/input_object/acceptenterpriseadministratorinvitationinput/) - // so we will include them here - if (def.kind === 'InputObjectTypeDefinition') { - const inputObject = {} - const inputFields = [] - - inputObject.name = def.name.value - inputObject.kind = helpers.getKind(def.kind) - inputObject.id = helpers.getId(inputObject.name) - inputObject.href = helpers.getFullLink('input-objects', inputObject.id) - inputObject.description = await helpers.getDescription(def.description.value) - inputObject.isDeprecated = helpers.getDeprecationStatus(def.directives) - inputObject.deprecationReason = await helpers.getDeprecationReason( - def.directives, - inputObject, - ) - inputObject.preview = await helpers.getPreview( - def.directives, - inputObject, - previewsPerVersion, - ) - - if (def.fields.length) { - await Promise.all( - def.fields.map(async (field) => { - const inputField = {} - - inputField.name = field.name.value - inputField.description = await helpers.getDescription(field.description.value) - inputField.type = helpers.getType(field) - inputField.id = helpers.getId(inputField.type) - inputField.kind = helpers.getTypeKind(inputField.type, schema) - inputField.href = helpers.getFullLink(inputField.kind, inputField.id) - inputField.isDeprecated = helpers.getDeprecationStatus(field.directives) - inputField.deprecationReason = await helpers.getDeprecationReason( - field.directives, - inputField, - ) - inputField.preview = await helpers.getPreview( - field.directives, - inputField, - previewsPerVersion, - ) - - inputFields.push(inputField) - }), - ) - } - - inputObject.inputFields = sortBy(inputFields, 'name') - - data.inputObjects.push(inputObject) - return - } - - // SCALARS - if (def.kind === 'ScalarTypeDefinition') { - const scalar = {} - scalar.name = def.name.value - scalar.kind = helpers.getKind(def.kind) - scalar.id = helpers.getId(scalar.name) - scalar.href = helpers.getFullLink('scalars', scalar.id) - scalar.description = await helpers.getDescription(def.description.value) - scalar.isDeprecated = helpers.getDeprecationStatus(def.directives) - scalar.deprecationReason = await helpers.getDeprecationReason(def.directives, scalar) - scalar.preview = await helpers.getPreview(def.directives, scalar, previewsPerVersion) - data.scalars.push(scalar) - } - }), - ) - - // add non-schema scalars and sort all scalars alphabetically - data.scalars = sortBy(data.scalars.concat(externalScalars), 'name') - - // sort all the types alphabetically - data.queries = sortBy(data.queries, 'name') - data.mutations = sortBy(data.mutations, 'name') - data.objects = sortBy(data.objects, 'name') - data.interfaces = sortBy(data.interfaces, 'name') - data.enums = sortBy(data.enums, 'name') - data.unions = sortBy(data.unions, 'name') - data.inputObjects = sortBy(data.inputObjects, 'name') - data.scalars = sortBy(data.scalars, 'name') - - return data -} diff --git a/src/graphql/scripts/utils/process-schemas.ts b/src/graphql/scripts/utils/process-schemas.ts new file mode 100755 index 000000000000..b2a2c95edda3 --- /dev/null +++ b/src/graphql/scripts/utils/process-schemas.ts @@ -0,0 +1,779 @@ +import { sortBy } from 'lodash-es' +import { parse, buildASTSchema, GraphQLSchema } from 'graphql' +import type { + DocumentNode, + ObjectTypeDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + ConstDirectiveNode, + DefinitionNode, +} from 'graphql/language' +import helpers from './schema-helpers' +import fs from 'fs/promises' +import path from 'path' + +interface PreviewInfo { + toggled_by: string[] +} + +// Interface for arguments returned by helpers.getArguments() +interface FieldArgumentInfo { + name: string + defaultValue?: any // GraphQL default values can be any JSON-serializable type + description: string + type: { + name: string + id: string + kind: string + href: string + } +} + +interface ScalarInfo { + name: string + description: string + id: string + href: string + kind?: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo +} + +interface QueryArgumentInfo { + name: string + defaultValue?: any // GraphQL default values can be any JSON-serializable type + type: string + id: string + kind: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo +} + +interface QueryInfo { + name: string + type: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + args: QueryArgumentInfo[] +} + +interface FieldInfo { + name: string + type: string + id: string + kind: string + href: string + description: string + arguments?: FieldArgumentInfo[] + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo +} + +interface InputFieldInfo { + name: string + type: string + id: string + kind: string + href: string +} + +interface ReturnFieldInfo { + name: string + type: string + id: string + kind: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo +} + +interface MutationInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + inputFields: InputFieldInfo[] + returnFields: ReturnFieldInfo[] +} + +interface InterfaceInfo { + name: string + id: string + href: string +} + +interface ObjectInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + implements?: InterfaceInfo[] + fields?: FieldInfo[] +} + +interface GraphQLInterfaceInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + fields: FieldInfo[] +} + +interface EnumValueInfo { + name: string + description: string +} + +interface EnumInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + values: EnumValueInfo[] +} + +interface PossibleTypeInfo { + name: string + id: string + href: string +} + +interface UnionInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + possibleTypes: PossibleTypeInfo[] +} + +interface InputFieldDetailInfo { + name: string + description: string + type: string + id: string + kind: string + href: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo +} + +interface InputObjectInfo { + name: string + kind: string + id: string + href: string + description: string + isDeprecated?: boolean + deprecationReason?: string + preview?: PreviewInfo + inputFields: InputFieldDetailInfo[] +} + +interface ProcessedSchemaData { + queries: QueryInfo[] + mutations: MutationInfo[] + objects: ObjectInfo[] + interfaces: GraphQLInterfaceInfo[] + enums: EnumInfo[] + unions: UnionInfo[] + inputObjects: InputObjectInfo[] + scalars: ScalarInfo[] +} + +const externalScalarsJSON: Array<{ name: string; description: string }> = JSON.parse( + await fs.readFile(path.join(process.cwd(), './src/graphql/lib/non-schema-scalars.json'), 'utf-8'), +) +const externalScalars: ScalarInfo[] = await Promise.all( + externalScalarsJSON.map(async (scalar): Promise => { + const description = await helpers.getDescription(scalar.description) + const id = helpers.getId(scalar.name) + const href = helpers.getFullLink('scalars', id) + return { + name: scalar.name, + description, + id, + href, + } + }), +) + +// select and format all the data from the schema that we need for the docs +// used in the build step +export default async function processSchemas( + idl: Buffer | string, + previewsPerVersion: PreviewInfo[], +): Promise { + const schemaAST: DocumentNode = parse(idl.toString()) + const schema: GraphQLSchema = buildASTSchema(schemaAST) + + // list of objects is used when processing mutations + const objectsInSchema = schemaAST.definitions.filter( + (def): def is ObjectTypeDefinitionNode => def.kind === 'ObjectTypeDefinition', + ) + + const data: ProcessedSchemaData = { + queries: [], + mutations: [], + objects: [], + interfaces: [], + enums: [], + unions: [], + inputObjects: [], + scalars: [], + } + + await Promise.all( + schemaAST.definitions.map(async (def: DefinitionNode) => { + // QUERIES + if (def.kind === 'ObjectTypeDefinition' && def.name.value === 'Query') { + await Promise.all( + (def.fields || []).map(async (field: FieldDefinitionNode) => { + const query: Partial = {} + const queryArgs: QueryArgumentInfo[] = [] + + query.name = field.name.value + const fieldType = helpers.getType(field) + if (!fieldType) return + query.type = fieldType + const fieldKind = helpers.getTypeKind(query.type, schema) + if (!fieldKind) return + query.kind = fieldKind + query.id = helpers.getId(query.type) + query.href = helpers.getFullLink(query.kind, query.id) + query.description = await helpers.getDescription(field.description?.value || '') + query.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + query.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + query as QueryInfo, + ) + query.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + query as QueryInfo, + previewsPerVersion, + ) + + await Promise.all( + (field.arguments || []).map(async (arg: InputValueDefinitionNode) => { + const queryArg: Partial = {} + queryArg.name = arg.name.value + // ConstValueNode is a complex union; accessing value property generically + queryArg.defaultValue = arg.defaultValue + ? (arg.defaultValue as any).value + : undefined + // InputValueDefinitionNode.type is compatible with getType's expected structure + const argType = helpers.getType(arg as any) + if (!argType) return + queryArg.type = argType + queryArg.id = helpers.getId(queryArg.type) + const argKind = helpers.getTypeKind(queryArg.type, schema) + if (!argKind) return + queryArg.kind = argKind + queryArg.href = helpers.getFullLink(queryArg.kind, queryArg.id) + queryArg.description = await helpers.getDescription(arg.description?.value || '') + queryArg.isDeprecated = helpers.getDeprecationStatus( + (arg.directives || []) as readonly ConstDirectiveNode[], + ) + queryArg.deprecationReason = await helpers.getDeprecationReason( + (arg.directives || []) as readonly ConstDirectiveNode[], + queryArg as QueryArgumentInfo, + ) + queryArg.preview = await helpers.getPreview( + (arg.directives || []) as readonly ConstDirectiveNode[], + queryArg as QueryArgumentInfo, + previewsPerVersion, + ) + queryArgs.push(queryArg as QueryArgumentInfo) + }), + ) + + query.args = sortBy(queryArgs, 'name') + data.queries.push(query as QueryInfo) + }), + ) + + return + } + + // MUTATIONS + if (def.kind === 'ObjectTypeDefinition' && def.name.value === 'Mutation') { + await Promise.all( + (def.fields || []).map(async (field: FieldDefinitionNode) => { + const mutation: Partial = {} + const inputFields: InputFieldInfo[] = [] + const returnFields: ReturnFieldInfo[] = [] + + mutation.name = field.name.value + mutation.kind = helpers.getKind(def.name.value) + mutation.id = helpers.getId(mutation.name) + mutation.href = helpers.getFullLink('mutations', mutation.id) + mutation.description = await helpers.getDescription(field.description?.value || '') + mutation.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + mutation.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + mutation as MutationInfo, + ) + mutation.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + mutation as MutationInfo, + previewsPerVersion, + ) + + // there is only ever one input field argument, but loop anyway + await Promise.all( + (field.arguments || []).map(async (arg: InputValueDefinitionNode) => { + const inputField: Partial = {} + inputField.name = arg.name.value + // InputValueDefinitionNode.type is compatible with getType's expected structure + const argType = helpers.getType(arg as any) + if (!argType) return + inputField.type = argType + inputField.id = helpers.getId(inputField.type) + const argKind = helpers.getTypeKind(inputField.type, schema) + if (!argKind) return + inputField.kind = argKind + inputField.href = helpers.getFullLink(inputField.kind, inputField.id) + inputFields.push(inputField as InputFieldInfo) + }), + ) + + mutation.inputFields = sortBy(inputFields, 'name') + + // get return fields + // first get the payload, then find payload object's fields. these are the mutation's return fields. + const returnType = helpers.getType(field) + if (!returnType) return + const mutationReturnFields = objectsInSchema.find( + (obj) => obj.name.value === returnType, + ) + + if (!mutationReturnFields) { + console.log(`no return fields found for ${returnType}`) + return + } + + await Promise.all( + mutationReturnFields.fields!.map(async (field: FieldDefinitionNode) => { + const returnField: Partial = {} + returnField.name = field.name.value + const fieldType = helpers.getType(field) + if (!fieldType) return + returnField.type = fieldType + returnField.id = helpers.getId(returnField.type) + const fieldKind = helpers.getTypeKind(returnField.type, schema) + if (!fieldKind) return + returnField.kind = fieldKind + returnField.href = helpers.getFullLink(returnField.kind, returnField.id) + returnField.description = await helpers.getDescription( + field.description?.value || '', + ) + returnField.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + returnField.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + returnField as ReturnFieldInfo, + ) + returnField.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + returnField as ReturnFieldInfo, + previewsPerVersion, + ) + returnFields.push(returnField as ReturnFieldInfo) + }), + ) + + mutation.returnFields = sortBy(returnFields, 'name') + + data.mutations.push(mutation as MutationInfo) + }), + ) + return + } + + // OBJECTS + if (def.kind === 'ObjectTypeDefinition') { + // objects ending with 'Payload' are only used to derive mutation values + // they are not included in the objects docs + if (def.name.value.endsWith('Payload')) return + + const object: Partial = {} + const objectImplements: InterfaceInfo[] = [] + const objectFields: FieldInfo[] = [] + + object.name = def.name.value + object.kind = helpers.getKind(def.kind) + object.id = helpers.getId(object.name) + object.href = helpers.getFullLink('objects', object.id) + object.description = await helpers.getDescription(def.description?.value || '') + object.isDeprecated = helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ) + object.deprecationReason = await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + object as ObjectInfo, + ) + object.preview = await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + object as ObjectInfo, + previewsPerVersion, + ) + + // an object's interfaces render in the `Implements` section + // interfaces do not have directives so they cannot be under preview/deprecated + if (def.interfaces && def.interfaces.length) { + await Promise.all( + def.interfaces.map(async (graphqlInterface) => { + const objectInterface: InterfaceInfo = { + name: graphqlInterface.name.value, + id: helpers.getId(graphqlInterface.name.value), + href: helpers.getFullLink('interfaces', helpers.getId(graphqlInterface.name.value)), + } + objectImplements.push(objectInterface) + }), + ) + } + + // an object's fields render in the `Fields` section + if (def.fields && def.fields.length) { + await Promise.all( + def.fields.map(async (field: FieldDefinitionNode) => { + const objectField: Partial = {} + + objectField.name = field.name.value + objectField.description = field.description + ? await helpers.getDescription(field.description.value) + : '' + const fieldType = helpers.getType(field) + if (!fieldType) return + objectField.type = fieldType + objectField.id = helpers.getId(objectField.type) + const fieldKind = helpers.getTypeKind(objectField.type, schema) + if (!fieldKind) return + objectField.kind = fieldKind + objectField.href = helpers.getFullLink(objectField.kind, objectField.id) + // InputValueDefinitionNode structure is compatible with ArgumentNode expected by getArguments + objectField.arguments = await helpers.getArguments( + (field.arguments || []) as any, + schema, + ) + objectField.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + objectField.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + objectField as FieldInfo, + ) + objectField.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + objectField as FieldInfo, + previewsPerVersion, + ) + + objectFields.push(objectField as FieldInfo) + }), + ) + } + + if (objectImplements.length) object.implements = sortBy(objectImplements, 'name') + if (objectFields.length) object.fields = sortBy(objectFields, 'name') + + data.objects.push(object as ObjectInfo) + return + } + + // INTERFACES + if (def.kind === 'InterfaceTypeDefinition') { + const graphqlInterface: Partial = {} + const interfaceFields: FieldInfo[] = [] + + graphqlInterface.name = def.name.value + graphqlInterface.kind = helpers.getKind(def.kind) + graphqlInterface.id = helpers.getId(graphqlInterface.name) + graphqlInterface.href = helpers.getFullLink('interfaces', graphqlInterface.id) + graphqlInterface.description = await helpers.getDescription(def.description?.value || '') + graphqlInterface.isDeprecated = helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ) + graphqlInterface.deprecationReason = await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + graphqlInterface as GraphQLInterfaceInfo, + ) + graphqlInterface.preview = await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + graphqlInterface as GraphQLInterfaceInfo, + previewsPerVersion, + ) + + // an interface's fields render in the "Fields" section + if (def.fields && def.fields.length) { + await Promise.all( + def.fields.map(async (field: FieldDefinitionNode) => { + const interfaceField: Partial = {} + + interfaceField.name = field.name.value + interfaceField.description = field.description + ? await helpers.getDescription(field.description.value) + : '' + const fieldType = helpers.getType(field) + if (!fieldType) return + interfaceField.type = fieldType + interfaceField.id = helpers.getId(interfaceField.type) + const fieldKind = helpers.getTypeKind(interfaceField.type, schema) + if (!fieldKind) return + interfaceField.kind = fieldKind + interfaceField.href = helpers.getFullLink(interfaceField.kind, interfaceField.id) + // InputValueDefinitionNode structure is compatible with ArgumentNode expected by getArguments + interfaceField.arguments = await helpers.getArguments( + (field.arguments || []) as any, + schema, + ) + interfaceField.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + interfaceField.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + interfaceField as FieldInfo, + ) + interfaceField.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + interfaceField as FieldInfo, + previewsPerVersion, + ) + + interfaceFields.push(interfaceField as FieldInfo) + }), + ) + } + + graphqlInterface.fields = sortBy(interfaceFields, 'name') + + data.interfaces.push(graphqlInterface as GraphQLInterfaceInfo) + return + } + + // ENUMS + if (def.kind === 'EnumTypeDefinition') { + const graphqlEnum: Partial = {} + const enumValues: EnumValueInfo[] = [] + + graphqlEnum.name = def.name.value + graphqlEnum.kind = helpers.getKind(def.kind) + graphqlEnum.id = helpers.getId(graphqlEnum.name) + graphqlEnum.href = helpers.getFullLink('enums', graphqlEnum.id) + graphqlEnum.description = await helpers.getDescription(def.description?.value || '') + graphqlEnum.isDeprecated = helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ) + graphqlEnum.deprecationReason = await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + graphqlEnum as EnumInfo, + ) + graphqlEnum.preview = await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + graphqlEnum as EnumInfo, + previewsPerVersion, + ) + + await Promise.all( + (def.values || []).map(async (value) => { + const enumValue: EnumValueInfo = { + name: value.name.value, + description: await helpers.getDescription(value.description?.value || ''), + } + enumValues.push(enumValue) + }), + ) + + graphqlEnum.values = sortBy(enumValues, 'name') + + data.enums.push(graphqlEnum as EnumInfo) + return + } + + // UNIONS + if (def.kind === 'UnionTypeDefinition') { + const union: Partial = {} + const possibleTypes: PossibleTypeInfo[] = [] + + union.name = def.name.value + union.kind = helpers.getKind(def.kind) + union.id = helpers.getId(union.name) + union.href = helpers.getFullLink('unions', union.id) + union.description = await helpers.getDescription(def.description?.value || '') + union.isDeprecated = helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ) + union.deprecationReason = await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + union as UnionInfo, + ) + union.preview = await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + union as UnionInfo, + previewsPerVersion, + ) + + // union types do not have directives so cannot be under preview/deprecated + await Promise.all( + (def.types || []).map(async (type) => { + const possibleType: PossibleTypeInfo = { + name: type.name.value, + id: helpers.getId(type.name.value), + href: helpers.getFullLink('objects', helpers.getId(type.name.value)), + } + possibleTypes.push(possibleType) + }), + ) + + union.possibleTypes = sortBy(possibleTypes, 'name') + + data.unions.push(union as UnionInfo) + return + } + + // INPUT OBJECTS + // NOTE: input objects ending with `Input` are NOT included in the v4 input objects sidebar + // but they are still present in the docs (e.g., https://developer.github.com/v4/input_object/acceptenterpriseadministratorinvitationinput/) + // so we will include them here + if (def.kind === 'InputObjectTypeDefinition') { + const inputObject: Partial = {} + const inputFields: InputFieldDetailInfo[] = [] + + inputObject.name = def.name.value + inputObject.kind = helpers.getKind(def.kind) + inputObject.id = helpers.getId(inputObject.name) + inputObject.href = helpers.getFullLink('input-objects', inputObject.id) + inputObject.description = await helpers.getDescription(def.description?.value || '') + inputObject.isDeprecated = helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ) + inputObject.deprecationReason = await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + inputObject as InputObjectInfo, + ) + inputObject.preview = await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + inputObject as InputObjectInfo, + previewsPerVersion, + ) + + if (def.fields && def.fields.length) { + await Promise.all( + def.fields.map(async (field: InputValueDefinitionNode) => { + const inputField: Partial = {} + + inputField.name = field.name.value + inputField.description = await helpers.getDescription(field.description?.value || '') + // InputValueDefinitionNode.type is compatible with getType's expected structure + const fieldType = helpers.getType(field as any) + if (!fieldType) return + inputField.type = fieldType + inputField.id = helpers.getId(inputField.type) + const fieldKind = helpers.getTypeKind(inputField.type, schema) + if (!fieldKind) return + inputField.kind = fieldKind + inputField.href = helpers.getFullLink(inputField.kind, inputField.id) + inputField.isDeprecated = helpers.getDeprecationStatus( + (field.directives || []) as readonly ConstDirectiveNode[], + ) + inputField.deprecationReason = await helpers.getDeprecationReason( + (field.directives || []) as readonly ConstDirectiveNode[], + inputField as InputFieldDetailInfo, + ) + inputField.preview = await helpers.getPreview( + (field.directives || []) as readonly ConstDirectiveNode[], + inputField as InputFieldDetailInfo, + previewsPerVersion, + ) + + inputFields.push(inputField as InputFieldDetailInfo) + }), + ) + } + + inputObject.inputFields = sortBy(inputFields, 'name') + + data.inputObjects.push(inputObject as InputObjectInfo) + return + } + + // SCALARS + if (def.kind === 'ScalarTypeDefinition') { + const scalar: ScalarInfo = { + name: def.name.value, + kind: helpers.getKind(def.kind), + id: helpers.getId(def.name.value), + href: helpers.getFullLink('scalars', helpers.getId(def.name.value)), + description: await helpers.getDescription(def.description?.value || ''), + isDeprecated: helpers.getDeprecationStatus( + (def.directives || []) as readonly ConstDirectiveNode[], + ), + deprecationReason: await helpers.getDeprecationReason( + (def.directives || []) as readonly ConstDirectiveNode[], + { + name: def.name.value, + }, + ), + preview: await helpers.getPreview( + (def.directives || []) as readonly ConstDirectiveNode[], + { name: def.name.value }, + previewsPerVersion, + ), + } + data.scalars.push(scalar) + } + }), + ) + + // add non-schema scalars and sort all scalars alphabetically + data.scalars = sortBy(data.scalars.concat(externalScalars), 'name') + + // sort all the types alphabetically + data.queries = sortBy(data.queries, 'name') + data.mutations = sortBy(data.mutations, 'name') + data.objects = sortBy(data.objects, 'name') + data.interfaces = sortBy(data.interfaces, 'name') + data.enums = sortBy(data.enums, 'name') + data.unions = sortBy(data.unions, 'name') + data.inputObjects = sortBy(data.inputObjects, 'name') + data.scalars = sortBy(data.scalars, 'name') + + return data +} diff --git a/src/graphql/scripts/utils/schema-helpers.ts b/src/graphql/scripts/utils/schema-helpers.ts index 61cee65e4d92..1d1968276068 100644 --- a/src/graphql/scripts/utils/schema-helpers.ts +++ b/src/graphql/scripts/utils/schema-helpers.ts @@ -9,6 +9,7 @@ import { isInputObjectType, GraphQLSchema, } from 'graphql' +import type { ConstDirectiveNode } from 'graphql/language' import path from 'path' interface GraphQLTypeInfo { @@ -42,11 +43,6 @@ interface ArgumentNode { type: any // GraphQL AST type nodes have complex nested structure } -interface DirectiveNode { - name: { value: string } - arguments: Array<{ value: { value: string; kind?: string } }> -} - interface SchemaMember { name: string isDeprecated?: boolean @@ -96,7 +92,7 @@ async function getArguments( } async function getDeprecationReason( - directives: DirectiveNode[], + directives: readonly ConstDirectiveNode[], schemaMember: SchemaMember, ): Promise { if (!schemaMember.isDeprecated) return @@ -108,10 +104,15 @@ async function getDeprecationReason( if (deprecationDirective.length > 1) console.log(`more than one deprecation found for ${schemaMember.name}`) - return renderContent(deprecationDirective[0].arguments[0].value.value) + const arg = deprecationDirective[0]?.arguments?.[0] + if (!arg) return + // ConstDirectiveNode arguments have deeply nested union types not fully exposed in GraphQL's type definitions + const value = (arg as any).value?.value + if (!value) return + return renderContent(value) } -function getDeprecationStatus(directives: DirectiveNode[]): boolean | undefined { +function getDeprecationStatus(directives: readonly ConstDirectiveNode[]): boolean | undefined { if (!directives.length) return return directives[0].name.value === 'deprecated' @@ -137,7 +138,7 @@ function getKind(type: string): string { } async function getPreview( - directives: DirectiveNode[], + directives: readonly ConstDirectiveNode[], schemaMember: SchemaMember, previewsPerVersion: PreviewInfo[], ): Promise { @@ -152,9 +153,13 @@ async function getPreview( console.log(`more than one preview found for ${schemaMember.name}`) // an input object's input field may have a ListValue directive that is not relevant to previews - if (previewDirective[0].arguments[0].value.kind !== 'StringValue') return + const firstArg = previewDirective[0]?.arguments?.[0] + if (!firstArg) return + // ConstDirectiveNode arguments have deeply nested union types not fully exposed in GraphQL's type definitions + const argValue = (firstArg as any).value + if (!argValue || argValue.kind !== 'StringValue') return - const previewName = previewDirective[0].arguments[0].value.value + const previewName = argValue.value const preview = previewsPerVersion.find((p) => p.toggled_by.includes(previewName)) if (!preview) console.error(`cannot find "${previewName}" in graphql_previews.yml`) From 1377b385ceef338aca00e672c38ecd407dbee565 Mon Sep 17 00:00:00 2001 From: "T. Greg Doucette" <58960990+LawDevNull@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:06:08 -0400 Subject: [PATCH 7/7] Create page for DPA-covered Previews (#58100) --- .../github-terms/github-dpa-previews.md | 19 +++++++++++++++++++ content/site-policy/github-terms/index.md | 1 + 2 files changed, 20 insertions(+) create mode 100644 content/site-policy/github-terms/github-dpa-previews.md diff --git a/content/site-policy/github-terms/github-dpa-previews.md b/content/site-policy/github-terms/github-dpa-previews.md new file mode 100644 index 000000000000..91c5d24e4476 --- /dev/null +++ b/content/site-policy/github-terms/github-dpa-previews.md @@ -0,0 +1,19 @@ +--- +title: GitHub DPA-Covered Previews +allowTitleToDifferFromFilename: true +versions: + fpt: '*' +topics: + - Policy + - Legal +--- + +## GitHub DPA-Covered Previews + +The pre-release products and features listed below use data handling that is the same as when the software becomes generally available. Under [GitHub’s Pre-Release License Terms](https://gh.io/preview-terms), this software is governed by [the GitHub Data Protection Agreement](https://gh.io/dpa) as of the date listed, and the Customer is the Data Controller beginning on that date. + +| Product / Feature | DPA Coverage Date | +|:---------------------------------------------------|:-------------------:| +| Copilot CLI | 28 October 2025 | +| Copilot Enterprise & Users Usage Metrics Dashboard | 28 October 2025 | +| Spark | 28 October 2025 | diff --git a/content/site-policy/github-terms/index.md b/content/site-policy/github-terms/index.md index cfd4e9d265b5..a61c00840074 100644 --- a/content/site-policy/github-terms/index.md +++ b/content/site-policy/github-terms/index.md @@ -12,6 +12,7 @@ children: - /github-community-guidelines - /github-community-code-of-conduct - /github-pre-release-license-terms + - /github-dpa-previews - /github-sponsors-additional-terms - /github-registered-developer-agreement - /github-marketplace-terms-of-service