Merge bbb196fd0d6a57f01a007c32cb8c6b5ec05bea93 into 21af8150b7ba315a9f75264ab77813b0b7c697a8

This commit is contained in:
John Smith 2025-02-19 17:53:23 -08:00 committed by GitHub
commit 247460bc41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 251 additions and 63 deletions

View File

@ -6,10 +6,12 @@ package actions
import ( import (
"context" "context"
"strings" "strings"
"unicode/utf8"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
) )
@ -32,26 +34,39 @@ type ActionVariable struct {
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
Data string `xorm:"LONGTEXT NOT NULL"` Data string `xorm:"LONGTEXT NOT NULL"`
Description string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
} }
const (
VariableDataMaxLength = 65536
VariableDescriptionMaxLength = 4096
)
func init() { func init() {
db.RegisterModel(new(ActionVariable)) 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 { if ownerID != 0 && repoID != 0 {
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally. // 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. // Remove OwnerID to avoid confusion; it's not worth returning an error here.
ownerID = 0 ownerID = 0
} }
if utf8.RuneCountInString(data) > VariableDataMaxLength {
return nil, util.NewInvalidArgumentErrorf("data too long")
}
description = util.TruncateRunes(description, VariableDescriptionMaxLength)
variable := &ActionVariable{ variable := &ActionVariable{
OwnerID: ownerID, OwnerID: ownerID,
RepoID: repoID, RepoID: repoID,
Name: strings.ToUpper(name), Name: strings.ToUpper(name),
Data: data, Data: data,
Description: description,
} }
return variable, db.Insert(ctx, variable) 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) { 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) variable.Name = strings.ToUpper(variable.Name)
count, err := db.GetEngine(ctx). count, err := db.GetEngine(ctx).
ID(variable.ID). ID(variable.ID).

View File

@ -374,6 +374,7 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312) // Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge), newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin), newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
newMigration(314, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
} }
return preparedMigrations return preparedMigrations
} }

View File

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

View File

@ -40,9 +40,15 @@ type Secret struct {
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL DEFAULT 0"` RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL DEFAULT 0"`
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
Data string `xorm:"LONGTEXT"` // encrypted data Data string `xorm:"LONGTEXT"` // encrypted data
Description string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
} }
const (
SecretDataMaxLength = 65536
SecretDescriptionMaxLength = 4096
)
// ErrSecretNotFound represents a "secret not found" error. // ErrSecretNotFound represents a "secret not found" error.
type ErrSecretNotFound struct { type ErrSecretNotFound struct {
Name string 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 // 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 { if ownerID != 0 && repoID != 0 {
// It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally. // 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. // 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) 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) encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
secret := &Secret{ secret := &Secret{
OwnerID: ownerID, OwnerID: ownerID,
RepoID: repoID, RepoID: repoID,
Name: strings.ToUpper(name), Name: strings.ToUpper(name),
Data: encrypted, Data: encrypted,
Description: description,
} }
return secret, db.Insert(ctx, secret) return secret, db.Insert(ctx, secret)
} }
@ -114,7 +128,13 @@ func (opts FindSecretsOptions) ToConds() builder.Cond {
} }
// UpdateSecret changes org or user reop secret. // 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) encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
if err != nil { if err != nil {
return err return err
@ -122,8 +142,9 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error {
s := &Secret{ 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 { if affected != 1 {
return ErrSecretNotFound{} return ErrSecretNotFound{}
} }

View File

@ -10,6 +10,8 @@ import "time"
type Secret struct { type Secret struct {
// the secret's name // the secret's name
Name string `json:"name"` Name string `json:"name"`
// the secret's description
Description string `json:"description"`
// swagger:strfmt date-time // swagger:strfmt date-time
Created time.Time `json:"created_at"` Created time.Time `json:"created_at"`
} }
@ -21,4 +23,9 @@ type CreateOrUpdateSecretOption struct {
// //
// required: true // required: true
Data string `json:"data" binding:"Required"` Data string `json:"data" binding:"Required"`
// Description of the secret to update
//
// required: false
Description string `json:"description"`
} }

View File

@ -10,6 +10,11 @@ type CreateVariableOption struct {
// //
// required: true // required: true
Value string `json:"value" binding:"Required"` Value string `json:"value" binding:"Required"`
// Description of the variable to create
//
// required: false
Description string `json:"description"`
} }
// UpdateVariableOption the option when updating variable // UpdateVariableOption the option when updating variable
@ -21,6 +26,11 @@ type UpdateVariableOption struct {
// //
// required: true // required: true
Value string `json:"value" binding:"Required"` 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 // ActionVariable return value of the query API
@ -34,4 +44,6 @@ type ActionVariable struct {
Name string `json:"name"` Name string `json:"name"`
// the value of the variable // the value of the variable
Data string `json:"data"` Data string `json:"data"`
// the description of the variable
Description string `json:"description"`
} }

View File

@ -3703,8 +3703,10 @@ secrets = Secrets
description = Secrets will be passed to certain actions and cannot be read otherwise. description = Secrets will be passed to certain actions and cannot be read otherwise.
none = There are no secrets yet. none = There are no secrets yet.
creation = Add Secret creation = Add Secret
creation.description = Description
creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ 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.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.success = The secret "%s" has been added.
creation.failed = Failed to add secret. creation.failed = Failed to add secret.
deletion = Remove secret deletion = Remove secret

View File

@ -62,6 +62,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
for k, v := range secrets { for k, v := range secrets {
apiSecrets[k] = &api.Secret{ apiSecrets[k] = &api.Secret{
Name: v.Name, Name: v.Name,
Description: v.Description,
Created: v.CreatedUnix.AsTime(), Created: v.CreatedUnix.AsTime(),
} }
} }
@ -106,7 +107,8 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) 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 err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
@ -234,6 +236,7 @@ func (Action) ListVariables(ctx *context.APIContext) {
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data, Data: v.Data,
Description: v.Description,
} }
} }
@ -285,6 +288,7 @@ func (Action) GetVariable(ctx *context.APIContext) {
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data, Data: v.Data,
Description: v.Description,
} }
ctx.JSON(http.StatusOK, variable) ctx.JSON(http.StatusOK, variable)
@ -386,7 +390,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return 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) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
} else { } else {
@ -453,6 +457,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
v.Name = opt.Name v.Name = opt.Name
v.Data = opt.Value v.Data = opt.Value
v.Description = opt.Description
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {

View File

@ -85,6 +85,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
for k, v := range secrets { for k, v := range secrets {
apiSecrets[k] = &api.Secret{ apiSecrets[k] = &api.Secret{
Name: v.Name, Name: v.Name,
Description: v.Description,
Created: v.CreatedUnix.AsTime(), Created: v.CreatedUnix.AsTime(),
} }
} }
@ -136,7 +137,8 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) 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 err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
@ -253,6 +255,7 @@ func (Action) GetVariable(ctx *context.APIContext) {
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data, Data: v.Data,
Description: v.Description,
} }
ctx.JSON(http.StatusOK, variable) ctx.JSON(http.StatusOK, variable)
@ -362,7 +365,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return 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) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
} else { } else {
@ -432,6 +435,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
v.Name = opt.Name v.Name = opt.Name
v.Data = opt.Value v.Data = opt.Value
v.Description = opt.Description
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
@ -494,6 +498,8 @@ func (Action) ListVariables(ctx *context.APIContext) {
OwnerID: v.OwnerID, OwnerID: v.OwnerID,
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data,
Description: v.Description,
} }
} }

View File

@ -49,7 +49,8 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) 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 err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
@ -153,7 +154,7 @@ func CreateVariable(ctx *context.APIContext) {
return 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) { if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
} else { } else {
@ -215,6 +216,7 @@ func UpdateVariable(ctx *context.APIContext) {
v.Name = opt.Name v.Name = opt.Name
v.Data = opt.Value v.Data = opt.Value
v.Description = opt.Description
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrInvalidArgument) {
@ -304,6 +306,7 @@ func GetVariable(ctx *context.APIContext) {
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data, Data: v.Data,
Description: v.Description,
} }
ctx.JSON(http.StatusOK, variable) ctx.JSON(http.StatusOK, variable)
@ -349,6 +352,7 @@ func ListVariables(ctx *context.APIContext) {
RepoID: v.RepoID, RepoID: v.RepoID,
Name: v.Name, Name: v.Name,
Data: v.Data, Data: v.Data,
Description: v.Description,
} }
} }

View File

@ -106,7 +106,8 @@ func Variables(ctx *context.Context) {
return return
} }
ctx.Data["Variables"] = variables ctx.Data["Variables"] = variables
ctx.Data["DataMaxLength"] = actions_model.VariableDataMaxLength
ctx.Data["DescriptionMaxLength"] = actions_model.VariableDescriptionMaxLength
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
} }
@ -124,7 +125,7 @@ func VariableCreate(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditVariableForm) 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 { if err != nil {
log.Error("CreateVariable: %v", err) log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
@ -157,6 +158,7 @@ func VariableUpdate(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditVariableForm) form := web.GetForm(ctx).(*forms.EditVariableForm)
variable.Name = form.Name variable.Name = form.Name
variable.Data = form.Data variable.Data = form.Data
variable.Description = form.Description
if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok { if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
log.Error("UpdateVariable: %v", err) log.Error("UpdateVariable: %v", err)

View File

@ -22,12 +22,15 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
} }
ctx.Data["Secrets"] = secrets 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) { func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm) 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 { if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err) log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed")) ctx.JSONError(ctx.Tr("secrets.creation.failed"))

View File

@ -13,7 +13,7 @@ import (
secret_service "code.gitea.io/gitea/services/secrets" 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 { if err := secret_service.ValidateName(name); err != nil {
return nil, err return nil, err
} }
@ -22,7 +22,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -41,7 +41,7 @@ func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionV
variable.Data = util.ReserveLineBreakForTextarea(variable.Data) 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 { func DeleteVariableByID(ctx context.Context, variableID int64) error {

View File

@ -327,6 +327,7 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er
type AddSecretForm struct { type AddSecretForm struct {
Name string `binding:"Required;MaxSize(255)"` Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"Required;MaxSize(65535)"` Data string `binding:"Required;MaxSize(65535)"`
Description string `binding:"MaxSize(65535)"`
} }
// Validate validates the fields // Validate validates the fields
@ -338,6 +339,7 @@ func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding
type EditVariableForm struct { type EditVariableForm struct {
Name string `binding:"Required;MaxSize(255)"` Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"Required;MaxSize(65535)"` Data string `binding:"Required;MaxSize(65535)"`
Description string `binding:"MaxSize(65535)"`
} }
func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {

View File

@ -10,7 +10,7 @@ import (
secret_model "code.gitea.io/gitea/models/secret" 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 { if err := ValidateName(name); err != nil {
return nil, false, err return nil, false, err
} }
@ -25,14 +25,14 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data
} }
if len(s) == 0 { 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 { if err != nil {
return nil, false, err return nil, false, err
} }
return s, true, nil 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 return nil, false, err
} }

View File

@ -22,6 +22,9 @@
<div class="flex-item-title"> <div class="flex-item-title">
{{.Name}} {{.Name}}
</div> </div>
<div class="flex-item-body">
{{if .Description}}{{.Description}}{{else}}-{{end}}
</div>
<div class="flex-item-body"> <div class="flex-item-body">
****** ******
</div> </div>
@ -72,9 +75,20 @@
<textarea required <textarea required
id="secret-data" id="secret-data"
name="data" name="data"
maxlength="{{.DataMaxLength}}"
placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}" placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
></textarea> ></textarea>
</div> </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> </div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form> </form>

View File

@ -7,6 +7,7 @@
data-modal-header="{{ctx.Locale.Tr "actions.variables.creation"}}" data-modal-header="{{ctx.Locale.Tr "actions.variables.creation"}}"
data-modal-dialog-variable-name="" data-modal-dialog-variable-name=""
data-modal-dialog-variable-data="" data-modal-dialog-variable-data=""
data-modal-dialog-variable-description=""
> >
{{ctx.Locale.Tr "actions.variables.creation"}} {{ctx.Locale.Tr "actions.variables.creation"}}
</button> </button>
@ -24,6 +25,9 @@
<div class="flex-item-title"> <div class="flex-item-title">
{{.Name}} {{.Name}}
</div> </div>
<div class="flex-item-body">
{{if .Description}}{{.Description}}{{else}}-{{end}}
</div>
<div class="flex-item-body"> <div class="flex-item-body">
{{.Data}} {{.Data}}
</div> </div>
@ -39,6 +43,7 @@
data-modal-header="{{ctx.Locale.Tr "actions.variables.edit"}}" data-modal-header="{{ctx.Locale.Tr "actions.variables.edit"}}"
data-modal-dialog-variable-name="{{.Name}}" data-modal-dialog-variable-name="{{.Name}}"
data-modal-dialog-variable-data="{{.Data}}" data-modal-dialog-variable-data="{{.Data}}"
data-modal-dialog-variable-description="{{.Description}}"
> >
{{svg "octicon-pencil"}} {{svg "octicon-pencil"}}
</button> </button>
@ -82,9 +87,20 @@
<textarea required <textarea required
name="data" name="data"
id="dialog-variable-data" id="dialog-variable-data"
maxlength="{{.DataMaxLength}}"
placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}" placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
></textarea> ></textarea>
</div> </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> </div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form> </form>

View File

@ -19319,6 +19319,11 @@
"type": "string", "type": "string",
"x-go-name": "Data" "x-go-name": "Data"
}, },
"description": {
"description": "the description of the variable",
"type": "string",
"x-go-name": "Description"
},
"name": { "name": {
"description": "the name of the variable", "description": "the name of the variable",
"type": "string", "type": "string",
@ -20982,6 +20987,11 @@
"description": "Data of the secret to update", "description": "Data of the secret to update",
"type": "string", "type": "string",
"x-go-name": "Data" "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" "x-go-package": "code.gitea.io/gitea/modules/structs"
@ -21492,6 +21502,11 @@
"value" "value"
], ],
"properties": { "properties": {
"description": {
"description": "Description of the variable to create",
"type": "string",
"x-go-name": "Description"
},
"value": { "value": {
"description": "Value of the variable to create", "description": "Value of the variable to create",
"type": "string", "type": "string",
@ -25453,6 +25468,11 @@
"format": "date-time", "format": "date-time",
"x-go-name": "Created" "x-go-name": "Created"
}, },
"description": {
"description": "the secret's description",
"type": "string",
"x-go-name": "Description"
},
"name": { "name": {
"description": "the secret's name", "description": "the secret's name",
"type": "string", "type": "string",
@ -26028,6 +26048,11 @@
"value" "value"
], ],
"properties": { "properties": {
"description": {
"description": "Description of the variable to update",
"type": "string",
"x-go-name": "Description"
},
"name": { "name": {
"description": "New name for the variable. If the field is empty, the variable name won't be updated.", "description": "New name for the variable. If the field is empty, the variable name won't be updated.",
"type": "string", "type": "string",

View File

@ -28,21 +28,21 @@ func TestActionsVariables(t *testing.T) {
require.NoError(t, db.DeleteAllRecords("action_variable")) require.NoError(t, db.DeleteAllRecords("action_variable"))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 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"}) user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
userWebURL := "/user/settings/actions/variables" userWebURL := "/user/settings/actions/variables"
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization}) 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"}) org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
orgWebURL := "/org/org3/settings/actions/variables" orgWebURL := "/org/org3/settings/actions/variables"
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 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"}) repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
repoWebURL := "/user2/repo1/settings/actions/variables" 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"}) globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
adminWebURL := "/-/admin/actions/variables" adminWebURL := "/-/admin/actions/variables"

View File

@ -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) { t.Run("Update", func(t *testing.T) {
name := "update_secret" name := "update_secret"
url := fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), name) url := fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), name)