Skip to content

109 docs validate readme template structure in elastic package #2716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ func validateSourceCommandAction(cmd *cobra.Command, args []string) error {
if skipped != nil {
logger.Infof("Skipped errors: %v", skipped)
}

if errs != nil {
return fmt.Errorf("linting package failed: %w", errs)
}

docsErrors := validation.ValidateDocsStructureFromPath(packageRootPath)
if docsErrors != nil {
return fmt.Errorf("documentation validation failed: %v", docsErrors)
}
return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/yuin/goldmark v1.4.13
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/tools v0.34.0
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
Expand Down
11 changes: 11 additions & 0 deletions internal/builder/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ func BuildPackage(ctx context.Context, options BuildOptions) (string, error) {
if errs != nil {
return "", fmt.Errorf("invalid content found in built package: %w", errs)
}

docsValidationErrors := validation.ValidateDocsStructureFromPath(destinationDir)
if docsValidationErrors != nil {
return "", fmt.Errorf("documentation validation failed: %v", docsValidationErrors)
}

return destinationDir, nil
}

Expand Down Expand Up @@ -230,6 +236,11 @@ func buildZippedPackage(ctx context.Context, options BuildOptions, destinationDi
}
}

docsValidationErrors := validation.ValidateDocsStructureFromZip(zippedPackagePath)
if docsValidationErrors != nil {
return "", fmt.Errorf("documentation validation failed: %v", docsValidationErrors)
}

if options.SignPackage {
err := signZippedPackage(options, zippedPackagePath)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/packages/buildmanifest/build_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type BuildManifest struct {

// Dependencies define external package dependencies.
type Dependencies struct {
ECS ECSDependency `config:"ecs"`
ECS ECSDependency `config:"ecs"`
DocsStructureEnforced bool `config:"docs_structure_enforced"`
}

// ECSDependency defines a dependency on ECS fields.
Expand Down
6 changes: 6 additions & 0 deletions internal/packages/installer/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ func NewForPackage(ctx context.Context, options Options) (Installer, error) {
}
}
logger.Debug("Skip validation of the built .zip package")

docsValidationErrors := validation.ValidateDocsStructureFromZip(options.ZipPath)
if docsValidationErrors != nil {
return nil, fmt.Errorf("documentation validation failed: %v", docsValidationErrors)
}

return CreateForZip(options.Kibana, options.ZipPath)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "1"
enforced_sections:
- name: "Overview"
description: "A brief introduction to the package, its purpose, and key features."
- name: "What data does this integration collect?"
description: "An overview of the data collected by the package, including any relevant metrics or logs."
- name: "What do I need to use this integration?"
description: "Requirements for using the package, such as dependencies, configurations, or prerequisites."
- name: "How do I deploy this integration?"
description: "Instructions for deploying the package, including any necessary configurations or setup steps."
- name: "Troubleshooting"
description: "Guidance on common issues and how to resolve them, including error messages and their meanings."
- name: "Performance and scaling"
description: "Information on how the package performs under different loads and how to scale it effectively."
84 changes: 84 additions & 0 deletions internal/validation/readme_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package validation

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)

func ValidateReadmeStructure(packageRoot string, enforcedSections []string) error {
docsFolderPath := filepath.Join(packageRoot, "docs")
files, err := os.ReadDir(docsFolderPath)
if err != nil && errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("docs folder %s not found: %w", docsFolderPath, err)
}

var errs DocsValidationError

for _, file := range files {
if !file.IsDir() {
fullPath := filepath.Join(docsFolderPath, file.Name())

content, err := os.ReadFile(fullPath)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", fullPath, err)
continue
}

validationErrs := validateContent(file.Name(), content, enforcedSections)
if validationErrs != nil {
errs = append(errs, validationErrs)
}
}
}

if len(errs) == 0 {
return nil
}

return errs
}

func validateContent(filename string, content []byte, enforcedSections []string) error {
var errs DocsValidationError

md := goldmark.New()

reader := text.NewReader(content)
doc := md.Parser().Parse(reader)

found := map[string]bool{}
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if heading, ok := n.(*ast.Heading); ok && entering {
text := extractHeadingText(heading, content)
for _, required := range enforcedSections {
if strings.EqualFold(text, required) {
found[required] = true
}
}
}
return ast.WalkContinue, nil
})

for _, header := range enforcedSections {
if !found[header] {
errs = append(errs, fmt.Errorf("missing required section '%s' in file '%s'", header, filename))
}
}

if len(errs) == 0 {
return nil
}

return errs
}
74 changes: 74 additions & 0 deletions internal/validation/readme_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package validation

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestValidateContent(t *testing.T) {
tests := []struct {
name string
filename string
content []byte
enforcedSections []string
expectedResult error
}{
{
name: "Valid content",
filename: "test.md",
content: []byte("# Overview\n\nThis is a valid overview section."),
enforcedSections: []string{"Overview"},
expectedResult: nil,
},
{
name: "Missing header",
filename: "test.md",
content: []byte("# Overview\n\nThis is a valid overview section."),
enforcedSections: []string{"Overview", "Setup"},
expectedResult: DocsValidationError{fmt.Errorf("missing required section 'Setup' in file 'test.md'")},
}, {
name: "Empty content",
filename: "test.md",
content: []byte(""),
enforcedSections: []string{"Overview", "Setup"},
expectedResult: DocsValidationError{fmt.Errorf("missing required section 'Overview' in file 'test.md'"), fmt.Errorf("missing required section 'Setup' in file 'test.md'")},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actualResult := validateContent(tt.filename, tt.content, tt.enforcedSections)
assert.Equal(t, tt.expectedResult, actualResult, "Result does not match expected")
})
}
}

func TestValidateReadmeStructure(t *testing.T) {
tests := []struct {
name string
packageRoot string
enforcedSections []string
expectedResult string
}{
{
name: "Valid test",
packageRoot: "testdata",
enforcedSections: []string{"Overview", "Setup"},
expectedResult: "\n\nmissing required section 'Setup' in file 'missing_headers.md'",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actualResult := ValidateReadmeStructure(tt.packageRoot, tt.enforcedSections)

assert.Equal(t, tt.expectedResult, actualResult.Error(), "Result does not match expected")
})
}
}
2 changes: 2 additions & 0 deletions internal/validation/testdata/docs/missing_headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Overview
This is the overview section
6 changes: 6 additions & 0 deletions internal/validation/testdata/docs/valid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Overview
This is the overview section


### Setup
This is the setup section
Loading