From d20280e39410495ba1d137ed3479159c24616d7b Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Fri, 1 Mar 2024 18:19:33 +0800 Subject: [PATCH 01/18] feat: create and update user scope variable --- models/actions/variable.go | 12 +++- modules/structs/variable.go | 21 ++++++ routers/api/v1/api.go | 7 ++ routers/api/v1/user/action.go | 86 +++++++++++++++++++++++++ routers/web/shared/actions/variables.go | 67 ++----------------- routers/web/shared/secrets/secrets.go | 4 +- services/actions/variables.go | 78 ++++++++++++++++++++++ 7 files changed, 211 insertions(+), 64 deletions(-) create mode 100644 modules/structs/variable.go create mode 100644 services/actions/variables.go diff --git a/models/actions/variable.go b/models/actions/variable.go index 12717e0ae4614..42e943a6ece49 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -54,12 +54,20 @@ type FindVariablesOpts struct { db.ListOptions OwnerID int64 RepoID int64 + Name string } func (opts FindVariablesOpts) ToConds() builder.Cond { cond := builder.NewCond() - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } return cond } diff --git a/modules/structs/variable.go b/modules/structs/variable.go new file mode 100644 index 0000000000000..4e2533e0085d4 --- /dev/null +++ b/modules/structs/variable.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// CreateVariableOption the option when creating variable +// swagger:model +type CreateVariableOption struct { + // Value of the variable to create + // required: true + Value string `json:"value" binding:"Required"` +} + +// UpdateVariableOption the option when updating variable +type UpdateVariableOption struct { + // New name for the variable. If the field is empty, the variable name won't be updated. + Name string `json:"name"` + // Value of the variable to update + // required: true + Value string `json:"value" binding:"Required"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0913571c27020..312c16b522de0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -955,6 +955,13 @@ func Routes() *web.Route { Delete(user.DeleteSecret) }) + m.Group("/variables", func() { + m.Combo("/{variablename}"). + Post(bind(api.CreateVariableOption{}), user.CreateVariable). + Put(bind(api.UpdateVariableOption{}), user.UpdateVariable). + Delete(user.DeleteVaribale) + }) + m.Group("/runners", func() { m.Get("/registration-token", reqToken(), user.GetRegistrationToken) }) diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index babb8c0cf7abb..47bc45e3229ad 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -5,11 +5,16 @@ package user import ( "errors" + "fmt" "net/http" + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/actions" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" ) @@ -101,3 +106,84 @@ func DeleteSecret(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +// CreateVariable create a user-level variable +func CreateVariable(ctx *context.APIContext) { + // swagger:operation PUT /user/actions/variables/{variablename} user createUserVariable + // --- + // summary: Create a user-level variable + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateVariable" + // responses: + // "201": + // description: response when creating a variable + // "204": + // description: response when updating a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateVariableOption) + + if _, err := actions_service.CreateVariable(ctx, ctx.Doer.ID, 0, ctx.Params("variablename"), opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// UpdateVariable update a user-level variable +func UpdateVariable(ctx *context.APIContext) { + + opt := web.GetForm(ctx).(*api.UpdateVariableOption) + + v, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindVariable", err) + return + } + if len(v) == 0 { + ctx.Error(http.StatusNotFound, "FindVariable", fmt.Errorf("varibale not found")) + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + if _, err := actions.UpdateVariable(ctx, v[0].ID, opt.Name, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteVaribale delete a user-level variable +func DeleteVaribale(ctx *context.APIContext) { + +} diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 0f705399c9705..2caba59bcc35a 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -4,17 +4,13 @@ package actions import ( - "errors" - "regexp" - "strings" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" - secret_service "code.gitea.io/gitea/services/secrets" ) func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { @@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { ctx.Data["Variables"] = variables } -// some regular expression of `variables` and `secrets` -// reference to: -// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables -// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets -var ( - forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI") -) - -func envNameCIRegexMatch(name string) error { - if forbiddenEnvNameCIRx.MatchString(name) { - log.Error("Env Name cannot be ci") - return errors.New("env name cannot be ci") - } - return nil -} - func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) { form := web.GetForm(ctx).(*forms.EditVariableForm) - if err := secret_service.ValidateName(form.Name); err != nil { - ctx.JSONError(err.Error()) - return - } - - if err := envNameCIRegexMatch(form.Name); err != nil { - ctx.JSONError(err.Error()) - return - } - - v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data)) + v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data) if err != nil { - log.Error("InsertVariable error: %v", err) + log.Error("CreateVariable: %v", err) ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) return } + ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) ctx.JSONRedirect(redirectURL) } @@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) { id := ctx.ParamsInt64(":variable_id") form := web.GetForm(ctx).(*forms.EditVariableForm) - if err := secret_service.ValidateName(form.Name); err != nil { - ctx.JSONError(err.Error()) - return - } - - if err := envNameCIRegexMatch(form.Name); err != nil { - ctx.JSONError(err.Error()) - return - } - - ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ - ID: id, - Name: strings.ToUpper(form.Name), - Data: ReserveLineBreakForTextarea(form.Data), - }) - if err != nil || !ok { - log.Error("UpdateVariable error: %v", err) + if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok { + log.Error("UpdateVariable: %v", err) ctx.JSONError(ctx.Tr("actions.variables.update.failed")) return } @@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) { func DeleteVariable(ctx *context.Context, redirectURL string) { id := ctx.ParamsInt64(":variable_id") - if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil { + if _, err := actions_service.DeleteVariable(ctx, id); err != nil { log.Error("Delete variable [%d] failed: %v", id, err) ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) return @@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) { ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) ctx.JSONRedirect(redirectURL) } - -func ReserveLineBreakForTextarea(input string) string { - // Since the content is from a form which is a textarea, the line endings are \r\n. - // It's a standard behavior of HTML. - // But we want to store them as \n like what GitHub does. - // And users are unlikely to really need to keep the \r. - // Other than this, we should respect the original content, even leading or trailing spaces. - return strings.ReplaceAll(input, "\r\n", "\n") -} diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index 73505ec372d4e..8c99b20a69621 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -8,7 +8,7 @@ import ( secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/web/shared/actions" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" secret_service "code.gitea.io/gitea/services/secrets" @@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { 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, actions.ReserveLineBreakForTextarea(form.Data)) + s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions_service.ReserveLineBreakForTextarea(form.Data)) 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 new file mode 100644 index 0000000000000..439227c834e44 --- /dev/null +++ b/services/actions/variables.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "regexp" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + secret_service "code.gitea.io/gitea/services/secrets" +) + +func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) { + if err := secret_service.ValidateName(name); err != nil { + return nil, err + } + + if err := envNameCIRegexMatch(name); err != nil { + return nil, err + } + + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, ReserveLineBreakForTextarea(data)) + if err != nil { + return nil, err + } + + return v, nil +} + +func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) { + if err := secret_service.ValidateName(name); err != nil { + return false, err + } + + if err := envNameCIRegexMatch(name); err != nil { + return false, err + } + + return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ + ID: variableID, + Name: strings.ToUpper(name), + Data: ReserveLineBreakForTextarea(data), + }) +} + +func DeleteVariable(ctx context.Context, variableID int64) (int64, error) { + return db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: variableID}) +} + +// some regular expression of `variables` and `secrets` +// reference to: +// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables +// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets +var ( + forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI") +) + +func envNameCIRegexMatch(name string) error { + if forbiddenEnvNameCIRx.MatchString(name) { + log.Error("Env Name cannot be ci") + return util.NewInvalidArgumentErrorf("env name cannot be ci") + } + return nil +} + +func ReserveLineBreakForTextarea(input string) string { + // Since the content is from a form which is a textarea, the line endings are \r\n. + // It's a standard behavior of HTML. + // But we want to store them as \n like what GitHub does. + // And users are unlikely to really need to keep the \r. + // Other than this, we should respect the original content, even leading or trailing spaces. + return strings.ReplaceAll(input, "\r\n", "\n") +} From 7fb32af2a87766405161952ee6c80f04daa2fcfc Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Fri, 1 Mar 2024 22:00:18 +0800 Subject: [PATCH 02/18] chore: lint and typo --- routers/api/v1/api.go | 2 +- routers/api/v1/user/action.go | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 312c16b522de0..7d33019d95916 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -959,7 +959,7 @@ func Routes() *web.Route { m.Combo("/{variablename}"). Post(bind(api.CreateVariableOption{}), user.CreateVariable). Put(bind(api.UpdateVariableOption{}), user.UpdateVariable). - Delete(user.DeleteVaribale) + Delete(user.DeleteVariable) }) m.Group("/runners", func() { diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 47bc45e3229ad..cad108026a49d 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -13,7 +13,6 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" @@ -152,7 +151,6 @@ func CreateVariable(ctx *context.APIContext) { // UpdateVariable update a user-level variable func UpdateVariable(ctx *context.APIContext) { - opt := web.GetForm(ctx).(*api.UpdateVariableOption) v, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ @@ -164,14 +162,14 @@ func UpdateVariable(ctx *context.APIContext) { return } if len(v) == 0 { - ctx.Error(http.StatusNotFound, "FindVariable", fmt.Errorf("varibale not found")) + ctx.Error(http.StatusNotFound, "FindVariable", fmt.Errorf("variable not found")) return } if opt.Name == "" { opt.Name = ctx.Params("variablename") } - if _, err := actions.UpdateVariable(ctx, v[0].ID, opt.Name, opt.Value); err != nil { + if _, err := actions_service.UpdateVariable(ctx, v[0].ID, opt.Name, opt.Value); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { @@ -183,7 +181,5 @@ func UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// DeleteVaribale delete a user-level variable -func DeleteVaribale(ctx *context.APIContext) { - -} +// DeleteVariable delete a user-level variable +func DeleteVariable(ctx *context.APIContext) {} From eacaba14e7621b7ecbaac098208df7cae40145d6 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Fri, 8 Mar 2024 18:23:39 +0800 Subject: [PATCH 03/18] feat: delete and get user-level var --- models/actions/variable.go | 20 ++-- modules/structs/variable.go | 14 ++- routers/api/v1/api.go | 5 +- routers/api/v1/swagger/options.go | 6 + routers/api/v1/user/action.go | 139 +++++++++++++++++++++--- routers/web/shared/actions/variables.go | 2 +- services/actions/variables.go | 37 ++++++- 7 files changed, 190 insertions(+), 33 deletions(-) diff --git a/models/actions/variable.go b/models/actions/variable.go index 42e943a6ece49..5c3fb2f4889be 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -6,12 +6,10 @@ package actions import ( "context" "errors" - "fmt" "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -71,15 +69,8 @@ func (opts FindVariablesOpts) ToConds() builder.Cond { return cond } -func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) { - var variable ActionVariable - has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable) - if err != nil { - return nil, err - } else if !has { - return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist) - } - return &variable, nil +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) { + return db.Find[ActionVariable](ctx, opts) } func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { @@ -90,3 +81,10 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) }) return count != 0, err } + +func DeleteVariable(ctx context.Context, id int64) error { + if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil { + return err + } + return nil +} diff --git a/modules/structs/variable.go b/modules/structs/variable.go index 4e2533e0085d4..db89d56431de9 100644 --- a/modules/structs/variable.go +++ b/modules/structs/variable.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package structs @@ -7,15 +7,27 @@ package structs // swagger:model type CreateVariableOption struct { // Value of the variable to create + // // required: true Value string `json:"value" binding:"Required"` } // UpdateVariableOption the option when updating variable +// swagger:model type UpdateVariableOption struct { // New name for the variable. If the field is empty, the variable name won't be updated. Name string `json:"name"` // Value of the variable to update + // // required: true Value string `json:"value" binding:"Required"` } + +// ActionVariable return value of the query API +// swagger:model +type ActionVariable struct { + OwnerID int64 `json:"owner_id"` + RepoID int64 `json:"repo_id"` + Name string `json:"name"` + Data string `json:"data"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 7d33019d95916..0a9f7a9fe3659 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -957,9 +957,10 @@ func Routes() *web.Route { m.Group("/variables", func() { m.Combo("/{variablename}"). + Get(user.GetVariable). + Delete(user.DeleteVariable). Post(bind(api.CreateVariableOption{}), user.CreateVariable). - Put(bind(api.UpdateVariableOption{}), user.UpdateVariable). - Delete(user.DeleteVariable) + Put(bind(api.UpdateVariableOption{}), user.UpdateVariable) }) m.Group("/runners", func() { diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 6f7859df62ed4..14eb1ecd5bbec 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -190,4 +190,10 @@ type swaggerParameterBodies struct { // in:body CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption + + // in:body + CreateVariableOption api.CreateVariableOption + + // in:body + UpdateVariableOption api.UpdateVariableOption } diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index cad108026a49d..e9ae9f8c53f75 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -5,11 +5,10 @@ package user import ( "errors" - "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -108,7 +107,7 @@ func DeleteSecret(ctx *context.APIContext) { // CreateVariable create a user-level variable func CreateVariable(ctx *context.APIContext) { - // swagger:operation PUT /user/actions/variables/{variablename} user createUserVariable + // swagger:operation POST /user/actions/variables/{variablename} user createUserVariable // --- // summary: Create a user-level variable // consumes: @@ -124,12 +123,12 @@ func CreateVariable(ctx *context.APIContext) { // - name: body // in: body // schema: - // "$ref": "#/definitions/CreateVariable" + // "$ref": "#/definitions/CreateVariableOption" // responses: // "201": // description: response when creating a variable // "204": - // description: response when updating a variable + // description: response when creating a variable // "400": // "$ref": "#/responses/error" // "404": @@ -149,27 +148,54 @@ func CreateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// UpdateVariable update a user-level variable +// UpdateVariable update a user-level variable which is created by current doer func UpdateVariable(ctx *context.APIContext) { + // swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable + // --- + // summary: Update a user-level variable which is created by current doer + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateVariableOption" + // responses: + // "201": + // description: response when updating a variable + // "204": + // description: response when updating a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + opt := web.GetForm(ctx).(*api.UpdateVariableOption) - v, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ OwnerID: ctx.Doer.ID, Name: ctx.Params("variablename"), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindVariable", err) - return - } - if len(v) == 0 { - ctx.Error(http.StatusNotFound, "FindVariable", fmt.Errorf("variable not found")) + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } return } if opt.Name == "" { opt.Name = ctx.Params("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v[0].ID, opt.Name, opt.Value); err != nil { + if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { @@ -181,5 +207,88 @@ func UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// DeleteVariable delete a user-level variable -func DeleteVariable(ctx *context.APIContext) {} +// DeleteVariable delete a user-level variable which is created by current doer +func DeleteVariable(ctx *context.APIContext) { + // swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable + // --- + // summary: Delete a user-level variable which is created by current doer + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "201": + // description: response when deleting a variable + // "204": + // description: response when deleting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// GetVariable get a user-level variable which is created by current doer +func GetVariable(ctx *context.APIContext) { + // swagger:operation GET /user/actions/variables/{variablename} user getUserVariable + // --- + // summary: Get a user-level variable which is created by current doer + // produces: + // - application/json + // parameters: + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": ”#/responses/ActionVariable“ + // "201": + // description: response when deleting a variable + // "204": + // description: response when deleting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + variable := &structs.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index 2caba59bcc35a..79c03e4e8ca21 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -55,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) { func DeleteVariable(ctx *context.Context, redirectURL string) { id := ctx.ParamsInt64(":variable_id") - if _, err := actions_service.DeleteVariable(ctx, id); err != nil { + if err := actions_service.DeleteVariableByID(ctx, id); err != nil { log.Error("Delete variable [%d] failed: %v", id, err) ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) return diff --git a/services/actions/variables.go b/services/actions/variables.go index 439227c834e44..eb98fc068c9d4 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -9,7 +9,6 @@ import ( "strings" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" secret_service "code.gitea.io/gitea/services/secrets" @@ -48,8 +47,40 @@ func UpdateVariable(ctx context.Context, variableID int64, name, data string) (b }) } -func DeleteVariable(ctx context.Context, variableID int64) (int64, error) { - return db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: variableID}) +func DeleteVariableByID(ctx context.Context, variableID int64) error { + return actions_model.DeleteVariable(ctx, variableID) +} + +func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error { + if err := secret_service.ValidateName(name); err != nil { + return err + } + + if err := envNameCIRegexMatch(name); err != nil { + return err + } + + v, err := GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return err + } + + return actions_model.DeleteVariable(ctx, v.ID) +} + +func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) { + vars, err := actions_model.FindVariables(ctx, opts) + if err != nil { + return nil, err + } + if len(vars) != 1 { + return nil, util.NewNotExistErrorf("variable not found") + } + return vars[0], nil } // some regular expression of `variables` and `secrets` From a4a0102f8e2875b9daa9d8aa3f673c02e83fc62c Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Mon, 11 Mar 2024 14:57:33 +0800 Subject: [PATCH 04/18] chore: generate swagger --- templates/swagger/v1_json.tmpl | 196 ++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 98198696bc40c..97bd139d18796 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15047,6 +15047,163 @@ } } }, + "/user/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get a user-level variable which is created by current doer", + "operationId": "getUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + }, + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update a user-level variable which is created by current doer", + "operationId": "updateUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating a variable" + }, + "204": { + "description": "response when updating a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create a user-level variable", + "operationId": "createUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating a variable" + }, + "204": { + "description": "response when creating a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete a user-level variable which is created by current doer", + "operationId": "deleteUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/applications/oauth2": { "get": { "produces": [ @@ -19091,6 +19248,21 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateVariableOption": { + "description": "CreateVariableOption the option when creating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "description": "Value of the variable to create", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateWikiPageOptions": { "description": "CreateWikiPageOptions form for creating wiki", "type": "object", @@ -23383,6 +23555,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateVariableOption": { + "description": "UpdateVariableOption the option when updating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "name": { + "description": "New name for the variable. If the field is empty, the variable name won't be updated.", + "type": "string", + "x-go-name": "Name" + }, + "value": { + "description": "Value of the variable to update", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "User": { "description": "User represents a user", "type": "object", @@ -24722,7 +24914,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/BadgeList" + "$ref": "#/definitions/UpdateVariableOption" } }, "redirect": { @@ -24821,4 +25013,4 @@ "TOTPHeader": [] } ] -} +} \ No newline at end of file From ca218ad301f70ad8fb59e2f44860ed6db9655a2e Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 12 Mar 2024 11:08:44 +0800 Subject: [PATCH 05/18] feat: org-level var api --- routers/api/v1/api.go | 8 ++ routers/api/v1/org/variables.go | 240 ++++++++++++++++++++++++++++++++ routers/api/v1/user/action.go | 21 ++- templates/swagger/v1_json.tmpl | 188 +++++++++++++++++++++++++ 4 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 routers/api/v1/org/variables.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f0a06c56a4276..6086621e53718 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1460,6 +1460,14 @@ func Routes() *web.Route { Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret) }) + m.Group("/variables", func() { + m.Combo("/{variablename}"). + Get(reqToken(), reqOrgOwnership(), org.GetVariable). + Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). + Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable). + Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable) + }) + m.Group("/runners", func() { m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken) }) diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go new file mode 100644 index 0000000000000..c3241499c4104 --- /dev/null +++ b/routers/api/v1/org/variables.go @@ -0,0 +1,240 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "errors" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/context" +) + +// GetVariable get an org-level variable +func GetVariable(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable + // --- + // summary: Get an org-level variable + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": ”#/responses/ActionVariable“ + // "201": + // description: response when getting a variable + // "204": + // description: response when getting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + variable := &api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +// DeleteVariable delete an org-level variable +func DeleteVariable(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable + // --- + // summary: Delete an org-level variable + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": ”#/responses/ActionVariable“ + // "201": + // description: response when deleting a variable + // "204": + // description: response when deleting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// CreateVariable create an org-level variable +func CreateVariable(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable + // --- + // summary: Create an org-level variable + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateVariableOption" + // responses: + // "201": + // description: response when creating an org-level variable + // "204": + // description: response when creating an org-level variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateVariableOption) + + ownerID := ctx.Org.Organization.ID + variableName := ctx.Params("variablename") + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// UpdateVariable update an org-level variable +func UpdateVariable(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable + // --- + // summary: Update an org-level variable + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateVariableOption" + // responses: + // "201": + // description: response when updating an org-level variable + // "204": + // description: response when updating an org-level variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.UpdateVariableOption) + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index e9ae9f8c53f75..ba3879fb93d24 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -136,7 +136,23 @@ func CreateVariable(ctx *context.APIContext) { opt := web.GetForm(ctx).(*api.CreateVariableOption) - if _, err := actions_service.CreateVariable(ctx, ctx.Doer.ID, 0, ctx.Params("variablename"), opt.Value); err != nil { + ownerID := ctx.Doer.ID + variableName := ctx.Params("variablename") + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "CreateVariable", err) } else { @@ -230,8 +246,7 @@ func DeleteVariable(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")) - if err != nil { + if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) } else if errors.Is(err, util.ErrNotExist) { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 97bd139d18796..6837acb13e178 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1844,6 +1844,194 @@ } } }, + "/orgs/{org}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variable", + "operationId": "getOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + }, + "201": { + "description": "response when getting a variable" + }, + "204": { + "description": "response when getting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Update an org-level variable", + "operationId": "updateOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating an org-level variable" + }, + "204": { + "description": "response when updating an org-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create an org-level variable", + "operationId": "createOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating an org-level variable" + }, + "204": { + "description": "response when creating an org-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete an org-level variable", + "operationId": "deleteOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + }, + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/activities/feeds": { "get": { "produces": [ From b23a8368b89235670e5e7e61faf27cb6dc0823dd Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 12 Mar 2024 15:09:22 +0800 Subject: [PATCH 06/18] feat: repo-level var api --- Makefile | 2 +- routers/api/v1/api.go | 8 ++ routers/api/v1/org/variables.go | 1 + routers/api/v1/repo/action.go | 242 ++++++++++++++++++++++++++++++++ routers/api/v1/user/action.go | 3 +- templates/swagger/v1_json.tmpl | 212 +++++++++++++++++++++++++++- 6 files changed, 464 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5ab8655c2fc3e..6adc9b72ea846 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@abb53530bfcf49c470e5f4c7071ee43f37ec7437 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3 diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6086621e53718..e91b4156d1648 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1081,6 +1081,14 @@ func Routes() *web.Route { Delete(reqToken(), reqOwner(), repo.DeleteSecret) }) + m.Group("/variables", func() { + m.Combo("/{variablename}"). + Get(reqToken(), reqOwner(), repo.GetVariable). + Delete(reqToken(), reqOwner(), repo.DeleteVariable). + Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable). + Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable) + }) + m.Group("/runners", func() { m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken) }) diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go index c3241499c4104..fb149115838b8 100644 --- a/routers/api/v1/org/variables.go +++ b/routers/api/v1/org/variables.go @@ -211,6 +211,7 @@ func UpdateVariable(ctx *context.APIContext) { // "$ref": "#/responses/notFound" opt := web.GetForm(ctx).(*api.UpdateVariableOption) + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ OwnerID: ctx.Org.Organization.ID, Name: ctx.Params("variablename"), diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index e0af276c71503..7e7e14813cfba 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -7,9 +7,11 @@ import ( "errors" "net/http" + actions_model "code.gitea.io/gitea/models/actions" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" ) @@ -127,3 +129,243 @@ func DeleteSecret(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +// GetVariable get a repo-level variable +func GetVariable(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable + // --- + // summary: Get a repo-level variable + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": ”#/responses/ActionVariable“ + // "201": + // description: response when getting a variable + // "204": + // description: response when getting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + variable := &api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +// DeleteVariable delete a repo-level variable +func DeleteVariable(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable + // --- + // summary: Delete a repo-level variable + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // responses: + // "200": + // "$ref": ”#/responses/ActionVariable“ + // "201": + // description: response when deleting a variable + // "204": + // description: response when deleting a variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// CreateVariable create a repo-level variable +func CreateVariable(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable + // --- + // summary: Create a repo-level variable + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateVariableOption" + // responses: + // "201": + // description: response when creating a repo-level variable + // "204": + // description: response when creating a repo-level variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateVariableOption) + + repoID := ctx.Repo.Repository.ID + variableName := ctx.Params("variablename") + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + RepoID: repoID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// UpdateVariable update a repo-level variable +func UpdateVariable(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable + // --- + // summary: Update a repo-level variable + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: variablename + // in: path + // description: name of the variable + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateVariableOption" + // responses: + // "201": + // description: response when updating a repo-level variable + // "204": + // description: response when updating a repo-level variable + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.UpdateVariableOption) + + v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetVariable", err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + } else { + ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index ba3879fb93d24..eac30f8a5431f 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -8,7 +8,6 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -298,7 +297,7 @@ func GetVariable(ctx *context.APIContext) { return } - variable := &structs.ActionVariable{ + variable := &api.ActionVariable{ OwnerID: v.OwnerID, RepoID: v.RepoID, Name: v.Name, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6837acb13e178..84db63341e057 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3911,6 +3911,216 @@ } } }, + "/repos/{owner}/{repo}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a repo-level variable", + "operationId": "getRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + }, + "201": { + "description": "response when getting a variable" + }, + "204": { + "description": "response when getting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update a repo-level variable", + "operationId": "updateRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating a repo-level variable" + }, + "204": { + "description": "response when updating a repo-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a repo-level variable", + "operationId": "createRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating a repo-level variable" + }, + "204": { + "description": "response when creating a repo-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a repo-level variable", + "operationId": "deleteRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + }, + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/activities/feeds": { "get": { "produces": [ @@ -25201,4 +25411,4 @@ "TOTPHeader": [] } ] -} \ No newline at end of file +} From e93167493bcc7b620b1b976235a715f667762e8e Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 12 Mar 2024 15:10:33 +0800 Subject: [PATCH 07/18] revert Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6adc9b72ea846..5ab8655c2fc3e 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@abb53530bfcf49c470e5f4c7071ee43f37ec7437 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3 From 6e1548cfe18dc8ad8ff0d600ded2767d090506f0 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 12 Mar 2024 15:39:41 +0800 Subject: [PATCH 08/18] lint swagger --- routers/api/v1/org/variables.go | 8 ++--- routers/api/v1/repo/action.go | 8 ++--- routers/api/v1/swagger/action.go | 7 ++++ routers/api/v1/user/action.go | 6 +--- templates/swagger/v1_json.tmpl | 61 +++++++++++++++++++------------- 5 files changed, 49 insertions(+), 41 deletions(-) diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go index fb149115838b8..7a2a9bd745be3 100644 --- a/routers/api/v1/org/variables.go +++ b/routers/api/v1/org/variables.go @@ -35,11 +35,7 @@ func GetVariable(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": ”#/responses/ActionVariable“ - // "201": - // description: response when getting a variable - // "204": - // description: response when getting a variable + // "$ref": "#/responses/ActionVariable" // "400": // "$ref": "#/responses/error" // "404": @@ -88,7 +84,7 @@ func DeleteVariable(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": ”#/responses/ActionVariable“ + // "$ref": "#/responses/ActionVariable" // "201": // description: response when deleting a variable // "204": diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 7e7e14813cfba..2492123d78248 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -155,11 +155,7 @@ func GetVariable(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": ”#/responses/ActionVariable“ - // "201": - // description: response when getting a variable - // "204": - // description: response when getting a variable + // "$ref": "#/responses/ActionVariable" // "400": // "$ref": "#/responses/error" // "404": @@ -212,7 +208,7 @@ func DeleteVariable(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": ”#/responses/ActionVariable“ + // "$ref": "#/responses/ActionVariable" // "201": // description: response when deleting a variable // "204": diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index 37717807180ac..a79c58e0589c7 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -18,3 +18,10 @@ type swaggerResponseSecret struct { // in:body Body api.Secret `json:"body"` } + +// ActionVariable +// swagger:response ActionVariable +type swaggerResponseActionVariable struct { + // in:body + Body api.ActionVariable `json:"body"` +} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index eac30f8a5431f..858a7bf6336ea 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -274,11 +274,7 @@ func GetVariable(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": ”#/responses/ActionVariable“ - // "201": - // description: response when deleting a variable - // "204": - // description: response when deleting a variable + // "$ref": "#/responses/ActionVariable" // "400": // "$ref": "#/responses/error" // "404": diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 84db63341e057..a940ca0704338 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1872,13 +1872,7 @@ ], "responses": { "200": { - "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" - }, - "201": { - "description": "response when getting a variable" - }, - "204": { - "description": "response when getting a variable" + "$ref": "#/responses/ActionVariable" }, "400": { "$ref": "#/responses/error" @@ -2015,7 +2009,7 @@ ], "responses": { "200": { - "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + "$ref": "#/responses/ActionVariable" }, "201": { "description": "response when deleting a variable" @@ -3946,13 +3940,7 @@ ], "responses": { "200": { - "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" - }, - "201": { - "description": "response when getting a variable" - }, - "204": { - "description": "response when getting a variable" + "$ref": "#/responses/ActionVariable" }, "400": { "$ref": "#/responses/error" @@ -4104,7 +4092,7 @@ ], "responses": { "200": { - "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" + "$ref": "#/responses/ActionVariable" }, "201": { "description": "response when deleting a variable" @@ -15466,13 +15454,7 @@ ], "responses": { "200": { - "$ref": "%E2%80%9D#/responses/ActionVariable%E2%80%9C" - }, - "201": { - "description": "response when deleting a variable" - }, - "204": { - "description": "response when deleting a variable" + "$ref": "#/responses/ActionVariable" }, "400": { "$ref": "#/responses/error" @@ -17745,6 +17727,31 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionVariable": { + "description": "ActionVariable return value of the query API", + "type": "object", + "properties": { + "data": { + "type": "string", + "x-go-name": "Data" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "owner_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OwnerID" + }, + "repo_id": { + "type": "integer", + "format": "int64", + "x-go-name": "RepoID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Activity": { "type": "object", "properties": { @@ -24354,6 +24361,12 @@ } } }, + "ActionVariable": { + "description": "ActionVariable", + "schema": { + "$ref": "#/definitions/ActionVariable" + } + }, "ActivityFeedsList": { "description": "ActivityFeedsList", "schema": { @@ -25411,4 +25424,4 @@ "TOTPHeader": [] } ] -} +} \ No newline at end of file From a4c633ebe3e17705c63a972f50adf61dafc09e1e Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 12 Mar 2024 15:41:03 +0800 Subject: [PATCH 09/18] chore: add new line at end of swgger file --- templates/swagger/v1_json.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a940ca0704338..59d6faa09c728 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -25424,4 +25424,4 @@ "TOTPHeader": [] } ] -} \ No newline at end of file +} From 512d0621e3c0ea490d95d5c6bf839b4e530d80eb Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Wed, 13 Mar 2024 18:25:47 +0800 Subject: [PATCH 10/18] feat: list variables --- routers/api/v1/api.go | 3 + routers/api/v1/org/variables.go | 54 ++++++++++++ routers/api/v1/repo/action.go | 58 +++++++++++++ routers/api/v1/swagger/action.go | 7 ++ routers/api/v1/user/action.go | 49 +++++++++++ templates/swagger/v1_json.tmpl | 141 +++++++++++++++++++++++++++++++ 6 files changed, 312 insertions(+) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e91b4156d1648..e870378c4b247 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -956,6 +956,7 @@ func Routes() *web.Route { }) m.Group("/variables", func() { + m.Get("", user.ListVariables) m.Combo("/{variablename}"). Get(user.GetVariable). Delete(user.DeleteVariable). @@ -1082,6 +1083,7 @@ func Routes() *web.Route { }) m.Group("/variables", func() { + m.Get("", reqToken(), reqOwner(), repo.ListVariables) m.Combo("/{variablename}"). Get(reqToken(), reqOwner(), repo.GetVariable). Delete(reqToken(), reqOwner(), repo.DeleteVariable). @@ -1469,6 +1471,7 @@ func Routes() *web.Route { }) m.Group("/variables", func() { + m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables) m.Combo("/{variablename}"). Get(reqToken(), reqOrgOwnership(), org.GetVariable). Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go index 7a2a9bd745be3..eaf7bdc45ba0f 100644 --- a/routers/api/v1/org/variables.go +++ b/routers/api/v1/org/variables.go @@ -8,13 +8,67 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" ) +// ListVariables list org-level variables +func ListVariables(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList + // --- + // summary: Get an org-level variables list + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/VariableList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindVariables", err) + return + } + + 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, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} + // GetVariable get an org-level variable func GetVariable(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 2492123d78248..e6504362c08c1 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -8,9 +8,11 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" @@ -365,3 +367,59 @@ func UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +// ListVariables list repo-level variables +func ListVariables(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList + // --- + // summary: Get repo-level variables list + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/VariableList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindVariables", err) + return + } + + variables := make([]*api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index a79c58e0589c7..665f4d0b85245 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -25,3 +25,10 @@ type swaggerResponseActionVariable struct { // in:body Body api.ActionVariable `json:"body"` } + +// VariableList +// swagger:response VariableList +type swaggerResponseVariableList struct { + // in:body + Body []api.ActionVariable `json:"body"` +} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 858a7bf6336ea..bf78c2c86493a 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -8,9 +8,11 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" @@ -302,3 +304,50 @@ func GetVariable(ctx *context.APIContext) { ctx.JSON(http.StatusOK, variable) } + +// ListVariables list user-level variables +func ListVariables(ctx *context.APIContext) { + // swagger:operation GET /user/actions/variables user getUserVariablesList + // --- + // summary: Get the user-level list of variables which is created by current doer + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/VariableList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{ + OwnerID: ctx.Doer.ID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindVariables", err) + return + } + + 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, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 59d6faa09c728..3c2f1cd542b0f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1844,6 +1844,50 @@ } } }, + "/orgs/{org}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variables list", + "operationId": "getOrgVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/actions/variables/{variablename}": { "get": { "produces": [ @@ -3905,6 +3949,57 @@ } } }, + "/repos/{owner}/{repo}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get repo-level variables list", + "operationId": "getRepoVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/variables/{variablename}": { "get": { "produces": [ @@ -15433,6 +15528,43 @@ } } }, + "/user/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get the user-level list of variables which is created by current doer", + "operationId": "getUserVariablesList", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/actions/variables/{variablename}": { "get": { "produces": [ @@ -25250,6 +25382,15 @@ } } }, + "VariableList": { + "description": "VariableList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionVariable" + } + } + }, "WatchInfo": { "description": "WatchInfo", "schema": { From 73f483c1ae9eb272568be4b6abeb4f9a82a0aac6 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Thu, 14 Mar 2024 09:13:21 +0800 Subject: [PATCH 11/18] chore: tab to space --- routers/api/v1/repo/action.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index e6504362c08c1..03321d956d7cb 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -396,7 +396,7 @@ func ListVariables(ctx *context.APIContext) { // type: integer // responses: // "200": - // "$ref": "#/responses/VariableList" + // "$ref": "#/responses/VariableList" // "400": // "$ref": "#/responses/error" // "404": From 1806ad7ca2de34204e1a7b8bbc5a233c5ece16e2 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Mon, 25 Mar 2024 18:33:16 +0800 Subject: [PATCH 12/18] fix: no check null for owner_id and repo_id --- models/actions/variable.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/models/actions/variable.go b/models/actions/variable.go index 1b40295d94e38..b0a455e675756 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -58,12 +58,11 @@ type FindVariablesOpts struct { func (opts FindVariablesOpts) ToConds() builder.Cond { cond := builder.NewCond() - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) - } - if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } + // Since we now support instance-level variables, + // there is no need to check for null values for `owner_id` and `repo_id` + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + if opts.Name != "" { cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) } From bf807a6477630c17d885eb374ed8ee0bfb958680 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 26 Mar 2024 14:53:13 +0800 Subject: [PATCH 13/18] chore: add integration test --- tests/integration/api_repo_variables_test.go | 149 +++++++++++++++++++ tests/integration/api_user_variables_test.go | 144 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 tests/integration/api_repo_variables_test.go create mode 100644 tests/integration/api_user_variables_test.go diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go new file mode 100644 index 0000000000000..bb833e1bf5c2d --- /dev/null +++ b/tests/integration/api_repo_variables_test.go @@ -0,0 +1,149 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIRepoVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var#test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go new file mode 100644 index 0000000000000..e0fba1a26e0a7 --- /dev/null +++ b/tests/integration/api_user_variables_test.go @@ -0,0 +1,144 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIUserVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var#test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} From da1863601286b7b0fe3beab8f4fcdb7b777e5b03 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 26 Mar 2024 15:03:52 +0800 Subject: [PATCH 14/18] chore: add comment for opts --- modules/structs/variable.go | 12 ++++++++---- templates/swagger/v1_json.tmpl | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/structs/variable.go b/modules/structs/variable.go index db89d56431de9..951b0bc1656ef 100644 --- a/modules/structs/variable.go +++ b/modules/structs/variable.go @@ -26,8 +26,12 @@ type UpdateVariableOption struct { // ActionVariable return value of the query API // swagger:model type ActionVariable struct { - OwnerID int64 `json:"owner_id"` - RepoID int64 `json:"repo_id"` - Name string `json:"name"` - Data string `json:"data"` + // the owner to which the variable belongs + OwnerID int64 `json:"owner_id"` + // the repository to which the variable belongs + RepoID int64 `json:"repo_id"` + // the name of the variable + Name string `json:"name"` + // the value of the varibale + Data string `json:"data"` } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8e8daed93faec..e9842917fd8d4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17864,19 +17864,23 @@ "type": "object", "properties": { "data": { + "description": "the value of the varibale", "type": "string", "x-go-name": "Data" }, "name": { + "description": "the name of the variable", "type": "string", "x-go-name": "Name" }, "owner_id": { + "description": "the owner to which the variable belongs", "type": "integer", "format": "int64", "x-go-name": "OwnerID" }, "repo_id": { + "description": "the repository to which the variable belongs", "type": "integer", "format": "int64", "x-go-name": "RepoID" From 6e21046968c707e3ea6728a0d59c05c70c9425f6 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 26 Mar 2024 15:15:48 +0800 Subject: [PATCH 15/18] fix: misspelling --- modules/structs/variable.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/structs/variable.go b/modules/structs/variable.go index 951b0bc1656ef..cc846cf0ec309 100644 --- a/modules/structs/variable.go +++ b/modules/structs/variable.go @@ -32,6 +32,6 @@ type ActionVariable struct { RepoID int64 `json:"repo_id"` // the name of the variable Name string `json:"name"` - // the value of the varibale + // the value of the variable Data string `json:"data"` } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e9842917fd8d4..186148df665f5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17864,7 +17864,7 @@ "type": "object", "properties": { "data": { - "description": "the value of the varibale", + "description": "the value of the variable", "type": "string", "x-go-name": "Data" }, From 75d96f1c46f77ef723fbab5fc76f1e0f8d3c6cc3 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Tue, 26 Mar 2024 16:11:36 +0800 Subject: [PATCH 16/18] fix: test case --- tests/integration/api_repo_variables_test.go | 2 +- tests/integration/api_user_variables_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go index bb833e1bf5c2d..7847962b07d89 100644 --- a/tests/integration/api_repo_variables_test.go +++ b/tests/integration/api_repo_variables_test.go @@ -54,7 +54,7 @@ func TestAPIRepoVariables(t *testing.T) { ExpectedStatus: http.StatusBadRequest, }, { - Name: "var#test", + Name: "var@test", ExpectedStatus: http.StatusBadRequest, }, { diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go index e0fba1a26e0a7..dd5501f0b9a29 100644 --- a/tests/integration/api_user_variables_test.go +++ b/tests/integration/api_user_variables_test.go @@ -49,7 +49,7 @@ func TestAPIUserVariables(t *testing.T) { ExpectedStatus: http.StatusBadRequest, }, { - Name: "var#test", + Name: "var@test", ExpectedStatus: http.StatusBadRequest, }, { From 48f23c98f3f58233bf14841a38cc51634ca89d5d Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Wed, 27 Mar 2024 09:45:53 +0800 Subject: [PATCH 17/18] chore: mv func to util pkg --- modules/util/util.go | 9 +++++++++ modules/util/util_test.go | 5 +++++ routers/web/shared/secrets/secrets.go | 4 ++-- services/actions/variables.go | 13 ++----------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/modules/util/util.go b/modules/util/util.go index c94fb910471c7..b6e730eb54bbd 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -221,3 +221,12 @@ func IfZero[T comparable](v, def T) T { } return v } + +func ReserveLineBreakForTextarea(input string) string { + // Since the content is from a form which is a textarea, the line endings are \r\n. + // It's a standard behavior of HTML. + // But we want to store them as \n like what GitHub does. + // And users are unlikely to really need to keep the \r. + // Other than this, we should respect the original content, even leading or trailing spaces. + return strings.ReplaceAll(input, "\r\n", "\n") +} diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 819e12ee91fff..5c5b13d04b4fc 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -235,3 +235,8 @@ func TestToPointer(t *testing.T) { val123 := 123 assert.False(t, &val123 == ToPointer(val123)) } + +func TestReserveLineBreakForTextarea(t *testing.T) { + assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata") + assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n") +} diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index 8c99b20a69621..3bd421f86ab5c 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -7,8 +7,8 @@ import ( "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" secret_service "code.gitea.io/gitea/services/secrets" @@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { 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, actions_service.ReserveLineBreakForTextarea(form.Data)) + s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data)) 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 eb98fc068c9d4..8dde9c4af5c16 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -23,7 +23,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin return nil, err } - v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, ReserveLineBreakForTextarea(data)) + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data)) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func UpdateVariable(ctx context.Context, variableID int64, name, data string) (b return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ ID: variableID, Name: strings.ToUpper(name), - Data: ReserveLineBreakForTextarea(data), + Data: util.ReserveLineBreakForTextarea(data), }) } @@ -98,12 +98,3 @@ func envNameCIRegexMatch(name string) error { } return nil } - -func ReserveLineBreakForTextarea(input string) string { - // Since the content is from a form which is a textarea, the line endings are \r\n. - // It's a standard behavior of HTML. - // But we want to store them as \n like what GitHub does. - // And users are unlikely to really need to keep the \r. - // Other than this, we should respect the original content, even leading or trailing spaces. - return strings.ReplaceAll(input, "\r\n", "\n") -} From dec6da375dd728b4a4b1f4bb5ca92bfe047cfef8 Mon Sep 17 00:00:00 2001 From: sillyguodong Date: Wed, 27 Mar 2024 09:53:24 +0800 Subject: [PATCH 18/18] chore: re-run actions