diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ece269e..333c4b5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: read diff --git a/cmd/publisher/commands/init.go b/cmd/publisher/commands/init.go index 5ca2b4b..9c5db75 100644 --- a/cmd/publisher/commands/init.go +++ b/cmd/publisher/commands/init.go @@ -308,7 +308,7 @@ func createServerJSON( URL: repoURL, Source: repoSource, }, - Version: version, + Version: version, Packages: []model.Package{pkg}, } } diff --git a/cmd/publisher/commands/logout.go b/cmd/publisher/commands/logout.go index 81b8ca3..3ac9abd 100644 --- a/cmd/publisher/commands/logout.go +++ b/cmd/publisher/commands/logout.go @@ -13,7 +13,7 @@ func LogoutCommand() error { } tokenPath := filepath.Join(homeDir, TokenFileName) - + // Check if token file exists if _, err := os.Stat(tokenPath); os.IsNotExist(err) { _, _ = fmt.Fprintln(os.Stdout, "Not logged in") @@ -30,7 +30,7 @@ func LogoutCommand() error { ".mcpregistry_github_token", ".mcpregistry_registry_token", } - + for _, file := range legacyFiles { path := filepath.Join(homeDir, file) if _, err := os.Stat(path); err == nil { @@ -40,4 +40,4 @@ func LogoutCommand() error { _, _ = fmt.Fprintln(os.Stdout, "✓ Successfully logged out") return nil -} \ No newline at end of file +} diff --git a/cmd/publisher/main.go b/cmd/publisher/main.go index f622749..db0ef92 100644 --- a/cmd/publisher/main.go +++ b/cmd/publisher/main.go @@ -67,4 +67,4 @@ func printUsage() { _, _ = fmt.Fprintln(os.Stdout, " publish Publish server.json to the registry") _, _ = fmt.Fprintln(os.Stdout) _, _ = fmt.Fprintln(os.Stdout, "Use 'mcp-publisher --help' for more information about a command.") -} \ No newline at end of file +} diff --git a/deploy/pkg/k8s/ingress.go b/deploy/pkg/k8s/ingress.go index fd9cdd2..e1ee50b 100644 --- a/deploy/pkg/k8s/ingress.go +++ b/deploy/pkg/k8s/ingress.go @@ -51,7 +51,7 @@ func SetupIngressController(ctx *pulumi.Context, cluster *providers.ProviderInfo Values: pulumi.Map{ "controller": pulumi.Map{ "service": pulumi.Map{ - "type": serviceType, + "type": serviceType, "annotations": pulumi.Map{}, }, "config": pulumi.Map{ diff --git a/deploy/pkg/providers/gcp/provider.go b/deploy/pkg/providers/gcp/provider.go index fe2513e..108a209 100644 --- a/deploy/pkg/providers/gcp/provider.go +++ b/deploy/pkg/providers/gcp/provider.go @@ -22,19 +22,19 @@ type Provider struct{} // createGCPProvider creates a GCP provider with explicit credentials if configured func createGCPProvider(ctx *pulumi.Context, name string) (*gcp.Provider, error) { gcpConf := config.New(ctx, "gcp") - + // Get project ID from config projectID := gcpConf.Get("project") if projectID == "" { return nil, fmt.Errorf("GCP project ID not configured. Set gcp:project") } - + // Get region from config or use default region := gcpConf.Get("region") if region == "" { region = "us-central1" } - + // Get credentials from config (base64 encoded service account JSON) credentials := gcpConf.Get("credentials") if credentials != "" { @@ -45,7 +45,7 @@ func createGCPProvider(ctx *pulumi.Context, name string) (*gcp.Provider, error) } credentials = string(decodedCreds) } - + // Create a GCP provider with explicit credentials if provided if credentials != "" { return gcp.NewProvider(ctx, name, &gcp.ProviderArgs{ @@ -54,7 +54,7 @@ func createGCPProvider(ctx *pulumi.Context, name string) (*gcp.Provider, error) Credentials: pulumi.String(credentials), }) } - + return nil, nil } diff --git a/docs/guides/contributing/releasing.md b/docs/guides/contributing/releasing.md index cfb485c..a6ce969 100644 --- a/docs/guides/contributing/releasing.md +++ b/docs/guides/contributing/releasing.md @@ -16,10 +16,34 @@ The release workflow will automatically: ## After Release -- Docker images will be available at: +The release workflow will automatically: +- Build and publish Docker images: - `ghcr.io/modelcontextprotocol/registry:latest` - Latest stable release - `ghcr.io/modelcontextprotocol/registry:vX.Y.Z` - Specific release version -- Binaries can be downloaded from the GitHub release page +- Binaries will be available on the GitHub release page + +**Important:** Creating a release does **not** automatically deploy to production. Production deployments are triggered by pushes to the `main` branch. + +## Deploying the Release to Production + +After creating a release, you need to ensure production pulls the new `:latest` Docker image. You have two options: + +1. **Recommended:** Merge a commit to `main` (e.g., a changelog update or version bump). This will trigger the deployment workflow which will: + - Build and push a new `:main` Docker image + - Deploy to staging + - Deploy to production (which uses `:latest` and will pull the new image) + +2. **Manual restart (requires kubectl access):** + ```bash + kubectl rollout restart deployment/mcp-registry -n default + ``` + +## Verifying the Deployment + +After deployment completes, verify the changes are live: +- Check the deployment workflow succeeded: https://github.com/modelcontextprotocol/registry/actions/workflows/deploy.yml +- Test the API endpoint to confirm schema changes are applied +- Check migrations ran successfully in the pod logs ## Docker Image Tags diff --git a/internal/api/handlers/v0/auth/http_test.go b/internal/api/handlers/v0/auth/http_test.go index 55a31cd..2668ada 100644 --- a/internal/api/handlers/v0/auth/http_test.go +++ b/internal/api/handlers/v0/auth/http_test.go @@ -387,7 +387,6 @@ func TestDefaultHTTPKeyFetcher(t *testing.T) { } } - func TestHTTPAuthHandler_Permissions(t *testing.T) { cfg := &config.Config{ JWTPrivateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", @@ -738,4 +737,3 @@ func TestHTTPvsDNS_PermissionDifferences(t *testing.T) { }) } } - diff --git a/internal/api/handlers/v0/edit.go b/internal/api/handlers/v0/edit.go index eb081ec..ee7d8af 100644 --- a/internal/api/handlers/v0/edit.go +++ b/internal/api/handlers/v0/edit.go @@ -97,8 +97,8 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg * // Prevent undeleting servers - once deleted, they stay deleted if currentServer.Meta.Official != nil && - currentServer.Meta.Official.Status == model.StatusDeleted && - newStatus != model.StatusDeleted { + currentServer.Meta.Official.Status == model.StatusDeleted && + newStatus != model.StatusDeleted { return nil, huma.Error400BadRequest("Cannot change status of deleted server. Deleted servers cannot be undeleted.") } diff --git a/internal/api/handlers/v0/edit_test.go b/internal/api/handlers/v0/edit_test.go index 5b7b173..db614e1 100644 --- a/internal/api/handlers/v0/edit_test.go +++ b/internal/api/handlers/v0/edit_test.go @@ -639,4 +639,4 @@ func TestEditServerEndpointEdgeCases(t *testing.T) { // Helper function func stringPtr(s string) *string { return &s -} \ No newline at end of file +} diff --git a/internal/api/handlers/v0/publish.go b/internal/api/handlers/v0/publish.go index 2a0e7a9..60348e5 100644 --- a/internal/api/handlers/v0/publish.go +++ b/internal/api/handlers/v0/publish.go @@ -75,7 +75,7 @@ func buildPermissionErrorMessage(attemptedResource string, permissions []auth.Pe permissionStrs = append(permissionStrs, perm.ResourcePattern) } } - + errorMsg := "You do not have permission to publish this server" if len(permissionStrs) > 0 { errorMsg += ". You have permission to publish: " + strings.Join(permissionStrs, ", ") @@ -83,6 +83,6 @@ func buildPermissionErrorMessage(attemptedResource string, permissions []auth.Pe errorMsg += ". You do not have any publish permissions" } errorMsg += ". Attempting to publish: " + attemptedResource - + return errorMsg } diff --git a/internal/api/handlers/v0/publish_integration_test.go b/internal/api/handlers/v0/publish_integration_test.go index 083258f..1bd6a19 100644 --- a/internal/api/handlers/v0/publish_integration_test.go +++ b/internal/api/handlers/v0/publish_integration_test.go @@ -162,9 +162,9 @@ func TestPublishIntegration(t *testing.T) { t.Run("publish fails with invalid token", func(t *testing.T) { publishReq := apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", - Description: "Test server", - Version: "1.0.0", + Name: "io.github.domdomegg/test-server", + Description: "Test server", + Version: "1.0.0", } body, err := json.Marshal(publishReq) @@ -185,7 +185,7 @@ func TestPublishIntegration(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "io.github.other/test-server", Description: "A test server", - Version: "1.0.0", + Version: "1.0.0", Repository: model.Repository{ URL: "https://github.com/example/test-server", Source: "github", @@ -221,7 +221,7 @@ func TestPublishIntegration(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "io.github.domdomegg/airtable-mcp-server", Description: "A test server with MCPB package", - Version: "1.7.2", + Version: "1.7.2", Packages: []model.Package{ { RegistryType: model.RegistryTypeMCPB, diff --git a/internal/api/handlers/v0/publish_registry_validation_test.go b/internal/api/handlers/v0/publish_registry_validation_test.go index 91530db..11ff938 100644 --- a/internal/api/handlers/v0/publish_registry_validation_test.go +++ b/internal/api/handlers/v0/publish_registry_validation_test.go @@ -47,7 +47,7 @@ func TestPublishRegistryValidation(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "com.example/test-server-with-npm", Description: "A test server with invalid npm package reference", - Version: "1.0.0", + Version: "1.0.0", Packages: []model.Package{ { RegistryType: model.RegistryTypeNPM, @@ -88,7 +88,7 @@ func TestPublishRegistryValidation(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "com.example/test-server-mcpb-validation", Description: "A test server with MCPB package and registry validation enabled", - Version: "0.0.36", + Version: "0.0.36", Packages: []model.Package{ { RegistryType: model.RegistryTypeMCPB, @@ -138,7 +138,7 @@ func TestPublishRegistryValidation(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "com.example/test-server-multiple-packages", Description: "A test server with multiple packages where second fails", - Version: "1.0.0", + Version: "1.0.0", Packages: []model.Package{ { RegistryType: model.RegistryTypeMCPB, @@ -189,7 +189,7 @@ func TestPublishRegistryValidation(t *testing.T) { publishReq := apiv0.ServerJSON{ Name: "com.example/test-server-first-package-fails", Description: "A test server where first package fails", - Version: "1.0.0", + Version: "1.0.0", Packages: []model.Package{ { RegistryType: model.RegistryTypeNPM, diff --git a/internal/api/handlers/v0/publish_test.go b/internal/api/handlers/v0/publish_test.go index d47e999..f64c0d8 100644 --- a/internal/api/handlers/v0/publish_test.go +++ b/internal/api/handlers/v0/publish_test.go @@ -113,9 +113,9 @@ func TestPublishEndpoint(t *testing.T) { { name: "invalid authorization header format", requestBody: apiv0.ServerJSON{ - Name: "io.github.domdomegg/test-server", - Description: "Test server", - Version: "1.0.0", + Name: "io.github.domdomegg/test-server", + Description: "Test server", + Version: "1.0.0", }, authHeader: "InvalidFormat", setupRegistryService: func(_ service.RegistryService) { @@ -129,7 +129,7 @@ func TestPublishEndpoint(t *testing.T) { requestBody: apiv0.ServerJSON{ Name: "test-server", Description: "A test server", - Version: "1.0.0", + Version: "1.0.0", }, authHeader: "Bearer invalidToken", setupRegistryService: func(_ service.RegistryService) { @@ -143,7 +143,7 @@ func TestPublishEndpoint(t *testing.T) { requestBody: apiv0.ServerJSON{ Name: "io.github.other/test-server", Description: "A test server", - Version: "1.0.0", + Version: "1.0.0", Repository: model.Repository{ URL: "https://github.com/example/test-server", Source: "github", @@ -167,7 +167,7 @@ func TestPublishEndpoint(t *testing.T) { requestBody: apiv0.ServerJSON{ Name: "example/test-server", Description: "A test server", - Version: "1.0.0", + Version: "1.0.0", Repository: model.Repository{ URL: "https://github.com/example/test-server", Source: "github", @@ -185,7 +185,7 @@ func TestPublishEndpoint(t *testing.T) { existingServer := apiv0.ServerJSON{ Name: "example/test-server", Description: "Existing test server", - Version: "1.0.0", + Version: "1.0.0", Repository: model.Repository{ URL: "https://github.com/example/test-server-existing", Source: "github", @@ -202,7 +202,7 @@ func TestPublishEndpoint(t *testing.T) { requestBody: apiv0.ServerJSON{ Name: "com.example/test-server-mcpb", Description: "A test server with MCPB package", - Version: "1.0.0", + Version: "1.0.0", Packages: []model.Package{ { RegistryType: model.RegistryTypeMCPB, @@ -499,7 +499,7 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) { mux.ServeHTTP(rr, req) // Assertions - assert.Equal(t, tc.expectedStatus, rr.Code, + assert.Equal(t, tc.expectedStatus, rr.Code, "%s: expected status %d, got %d", tc.description, tc.expectedStatus, rr.Code) if tc.expectedStatus == http.StatusBadRequest { @@ -508,4 +508,4 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/internal/api/handlers/v0/servers.go b/internal/api/handlers/v0/servers.go index 3734f5f..9628ffe 100644 --- a/internal/api/handlers/v0/servers.go +++ b/internal/api/handlers/v0/servers.go @@ -41,6 +41,7 @@ type ServerVersionsInput struct { } // RegisterServersEndpoints registers all server-related endpoints +// //nolint:cyclop // Multiple endpoint registrations are inherently complex func RegisterServersEndpoints(api huma.API, registry service.RegistryService) { // List servers endpoint diff --git a/internal/api/handlers/v0/servers_test.go b/internal/api/handlers/v0/servers_test.go index 8dd5b3f..c281359 100644 --- a/internal/api/handlers/v0/servers_test.go +++ b/internal/api/handlers/v0/servers_test.go @@ -509,4 +509,4 @@ func TestServersEndpointEdgeCases(t *testing.T) { assert.Contains(t, []model.Status{model.StatusActive, model.StatusDeprecated, model.StatusDeleted}, server.Meta.Official.Status) } }) -} \ No newline at end of file +} diff --git a/internal/api/server.go b/internal/api/server.go index 523cfec..ef3c27f 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -23,12 +23,12 @@ func TrailingSlashMiddleware(next http.Handler) http.Handler { // Create a copy of the URL and remove the trailing slash newURL := *r.URL newURL.Path = strings.TrimSuffix(r.URL.Path, "/") - + // Use 308 Permanent Redirect to preserve the request method http.Redirect(w, r, newURL.String(), http.StatusPermanentRedirect) return } - + next.ServeHTTP(w, r) }) } diff --git a/internal/auth/blocks.go b/internal/auth/blocks.go index 1598355..c839f50 100644 --- a/internal/auth/blocks.go +++ b/internal/auth/blocks.go @@ -6,4 +6,4 @@ var BlockedNamespaces = []string{ // Add blocked namespaces here, e.g.: // "io.github.spammer", // "com.evil-domain", -} \ No newline at end of file +} diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go index 6e9d718..d8ee815 100644 --- a/internal/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -300,9 +300,9 @@ func TestJWTManager_BlockedNamespaces(t *testing.T) { originalBlocked := auth.BlockedNamespaces auth.BlockedNamespaces = []string{"io.github.spammer"} defer func() { auth.BlockedNamespaces = originalBlocked }() - + jwtManager := auth.NewJWTManager(cfg) - + claims := auth.JWTClaims{ AuthMethod: auth.MethodGitHubAT, AuthMethodSubject: "spammer", @@ -325,9 +325,9 @@ func TestJWTManager_BlockedNamespaces(t *testing.T) { originalBlocked := auth.BlockedNamespaces auth.BlockedNamespaces = []string{"io.github.spammer"} defer func() { auth.BlockedNamespaces = originalBlocked }() - + jwtManager := auth.NewJWTManager(cfg) - + claims := auth.JWTClaims{ AuthMethod: auth.MethodGitHubAT, AuthMethodSubject: "gooduser", @@ -349,9 +349,9 @@ func TestJWTManager_BlockedNamespaces(t *testing.T) { originalBlocked := auth.BlockedNamespaces auth.BlockedNamespaces = []string{"io.github.badorg"} defer func() { auth.BlockedNamespaces = originalBlocked }() - + jwtManager := auth.NewJWTManager(cfg) - + claims := auth.JWTClaims{ AuthMethod: auth.MethodGitHubAT, AuthMethodSubject: "user", @@ -378,9 +378,9 @@ func TestJWTManager_BlockedNamespaces(t *testing.T) { originalBlocked := auth.BlockedNamespaces auth.BlockedNamespaces = []string{"io.github.spammer"} defer func() { auth.BlockedNamespaces = originalBlocked }() - + jwtManager := auth.NewJWTManager(cfg) - + claims := auth.JWTClaims{ AuthMethod: auth.MethodNone, AuthMethodSubject: "admin", diff --git a/internal/auth/types.go b/internal/auth/types.go index 83cbd2f..38b7959 100644 --- a/internal/auth/types.go +++ b/internal/auth/types.go @@ -16,4 +16,4 @@ const ( MethodHTTP Method = "http" // No authentication - should only be used for local development and testing MethodNone Method = "none" -) \ No newline at end of file +) diff --git a/internal/config/config.go b/internal/config/config.go index c864c09..60e9a06 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,15 +7,15 @@ import ( // Config holds the application configuration // See .env.example for more documentation type Config struct { - ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` - DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/mcp-registry?sslmode=disable"` - SeedFrom string `env:"SEED_FROM" envDefault:""` - Version string `env:"VERSION" envDefault:"dev"` - GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""` - GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""` - JWTPrivateKey string `env:"JWT_PRIVATE_KEY" envDefault:""` - EnableAnonymousAuth bool `env:"ENABLE_ANONYMOUS_AUTH" envDefault:"false"` - EnableRegistryValidation bool `env:"ENABLE_REGISTRY_VALIDATION" envDefault:"true"` + ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` + DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/mcp-registry?sslmode=disable"` + SeedFrom string `env:"SEED_FROM" envDefault:""` + Version string `env:"VERSION" envDefault:"dev"` + GithubClientID string `env:"GITHUB_CLIENT_ID" envDefault:""` + GithubClientSecret string `env:"GITHUB_CLIENT_SECRET" envDefault:""` + JWTPrivateKey string `env:"JWT_PRIVATE_KEY" envDefault:""` + EnableAnonymousAuth bool `env:"ENABLE_ANONYMOUS_AUTH" envDefault:"false"` + EnableRegistryValidation bool `env:"ENABLE_REGISTRY_VALIDATION" envDefault:"true"` // OIDC Configuration OIDCEnabled bool `env:"OIDC_ENABLED" envDefault:"false"` diff --git a/internal/database/postgres_test.go b/internal/database/postgres_test.go index 7606b79..ee90fdc 100644 --- a/internal/database/postgres_test.go +++ b/internal/database/postgres_test.go @@ -19,11 +19,11 @@ func TestPostgreSQL_CreateServer(t *testing.T) { ctx := context.Background() tests := []struct { - name string - serverJSON *apiv0.ServerJSON + name string + serverJSON *apiv0.ServerJSON officialMeta *apiv0.RegistryExtensions - expectError bool - errorType error + expectError bool + errorType error }{ { name: "successful server creation", @@ -285,13 +285,13 @@ func TestPostgreSQL_ListServers(t *testing.T) { } tests := []struct { - name string - filter *database.ServerFilter - cursor string - limit int - expectedCount int - expectedNames []string - expectError bool + name string + filter *database.ServerFilter + cursor string + limit int + expectedCount int + expectedNames []string + expectError bool }{ { name: "list all servers", @@ -418,12 +418,12 @@ func TestPostgreSQL_UpdateServer(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - serverName string - version string - updatedServer *apiv0.ServerJSON - expectError bool - errorType error + name string + serverName string + version string + updatedServer *apiv0.ServerJSON + expectError bool + errorType error }{ { name: "successful server update", @@ -966,4 +966,4 @@ func boolPtr(b bool) *bool { func timePtr(t time.Time) *time.Time { return &t -} \ No newline at end of file +} diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 3b6509d..94d29e1 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -61,7 +61,6 @@ func (s *Service) ImportFromPath(ctx context.Context, path string) error { return nil } - // readSeedFile reads seed data from various sources func readSeedFile(ctx context.Context, path string) ([]*apiv0.ServerJSON, error) { var data []byte @@ -189,4 +188,3 @@ func fetchFromRegistryAPI(ctx context.Context, baseURL string) ([]*apiv0.ServerJ return allRecords, nil } - diff --git a/internal/service/registry_service_test.go b/internal/service/registry_service_test.go index 927cba4..6866361 100644 --- a/internal/service/registry_service_test.go +++ b/internal/service/registry_service_test.go @@ -447,14 +447,14 @@ func TestUpdateServer(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - serverName string - version string - updatedServer *apiv0.ServerJSON - newStatus *string - expectError bool - errorMsg string - checkResult func(*testing.T, *apiv0.ServerResponse) + name string + serverName string + version string + updatedServer *apiv0.ServerJSON + newStatus *string + expectError bool + errorMsg string + checkResult func(*testing.T, *apiv0.ServerResponse) }{ { name: "successful server update", @@ -766,4 +766,4 @@ func TestVersionComparison(t *testing.T) { // Helper functions func stringPtr(s string) *string { return &s -} \ No newline at end of file +} diff --git a/internal/validators/constants.go b/internal/validators/constants.go index b7279dd..661ac28 100644 --- a/internal/validators/constants.go +++ b/internal/validators/constants.go @@ -36,4 +36,4 @@ type RepositorySource string const ( SourceGitHub RepositorySource = "github" SourceGitLab RepositorySource = "gitlab" -) \ No newline at end of file +) diff --git a/internal/validators/utils.go b/internal/validators/utils.go index 52c4b00..69f2615 100644 --- a/internal/validators/utils.go +++ b/internal/validators/utils.go @@ -35,7 +35,7 @@ func HasNoSpaces(s string) bool { func extractTemplateVariables(url string) []string { re := regexp.MustCompile(`\{([^}]+)\}`) matches := re.FindAllStringSubmatch(url, -1) - + var variables []string for _, match := range matches { if len(match) > 1 { @@ -55,16 +55,16 @@ func replaceTemplateVariables(rawURL string) string { "{protocol}": "http", "{scheme}": "http", } - + result := rawURL for placeholder, replacement := range templateReplacements { result = strings.ReplaceAll(result, placeholder, replacement) } - + // Handle any remaining {variable} patterns with generic placeholder re := regexp.MustCompile(`\{[^}]+\}`) result = re.ReplaceAllString(result, "placeholder") - + return result } @@ -72,7 +72,7 @@ func replaceTemplateVariables(rawURL string) string { func IsValidURL(rawURL string) bool { // Replace template variables with placeholders for parsing testURL := replaceTemplateVariables(rawURL) - + // Parse the URL u, err := url.Parse(testURL) if err != nil { @@ -131,19 +131,19 @@ func IsValidRemoteURL(rawURL string) bool { if !IsValidURL(rawURL) { return false } - + // Parse the URL to check for localhost restriction u, err := url.Parse(rawURL) if err != nil { return false } - + // Reject localhost URLs for remotes (security/production concerns) hostname := u.Hostname() if hostname == "localhost" || hostname == "127.0.0.1" || strings.HasSuffix(hostname, ".localhost") { return false } - + return true } @@ -155,31 +155,31 @@ func IsValidTemplatedURL(rawURL string, availableVariables []string, allowTempla if !IsValidURL(rawURL) { return false } - + // Extract template variables from URL templateVars := extractTemplateVariables(rawURL) - + // If no templates are found, it's a valid static URL if len(templateVars) == 0 { return true } - + // If templates are not allowed (e.g., for remotes), reject URLs with templates if !allowTemplates { return false } - + // Validate that all template variables are available availableSet := make(map[string]bool) for _, v := range availableVariables { availableSet[v] = true } - + for _, templateVar := range templateVars { if !availableSet[templateVar] { return false } } - + return true } diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 0c9c319..d21a421 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -505,4 +505,4 @@ func isValidHostForDomain(hostname, publisherDomain string) bool { } return false -} \ No newline at end of file +} diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index 41101f1..0bdc508 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -893,10 +893,10 @@ func TestValidate_ServerNameFormat(t *testing.T) { func TestValidate_MultipleSlashesInServerName(t *testing.T) { tests := []struct { - name string - serverName string - expectError bool - errorMsg string + name string + serverName string + expectError bool + errorMsg string }{ { name: "single slash - valid", @@ -1592,4 +1592,4 @@ func createValidServerWithArgument(arg model.Argument) apiv0.ServerJSON { }, }, } -} \ No newline at end of file +} diff --git a/pkg/model/constants.go b/pkg/model/constants.go index a1b2c25..f21f172 100644 --- a/pkg/model/constants.go +++ b/pkg/model/constants.go @@ -33,4 +33,4 @@ const ( RuntimeHintUVX = "uvx" RuntimeHintDocker = "docker" RuntimeHintDNX = "dnx" -) \ No newline at end of file +)