From 18680d0a1570b5165d4cbbf1b873248b7e2513d1 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 15 Apr 2025 16:02:33 +0000 Subject: [PATCH] chore: add directory validation in separate file --- cmd/readmevalidation/contributors.go | 7 +- cmd/readmevalidation/errors.go | 28 ++++++++ cmd/readmevalidation/main.go | 13 +++- cmd/readmevalidation/readmes.go | 37 +++------- cmd/readmevalidation/repostructure.go | 97 +++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 cmd/readmevalidation/errors.go create mode 100644 cmd/readmevalidation/repostructure.go diff --git a/cmd/readmevalidation/contributors.go b/cmd/readmevalidation/contributors.go index 8dd6f81a..177340c4 100644 --- a/cmd/readmevalidation/contributors.go +++ b/cmd/readmevalidation/contributors.go @@ -194,10 +194,6 @@ func validateContributorAvatarURL(avatarURL *string) []error { return problems } -func addFilePathToError(filePath string, err error) error { - return fmt.Errorf("%q: %v", filePath, err) -} - func validateContributorProfile(yml contributorProfile) []error { allProblems := []error{} @@ -313,7 +309,6 @@ func aggregateContributorReadmeFiles() ([]readme, error) { for _, e := range dirEntries { dirPath := path.Join(rootRegistryPath, e.Name()) if !e.IsDir() { - problems = append(problems, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath)) continue } @@ -331,7 +326,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) { if len(problems) != 0 { return nil, validationPhaseError{ - phase: validationPhaseFilesystemRead, + phase: validationPhaseFileLoad, errors: problems, } } diff --git a/cmd/readmevalidation/errors.go b/cmd/readmevalidation/errors.go new file mode 100644 index 00000000..c60e3fcd --- /dev/null +++ b/cmd/readmevalidation/errors.go @@ -0,0 +1,28 @@ +package main + +import "fmt" + +var _ error = validationPhaseError{} + +// validationPhaseError represents an error that occurred during a specific +// phase of README validation. It should be used to collect ALL validation +// errors that happened during a specific phase, rather than the first one +// encountered. +type validationPhaseError struct { + phase validationPhase + errors []error +} + +func (vpe validationPhaseError) Error() string { + msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String()) + for _, e := range vpe.errors { + msg += fmt.Sprintf("\n- %v", e) + } + msg += "\n" + + return msg +} + +func addFilePathToError(filePath string, err error) error { + return fmt.Errorf("%q: %v", filePath, err) +} diff --git a/cmd/readmevalidation/main.go b/cmd/readmevalidation/main.go index f14ed121..d0d7ac34 100644 --- a/cmd/readmevalidation/main.go +++ b/cmd/readmevalidation/main.go @@ -55,10 +55,17 @@ func main() { } else { log.Println("Provided API token does not belong to a Coder employee. Some README validation steps will be skipped compared to when they run in CI.") } - fmt.Printf("actor %q is %s\n", actorUsername, actorOrgStatus.String()) + fmt.Printf("Script GitHub actor %q has Coder organization status %q\n", actorUsername, actorOrgStatus.String()) log.Println("Starting README validation") + // Validate file structure of main README directory + err = validateRepoStructure() + if err != nil { + log.Panic(err) + } + + // Validate contributor README files allReadmeFiles, err := aggregateContributorReadmeFiles() if err != nil { log.Panic(err) @@ -75,4 +82,8 @@ func main() { } log.Println("All relative URLs for READMEs are valid") log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath) + + // Validate modules + + // Validate templates } diff --git a/cmd/readmevalidation/readmes.go b/cmd/readmevalidation/readmes.go index 47bb1962..a8d2bd68 100644 --- a/cmd/readmevalidation/readmes.go +++ b/cmd/readmevalidation/readmes.go @@ -3,7 +3,6 @@ package main import ( "bufio" "errors" - "fmt" "strings" ) @@ -72,12 +71,15 @@ func separateFrontmatter(readmeText string) (string, string, error) { type validationPhase int const ( - // validationPhaseFilesystemRead indicates when a README file is being read - // from the file system - validationPhaseFilesystemRead validationPhase = iota + // + validationPhaseStructureValidation validationPhase = iota - // validationPhaseReadmeParsing indicates when a README's frontmatter is being - // parsed as YAML. This phase does not include YAML validation. + // validationPhaseFileLoad indicates when a README file is being read from + // the file system + validationPhaseFileLoad + + // validationPhaseReadmeParsing indicates when a README's frontmatter is + // being parsed as YAML. This phase does not include YAML validation. validationPhaseReadmeParsing // validationPhaseReadmeValidation indicates when a README's frontmatter is @@ -92,7 +94,7 @@ const ( func (p validationPhase) String() string { switch p { - case validationPhaseFilesystemRead: + case validationPhaseFileLoad: return "Filesystem reading" case validationPhaseReadmeParsing: return "README parsing" @@ -104,24 +106,3 @@ func (p validationPhase) String() string { return "Unknown validation phase" } } - -var _ error = validationPhaseError{} - -// validationPhaseError represents an error that occurred during a specific -// phase of README validation. It should be used to collect ALL validation -// errors that happened during a specific phase, rather than the first one -// encountered. -type validationPhaseError struct { - phase validationPhase - errors []error -} - -func (vpe validationPhaseError) Error() string { - msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String()) - for _, e := range vpe.errors { - msg += fmt.Sprintf("\n- %v", e) - } - msg += "\n" - - return msg -} diff --git a/cmd/readmevalidation/repostructure.go b/cmd/readmevalidation/repostructure.go new file mode 100644 index 00000000..ccc25c55 --- /dev/null +++ b/cmd/readmevalidation/repostructure.go @@ -0,0 +1,97 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path" +) + +func validateCoderResourceDirectory(directoryPath string) []error { + errs := []error{} + + dir, err := os.Stat(directoryPath) + if err != nil { + // It's valid for a specific resource directory not to exist. It's just + // that if it does exist, it must follow specific rules + if !errors.Is(err, os.ErrNotExist) { + errs = append(errs, addFilePathToError(directoryPath, err)) + } + return errs + } + + if !dir.IsDir() { + errs = append(errs, fmt.Errorf("%q: path is not a directory", directoryPath)) + return errs + } + + files, err := os.ReadDir(directoryPath) + if err != nil { + errs = append(errs, fmt.Errorf("%q: %v", directoryPath, err)) + return errs + } + for _, f := range files { + if !f.IsDir() { + continue + } + + resourceReadmePath := path.Join(directoryPath, f.Name(), "README.md") + _, err := os.Stat(resourceReadmePath) + if err == nil { + continue + } + + if errors.Is(err, os.ErrNotExist) { + errs = append(errs, fmt.Errorf("%q: README file does not exist", resourceReadmePath)) + } else { + errs = append(errs, addFilePathToError(resourceReadmePath, err)) + } + } + + return errs +} + +func validateRegistryDirectory() []error { + dirEntries, err := os.ReadDir(rootRegistryPath) + if err != nil { + return []error{err} + } + + problems := []error{} + for _, e := range dirEntries { + dirPath := path.Join(rootRegistryPath, e.Name()) + if !e.IsDir() { + problems = append(problems, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath)) + continue + } + + readmePath := path.Join(dirPath, "README.md") + _, err := os.Stat(readmePath) + if err != nil { + problems = append(problems, err) + } + + modulesPath := path.Join(dirPath, "modules") + if errs := validateCoderResourceDirectory(modulesPath); len(errs) != 0 { + problems = append(problems, errs...) + } + templatesPath := path.Join(dirPath, "templates") + if errs := validateCoderResourceDirectory(templatesPath); len(errs) != 0 { + problems = append(problems, errs...) + } + } + + return problems +} + +func validateRepoStructure() error { + errs := validateRegistryDirectory() + if len(errs) != 0 { + return validationPhaseError{ + phase: validationPhaseFileLoad, + errors: errs, + } + } + + return nil +}