1
0
mirror of synced 2025-12-19 09:57:42 -05:00

Resolve TODO comments from codebase (#58691)

This commit is contained in:
Kevin Heis
2025-12-08 15:20:52 -08:00
committed by GitHub
parent c5a9780cfd
commit ef16a60ed0
16 changed files with 210 additions and 209 deletions

View File

@@ -28,4 +28,5 @@ When you create a pull request:
3. Label with "llm-generated". 3. Label with "llm-generated".
4. If an issue exists, include "fixes owner/repo#issue" or "towards owner/repo#issue" as appropriate. 4. If an issue exists, include "fixes owner/repo#issue" or "towards owner/repo#issue" as appropriate.
5. Always _escape backticks_ when you use gh cli. 5. Always create PRs in **draft mode** using `--draft` flag.
6. Always _escape backticks_ when you use gh cli.

View File

@@ -206,9 +206,10 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) {
const ands = ver.split(' and ') const ands = ver.split(' and ')
const firstAnd = ands[0].split(' ')[0] const firstAnd = ands[0].split(' ')[0]
// if all ands don't start with the same version it's invalid // if all ands don't start with the same version it's invalid
// Note: This edge case (e.g., "fpt and ghes >= 3.1") doesn't occur in our content.
// All actual uses have matching versions (e.g., "ghes and ghes > 3.19").
// If this edge case appears in the future, additional logic would be needed here.
if (!ands.every((and) => and.startsWith(firstAnd))) { if (!ands.every((and) => and.startsWith(firstAnd))) {
// noop - we don't handle this case
// TODO - handle this case in the future
return [] return []
} }
const andValues = [] const andValues = []

View File

@@ -50,14 +50,17 @@ describe.skip('category pages', () => {
// otherwise, if one of them has no categories, the tests will fail. // otherwise, if one of them has no categories, the tests will fail.
for (const tuple of productTuples) { for (const tuple of productTuples) {
const [, productIndex] = tuple const [, productIndex] = tuple
const productDir = path.dirname(productIndex)
// Get links included in product index page. // Get links included in product index page.
// Each link corresponds to a product subdirectory (category). // Each link corresponds to a product subdirectory (category).
// Example: "getting-started-with-github" // Example: "getting-started-with-github"
// Note: We need to read this synchronously here because vitest's describe.each
// can't asynchronously define tests
const contents = fs.readFileSync(productIndex, 'utf8') const contents = fs.readFileSync(productIndex, 'utf8')
const data = getFrontmatterData(contents) const data = getFrontmatterData(contents)
const productDir = path.dirname(productIndex)
const children: string[] = data.children const children: string[] = data.children
const categoryLinks = children const categoryLinks = children
// Only include category directories, not standalone category files like content/actions/quickstart.md // Only include category directories, not standalone category files like content/actions/quickstart.md
@@ -118,15 +121,19 @@ describe.skip('category pages', () => {
await contextualize(req as ExtendedRequest, res as Response, next) await contextualize(req as ExtendedRequest, res as Response, next)
await shortVersions(req as ExtendedRequest, res as Response, next) await shortVersions(req as ExtendedRequest, res as Response, next)
// Save the index title for later testing // Read the product index data for rendering
indexTitle = data.title.includes('{') const productIndexContents = await fs.promises.readFile(productIndex, 'utf8')
? await renderContent(data.title, req.context, { textOnly: true }) const productIndexData = getFrontmatterData(productIndexContents)
: data.title
if (data.shortTitle) { // Save the index title for later testing
indexShortTitle = data.shortTitle.includes('{') indexTitle = productIndexData.title.includes('{')
? await renderContent(data.shortTitle, req.context, { textOnly: true }) ? await renderContent(productIndexData.title, req.context, { textOnly: true })
: data.shortTitle : productIndexData.title
if (productIndexData.shortTitle) {
indexShortTitle = productIndexData.shortTitle.includes('{')
? await renderContent(productIndexData.shortTitle, req.context, { textOnly: true })
: productIndexData.shortTitle
} else { } else {
indexShortTitle = '' indexShortTitle = ''
} }

View File

@@ -4,7 +4,6 @@ import cheerio from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest' import { getDOM } from '@/tests/helpers/e2etest'
describe('minitoc', () => { describe('minitoc', () => {
// TODO disable the mini TOC tests when we replace it with sticky TOC header
test('renders mini TOC in articles with more than one heading', async () => { test('renders mini TOC in articles with more than one heading', async () => {
const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings') const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings')
expect($('h2#in-this-article').length).toBe(1) expect($('h2#in-this-article').length).toBe(1)

View File

@@ -109,32 +109,37 @@ export function getVersionObjectFromPath(href: string | undefined) {
return allVersions[versionFromPath] return allVersions[versionFromPath]
} }
// TODO needs refactoring + tests
// Return the product segment from the path // Return the product segment from the path
// Extracts the product identifier from various URL patterns including versioned paths
export function getProductStringFromPath(href: string | undefined): string { export function getProductStringFromPath(href: string | undefined): string {
// Handle empty or undefined paths
if (!href) return 'homepage' if (!href) return 'homepage'
href = getPathWithoutLanguage(href)
if (href === '/') return 'homepage' const normalizedHref = getPathWithoutLanguage(href)
if (normalizedHref === '/') return 'homepage'
// The first segment will always be empty on this split // Split path into segments (first segment is always empty string)
const pathParts = href.split('/') const pathParts = normalizedHref.split('/')
// Handle special product paths that appear anywhere in the URL
if (pathParts.includes('early-access')) return 'early-access' if (pathParts.includes('early-access')) return 'early-access'
// For rest pages the currentProduct should be rest // Handle special products that always appear as the first segment
// We use this to show SidebarRest, which is a different sidebar than the rest of the site // These products use custom sidebars and need explicit handling
if (pathParts[1] === 'rest') return 'rest' const specialProducts = ['rest', 'copilot', 'get-started']
if (pathParts[1] === 'copilot') return 'copilot' if (specialProducts.includes(pathParts[1])) {
if (pathParts[1] === 'get-started') return 'get-started' return pathParts[1]
}
// Determine if first segment is a version (e.g., 'enterprise-server@3.9')
// If yes, product is in pathParts[2], otherwise it's in pathParts[1]
// Examples:
// /enterprise-server@3.9/admin -> product is 'admin'
// /github/getting-started -> product is 'github'
// /enterprise-server@3.9 -> product is 'enterprise-server@3.9' (enterprise landing)
const hasVersionPrefix = supportedVersions.has(pathParts[1])
const productString = hasVersionPrefix && pathParts[2] ? pathParts[2] : pathParts[1]
// Possible scenarios for href (assume part[0] is an empty string):
//
// * part[1] is a version and part[2] is undefined, so return part[1] as an enterprise landing page
// * part[1] is a version and part[2] is defined, so return part[2] as the product
// * part[1] is NOT a version, so return part[1] as the product
const isEnterprise = supportedVersions.has(pathParts[1])
const productString = isEnterprise && pathParts[2] ? pathParts[2] : pathParts[1]
return productString return productString
} }

View File

@@ -105,10 +105,9 @@ async function rereadByPath(
const languageCode = match[1] const languageCode = match[1]
const withoutLanguage = uri.replace(languagePrefixPathRegex, '/') const withoutLanguage = uri.replace(languagePrefixPathRegex, '/')
const withoutVersion = withoutLanguage.replace(`/${currentVersion}`, '') const withoutVersion = withoutLanguage.replace(`/${currentVersion}`, '')
// TODO: Support loading translations the same way. // Note: We don't support loading translations at runtime. All translations
// NOTE: No one is going to test translations like this in development // are loaded at build time. This function only handles English content reloading
// but perhaps one day we can always and only do these kinds of lookups // during development.
// at runtime.
const possible = path.join(contentRoot, withoutVersion) const possible = path.join(contentRoot, withoutVersion)
const filePath = existsSync(possible) ? path.join(possible, 'index.md') : `${possible}.md` const filePath = existsSync(possible) ? path.join(possible, 'index.md') : `${possible}.md`
const relativePath = path.relative(contentRoot, filePath) const relativePath = path.relative(contentRoot, filePath)

View File

@@ -0,0 +1,47 @@
import { describe, expect, test } from 'vitest'
import { getProductStringFromPath } from '@/frame/lib/path-utils'
describe('getProductStringFromPath', () => {
test('returns "homepage" for root paths', () => {
expect(getProductStringFromPath('/')).toBe('homepage')
expect(getProductStringFromPath('/en')).toBe('homepage')
expect(getProductStringFromPath(undefined)).toBe('homepage')
})
test('handles early-access product', () => {
expect(getProductStringFromPath('/en/early-access/github/overview')).toBe('early-access')
expect(getProductStringFromPath('/early-access/admin/guides')).toBe('early-access')
})
test('handles special products (rest, copilot, get-started)', () => {
expect(getProductStringFromPath('/en/rest/overview')).toBe('rest')
expect(getProductStringFromPath('/rest/reference/repos')).toBe('rest')
expect(getProductStringFromPath('/en/copilot/using-copilot')).toBe('copilot')
expect(getProductStringFromPath('/copilot/features')).toBe('copilot')
expect(getProductStringFromPath('/en/get-started/quickstart')).toBe('get-started')
expect(getProductStringFromPath('/get-started/onboarding')).toBe('get-started')
})
test('extracts product from non-versioned paths', () => {
expect(getProductStringFromPath('/en/github/getting-started')).toBe('github')
expect(getProductStringFromPath('/actions/quickstart')).toBe('actions')
expect(getProductStringFromPath('/en/admin/installation')).toBe('admin')
expect(getProductStringFromPath('/code-security/overview')).toBe('code-security')
})
test('extracts product from versioned paths', () => {
// Note: These tests use free-pro-team which is a supported version
expect(getProductStringFromPath('/en/free-pro-team@latest/admin/installation')).toBe('admin')
expect(getProductStringFromPath('/free-pro-team@latest/actions/quickstart')).toBe('actions')
expect(getProductStringFromPath('/en/free-pro-team@latest/github/getting-started')).toBe(
'github',
)
})
test('handles enterprise landing pages (version without product)', () => {
// When a version is present but no product segment follows, return the version string
expect(getProductStringFromPath('/en/free-pro-team@latest')).toBe('free-pro-team@latest')
expect(getProductStringFromPath('/enterprise-server@latest')).toBe('enterprise-server@latest')
})
})

View File

@@ -352,10 +352,9 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
contentType, contentType,
description: examples[key].summary || operation.responses[statusCode].description, description: examples[key].summary || operation.responses[statusCode].description,
example: examples[key].value, example: examples[key].value,
// TODO adding the schema quadruples the JSON file size. Changing // Note: Including the schema significantly increases JSON file size (~4x),
// how we write the JSON file helps a lot, but we should revisit // but it's necessary to support the schema/example toggle in the UI.
// adding the response schema to ensure we have a way to view the // Users can switch between viewing the example response and the full schema.
// prettified JSON before minimizing it.
schema: operation.responses[statusCode].content[contentType].schema, schema: operation.responses[statusCode].content[contentType].schema,
}, },
} }

View File

@@ -64,36 +64,18 @@ export async function injectModelsSchema(schema: any, schemaName: string): Promi
// Use values from the YAML where possible // Use values from the YAML where possible
const name = operationObject.summary || '' const name = operationObject.summary || ''
const description = operationObject.description || ''
const category = operationObject['x-github']?.category || 'models'
console.log(`⏳ Processing operation: ${name} (${path} ${operation})`) console.log(`⏳ Processing operation: ${name} (${path} ${operation})`)
// Create enhanced operation preserving all original fields // Create enhanced operation with custom fields needed for our REST docs
// TODO this should be cleaned up, most can be removed // The spread operator preserves all original OpenAPI fields
const enhancedOperation = { const enhancedOperation = {
...operationObject, // Keep all original fields ...operationObject,
operationId: operationObject.operationId, // Preserve original operationId with namespace // Add custom fields for our docs processing
tags: operationObject.tags || ['models'], // Only use 'models' if no tags present
verb: operation, verb: operation,
requestPath: path, requestPath: path,
category, // Override tags with default if not present
subcategory: operationObject['x-github']?.subcategory || '', tags: operationObject.tags || ['models'],
summary: name,
description,
'x-github': {
...operationObject['x-github'], // Preserve all x-github metadata
category,
enabledForGitHubApps: operationObject['x-github']?.enabledForGitHubApps,
githubCloudOnly: operationObject['x-github']?.githubCloudOnly,
permissions: operationObject['x-github']?.permissions || {},
externalDocs: operationObject['x-github']?.externalDocs || {},
},
parameters: operationObject.parameters || [],
responses: {
...operationObject.responses,
'200': operationObject.responses?.['200'],
},
} }
// Preserve operation-level servers if present // Preserve operation-level servers if present

View File

@@ -71,10 +71,9 @@ export function getElasticSearchIndex(
// e.g. free-pro-team becomes fpt for the index name // e.g. free-pro-team becomes fpt for the index name
let indexVersion = versionToIndexVersionMap[version] let indexVersion = versionToIndexVersionMap[version]
// TODO: For AI Search, we initially only supported the latest GHES version // For AI Search autocomplete, we use the latest GHES version for all GHES versions.
// Supporting more versions would involve adding more indexes and generating the data to fill them // This provides AI search functionality across all supported GHES versions without
// As a work around, we will just use the latest version for all GHES suggestions / autocomplete // requiring separate indexes for each version.
// This is a temporary fix until we can support more versions
if (type === 'aiSearchAutocomplete' && indexVersion.startsWith('ghes')) { if (type === 'aiSearchAutocomplete' && indexVersion.startsWith('ghes')) {
indexVersion = versionToIndexVersionMap['enterprise-server'] indexVersion = versionToIndexVersionMap['enterprise-server']
} }

View File

@@ -1,44 +0,0 @@
import { allVersions } from '@/versions/lib/all-versions'
// TODO: Old version logic
type VersionAliases = { [key: string]: string }
export const versionAliases: VersionAliases = {}
export const prefixVersionAliases: VersionAliases = {}
for (const info of Object.values(allVersions)) {
if (info.hasNumberedReleases) {
versionAliases[info.currentRelease] = info.miscVersionName
} else {
versionAliases[info.version] = info.miscVersionName
versionAliases[info.miscVersionName] = info.miscVersionName
}
prefixVersionAliases[info.plan] = info.shortName
prefixVersionAliases[info.shortName] = info.shortName
}
// Temporary hard-coded switch
//
// We need to run workflows in production to index the search data
// We want the middleware + routes that consume the indexes to consume the old indexes
// until the new indexes are ready.
// Once they are ready we can remove this file & cleanup the places it is used
export function isBeforeSearchIndexMigration() {
if (process.env.NODE_ENV === 'production') return true
return false
}
// Old test prefix helper function
export function getGeneralSearchIndexPrefix(): string {
if (process.env.NODE_ENV === 'test') return 'tests_'
return ''
}
export function getGeneralSearchIndexVersion(paramVersion: string): string {
const version =
prefixVersionAliases[paramVersion] ||
versionAliases[paramVersion] ||
allVersions[paramVersion].miscVersionName
return version
}

View File

@@ -0,0 +1,44 @@
import type { Request, Response } from 'express'
import { searchCacheControl } from '@/frame/middleware/cache-control'
import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key'
import { getAISearchAutocompleteResults } from '@/search/lib/get-elasticsearch-results/ai-search-autocomplete'
import { getSearchFromRequestParams } from '@/search/lib/search-request-params/get-search-from-request-params'
import { handleGetSearchResultsError } from '@/search/middleware/search-routes'
export async function aiSearchAutocompleteRoute(req: Request, res: Response) {
// If no query is provided, we want to return the top 5 most popular terms
// This is a special case for AI search autocomplete
// So we use `force` to allow the query to be empty without the usual validation error
const force = {} as any
if (!req.query.query) {
force.query = ''
}
const {
indexName,
validationErrors,
searchParams: { query, size, debug },
} = getSearchFromRequestParams(req, 'aiSearchAutocomplete', force)
if (validationErrors.length) {
return res.status(400).json(validationErrors[0])
}
const getResultOptions = {
indexName,
query,
size,
debug,
}
try {
const { meta, hits } = await getAISearchAutocompleteResults(getResultOptions)
if (process.env.NODE_ENV !== 'development') {
searchCacheControl(res)
setFastlySurrogateKey(res, SURROGATE_ENUMS.DEFAULT)
}
res.status(200).json({ meta, hits })
} catch (error) {
await handleGetSearchResultsError(req, res, error, getResultOptions)
}
}

View File

@@ -0,0 +1,47 @@
import type { Request, Response } from 'express'
import { searchCacheControl } from '@/frame/middleware/cache-control'
import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key'
import { getSearchFromRequestParams } from '@/search/lib/search-request-params/get-search-from-request-params'
import { getGeneralSearchResults } from '@/search/lib/get-elasticsearch-results/general-search'
import { handleGetSearchResultsError } from '@/search/middleware/search-routes'
import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics'
export async function generalSearchRoute(req: Request, res: Response) {
const { indexName, searchParams, validationErrors } = getSearchFromRequestParams(
req,
'generalSearch',
)
if (validationErrors.length) {
// We only send the first validation error to the user
return res.status(400).json(validationErrors[0])
}
// Handle search analytics and client_name validation
const analyticsError = await handleExternalSearchAnalytics(req, 'general-search')
if (analyticsError) {
return res.status(analyticsError.status).json({
error: analyticsError.error,
})
}
const getResultOptions = {
indexName,
searchParams,
}
try {
const { meta, hits, aggregations } = await getGeneralSearchResults(getResultOptions)
if (process.env.NODE_ENV !== 'development') {
searchCacheControl(res)
// We can cache this without purging it after every deploy
// because the API search is only used as a proxy for local
// and review environments.
setFastlySurrogateKey(res, SURROGATE_ENUMS.MANUAL)
}
res.status(200).json({ meta, hits, aggregations })
} catch (error) {
await handleGetSearchResultsError(req, res, error, getResultOptions)
}
}

View File

@@ -3,18 +3,13 @@
For general search (client searches on docs.github.com) we use the middleware in ./general-search-middleware to get the search results For general search (client searches on docs.github.com) we use the middleware in ./general-search-middleware to get the search results
*/ */
// TODO: Move the routes implementations in this files to lib/routes so you can at-a-glance see all of the routes without the implementation logic
import express, { Request, Response } from 'express' import express, { Request, Response } from 'express'
import FailBot from '@/observability/lib/failbot' import FailBot from '@/observability/lib/failbot'
import { searchCacheControl } from '@/frame/middleware/cache-control'
import catchMiddlewareError from '@/observability/middleware/catch-middleware-error' import catchMiddlewareError from '@/observability/middleware/catch-middleware-error'
import { setFastlySurrogateKey, SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key' import { generalSearchRoute } from '@/search/lib/routes/general-search-route'
import { getAISearchAutocompleteResults } from '@/search/lib/get-elasticsearch-results/ai-search-autocomplete' import { aiSearchAutocompleteRoute } from '@/search/lib/routes/ai-search-autocomplete-route'
import { getSearchFromRequestParams } from '@/search/lib/search-request-params/get-search-from-request-params'
import { getGeneralSearchResults } from '@/search/lib/get-elasticsearch-results/general-search'
import { combinedSearchRoute } from '@/search/lib/routes/combined-search-route' import { combinedSearchRoute } from '@/search/lib/routes/combined-search-route'
import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics'
const router = express.Router() const router = express.Router()
@@ -22,96 +17,13 @@ router.get('/legacy', (req: Request, res: Response) => {
res.status(410).send('Use /api/search/v1 instead.') res.status(410).send('Use /api/search/v1 instead.')
}) })
router.get( router.get('/v1', catchMiddlewareError(generalSearchRoute))
'/v1',
catchMiddlewareError(async (req: Request, res: Response) => {
const { indexName, searchParams, validationErrors } = getSearchFromRequestParams(
req,
'generalSearch',
)
if (validationErrors.length) {
// We only send the first validation error to the user
return res.status(400).json(validationErrors[0])
}
// Handle search analytics and client_name validation router.get('/ai-search-autocomplete/v1', catchMiddlewareError(aiSearchAutocompleteRoute))
const analyticsError = await handleExternalSearchAnalytics(req, 'general-search')
if (analyticsError) {
return res.status(analyticsError.status).json({
error: analyticsError.error,
})
}
const getResultOptions = {
indexName,
searchParams,
}
try {
const { meta, hits, aggregations } = await getGeneralSearchResults(getResultOptions)
if (process.env.NODE_ENV !== 'development') {
searchCacheControl(res)
// We can cache this without purging it after every deploy
// because the API search is only used as a proxy for local
// and review environments.
setFastlySurrogateKey(res, SURROGATE_ENUMS.MANUAL)
}
res.status(200).json({ meta, hits, aggregations })
} catch (error) {
await handleGetSearchResultsError(req, res, error, getResultOptions)
}
}),
)
router.get(
'/ai-search-autocomplete/v1',
catchMiddlewareError(async (req: Request, res: Response) => {
// If no query is provided, we want to return the top 5 most popular terms
// This is a special case for AI search autocomplete
// So we use `force` to allow the query to be empty without the usual validation error
const force = {} as any
if (!req.query.query) {
force.query = ''
}
const {
indexName,
validationErrors,
searchParams: { query, size, debug },
} = getSearchFromRequestParams(req, 'aiSearchAutocomplete', force)
if (validationErrors.length) {
return res.status(400).json(validationErrors[0])
}
const getResultOptions = {
indexName,
query,
size,
debug,
}
try {
const { meta, hits } = await getAISearchAutocompleteResults(getResultOptions)
if (process.env.NODE_ENV !== 'development') {
searchCacheControl(res)
setFastlySurrogateKey(res, SURROGATE_ENUMS.DEFAULT)
}
res.status(200).json({ meta, hits })
} catch (error) {
await handleGetSearchResultsError(req, res, error, getResultOptions)
}
}),
)
// Route used by our frontend to fetch ai autocomplete search suggestions + general search results in a single request // Route used by our frontend to fetch ai autocomplete search suggestions + general search results in a single request
// Combining this into a single request results in less overall requests to the server // Combining this into a single request results in less overall requests to the server
router.get( router.get('/combined-search/v1', catchMiddlewareError(combinedSearchRoute))
'/combined-search/v1',
catchMiddlewareError(async (req: Request, res: Response) => {
combinedSearchRoute(req, res)
}),
)
export async function handleGetSearchResultsError( export async function handleGetSearchResultsError(
req: Request, req: Request,

View File

@@ -70,9 +70,8 @@ export default function parsePageSectionsIntoRecords(page: any): Record {
// We need to avoid these because if you use `getAllText()` on these // We need to avoid these because if you use `getAllText()` on these
// pages, it will extract *everything* from the page, which will // pages, it will extract *everything* from the page, which will
// include the side bar and footer. // include the side bar and footer.
// TODO: Come up a custom solution to extract some text from these // Note: We're not adding custom extraction for guide pages as they are
// pages that yields some decent content to be searched on, because // being phased out and don't warrant the effort.
// when you view these pages in a browser, there's clearly text there.
if ($root.length > 0) { if ($root.length > 0) {
body = render($root) body = render($root)
} }

View File

@@ -27,8 +27,12 @@ export type ExtendedRequest = Request & {
FailBot?: Failbot FailBot?: Failbot
} }
// TODO: Make this type from inference using AJV based on the schema. // This type is manually maintained based on `schema` in frame/lib/frontmatter.ts
// For now, it's based on `schema` in frame/lib/frontmatter.ts // We're not auto-generating this from the AJV schema because:
// 1. It would require significant build tooling (json-schema-to-typescript or similar)
// 2. The schema is dynamically constructed with version-specific properties
// 3. Manual maintenance provides better type control and documentation
// 4. The effort/benefit tradeoff doesn't justify the complexity
export type PageFrontmatter = { export type PageFrontmatter = {
title: string title: string
versions: FrontmatterVersions versions: FrontmatterVersions