From e94dfd2df61587f8c39c5327b4c25c8b6f27ad5d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 26 Aug 2025 21:52:22 -0400 Subject: [PATCH] fix: add validation for Github-Flavored Markdown Alerts (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No issue to link – this was a problem we discovered while updating the Registry website ## Description This PR adds (very) basic validation for the GitHub Flavored Markdown alerts that we allow contributors to add to their README files. The errors that get generated should be correct, but the error messages themselves aren't as helpful as they could be. I'm going to be handling that in a separate PR, just so we can get this one in sooner. ### Changes made - Added function for validating the core structure of all GFM alerts - Updated existing README files that were failing the new validation requirements ## Type of Change - [ ] New module - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other --- cmd/readmevalidation/codermodules.go | 3 + cmd/readmevalidation/coderresources.go | 76 +++++++++++++++++++ cmd/readmevalidation/codertemplates.go | 3 + .../coder/templates/azure-linux/README.md | 2 +- .../coder/templates/azure-windows/README.md | 2 +- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/cmd/readmevalidation/codermodules.go b/cmd/readmevalidation/codermodules.go index 4c9d854d..005a98ee 100644 --- a/cmd/readmevalidation/codermodules.go +++ b/cmd/readmevalidation/codermodules.go @@ -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...) } diff --git a/cmd/readmevalidation/coderresources.go b/cmd/readmevalidation/coderresources.go index f7237a36..818d73c8 100644 --- a/cmd/readmevalidation/coderresources.go +++ b/cmd/readmevalidation/coderresources.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "errors" "net/url" "os" @@ -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 { @@ -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 +} diff --git a/cmd/readmevalidation/codertemplates.go b/cmd/readmevalidation/codertemplates.go index df0c6c0b..b076ad8a 100644 --- a/cmd/readmevalidation/codertemplates.go +++ b/cmd/readmevalidation/codertemplates.go @@ -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...) } diff --git a/registry/coder/templates/azure-linux/README.md b/registry/coder/templates/azure-linux/README.md index 89e910d3..33d771ed 100644 --- a/registry/coder/templates/azure-linux/README.md +++ b/registry/coder/templates/azure-linux/README.md @@ -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. diff --git a/registry/coder/templates/azure-windows/README.md b/registry/coder/templates/azure-windows/README.md index cf6b1a86..13598061 100644 --- a/registry/coder/templates/azure-windows/README.md +++ b/registry/coder/templates/azure-windows/README.md @@ -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.