Skip to content

Commit 1634c40

Browse files
committed
chore: add simplified version of module validation logic
1 parent 31c69e2 commit 1634c40

File tree

4 files changed

+154
-30
lines changed

4 files changed

+154
-30
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
)
9+
10+
var supportedResourceTypes = []string{"modules", "templates"}
11+
12+
type coderResourceFrontmatter struct {
13+
Description string `yaml:"description"`
14+
IconURL string `yaml:"icon"`
15+
DisplayName *string `yaml:"display_name"`
16+
Verified *bool `yaml:"verified"`
17+
Tags []string `yaml:"tags"`
18+
}
19+
20+
// coderResourceReadme represents a README describing a Terraform resource used
21+
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both
22+
// Coder Modules and Coder Templates
23+
type coderResourceReadme struct {
24+
resourceType string
25+
filePath string
26+
body string
27+
frontmatter coderResourceFrontmatter
28+
}
29+
30+
func validateCoderResourceDisplayName(displayName *string) error {
31+
if displayName != nil && *displayName == "" {
32+
return errors.New("if defined, display_name must not be empty string")
33+
}
34+
return nil
35+
}
36+
37+
func validateCoderResourceDescription(description string) error {
38+
if description == "" {
39+
return errors.New("frontmatter description cannot be empty")
40+
}
41+
return nil
42+
}
43+
44+
func validateCoderResourceIconURL(iconURL string) []error {
45+
problems := []error{}
46+
47+
if iconURL == "" {
48+
problems = append(problems, errors.New("icon URL cannot be empty"))
49+
return problems
50+
}
51+
52+
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
53+
if isAbsoluteURL {
54+
if _, err := url.ParseRequestURI(iconURL); err != nil {
55+
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
56+
}
57+
if strings.Contains(iconURL, "?") {
58+
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
59+
}
60+
return problems
61+
}
62+
63+
// Would normally be skittish about having relative paths like this, but it
64+
// should be safe because we have guarantees about the structure of the
65+
// repo, and where this logic will run
66+
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
67+
strings.HasPrefix(iconURL, "/") ||
68+
strings.HasPrefix(iconURL, "../../../../.icons")
69+
if !isPermittedRelativeURL {
70+
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
71+
}
72+
73+
return problems
74+
}
75+
76+
func validateCoderResourceTags(tags []string) error {
77+
if len(tags) == 0 {
78+
return nil
79+
}
80+
81+
// All of these tags are used for the module/template filter controls in the
82+
// Registry site. Need to make sure they can all be placed in the browser
83+
// URL without issue
84+
invalidTags := []string{}
85+
for _, t := range tags {
86+
if t != url.QueryEscape(t) {
87+
invalidTags = append(invalidTags, t)
88+
}
89+
}
90+
91+
if len(invalidTags) != 0 {
92+
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
93+
}
94+
return nil
95+
}
96+
97+
func validateCoderResourceChanges(resource coderResourceReadme) []error {
98+
var errs []error
99+
100+
if err := validateReadmeBody(resource.body); err != nil {
101+
errs = append(errs, addFilePathToError(resource.filePath, err))
102+
}
103+
104+
if err := validateCoderResourceDisplayName(resource.frontmatter.DisplayName); err != nil {
105+
errs = append(errs, addFilePathToError(resource.filePath, err))
106+
}
107+
if err := validateCoderResourceDescription(resource.frontmatter.Description); err != nil {
108+
errs = append(errs, addFilePathToError(resource.filePath, err))
109+
}
110+
if err := validateCoderResourceTags(resource.frontmatter.Tags); err != nil {
111+
errs = append(errs, addFilePathToError(resource.filePath, err))
112+
}
113+
114+
for _, err := range validateCoderResourceIconURL(resource.frontmatter.IconURL) {
115+
errs = append(errs, addFilePathToError(resource.filePath, err))
116+
}
117+
118+
return errs
119+
}

cmd/readmevalidation/contributors.go

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type contributorProfileFrontmatter struct {
2929
ContributorStatus *string `yaml:"status"`
3030
}
3131

32-
type contributorProfile struct {
32+
type contributorProfileReadme struct {
3333
frontmatter contributorProfileFrontmatter
3434
filePath string
3535
}
@@ -192,57 +192,57 @@ func validateContributorAvatarURL(avatarURL *string) []error {
192192
return errs
193193
}
194194

195-
func validateContributorYaml(yml contributorProfile) []error {
195+
func validateContributorReadme(rm contributorProfileReadme) []error {
196196
allErrs := []error{}
197197

198-
if err := validateContributorGithubUsername(yml.frontmatter.GithubUsername); err != nil {
199-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
198+
if err := validateContributorGithubUsername(rm.frontmatter.GithubUsername); err != nil {
199+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
200200
}
201-
if err := validateContributorDisplayName(yml.frontmatter.DisplayName); err != nil {
202-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
201+
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
202+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
203203
}
204-
if err := validateContributorLinkedinURL(yml.frontmatter.LinkedinURL); err != nil {
205-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
204+
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
205+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
206206
}
207-
if err := validateContributorWebsite(yml.frontmatter.WebsiteURL); err != nil {
208-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
207+
if err := validateContributorWebsite(rm.frontmatter.WebsiteURL); err != nil {
208+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
209209
}
210-
if err := validateContributorStatus(yml.frontmatter.ContributorStatus); err != nil {
211-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
210+
if err := validateContributorStatus(rm.frontmatter.ContributorStatus); err != nil {
211+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
212212
}
213213

214-
for _, err := range validateContributorEmployerGithubUsername(yml.frontmatter.EmployerGithubUsername, yml.frontmatter.GithubUsername) {
215-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
214+
for _, err := range validateContributorEmployerGithubUsername(rm.frontmatter.EmployerGithubUsername, rm.frontmatter.GithubUsername) {
215+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
216216
}
217-
for _, err := range validateContributorSupportEmail(yml.frontmatter.SupportEmail) {
218-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
217+
for _, err := range validateContributorSupportEmail(rm.frontmatter.SupportEmail) {
218+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
219219
}
220-
for _, err := range validateContributorAvatarURL(yml.frontmatter.AvatarURL) {
221-
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
220+
for _, err := range validateContributorAvatarURL(rm.frontmatter.AvatarURL) {
221+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
222222
}
223223

224224
return allErrs
225225
}
226226

227-
func parseContributorProfile(rm readme) (contributorProfile, error) {
227+
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
228228
fm, _, err := separateFrontmatter(rm.rawText)
229229
if err != nil {
230-
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
230+
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
231231
}
232232

233233
yml := contributorProfileFrontmatter{}
234234
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
235-
return contributorProfile{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
235+
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
236236
}
237237

238-
return contributorProfile{
238+
return contributorProfileReadme{
239239
filePath: rm.filePath,
240240
frontmatter: yml,
241241
}, nil
242242
}
243243

244-
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfile, error) {
245-
profilesByUsername := map[string]contributorProfile{}
244+
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
245+
profilesByUsername := map[string]contributorProfileReadme{}
246246
yamlParsingErrors := []error{}
247247
for _, rm := range readmeEntries {
248248
p, err := parseContributorProfile(rm)
@@ -267,7 +267,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
267267
employeeGithubGroups := map[string][]string{}
268268
yamlValidationErrors := []error{}
269269
for _, p := range profilesByUsername {
270-
errors := validateContributorYaml(p)
270+
errors := validateContributorReadme(p)
271271
if len(errors) > 0 {
272272
yamlValidationErrors = append(yamlValidationErrors, errors...)
273273
continue
@@ -332,7 +332,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
332332
return allReadmeFiles, nil
333333
}
334334

335-
func validateContributorRelativeUrls(contributors map[string]contributorProfile) error {
335+
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error {
336336
// This function only validates relative avatar URLs for now, but it can be
337337
// beefed up to validate more in the future
338338
errs := []error{}

cmd/readmevalidation/readmeFiles.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ func separateFrontmatter(readmeText string) (string, string, error) {
6666
return fm, strings.TrimSpace(body), nil
6767
}
6868

69+
func validateReadmeBody(body string) error {
70+
trimmed := strings.TrimSpace(body)
71+
if !strings.HasPrefix(trimmed, "# ") {
72+
return errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")
73+
}
74+
return nil
75+
}
76+
6977
// validationPhase represents a specific phase during README validation. It is
7078
// expected that each phase is discrete, and errors during one will prevent a
7179
// future phase from starting.

cmd/readmevalidation/repoStructure.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import (
99
"strings"
1010
)
1111

12-
var (
13-
supportedResourceTypes = []string{"modules", "templates"}
14-
supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
15-
)
12+
var supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
1613

1714
func validateCoderResourceSubdirectory(dirPath string) []error {
1815
errs := []error{}

0 commit comments

Comments
 (0)