From ee5d35490f5b4454d0eca7e8a75ce0ae9a66f3ba Mon Sep 17 00:00:00 2001 From: yystopf Date: Mon, 1 Dec 2025 19:29:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9Asecret=E5=92=8Cvaria?= =?UTF-8?q?ble=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.go | 2 +- models/actions/secret.go | 59 +++++++ models/actions/variables.go | 65 +++++++ modules/structs/secret.go | 34 ++++ modules/structs/variable.go | 36 ++++ modules/util/util.go | 12 ++ routers/hat/hat.go | 33 +++- routers/hat/org/action.go | 261 ++++++++++++++++++++++++++++ routers/hat/repo/actions/actions.go | 223 +++++++++++++++++++++++- services/actions/secrets.go | 105 +++++++++++ services/actions/variables.go | 69 ++++++++ 11 files changed, 893 insertions(+), 6 deletions(-) create mode 100644 models/actions/secret.go create mode 100644 models/actions/variables.go create mode 100644 modules/structs/secret.go create mode 100644 modules/structs/variable.go create mode 100644 modules/util/util.go create mode 100644 routers/hat/org/action.go create mode 100644 services/actions/secrets.go create mode 100644 services/actions/variables.go diff --git a/main.go b/main.go index 0a99b0e..2a95a1a 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( ) var ( - Version = "v2.8.6, by v1.21.0, 20251111 " + Version = "v2.8.7, by v1.21.0, 20251201 " Tags = "" MakeVersion = "" ) diff --git a/models/actions/secret.go b/models/actions/secret.go new file mode 100644 index 0000000..737da74 --- /dev/null +++ b/models/actions/secret.go @@ -0,0 +1,59 @@ +package actions + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" + "xorm.io/builder" +) + +type FindSecretsOptions struct { + db.ListOptions + OwnerID int64 + RepoID int64 + SecretID int64 + Name string +} + +func (opts *FindSecretsOptions) 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}) + } + if opts.SecretID != 0 { + cond = cond.And(builder.Eq{"id": opts.SecretID}) + } + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } + + return cond +} + +func FindSecrets(ctx context.Context, opts FindSecretsOptions) ([]*secret_model.Secret, error) { + var secrets []*secret_model.Secret + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return secrets, sess. + Where(opts.toConds()). + OrderBy("created_unix desc"). + Find(&secrets) +} + +func CountSecrets(ctx context.Context, opts *FindSecretsOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toConds()).Count(new(secret_model.Secret)) +} + +func DeleteSecret(ctx context.Context, secret *secret_model.Secret) error { + if _, err := db.DeleteByID(ctx, secret.ID, secret); err != nil { + return err + } + return nil +} diff --git a/models/actions/variables.go b/models/actions/variables.go new file mode 100644 index 0000000..ebe340a --- /dev/null +++ b/models/actions/variables.go @@ -0,0 +1,65 @@ +package actions + +import ( + "context" + "strings" + + gitea_actions "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "xorm.io/builder" +) + +type FindVariablesOpts struct { + db.ListOptions + IDs []int64 + RepoID int64 + OwnerID int64 // it will be ignored if RepoID is set + Name string +} + +func (opts FindVariablesOpts) ToConds() builder.Cond { + cond := builder.NewCond() + if len(opts.IDs) > 0 { + if len(opts.IDs) == 1 { + cond = cond.And(builder.Eq{"id": opts.IDs[0]}) + } else { + cond = cond.And(builder.In("id", opts.IDs)) + } + } + + // 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{"repo_id": opts.RepoID}) + if opts.RepoID != 0 { // if RepoID is set + // ignore OwnerID and treat it as 0 + cond = cond.And(builder.Eq{"owner_id": 0}) + } else { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } + return cond +} + +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*gitea_actions.ActionVariable, error) { + var variables []*gitea_actions.ActionVariable + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return variables, sess.Where(opts.ToConds()).OrderBy("created_unix desc").Find(&variables) +} + +func CountVariables(ctx context.Context, opts FindVariablesOpts) (int64, error) { + sess := db.GetEngine(ctx) + return sess.Where(opts.ToConds()).Count(new(gitea_actions.ActionVariable)) +} + +func DeleteVariable(ctx context.Context, variable *gitea_actions.ActionVariable) error { + if _, err := db.DeleteByID(ctx, variable.ID, variable); err != nil { + return err + } + return nil +} diff --git a/modules/structs/secret.go b/modules/structs/secret.go new file mode 100644 index 0000000..c7b38d1 --- /dev/null +++ b/modules/structs/secret.go @@ -0,0 +1,34 @@ +package structs + +import "time" + +// CreateVariableOption the option when creating variable +// swagger:model +type CreateSecretOption struct { + // Value of the variable to create + // + // required: true + Data string `json:"data" binding:"Required"` +} + +// UpdateVariableOption the option when updating variable +// swagger:model +type UpdateSecretOption 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 + Data string `json:"data" binding:"Required"` + + // Description of the variable to update +} + +// Secret represents a secret +// swagger:model +type Secret struct { + // the secret's name + Name string `json:"name"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` +} diff --git a/modules/structs/variable.go b/modules/structs/variable.go new file mode 100644 index 0000000..034523a --- /dev/null +++ b/modules/structs/variable.go @@ -0,0 +1,36 @@ +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 +// 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"` + + // Description of the variable to update +} + +// ActionVariable return value of the query API +// swagger:model +type ActionVariable struct { + // 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 variable + Data string `json:"data"` +} diff --git a/modules/util/util.go b/modules/util/util.go new file mode 100644 index 0000000..c301b67 --- /dev/null +++ b/modules/util/util.go @@ -0,0 +1,12 @@ +package util + +import "strings" + +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/hat/hat.go b/routers/hat/hat.go index 55218ef..91611ba 100644 --- a/routers/hat/hat.go +++ b/routers/hat/hat.go @@ -133,7 +133,21 @@ func Routers() *web.Route { m.Get("/code", context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode), repo.Code) m.Combo("").Delete(reqToken(), reqOwner(), repo.Delete) m.Group("/actions", func() { - m.Get("/secrets", context.ReferencesGitRepo(), actions.ListActionsSecrets) + m.Group("/secrets", func() { + m.Get("", reqToken(), reqAdmin(), actions.ListActionsSecrets) + m.Combo("/{secretname}"). + Delete(reqToken(), reqAdmin(), actions.DeleteActionsSecret). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateSecretOption{}), actions.CreateActionsSecret). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateSecretOption{}), actions.UpdateActionsSecret) + }) + m.Group("/variables", func() { + m.Get("", reqToken(), reqAdmin(), actions.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqAdmin(), actions.GetVariable). + Delete(reqToken(), reqAdmin(), actions.DeleteVariable). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateVariableOption{}), actions.CreateVariable). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateVariableOption{}), actions.UpdateVariable) + }) m.Get("", context.ReferencesGitRepo(), actions.ListActions) m.Post("/disable", reqAdmin(), actions.DisableWorkflowFile) m.Post("/enable", reqAdmin(), actions.EnableWorkflowFile) @@ -253,6 +267,23 @@ func Routers() *web.Route { m.Post("/orgs", reqToken(), bind(gitea_api.CreateOrgOption{}), org.Create) m.Group("/orgs/{org}", func() { m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(hat_api.EditOrgOption{}), org.Edit) + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqAdmin(), org.ListActionsSecrets) + m.Combo("/{secretname}"). + Delete(reqToken(), reqAdmin(), org.DeleteActionsSecret). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateSecretOption{}), org.CreateActionsSecret). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateSecretOption{}), org.UpdateActionsSecret) + }) + m.Group("/variables", func() { + m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqOrgOwnership(), org.GetVariable). + Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). + Post(reqToken(), reqOrgOwnership(), bind(hat_api.CreateVariableOption{}), org.CreateVariable). + Put(reqToken(), reqOrgOwnership(), bind(hat_api.UpdateVariableOption{}), org.UpdateVariable) + }) + }) }, orgAssignment(true)) m.Group("/teams/{teamid}", func() { diff --git a/routers/hat/org/action.go b/routers/hat/org/action.go new file mode 100644 index 0000000..7e06361 --- /dev/null +++ b/routers/hat/org/action.go @@ -0,0 +1,261 @@ +package org + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/context" + 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" + + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" + hat_actions_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/actions" +) + +func DeleteActionsSecret(ctx *context.APIContext) { + if err := hat_actions_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecretByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateSecretOption) + ownerID := ctx.Org.Organization.ID + secretName := ctx.Params("secretname") + + v, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + Name: secretName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("secret name %s already exists", secretName)) + return + } + if _, err := hat_actions_service.CreateSecret(ctx, ownerID, 0, secretName, opt.Data); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + ctx.Status(http.StatusCreated) +} + +func UpdateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateSecretOption) + + _, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("secretname"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("secretname") + } + + _, err = hat_actions_service.UpdateSecret(ctx, ctx.Org.Organization.ID, 0, opt.Name, opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) + +} + +func ListActionsSecrets(ctx *context.APIContext) { + opts := &hat_actions_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + count, err := hat_actions_model.CountSecrets(ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + secrets, err := hat_actions_model.FindSecrets(ctx, *opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +func GetVariable(ctx *context.APIContext) { + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + variable := &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +func DeleteVariable(ctx *context.APIContext) { + if err := hat_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.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateVariableOption) + ownerID := ctx.Org.Organization.ID + variableName := ctx.Params("variablename") + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ownerID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := hat_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.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +func UpdateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateVariableOption) + + v, err := hat_actions_service.GetVariable(ctx, hat_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.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := hat_actions_service.UpdateVariableNameData(ctx, v); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariableNameData", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ListVariables(ctx *context.APIContext) { + opts := hat_actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + vars, err := hat_actions_model.FindVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + count, err := hat_actions_model.CountVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + variables := make([]*hat_api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &hat_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/routers/hat/repo/actions/actions.go b/routers/hat/repo/actions/actions.go index b44e472..7e56bb1 100644 --- a/routers/hat/repo/actions/actions.go +++ b/routers/hat/repo/actions/actions.go @@ -15,7 +15,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/actions" @@ -32,24 +31,103 @@ import ( actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/convert" hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" + hat_actions_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/actions" jobparser "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" "xorm.io/builder" ) +func DeleteActionsSecret(ctx *context.APIContext) { + if err := hat_actions_service.DeleteSecretByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("secretname")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecretByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateSecretOption) + repoID := ctx.Repo.Repository.ID + secretName := ctx.Params("secretname") + + v, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + RepoID: repoID, + Name: secretName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("secret name %s already exists", secretName)) + return + } + if _, err := hat_actions_service.CreateSecret(ctx, 0, repoID, secretName, opt.Data); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + ctx.Status(http.StatusCreated) +} + +func UpdateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateSecretOption) + + _, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("secretname"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("secretname") + } + + _, err = hat_actions_service.UpdateSecret(ctx, 0, ctx.Repo.Repository.ID, opt.Name, opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) + +} + func ListActionsSecrets(ctx *context.APIContext) { - opts := &secret_model.FindSecretsOptions{ + opts := &hat_actions_model.FindSecretsOptions{ RepoID: ctx.Repo.Repository.ID, ListOptions: utils.GetListOptions(ctx), } - count, err := secret_model.CountSecrets(ctx, opts) + count, err := hat_actions_model.CountSecrets(ctx, opts) if err != nil { ctx.InternalServerError(err) return } - secrets, err := secret_model.FindSecrets(ctx, *opts) + secrets, err := hat_actions_model.FindSecrets(ctx, *opts) if err != nil { ctx.InternalServerError(err) return @@ -67,6 +145,143 @@ func ListActionsSecrets(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiSecrets) } +func GetVariable(ctx *context.APIContext) { + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + variable := &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +func DeleteVariable(ctx *context.APIContext) { + if err := hat_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.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateVariableOption) + repoID := ctx.Repo.Repository.ID + variableName := ctx.Params("variablename") + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + RepoID: repoID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := hat_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.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +func UpdateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateVariableOption) + + v, err := hat_actions_service.GetVariable(ctx, hat_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.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := hat_actions_service.UpdateVariableNameData(ctx, v); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariableNameData", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ListVariables(ctx *context.APIContext) { + opts := hat_actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + ListOptions: utils.GetListOptions(ctx), + } + vars, err := hat_actions_model.FindVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + count, err := hat_actions_model.CountVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + variables := make([]*hat_api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} + func Run(ctx *context.APIContext) { workflow := ctx.FormString("workflow") ref := ctx.FormString("ref") diff --git a/services/actions/secrets.go b/services/actions/secrets.go new file mode 100644 index 0000000..44dd92d --- /dev/null +++ b/services/actions/secrets.go @@ -0,0 +1,105 @@ +package actions + +import ( + "context" + "regexp" + "strings" + "sync" + + secret_model "code.gitea.io/gitea/models/secret" + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + + "code.gitea.io/gitea/modules/util" +) + +var globalVars = sync.OnceValue(func() (ret struct { + namePattern, forbiddenPrefixPattern *regexp.Regexp +}, +) { + ret.namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") + ret.forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_") + return ret +}) + +func ValidateName(name string) error { + vars := globalVars() + if !vars.namePattern.MatchString(name) || + vars.forbiddenPrefixPattern.MatchString(name) || + strings.EqualFold(name, "CI") /* CI is always set to true in GitHub Actions*/ { + return util.NewInvalidArgumentErrorf("invalid variable or secret name") + } + return nil +} + +func CreateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, error) { + if err := ValidateName(name); err != nil { + return nil, err + } + + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data) + if err != nil { + return nil, err + } + + return s, nil +} + +func UpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (bool, error) { + if err := ValidateName(name); err != nil { + return false, err + } + + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return false, err + } + + if s == nil { + return false, util.NewNotExistErrorf("secret not found") + } + + err = secret_model.UpdateSecret(ctx, s.ID, data) + if err != nil { + return false, err + } + + return true, nil +} + +func GetSecret(ctx context.Context, opts hat_actions_model.FindSecretsOptions) (*secret_model.Secret, error) { + vars, err := hat_actions_model.FindSecrets(ctx, opts) + if err != nil { + return nil, err + } + if len(vars) != 1 { + return nil, util.NewNotExistErrorf("variable not found") + } + return vars[0], nil +} + +func DeleteSecretByName(ctx context.Context, ownerID, repoID int64, name string) error { + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return err + } + + return hat_actions_model.DeleteSecret(ctx, s) +} + +func DeleteSecretByID(ctx context.Context, secretID int64) error { + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + SecretID: secretID, + }) + if err != nil { + return err + } + return hat_actions_model.DeleteSecret(ctx, s) +} diff --git a/services/actions/variables.go b/services/actions/variables.go new file mode 100644 index 0000000..31b8cd1 --- /dev/null +++ b/services/actions/variables.go @@ -0,0 +1,69 @@ +package actions + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/util" + + secret_service "code.gitea.io/gitea/services/secrets" + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_util "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/util" +) + +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 + } + + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, hat_util.ReserveLineBreakForTextarea(data)) + if err != nil { + return nil, err + } + + return v, nil +} + +func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { + if err := secret_service.ValidateName(variable.Name); err != nil { + return false, err + } + + variable.Data = hat_util.ReserveLineBreakForTextarea(variable.Data) + + return actions_model.UpdateVariable(ctx, variable) +} + +func DeleteVariableByID(ctx context.Context, variableID int64) error { + v, err := GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + IDs: []int64{variableID}, + }) + if err != nil { + return err + } + return hat_actions_model.DeleteVariable(ctx, v) +} + +func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error { + v, err := GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return err + } + + return hat_actions_model.DeleteVariable(ctx, v) +} + +func GetVariable(ctx context.Context, opts hat_actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) { + vars, err := hat_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 +}