diff --git a/cmd/actions/secrets/create.go b/cmd/actions/secrets/create.go index 1f070844..37211742 100644 --- a/cmd/actions/secrets/create.go +++ b/cmd/actions/secrets/create.go @@ -6,6 +6,8 @@ package secrets import ( stdctx "context" "fmt" + "regexp" + "strings" gitea "gitea.dev/sdk" @@ -50,6 +52,9 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error { client := c.Login.Client() secretName := cmd.Args().First() + if err := validateSecretName(secretName); err != nil { + return err + } // Read secret value using the utility secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{ @@ -62,6 +67,10 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error { return err } + if err := validateSecretValue(secretValue); err != nil { + return err + } + _, err = client.Actions.CreateRepoSecret(ctx, c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{ Data: secretValue, }) @@ -72,3 +81,41 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error { fmt.Printf("Secret '%s' created successfully\n", secretName) return nil } + +// validateSecretName validates that a secret name follows the required format. +func validateSecretName(name string) error { + if name == "" { + return fmt.Errorf("secret name cannot be empty") + } + + // Secret names must be uppercase letters, numbers, and underscores only. + // Cannot start with a number. + validPattern := regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`) + if !validPattern.MatchString(name) { + return fmt.Errorf("secret name must contain only uppercase letters, numbers, and underscores, and cannot start with a number") + } + + // Reserved prefixes used by Gitea/GitHub internals. + reservedPrefixes := []string{"GITHUB_", "GITEA_"} + for _, prefix := range reservedPrefixes { + if strings.HasPrefix(name, prefix) { + return fmt.Errorf("secret name cannot start with reserved prefix: %s", prefix) + } + } + + return nil +} + +// validateSecretValue validates that a secret value is acceptable. +func validateSecretValue(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("secret value cannot be empty or whitespace only") + } + + // 64KB upper limit per Gitea API. + if len(value) > 65536 { + return fmt.Errorf("secret value cannot exceed 64KB") + } + + return nil +} diff --git a/cmd/actions/secrets/create_test.go b/cmd/actions/secrets/create_test.go index 5ac0869c..65ffb15a 100644 --- a/cmd/actions/secrets/create_test.go +++ b/cmd/actions/secrets/create_test.go @@ -4,9 +4,133 @@ package secrets import ( + "strings" "testing" ) +func TestValidateSecretName(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "valid name", + input: "VALID_SECRET_NAME", + wantErr: false, + }, + { + name: "valid name with numbers", + input: "SECRET_123", + wantErr: false, + }, + { + name: "valid name starting with underscore", + input: "_SECRET", + wantErr: false, + }, + { + name: "invalid - lowercase", + input: "invalid_secret", + wantErr: true, + }, + { + name: "invalid - mixed case", + input: "Invalid_Secret", + wantErr: true, + }, + { + name: "invalid - spaces", + input: "INVALID SECRET", + wantErr: true, + }, + { + name: "invalid - special chars", + input: "INVALID-SECRET!", + wantErr: true, + }, + { + name: "invalid - starts with number", + input: "1SECRET", + wantErr: true, + }, + { + name: "invalid - empty", + input: "", + wantErr: true, + }, + { + name: "invalid - reserved GITHUB_ prefix", + input: "GITHUB_TOKEN", + wantErr: true, + }, + { + name: "invalid - reserved GITEA_ prefix", + input: "GITEA_SECRET", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSecretName(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("validateSecretName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + }) + } +} + +func TestValidateSecretValue(t *testing.T) { + largeValue := strings.Repeat("a", 65537) + + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "valid value", + input: "secret_value", + wantErr: false, + }, + { + name: "valid value with whitespace", + input: "secret value with spaces", + wantErr: false, + }, + { + name: "invalid - empty", + input: "", + wantErr: true, + }, + { + name: "invalid - whitespace only", + input: " \t\n ", + wantErr: true, + }, + { + name: "invalid - exceeds 64KB", + input: largeValue, + wantErr: true, + }, + { + name: "valid - exactly 64KB", + input: strings.Repeat("a", 65536), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSecretValue(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("validateSecretValue(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + }) + } +} + func TestGetSecretSourceArgs(t *testing.T) { tests := []struct { name string