Skip to content

Commit 7169e83

Browse files
authored
Merge pull request #42483 from github/repo-sync
Repo sync
2 parents b56371d + 650d266 commit 7169e83

File tree

34 files changed

+671
-24
lines changed

34 files changed

+671
-24
lines changed

config/moda/secrets/production/secrets.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ secrets:
44
kind: latest_at_deployment_start
55
key: COOKIE_SECRET
66
type: salt
7+
owner: '@github/docs-engineering'
8+
externally_usable: true

config/moda/secrets/staging/secrets.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ secrets:
44
kind: latest_at_deployment_start
55
key: COOKIE_SECRET
66
type: salt
7+
owner: '@github/docs-engineering'
8+
externally_usable: true

data/reusables/contributing/content-linter-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
| GHD060 | journey-tracks-unique-ids | Journey track IDs must be unique within a page | error | frontmatter, journey-tracks, unique-ids |
6565
| GHD061 | frontmatter-hero-image | Hero image paths must be absolute, extensionless, and point to valid images in /assets/images/banner-images/ | error | frontmatter, images |
6666
| GHD062 | frontmatter-intro-links | introLinks keys must be valid keys defined in data/ui.yml under product_landing | error | frontmatter, single-source |
67+
| GHD063 | frontmatter-children | Children frontmatter paths must exist. Supports relative paths and absolute /content/ paths for cross-product inclusion. | error | frontmatter, children |
6768
| [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 | |
6869
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
6970
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { addError } from 'markdownlint-rule-helpers'
4+
5+
import { getFrontmatter } from '../helpers/utils'
6+
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
7+
8+
interface Frontmatter {
9+
children?: string[]
10+
[key: string]: unknown
11+
}
12+
13+
/**
14+
* Check if a child path is valid.
15+
* Supports both:
16+
* - Relative paths (e.g., /local-child) resolved from current directory
17+
* - Absolute /content/ paths (e.g., /content/actions/workflows) resolved from content root
18+
*/
19+
function isValidChildPath(childPath: string, currentFilePath: string): boolean {
20+
const ROOT = process.env.ROOT || '.'
21+
const contentDir = path.resolve(ROOT, 'content')
22+
23+
let resolvedPath: string
24+
25+
if (childPath.startsWith('/content/')) {
26+
// Absolute path from content root - strip /content/ prefix
27+
const absoluteChildPath = childPath.slice('/content/'.length)
28+
resolvedPath = path.resolve(contentDir, absoluteChildPath)
29+
} else {
30+
// Relative path from current file's directory
31+
const currentDir: string = path.dirname(currentFilePath)
32+
const normalizedPath = childPath.startsWith('/') ? childPath.substring(1) : childPath
33+
resolvedPath = path.resolve(currentDir, normalizedPath)
34+
}
35+
36+
// Security check: ensure resolved path stays within content directory
37+
// This prevents path traversal attacks using sequences like '../'
38+
if (!resolvedPath.startsWith(contentDir + path.sep) && resolvedPath !== contentDir) {
39+
return false
40+
}
41+
42+
// Check for direct .md file
43+
const mdPath = `${resolvedPath}.md`
44+
if (fs.existsSync(mdPath) && fs.statSync(mdPath).isFile()) {
45+
return true
46+
}
47+
48+
// Check for index.md file in directory
49+
const indexPath = path.join(resolvedPath, 'index.md')
50+
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
51+
return true
52+
}
53+
54+
// Check if the path exists as a directory (may have children)
55+
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
56+
return true
57+
}
58+
59+
return false
60+
}
61+
62+
export const frontmatterChildren = {
63+
names: ['GHD063', 'frontmatter-children'],
64+
description:
65+
'Children frontmatter paths must exist. Supports relative paths and absolute /content/ paths for cross-product inclusion.',
66+
tags: ['frontmatter', 'children'],
67+
function: (params: RuleParams, onError: RuleErrorCallback) => {
68+
const fm = getFrontmatter(params.lines) as Frontmatter | null
69+
if (!fm || !fm.children) return
70+
71+
const childrenLine: string | undefined = params.lines.find((line) =>
72+
line.startsWith('children:'),
73+
)
74+
75+
if (!childrenLine) return
76+
77+
const lineNumber: number = params.lines.indexOf(childrenLine) + 1
78+
79+
if (Array.isArray(fm.children)) {
80+
const invalidPaths: string[] = []
81+
82+
for (const child of fm.children) {
83+
if (!isValidChildPath(child, params.name)) {
84+
invalidPaths.push(child)
85+
}
86+
}
87+
88+
if (invalidPaths.length > 0) {
89+
addError(
90+
onError,
91+
lineNumber,
92+
`Found invalid children paths: ${invalidPaths.join(', ')}. For cross-product paths, use /content/ prefix (e.g., /content/actions/workflows).`,
93+
childrenLine,
94+
[1, childrenLine.length],
95+
null,
96+
)
97+
}
98+
}
99+
},
100+
}

src/content-linter/lib/linting-rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { journeyTracksGuidePathExists } from './journey-tracks-guide-path-exists
5353
import { journeyTracksUniqueIds } from './journey-tracks-unique-ids'
5454
import { frontmatterHeroImage } from './frontmatter-hero-image'
5555
import { frontmatterIntroLinks } from './frontmatter-intro-links'
56+
import { frontmatterChildren } from './frontmatter-children'
5657

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

121123
// Search-replace rules
122124
searchReplace, // Open-source plugin

src/content-linter/style/github-docs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ export const githubDocsFrontmatterConfig = {
284284
'partial-markdown-files': false,
285285
'yml-files': false,
286286
},
287+
'frontmatter-children': {
288+
// GHD063
289+
severity: 'error',
290+
'partial-markdown-files': false,
291+
'yml-files': false,
292+
},
287293
}
288294

289295
// Configures rules from the `github/markdownlint-github` repo

src/content-linter/tests/category-pages.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ describe.skip('category pages', () => {
239239
})
240240

241241
function getPath(productDir: string, link: string, filename: string) {
242+
// Handle absolute /content/ paths for cross-product children
243+
// The link parameter contains the child path from frontmatter
244+
if (link.startsWith('/content/')) {
245+
const absolutePath = link.slice('/content/'.length)
246+
if (filename === 'index') {
247+
return path.join(contentDir, absolutePath, 'index.md')
248+
}
249+
return path.join(contentDir, absolutePath, `${filename}.md`)
250+
}
242251
return path.join(productDir, link, `${filename}.md`)
243252
}
244253

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Invalid children paths
3+
children:
4+
- /content/nonexistent/product
5+
- /another/invalid/path
6+
---
7+
8+
This page has invalid children paths.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: No children
3+
---
4+
5+
This page has no children frontmatter.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Valid children with content prefix
3+
children:
4+
- /content/get-started/foo
5+
- /content/get-started/learning-about-github
6+
---
7+
8+
This page has valid /content/ prefixed children paths.

0 commit comments

Comments
 (0)