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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# ---------------------------------------------------------------
# To update the sha:
# https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble
FROM ghcr.io/github/gh-base-image/gh-base-noble:20250924-191915-gc04d4a50b AS base
FROM ghcr.io/github/gh-base-image/gh-base-noble:20250929-093120-g65a62eb8c AS base

# Install curl for Node install and determining the early access branch
# Install git for cloning docs-early-access & translations repos
Expand Down
24 changes: 23 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,29 @@ import fs from 'fs'
import path from 'path'

import frontmatter from '@gr2m/gray-matter'
import { getLogLevelNumber } from './src/observability/logger/lib/log-levels.js'
// Hardcoded log level function since next.config.js cannot import from TypeScript files
// Matches ./src/observability/logger/lib/log-levels
function getLogLevelNumber() {
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
}

let defaultLogLevel = 'info'
if (
!process.env.LOG_LEVEL &&
(process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test')
) {
defaultLogLevel = 'debug'
}

const envLogLevel = process.env.LOG_LEVEL?.toLowerCase() || defaultLogLevel
const logLevel = LOG_LEVELS[envLogLevel] !== undefined ? envLogLevel : defaultLogLevel

return LOG_LEVELS[logLevel]
}

// Replace imports with hardcoded values
const ROOT = process.env.ROOT || '.'
Expand Down
41 changes: 0 additions & 41 deletions src/content-linter/lib/helpers/schema-utils.js

This file was deleted.

79 changes: 79 additions & 0 deletions src/content-linter/lib/helpers/schema-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getFrontmatter } from './utils'

// AJV validation error object structure
interface AjvValidationError {
instancePath: string
keyword: string
message: string
params: {
additionalProperty?: string
missingProperty?: string
[key: string]: unknown
}
}

// Processed error object for markdown linting
interface ProcessedValidationError {
instancePath: string
detail: string
context: string
errorProperty: string
searchProperty: string
}

export function formatAjvErrors(errors: AjvValidationError[] = []): ProcessedValidationError[] {
const processedErrors: ProcessedValidationError[] = []

errors.forEach((errorObj: AjvValidationError) => {
const error: Partial<ProcessedValidationError> = {}

error.instancePath =
errorObj.instancePath === ''
? errorObj.instancePath
: errorObj.instancePath.slice(1).replace('/', '.')

if (errorObj.keyword === 'additionalProperties') {
error.detail = 'The frontmatter includes an unsupported property.'
const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : ''
error.context = `Remove the property \`${errorObj.params.additionalProperty}\`${pathContext}.`
error.errorProperty = errorObj.params.additionalProperty
error.searchProperty = error.errorProperty
}

// required rule
if (errorObj.keyword === 'required') {
error.detail = 'The frontmatter has a missing required property'
const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : ''
error.context = `Add the missing property \`${errorObj.params.missingProperty}\`${pathContext}`
error.errorProperty = errorObj.params.missingProperty
error.searchProperty = error.instancePath.split('.').pop()
}

// all other rules
if (!error.detail) {
error.detail = `Frontmatter ${errorObj.message}.`
error.context = Object.values(errorObj.params).join('')
error.errorProperty = error.context
error.searchProperty = error.errorProperty
}

processedErrors.push(error as ProcessedValidationError)
})

return processedErrors
}

// Alias for backward compatibility
export const processSchemaValidationErrors = formatAjvErrors

// Schema validator interface - generic due to different schema types (AJV, JSON Schema, etc.)
interface SchemaValidator {
validate(data: unknown): boolean
}

export function getSchemaValidator(
frontmatterLines: string[],
): (schema: SchemaValidator) => boolean {
const frontmatter = getFrontmatter(frontmatterLines)
return (schema: SchemaValidator) => schema.validate(frontmatter)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback } from '../../types'

import { getFrontmatter } from '../helpers/utils'

Expand All @@ -7,7 +9,7 @@ export const frontmatterHiddenDocs = {
description:
'Articles with frontmatter property `hidden` can only be located in specific products',
tags: ['frontmatter', 'feature', 'early-access'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const fm = getFrontmatter(params.lines)
if (!fm || !fm.hidden) return

Expand All @@ -24,7 +26,8 @@ export const frontmatterHiddenDocs = {

if (allowedProductPaths.some((allowedPath) => params.name.includes(allowedPath))) return

const hiddenLine = params.lines.find((line) => line.startsWith('hidden:'))
const hiddenLine = params.lines.find((line: string) => line.startsWith('hidden:'))
if (!hiddenLine) return
const lineNumber = params.lines.indexOf(hiddenLine) + 1

addError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
import { addError, ellipsify } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback } from '../../types'

import { getRange } from '../helpers/utils'
/*
This rule currently only checks for one hardcoded string but
can be generalized in the future to check for strings that
can be generalized in the future to check for strings that
have data reusables.
*/
export const githubOwnedActionReferences = {
names: ['GHD013', 'github-owned-action-references'],
description: 'GitHub-owned action references should not be hardcoded',
tags: ['feature', 'actions'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const filepath = params.name
if (filepath.startsWith('data/reusables/actions/action-')) return

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
import { addError, ellipsify } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback } from '../../types'

import { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter'

/*
This rule currently only checks for one hardcoded string but
can be generalized in the future to check for strings that
can be generalized in the future to check for strings that
have data variables.
*/
export const hardcodedDataVariable = {
names: ['GHD005', 'hardcoded-data-variable'],
description:
'Strings that contain "personal access token" should use the product variable instead',
tags: ['single-source'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
if (params.name.startsWith('data/variables/product.yml')) return
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
isStringQuoted,
isStringPunctuated,
} from '../helpers/utils'
import type { RuleParams, RuleErrorCallback } from '../../types'

export const imageAltTextEndPunctuation = {
names: ['GHD032', 'image-alt-text-end-punctuation'],
description: 'Alternate text for images should end with punctuation',
tags: ['accessibility', 'images'],
parser: 'markdownit',
function: (params, onError) => {
forEachInlineChild(params, 'image', function forToken(token) {
function: (params: RuleParams, onError: RuleErrorCallback) => {
forEachInlineChild(params, 'image', function forToken(token: any) {
const imageAltText = token.content.trim()

// If the alt text is empty, there is nothing to check and you can't
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback } from '../../types'

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

interface ImageToken {
content: string
lineNumber: number
line: string
range: [number, number]
}

export const incorrectAltTextLength = {
names: ['GHD033', 'incorrect-alt-text-length'],
description: 'Images alternate text should be between 40-150 characters',
tags: ['accessibility', 'images'],
parser: 'markdownit',
asynchronous: true,
function: (params, onError) => {
forEachInlineChild(params, 'image', async function forToken(token) {
function: (params: RuleParams, onError: RuleErrorCallback) => {
forEachInlineChild(params, 'image', async function forToken(token: ImageToken) {
let renderedString = token.content

if (token.content.includes('{%') || token.content.includes('{{')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
import { addError, filterTokens } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback } from '../../types'

import { doesStringEndWithPeriod, getRange, isStringQuoted } from '../helpers/utils'

Expand All @@ -7,8 +9,8 @@ export const linkPunctuation = {
description: 'Internal link titles must not contain punctuation',
tags: ['links', 'url'],
parser: 'markdownit',
function: (params, onError) => {
filterTokens(params, 'inline', (token) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
filterTokens(params, 'inline', (token: any) => {
const { children, line } = token
let inLink = false
for (const child of children) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ describe('lint learning tracks', () => {
if (yamlFileList.length < 1) return

describe.each(yamlFileList)('%s', (yamlAbsPath) => {
let yamlContent
// Using any type because YAML content structure is dynamic and varies per file
let yamlContent: any

beforeAll(async () => {
const fileContents = await readFile(yamlAbsPath, 'utf8')
yamlContent = await yaml.load(fileContents)
})

test('contains valid liquid', () => {
const toLint = []
Object.values(yamlContent).forEach(({ title, description }) => {
// Using any[] for toLint since it contains mixed string content from various YAML properties
const toLint: any[] = []
// Using any for destructured params as YAML structure varies across different learning track files
Object.values(yamlContent).forEach(({ title, description }: any) => {
toLint.push(title)
toLint.push(description)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { describe, expect, test } from 'vitest'

import { runRule } from '../../lib/init-test'
import { incorrectAltTextLength } from '../../lib/linting-rules/image-alt-text-length'
import type { Rule } from '../../types'

describe(incorrectAltTextLength.names.join(' - '), () => {
test('image with incorrect alt text length fails', async () => {
const markdown = [
`![${'x'.repeat(39)}](./image.png)`,
`![${'x'.repeat(151)}](./image.png)`,
].join('\n')
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(2)
expect(errors[0].lineNumber).toBe(1)
Expand All @@ -22,7 +23,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => {
`![${'x'.repeat(40)}](./image.png)`,
`![${'x'.repeat(150)}](./image.png)`,
].join('\n')
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})
Expand All @@ -33,7 +34,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => {
// Completely empty
'![](/images/this-is-ok.png)',
].join('\n')
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest'

import { runRule } from '../../lib/init-test'
import { internalLinksNoLang } from '../../lib/linting-rules/internal-links-no-lang'
import type { Rule } from '../../types'

describe(internalLinksNoLang.names.join(' - '), () => {
test('internal links with hardcoded language codes fail', async () => {
Expand All @@ -10,7 +11,7 @@ describe(internalLinksNoLang.names.join(' - '), () => {
'[Link to just a landing page in english](/en)',
'[Korean Docs](/ko/actions)',
].join('\n')
const result = await runRule(internalLinksNoLang, { strings: { markdown } })
const result = await runRule(internalLinksNoLang as Rule, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(3)
expect(errors.map((error) => error.lineNumber)).toEqual([1, 2, 3])
Expand All @@ -31,7 +32,7 @@ describe(internalLinksNoLang.names.join(' - '), () => {
// A link that starts with a language code
'[Enterprise](/enterprise/overview)',
].join('\n')
const result = await runRule(internalLinksNoLang, { strings: { markdown } })
const result = await runRule(internalLinksNoLang as Rule, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})
Expand Down
Loading
Loading