From eae5f430f9cd339fe1666168d6a1364e7b824504 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 27 Jul 2026 11:39:20 +0000 Subject: [PATCH 1/4] feat(cmd/readmevalidation): enforce lowercase namespaces in registry/ --- CONTRIBUTING.md | 2 + cmd/readmevalidation/repostructure.go | 33 +++++++++++++- cmd/readmevalidation/repostructure_test.go | 51 ++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 cmd/readmevalidation/repostructure_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f46eb674..e45bdb070 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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** and may only contain letters, numbers, and hyphens. 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/` diff --git a/cmd/readmevalidation/repostructure.go b/cmd/readmevalidation/repostructure.go index 984218869..4307ad473 100644 --- a/cmd/readmevalidation/repostructure.go +++ b/cmd/readmevalidation/repostructure.go @@ -16,6 +16,35 @@ 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 and contains only alphanumeric characters +// and hyphens. A namespace becomes part of the case-sensitive module source path +// (registry.coder.com///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. Renaming them would +// break the module source paths that users already reference in their Terraform configurations, so they are exempt. +// Do not add new entries. +var grandfatheredMixedCaseNamespaces = []string{ + "AJ0070", + "BenraouaneSoufiane", + "Excellencedev", + "IamTaoChen", +} + +// 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 validNameRe.MatchString(namespaceName) { + return xerrors.Errorf("namespace name must be lowercase (use %q)", strings.ToLower(namespaceName)) + } + return xerrors.New("namespace name contains invalid characters (only lowercase alphanumeric characters and hyphens are allowed)") +} // validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all // expected file conventions @@ -91,8 +120,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 } diff --git a/cmd/readmevalidation/repostructure_test.go b/cmd/readmevalidation/repostructure_test.go new file mode 100644 index 000000000..d4cf75a24 --- /dev/null +++ b/cmd/readmevalidation/repostructure_test.go @@ -0,0 +1,51 @@ +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}, + + // Mixed-case namespaces that predate the rule stay allowed. + {name: "AJ0070", shouldPass: true}, + {name: "BenraouaneSoufiane", shouldPass: true}, + {name: "Excellencedev", shouldPass: true}, + {name: "IamTaoChen", shouldPass: true}, + + // New mixed-case namespaces are rejected. + {name: "Coder", shouldPass: false}, + {name: "CoderLabs", shouldPass: false}, + {name: "coder-Labs", 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}, + } + + 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) + } + }) + } +} From a5ff1d0b91a005b2bed97657acc6d13ee9527658 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 27 Jul 2026 16:27:43 +0000 Subject: [PATCH 2/4] docs(cmd/readmevalidation): clarify why mixed-case namespaces are grandfathered --- cmd/readmevalidation/repostructure.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/readmevalidation/repostructure.go b/cmd/readmevalidation/repostructure.go index 4307ad473..db41574f8 100644 --- a/cmd/readmevalidation/repostructure.go +++ b/cmd/readmevalidation/repostructure.go @@ -22,9 +22,9 @@ var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$ // mistype. var validNamespaceRe = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`) -// grandfatheredMixedCaseNamespaces lists the namespaces that existed before the lowercase rule. Renaming them would -// break the module source paths that users already reference in their Terraform configurations, so they are exempt. -// Do not add new entries. +// grandfatheredMixedCaseNamespaces lists the namespaces that existed before the lowercase rule. Renaming them requires +// coordination with the registry server, because published module source paths are case-sensitive and the 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", From 7a80c2fba1d8193ca5b32dbbe79f8ee25b4da688 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 7 Aug 2026 07:22:14 +0000 Subject: [PATCH 3/4] chore(registry): lowercase the excellencedev and iamtaochen namespaces --- cmd/readmevalidation/repostructure.go | 8 +++----- cmd/readmevalidation/repostructure_test.go | 6 ++++-- .../.images/avatar.png | Bin registry/{Excellencedev => excellencedev}/README.md | 0 .../templates/hetzner-linux/README.md | 0 .../templates/hetzner-linux/cloud-config.yaml.tftpl | 0 .../templates/hetzner-linux/main.tf | 0 .../{IamTaoChen => iamtaochen}/.images/avatar.png | Bin registry/{IamTaoChen => iamtaochen}/README.md | 0 .../templates/ssh-linux/README.md | 0 .../templates/ssh-linux/main.tf | 0 11 files changed, 7 insertions(+), 7 deletions(-) rename registry/{Excellencedev => excellencedev}/.images/avatar.png (100%) rename registry/{Excellencedev => excellencedev}/README.md (100%) rename registry/{Excellencedev => excellencedev}/templates/hetzner-linux/README.md (100%) rename registry/{Excellencedev => excellencedev}/templates/hetzner-linux/cloud-config.yaml.tftpl (100%) rename registry/{Excellencedev => excellencedev}/templates/hetzner-linux/main.tf (100%) rename registry/{IamTaoChen => iamtaochen}/.images/avatar.png (100%) rename registry/{IamTaoChen => iamtaochen}/README.md (100%) rename registry/{IamTaoChen => iamtaochen}/templates/ssh-linux/README.md (100%) rename registry/{IamTaoChen => iamtaochen}/templates/ssh-linux/main.tf (100%) diff --git a/cmd/readmevalidation/repostructure.go b/cmd/readmevalidation/repostructure.go index db41574f8..7dcccff4b 100644 --- a/cmd/readmevalidation/repostructure.go +++ b/cmd/readmevalidation/repostructure.go @@ -22,14 +22,12 @@ var validNameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$ // mistype. var validNamespaceRe = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`) -// grandfatheredMixedCaseNamespaces lists the namespaces that existed before the lowercase rule. Renaming them requires -// coordination with the registry server, because published module source paths are case-sensitive and the server has no -// alias mechanism for the old path. They are exempt until that migration happens. Do not add new entries. +// 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", - "Excellencedev", - "IamTaoChen", } // validateNamespaceName validates the directory name of a single namespace under /registry. diff --git a/cmd/readmevalidation/repostructure_test.go b/cmd/readmevalidation/repostructure_test.go index d4cf75a24..8e5142415 100644 --- a/cmd/readmevalidation/repostructure_test.go +++ b/cmd/readmevalidation/repostructure_test.go @@ -14,17 +14,19 @@ func TestValidateNamespaceName(t *testing.T) { {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}, - {name: "Excellencedev", shouldPass: true}, - {name: "IamTaoChen", 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}, diff --git a/registry/Excellencedev/.images/avatar.png b/registry/excellencedev/.images/avatar.png similarity index 100% rename from registry/Excellencedev/.images/avatar.png rename to registry/excellencedev/.images/avatar.png diff --git a/registry/Excellencedev/README.md b/registry/excellencedev/README.md similarity index 100% rename from registry/Excellencedev/README.md rename to registry/excellencedev/README.md diff --git a/registry/Excellencedev/templates/hetzner-linux/README.md b/registry/excellencedev/templates/hetzner-linux/README.md similarity index 100% rename from registry/Excellencedev/templates/hetzner-linux/README.md rename to registry/excellencedev/templates/hetzner-linux/README.md diff --git a/registry/Excellencedev/templates/hetzner-linux/cloud-config.yaml.tftpl b/registry/excellencedev/templates/hetzner-linux/cloud-config.yaml.tftpl similarity index 100% rename from registry/Excellencedev/templates/hetzner-linux/cloud-config.yaml.tftpl rename to registry/excellencedev/templates/hetzner-linux/cloud-config.yaml.tftpl diff --git a/registry/Excellencedev/templates/hetzner-linux/main.tf b/registry/excellencedev/templates/hetzner-linux/main.tf similarity index 100% rename from registry/Excellencedev/templates/hetzner-linux/main.tf rename to registry/excellencedev/templates/hetzner-linux/main.tf diff --git a/registry/IamTaoChen/.images/avatar.png b/registry/iamtaochen/.images/avatar.png similarity index 100% rename from registry/IamTaoChen/.images/avatar.png rename to registry/iamtaochen/.images/avatar.png diff --git a/registry/IamTaoChen/README.md b/registry/iamtaochen/README.md similarity index 100% rename from registry/IamTaoChen/README.md rename to registry/iamtaochen/README.md diff --git a/registry/IamTaoChen/templates/ssh-linux/README.md b/registry/iamtaochen/templates/ssh-linux/README.md similarity index 100% rename from registry/IamTaoChen/templates/ssh-linux/README.md rename to registry/iamtaochen/templates/ssh-linux/README.md diff --git a/registry/IamTaoChen/templates/ssh-linux/main.tf b/registry/iamtaochen/templates/ssh-linux/main.tf similarity index 100% rename from registry/IamTaoChen/templates/ssh-linux/main.tf rename to registry/iamtaochen/templates/ssh-linux/main.tf From 11fadfe59b5edcb25be55cadf87263674cdf82b5 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 7 Aug 2026 16:48:05 +0000 Subject: [PATCH 4/4] fix(cmd/readmevalidation): drop validNameRe from namespace check and document boundary rule --- CONTRIBUTING.md | 2 +- cmd/readmevalidation/repostructure.go | 15 ++++++++------- cmd/readmevalidation/repostructure_test.go | 2 ++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e45bdb070..4d1383fbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ 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** and may only contain letters, numbers, and hyphens. 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. +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 diff --git a/cmd/readmevalidation/repostructure.go b/cmd/readmevalidation/repostructure.go index 7dcccff4b..09782d376 100644 --- a/cmd/readmevalidation/repostructure.go +++ b/cmd/readmevalidation/repostructure.go @@ -16,10 +16,10 @@ 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 and contains only alphanumeric characters -// and hyphens. A namespace becomes part of the case-sensitive module source path -// (registry.coder.com///coder), so mixed case produces paths that are inconsistent and easy to -// mistype. +// 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///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 @@ -38,10 +38,11 @@ func validateNamespaceName(namespaceName string) error { if validNamespaceRe.MatchString(namespaceName) { return nil } - if validNameRe.MatchString(namespaceName) { - return xerrors.Errorf("namespace name must be lowercase (use %q)", strings.ToLower(namespaceName)) + // 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 contains invalid characters (only lowercase alphanumeric characters and hyphens are allowed)") + 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 diff --git a/cmd/readmevalidation/repostructure_test.go b/cmd/readmevalidation/repostructure_test.go index 8e5142415..953ddd93f 100644 --- a/cmd/readmevalidation/repostructure_test.go +++ b/cmd/readmevalidation/repostructure_test.go @@ -35,6 +35,8 @@ func TestValidateNamespaceName(t *testing.T) { {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 {