Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore: add validation for module source URLs#406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
matifali wants to merge10 commits intomain
base:main
Choose a base branch
Loading
fromatif/validate-readme-source
Open
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
10 commits
Select commitHold shift + click to select a range
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
PrevPrevious commit
NextNext commit
Address PR review comments
- Make regex more specific for registry.coder.com patterns only- Refactor to add namespace and resourceName fields to coderResourceReadme struct- Inline path parsing logic into parseCoderResourceReadme- Update validateModuleSourceURL to use struct fields instead of filePath parameter- Simplify Terraform block detection logic- Reduce nesting with early continue statements- Add comment explaining regex pattern- Extract registry.coder.com into a constant- Improve test readability with extracted variables- Remove redundant checks in tests- Replace custom contains function with strings.ContainsCo-authored-by: matifali <matifali@users.noreply.github.com>
  • Loading branch information
@blink-so@matifali
blink-so[bot] andmatifali committedSep 4, 2025
commit9c00c576a36f13c6718b87b6e0b6d0ef8fcf7977

Some comments aren't visible on the classic Files Changed page.

69 changes: 31 additions & 38 deletionscmd/readmevalidation/codermodules.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,71 +3,64 @@ package main
import (
"bufio"
"context"
"path/filepath"
"regexp"
"strings"

"golang.org/x/xerrors"
)

var (
terraformSourceRe = regexp.MustCompile(`^\s*source\s*=\s*"([^"]+)"`)
// 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"`)
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to swap the[^/]+ patterns for something even more specific? Right now, they allow any non-slash character, but don't we want to make sure the URLs don't have certain characters like underscores?

)

func extractNamespaceAndModuleFromPath(filePath string) (namespace string, moduleName string, err error) {
// Expected path format: registry/<namespace>/modules/<module-name>/README.md.
parts := strings.Split(filepath.Clean(filePath), string(filepath.Separator))
if len(parts) < 5 || parts[0] != "registry" || parts[2] != "modules" || parts[4] != "README.md" {
return "", "", xerrors.Errorf("invalid module path format: %s", filePath)
}
namespace = parts[1]
moduleName = parts[3]
return namespace, moduleName, nil
}

func validateModuleSourceURL(body string, filePath string) []error {
func validateModuleSourceURL(rm coderResourceReadme) []error {
var errs []error

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

expectedSource :="registry.coder.com/" + namespace + "/" +moduleName + "/coder"
expectedSource :=registryDomain + "/" +rm.namespace + "/" +rm.resourceName + "/coder"
Copy link
Member

Choose a reason for hiding this comment

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

Nit: since this variable isn't used for a while, we can move it down, closer to where it's used


trimmed := strings.TrimSpace(body)
trimmed := strings.TrimSpace(rm.body)
foundCorrectSource := false
Copy link
Member

@ParkreinerParkreinerOct 6, 2025
edited
Loading

Choose a reason for hiding this comment

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

Edit: I think there's something we can salvage from this comment, but I realized in the second comment in the chain that it doesn't do everything we want

I don't know if this variable makes sense. With how it's set up right now, if we have at least one correct source in the README file, we'll automatically ignore all otherincorrect sources. And from a state modeling perspective, the variable also feels redundant, when a single slice should be able to give us all the info we need

What makes more sense to me is to:

  1. HaveincorrectSources as the main validation state
  2. Instead of having theactualSource == expectedSource check, only have aactualSource != expectedSource check. If that triggers, push the incorrect source to theincorrectSources slice. If things match, we'll do nothing, and let the code fall through to the rest of the loop
  3. Once the main loop is done and we're done processing the lines, check if the slice is not empty. If it's not, return an error with a list of all sources that are incorrect

Copy link
Member

Choose a reason for hiding this comment

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

Actually, now that I'm thinking over the code more, I think I understand what the old code was trying to do, and why my suggestion doesn't actually work

I'm not super up to speed on our Terraform conventions, but if a module has a name likemodule "blah", would it be expected that the source URL should always have"blah" in it, too? If so, I think we'd need to check the name of each module block, and compare that against the source URL line

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

but if a module has a name like module "blah", would it be expected that the source URL should always have "blah" in it, too?

No it can be different. We just use the same name for convenience.

isInsideTerraform := false
firstTerraformBlock := true

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

if strings.HasPrefix(nextLine, "```") {
if strings.HasPrefix(nextLine, "```tf")&& firstTerraformBlock{
if strings.HasPrefix(nextLine, "```tf") {
isInsideTerraform = true
firstTerraformBlock = false
} else if isInsideTerraform {
// End of first terraform block.
continue
}
if isInsideTerraform {
break
}
continue
}

if isInsideTerraform {
// Check for any source line in the first terraform block.
if matches := terraformSourceRe.FindStringSubmatch(nextLine); matches != nil {
actualSource := matches[1]
if actualSource == expectedSource {
foundCorrectSource = true
break
} else if strings.HasPrefix(actualSource, "registry.coder.com/") && strings.Contains(actualSource, "/"+moduleName+"/coder") {
// Found source for this module but with wrong namespace/format.
errs = append(errs, xerrors.Errorf("incorrect source URL format: found %q, expected %q", actualSource, expectedSource))
return errs
}
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
}
}

Expand DownExpand Up@@ -164,7 +157,7 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateModuleSourceURL(rm.body, rm.filePath) {
for _, err := range validateModuleSourceURL(rm) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
Expand DownExpand Up@@ -216,4 +209,4 @@ func validateAllCoderModules() error {
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
return nil
}
}
108 changes: 66 additions & 42 deletionscmd/readmevalidation/codermodules_test.go
View file
Open in desktop
Copy link
MemberAuthor

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

Copy link
Member

Choose a reason for hiding this comment

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

Assuming we still want to process all the blocks in a file, I think it'd be really good to have some test cases that include 2+ blocks

Original file line numberDiff line numberDiff 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" {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I'm just now realizing how the sample MD contents are formatted, and I think I'd prefer for each one to be a single raw string. All the+ signs are a bit hard to read in the GitHub UI, and this is a case where all the spacing and lines do matter

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@@ -27,9 +57,14 @@ func TestValidateModuleSourceURL(t *testing.T) {
t.Run("Valid source URL format", func(t *testing.T) {
t.Parallel()

body := "# Test Module\n\n```tf\nmodule \"test-module\" {\n source = \"registry.coder.com/test-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
filePath := "registry/test-namespace/modules/test-module/README.md"
errs := validateModuleSourceURL(body, filePath)
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)
}
Expand All@@ -38,68 +73,57 @@ func TestValidateModuleSourceURL(t *testing.T) {
t.Run("Invalid source URL format - wrong namespace", func(t *testing.T) {
t.Parallel()

body := "# Test Module\n\n```tf\nmodule \"test-module\" {\n source = \"registry.coder.com/wrong-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
filePath := "registry/test-namespace/modules/test-module/README.md"
errs := validateModuleSourceURL(body, filePath)
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)
}
iflen(errs) > 0 && !contains(errs[0].Error(), "incorrect source URL format") {
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()

body := "# Test Module\n\n```tf\nmodule \"other-module\" {\n source = \"registry.coder.com/other/other-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
filePath := "registry/test-namespace/modules/test-module/README.md"
errs := validateModuleSourceURL(body, filePath)
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)
}
iflen(errs) > 0 && !contains(errs[0].Error(), "did not find correct source URL") {
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("Module name with hyphens vs underscores", func(t *testing.T) {
t.Parallel()

body := "# Test Module\n\n```tf\nmodule \"test_module\" {\n source = \"registry.coder.com/test-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
filePath := "registry/test-namespace/modules/test-module/README.md"
errs := validateModuleSourceURL(body, filePath)
if len(errs) != 0 {
t.Errorf("Expected no errors for hyphen/underscore variation, got: %v", errs)
}
})

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

body := "# Test Module"
filePath := "invalid/path/format"
errs := validateModuleSourceURL(body, filePath)
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)
}
iflen(errs) > 0 && !contains(errs[0].Error(), "invalid module path format") {
if!strings.Contains(errs[0].Error(), "invalid module path format") {
t.Errorf("Expected path format error, got: %s", errs[0].Error())
}
})
}

func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
indexOfSubstring(s, substr) >= 0)))
}

func indexOfSubstring(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
}
14 changes: 14 additions & 0 deletionscmd/readmevalidation/coderresources.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,7 @@ 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
Expand DownExpand Up@@ -53,6 +54,8 @@ var supportedCoderResourceStructKeys = []string{
type coderResourceReadme struct {
resourceType string
filePath string
namespace string
resourceName string
body string
frontmatter coderResourceFrontmatter
}
Expand DownExpand 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

[8]ページ先頭

©2009-2025 Movatter.jp