Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ registry/[namespace]/

For example: `/registry/your-username/modules/` and `/registry/your-username/templates/`. If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.

Namespace directory names must be **lowercase**, may only contain letters, numbers, and hyphens, and must start and end with a letter or number. The namespace becomes part of the case-sensitive module source path (`registry.coder.com/[namespace]/[module]/coder`), so lowercase keeps those paths predictable. A few namespaces created before this rule keep their mixed-case names because renaming them would break existing module references.

### Images and Icons

- **Namespace avatars**: Must be named `avatar.png` or `avatar.svg` in `/registry/[namespace]/.images/`
Expand Down
32 changes: 30 additions & 2 deletions cmd/readmevalidation/repostructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@ var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images"
// 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])?$`)

// validNamespaceRe validates that a namespace directory name is lowercase, contains only alphanumeric characters and
// hyphens, and starts and ends with an alphanumeric character. A namespace becomes part of the case-sensitive module
// source path (registry.coder.com/<namespace>/<module>/coder), so mixed case produces paths that are inconsistent and
// easy to mistype.
var validNamespaceRe = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`)

// grandfatheredMixedCaseNamespaces lists the namespaces that existed before the lowercase rule and cannot be renamed
// yet. Both have published modules whose source paths are case-sensitive, and the registry server has no alias
// mechanism for the old path. They are exempt until that migration happens. Do not add new entries.
var grandfatheredMixedCaseNamespaces = []string{
"AJ0070",
"BenraouaneSoufiane",
}

// validateNamespaceName validates the directory name of a single namespace under /registry.
func validateNamespaceName(namespaceName string) error {
if slices.Contains(grandfatheredMixedCaseNamespaces, namespaceName) {
return nil
}
if validNamespaceRe.MatchString(namespaceName) {
return nil
}
// If the lowercased name is valid, case was the only problem, so point at the name to use.
if lowercased := strings.ToLower(namespaceName); validNamespaceRe.MatchString(lowercased) {
return xerrors.Errorf("namespace name must be lowercase (use %q)", lowercased)
}
return xerrors.New("namespace name must contain only lowercase alphanumeric characters and hyphens, starting and ending with an alphanumeric character")
}

// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
// expected file conventions
Expand Down Expand Up @@ -91,8 +119,8 @@ func validateRegistryDirectory() []error {
}

// 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))
if err := validateNamespaceName(nDir.Name()); err != nil {
allErrs = append(allErrs, xerrors.Errorf("%q: %w", namespacePath, err))
continue
}

Expand Down
55 changes: 55 additions & 0 deletions cmd/readmevalidation/repostructure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import "testing"

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

testCases := []struct {
name string
shouldPass bool
}{
// Lowercase namespaces are always allowed.
{name: "coder", shouldPass: true},
{name: "coder-labs", shouldPass: true},
{name: "aj0070", shouldPass: true},
{name: "user123", shouldPass: true},
{name: "excellencedev", shouldPass: true},
{name: "iamtaochen", shouldPass: true},

// Mixed-case namespaces that predate the rule stay allowed.
{name: "AJ0070", shouldPass: true},
{name: "BenraouaneSoufiane", shouldPass: true},

// New mixed-case namespaces are rejected.
{name: "Coder", shouldPass: false},
{name: "CoderLabs", shouldPass: false},
{name: "coder-Labs", shouldPass: false},
{name: "Excellencedev", shouldPass: false},
{name: "IamTaoChen", shouldPass: false},

// Other invalid names are still rejected.
{name: "", shouldPass: false},
{name: "-coder", shouldPass: false},
{name: "coder-", shouldPass: false},
{name: "coder_labs", shouldPass: false},
{name: "coder labs", shouldPass: false},
{name: "coder.labs", shouldPass: false},
{name: "Coder_Labs", shouldPass: false},
{name: "-Coder", shouldPass: false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

err := validateNamespaceName(tc.name)
if tc.shouldPass && err != nil {
t.Errorf("expected %q to be a valid namespace name, got error: %v", tc.name, err)
}
if !tc.shouldPass && err == nil {
t.Errorf("expected %q to be an invalid namespace name, got no error", tc.name)
}
})
}
Comment thread
matifali marked this conversation as resolved.
}
File renamed without changes.
File renamed without changes.