diff --git a/models/actions/run.go b/models/actions/run.go index 4656aa22a2933..a036775b962d0 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -55,6 +55,7 @@ type ActionRun struct { func init() { db.RegisterModel(new(ActionRun)) db.RegisterModel(new(ActionRunIndex)) + db.RegisterModel(new(ActionWorkflow)) } func (run *ActionRun) HTMLURL() string { diff --git a/models/actions/workflow.go b/models/actions/workflow.go new file mode 100644 index 0000000000000..6484b0ca6fe1b --- /dev/null +++ b/models/actions/workflow.go @@ -0,0 +1,176 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "sort" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" +) + +type WorkflowBranch struct { + Branch string + Labels []string + ErrMsg string +} + +type ActionWorkflow struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + Name string + Branchs []*WorkflowBranch `xorm:"JSON TEXT"` + ErrMsg string `json:"-"` // for ui logic +} + +func (w *ActionWorkflow) LoadLabels() []string { + labelsSet := make(container.Set[string]) + + for _, b := range w.Branchs { + for _, l := range b.Labels { + labelsSet.AddMultiple(l) + } + } + + labels := labelsSet.Values() + sort.Slice(labels, func(i, j int) bool { + return labels[i] < labels[j] + }) + + return labels +} + +func (w *ActionWorkflow) UpdateBranchLabels(branch string, labels []string) { + if w.Branchs == nil { + w.Branchs = make([]*WorkflowBranch, 0, 5) + } + + for _, b := range w.Branchs { + if b.Branch == branch { + b.Labels = labels + return + } + } + + w.Branchs = append(w.Branchs, &WorkflowBranch{Branch: branch, Labels: labels}) +} + +func (w *ActionWorkflow) UpdateBranchErrMsg(branch string, errMsg error) { + if w.Branchs == nil { + w.Branchs = make([]*WorkflowBranch, 0, 5) + } + + for _, b := range w.Branchs { + if b.Branch == branch { + if errMsg == nil { + b.ErrMsg = "" + } else { + b.ErrMsg = errMsg.Error() + } + return + } + } + + w.Branchs = append(w.Branchs, &WorkflowBranch{Branch: branch, ErrMsg: errMsg.Error()}) +} + +func (w *ActionWorkflow) DeleteBranch(branch string) { + for index, b := range w.Branchs { + if b.Branch == branch { + w.Branchs = append(w.Branchs[:index], w.Branchs[index+1:]...) + break + } + } +} + +func (w *ActionWorkflow) BranchNum() int { + return len(w.Branchs) +} + +func UpdateWorkFlowErrMsg(ctx context.Context, repoID int64, workflow, branch string, err error) error { + return updateWorkFlowLabelsOrErrmsg(ctx, repoID, workflow, branch, nil, err) +} + +func UpdateWorkFlowLabels(ctx context.Context, repoID int64, workflow, branch string, labels []string) error { + return updateWorkFlowLabelsOrErrmsg(ctx, repoID, workflow, branch, labels, nil) +} + +func updateWorkFlowLabelsOrErrmsg(ctx context.Context, repoID int64, workflow, branch string, labels []string, errMsg error) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + w := &ActionWorkflow{ + RepoID: repoID, + Name: workflow, + } + has, err := db.GetEngine(ctx).Get(w) + if err != nil { + return err + } + + if len(labels) > 0 { + w.UpdateBranchLabels(branch, labels) + } + + w.UpdateBranchErrMsg(branch, errMsg) + + if has { + _, err = db.GetEngine(ctx).Cols("branchs").ID(w.ID).Update(w) + } else { + _, err = db.GetEngine(ctx).Insert(w) + } + if err != nil { + return err + } + + return committer.Commit() +} + +func DeleteWorkFlowBranch(ctx context.Context, repoID int64, workflow, branch string) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + w := &ActionWorkflow{ + RepoID: repoID, + Name: workflow, + } + has, err := db.GetEngine(ctx).Get(w) + if err != nil { + return err + } + if !has { + return nil + } + + w.DeleteBranch(branch) + + if w.BranchNum() == 0 { + _, err = db.GetEngine(ctx).ID(w.ID).Delete(w) + } else { + _, err = db.GetEngine(ctx).Cols("branchs").ID(w.ID).Update(w) + } + if err != nil { + return err + } + + return committer.Commit() +} + +func ListWorkflowBranchs(ctx context.Context, repoID int64) ([]*ActionWorkflow, error) { + result := make([]*ActionWorkflow, 0, 10) + + err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&result) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/models/actions/workflow_test.go b/models/actions/workflow_test.go new file mode 100644 index 0000000000000..74f3fdf0e3ecc --- /dev/null +++ b/models/actions/workflow_test.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestWorkFlowLabels(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // test update + err := UpdateWorkFlowLabels(db.DefaultContext, 2, "test-workflow-2.yml", "test1", []string{"aa", "bb"}) + assert.NoError(t, err) + + w := unittest.AssertExistsAndLoadBean(t, &ActionWorkflow{RepoID: 2, Name: "test-workflow-2.yml"}) + assert.EqualValues(t, []string{"aa", "bb"}, w.LoadLabels()) + + err = UpdateWorkFlowLabels(db.DefaultContext, 2, "test-workflow-2.yml", "test2", []string{"aa", "bb", "cccc"}) + assert.NoError(t, err) + + w = unittest.AssertExistsAndLoadBean(t, &ActionWorkflow{RepoID: 2, Name: "test-workflow-2.yml"}) + assert.EqualValues(t, []string{"aa", "bb", "cccc"}, w.LoadLabels()) + + err = UpdateWorkFlowLabels(db.DefaultContext, 2, "test-workflow-2.yml", "test2", []string{"aa", "bb", "dddd"}) + assert.NoError(t, err) + + w = unittest.AssertExistsAndLoadBean(t, &ActionWorkflow{RepoID: 2, Name: "test-workflow-2.yml"}) + assert.EqualValues(t, []string{"aa", "bb", "dddd"}, w.LoadLabels()) + + // test delete + err = DeleteWorkFlowBranch(db.DefaultContext, 2, "test-workflow-2.yml", "test2") + assert.NoError(t, err) + w = unittest.AssertExistsAndLoadBean(t, &ActionWorkflow{RepoID: 2, Name: "test-workflow-2.yml"}) + assert.EqualValues(t, []string{"aa", "bb"}, w.LoadLabels()) + + err = DeleteWorkFlowBranch(db.DefaultContext, 2, "test-workflow-2.yml", "test1") + assert.NoError(t, err) + unittest.AssertNotExistsBean(t, &ActionWorkflow{RepoID: 2, Name: "test-workflow-2.yml"}) +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 408fdb8f8ef22..bc0db3355cebd 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,9 +5,13 @@ package actions import ( "bytes" + "context" "io" "strings" + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -654,3 +658,65 @@ func matchPackageEvent(commit *git.Commit, payload *api.PackagePayload, evt *job } return matchTimes == len(evt.Acts()) } + +func UpdateWorkflowBranchLabelsForBranch(ctx context.Context, gitRepo *git.Repository, repo *repo_model.Repository, branch string) error { + commit, err := gitRepo.GetBranchCommit(branch) + if err != nil { + return err + } + + entries, err := ListWorkflows(commit) + if err != nil { + return err + } + + for _, entry := range entries { + content, err := GetContentFromEntry(entry) + if err != nil { + return err + } + wf, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + err = actions_model.UpdateWorkFlowErrMsg(ctx, repo.ID, entry.Name(), branch, err) + if err != nil { + log.Error("actions_model.UpdateWorkFlowErrMsg: %v", err) + } + continue + } + + // Check whether have matching runner + labelsSet := make(container.Set[string]) + + for _, j := range wf.Jobs { + labelsSet.AddMultiple(j.RunsOn()...) + } + + err = actions_model.UpdateWorkFlowLabels(ctx, repo.ID, entry.Name(), branch, labelsSet.Values()) + if err != nil { + log.Error("actions_model.UpdateWorkFlowLabels: %v", err) + } + } + + return nil +} + +func DeleteWorkflowBranchLabelsForBranch(ctx context.Context, gitRepo *git.Repository, repo *repo_model.Repository, oldCommitID, branch string) error { + oldCommit, err := gitRepo.GetCommit(oldCommitID) + if err != nil { + return err + } + + entries, err := ListWorkflows(oldCommit) + if err != nil { + return err + } + + for _, entry := range entries { + err = actions_model.DeleteWorkFlowBranch(ctx, repo.ID, entry.Name(), branch) + if err != nil { + log.Error("actions_model.UpdateWorkFlowLabels: %v", err) + } + } + + return nil +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9c582a885013d..459a17c9676f4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3523,7 +3523,8 @@ runs.commit = Commit runs.scheduled = Scheduled runs.pushed_by = pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s -runs.no_matching_runner_helper = No matching runner: %s +runs.invalid_workflow_helper_branch = Workflow config file on branch %s is invalid. Please check your config file: %s +runs.no_matching_runner_helper = No matching runner on branch %s : %s runs.actor = Actor runs.status = Status runs.actors_no_select = All actors diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index fd541647b716e..9c8cea027a7d2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -4,15 +4,12 @@ package actions import ( - "bytes" "fmt" "net/http" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" @@ -20,8 +17,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/convert" - - "github.com/nektos/act/pkg/model" ) const ( @@ -58,70 +53,40 @@ func List(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageIsActions"] = true - var workflows []Workflow - if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { + workflows, err := actions_model.ListWorkflowBranchs(ctx, ctx.Repo.Repository.ID) + if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return - } else if !empty { - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - entries, err := actions.ListWorkflows(commit) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } + } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - WithAvailable: true, - }) - if err != nil { - ctx.ServerError("FindRunners", err) - return - } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) - } + // Get all runner labels + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return + } + allRunnerLabels := make(container.Set[string]) + for _, r := range runners { + allRunnerLabels.AddMultiple(r.AgentLabels...) + } - workflows = make([]Workflow, 0, len(entries)) - for _, entry := range entries { - workflow := Workflow{Entry: *entry} - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) - if err != nil { - workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error()) - workflows = append(workflows, workflow) - continue + for _, w := range workflows { + w.ErrMsg = "" + + for _, b := range w.Branchs { + if len(b.ErrMsg) != 0 { + w.ErrMsg += ctx.Locale.Tr("actions.runs.invalid_workflow_helper_branch", b.Branch, b.ErrMsg) + "; " } - // Check whether have matching runner - for _, j := range wf.Jobs { - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { + + for _, l := range b.Labels { + if !allRunnerLabels.Contains(l) { + w.ErrMsg += ctx.Locale.Tr("actions.runs.no_matching_runner_helper", b.Branch, l) + "; " break } } - workflows = append(workflows, workflow) } } ctx.Data["workflows"] = workflows diff --git a/services/repository/delete.go b/services/repository/delete.go index 08d6800ee7660..9ad135eb09034 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -162,6 +162,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID &actions_model.ActionScheduleSpec{RepoID: repoID}, &actions_model.ActionSchedule{RepoID: repoID}, &actions_model.ActionArtifact{RepoID: repoID}, + &actions_model.ActionWorkflow{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } diff --git a/services/repository/push.go b/services/repository/push.go index 391c8ad4ca7f1..c477b1d0bcf3b 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -14,6 +14,7 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" @@ -269,6 +270,12 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { if err := CacheRef(graceful.GetManager().HammerContext(), repo, gitRepo, opts.RefFullName); err != nil { log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err) } + + err = actions.UpdateWorkflowBranchLabelsForBranch(ctx, gitRepo, repo, branch) + if err != nil { + return err + } + } else { notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) if err = pull_service.CloseBranchPulls(ctx, pusher, repo.ID, branch); err != nil { @@ -279,6 +286,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil { return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err) } + + err = actions.DeleteWorkflowBranchLabelsForBranch(ctx, gitRepo, repo, opts.OldCommitID, branch) + if err != nil { + return err + } } // Even if user delete a branch on a repository which he didn't watch, he will be watch that. diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index 62d30305b3603..ad43bc6836fb9 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -10,14 +10,14 @@