From 2ed1451c709630dee02168951a6f0ae17994d867 Mon Sep 17 00:00:00 2001 From: Sarah Schneider Date: Wed, 5 Nov 2025 12:30:37 -0500 Subject: [PATCH 1/2] Deprecate header-content-requirement (#58348) --- .../contributing/content-linter-rules.md | 1 - .../header-content-requirement.ts | 113 ----- src/content-linter/lib/linting-rules/index.ts | 2 - src/content-linter/scripts/lint-content.ts | 2 +- src/content-linter/style/github-docs.ts | 6 - .../tests/unit/header-content-requirement.ts | 385 ------------------ .../tests/unit/rule-filtering.ts | 16 +- 7 files changed, 8 insertions(+), 517 deletions(-) delete mode 100644 src/content-linter/lib/linting-rules/header-content-requirement.ts delete mode 100644 src/content-linter/tests/unit/header-content-requirement.ts diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index 144e24c7f19a..a2555eb777e0 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -57,7 +57,6 @@ | GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases | | GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting | | GHD051 | frontmatter-versions-whitespace | Versions frontmatter should not contain unnecessary whitespace | warning | frontmatter, versions | -| GHD053 | header-content-requirement | Headers must have content between them, such as an introduction | warning | headers, structure, content | | GHD054 | third-party-actions-reusable | Code examples with third-party actions must include disclaimer reusable | warning | actions, reusable, third-party | | GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended | | GHD057 | ctas-schema | CTA URLs must conform to the schema | error | ctas, schema, urls | diff --git a/src/content-linter/lib/linting-rules/header-content-requirement.ts b/src/content-linter/lib/linting-rules/header-content-requirement.ts deleted file mode 100644 index b68142b95151..000000000000 --- a/src/content-linter/lib/linting-rules/header-content-requirement.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { addError, filterTokens } from 'markdownlint-rule-helpers' - -import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types' - -interface HeadingInfo { - token: MarkdownToken - lineNumber: number - level: number - line: string -} - -export const headerContentRequirement = { - names: ['GHD053', 'header-content-requirement'], - description: 'Headers must have content between them, such as an introduction', - tags: ['headers', 'structure', 'content'], - function: (params: RuleParams, onError: RuleErrorCallback) => { - const headings: HeadingInfo[] = [] - - // Collect all heading tokens with their line numbers and levels - filterTokens(params, 'heading_open', (token: MarkdownToken) => { - headings.push({ - token, - lineNumber: token.lineNumber, - level: parseInt(token.tag!.slice(1)), // Extract number from h1, h2, etc. - line: params.lines[token.lineNumber - 1], - }) - }) - - // Check each pair of consecutive headings - for (let i = 0; i < headings.length - 1; i++) { - const currentHeading = headings[i] - const nextHeading = headings[i + 1] - - // Only check if next heading is a subheading (higher level number) - if (nextHeading.level > currentHeading.level) { - const hasContent = checkForContentBetweenHeadings( - params.lines, - currentHeading.lineNumber, - nextHeading.lineNumber, - ) - - if (!hasContent) { - addError( - onError, - nextHeading.lineNumber, - `Header must have introductory content before subheader. Add content between "${currentHeading.line.trim()}" and "${nextHeading.line.trim()}".`, - nextHeading.line, - null, // No specific range within the line - null, // No fix possible - requires manual content addition - ) - } - } - } - }, -} - -/** - * Check if there is meaningful content between two headings - * Returns true if content exists, false if only whitespace/empty lines - */ -function checkForContentBetweenHeadings( - lines: string[], - startLineNumber: number, - endLineNumber: number, -): boolean { - // Convert to 0-based indexes and skip the heading lines themselves - const startIndex = startLineNumber // Skip the current heading line - const endIndex = endLineNumber - 2 // Stop before the next heading line - - // Check each line between the headings - for (let i = startIndex; i <= endIndex; i++) { - if (i >= lines.length) break - - const line = lines[i].trim() - - // Skip empty lines - if (line === '') continue - - // Skip frontmatter delimiters - if (line === '---') continue - - // Skip Liquid tags that don't produce visible content - if (isNonContentLiquidTag(line)) continue - - // If we find any other content, consider it valid - if (line.length > 0) { - return true - } - } - - return false -} - -/** - * Check if a line contains only Liquid tags that don't produce visible content - * This helps avoid false positives for conditional blocks - */ -function isNonContentLiquidTag(line: string): boolean { - // Match common non-content Liquid tags - const nonContentTags = [ - /^{%\s*ifversion\s+.*%}$/, - /^{%\s*elsif\s+.*%}$/, - /^{%\s*else\s*%}$/, - /^{%\s*endif\s*%}$/, - /^{%\s*if\s+.*%}$/, - /^{%\s*unless\s+.*%}$/, - /^{%\s*endunless\s*%}$/, - /^{%\s*comment\s*%}$/, - /^{%\s*endcomment\s*%}$/, - ] - - return nonContentTags.some((pattern) => pattern.test(line)) -} diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 10a8ecaf49bb..233de0259637 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -46,7 +46,6 @@ import { octiconAriaLabels } from '@/content-linter/lib/linting-rules/octicon-ar import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions' import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology' import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace' -import { headerContentRequirement } from '@/content-linter/lib/linting-rules/header-content-requirement' import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable' import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended' import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema' @@ -111,7 +110,6 @@ export const gitHubDocsMarkdownlint = { outdatedReleasePhaseTerminology, // GHD046 tableColumnIntegrity, // GHD047 frontmatterVersionsWhitespace, // GHD051 - headerContentRequirement, // GHD053 thirdPartyActionsReusable, // GHD054 frontmatterLandingRecommended, // GHD056 ctasSchema, // GHD057 diff --git a/src/content-linter/scripts/lint-content.ts b/src/content-linter/scripts/lint-content.ts index ae189ba98307..aae98ca2ee41 100755 --- a/src/content-linter/scripts/lint-content.ts +++ b/src/content-linter/scripts/lint-content.ts @@ -738,7 +738,7 @@ function getCustomRule(ruleName: string): Rule | MarkdownlintRule { } // Check if a rule should be included based on user-specified rules -// Handles both short names (e.g., GHD053, MD001) and long names (e.g., header-content-requirement, heading-increment) +// Handles both short names (e.g., GHD047, MD001) and long names (e.g., table-column-integrity, heading-increment) export function shouldIncludeRule(ruleName: string, runRules: string[]) { // First check if the rule name itself is in the list if (runRules.includes(ruleName)) { diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts index edccc56973e0..aa914dc108a5 100644 --- a/src/content-linter/style/github-docs.ts +++ b/src/content-linter/style/github-docs.ts @@ -186,12 +186,6 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'header-content-requirement': { - // GHD053 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, 'third-party-actions-reusable': { // GHD054 severity: 'warning', diff --git a/src/content-linter/tests/unit/header-content-requirement.ts b/src/content-linter/tests/unit/header-content-requirement.ts deleted file mode 100644 index 67d262662bbd..000000000000 --- a/src/content-linter/tests/unit/header-content-requirement.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { runRule } from '../../lib/init-test' -import { headerContentRequirement } from '../../lib/linting-rules/header-content-requirement' - -// Configure the test fixture to not split frontmatter and content -const fmOptions = { markdownlintOptions: { frontMatter: null } } - -describe(headerContentRequirement.names.join(' - '), () => { - describe('valid cases', () => { - test('should pass when headers have content between them', async () => { - const content = `--- -title: Test ---- - -# Main Header - -This is introductory content for the main section. - -## Subheader - -This section has proper introduction content. - -### Nested Subheader - -More content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should pass when headers are at the same level', async () => { - const content = `--- -title: Test ---- - -# First Header - -Some content here. - -# Second Header - -More content here. - -# Third Header - -Even more content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should pass when subheader goes back to higher level', async () => { - const content = `--- -title: Test ---- - -## Level 2 Header - -Content here. - -# Level 1 Header - -This is fine - going from h2 to h1. - -## Another Level 2 - -More content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should pass with liquid conditionals between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - -Some intro content. - -{% ifversion fpt %} - -## Conditional Subheader - -This content appears conditionally. - -{% endif %} -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should pass with lists between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - -* Item 1 -* Item 2 -* Item 3 - -## Subheader - -More content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should pass with code blocks between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - -\`\`\`javascript -console.log('hello'); -\`\`\` - -## Subheader - -More content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - }) - - describe('invalid cases', () => { - test('should fail when h1 is immediately followed by h2', async () => { - const content = `--- -title: Test ---- - -# Main Header - -## Subheader - -Content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(1) - expect(result.content[0].errorDetail).toContain('Header must have introductory content') - expect(result.content[0].errorDetail).toContain('Main Header') - expect(result.content[0].errorDetail).toContain('Subheader') - }) - - test('should fail when h2 is immediately followed by h3', async () => { - const content = `--- -title: Test ---- - -# Main Header - -Some content here. - -## Level 2 Header - -### Level 3 Header - -Content at level 3. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(1) - expect(result.content[0].errorDetail).toContain('Level 2 Header') - expect(result.content[0].errorDetail).toContain('Level 3 Header') - }) - - test('should fail with multiple consecutive violations', async () => { - const content = `--- -title: Test ---- - -# Main Header - -## Level 2 Header - -### Level 3 Header - -#### Level 4 Header - -Content finally appears here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(3) // Three violations: h1->h2, h2->h3, h3->h4 - }) - - test('should fail when only whitespace between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - - -## Subheader - -Content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(1) - }) - - test('should fail when only liquid tags without content between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - -{% ifversion fpt %} -{% endif %} - -## Subheader - -Content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(1) - }) - }) - - describe('edge cases', () => { - test('should handle document with only one header', async () => { - const content = `--- -title: Test ---- - -# Only Header - -Some content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should handle deep nesting correctly', async () => { - const content = `--- -title: Test ---- - -# H1 - -Content for h1. - -## H2 - -Content for h2. - -### H3 - -Content for h3. - -#### H4 - -Content for h4. - -##### H5 - -Content for h5. - -###### H6 - -Content for h6. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should handle mixed content types between headers', async () => { - const content = `--- -title: Test ---- - -# Main Header - -Some text content. - -> Blockquote content. - -{% data reusables.example.reusable %} - -## Subheader - -More content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - - test('should handle frontmatter correctly', async () => { - const content = `--- -title: Test -versions: - fpt: '*' - ghec: '*' ---- - -# Main Header - -## Subheader - -Content here. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(1) - }) - - test('should handle complex liquid blocks', async () => { - const content = `--- -title: Test ---- - -# Main Header - -{% ifversion fpt %} -This content appears in FPT. -{% elsif ghec %} -This content appears in GHEC. -{% else %} -This content appears elsewhere. -{% endif %} - -## Subheader - -More content. -` - const result = await runRule(headerContentRequirement, { - strings: { content }, - ...fmOptions, - }) - expect(result.content.length).toBe(0) - }) - }) -}) diff --git a/src/content-linter/tests/unit/rule-filtering.ts b/src/content-linter/tests/unit/rule-filtering.ts index 22cebe280f97..7488c5f94b76 100644 --- a/src/content-linter/tests/unit/rule-filtering.ts +++ b/src/content-linter/tests/unit/rule-filtering.ts @@ -15,8 +15,8 @@ vi.mock('../../lib/helpers/get-rules', () => ({ ], customRules: [ { - names: ['GHD053', 'header-content-requirement'], - description: 'Headers must have content below them', + names: ['GHD047', 'table-column-integrity'], + description: 'Tables must have consistent column counts across all rows', }, { names: ['GHD001', 'link-punctuation'], @@ -29,9 +29,7 @@ vi.mock('../../lib/helpers/get-rules', () => ({ describe('shouldIncludeRule', () => { test('includes rule by long name', () => { expect(shouldIncludeRule('heading-increment', ['heading-increment'])).toBe(true) - expect(shouldIncludeRule('header-content-requirement', ['header-content-requirement'])).toBe( - true, - ) + expect(shouldIncludeRule('table-column-integrity', ['table-column-integrity'])).toBe(true) }) test('includes built-in rule by short code', () => { @@ -40,19 +38,19 @@ describe('shouldIncludeRule', () => { }) test('includes custom rule by short code', () => { - expect(shouldIncludeRule('header-content-requirement', ['GHD053'])).toBe(true) + expect(shouldIncludeRule('table-column-integrity', ['GHD047'])).toBe(true) expect(shouldIncludeRule('link-punctuation', ['GHD001'])).toBe(true) }) test('excludes rule not in list', () => { expect(shouldIncludeRule('heading-increment', ['MD002'])).toBe(false) - expect(shouldIncludeRule('header-content-requirement', ['GHD001'])).toBe(false) + expect(shouldIncludeRule('table-column-integrity', ['GHD001'])).toBe(false) }) test('handles multiple rules', () => { - const runRules = ['MD001', 'GHD053', 'some-other-rule'] + const runRules = ['MD001', 'GHD047', 'some-other-rule'] expect(shouldIncludeRule('heading-increment', runRules)).toBe(true) - expect(shouldIncludeRule('header-content-requirement', runRules)).toBe(true) + expect(shouldIncludeRule('table-column-integrity', runRules)).toBe(true) expect(shouldIncludeRule('first-heading-h1', runRules)).toBe(false) }) From 2830f699ce401aac932beec4679705d420e173bf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:05:11 -0800 Subject: [PATCH 2/2] Fix broken anchor links in repository custom instructions documentation (#58251) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: johnmog <12915670+johnmog@users.noreply.github.com> Co-authored-by: Bestra <2043348+Bestra@users.noreply.github.com> Co-authored-by: John Mogensen --- .../add-repository-instructions.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/copilot/how-tos/configure-custom-instructions/add-repository-instructions.md b/content/copilot/how-tos/configure-custom-instructions/add-repository-instructions.md index 41a7ccb0a2f1..9a362e0893dc 100644 --- a/content/copilot/how-tos/configure-custom-instructions/add-repository-instructions.md +++ b/content/copilot/how-tos/configure-custom-instructions/add-repository-instructions.md @@ -249,11 +249,11 @@ Once saved, these instructions will apply to the current project in Eclipse that * **Repository-wide custom instructions**, which apply to all requests made in the context of a repository. - These are specified in a `copilot-instructions.md` file in the `.github` directory of the repository. See [Creating repository-wide custom instructions](#creating-repository-wide-custom-instructions). + These are specified in a `copilot-instructions.md` file in the `.github` directory of the repository. See [Creating repository-wide custom instructions](#creating-repository-wide-custom-instructions-1). * **Path-specific custom instructions**, which apply to requests made in the context of files that match a specified path. - These are specified in one or more `NAME.instructions.md` files within the `.github/instructions` directory in the repository. See [Creating path-specific custom instructions](#creating-path-specific-custom-instructions). + These are specified in one or more `NAME.instructions.md` files within the `.github/instructions` directory in the repository. See [Creating path-specific custom instructions](#creating-path-specific-custom-instructions-1). If the path you specify matches a file that {% data variables.product.prodname_copilot_short %} is working on, and a repository-wide custom instructions file also exists, then the instructions from both files are used. @@ -303,11 +303,11 @@ Once saved, these instructions will apply to the current project in Eclipse that * **Repository-wide custom instructions** apply to all requests made in the context of a repository. - These are specified in a `copilot-instructions.md` file in the `.github` directory of the repository. See [Creating repository-wide custom instructions](#creating-repository-wide-custom-instructions-1). + These are specified in a `copilot-instructions.md` file in the `.github` directory of the repository. See [Creating repository-wide custom instructions](#creating-repository-wide-custom-instructions-2). * **Path-specific custom instructions** apply to requests made in the context of files that match a specified path. - These are specified in one or more `NAME.instructions.md` files within the `.github/instructions` directory in the repository. See [Creating path-specific custom instructions](#creating-path-specific-custom-instructions-1). + These are specified in one or more `NAME.instructions.md` files within the `.github/instructions` directory in the repository. See [Creating path-specific custom instructions](#creating-path-specific-custom-instructions-2). If the path you specify matches a file that {% data variables.product.prodname_copilot_short %} is working on, and a repository-wide custom instructions file also exists, then the instructions from both files are used.