Skip to content

Commit 3ac0c62

Browse files
committed
fix: add missing validation for GitHub usernames
1 parent af2016a commit 3ac0c62

File tree

2 files changed

+62
-6
lines changed

2 files changed

+62
-6
lines changed

cmd/readmevalidation/contributors.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ type contributorProfileFrontmatter struct {
1919
Bio string `yaml:"bio"`
2020
ContributorStatus string `yaml:"status"`
2121
AvatarURL *string `yaml:"avatar"`
22+
GithubUsername *string `yaml:"github"`
2223
LinkedinURL *string `yaml:"linkedin"`
2324
WebsiteURL *string `yaml:"website"`
2425
SupportEmail *string `yaml:"support_email"`
2526
}
2627

28+
// A slice version of the struct tags from contributorProfileFrontmatter. Might be worth using reflection to generate
29+
// this list at runtime in the future, but this should be okay for now
30+
var supportedContributorProfileStructKeys = []string{"display_name", "bio", "status", "avatar", "linkedin", "github", "website", "support_email"}
31+
2732
type contributorProfileReadme struct {
2833
frontmatter contributorProfileFrontmatter
2934
namespace string
@@ -50,6 +55,22 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
5055
return nil
5156
}
5257

58+
func validateGithubUsername(username *string) error {
59+
if username == nil {
60+
return nil
61+
}
62+
63+
name := *username
64+
trimmed := strings.TrimSpace(name)
65+
if trimmed == "" {
66+
return xerrors.New("username must have non-whitespace characters")
67+
}
68+
if name != trimmed {
69+
return xerrors.Errorf("username %q has extra whitespace", trimmed)
70+
}
71+
return nil
72+
}
73+
5374
// validateContributorSupportEmail does best effort validation of a contributors email address. We can't 100% validate
5475
// that this is correct without actually sending an email, especially because some contributors are individual developers
5576
// and we don't want to do that on every single run of the CI pipeline. The best we can do is verify the general structure.
@@ -153,6 +174,9 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
153174
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
154175
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
155176
}
177+
if err := validateGithubUsername(rm.frontmatter.GithubUsername); err != nil {
178+
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
179+
}
156180
if err := validateContributorWebsite(rm.frontmatter.WebsiteURL); err != nil {
157181
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
158182
}
@@ -170,15 +194,24 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
170194
return allErrs
171195
}
172196

173-
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
197+
func parseContributorProfile(rm readme) (contributorProfileReadme, []error) {
174198
fm, _, err := separateFrontmatter(rm.rawText)
175199
if err != nil {
176-
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
200+
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)}
201+
}
202+
203+
keyErrs := validateFrontmatterYamlKeys(fm, supportedContributorProfileStructKeys)
204+
if len(keyErrs) != 0 {
205+
remapped := []error{}
206+
for _, e := range keyErrs {
207+
remapped = append(remapped, addFilePathToError(rm.filePath, e))
208+
}
209+
return contributorProfileReadme{}, remapped
177210
}
178211

179212
yml := contributorProfileFrontmatter{}
180213
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
181-
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
214+
return contributorProfileReadme{}, []error{xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)}
182215
}
183216

184217
return contributorProfileReadme{
@@ -192,9 +225,9 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
192225
profilesByNamespace := map[string]contributorProfileReadme{}
193226
yamlParsingErrors := []error{}
194227
for _, rm := range readmeEntries {
195-
p, err := parseContributorProfile(rm)
196-
if err != nil {
197-
yamlParsingErrors = append(yamlParsingErrors, err)
228+
p, errs := parseContributorProfile(rm)
229+
if len(errs) != 0 {
230+
yamlParsingErrors = append(yamlParsingErrors, errs...)
198231
continue
199232
}
200233

cmd/readmevalidation/readmefiles.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"fmt"
66
"regexp"
7+
"slices"
78
"strings"
89

910
"golang.org/x/xerrors"
@@ -170,3 +171,25 @@ func validateReadmeBody(body string) []error {
170171

171172
return errs
172173
}
174+
175+
func validateFrontmatterYamlKeys(frontmatter string, allowedKeys []string) []error {
176+
if len(allowedKeys) == 0 {
177+
return []error{xerrors.New("Set of allowed keys is empty")}
178+
}
179+
180+
var key string
181+
var cutOk bool
182+
var line string
183+
184+
var errs []error
185+
lineScanner := bufio.NewScanner(strings.NewReader(frontmatter))
186+
for lineScanner.Scan() {
187+
line = lineScanner.Text()
188+
key, _, cutOk = strings.Cut(line, ":")
189+
if !cutOk || slices.Contains(allowedKeys, key) {
190+
continue
191+
}
192+
errs = append(errs, xerrors.Errorf("detected unknown key %q", key))
193+
}
194+
return errs
195+
}

0 commit comments

Comments
 (0)