fix(actions): validate secret name and value on create

`runSecretsCreate` previously accepted any string as a secret name and
value, forwarding invalid input directly to the Gitea API. The API
either rejected with a generic error or accepted inputs that don't match
Gitea's documented constraints, producing confusing UX.

Add `validateSecretName` enforcing the Gitea Actions naming rules:
uppercase letters, numbers, and underscores only; cannot start with a
number; cannot use the reserved `GITHUB_` / `GITEA_` prefixes.

Add `validateSecretValue` enforcing a non-empty value with a 64KB
upper bound (matches the Gitea API limit).

Mirror the structure of the existing variable validation in
`cmd/actions/variables/set.go` for consistency.
This commit is contained in:
Ross Golder 2026-07-30 07:44:13 +07:00
parent 58931b5d17
commit 50bcc1a49d
No known key found for this signature in database
GPG key ID: 253A7E508D2D59CD
2 changed files with 171 additions and 0 deletions

View file

@ -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
}

View file

@ -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