From cfc10bffcf6485f6266ea61b2c98d1d5771de5ba Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Wed, 17 Jun 2026 11:31:48 +0300 Subject: [PATCH 1/8] [dmt] add markdownlint documentation rule Signed-off-by: Ruslan Gorbunov --- go.mod | 1 + go.sum | 3 + internal/module/module.go | 7 +- pkg/config.go | 1 + pkg/config/global/global.go | 1 + pkg/linters/docs/documentation.go | 2 + pkg/linters/docs/rules/markdown.go | 102 +++++++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 pkg/linters/docs/rules/markdown.go diff --git a/go.mod b/go.mod index c6dc1389..b298a665 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/ldmonster/go-markdownlint v0.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect diff --git a/go.sum b/go.sum index ed6b3716..281e0dfa 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -258,6 +259,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= +github.com/ldmonster/go-markdownlint v0.0.1 h1:FSitTYz1pnRIrElTUfbjnOmRxhnyS1DxxGsSFnlrTLs= +github.com/ldmonster/go-markdownlint v0.0.1/go.mod h1:d501SE7F9c34YnMsZWiPJikJ/hcl6PgS9y3bKe62cwY= github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU= github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= diff --git a/internal/module/module.go b/internal/module/module.go index 188dd86e..7e034242 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -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) { @@ -387,7 +388,7 @@ func mapExclusionRulesAndSettings(linterSettings *pkg.LintersSettings, configSet mapRBACExclusions(linterSettings, configSettings) mapHooksSettings(linterSettings, configSettings) mapModuleExclusionsAndSettings(linterSettings, configSettings) - // no excluded rules - mapDocumentationExclusionsAndSettings(linterSettings, configSettings) + mapDocumentationSettings(linterSettings, configSettings) } // mapContainerExclusions maps Container linter exclusion rules @@ -491,6 +492,10 @@ func mapModuleExclusionsAndSettings(linterSettings *pkg.LintersSettings, configS linterSettings.Module.HelmignoreRuleSettings.Disable = configSettings.Module.Helmignore.Disable } +// mapDocumentationSettings maps Documentation linter settings (placeholder for future exclusion rules) +func mapDocumentationSettings(_ *pkg.LintersSettings, _ *config.LintersSettings) { +} + func NewModule(path string, vals *chartutil.Values, globalSchema *spec.Schema, rootConfig *config.RootConfig, errorList *dmtErrors.LintRuleErrorsList) (*Module, error) { module, err := newModuleFromPath(path) if err != nil { diff --git a/pkg/config.go b/pkg/config.go index bf828295..dfa6361e 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -87,6 +87,7 @@ type DocumentationLinterRules struct { BilingualRule RuleConfig CyrillicInEnglishRule RuleConfig NoLangKeyRule RuleConfig + MarkdownlintRule RuleConfig } type NoCyrillicLinterConfig struct { diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index d6c969a9..0540a7f4 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -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 { diff --git a/pkg/linters/docs/documentation.go b/pkg/linters/docs/documentation.go index e05bb1c3..f010937b 100644 --- a/pkg/linters/docs/documentation.go +++ b/pkg/linters/docs/documentation.go @@ -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().Run(m, errorList.WithMaxLevel(l.cfg.Rules.MarkdownlintRule.GetLevel())) } func (l *Documentation) Name() string { diff --git a/pkg/linters/docs/rules/markdown.go b/pkg/linters/docs/rules/markdown.go new file mode 100644 index 00000000..b5ffc704 --- /dev/null +++ b/pkg/linters/docs/rules/markdown.go @@ -0,0 +1,102 @@ +// Copyright 2026 Flant JSC +// Licensed under the Apache License, Version 2.0 + +package rules + +import ( + "context" + "os" + "path/filepath" + "strings" + + gomarkdownlint "github.com/ldmonster/go-markdownlint" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + MarkdownName = "markdownlint" +) + +func NewMarkdownRule() *MarkdownRule { + return &MarkdownRule{ + RuleMeta: pkg.RuleMeta{ + Name: MarkdownName, + }, + } +} + +type MarkdownRule struct { + pkg.RuleMeta + pkg.PathRule +} + +func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { + errorList = errorList.WithRule(r.GetName()) + + if !r.Enabled(m.GetName()) { + return + } + + modulePath := m.GetPath() + + mdFiles, err := collectMarkdownFiles(modulePath) + if err != nil { + errorList. + WithFilePath(modulePath). + WithValue(err.Error()). + Errorf("failed to collect markdown files: %s", err) + + return + } + + if len(mdFiles) == 0 { + return + } + + cfg := gomarkdownlint.ConfigFromMap(map[string]any{ + "default": true, + }) + + results, err := gomarkdownlint.LintFiles(context.Background(), mdFiles, cfg) + if err != nil { + errorList. + WithFilePath(modulePath). + WithValue(err.Error()). + Errorf("markdownlint failed: %s", err) + + return + } + + for file, errs := range results { + for _, mdErr := range errs { + errorList. + WithFilePath(file). + WithLineNumber(mdErr.LineNumber). + Errorf("%s %s", strings.Join(mdErr.RuleNames, "/"), mdErr.RuleDescription) + } + } +} + +func collectMarkdownFiles(root string) ([]string, error) { + var files []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if strings.HasSuffix(strings.ToLower(info.Name()), ".md") { + files = append(files, path) + } + + return nil + }) + + return files, err +} From af3a34ad551177c67a30c1aac6f64a50fbc6d494 Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Mon, 22 Jun 2026 11:29:52 +0300 Subject: [PATCH 2/8] chore: use deckhouse markdownlint config Signed-off-by: Ruslan Gorbunov --- pkg/linters/docs/rules/markdown.go | 101 ++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/pkg/linters/docs/rules/markdown.go b/pkg/linters/docs/rules/markdown.go index b5ffc704..f6420be8 100644 --- a/pkg/linters/docs/rules/markdown.go +++ b/pkg/linters/docs/rules/markdown.go @@ -55,9 +55,7 @@ func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { return } - cfg := gomarkdownlint.ConfigFromMap(map[string]any{ - "default": true, - }) + cfg := gomarkdownlint.ConfigFromMap(deckhouseMarkdownlintConfig()) results, err := gomarkdownlint.LintFiles(context.Background(), mdFiles, cfg) if err != nil { @@ -79,6 +77,103 @@ func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { } } +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 + }, + + // 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 + }, + } +} + func collectMarkdownFiles(root string) ([]string, error) { var files []string From b6000459d5eaa83b4e7c865dd9f57e3bc598bf54 Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Mon, 22 Jun 2026 11:41:13 +0300 Subject: [PATCH 3/8] add go-markdownlint as direct dependency Signed-off-by: Ruslan Gorbunov --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b298a665..9cd33e4b 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/iancoleman/strcase v0.3.0 github.com/itchyny/gojq v0.12.19 github.com/kyokomi/emoji v2.2.4+incompatible + github.com/ldmonster/go-markdownlint v0.0.1 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/mapstructure v1.5.0 @@ -101,7 +102,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/ldmonster/go-markdownlint v0.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect From 937e081762f05ffe5d0c8b8b0f8707708dc92a34 Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Wed, 24 Jun 2026 05:30:44 +0300 Subject: [PATCH 4/8] add markdownlint rule to documentation linter Signed-off-by: Ruslan Gorbunov --- README.md | 2 +- internal/module/module.go | 6 +- pkg/linters/docs/README.md | 69 ++++++++++++++++++- pkg/linters/docs/documentation.go | 2 +- pkg/linters/docs/rules/markdown.go | 69 +++++++++---------- .../markdownlint-violations/expected.yaml | 11 +++ .../module/docs/README.md | 2 + .../module/module.yaml | 3 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 2 + 10 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/expected.yaml create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/module.yaml create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/openapi/values.yaml diff --git a/README.md b/README.md index b5f2e9c2..91029dec 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/internal/module/module.go b/internal/module/module.go index 7e034242..446836ed 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -388,7 +388,7 @@ func mapExclusionRulesAndSettings(linterSettings *pkg.LintersSettings, configSet mapRBACExclusions(linterSettings, configSettings) mapHooksSettings(linterSettings, configSettings) mapModuleExclusionsAndSettings(linterSettings, configSettings) - mapDocumentationSettings(linterSettings, configSettings) + // no excluded rules - mapDocumentationExclusionsAndSettings(linterSettings, configSettings) } // mapContainerExclusions maps Container linter exclusion rules @@ -492,10 +492,6 @@ func mapModuleExclusionsAndSettings(linterSettings *pkg.LintersSettings, configS linterSettings.Module.HelmignoreRuleSettings.Disable = configSettings.Module.Helmignore.Disable } -// mapDocumentationSettings maps Documentation linter settings (placeholder for future exclusion rules) -func mapDocumentationSettings(_ *pkg.LintersSettings, _ *config.LintersSettings) { -} - func NewModule(path string, vals *chartutil.Values, globalSchema *spec.Schema, rootConfig *config.RootConfig, errorList *dmtErrors.LintRuleErrorsList) (*Module, error) { module, err := newModuleFromPath(path) if err != nil { diff --git a/pkg/linters/docs/README.md b/pkg/linters/docs/README.md index c2ff6b59..6015b3e9 100644 --- a/pkg/linters/docs/README.md +++ b/pkg/linters/docs/README.md @@ -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. @@ -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 @@ -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 + +# 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 + +# 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. diff --git a/pkg/linters/docs/documentation.go b/pkg/linters/docs/documentation.go index f010937b..45b71645 100644 --- a/pkg/linters/docs/documentation.go +++ b/pkg/linters/docs/documentation.go @@ -45,7 +45,7 @@ func (l *Documentation) Run(m *module.Module) { rules.NewNoLangKeyRule().CheckFiles(m, errorList.WithMaxLevel(l.cfg.Rules.NoLangKeyRule.GetLevel())) - rules.NewMarkdownRule().Run(m, errorList.WithMaxLevel(l.cfg.Rules.MarkdownlintRule.GetLevel())) + rules.NewMarkdownRule().CheckFiles(m, errorList.WithMaxLevel(l.cfg.Rules.MarkdownlintRule.GetLevel())) } func (l *Documentation) Name() string { diff --git a/pkg/linters/docs/rules/markdown.go b/pkg/linters/docs/rules/markdown.go index f6420be8..6789798a 100644 --- a/pkg/linters/docs/rules/markdown.go +++ b/pkg/linters/docs/rules/markdown.go @@ -11,18 +11,19 @@ import ( gomarkdownlint "github.com/ldmonster/go-markdownlint" + "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/errors" ) const ( - MarkdownName = "markdownlint" + MarkdownlintRuleName = "markdownlint" ) func NewMarkdownRule() *MarkdownRule { return &MarkdownRule{ RuleMeta: pkg.RuleMeta{ - Name: MarkdownName, + Name: MarkdownlintRuleName, }, } } @@ -32,7 +33,7 @@ type MarkdownRule struct { pkg.PathRule } -func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { +func (r *MarkdownRule) CheckFiles(m pkg.Module, errorList *errors.LintRuleErrorsList) { errorList = errorList.WithRule(r.GetName()) if !r.Enabled(m.GetName()) { @@ -40,24 +41,39 @@ func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { } modulePath := m.GetPath() + if modulePath == "" { + return + } - mdFiles, err := collectMarkdownFiles(modulePath) - if err != nil { - errorList. - WithFilePath(modulePath). - WithValue(err.Error()). - Errorf("failed to collect markdown files: %s", err) - + docsPath := filepath.Join(modulePath, "docs") + if _, err := os.Stat(docsPath); err != nil { return } - if len(mdFiles) == 0 { + 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(), mdFiles, cfg) + results, err := gomarkdownlint.LintFiles(context.Background(), files, cfg) if err != nil { errorList. WithFilePath(modulePath). @@ -68,15 +84,20 @@ func (r *MarkdownRule) Run(m pkg.Module, errorList *errors.LintRuleErrorsList) { } for file, errs := range results { + relPath := fsutils.Rel(modulePath, file) for _, mdErr := range errs { errorList. - WithFilePath(file). + 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) @@ -173,25 +194,3 @@ func deckhouseMarkdownlintConfig() map[string]any { }, } } - -func collectMarkdownFiles(root string) ([]string, error) { - var files []string - - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - if strings.HasSuffix(strings.ToLower(info.Name()), ".md") { - files = append(files, path) - } - - return nil - }) - - return files, err -} diff --git a/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml b/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml new file mode 100644 index 00000000..6d21f55d --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml @@ -0,0 +1,11 @@ +description: >- + A module whose docs/README.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" \ No newline at end of file diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md b/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md new file mode 100644 index 00000000..c26c11d1 --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md @@ -0,0 +1,2 @@ +# Markdownlint Violations +# Markdownlint Violations \ No newline at end of file diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/module.yaml b/test/e2e/testdata/documentation/markdownlint-violations/module/module.yaml new file mode 100644 index 00000000..1395f4e3 --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/module/module.yaml @@ -0,0 +1,3 @@ +name: test-module-markdownlint-violations +namespace: d8-test +weight: 100 \ No newline at end of file diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/config-values.yaml b/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/config-values.yaml new file mode 100644 index 00000000..4ee2fb1f --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} \ No newline at end of file diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/values.yaml b/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/values.yaml new file mode 100644 index 00000000..4ee2fb1f --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/module/openapi/values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} \ No newline at end of file From 45e0eca000b82f5b892198aa3b1f2ce04155b8dd Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Wed, 24 Jun 2026 18:02:12 +0300 Subject: [PATCH 5/8] exclude readme files from markdown lint Signed-off-by: Ruslan Gorbunov --- pkg/linters/docs/rules/markdown.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/linters/docs/rules/markdown.go b/pkg/linters/docs/rules/markdown.go index 6789798a..b303b543 100644 --- a/pkg/linters/docs/rules/markdown.go +++ b/pkg/linters/docs/rules/markdown.go @@ -60,6 +60,14 @@ func (r *MarkdownRule) CheckFiles(m pkg.Module, errorList *errors.LintRuleErrors continue } + // Mirror the deckhouse testing/.markdownlintignore entry "README.md", + // which excludes every README.md at any depth. These files are mostly + // demo/quickstart scaffolding and were never covered by the legacy + // markdownlint job; keep them out to preserve parity. + if filepath.Base(fileName) == "README.md" { + continue + } + mdFiles = append(mdFiles, fileName) } @@ -119,6 +127,16 @@ func deckhouseMarkdownlintConfig() map[string]any { "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 From 2fe971b3d27e6507811a4b909a50a3a808560262 Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Fri, 26 Jun 2026 11:23:45 +0300 Subject: [PATCH 6/8] [fix] use forked markdownlint and lint readmes Signed-off-by: Ruslan Gorbunov --- go.mod | 2 +- go.sum | 2 -- pkg/linters/docs/rules/markdown.go | 10 +--------- .../markdownlint-violations/expected.yaml | 2 +- .../markdownlint-violations/module/docs/README.md | 2 -- 5 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md diff --git a/go.mod b/go.mod index 9cd33e4b..57ac6365 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -20,7 +21,6 @@ require ( github.com/iancoleman/strcase v0.3.0 github.com/itchyny/gojq v0.12.19 github.com/kyokomi/emoji v2.2.4+incompatible - github.com/ldmonster/go-markdownlint v0.0.1 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/mapstructure v1.5.0 diff --git a/go.sum b/go.sum index 281e0dfa..25ab4c40 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= -github.com/ldmonster/go-markdownlint v0.0.1 h1:FSitTYz1pnRIrElTUfbjnOmRxhnyS1DxxGsSFnlrTLs= -github.com/ldmonster/go-markdownlint v0.0.1/go.mod h1:d501SE7F9c34YnMsZWiPJikJ/hcl6PgS9y3bKe62cwY= github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU= github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= diff --git a/pkg/linters/docs/rules/markdown.go b/pkg/linters/docs/rules/markdown.go index b303b543..b0a16beb 100644 --- a/pkg/linters/docs/rules/markdown.go +++ b/pkg/linters/docs/rules/markdown.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - gomarkdownlint "github.com/ldmonster/go-markdownlint" + gomarkdownlint "github.com/fuldaxxx/go-markdownlint" "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/pkg" @@ -60,14 +60,6 @@ func (r *MarkdownRule) CheckFiles(m pkg.Module, errorList *errors.LintRuleErrors continue } - // Mirror the deckhouse testing/.markdownlintignore entry "README.md", - // which excludes every README.md at any depth. These files are mostly - // demo/quickstart scaffolding and were never covered by the legacy - // markdownlint job; keep them out to preserve parity. - if filepath.Base(fileName) == "README.md" { - continue - } - mdFiles = append(mdFiles, fileName) } diff --git a/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml b/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml index 6d21f55d..7fc16168 100644 --- a/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml +++ b/test/e2e/testdata/documentation/markdownlint-violations/expected.yaml @@ -1,5 +1,5 @@ description: >- - A module whose docs/README.md has markdownlint violations: a duplicate + 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: diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md b/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md deleted file mode 100644 index c26c11d1..00000000 --- a/test/e2e/testdata/documentation/markdownlint-violations/module/docs/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Markdownlint Violations -# Markdownlint Violations \ No newline at end of file From e86499d535e7c8b750bd1514169a6191d87f9d26 Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Fri, 26 Jun 2026 11:24:05 +0300 Subject: [PATCH 7/8] [dmt] add markdownlint violation fixture Signed-off-by: Ruslan Gorbunov --- .../markdownlint-violations/module/docs/CONFIGURATION.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test/e2e/testdata/documentation/markdownlint-violations/module/docs/CONFIGURATION.md diff --git a/test/e2e/testdata/documentation/markdownlint-violations/module/docs/CONFIGURATION.md b/test/e2e/testdata/documentation/markdownlint-violations/module/docs/CONFIGURATION.md new file mode 100644 index 00000000..c26c11d1 --- /dev/null +++ b/test/e2e/testdata/documentation/markdownlint-violations/module/docs/CONFIGURATION.md @@ -0,0 +1,2 @@ +# Markdownlint Violations +# Markdownlint Violations \ No newline at end of file From 5cbea999ca151e48b82b69373219dbb84f38fb5c Mon Sep 17 00:00:00 2001 From: Ruslan Gorbunov Date: Fri, 26 Jun 2026 11:27:36 +0300 Subject: [PATCH 8/8] [chore] add markdownlint dependency Signed-off-by: Ruslan Gorbunov --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 25ab4c40..bf61b217 100644 --- a/go.sum +++ b/go.sum @@ -100,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=