Skip to content
Draft
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ DMT includes **9 specialized linters** to validate different aspects of your Dec
| Linter | Purpose | Key Checks |
|--------|---------|------------|
| [**Container**](pkg/linters/container/README.md) | Container configuration validation | Duplicate names, env vars, security contexts, probes, resource limits |
| [**Documentation**](pkg/linters/docs/README.md) | Documentation quality | README presence, bilingual support, no cyrillic in English docs |
| [**Documentation**](pkg/linters/docs/README.md) | Documentation quality | README presence, bilingual support, no cyrillic in English docs, markdown style |
| [**Hooks**](pkg/linters/hooks/README.md) | Hook validation | Hook syntax, ingress configurations |
| [**Images**](pkg/linters/images/README.md) | Image build instructions | Dockerfile best practices, werf configuration |
| [**Module**](pkg/linters/module/README.md) | Module structure | module.yaml format, OpenAPI conversions, oss.yaml, license files |
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/bmatcuk/doublestar v1.3.4
github.com/deckhouse/deckhouse/pkg/log v0.2.1
github.com/fatih/color v1.19.0
github.com/fuldaxxx/go-markdownlint v0.0.0-20260626082153-d77e827f85b0
github.com/go-openapi/spec v0.22.4
github.com/gobwas/glob v0.2.3
github.com/gogo/protobuf v1.3.2
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
Expand Down Expand Up @@ -99,6 +100,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fuldaxxx/go-markdownlint v0.0.0-20260626082153-d77e827f85b0 h1:143MJp2qaqsTQajO5alStfx8rSaJlimNS+AVROx3Y2o=
github.com/fuldaxxx/go-markdownlint v0.0.0-20260626082153-d77e827f85b0/go.mod h1:cXFi4L+kE9juW9qj8GPyctu5kNvDbiq3FX8ckpX9Su0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down
1 change: 1 addition & 0 deletions internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ func mapDocumentationRules(linterSettings *pkg.LintersSettings, configSettings *
rules.ReadmeRule.SetLevel(globalRules.ReadmeRule.Impact, fallbackImpact)
rules.CyrillicInEnglishRule.SetLevel(globalRules.NoCyrillicExcludeRules.Impact, fallbackImpact)
rules.NoLangKeyRule.SetLevel(globalRules.NoLangKeyRule.Impact, fallbackImpact)
rules.MarkdownlintRule.SetLevel(globalRules.MarkdownlintRule.Impact, fallbackImpact)
}

func mapModuleRules(linterSettings *pkg.LintersSettings, configSettings *config.LintersSettings, globalConfig *global.Linters) {
Expand Down
1 change: 1 addition & 0 deletions pkg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type DocumentationLinterRules struct {
BilingualRule RuleConfig
CyrillicInEnglishRule RuleConfig
NoLangKeyRule RuleConfig
MarkdownlintRule RuleConfig
}

type NoCyrillicLinterConfig struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type DocumentationRules struct {
ReadmeRule RuleConfig `mapstructure:"readme"`
NoCyrillicExcludeRules RuleConfig `mapstructure:"cyrillic-in-english"`
NoLangKeyRule RuleConfig `mapstructure:"no-lang-key"`
MarkdownlintRule RuleConfig `mapstructure:"markdownlint"`
}

type OpenAPILinterConfig struct {
Expand Down
69 changes: 68 additions & 1 deletion pkg/linters/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

The **Documentation Linter** validates module documentation to ensure proper structure, completeness, and language consistency. This linter enforces bilingual documentation requirements, checks for documentation file presence, and validates that English documentation doesn't contain cyrillic characters.
The **Documentation Linter** validates module documentation to ensure proper structure, completeness, and language consistency. This linter enforces bilingual documentation requirements, checks for documentation file presence, validates that English documentation doesn't contain cyrillic characters, and ensures markdown files follow deckhouse markdown style conventions.

Proper documentation is critical for Deckhouse modules as it helps users understand module features, configuration options, and usage patterns. The linter ensures documentation meets quality standards and is accessible to both English and Russian-speaking audiences.

Expand All @@ -14,6 +14,7 @@ Proper documentation is critical for Deckhouse modules as it helps users underst
| [bilingual](#bilingual) | Validates documentation exists in both English and Russian | ✅ | enabled |
| [cyrillic-in-english](#cyrillic-in-english) | Validates English documentation doesn't contain cyrillic characters | ✅ | enabled |
| [no-lang-key](#no-lang-key) | Validates documentation front matter doesn't contain `lang` key | ✅ | enabled |
| [markdownlint](#markdownlint) | Validates markdown files in docs/ follow deckhouse markdown style | ✅ | enabled |

## Rule Details

Expand Down Expand Up @@ -390,6 +391,72 @@ linters-settings:

---

### markdownlint

**Purpose:** Ensures markdown files in the `docs/` directory follow consistent deckhouse markdown style conventions (headings, lists, code blocks, etc.).

**Description:**

This rule runs the [go-markdownlint](https://github.com/ldmonster/go-markdownlint) library against every `.md` file under `docs/` (recursively, including `docs/internal/...`) and reports any markdown style violations. The built-in rule set is enabled by default; only a fixed set of deckhouse-specific overrides is applied (line-length limits, blanks-around-headings, duplicate-heading siblings, etc.).

**What it checks:**

1. Recursively scans all `.md` files under `docs/` (top-level and nested, e.g. `docs/internal/`)
2. Lints each file with the built-in markdownlint rules using the deckhouse configuration overrides
3. Reports the rule name(s), description, file path and line number for each violation

**Why it matters:**

Consistent markdown style across all modules makes the documentation easier to read, review and maintain, and keeps it aligned with the rest of the deckhouse documentation.

**Examples:**

❌ **Incorrect** - Duplicate top-level heading (MD025) and missing trailing newline (MD047):

```markdown
<!-- docs/README.md -->
# My Module

# My Module
```
```

(file has no trailing newline)

**Error:**
```
MD025/single-title/single-h1 Multiple top-level headings in the same document
File: docs/README.md
Line: 3

MD047/single-trailing-newline Files should end with a single newline character
File: docs/README.md
Line: 3
```

✅ **Correct** - Single top-level heading and trailing newline:

```markdown
<!-- docs/README.md -->
# My Module
```

**Configuration:**

To downgrade or disable this rule:

```yaml
# .dmt.yaml
linters-settings:
documentation:
rules:
markdownlint:
impact: warn # report findings but don't fail the run
# impact: ignored # disable the rule entirely
```

---

## Configuration

The Documentation linter can be configured at both the module level and for individual rules.
Expand Down
2 changes: 2 additions & 0 deletions pkg/linters/docs/documentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func (l *Documentation) Run(m *module.Module) {
rules.NewCyrillicInEnglishRule().CheckFiles(m, errorList.WithMaxLevel(l.cfg.Rules.CyrillicInEnglishRule.GetLevel()))

rules.NewNoLangKeyRule().CheckFiles(m, errorList.WithMaxLevel(l.cfg.Rules.NoLangKeyRule.GetLevel()))

rules.NewMarkdownRule().CheckFiles(m, errorList.WithMaxLevel(l.cfg.Rules.MarkdownlintRule.GetLevel()))
}

func (l *Documentation) Name() string {
Expand Down
206 changes: 206 additions & 0 deletions pkg/linters/docs/rules/markdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright 2026 Flant JSC
// Licensed under the Apache License, Version 2.0

package rules

import (
"context"
"os"
"path/filepath"
"strings"

gomarkdownlint "github.com/fuldaxxx/go-markdownlint"

"github.com/deckhouse/dmt/internal/fsutils"
"github.com/deckhouse/dmt/pkg"
"github.com/deckhouse/dmt/pkg/errors"
)

const (
MarkdownlintRuleName = "markdownlint"
)

func NewMarkdownRule() *MarkdownRule {
return &MarkdownRule{
RuleMeta: pkg.RuleMeta{
Name: MarkdownlintRuleName,
},
}
}

type MarkdownRule struct {
pkg.RuleMeta
pkg.PathRule
}

func (r *MarkdownRule) CheckFiles(m pkg.Module, errorList *errors.LintRuleErrorsList) {
errorList = errorList.WithRule(r.GetName())

if !r.Enabled(m.GetName()) {
return
}

modulePath := m.GetPath()
if modulePath == "" {
return
}

docsPath := filepath.Join(modulePath, "docs")
if _, err := os.Stat(docsPath); err != nil {
return
}

files := fsutils.GetFiles(docsPath, false, fsutils.FilterFileByExtensions(".md"))

var mdFiles []string

for _, fileName := range files {
relFromModule := fsutils.Rel(modulePath, fileName)
if !r.Enabled(relFromModule) {
continue
}

mdFiles = append(mdFiles, fileName)
}

r.checkFiles(modulePath, mdFiles, errorList)
}

func (r *MarkdownRule) checkFiles(modulePath string, files []string, errorList *errors.LintRuleErrorsList) {
if len(files) == 0 {
return
}

cfg := gomarkdownlint.ConfigFromMap(deckhouseMarkdownlintConfig())

results, err := gomarkdownlint.LintFiles(context.Background(), files, cfg)
if err != nil {
errorList.
WithFilePath(modulePath).
WithValue(err.Error()).
Errorf("markdownlint failed: %s", err)

return
}

for file, errs := range results {
relPath := fsutils.Rel(modulePath, file)
for _, mdErr := range errs {
errorList.
WithFilePath(relPath).
WithLineNumber(mdErr.LineNumber).
Errorf("%s %s", strings.Join(mdErr.RuleNames, "/"), mdErr.RuleDescription)
}
}
}

// deckhouseMarkdownlintConfig returns the markdownlint configuration.
// go-markdownlint enables every built-in rule by default (ruleDefaultEnable is
// true when the "default" key is absent), so we do not set "default" and only
// list the rule overrides below.
func deckhouseMarkdownlintConfig() map[string]any {
return map[string]any{
// MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading (deprecated)
"MD002": false,

// MD004/ul-style - Unordered list style
"MD004": false,

// MD013/line-length - Line length
"MD013": map[string]any{
"line_length": 1000, // Number of characters
"heading_line_length": 128, // Number of characters for headings
"code_block_line_length": 400, // Number of characters for code blocks
"code_blocks": true, // Include code blocks
"tables": true, // Include tables
"headings": true, // Include headings
"headers": true, // Include headings (deprecated alias)
"strict": false, // Strict length checking
"stern": false, // Stern length checking
},

// MD053/link-image-reference-definitions - Link and image reference
// definitions should be needed.
"MD053": false,

// MD059/descriptive-link-text - Link text should be descriptive.
"MD059": false,

// MD060/table-column-style - Table column style.
"MD060": false,

// MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines
"MD022": map[string]any{
"lines_above": 1, // Blank lines above heading
"lines_below": 1, // Blank lines below heading
},

// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
"MD024": map[string]any{
"siblings_only": true, // Only check sibling headings
},

// MD026/no-trailing-punctuation - Trailing punctuation in heading
"MD026": map[string]any{
"punctuation": ".,;:!。,;:!", // Punctuation characters
},

// MD029/ol-prefix - Ordered list item prefix
"MD029": map[string]any{
"style": "one_or_ordered", // List style
},

// MD033/no-inline-html - Inline HTML
"MD033": false,

// MD032/blanks-around-lists - Lists should be surrounded by blank lines
"MD032": false,

// MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading
"MD041": map[string]any{
"level": 1, // Heading level
"front_matter_title": `^\s*title\s*[:=]`, // RegExp for matching title in front matter
},

// MD042/no-empty-links - No empty links
"MD042": true,

// MD043/required-headings/required-headers - Required heading structure
"MD043": map[string]any{
"headings": nil, // List of headings
"headers": nil, // List of headings (deprecated alias)
},

// MD044/proper-names - Proper names should have the correct capitalization
"MD044": map[string]any{
"names": []string{}, // List of proper names
"code_blocks": true, // Include code blocks
},

// MD045/no-alt-text - Images should have alternate text (alt text)
"MD045": true,

// MD046/code-block-style - Code block style
"MD046": map[string]any{
"style": "consistent", // Block style
},

// MD047/single-trailing-newline - Files should end with a single newline character
"MD047": true,

// MD048/code-fence-style - Code fence style
"MD048": map[string]any{
"style": "consistent", // Code fence style
},

// MD049/emphasis-style - Emphasis style should be consistent
"MD049": map[string]any{
"style": "consistent", // Emphasis style should be consistent
},

// MD050/strong-style - Strong style should be consistent
"MD050": map[string]any{
"style": "consistent", // Strong style should be consistent
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description: >-
A module whose docs/CONFIGURATION.md has markdownlint violations: a duplicate
top-level heading (MD025) and a missing trailing newline (MD047). The
documentation linter's markdownlint rule must report both.
expect:
- linter: documentation
rule: markdownlint
textContains: "MD025/single-title/single-h1"
- linter: documentation
rule: markdownlint
textContains: "MD047/single-trailing-newline"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Markdownlint Violations
# Markdownlint Violations
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: test-module-markdownlint-violations
namespace: d8-test
weight: 100
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type: object
properties: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type: object
properties: {}
Loading