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

Commit 72d7ee4

Browse files
committed
Add validation for Terraform module source URLs
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53b912b..eb3cf8b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,8 +63,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.23.2" - - name: Validate contributors + go-version: "1.25.0" + - name: Validate Reademde run: go build ./cmd/readmevalidation && ./readmevalidation - name: Remove build file artifact run: rm ./readmevalidation Signed-off-by: Muhammad Atif Ali <me@matifali.dev>
1 parent 9452763 commit 72d7ee4

File tree

4 files changed

+166
-3
lines changed

4 files changed

+166
-3
lines changed

‎.github/workflows/ci.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ jobs:
6363
- name: Set up Go
6464
uses: actions/setup-go@v5
6565
with:
66-
go-version: "1.23.2"
67-
- name: Validate contributors
66+
go-version: "1.25.0"
67+
- name: Validate Reademde
6868
run: go build ./cmd/readmevalidation && ./readmevalidation
6969
- name: Remove build file artifact
7070
run: rm ./readmevalidation

‎cmd/readmevalidation/codermodules.go‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,87 @@ package main
33
import (
44
"bufio"
55
"context"
6+
"path/filepath"
7+
"regexp"
68
"strings"
79

810
"golang.org/x/xerrors"
911
)
1012

13+
var (
14+
terraformSourceRe = regexp.MustCompile(`^\s*source\s*=\s*"([^"]+)"`)
15+
)
16+
17+
func normalizeModuleName(name string) string {
18+
// Normalize module names by replacing hyphens with underscores for comparison
19+
// since Terraform allows both but directory names typically use hyphens
20+
return strings.ReplaceAll(name, "-", "_")
21+
}
22+
23+
func extractNamespaceAndModuleFromPath(filePath string) (string, string, error) {
24+
// Expected path format: registry/<namespace>/modules/<module-name>/README.md
25+
parts := strings.Split(filepath.Clean(filePath), string(filepath.Separator))
26+
if len(parts) < 5 || parts[0] != "registry" || parts[2] != "modules" || parts[4] != "README.md" {
27+
return "", "", xerrors.Errorf("invalid module path format: %s", filePath)
28+
}
29+
namespace := parts[1]
30+
moduleName := parts[3]
31+
return namespace, moduleName, nil
32+
}
33+
34+
func validateModuleSourceURL(body string, filePath string) []error {
35+
var errs []error
36+
37+
namespace, moduleName, err := extractNamespaceAndModuleFromPath(filePath)
38+
if err != nil {
39+
return []error{err}
40+
}
41+
42+
expectedSource := "registry.coder.com/" + namespace + "/" + moduleName + "/coder"
43+
44+
trimmed := strings.TrimSpace(body)
45+
foundCorrectSource := false
46+
isInsideTerraform := false
47+
firstTerraformBlock := true
48+
49+
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
50+
for lineScanner.Scan() {
51+
nextLine := lineScanner.Text()
52+
53+
if strings.HasPrefix(nextLine, "```") {
54+
if strings.HasPrefix(nextLine, "```tf") && firstTerraformBlock {
55+
isInsideTerraform = true
56+
firstTerraformBlock = false
57+
} else if isInsideTerraform {
58+
// End of first terraform block
59+
break
60+
}
61+
continue
62+
}
63+
64+
if isInsideTerraform {
65+
// Check for any source line in the first terraform block
66+
if matches := terraformSourceRe.FindStringSubmatch(nextLine); matches != nil {
67+
actualSource := matches[1]
68+
if actualSource == expectedSource {
69+
foundCorrectSource = true
70+
break
71+
} else if strings.HasPrefix(actualSource, "registry.coder.com/") && strings.Contains(actualSource, "/"+moduleName+"/coder") {
72+
// Found source for this module but with wrong namespace/format
73+
errs = append(errs, xerrors.Errorf("incorrect source URL format: found %q, expected %q", actualSource, expectedSource))
74+
return errs
75+
}
76+
}
77+
}
78+
}
79+
80+
if !foundCorrectSource {
81+
errs = append(errs, xerrors.Errorf("did not find correct source URL %q in first Terraform code block", expectedSource))
82+
}
83+
84+
return errs
85+
}
86+
1187
func validateCoderModuleReadmeBody(body string) []error {
1288
var errs []error
1389

@@ -94,6 +170,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
94170
for _, err := range validateCoderModuleReadmeBody(rm.body) {
95171
errs = append(errs, addFilePathToError(rm.filePath, err))
96172
}
173+
for _, err := range validateModuleSourceURL(rm.body, rm.filePath) {
174+
errs = append(errs, addFilePathToError(rm.filePath, err))
175+
}
97176
for _, err := range validateResourceGfmAlerts(rm.body) {
98177
errs = append(errs, addFilePathToError(rm.filePath, err))
99178
}

‎cmd/readmevalidation/codermodules_test.go‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,86 @@ func TestValidateCoderResourceReadmeBody(t *testing.T) {
2020
}
2121
})
2222
}
23+
24+
func TestValidateModuleSourceURL(t *testing.T) {
25+
t.Parallel()
26+
27+
t.Run("Valid source URL format", func(t *testing.T) {
28+
t.Parallel()
29+
30+
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"
31+
filePath := "registry/test-namespace/modules/test-module/README.md"
32+
errs := validateModuleSourceURL(body, filePath)
33+
if len(errs) != 0 {
34+
t.Errorf("Expected no errors, got: %v", errs)
35+
}
36+
})
37+
38+
t.Run("Invalid source URL format - wrong namespace", func(t *testing.T) {
39+
t.Parallel()
40+
41+
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"
42+
filePath := "registry/test-namespace/modules/test-module/README.md"
43+
errs := validateModuleSourceURL(body, filePath)
44+
if len(errs) != 1 {
45+
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
46+
}
47+
if len(errs) > 0 && !contains(errs[0].Error(), "incorrect source URL format") {
48+
t.Errorf("Expected source URL format error, got: %s", errs[0].Error())
49+
}
50+
})
51+
52+
t.Run("Missing source URL", func(t *testing.T) {
53+
t.Parallel()
54+
55+
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"
56+
filePath := "registry/test-namespace/modules/test-module/README.md"
57+
errs := validateModuleSourceURL(body, filePath)
58+
if len(errs) != 1 {
59+
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
60+
}
61+
if len(errs) > 0 && !contains(errs[0].Error(), "did not find correct source URL") {
62+
t.Errorf("Expected missing source URL error, got: %s", errs[0].Error())
63+
}
64+
})
65+
66+
t.Run("Module name with hyphens vs underscores", func(t *testing.T) {
67+
t.Parallel()
68+
69+
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"
70+
filePath := "registry/test-namespace/modules/test-module/README.md"
71+
errs := validateModuleSourceURL(body, filePath)
72+
if len(errs) != 0 {
73+
t.Errorf("Expected no errors for hyphen/underscore variation, got: %v", errs)
74+
}
75+
})
76+
77+
t.Run("Invalid file path format", func(t *testing.T) {
78+
t.Parallel()
79+
80+
body := "# Test Module"
81+
filePath := "invalid/path/format"
82+
errs := validateModuleSourceURL(body, filePath)
83+
if len(errs) != 1 {
84+
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
85+
}
86+
if len(errs) > 0 && !contains(errs[0].Error(), "invalid module path format") {
87+
t.Errorf("Expected path format error, got: %s", errs[0].Error())
88+
}
89+
})
90+
}
91+
92+
func contains(s, substr string) bool {
93+
return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) &&
94+
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
95+
indexOfSubstring(s, substr) >= 0)))
96+
}
97+
98+
func indexOfSubstring(s, substr string) int {
99+
for i := 0; i <= len(s)-len(substr); i++ {
100+
if s[i:i+len(substr)] == substr {
101+
return i
102+
}
103+
}
104+
return -1
105+
}

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
66
"terraform-validate": "./scripts/terraform_validate.sh",
77
"test": "./scripts/terraform_test_all.sh",
8-
"update-version": "./update-version.sh"
8+
"update-version": "./update-version.sh",
9+
"validate-readme": "go build ./cmd/readmevalidation && ./readmevalidation"
910
},
1011
"devDependencies": {
1112
"@types/bun": "^1.2.21",

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /