Repository: go-gitea/gitea
Pull request #33526: Added Description Field for Secrets and Variables 
Author: login: LaoQi
Labels: name: lgtm/done
description: This PR has enough approvals to get merged. There are no important open reservations anymore.
Created: 2025-02-06T17:59:51Z | Merged: 2025-03-17T19:24:54Z

Fixes #33484 


Linked issue #33484: Add Description Field for Secrets and Variables
### Feature Description

Currently, Gitea does not provide a field for adding descriptions to secrets and variables in the repository settings. This makes it difficult for users to understand the purpose of a specific secret or variable, especially in larger projects with multiple contributors.
Thank you for considering this feature request!

### Screenshots

_No response_
Linked issue comments: user: login: LaoQi
created_at: 2025-02-06T03:08:09Z
body: I've added a basic description feature. For secrets, it works by replacing the ***** section and seems okay so far. But for variables, replacing the original values seems not well. Any other ideas?

![Image](https://github.com/user-attachments/assets/3a6f4d39-222a-44b5-8d74-4b5fdfb9f108)

![Image](https://github.com/user-attachments/assets/aa81d727-5e3d-4efa-9550-8e88938ee63b)
author_association: CONTRIBUTOR
user: login: lunny
created_at: 2025-02-06T03:33:29Z
body: Maybe always put 3 lines, secrets could put `****` and variables put `<value>`.
author_association: MEMBER
user: login: John-jk
created_at: 2025-02-06T06:26:06Z
body: How about this? If there is a description, the icon appears, if not, there is no icon. There will be fewer extra lines and they will all have the same height.

![Image](https://github.com/user-attachments/assets/2710aabb-8b05-4793-8fc4-f8b5b4794e4e)
author_association: NONE
user: login: LaoQi
created_at: 2025-02-06T11:13:58Z
body: I will try a 3 line layout first, using tips doesn't feel intuitive enough.
author_association: CONTRIBUTOR

Diff:
diff --git a/models/actions/variable.go b/models/actions/variable.go
index 1929cffbd8623..7154843c170e7 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -6,10 +6,12 @@ package actions
 import (
 	"context"
 	"strings"
+	"unicode/utf8"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 
 	"xorm.io/builder"
 )
@@ -32,26 +34,39 @@ type ActionVariable struct {
 	RepoID      int64              `xorm:"INDEX UNIQUE(owner_repo_name)"`
 	Name        string             `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
 	Data        string             `xorm:"LONGTEXT NOT NULL"`
+	Description string             `xorm:"TEXT"`
 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
 	UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
 }
 
+const (
+	VariableDataMaxLength        = 65536
+	VariableDescriptionMaxLength = 4096
+)
+
 func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
+	if utf8.RuneCountInString(data) > VariableDataMaxLength {
+		return nil, util.NewInvalidArgumentErrorf("data too long")
+	}
+
+	description = util.TruncateRunes(description, VariableDescriptionMaxLength)
+
 	variable := &ActionVariable{
-		OwnerID: ownerID,
-		RepoID:  repoID,
-		Name:    strings.ToUpper(name),
-		Data:    data,
+		OwnerID:     ownerID,
+		RepoID:      repoID,
+		Name:        strings.ToUpper(name),
+		Data:        data,
+		Description: description,
 	}
 	return variable, db.Insert(ctx, variable)
 }
@@ -96,6 +111,12 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
 }
 
 func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
+	if utf8.RuneCountInString(variable.Data) > VariableDataMaxLength {
+		return false, util.NewInvalidArgumentErrorf("data too long")
+	}
+
+	variable.Description = util.TruncateRunes(variable.Description, VariableDescriptionMaxLength)
+
 	variable.Name = strings.ToUpper(variable.Name)
 	count, err := db.GetEngine(ctx).
 		ID(variable.ID).
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index f9ab993abfbfc..297c50a267f2d 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -376,6 +376,7 @@ func prepareMigrationTasks() []*migration {
 		newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
 		newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables),
 		newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
+		newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
 	}
 	return preparedMigrations
 }
diff --git a/models/migrations/v1_24/v316.go b/models/migrations/v1_24/v316.go
new file mode 100644
index 0000000000000..0378133e53c4b
--- /dev/null
+++ b/models/migrations/v1_24/v316.go
@@ -0,0 +1,20 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_24 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+func AddDescriptionForSecretsAndVariables(x *xorm.Engine) error {
+	type Secret struct {
+		Description string `xorm:"TEXT"`
+	}
+
+	type ActionVariable struct {
+		Description string `xorm:"TEXT"`
+	}
+
+	return x.Sync(new(Secret), new(ActionVariable))
+}
diff --git a/models/secret/secret.go b/models/secret/secret.go
index eab9cf0712d83..10a0287dfd961 100644
--- a/models/secret/secret.go
+++ b/models/secret/secret.go
@@ -40,9 +40,15 @@ type Secret struct {
 	RepoID      int64              `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL DEFAULT 0"`
 	Name        string             `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
 	Data        string             `xorm:"LONGTEXT"` // encrypted data
+	Description string             `xorm:"TEXT"`
 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
 }
 
+const (
+	SecretDataMaxLength        = 65536
+	SecretDescriptionMaxLength = 4096
+)
+
 // ErrSecretNotFound represents a "secret not found" error.
 type ErrSecretNotFound struct {
 	Name string
@@ -57,7 +63,7 @@ func (err ErrSecretNotFound) Unwrap() error {
 }
 
 // InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database
-func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) {
+func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data, description string) (*Secret, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
@@ -67,15 +73,23 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
 		return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
 	}
 
+	if len(data) > SecretDataMaxLength {
+		return nil, util.NewInvalidArgumentErrorf("data too long")
+	}
+
+	description = util.TruncateRunes(description, SecretDescriptionMaxLength)
+
 	encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
 	if err != nil {
 		return nil, err
 	}
+
 	secret := &Secret{
-		OwnerID: ownerID,
-		RepoID:  repoID,
-		Name:    strings.ToUpper(name),
-		Data:    encrypted,
+		OwnerID:     ownerID,
+		RepoID:      repoID,
+		Name:        strings.ToUpper(name),
+		Data:        encrypted,
+		Description: description,
 	}
 	return secret, db.Insert(ctx, secret)
 }
@@ -114,16 +128,23 @@ func (opts FindSecretsOptions) ToConds() builder.Cond {
 }
 
 // UpdateSecret changes org or user reop secret.
-func UpdateSecret(ctx context.Context, secretID int64, data string) error {
+func UpdateSecret(ctx context.Context, secretID int64, data, description string) error {
+	if len(data) > SecretDataMaxLength {
+		return util.NewInvalidArgumentErrorf("data too long")
+	}
+
+	description = util.TruncateRunes(description, SecretDescriptionMaxLength)
+
 	encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
 	if err != nil {
 		return err
 	}
 
 	s := &Secret{
-		Data: encrypted,
+		Data:        encrypted,
+		Description: description,
 	}
-	affected, err := db.GetEngine(ctx).ID(secretID).Cols("data").Update(s)
+	affected, err := db.GetEngine(ctx).ID(secretID).Cols("data", "description").Update(s)
 	if affected != 1 {
 		return ErrSecretNotFound{}
 	}
diff --git a/modules/structs/secret.go b/modules/structs/secret.go
index a0673ca08c9ee..2afb41ec431db 100644
--- a/modules/structs/secret.go
+++ b/modules/structs/secret.go
@@ -10,6 +10,8 @@ import "time"
 type Secret struct {
 	// the secret's name
 	Name string `json:"name"`
+	// the secret's description
+	Description string `json:"description"`
 	// swagger:strfmt date-time
 	Created time.Time `json:"created_at"`
 }
@@ -21,4 +23,9 @@ type CreateOrUpdateSecretOption struct {
 	//
 	// required: true
 	Data string `json:"data" binding:"Required"`
+
+	// Description of the secret to update
+	//
+	// required: false
+	Description string `json:"description"`
 }
diff --git a/modules/structs/variable.go b/modules/structs/variable.go
index cc846cf0ec309..51989373033bd 100644
--- a/modules/structs/variable.go
+++ b/modules/structs/variable.go
@@ -10,6 +10,11 @@ type CreateVariableOption struct {
 	//
 	// required: true
 	Value string `json:"value" binding:"Required"`
+
+	// Description of the variable to create
+	//
+	// required: false
+	Description string `json:"description"`
 }
 
 // UpdateVariableOption the option when updating variable
@@ -21,6 +26,11 @@ type UpdateVariableOption struct {
 	//
 	// required: true
 	Value string `json:"value" binding:"Required"`
+
+	// Description of the variable to update
+	//
+	// required: false
+	Description string `json:"description"`
 }
 
 // ActionVariable return value of the query API
@@ -34,4 +44,6 @@ type ActionVariable struct {
 	Name string `json:"name"`
 	// the value of the variable
 	Data string `json:"data"`
+	// the description of the variable
+	Description string `json:"description"`
 }
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 3ee7c8818c29e..876e135b22f57 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3712,8 +3712,10 @@ secrets = Secrets
 description = Secrets will be passed to certain actions and cannot be read otherwise.
 none = There are no secrets yet.
 creation = Add Secret
+creation.description = Description
 creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_
 creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted.
+creation.description_placeholder = Enter short description (optional).
 creation.success = The secret "%s" has been added.
 creation.failed = Failed to add secret.
 deletion = Remove secret
diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go
index d9bdb3ab48c26..b1cd2f0c3c21c 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -61,8 +61,9 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
 	apiSecrets := make([]*api.Secret, len(secrets))
 	for k, v := range secrets {
 		apiSecrets[k] = &api.Secret{
-			Name:    v.Name,
-			Created: v.CreatedUnix.AsTime(),
+			Name:        v.Name,
+			Description: v.Description,
+			Created:     v.CreatedUnix.AsTime(),
 		}
 	}
 
@@ -106,7 +107,7 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
 	if err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
@@ -230,10 +231,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
 	variables := make([]*api.ActionVariable, len(vars))
 	for i, v := range vars {
 		variables[i] = &api.ActionVariable{
-			OwnerID: v.OwnerID,
-			RepoID:  v.RepoID,
-			Name:    v.Name,
-			Data:    v.Data,
+			OwnerID:     v.OwnerID,
+			RepoID:      v.RepoID,
+			Name:        v.Name,
+			Data:        v.Data,
+			Description: v.Description,
 		}
 	}
 
@@ -281,10 +283,11 @@ func (Action) GetVariable(ctx *context.APIContext) {
 	}
 
 	variable := &api.ActionVariable{
-		OwnerID: v.OwnerID,
-		RepoID:  v.RepoID,
-		Name:    v.Name,
-		Data:    v.Data,
+		OwnerID:     v.OwnerID,
+		RepoID:      v.RepoID,
+		Name:        v.Name,
+		Data:        v.Data,
+		Description: v.Description,
 	}
 
 	ctx.JSON(http.StatusOK, variable)
@@ -386,7 +389,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
 		return
 	}
 
-	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
 		} else {
@@ -453,6 +456,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
 
 	v.Name = opt.Name
 	v.Data = opt.Value
+	v.Description = opt.Description
 
 	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 6b4ce37fcf4a2..2ace9fa2959ee 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -84,8 +84,9 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
 	apiSecrets := make([]*api.Secret, len(secrets))
 	for k, v := range secrets {
 		apiSecrets[k] = &api.Secret{
-			Name:    v.Name,
-			Created: v.CreatedUnix.AsTime(),
+			Name:        v.Name,
+			Description: v.Description,
+			Created:     v.CreatedUnix.AsTime(),
 		}
 	}
 
@@ -136,7 +137,7 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
 	if err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
@@ -249,10 +250,11 @@ func (Action) GetVariable(ctx *context.APIContext) {
 	}
 
 	variable := &api.ActionVariable{
-		OwnerID: v.OwnerID,
-		RepoID:  v.RepoID,
-		Name:    v.Name,
-		Data:    v.Data,
+		OwnerID:     v.OwnerID,
+		RepoID:      v.RepoID,
+		Name:        v.Name,
+		Data:        v.Data,
+		Description: v.Description,
 	}
 
 	ctx.JSON(http.StatusOK, variable)
@@ -362,7 +364,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
 		return
 	}
 
-	if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
+	if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
 		} else {
@@ -432,6 +434,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
 
 	v.Name = opt.Name
 	v.Data = opt.Value
+	v.Description = opt.Description
 
 	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
@@ -491,9 +494,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
 	variables := make([]*api.ActionVariable, len(vars))
 	for i, v := range vars {
 		variables[i] = &api.ActionVariable{
-			OwnerID: v.OwnerID,
-			RepoID:  v.RepoID,
-			Name:    v.Name,
+			OwnerID:     v.OwnerID,
+			RepoID:      v.RepoID,
+			Name:        v.Name,
+			Data:        v.Data,
+			Description: v.Description,
 		}
 	}
 
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index 1c8e3b6e71fa1..04097fcc95b3f 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -49,7 +49,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
 	if err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
@@ -153,7 +153,7 @@ func CreateVariable(ctx *context.APIContext) {
 		return
 	}
 
-	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+	if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.APIError(http.StatusBadRequest, err)
 		} else {
@@ -215,6 +215,7 @@ func UpdateVariable(ctx *context.APIContext) {
 
 	v.Name = opt.Name
 	v.Data = opt.Value
+	v.Description = opt.Description
 
 	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
@@ -300,10 +301,11 @@ func GetVariable(ctx *context.APIContext) {
 	}
 
 	variable := &api.ActionVariable{
-		OwnerID: v.OwnerID,
-		RepoID:  v.RepoID,
-		Name:    v.Name,
-		Data:    v.Data,
+		OwnerID:     v.OwnerID,
+		RepoID:      v.RepoID,
+		Name:        v.Name,
+		Data:        v.Data,
+		Description: v.Description,
 	}
 
 	ctx.JSON(http.StatusOK, variable)
@@ -345,10 +347,11 @@ func ListVariables(ctx *context.APIContext) {
 	variables := make([]*api.ActionVariable, len(vars))
 	for i, v := range vars {
 		variables[i] = &api.ActionVariable{
-			OwnerID: v.OwnerID,
-			RepoID:  v.RepoID,
-			Name:    v.Name,
-			Data:    v.Data,
+			OwnerID:     v.OwnerID,
+			RepoID:      v.RepoID,
+			Name:        v.Name,
+			Data:        v.Data,
+			Description: v.Description,
 		}
 	}
 
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 7e8100b9485c0..9cc1676d7b418 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -106,7 +106,8 @@ func Variables(ctx *context.Context) {
 		return
 	}
 	ctx.Data["Variables"] = variables
-
+	ctx.Data["DataMaxLength"] = actions_model.VariableDataMaxLength
+	ctx.Data["DescriptionMaxLength"] = actions_model.VariableDescriptionMaxLength
 	ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
 }
 
@@ -124,7 +125,7 @@ func VariableCreate(ctx *context.Context) {
 
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data)
+	v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data, form.Description)
 	if err != nil {
 		log.Error("CreateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
@@ -157,6 +158,7 @@ func VariableUpdate(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 	variable.Name = form.Name
 	variable.Data = form.Data
+	variable.Description = form.Description
 
 	if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
 		log.Error("UpdateVariable: %v", err)
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index 3bd421f86ab5c..c8b80ebb2638c 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -22,12 +22,14 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
 	}
 
 	ctx.Data["Secrets"] = secrets
+	ctx.Data["DataMaxLength"] = secret_model.SecretDataMaxLength
+	ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
 }
 
 func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.AddSecretForm)
 
-	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
+	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description)
 	if err != nil {
 		log.Error("CreateOrUpdateSecret failed: %v", err)
 		ctx.JSONError(ctx.Tr("secrets.creation.failed"))
diff --git a/services/actions/variables.go b/services/actions/variables.go
index 95f088dbd334e..2603f1d461a48 100644
--- a/services/actions/variables.go
+++ b/services/actions/variables.go
@@ -13,7 +13,7 @@ import (
 	secret_service "code.gitea.io/gitea/services/secrets"
 )
 
-func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
+func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*actions_model.ActionVariable, error) {
 	if err := secret_service.ValidateName(name); err != nil {
 		return nil, err
 	}
@@ -22,7 +22,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
 		return nil, err
 	}
 
-	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
+	v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description)
 	if err != nil {
 		return nil, err
 	}
@@ -41,7 +41,7 @@ func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionV
 
 	variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
 
-	return actions_model.UpdateVariableCols(ctx, variable, "name", "data")
+	return actions_model.UpdateVariableCols(ctx, variable, "name", "data", "description")
 }
 
 func DeleteVariableByID(ctx context.Context, variableID int64) error {
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index c9ce71e8861c2..ddf2bd09b0bb6 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -323,8 +323,9 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er
 
 // AddSecretForm for adding secrets
 type AddSecretForm struct {
-	Name string `binding:"Required;MaxSize(255)"`
-	Data string `binding:"Required;MaxSize(65535)"`
+	Name        string `binding:"Required;MaxSize(255)"`
+	Data        string `binding:"Required;MaxSize(65535)"`
+	Description string `binding:"MaxSize(65535)"`
 }
 
 // Validate validates the fields
@@ -334,8 +335,9 @@ func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding
 }
 
 type EditVariableForm struct {
-	Name string `binding:"Required;MaxSize(255)"`
-	Data string `binding:"Required;MaxSize(65535)"`
+	Name        string `binding:"Required;MaxSize(255)"`
+	Data        string `binding:"Required;MaxSize(65535)"`
+	Description string `binding:"MaxSize(65535)"`
 }
 
 func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
diff --git a/services/secrets/secrets.go b/services/secrets/secrets.go
index 031c474dd7265..ec6a3cb0623a9 100644
--- a/services/secrets/secrets.go
+++ b/services/secrets/secrets.go
@@ -10,7 +10,7 @@ import (
 	secret_model "code.gitea.io/gitea/models/secret"
 )
 
-func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, bool, error) {
+func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data, description string) (*secret_model.Secret, bool, error) {
 	if err := ValidateName(name); err != nil {
 		return nil, false, err
 	}
@@ -25,14 +25,14 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data
 	}
 
 	if len(s) == 0 {
-		s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data)
+		s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data, description)
 		if err != nil {
 			return nil, false, err
 		}
 		return s, true, nil
 	}
 
-	if err := secret_model.UpdateSecret(ctx, s[0].ID, data); err != nil {
+	if err := secret_model.UpdateSecret(ctx, s[0].ID, data, description); err != nil {
 		return nil, false, err
 	}
 
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index 59596d1013282..977f308b71ea3 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -22,6 +22,9 @@
 				<div class="flex-item-title">
 					{{.Name}}
 				</div>
+				<div class="flex-item-body">
+					{{if .Description}}{{.Description}}{{else}}-{{end}}
+				</div>
 				<div class="flex-item-body">
 					******
 				</div>
@@ -72,9 +75,20 @@
 				<textarea required
 					id="secret-data"
 					name="data"
+					maxlength="{{.DataMaxLength}}"
 					placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
 				></textarea>
 			</div>
+			<div class="field">
+				<label for="secret-description">{{ctx.Locale.Tr "secrets.creation.description"}}</label>
+				<textarea
+					id="secret-description"
+					name="description"
+					rows="2"
+					maxlength="{{.DescriptionMaxLength}}"
+					placeholder="{{ctx.Locale.Tr "secrets.creation.description_placeholder"}}"
+				></textarea>
+			</div>
 		</div>
 		{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 	</form>
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index 7a0ab48cefaa0..2edca431c1497 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -7,6 +7,7 @@
 			data-modal-header="{{ctx.Locale.Tr "actions.variables.creation"}}"
 			data-modal-dialog-variable-name=""
 			data-modal-dialog-variable-data=""
+			data-modal-dialog-variable-description=""
 		>
 			{{ctx.Locale.Tr "actions.variables.creation"}}
 		</button>
@@ -24,6 +25,9 @@
 				<div class="flex-item-title">
 					{{.Name}}
 				</div>
+				<div class="flex-item-body">
+					{{if .Description}}{{.Description}}{{else}}-{{end}}
+				</div>
 				<div class="flex-item-body">
 					{{.Data}}
 				</div>
@@ -39,6 +43,7 @@
 					data-modal-header="{{ctx.Locale.Tr "actions.variables.edit"}}"
 					data-modal-dialog-variable-name="{{.Name}}"
 					data-modal-dialog-variable-data="{{.Data}}"
+					data-modal-dialog-variable-description="{{.Description}}"
 				>
 					{{svg "octicon-pencil"}}
 				</button>
@@ -82,9 +87,20 @@
 				<textarea required
 					name="data"
 					id="dialog-variable-data"
+					maxlength="{{.DataMaxLength}}"
 					placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
 				></textarea>
 			</div>
+			<div class="field">
+				<label for="dialog-variable-description">{{ctx.Locale.Tr "secrets.creation.description"}}</label>
+				<textarea
+					name="description"
+					id="dialog-variable-description"
+					rows="2"
+					maxlength="{{.DescriptionMaxLength}}"
+					placeholder="{{ctx.Locale.Tr "secrets.creation.description_placeholder"}}"
+				></textarea>
+			</div>
 		</div>
 		{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
 	</form>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 48ed958ca2fa7..1efaf1a875ab4 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -19325,6 +19325,11 @@
           "type": "string",
           "x-go-name": "Data"
         },
+        "description": {
+          "description": "the description of the variable",
+          "type": "string",
+          "x-go-name": "Description"
+        },
         "name": {
           "description": "the name of the variable",
           "type": "string",
@@ -20988,6 +20993,11 @@
           "description": "Data of the secret to update",
           "type": "string",
           "x-go-name": "Data"
+        },
+        "description": {
+          "description": "Description of the secret to update",
+          "type": "string",
+          "x-go-name": "Description"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -21498,6 +21508,11 @@
         "value"
       ],
       "properties": {
+        "description": {
+          "description": "Description of the variable to create",
+          "type": "string",
+          "x-go-name": "Description"
+        },
         "value": {
           "description": "Value of the variable to create",
           "type": "string",
@@ -25459,6 +25474,11 @@
           "format": "date-time",
           "x-go-name": "Created"
         },
+        "description": {
+          "description": "the secret's description",
+          "type": "string",
+          "x-go-name": "Description"
+        },
         "name": {
           "description": "the secret's name",
           "type": "string",
@@ -26034,6 +26054,11 @@
         "value"
       ],
       "properties": {
+        "description": {
+          "description": "Description of the variable to update",
+          "type": "string",
+          "x-go-name": "Description"
+        },
         "name": {
           "description": "New name for the variable. If the field is empty, the variable name won't be updated.",
           "type": "string",
diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go
index 1fd3294109574..c0d0bd371b5dc 100644
--- a/tests/integration/actions_variables_test.go
+++ b/tests/integration/actions_variables_test.go
@@ -27,21 +27,21 @@ func TestActionsVariables(t *testing.T) {
 	require.NoError(t, db.DeleteAllRecords("action_variable"))
 
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-	_, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var")
+	_, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var", "user2-var-description")
 	user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
 	userWebURL := "/user/settings/actions/variables"
 
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
-	_, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var")
+	_, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var", "org3-var-description")
 	org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
 	orgWebURL := "/org/org3/settings/actions/variables"
 
 	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
-	_, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var")
+	_, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var", "repo1-var-description")
 	repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
 	repoWebURL := "/user2/repo1/settings/actions/variables"
 
-	_, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var")
+	_, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var", "global-var-description")
 	globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
 	adminWebURL := "/-/admin/actions/variables"
 
diff --git a/tests/integration/api_repo_secrets_test.go b/tests/integration/api_repo_secrets_test.go
index c3074d9eceedd..2059aff484844 100644
--- a/tests/integration/api_repo_secrets_test.go
+++ b/tests/integration/api_repo_secrets_test.go
@@ -73,6 +73,33 @@ func TestAPIRepoSecrets(t *testing.T) {
 		}
 	})
 
+	t.Run("CreateWithDescription", func(t *testing.T) {
+		cases := []struct {
+			Name           string
+			Description    string
+			ExpectedStatus int
+		}{
+			{
+				Name:           "no_description",
+				Description:    "",
+				ExpectedStatus: http.StatusCreated,
+			},
+			{
+				Name:           "description",
+				Description:    "some description",
+				ExpectedStatus: http.StatusCreated,
+			},
+		}
+
+		for _, c := range cases {
+			req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), c.Name), api.CreateOrUpdateSecretOption{
+				Data:        "data",
+				Description: c.Description,
+			}).AddTokenAuth(token)
+			MakeRequest(t, req, c.ExpectedStatus)
+		}
+	})
+
 	t.Run("Update", func(t *testing.T) {
 		name := "update_secret"
 		url := fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), name)


Reviews:
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-07T03:28:04Z
user: login: LaoQi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T14:30:07Z
user: login: lafriks
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-07T18:33:16Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:19:14Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:20:10Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:20:48Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:21:02Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:22:13Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T19:22:33Z
user: login: LaoQi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T04:16:51Z
user: login: LaoQi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T04:20:08Z
user: login: silverwind
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T00:39:23Z
user: login: silverwind
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T00:41:05Z
user: login: lunny
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-16T20:29:19Z
user: login: hiifong
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T15:20:33Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:42:00Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:43:32Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:44:09Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:48:09Z

Review comments:
diff_hunk: @@ -40,18 +41,19 @@ func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
 	variable := &ActionVariable{
-		OwnerID: ownerID,
-		RepoID:  repoID,
-		Name:    strings.ToUpper(name),
-		Data:    data,
+		OwnerID:     ownerID,
+		RepoID:      repoID,
+		Name:        strings.ToUpper(name),
+		Data:        data,
+		Description: description,
path: models/actions/variable.go
user: login: lunny
body: It maybe very long, the length should be checked.
created_at: 2025-02-07T03:28:04Z
author_association: MEMBER
diff_hunk: @@ -40,18 +41,19 @@ func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
 	variable := &ActionVariable{
-		OwnerID: ownerID,
-		RepoID:  repoID,
-		Name:    strings.ToUpper(name),
-		Data:    data,
+		OwnerID:     ownerID,
+		RepoID:      repoID,
+		Name:        strings.ToUpper(name),
+		Data:        data,
+		Description: description,
path: models/actions/variable.go
user: login: LaoQi
body: The field I've designed uses the TEXT data type in the database, with a current length limit of 65536 characters. If truncation is needed here, what would be an appropriate length to truncate it to?
created_at: 2025-02-07T14:30:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +81,15 @@
 					placeholder="{{ctx.Locale.Tr "secrets.creation.name_placeholder"}}"
 				>
 			</div>
+			<div class="field">
+				<label for="dialog-variable-description">{{ctx.Locale.Tr "secrets.creation.description"}}</label>
+				<input
path: templates/shared/variables/variable_list.tmpl
user: login: lafriks
body: Should probably also add max length to this field
created_at: 2025-02-07T18:33:11Z
author_association: MEMBER
diff_hunk: @@ -32,26 +33,41 @@ type ActionVariable struct {
 	RepoID      int64              `xorm:"INDEX UNIQUE(owner_repo_name)"`
 	Name        string             `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
 	Data        string             `xorm:"LONGTEXT NOT NULL"`
+	Description string             `xorm:"TEXT"`
 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
 	UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
 }
 
+const (
+	VariableDataMaxLength        = 65536
+	VariableDescriptionMaxLength = 4096
+)
+
 func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
+	if utf8.RuneCountInString(data) > VariableDataMaxLength {
path: models/actions/variable.go
user: login: lunny
body: I think we should return failure here because data should not be truncated.
created_at: 2025-02-10T19:19:14Z
author_association: MEMBER
diff_hunk: @@ -32,26 +33,41 @@ type ActionVariable struct {
 	RepoID      int64              `xorm:"INDEX UNIQUE(owner_repo_name)"`
 	Name        string             `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
 	Data        string             `xorm:"LONGTEXT NOT NULL"`
+	Description string             `xorm:"TEXT"`
 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
 	UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
 }
 
+const (
+	VariableDataMaxLength        = 65536
+	VariableDescriptionMaxLength = 4096
+)
+
 func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
+	if utf8.RuneCountInString(data) > VariableDataMaxLength {
+		data = string([]rune(data)[:VariableDataMaxLength])
+	}
+
+	if utf8.RuneCountInString(description) > VariableDescriptionMaxLength {
path: models/actions/variable.go
user: login: lunny
body: Use `util.SplitStringAtByteN` directly.
created_at: 2025-02-10T19:20:10Z
author_association: MEMBER
diff_hunk: @@ -67,15 +74,25 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
 		return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
 	}
 
+	if len(data) > SecretDataMaxLength {
path: models/secret/secret.go
user: login: lunny
body: I think we should return failure here because data should not be truncated.
created_at: 2025-02-10T19:20:48Z
author_association: MEMBER
diff_hunk: @@ -67,15 +74,25 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
 		return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
 	}
 
+	if len(data) > SecretDataMaxLength {
+		data = data[:SecretDataMaxLength]
+	}
+
+	if utf8.RuneCountInString(description) > SecretDescriptionMaxLength {
path: models/secret/secret.go
user: login: lunny
body: Use `util.SplitStringAtByteN` directly.
created_at: 2025-02-10T19:21:02Z
author_association: MEMBER
diff_hunk: @@ -62,7 +65,6 @@
 				<input autofocus required
 					id="secret-name"
 					name="name"
-					value="{{.name}}"
path: templates/shared/secrets/add_list.tmpl
user: login: lunny
body: Why remove this?
created_at: 2025-02-10T19:22:13Z
author_association: MEMBER
diff_hunk: @@ -72,7 +77,6 @@
 				<input autofocus required
 					name="name"
 					id="dialog-variable-name"
-					value="{{.name}}"
path: templates/shared/variables/variable_list.tmpl
user: login: lunny
body: As above
created_at: 2025-02-10T19:22:33Z
author_association: MEMBER
diff_hunk: @@ -32,26 +33,41 @@ type ActionVariable struct {
 	RepoID      int64              `xorm:"INDEX UNIQUE(owner_repo_name)"`
 	Name        string             `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
 	Data        string             `xorm:"LONGTEXT NOT NULL"`
+	Description string             `xorm:"TEXT"`
 	CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
 	UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
 }
 
+const (
+	VariableDataMaxLength        = 65536
+	VariableDescriptionMaxLength = 4096
+)
+
 func init() {
 	db.RegisterModel(new(ActionVariable))
 }
 
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
+	if utf8.RuneCountInString(data) > VariableDataMaxLength {
path: models/actions/variable.go
user: login: LaoQi
body: yes, you are right
created_at: 2025-02-11T04:16:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,7 +65,6 @@
 				<input autofocus required
 					id="secret-name"
 					name="name"
-					value="{{.name}}"
path: templates/shared/secrets/add_list.tmpl
user: login: LaoQi
body: Is this property actually needed? I don't see it used anywhere, and the template isn't setting its value.
created_at: 2025-02-11T04:20:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,6 +25,9 @@
 				<div class="flex-item-title">
 					{{.Name}}
 				</div>
+				<div class="flex-item-body">
+					{{if .Description}} {{.Description}} {{else}} - {{end}}
path: templates/shared/variables/variable_list.tmpl
user: login: silverwind
body: ```suggestion
					{{if .Description}}{{.Description}}{{else}}-{{end}}
```

Whitespace inside HTML tags is irrelevant in flexbox. If you really need it for spacing, use `&nbsp;` or add margin classes.
created_at: 2025-02-14T00:39:23Z
author_association: MEMBER
diff_hunk: @@ -22,6 +22,9 @@
 				<div class="flex-item-title">
 					{{.Name}}
 				</div>
+				<div class="flex-item-body">
+					{{if .Description}} {{.Description}} {{else}} - {{end}}
path: templates/shared/secrets/add_list.tmpl
user: login: silverwind
body: ```suggestion
					{{if .Description}}{{.Description}}{{else}}-{{end}}
```
created_at: 2025-02-14T00:41:04Z
author_association: MEMBER
diff_hunk: @@ -49,7 +49,8 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(
+		ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
path: routers/api/v1/user/action.go
user: login: wxiaoguang
body: ```suggestion
	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
```
created_at: 2025-03-17T15:42:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,7 +107,8 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(
+		ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
path: routers/api/v1/org/action.go
user: login: wxiaoguang
body: ```suggestion
	_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
```
created_at: 2025-03-17T15:43:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,7 +137,8 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
 
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
 
-	_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data)
+	_, created, err := secret_service.CreateOrUpdateSecret(
+		ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
path: routers/api/v1/repo/action.go
user: login: wxiaoguang
body: ```suggestion
	_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
```
created_at: 2025-03-17T15:44:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,12 +22,15 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
 	}
 
 	ctx.Data["Secrets"] = secrets
+	ctx.Data["DataMaxLength"] = secret_model.SecretDataMaxLength
+	ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
 }
 
 func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
 	form := web.GetForm(ctx).(*forms.AddSecretForm)
 
-	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
+	s, _, err := secret_service.CreateOrUpdateSecret(
+		ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description)
path: routers/web/shared/secrets/secrets.go
user: login: wxiaoguang
body: ```suggestion
	s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description)
```
created_at: 2025-03-17T15:48:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: LaoQi
created_at: 2025-02-08T03:13:44Z
body: Is there a standardized length limit for these texts in the project? Maybe we should define constants for these. For now, I've capped them all at 4096 characters
author_association: CONTRIBUTOR
user: login: lunny
created_at: 2025-02-09T03:26:39Z
body: LGTM except https://github.com/go-gitea/gitea/pull/33526#discussion_r1946991956

Screenshots are necessary.
author_association: MEMBER
user: login: LaoQi
created_at: 2025-02-09T06:56:59Z
body: I noticed that the data field also doesn't have a length restriction. I'll add a length constraint to it as well. For this field, I think a limit of 65536 would be appropriate, although we can truncate the display when showing it.
The input data is generally UTF-8 encoded, so I will use UTF-8 character counting.
eg:
```golang
const (
	VariableDataMaxLength        = 65536
	VariableDescriptionMaxLength = 4096
)
if utf8.RuneCountInString(data) > VariableDataMaxLength {
	data = string([]rune(data)[:VariableDataMaxLength])
}

if utf8.RuneCountInString(description) > VariableDescriptionMaxLength {
	description = string([]rune(description)[:VariableDescriptionMaxLength])
}
```

Any other suggestions?
author_association: CONTRIBUTOR
user: login: LaoQi
created_at: 2025-02-09T19:43:55Z
body: ![034215](https://github.com/user-attachments/assets/47ce5115-adf8-4e3f-b711-13bac9712d4c)
![034046](https://github.com/user-attachments/assets/fad504af-57fb-4537-b3c7-532d3e992570)
![034021](https://github.com/user-attachments/assets/2c2a78c9-62a0-4695-b99c-70aa4f56ffae)

author_association: CONTRIBUTOR
user: login: lunny
created_at: 2025-02-09T21:25:47Z
body: I think the description should be moved under value because it's optional. And it's better to have 2 rows at least.
author_association: MEMBER
user: login: LaoQi
created_at: 2025-02-10T03:37:12Z
body: > I think the description should be moved under value because it's optional. And it's better to have 2 rows at least.

Does it refer only to the form, or does it also include the list? Perhaps it would be better to place the description in the list above.
author_association: CONTRIBUTOR
user: login: LaoQi
created_at: 2025-02-10T14:51:45Z
body: ![225010](https://github.com/user-attachments/assets/8ecce258-eb8e-4706-959f-416db5e67306)
author_association: CONTRIBUTOR
user: login: silverwind
created_at: 2025-03-16T08:07:59Z
body: Were all reviews addressed? If so, please resolve them.
author_association: MEMBER
user: login: LaoQi
created_at: 2025-03-16T19:11:07Z
body: All problems have been fixed.
author_association: CONTRIBUTOR