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
3 changes: 3 additions & 0 deletions cmd/readmevalidation/codermodules.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
Expand Down
76 changes: 76 additions & 0 deletions cmd/readmevalidation/coderresources.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"errors"
"net/url"
"os"
Expand All @@ -16,11 +17,16 @@ import (
var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}

// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)

// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
// the renderer for the Registry website
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)

type coderResourceFrontmatter struct {
Expand Down Expand Up @@ -277,3 +283,73 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
}
return allReadmeFiles, nil
}

func validateResourceGfmAlerts(readmeBody string) []error {
trimmed := strings.TrimSpace(readmeBody)
if trimmed == "" {
return nil
}

var errs []error
var sourceLine string
isInsideGfmQuotes := false
isInsideCodeBlock := false

lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
sourceLine = lineScanner.Text()

if strings.HasPrefix(sourceLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
continue
}
if isInsideCodeBlock {
continue
}

isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")

currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
if currentMatch == nil {
continue
}

// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
// rest of the content, so this will prevent any other validations from happening for the given line
if isInsideGfmQuotes {
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
continue
}

leadingWhitespace := currentMatch[1]
if len(leadingWhitespace) != 1 {
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true

alertHeader := currentMatch[2]
upperHeader := strings.ToUpper(alertHeader)
if !slices.Contains(gfmAlertTypes, upperHeader) {
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
}
if alertHeader != upperHeader {
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
}

trailingWhitespace := currentMatch[3]
if trailingWhitespace != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
}

extraContent := currentMatch[4]
if extraContent != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
}
}

if gfmAlertRegex.Match([]byte(sourceLine)) {
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
}

return errs
}
3 changes: 3 additions & 0 deletions cmd/readmevalidation/codertemplates.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func validateCoderTemplateReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
Expand Down
2 changes: 1 addition & 1 deletion registry/coder/templates/azure-linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This means, when the workspace restarts, any tools or files outside of the home

### Persistent VM

> [!IMPORTANT]
> [!IMPORTANT]
> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner.
> You will have to do this installation manually as it is not included in our official images.

Expand Down
2 changes: 1 addition & 1 deletion registry/coder/templates/azure-windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This means, when the workspace restarts, any tools or files outside of the data

### Persistent VM

> [!IMPORTANT]
> [!IMPORTANT]
> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner.
> You will have to do this installation manually as it is not included in our official images.

Expand Down