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
2 changes: 1 addition & 1 deletion .github/workflows/lint-entire-content-data-markdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
REPORT_AUTHOR: docs-bot
REPORT_LABEL: broken content markdown report
REPORT_REPOSITORY: github/docs-content
run: npm run post-lints -- --path /tmp/lint-results.json
run: npm run lint-report -- --path /tmp/lint-results.json

- uses: ./.github/actions/slack-alert
if: ${{ failure() && github.event_name != 'workflow_dispatch' }}
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 @@
| GHD038 | expired-content | Expired content must be remediated. | warning | expired |
| GHD039 | expiring-soon | Content that expires soon should be proactively addressed. | warning | expired |
| [GHD040](https://github.com/github/docs/blob/main/src/content-linter/README.md) | table-liquid-versioning | Tables must use the correct liquid versioning format | error | tables |
| GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting |
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
| GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format |
| GHD043 | link-quotation | Internal link titles must not be surrounded by quotations | error | links, url |
Expand Down
4 changes: 3 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import fs from 'fs'
import path from 'path'

import frontmatter from 'gray-matter'
import { languageKeys } from '#src/languages/lib/languages.js'
import { ROOT } from '#src/frame/lib/constants.js'

// Hard-coded language keys to avoid TypeScript import in config file
const languageKeys = ['en', 'es', 'ja', 'pt', 'zh', 'ru', 'fr', 'ko', 'de']

const homepage = path.posix.join(ROOT, 'content/index.md')
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
const productIds = data.children
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"move-content": "tsx src/content-render/scripts/move-content.js",
"openapi-docs": "tsx src/rest/docs.js",
"playwright-test": "playwright test --config src/fixtures/playwright.config.ts --project=\"Google Chrome\"",
"post-lints": "tsx src/content-linter/scripts/post-lints.js",
"lint-report": "tsx src/content-linter/scripts/lint-report.js",
"postinstall": "cp package-lock.json .installed.package-lock.json && echo \"Updated .installed.package-lock.json\" # see husky/post-checkout and husky/post-merge",
"precompute-pageinfo": "tsx src/article-api/scripts/precompute-pageinfo.ts",
"prepare": "husky src/workflows/husky",
Expand Down
2 changes: 1 addition & 1 deletion src/archives/lib/old-versions-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'path'
import { supported, latest } from '@/versions/lib/enterprise-server-releases.js'
import patterns from '@/frame/lib/patterns.js'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version.js'
import { allVersions } from '@/versions/lib/all-versions.js'
import { allVersions } from '@/versions/lib/all-versions'
const latestNewVersion = `enterprise-server@${latest}`
const oldVersions = ['dotcom'].concat(supported)
const newVersions = Object.keys(allVersions)
Expand Down
2 changes: 1 addition & 1 deletion src/archives/middleware/archived-enterprise-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@/frame/middleware/set-fastly-surrogate-key.js'
import { readCompressedJsonFileFallbackLazily } from '@/frame/lib/read-json-file.js'
import { archivedCacheControl, languageCacheControl } from '@/frame/middleware/cache-control.js'
import { pathLanguagePrefixed, languagePrefixPathRegex } from '@/languages/lib/languages.js'
import { pathLanguagePrefixed, languagePrefixPathRegex } from '@/languages/lib/languages'
import getRedirect, { splitPathByLanguage } from '@/redirects/lib/get-redirect.js'
import getRemoteJSON from '@/frame/lib/get-remote-json.js'
import { ExtendedRequest } from '@/types'
Expand Down
2 changes: 1 addition & 1 deletion src/article-api/scripts/precompute-pageinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { brotliCompressSync } from 'zlib'
import chalk from 'chalk'
import { program, Option } from 'commander'

import { languageKeys } from '@/languages/lib/languages.js'
import { languageKeys } from '@/languages/lib/languages'
import { loadPages, loadUnversionedTree } from '@/frame/lib/page-data.js'
import { CACHE_FILE_PATH, getPageInfo } from '../middleware/article-pageinfo'

Expand Down
2 changes: 1 addition & 1 deletion src/article-api/tests/pagelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { beforeAll, describe, expect, test } from 'vitest'

import { get } from '@/tests/helpers/e2etest.js'

import { allVersionKeys } from '@/versions/lib/all-versions.js'
import { allVersionKeys } from '@/versions/lib/all-versions'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version.js'

describe.each(allVersionKeys)('pagelist api for %s', async (versionKey) => {
Expand Down
2 changes: 1 addition & 1 deletion src/audit-logs/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path'

import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file.js'
import { getOpenApiVersion } from '@/versions/lib/all-versions.js'
import { getOpenApiVersion } from '@/versions/lib/all-versions'
import type {
AuditLogEventT,
CategorizedEvents,
Expand Down
2 changes: 1 addition & 1 deletion src/audit-logs/tests/rendering.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test, vi } from 'vitest'

import { getDOM } from '@/tests/helpers/e2etest.js'
import { allVersions } from '@/versions/lib/all-versions.js'
import { allVersions } from '@/versions/lib/all-versions'
import { getCategorizedAuditLogEvents } from '../lib'

describe('audit log events docs', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/automated-pipelines/lib/update-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { rimraf } from 'rimraf'
import { mkdirp } from 'mkdirp'
import { difference, isEqual } from 'lodash-es'

import { allVersions } from '@/versions/lib/all-versions.js'
import { allVersions } from '@/versions/lib/all-versions'
import getApplicableVersions from '@/versions/lib/get-applicable-versions.js'
import type { MarkdownFrontmatter } from '@/types'

Expand Down
2 changes: 1 addition & 1 deletion src/automated-pipelines/tests/frontmatter-versions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest'

import { supported } from '@/versions/lib/enterprise-server-releases.js'
import { allVersionKeys, allVersions } from '@/versions/lib/all-versions.js'
import { allVersionKeys, allVersions } from '@/versions/lib/all-versions'
import { convertVersionsToFrontmatter } from '../lib/update-markdown.js'

describe('frontmatter versions are generated correctly from automated data', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/codeql-cli/scripts/convert-markdown-for-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { visitParents } from 'unist-util-visit-parents'
import { visit, SKIP } from 'unist-util-visit'
import { remove } from 'unist-util-remove'

import { languageKeys } from '#src/languages/lib/languages.js'
import { languageKeys } from '#src/languages/lib/languages.ts'
import { MARKDOWN_OPTIONS } from '../../content-linter/lib/helpers/unified-formatter-options.js'

const { targetDirectory, removeKeywords } = JSON.parse(
Expand Down
4 changes: 2 additions & 2 deletions src/content-linter/lib/helpers/get-lintable-yml.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import yaml from 'js-yaml'
import fs from 'fs/promises'

import dataSchemas from '#src/data-directory/lib/data-schemas/index.js'
import ajv from '#src/tests/lib/validate-json-schema.js'
import dataSchemas from '#src/data-directory/lib/data-schemas/index.ts'
import ajv from '#src/tests/lib/validate-json-schema.ts'

// AJV already has a built-in way to extract out properties
// with a specific keyword using a custom validator function.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { addError } from 'markdownlint-rule-helpers'

import { liquid } from '#src/content-render/index.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { allVersions } from '#src/versions/lib/all-versions.ts'
import { forEachInlineChild, getRange } from '../helpers/utils.js'

export const incorrectAltTextLength = {
Expand Down
2 changes: 2 additions & 0 deletions src/content-linter/lib/linting-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { raiReusableUsage } from './rai-reusable-usage.js'
import { imageNoGif } from './image-no-gif.js'
import { expiredContent, expiringSoon } from './expired-content.js'
import { tableLiquidVersioning } from './table-liquid-versioning.js'
import { tableColumnIntegrity } from './table-column-integrity.js'
import { thirdPartyActionPinning } from './third-party-action-pinning.js'
import { liquidTagWhitespace } from './liquid-tag-whitespace.js'
import { linkQuotation } from './link-quotation.js'
Expand Down Expand Up @@ -85,6 +86,7 @@ export const gitHubDocsMarkdownlint = {
expiredContent,
expiringSoon,
tableLiquidVersioning,
tableColumnIntegrity,
thirdPartyActionPinning,
liquidTagWhitespace,
linkQuotation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { filterTokens } from 'markdownlint-rule-helpers'

import { addFixErrorDetail, getRange } from '../helpers/utils.js'
import { allLanguageKeys } from '#src/languages/lib/languages.js'
import { allLanguageKeys } from '#src/languages/lib/languages.ts'

export const internalLinksNoLang = {
names: ['GHD002', 'internal-links-no-lang'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../helpers/liquid-utils.js'
import { getFrontmatter, getFrontmatterLines } from '../helpers/utils.js'
import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { allVersions } from '#src/versions/lib/all-versions.ts'
import { difference } from 'lodash-es'
import { convertVersionsToFrontmatter } from '#src/automated-pipelines/lib/update-markdown.js'
import {
Expand Down
2 changes: 1 addition & 1 deletion src/content-linter/lib/linting-rules/liquid-versioning.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TokenKind } from 'liquidjs'
import { addError } from 'markdownlint-rule-helpers'

import { getRange, addFixErrorDetail } from '../helpers/utils.js'
import { allVersions, allVersionShortnames } from '#src/versions/lib/all-versions.js'
import { allVersions, allVersionShortnames } from '#src/versions/lib/all-versions.ts'
import {
supported,
next,
Expand Down
134 changes: 134 additions & 0 deletions src/content-linter/lib/linting-rules/table-column-integrity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { addError } from 'markdownlint-rule-helpers'
import { getRange } from '../helpers/utils.js'
import frontmatter from '#src/frame/lib/read-frontmatter.js'

// Regex to detect table rows (must start with |, contain at least one more |, and end with optional whitespace)
const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/
// Regex to detect table separator rows (contains only |, :, -, and whitespace)
const TABLE_SEPARATOR_REGEX = /^\s*\|[\s\-:|\s]*\|\s*$/
// Regex to detect Liquid-only cells (whitespace, liquid tag, whitespace)
const LIQUID_ONLY_CELL_REGEX = /^\s*{%\s*(ifversion|else|endif|elsif).*%}\s*$/

/**
* Counts the number of columns in a table row by splitting on | and handling edge cases
*/
function countColumns(row) {
// Remove leading and trailing whitespace
const trimmed = row.trim()

// Handle empty rows
if (!trimmed || !trimmed.includes('|')) {
return 0
}

// Split by | and filter out empty cells at start/end (from leading/trailing |)
const cells = trimmed.split('|')

// Remove first and last elements if they're empty (from leading/trailing |)
if (cells.length > 0 && cells[0].trim() === '') {
cells.shift()
}
if (cells.length > 0 && cells[cells.length - 1].trim() === '') {
cells.pop()
}

return cells.length
}

/**
* Checks if a table row contains only Liquid conditionals
*/
function isLiquidOnlyRow(row) {
const trimmed = row.trim()
if (!trimmed.includes('|')) return false

const cells = trimmed.split('|')
// Remove empty cells from leading/trailing |
const filteredCells = cells.filter((cell, index) => {
if (index === 0 && cell.trim() === '') return false
if (index === cells.length - 1 && cell.trim() === '') return false
return true
})

// Check if all cells contain only Liquid tags
return (
filteredCells.length > 0 && filteredCells.every((cell) => LIQUID_ONLY_CELL_REGEX.test(cell))
)
}

export const tableColumnIntegrity = {
names: ['GHD047', 'table-column-integrity'],
description: 'Tables must have consistent column counts across all rows',
tags: ['tables', 'accessibility', 'formatting'],
severity: 'error',
function: (params, onError) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data
if (fm && fm.autogenerated) return

const lines = params.lines
let inTable = false
let expectedColumnCount = null
let tableStartLine = null
let headerRow = null

for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const isTableRow = TABLE_ROW_REGEX.test(line)
const isSeparatorRow = TABLE_SEPARATOR_REGEX.test(line)

// Check if we're starting a new table
if (!inTable && isTableRow) {
// Look ahead to see if next line is a separator (confirming this is a table)
const nextLine = lines[i + 1]
if (nextLine && TABLE_SEPARATOR_REGEX.test(nextLine)) {
inTable = true
tableStartLine = i + 1
headerRow = line
expectedColumnCount = countColumns(line)
continue
}
}

// Check if we're ending a table
if (inTable && !isTableRow) {
inTable = false
expectedColumnCount = null
tableStartLine = null
headerRow = null
continue
}

// If we're in a table, validate column count
if (inTable && isTableRow && !isSeparatorRow) {
// Skip Liquid-only rows as they're allowed to have different column counts
if (isLiquidOnlyRow(line)) {
continue
}

const actualColumnCount = countColumns(line)

if (actualColumnCount !== expectedColumnCount) {
const range = getRange(line, line.trim())
let errorMessage

if (actualColumnCount > expectedColumnCount) {
errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${actualColumnCount - expectedColumnCount} more column(s) to the header row to match this row.`
} else {
errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${expectedColumnCount - actualColumnCount} missing column(s) to this row.`
}

addError(
onError,
i + 1,
errorMessage,
line,
range,
null, // No auto-fix available due to complexity
)
}
}
}
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import yaml from 'js-yaml'
import { addError, filterTokens } from 'markdownlint-rule-helpers'

import { liquid } from '#src/content-render/index.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { allVersions } from '#src/versions/lib/all-versions.ts'

// Detects third-party actions in the format `owner/repo@ref`
const actionRegex = /[\w-]+\/[\w-]+@[\w-]+/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import yaml from 'js-yaml'
import { addError, filterTokens } from 'markdownlint-rule-helpers'

import { liquid } from '#src/content-render/index.js'
import { allVersions } from '#src/versions/lib/all-versions.js'
import { allVersions } from '#src/versions/lib/all-versions.ts'

const scheduledYamlJobs = []

Expand Down
2 changes: 1 addition & 1 deletion src/content-linter/scripts/lint-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { defaultConfig } from '../lib/default-markdownlint-options.js'
import { prettyPrintResults } from './pretty-print-results.js'
import { getLintableYml } from '#src/content-linter/lib/helpers/get-lintable-yml.js'
import { printAnnotationResults } from '../lib/helpers/print-annotations.js'
import languages from '#src/languages/lib/languages.js'
import languages from '#src/languages/lib/languages.ts'

program
.description('Run GitHub Docs Markdownlint rules.')
Expand Down
Loading
Loading