Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.23.2"
- name: Validate contributors
go-version: "1.24"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having weird issues with running Golang CI locally, and had to match the Go version.

- name: Validate README
run: go build ./cmd/readmevalidation && ./readmevalidation
- name: Remove build file artifact
run: rm ./readmevalidation
1 change: 0 additions & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ on:
push:
branches:
- main
- master
pull_request:

permissions:
Expand Down
68 changes: 67 additions & 1 deletion cmd/readmevalidation/codermodules.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,74 @@ package main
import (
"bufio"
"context"
"regexp"
"strings"

"golang.org/x/xerrors"
)

var (
// Matches Terraform source lines with registry.coder.com URLs
// Pattern: source = "registry.coder.com/namespace/module/coder"
terraformSourceRe = regexp.MustCompile(`^\s*source\s*=\s*"` + registryDomain + `/([^/]+)/([^/]+)/coder"`)
)

func validateModuleSourceURL(rm coderResourceReadme) []error {
var errs []error

// Skip validation if we couldn't parse namespace/resourceName from path
if rm.namespace == "" || rm.resourceName == "" {
return []error{xerrors.Errorf("invalid module path format: %s", rm.filePath)}
}

expectedSource := registryDomain + "/" + rm.namespace + "/" + rm.resourceName + "/coder"

trimmed := strings.TrimSpace(rm.body)
foundCorrectSource := false
isInsideTerraform := false

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

if strings.HasPrefix(nextLine, "```") {
if strings.HasPrefix(nextLine, "```tf") {
isInsideTerraform = true
continue
}
if isInsideTerraform {
break
}
continue
}

if !isInsideTerraform {
continue
}

// Check for source line in the first terraform block
if matches := terraformSourceRe.FindStringSubmatch(nextLine); matches != nil {
actualNamespace := matches[1]
actualModule := matches[2]
actualSource := registryDomain + "/" + actualNamespace + "/" + actualModule + "/coder"

if actualSource == expectedSource {
foundCorrectSource = true
break
}
// Found a registry.coder.com source but with wrong namespace/module
errs = append(errs, xerrors.Errorf("incorrect source URL format: found %q, expected %q", actualSource, expectedSource))
return errs
}
}

if !foundCorrectSource {
errs = append(errs, xerrors.Errorf("did not find correct source URL %q in first Terraform code block", expectedSource))
}

return errs
}

func validateCoderModuleReadmeBody(body string) []error {
var errs []error

Expand Down Expand Up @@ -94,6 +157,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateModuleSourceURL(rm) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
Expand Down Expand Up @@ -143,4 +209,4 @@ func validateAllCoderModules() error {
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
}
107 changes: 107 additions & 0 deletions cmd/readmevalidation/codermodules_test.go
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests generated by gemini CLI

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,42 @@ package main

import (
_ "embed"
"strings"
"testing"
)

//go:embed testSamples/sampleReadmeBody.md
var testBody string

// Test bodies extracted for better readability
var (
validModuleBody = `# Test Module

` + "```tf\n" + `module "test-module" {
source = "registry.coder.com/test-namespace/test-module/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
` + "```\n"

wrongNamespaceBody = `# Test Module

` + "```tf\n" + `module "test-module" {
source = "registry.coder.com/wrong-namespace/test-module/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
` + "```\n"

missingSourceBody = `# Test Module

` + "```tf\n" + `module "test-module" {
version = "1.0.0"
agent_id = coder_agent.example.id
}
` + "```\n"
)

func TestValidateCoderResourceReadmeBody(t *testing.T) {
t.Parallel()

Expand All @@ -20,3 +50,80 @@ func TestValidateCoderResourceReadmeBody(t *testing.T) {
}
})
}

func TestValidateModuleSourceURL(t *testing.T) {
t.Parallel()

t.Run("Valid source URL format", func(t *testing.T) {
t.Parallel()

rm := coderResourceReadme{
resourceType: "modules",
filePath: "registry/test-namespace/modules/test-module/README.md",
namespace: "test-namespace",
resourceName: "test-module",
body: validModuleBody,
}
errs := validateModuleSourceURL(rm)
if len(errs) != 0 {
t.Errorf("Expected no errors, got: %v", errs)
}
})

t.Run("Invalid source URL format - wrong namespace", func(t *testing.T) {
t.Parallel()

rm := coderResourceReadme{
resourceType: "modules",
filePath: "registry/test-namespace/modules/test-module/README.md",
namespace: "test-namespace",
resourceName: "test-module",
body: wrongNamespaceBody,
}
errs := validateModuleSourceURL(rm)
if len(errs) != 1 {
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
}
if !strings.Contains(errs[0].Error(), "incorrect source URL format") {
t.Errorf("Expected source URL format error, got: %s", errs[0].Error())
}
})

t.Run("Missing source URL", func(t *testing.T) {
t.Parallel()

rm := coderResourceReadme{
resourceType: "modules",
filePath: "registry/test-namespace/modules/test-module/README.md",
namespace: "test-namespace",
resourceName: "test-module",
body: missingSourceBody,
}
errs := validateModuleSourceURL(rm)
if len(errs) != 1 {
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
}
if !strings.Contains(errs[0].Error(), "did not find correct source URL") {
t.Errorf("Expected missing source URL error, got: %s", errs[0].Error())
}
})

t.Run("Invalid file path format", func(t *testing.T) {
t.Parallel()

rm := coderResourceReadme{
resourceType: "modules",
filePath: "invalid/path/format",
namespace: "", // Empty because path parsing failed
resourceName: "", // Empty because path parsing failed
body: "# Test Module",
}
errs := validateModuleSourceURL(rm)
if len(errs) != 1 {
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
}
if !strings.Contains(errs[0].Error(), "invalid module path format") {
t.Errorf("Expected path format error, got: %s", errs[0].Error())
}
})
}
26 changes: 20 additions & 6 deletions cmd/readmevalidation/coderresources.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
registryDomain = "registry.coder.com"

// 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
// the renderer for the Registry website.
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)

Expand All @@ -39,7 +40,7 @@ type coderResourceFrontmatter struct {
}

// A slice version of the struct tags from coderResourceFrontmatter. Might be worth using reflection to generate this
// list at runtime in the future, but this should be okay for now
// list at runtime in the future, but this should be okay for now.
var supportedCoderResourceStructKeys = []string{
"description", "icon", "display_name", "verified", "tags", "supported_os",
// TODO: This is an old, officially deprecated key from the archived coder/modules repo. We can remove this once we
Expand All @@ -53,6 +54,8 @@ var supportedCoderResourceStructKeys = []string{
type coderResourceReadme struct {
resourceType string
filePath string
namespace string
resourceName string
body string
frontmatter coderResourceFrontmatter
}
Expand Down Expand Up @@ -183,9 +186,20 @@ func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceRead
return coderResourceReadme{}, []error{xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)}
}

// Extract namespace and resource name from file path
// Expected path format: registry/<namespace>/<resourceType>/<resource-name>/README.md
var namespace, resourceName string
parts := strings.Split(path.Clean(rm.filePath), "/")
if len(parts) >= 5 && parts[0] == "registry" && parts[2] == resourceType && parts[4] == "README.md" {
namespace = parts[1]
resourceName = parts[3]
}

return coderResourceReadme{
resourceType: resourceType,
filePath: rm.filePath,
namespace: namespace,
resourceName: resourceName,
body: body,
frontmatter: yml,
}, nil
Expand Down Expand Up @@ -315,15 +329,15 @@ func validateResourceGfmAlerts(readmeBody string) []error {
}

// 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
// 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"))
errs = append(errs, xerrors.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"))
errs = append(errs, xerrors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true

Expand All @@ -347,7 +361,7 @@ func validateResourceGfmAlerts(readmeBody string) []error {
}
}

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

Expand Down
2 changes: 1 addition & 1 deletion cmd/readmevalidation/contributors.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type contributorProfileFrontmatter struct {
}

// A slice version of the struct tags from contributorProfileFrontmatter. Might be worth using reflection to generate
// this list at runtime in the future, but this should be okay for now
// this list at runtime in the future, but this should be okay for now.
var supportedContributorProfileStructKeys = []string{"display_name", "bio", "status", "avatar", "linkedin", "github", "website", "support_email"}

type contributorProfileReadme struct {
Expand Down
11 changes: 5 additions & 6 deletions cmd/readmevalidation/repostructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import (

var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")

// validNameRe validates that names contain only alphanumeric characters and hyphens
// validNameRe validates that names contain only alphanumeric characters and hyphens.
var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`)


// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
// expected file conventions
// expected file conventions.
func validateCoderResourceSubdirectory(dirPath string) []error {
resourceDir, err := os.Stat(dirPath)
if err != nil {
Expand Down Expand Up @@ -47,7 +46,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
continue
}

// Validate module/template name
// Validate module/template name.
if !validNameRe.MatchString(f.Name()) {
errs = append(errs, xerrors.Errorf("%q: name contains invalid characters (only alphanumeric characters and hyphens are allowed)", path.Join(dirPath, f.Name())))
continue
Expand Down Expand Up @@ -90,7 +89,7 @@ func validateRegistryDirectory() []error {
continue
}

// Validate namespace name
// Validate namespace name.
if !validNameRe.MatchString(nDir.Name()) {
allErrs = append(allErrs, xerrors.Errorf("%q: namespace name contains invalid characters (only alphanumeric characters and hyphens are allowed)", namespacePath))
continue
Expand Down Expand Up @@ -136,7 +135,7 @@ func validateRegistryDirectory() []error {

// validateRepoStructure validates that the structure of the repo is "correct enough" to do all necessary validation
// checks. It is NOT an exhaustive validation of the entire repo structure – it only checks the parts of the repo that
// are relevant for the main validation steps
// are relevant for the main validation steps.
func validateRepoStructure() error {
var errs []error
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module coder.com/coder-registry

go 1.23.2
go 1.24

require (
cdr.dev/slog v1.6.1
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
"terraform-validate": "./scripts/terraform_validate.sh",
"test": "./scripts/terraform_test_all.sh",
"update-version": "./update-version.sh"
"update-version": "./update-version.sh",
"validate-readme": "go build ./cmd/readmevalidation && ./readmevalidation"
},
"devDependencies": {
"@types/bun": "^1.2.21",
Expand Down