Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,5 @@ jobs:
# Enable debug logging when "Re-run jobs with debug logging" is used in GitHub Actions UI
# This will output additional timing and path information to help diagnose timeout issues
RUNNER_DEBUG: ${{ runner.debug }}
run: npm test -- src/${{ matrix.name }}/tests/
VITEST_FLAGS: ${{ matrix.name == 'article-api' && '--no-file-parallelism --maxWorkers=1' || '' }}
run: npm test -- $VITEST_FLAGS src/${{ matrix.name }}/tests/
2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-boxwood/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-cedar/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-cypress/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-fir/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-hemlock/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-holly/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-juniper/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-laurel/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-pine/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-redwood/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-sequoia/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/docs-internal-staging-spruce/secrets.yml

This file was deleted.

2 changes: 2 additions & 0 deletions config/moda/secrets/production/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ secrets:
kind: latest_at_deployment_start
key: COOKIE_SECRET
type: salt
owner: '@github/docs-engineering'
externally_usable: true
2 changes: 0 additions & 2 deletions config/moda/secrets/review-os/secrets.yml

This file was deleted.

2 changes: 0 additions & 2 deletions config/moda/secrets/review/secrets.yml

This file was deleted.

2 changes: 2 additions & 0 deletions config/moda/secrets/staging/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ secrets:
kind: latest_at_deployment_start
key: COOKIE_SECRET
type: salt
owner: '@github/docs-engineering'
externally_usable: true
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ The list of valid values for `source` are:
* Wiki_content
* Wiki_commit
* Npm
* Manual_submission
* Unknown

### Implement signature verification in your secret alert service
Expand Down
1 change: 1 addition & 0 deletions data/reusables/contributing/content-linter-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
| GHD060 | journey-tracks-unique-ids | Journey track IDs must be unique within a page | error | frontmatter, journey-tracks, unique-ids |
| GHD061 | frontmatter-hero-image | Hero image paths must be absolute, extensionless, and point to valid images in /assets/images/banner-images/ | error | frontmatter, images |
| GHD062 | frontmatter-intro-links | introLinks keys must be valid keys defined in data/ui.yml under product_landing | error | frontmatter, single-source |
| GHD063 | frontmatter-children | Children frontmatter paths must exist. Supports relative paths and absolute /content/ paths for cross-product inclusion. | error | frontmatter, children |
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/app/lib/main-context-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function adaptAppRouterContextToMainContext(
oldestSupported: '',
nextDeprecationDate: '',
supported: [],
releasesWithOldestDeprecationDate: [],
},
enterpriseServerVersions: [],
error: '',
Expand Down
17 changes: 3 additions & 14 deletions src/article-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ The `/api/article` endpoints return information about a page by `pathname`.

### Autogenerated Content Transformers

For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture:
For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture.

#### Current Transformers

- **REST Transformer** (`rest-transformer.ts`) - Converts REST API operations into markdown, including endpoints, parameters, status codes, and code examples
- **GraphQL Transformer** (`graphql-transformer.ts`) - Converts GraphQL schema documentation into markdown, including queries, mutations, objects, interfaces, enums, unions, input objects, scalars, changelog, and breaking changes
#### Transformers

To add a new transformer for other autogenerated content types:
1. Create a new transformer file implementing the `PageTransformer` interface
Expand Down Expand Up @@ -195,15 +192,7 @@ npm run test -- src/article-api/tests

- Team: Docs Engineering

## Transformers

Currently implemented transformers:
- **REST API transformer** (`rest-transformer.ts`) - Converts REST API autogenerated content
- **GraphQL transformer** (`graphql-transformer.ts`) - Converts GraphQL API autogenerated content
- **Audit logs transformer** (`audit-logs-transformer.ts`) - Converts audit log tables to markdown

### Known limitations
- Some autogenerated content types don't have transformers yet
- Cache invalidation is manual
- No built-in rate limiting
- No built-in rate limiting (uses Fastly instead)
- Limited API versioning
100 changes: 100 additions & 0 deletions src/content-linter/lib/linting-rules/frontmatter-children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import fs from 'fs'
import path from 'path'
import { addError } from 'markdownlint-rule-helpers'

import { getFrontmatter } from '../helpers/utils'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'

interface Frontmatter {
children?: string[]
[key: string]: unknown
}

/**
* Check if a child path is valid.
* Supports both:
* - Relative paths (e.g., /local-child) resolved from current directory
* - Absolute /content/ paths (e.g., /content/actions/workflows) resolved from content root
*/
function isValidChildPath(childPath: string, currentFilePath: string): boolean {
const ROOT = process.env.ROOT || '.'
const contentDir = path.resolve(ROOT, 'content')

let resolvedPath: string

if (childPath.startsWith('/content/')) {
// Absolute path from content root - strip /content/ prefix
const absoluteChildPath = childPath.slice('/content/'.length)
resolvedPath = path.resolve(contentDir, absoluteChildPath)
} else {
// Relative path from current file's directory
const currentDir: string = path.dirname(currentFilePath)
const normalizedPath = childPath.startsWith('/') ? childPath.substring(1) : childPath
resolvedPath = path.resolve(currentDir, normalizedPath)
}

// Security check: ensure resolved path stays within content directory
// This prevents path traversal attacks using sequences like '../'
if (!resolvedPath.startsWith(contentDir + path.sep) && resolvedPath !== contentDir) {
return false
}

// Check for direct .md file
const mdPath = `${resolvedPath}.md`
if (fs.existsSync(mdPath) && fs.statSync(mdPath).isFile()) {
return true
}

// Check for index.md file in directory
const indexPath = path.join(resolvedPath, 'index.md')
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
return true
}

// Check if the path exists as a directory (may have children)
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
return true
}

return false
}

export const frontmatterChildren = {
names: ['GHD063', 'frontmatter-children'],
description:
'Children frontmatter paths must exist. Supports relative paths and absolute /content/ paths for cross-product inclusion.',
tags: ['frontmatter', 'children'],
function: (params: RuleParams, onError: RuleErrorCallback) => {
const fm = getFrontmatter(params.lines) as Frontmatter | null
if (!fm || !fm.children) return

const childrenLine: string | undefined = params.lines.find((line) =>
line.startsWith('children:'),
)

if (!childrenLine) return

const lineNumber: number = params.lines.indexOf(childrenLine) + 1

if (Array.isArray(fm.children)) {
const invalidPaths: string[] = []

for (const child of fm.children) {
if (!isValidChildPath(child, params.name)) {
invalidPaths.push(child)
}
}

if (invalidPaths.length > 0) {
addError(
onError,
lineNumber,
`Found invalid children paths: ${invalidPaths.join(', ')}. For cross-product paths, use /content/ prefix (e.g., /content/actions/workflows).`,
childrenLine,
[1, childrenLine.length],
null,
)
}
}
},
}
2 changes: 2 additions & 0 deletions src/content-linter/lib/linting-rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { journeyTracksGuidePathExists } from './journey-tracks-guide-path-exists
import { journeyTracksUniqueIds } from './journey-tracks-unique-ids'
import { frontmatterHeroImage } from './frontmatter-hero-image'
import { frontmatterIntroLinks } from './frontmatter-intro-links'
import { frontmatterChildren } from './frontmatter-children'

// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
// The elements in the array have a 'names' property that contains rule identifiers
Expand Down Expand Up @@ -117,6 +118,7 @@ export const gitHubDocsMarkdownlint = {
journeyTracksUniqueIds, // GHD060
frontmatterHeroImage, // GHD061
frontmatterIntroLinks, // GHD062
frontmatterChildren, // GHD063

// Search-replace rules
searchReplace, // Open-source plugin
Expand Down
6 changes: 6 additions & 0 deletions src/content-linter/style/github-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ export const githubDocsFrontmatterConfig = {
'partial-markdown-files': false,
'yml-files': false,
},
'frontmatter-children': {
// GHD063
severity: 'error',
'partial-markdown-files': false,
'yml-files': false,
},
}

// Configures rules from the `github/markdownlint-github` repo
Expand Down
9 changes: 9 additions & 0 deletions src/content-linter/tests/category-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ describe.skip('category pages', () => {
})

function getPath(productDir: string, link: string, filename: string) {
// Handle absolute /content/ paths for cross-product children
// The link parameter contains the child path from frontmatter
if (link.startsWith('/content/')) {
const absolutePath = link.slice('/content/'.length)
if (filename === 'index') {
return path.join(contentDir, absolutePath, 'index.md')
}
return path.join(contentDir, absolutePath, `${filename}.md`)
}
return path.join(productDir, link, `${filename}.md`)
}

Expand Down
Loading