11package main
22
33import (
4+ "bufio"
45 "errors"
56 "net/url"
67 "os"
@@ -16,11 +17,16 @@ import (
1617var (
1718 supportedResourceTypes = []string {"modules" , "templates" }
1819 operatingSystems = []string {"windows" , "macos" , "linux" }
20+ gfmAlertTypes = []string {"NOTE" , "IMPORTANT" , "CAUTION" , "WARNING" , "TIP" }
1921
2022 // TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
2123 // realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
2224 // structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
2325 terraformVersionRe = regexp .MustCompile (`^\s*\bversion\s+=` )
26+
27+ // Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
28+ // the renderer for the Registry website
29+ gfmAlertRegex = regexp .MustCompile (`^>(\s*)\[!(\w+)\](\s*)(.*)` )
2430)
2531
2632type coderResourceFrontmatter struct {
@@ -277,3 +283,73 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
277283 }
278284 return allReadmeFiles , nil
279285}
286+
287+ func validateResourceGfmAlerts (readmeBody string ) []error {
288+ trimmed := strings .TrimSpace (readmeBody )
289+ if trimmed == "" {
290+ return nil
291+ }
292+
293+ var errs []error
294+ var sourceLine string
295+ isInsideGfmQuotes := false
296+ isInsideCodeBlock := false
297+
298+ lineScanner := bufio .NewScanner (strings .NewReader (trimmed ))
299+ for lineScanner .Scan () {
300+ sourceLine = lineScanner .Text ()
301+
302+ if strings .HasPrefix (sourceLine , "```" ) {
303+ isInsideCodeBlock = ! isInsideCodeBlock
304+ continue
305+ }
306+ if isInsideCodeBlock {
307+ continue
308+ }
309+
310+ isInsideGfmQuotes = isInsideGfmQuotes && strings .HasPrefix (sourceLine , "> " )
311+
312+ currentMatch := gfmAlertRegex .FindStringSubmatch (sourceLine )
313+ if currentMatch == nil {
314+ continue
315+ }
316+
317+ // Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
318+ // rest of the content, so this will prevent any other validations from happening for the given line
319+ if isInsideGfmQuotes {
320+ errs = append (errs , errors .New ("registry does not support nested GFM alerts" ))
321+ continue
322+ }
323+
324+ leadingWhitespace := currentMatch [1 ]
325+ if len (leadingWhitespace ) != 1 {
326+ errs = append (errs , errors .New ("GFM alerts must have one space between the '>' and the start of the GFM brackets" ))
327+ }
328+ isInsideGfmQuotes = true
329+
330+ alertHeader := currentMatch [2 ]
331+ upperHeader := strings .ToUpper (alertHeader )
332+ if ! slices .Contains (gfmAlertTypes , upperHeader ) {
333+ errs = append (errs , xerrors .Errorf ("GFM alert type %q is not supported" , alertHeader ))
334+ }
335+ if alertHeader != upperHeader {
336+ errs = append (errs , xerrors .Errorf ("GFM alerts must be in all caps" ))
337+ }
338+
339+ trailingWhitespace := currentMatch [3 ]
340+ if trailingWhitespace != "" {
341+ errs = append (errs , xerrors .Errorf ("GFM alerts must not have any trailing whitespace after the closing bracket" ))
342+ }
343+
344+ extraContent := currentMatch [4 ]
345+ if extraContent != "" {
346+ errs = append (errs , xerrors .Errorf ("GFM alerts must not have any extra content on the same line" ))
347+ }
348+ }
349+
350+ if gfmAlertRegex .Match ([]byte (sourceLine )) {
351+ errs = append (errs , xerrors .Errorf ("README has an incomplete GFM alert at the end of the file" ))
352+ }
353+
354+ return errs
355+ }
0 commit comments