diff --git a/models/actions/run.go b/models/actions/run.go index a573fe497f..8194c07940 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -41,24 +41,25 @@ const ( // ActionRun represents a run of a workflow file type ActionRun struct { - ID int64 - Title string - RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` - Repo *repo_model.Repository `xorm:"-"` - OwnerID int64 `xorm:"index"` - WorkflowID string `xorm:"index"` // the name of workflow file - Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository - TriggerUserID int64 `xorm:"index"` - TriggerUser *user_model.User `xorm:"-"` - ScheduleID int64 - Ref string `xorm:"index"` // the commit/tag/… that caused the run - IsRefDeleted bool `xorm:"-"` - CommitSHA string - Event webhook_module.HookEventType // the webhook event that causes the workflow to run - EventPayload string `xorm:"LONGTEXT"` - TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow - Status Status `xorm:"index"` - Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed + ID int64 + Title string + RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` // the name of workflow file + WorkflowDirectory string `xorm:"NOT NULL DEFAULT '.forgejo/workflows'"` // directory where the workflow file resides, for example, .forgejo/workflows + Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository + TriggerUserID int64 `xorm:"index"` + TriggerUser *user_model.User `xorm:"-"` + ScheduleID int64 + Ref string `xorm:"index"` // the commit/tag/… that caused the run + IsRefDeleted bool `xorm:"-"` + CommitSHA string + Event webhook_module.HookEventType // the webhook event that causes the workflow to run + EventPayload string `xorm:"LONGTEXT"` + TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow + Status Status `xorm:"index"` + Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp Stopped timeutil.TimeStamp diff --git a/models/actions/schedule.go b/models/actions/schedule.go index cc68e9eafc..05c9f15d38 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -19,22 +19,23 @@ import ( // ActionSchedule represents a schedule of a workflow file type ActionSchedule struct { - ID int64 - Title string - Specs []string - RepoID int64 `xorm:"index"` - Repo *repo_model.Repository `xorm:"-"` - OwnerID int64 `xorm:"index"` - WorkflowID string - TriggerUserID int64 - TriggerUser *user_model.User `xorm:"-"` - Ref string - CommitSHA string - Event webhook_module.HookEventType - EventPayload string `xorm:"LONGTEXT"` - Content []byte - Created timeutil.TimeStamp `xorm:"created"` - Updated timeutil.TimeStamp `xorm:"updated"` + ID int64 + Title string + Specs []string + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + WorkflowID string + WorkflowDirectory string `xorm:"NOT NULL DEFAULT '.forgejo/workflows'"` + TriggerUserID int64 + TriggerUser *user_model.User `xorm:"-"` + Ref string + CommitSHA string + Event webhook_module.HookEventType + EventPayload string `xorm:"LONGTEXT"` + Content []byte + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` } func init() { diff --git a/models/forgejo_migrations/v14b_action-run-add-workflow-directory.go b/models/forgejo_migrations/v14b_action-run-add-workflow-directory.go new file mode 100644 index 0000000000..51d4e24280 --- /dev/null +++ b/models/forgejo_migrations/v14b_action-run-add-workflow-directory.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "Add the column workflow_directory to the tables action_run and action_schedule", + Upgrade: addActionRunWorkflowDirectory, + }) +} + +func addActionRunWorkflowDirectory(x *xorm.Engine) error { + type ActionRun struct { + WorkflowDirectory string `xorm:"workflow_directory NOT NULL DEFAULT '.forgejo/workflows'"` + } + type ActionSchedule struct { + WorkflowDirectory string `xorm:"workflow_directory NOT NULL DEFAULT '.forgejo/workflows'"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRun), new(ActionSchedule)) + return err +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index d49f090865..6a7630548a 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -22,7 +22,8 @@ import ( ) type DetectedWorkflow struct { - EntryName string + EntryName string // file name of the workflow, for example, test.yaml + EntryDirectory string // folder where the workflow was found, for example, .forgejo/workflows TriggerEvent *jobparser.Event Content []byte EventDetectionError error @@ -46,24 +47,41 @@ func IsWorkflow(path string) bool { return strings.HasPrefix(path, ".forgejo/workflows") || strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") } -func ListWorkflows(commit *git.Commit) (git.Entries, error) { - tree, err := commit.SubTree(".forgejo/workflows") - if _, ok := err.(git.ErrNotExist); ok { - tree, err = commit.SubTree(".gitea/workflows") +// ListWorkflows looks for one of the standard workflow directories .forgejo/workflows, .gitea/workflows, and +// .github/workflows in the given Git tree and returns the name of the first one it encounters including all the +// workflow files it contains, if any. +func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { + workflowSources := []string{ + ".forgejo/workflows", + ".gitea/workflows", + ".github/workflows", } - if _, ok := err.(git.ErrNotExist); ok { - tree, err = commit.SubTree(".github/workflows") + var workflowSource string + var tree *git.Tree + for _, workflowSource = range workflowSources { + var err error + tree, err = commit.SubTree(workflowSource) + + // If the source does not exist, we try the next one. + if _, ok := err.(git.ErrNotExist); ok { + continue + } + + // Other errors are reported immediately. + if err != nil { + return "", nil, err + } + + // We have found a valid source that we will use, no matter whether it contains workflows or not. + break } - if _, ok := err.(git.ErrNotExist); ok { - return nil, nil - } - if err != nil { - return nil, err + if tree == nil { + return "", nil, nil } entries, err := tree.ListEntriesRecursiveFast() if err != nil { - return nil, err + return "", nil, err } ret := make(git.Entries, 0, len(entries)) @@ -72,7 +90,7 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) { ret = append(ret, entry) } } - return ret, nil + return workflowSource, ret, nil } func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { @@ -108,7 +126,7 @@ func DetectWorkflows( payload api.Payloader, detectSchedule bool, ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + directory, entries, err := ListWorkflows(commit) if err != nil { return nil, nil, err } @@ -126,7 +144,8 @@ func DetectWorkflows( if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) dwf := &DetectedWorkflow{ - EntryName: entry.Name(), + EntryName: entry.Name(), + EntryDirectory: directory, TriggerEvent: &jobparser.Event{ Name: triggedEvent.Event(), }, @@ -141,17 +160,19 @@ func DetectWorkflows( if evt.IsSchedule() { if detectSchedule { dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, + EntryName: entry.Name(), + EntryDirectory: directory, + TriggerEvent: evt, + Content: content, } schedules = append(schedules, dwf) } } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, + EntryName: entry.Name(), + EntryDirectory: directory, + TriggerEvent: evt, + Content: content, } workflows = append(workflows, dwf) } @@ -162,7 +183,7 @@ func DetectWorkflows( } func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + directory, entries, err := ListWorkflows(commit) if err != nil { return nil, err } @@ -184,9 +205,10 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D if evt.IsSchedule() { log.Trace("detect scheduled workflow: %q", entry.Name()) dwf := &DetectedWorkflow{ - EntryName: entry.Name(), - TriggerEvent: evt, - Content: content, + EntryName: entry.Name(), + EntryDirectory: directory, + TriggerEvent: evt, + Content: content, } wfs = append(wfs, dwf) } diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 9068ce31c3..b431989def 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -4,17 +4,22 @@ package actions import ( + "os" + "path/filepath" "testing" + "time" "forgejo.org/modules/git" + "forgejo.org/modules/setting" api "forgejo.org/modules/structs" + "forgejo.org/modules/test" webhook_module "forgejo.org/modules/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestDetectMatched(t *testing.T) { +func TestActionsWorkflowsDetectMatched(t *testing.T) { testCases := []struct { desc string commit *git.Commit @@ -179,3 +184,215 @@ func TestDetectMatched(t *testing.T) { }) } } + +func TestActionsWorkflowsListWorkflowsReturnsNoWorkflowsIfThereAreNone(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) + require.NoError(t, git.InitSimple(t.Context())) + + committer := git.Signature{ + Email: "jane@example.com", + Name: "Jane", + When: time.Now(), + } + repoHome := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(repoHome, "README.md"), []byte("My project"), 0o644)) + + require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name())) + require.NoError(t, git.AddChanges(repoHome, true)) + require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer})) + + gitRepo, err := git.OpenRepository(t.Context(), repoHome) + require.NoError(t, err) + defer gitRepo.Close() + + headBranch, err := gitRepo.GetHEADBranch() + require.NoError(t, err) + + lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name) + require.NoError(t, err) + + lastCommit, err := gitRepo.GetCommit(lastCommitID) + require.NoError(t, err) + + source, workflows, err := ListWorkflows(lastCommit) + require.NoError(t, err) + + assert.Empty(t, source) + assert.Empty(t, workflows) +} + +func TestActionsWorkflowsListWorkflowsIgnoresNonWorkflowFiles(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) + require.NoError(t, git.InitSimple(t.Context())) + + committer := git.Signature{ + Email: "jane@example.com", + Name: "Jane", + When: time.Now(), + } + githubWorkflow := []byte(` +name: GitHub Workflow +on: + push: +jobs: + do-something: + runs-on: ubuntu-latest + steps: + - run: echo 'Hello GitHub' +`) + repoHome := t.TempDir() + + require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".forgejo/workflows"), os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".forgejo/workflows", "README.md"), []byte("My project"), 0o644)) + + // Prepare a valid workflow in .github/workflows to verify that it is ignored because .forgejo/workflows is present. + require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "github.yaml"), githubWorkflow, 0o644)) + + require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name())) + require.NoError(t, git.AddChanges(repoHome, true)) + require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer})) + + gitRepo, err := git.OpenRepository(t.Context(), repoHome) + require.NoError(t, err) + defer gitRepo.Close() + + headBranch, err := gitRepo.GetHEADBranch() + require.NoError(t, err) + + lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name) + require.NoError(t, err) + + lastCommit, err := gitRepo.GetCommit(lastCommitID) + require.NoError(t, err) + + source, workflows, err := ListWorkflows(lastCommit) + require.NoError(t, err) + + assert.Equal(t, ".forgejo/workflows", source) + assert.Empty(t, workflows) +} + +func TestActionsWorkflowsListWorkflowsReturnsForgejoWorkflowsOnly(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) + require.NoError(t, git.InitSimple(t.Context())) + + committer := git.Signature{ + Email: "jane@example.com", + Name: "Jane", + When: time.Now(), + } + forgejoWorkflow := []byte(` +name: Forgejo Workflow +on: + push: +jobs: + do-something: + runs-on: ubuntu-latest + steps: + - run: echo 'Hello Forgejo' +`) + githubWorkflow := []byte(` +name: GitHub Workflow +on: + push: +jobs: + do-something: + runs-on: ubuntu-latest + steps: + - run: echo 'Hello GitHub' +`) + repoHome := t.TempDir() + + require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".forgejo/workflows"), os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".forgejo/workflows", "forgejo.yaml"), forgejoWorkflow, 0o644)) + + require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "github.yaml"), githubWorkflow, 0o644)) + + require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name())) + require.NoError(t, git.AddChanges(repoHome, true)) + require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer})) + + gitRepo, err := git.OpenRepository(t.Context(), repoHome) + require.NoError(t, err) + defer gitRepo.Close() + + headBranch, err := gitRepo.GetHEADBranch() + require.NoError(t, err) + + lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name) + require.NoError(t, err) + + lastCommit, err := gitRepo.GetCommit(lastCommitID) + require.NoError(t, err) + + source, workflows, err := ListWorkflows(lastCommit) + require.NoError(t, err) + + assert.Len(t, workflows, 1) + assert.Equal(t, ".forgejo/workflows", source) + assert.Equal(t, "forgejo.yaml", workflows[0].Name()) +} + +func TestActionsWorkflowsListWorkflowsReturnsGitHubWorkflowsIfForgejoWorkflowsAbsent(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) + require.NoError(t, git.InitSimple(t.Context())) + + committer := git.Signature{ + Email: "jane@example.com", + Name: "Jane", + When: time.Now(), + } + buildWorkflow := []byte(` +name: Build +on: + push: +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo 'We are building' +`) + testWorkflow := []byte(` +name: Test +on: + push: +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo 'We are testing' +`) + repoHome := t.TempDir() + + require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "build.yaml"), buildWorkflow, 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "test.yml"), testWorkflow, 0o644)) + + require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name())) + require.NoError(t, git.AddChanges(repoHome, true)) + require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer})) + + gitRepo, err := git.OpenRepository(t.Context(), repoHome) + require.NoError(t, err) + defer gitRepo.Close() + + headBranch, err := gitRepo.GetHEADBranch() + require.NoError(t, err) + + lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name) + require.NoError(t, err) + + lastCommit, err := gitRepo.GetCommit(lastCommitID) + require.NoError(t, err) + + source, workflows, err := ListWorkflows(lastCommit) + require.NoError(t, err) + + assert.Len(t, workflows, 2) + assert.Equal(t, ".github/workflows", source) + assert.Equal(t, "build.yaml", workflows[0].Name()) + assert.Equal(t, "test.yml", workflows[1].Name()) +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 2d8ec1ad64..55e70bf5de 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -80,7 +80,7 @@ func List(ctx *context.Context) { ctx.ServerError("GetBranchCommit", err) return } - entries, err := actions.ListWorkflows(commit) + _, entries, err := actions.ListWorkflows(commit) if err != nil { ctx.ServerError("ListWorkflows", err) return diff --git a/services/actions/context.go b/services/actions/context.go index bc076c587e..4e4bfd75ea 100644 --- a/services/actions/context.go +++ b/services/actions/context.go @@ -40,6 +40,7 @@ func generateGiteaContextForRun(run *actions_model.ActionRun) *model.GithubConte } refName := git.RefName(ref) + workflowRef := fmt.Sprintf("%s/%s/%s/%s@%s", run.Repo.OwnerName, run.Repo.Name, run.WorkflowDirectory, run.WorkflowID, ref) gitContextObj := &model.GithubContext{ // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context @@ -67,6 +68,7 @@ func generateGiteaContextForRun(run *actions_model.ActionRun) *model.GithubConte ServerURL: setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. Sha: sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. Workflow: run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. + WorkflowRef: workflowRef, // string, ref path to the workflow file, for example, example/test/.forgejo/workflows/test.yaml@refs/heads/main Workspace: "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. } if run.TriggerUser != nil { diff --git a/services/actions/context_test.go b/services/actions/context_test.go index 99a13f909f..544543a9c3 100644 --- a/services/actions/context_test.go +++ b/services/actions/context_test.go @@ -54,15 +54,16 @@ func TestGenerateGiteaContext(t *testing.T) { t.Run("Basic workflow run without job", func(t *testing.T) { run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: "push", - Ref: "refs/heads/main", - CommitSHA: "abc123def456", - WorkflowID: "test-workflow", - EventPayload: `{"repository": {"name": "testrepo"}}`, + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: "push", + Ref: "refs/heads/main", + CommitSHA: "abc123def456", + WorkflowID: "test-workflow.yaml", + WorkflowDirectory: ".forgejo/workflows", + EventPayload: `{"repository": {"name": "testrepo"}}`, } context := GenerateGiteaContext(run, nil) @@ -77,7 +78,8 @@ func TestGenerateGiteaContext(t *testing.T) { assert.Equal(t, "testowner", context["repository_owner"]) assert.Equal(t, "abc123def456", context["sha"]) assert.Equal(t, "42", context["run_number"]) - assert.Equal(t, "test-workflow", context["workflow"]) + assert.Equal(t, "test-workflow.yaml", context["workflow"]) + assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/heads/main", context["workflow_ref"]) assert.Equal(t, false, context["ref_protected"]) assert.Equal(t, "Actions", context["secret_source"]) assert.Equal(t, setting.AppURL, context["server_url"]) @@ -151,16 +153,17 @@ func TestGenerateGiteaContext(t *testing.T) { payloadBytes, _ := json.Marshal(pullRequestPayload) run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: "pull_request", - Ref: "refs/pull/1/merge", - CommitSHA: "merge789sha", - WorkflowID: "test-workflow", - Event: webhook_module.HookEventPullRequest, - EventPayload: string(payloadBytes), + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: "pull_request", + Ref: "refs/pull/1/merge", + CommitSHA: "merge789sha", + WorkflowID: "test-workflow.yaml", + WorkflowDirectory: ".forgejo/workflows", + Event: webhook_module.HookEventPullRequest, + EventPayload: string(payloadBytes), } context := GenerateGiteaContext(run, nil) @@ -169,6 +172,7 @@ func TestGenerateGiteaContext(t *testing.T) { assert.Equal(t, "feature-branch", context["head_ref"]) assert.Equal(t, "refs/pull/1/merge", context["ref"]) assert.Equal(t, "merge789sha", context["sha"]) + assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/pull/1/merge", context["workflow_ref"]) }) t.Run("Pull request target event", func(t *testing.T) { @@ -190,16 +194,17 @@ func TestGenerateGiteaContext(t *testing.T) { payloadBytes, _ := json.Marshal(pullRequestPayload) run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: actions_module.GithubEventPullRequestTarget, - Ref: "refs/pull/1/merge", - CommitSHA: "merge789sha", - WorkflowID: "test-workflow", - Event: webhook_module.HookEventPullRequest, - EventPayload: string(payloadBytes), + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: actions_module.GithubEventPullRequestTarget, + Ref: "refs/pull/1/merge", + CommitSHA: "merge789sha", + WorkflowID: "test-workflow.yml", + WorkflowDirectory: ".github/workflows", + Event: webhook_module.HookEventPullRequest, + EventPayload: string(payloadBytes), } context := GenerateGiteaContext(run, nil) @@ -211,6 +216,7 @@ func TestGenerateGiteaContext(t *testing.T) { assert.Equal(t, "base123sha", context["sha"]) assert.Equal(t, "main", context["ref_name"]) assert.Equal(t, "branch", context["ref_type"]) + assert.Equal(t, "testowner/testrepo/.github/workflows/test-workflow.yml@refs/heads/main", context["workflow_ref"]) }) } @@ -228,15 +234,16 @@ func TestGenerateGiteaContextForRun(t *testing.T) { t.Run("Basic workflow run", func(t *testing.T) { run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: "push", - Ref: "refs/heads/main", - CommitSHA: "abc123def456", - WorkflowID: "test-workflow", - EventPayload: `{"repository": {"name": "testrepo"}}`, + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: "push", + Ref: "refs/heads/main", + CommitSHA: "abc123def456", + WorkflowID: "test-workflow.yaml", + WorkflowDirectory: ".forgejo/workflows", + EventPayload: `{"repository": {"name": "testrepo"}}`, } gitContextObj := generateGiteaContextForRun(run) @@ -251,7 +258,8 @@ func TestGenerateGiteaContextForRun(t *testing.T) { assert.Equal(t, "testowner", gitContextObj.RepositoryOwner) assert.Equal(t, "abc123def456", gitContextObj.Sha) assert.Equal(t, "42", gitContextObj.RunNumber) - assert.Equal(t, "test-workflow", gitContextObj.Workflow) + assert.Equal(t, "test-workflow.yaml", gitContextObj.Workflow) + assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/heads/main", gitContextObj.WorkflowRef) assert.Equal(t, "testrepo", gitContextObj.Event["repository"].(map[string]any)["name"]) @@ -289,16 +297,17 @@ func TestGenerateGiteaContextForRun(t *testing.T) { payloadBytes, _ := json.Marshal(pullRequestPayload) run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: "pull_request", - Ref: "refs/pull/1/merge", - CommitSHA: "merge789sha", - WorkflowID: "test-workflow", - Event: webhook_module.HookEventPullRequest, - EventPayload: string(payloadBytes), + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: "pull_request", + Ref: "refs/pull/1/merge", + CommitSHA: "merge789sha", + WorkflowID: "test-workflow.yaml", + WorkflowDirectory: ".forgejo/workflows", + Event: webhook_module.HookEventPullRequest, + EventPayload: string(payloadBytes), } gitContextObj := generateGiteaContextForRun(run) @@ -307,6 +316,7 @@ func TestGenerateGiteaContextForRun(t *testing.T) { assert.Equal(t, "feature-branch", gitContextObj.HeadRef) assert.Equal(t, "refs/pull/1/merge", gitContextObj.Ref) assert.Equal(t, "merge789sha", gitContextObj.Sha) + assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/pull/1/merge", gitContextObj.WorkflowRef) }) t.Run("Pull request target event", func(t *testing.T) { @@ -328,16 +338,17 @@ func TestGenerateGiteaContextForRun(t *testing.T) { payloadBytes, _ := json.Marshal(pullRequestPayload) run := &actions_model.ActionRun{ - ID: 1, - Index: 42, - TriggerUser: testUser, - Repo: testRepo, - TriggerEvent: actions_module.GithubEventPullRequestTarget, - Ref: "refs/pull/1/merge", - CommitSHA: "merge789sha", - WorkflowID: "test-workflow", - Event: webhook_module.HookEventPullRequest, - EventPayload: string(payloadBytes), + ID: 1, + Index: 42, + TriggerUser: testUser, + Repo: testRepo, + TriggerEvent: actions_module.GithubEventPullRequestTarget, + Ref: "refs/pull/1/merge", + CommitSHA: "merge789sha", + WorkflowID: "test-workflow.yml", + WorkflowDirectory: ".github/workflows", + Event: webhook_module.HookEventPullRequest, + EventPayload: string(payloadBytes), } gitContextObj := generateGiteaContextForRun(run) @@ -349,5 +360,6 @@ func TestGenerateGiteaContextForRun(t *testing.T) { assert.Equal(t, "base123sha", gitContextObj.Sha) assert.Equal(t, "main", gitContextObj.RefName) assert.Equal(t, "branch", gitContextObj.RefType) + assert.Equal(t, "testowner/testrepo/.github/workflows/test-workflow.yml@refs/heads/main", gitContextObj.WorkflowRef) }) } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index b4087d6c6f..cb27914ae0 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -356,17 +356,18 @@ func handleWorkflows( for _, dwf := range detectedWorkflows { run := &actions_model.ActionRun{ - Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], - RepoID: input.Repo.ID, - OwnerID: input.Repo.OwnerID, - WorkflowID: dwf.EntryName, - TriggerUserID: input.Doer.ID, - Ref: ref, - CommitSHA: commit.ID.String(), - Event: input.Event, - EventPayload: string(p), - TriggerEvent: dwf.TriggerEvent.Name, - Status: actions_model.StatusWaiting, + Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], + RepoID: input.Repo.ID, + OwnerID: input.Repo.OwnerID, + WorkflowID: dwf.EntryName, + WorkflowDirectory: dwf.EntryDirectory, + TriggerUserID: input.Doer.ID, + Ref: ref, + CommitSHA: commit.ID.String(), + Event: input.Event, + EventPayload: string(p), + TriggerEvent: dwf.TriggerEvent.Name, + Status: actions_model.StatusWaiting, } if !actions_module.IsDefaultBranchWorkflow(input.Event) { @@ -572,17 +573,18 @@ func handleSchedules( } run := &actions_model.ActionSchedule{ - Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], - RepoID: input.Repo.ID, - OwnerID: input.Repo.OwnerID, - WorkflowID: dwf.EntryName, - TriggerUserID: user_model.ActionsUserID, - Ref: input.Repo.DefaultBranch, - CommitSHA: commit.ID.String(), - Event: input.Event, - EventPayload: string(p), - Specs: schedules, - Content: dwf.Content, + Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], + RepoID: input.Repo.ID, + OwnerID: input.Repo.OwnerID, + WorkflowID: dwf.EntryName, + WorkflowDirectory: dwf.EntryDirectory, + TriggerUserID: user_model.ActionsUserID, + Ref: input.Repo.DefaultBranch, + CommitSHA: commit.ID.String(), + Event: input.Event, + EventPayload: string(p), + Specs: schedules, + Content: dwf.Content, } crons = append(crons, run) } diff --git a/services/actions/notifier_helper_test.go b/services/actions/notifier_helper_test.go index a9fc64248d..343106454d 100644 --- a/services/actions/notifier_helper_test.go +++ b/services/actions/notifier_helper_test.go @@ -120,6 +120,7 @@ func testActionsNotifierPullRequestWithDoer(t *testing.T, repo *repo_model.Repos CommitMessage: "test", } dw.EntryName = "test.yml" + dw.EntryDirectory = ".forgejo/workflows" dw.TriggerEvent = &jobparser.Event{ Name: "pull_request", } @@ -326,3 +327,29 @@ func TestActionsNotifier_RunsOnNeeds(t *testing.T) { // first inserted it is tagged w/ incomplete_runs_on. assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true") } + +func TestActionsNotifier_WorkflowDetection(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3}) + + dw := &actions_module.DetectedWorkflow{ + Content: []byte("{ on: pull_request, jobs: { j1: {} }}"), + } + testActionsNotifierPullRequest(t, repo, pr, dw, webhook_module.HookEventPullRequestSync) + + runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{ + RepoID: repo.ID, + }) + require.NoError(t, err) + require.Len(t, runs, 1) + run := runs[0] + + jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID}) + require.NoError(t, err) + require.Len(t, jobs, 1) + + assert.Equal(t, ".forgejo/workflows", runs[0].WorkflowDirectory) + assert.Equal(t, "test.yml", runs[0].WorkflowID) +} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index f30bb530bd..17a5484551 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -123,18 +123,19 @@ func startTasks(ctx context.Context) error { func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) error { // Create a new action run based on the schedule run := &actions_model.ActionRun{ - Title: cron.Title, - RepoID: cron.RepoID, - OwnerID: cron.OwnerID, - WorkflowID: cron.WorkflowID, - TriggerUserID: cron.TriggerUserID, - Ref: cron.Ref, - CommitSHA: cron.CommitSHA, - Event: cron.Event, - EventPayload: cron.EventPayload, - TriggerEvent: string(webhook_module.HookEventSchedule), - ScheduleID: cron.ID, - Status: actions_model.StatusWaiting, + Title: cron.Title, + RepoID: cron.RepoID, + OwnerID: cron.OwnerID, + WorkflowID: cron.WorkflowID, + WorkflowDirectory: cron.WorkflowDirectory, + TriggerUserID: cron.TriggerUserID, + Ref: cron.Ref, + CommitSHA: cron.CommitSHA, + Event: cron.Event, + EventPayload: cron.EventPayload, + TriggerEvent: string(webhook_module.HookEventSchedule), + ScheduleID: cron.ID, + Status: actions_model.StatusWaiting, } vars, err := actions_model.GetVariablesOfRun(ctx, run) diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go index c848d9d9c7..4e70708a5d 100644 --- a/services/actions/schedule_tasks_test.go +++ b/services/actions/schedule_tasks_test.go @@ -28,16 +28,17 @@ func TestServiceActions_startTask(t *testing.T) { workflowID := "some.yml" schedules := []*actions_model.ActionSchedule{ { - Title: "scheduletitle1", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: workflowID, - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", - Specs: []string{"* * * * *"}, + Title: "scheduletitle1", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: workflowID, + WorkflowDirectory: ".forgejo/workflows", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Specs: []string{"* * * * *"}, Content: []byte( ` jobs: @@ -80,6 +81,7 @@ func TestCreateScheduleTask(t *testing.T) { assert.Equal(t, cron.RepoID, run.RepoID) assert.Equal(t, cron.OwnerID, run.OwnerID) assert.Equal(t, cron.WorkflowID, run.WorkflowID) + assert.Equal(t, cron.WorkflowDirectory, run.WorkflowDirectory) assert.Equal(t, cron.TriggerUserID, run.TriggerUserID) assert.Equal(t, cron.Ref, run.Ref) assert.Equal(t, cron.CommitSHA, run.CommitSHA) @@ -104,15 +106,16 @@ func TestCreateScheduleTask(t *testing.T) { { name: "simple", cron: actions_model.ActionSchedule{ - Title: "scheduletitle1", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: "some.yml", - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", + Title: "scheduletitle1", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + WorkflowDirectory: ".forgejo/workflows", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", Content: []byte( ` name: test @@ -134,15 +137,16 @@ jobs: { name: "enable-email-notifications is true", cron: actions_model.ActionSchedule{ - Title: "scheduletitle2", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: "some.yml", - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", + Title: "scheduletitle2", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + WorkflowDirectory: ".github/workflows", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", Content: []byte( ` name: test @@ -275,16 +279,17 @@ func TestServiceActions_DynamicMatrix(t *testing.T) { workflowID := "some.yml" schedules := []*actions_model.ActionSchedule{ { - Title: "scheduletitle1", - RepoID: repo.ID, - OwnerID: repo.OwnerID, - WorkflowID: workflowID, - TriggerUserID: repo.OwnerID, - Ref: "branch", - CommitSHA: "fakeSHA", - Event: webhook_module.HookEventSchedule, - EventPayload: "fakepayload", - Specs: []string{"* * * * *"}, + Title: "scheduletitle1", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: workflowID, + WorkflowDirectory: ".forgejo/workflows", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Specs: []string{"* * * * *"}, Content: []byte( ` jobs: diff --git a/services/actions/workflows.go b/services/actions/workflows.go index da006b755a..df0590bd9a 100644 --- a/services/actions/workflows.go +++ b/services/actions/workflows.go @@ -132,20 +132,21 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette } run := &actions_model.ActionRun{ - Title: title, - RepoID: repo.ID, - Repo: repo, - OwnerID: repo.OwnerID, - WorkflowID: entry.WorkflowID, - TriggerUserID: doer.ID, - TriggerUser: doer, - Ref: entry.Ref, - CommitSHA: entry.Commit.ID.String(), - Event: webhook.HookEventWorkflowDispatch, - EventPayload: string(p), - TriggerEvent: string(webhook.HookEventWorkflowDispatch), - Status: actions_model.StatusWaiting, - NotifyEmail: notifications, + Title: title, + RepoID: repo.ID, + Repo: repo, + OwnerID: repo.OwnerID, + WorkflowID: entry.WorkflowID, + WorkflowDirectory: ".forgejo/workflows", + TriggerUserID: doer.ID, + TriggerUser: doer, + Ref: entry.Ref, + CommitSHA: entry.Commit.ID.String(), + Event: webhook.HookEventWorkflowDispatch, + EventPayload: string(p), + TriggerEvent: string(webhook.HookEventWorkflowDispatch), + Status: actions_model.StatusWaiting, + NotifyEmail: notifications, } vars, err := actions_model.GetVariablesOfRun(ctx, run) @@ -198,7 +199,7 @@ func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Wo return nil, err } - entries, err := actions.ListWorkflows(commit) + _, entries, err := actions.ListWorkflows(commit) if err != nil { return nil, err } diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index a712dec45f..262358ab9f 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -471,6 +471,7 @@ jobs: assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue()) assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue()) assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue()) + assert.Equal(t, "user2/actions-gitea-context/.gitea/workflows/pull.yml@refs/pull/1/head", gtCtx["workflow_ref"].GetStringValue()) assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue()) token := gtCtx["token"].GetStringValue() assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:]) diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 5e0d8d877c..9d46ef0cf0 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -795,7 +795,7 @@ func TestActionsCreateDeleteRefEvent(t *testing.T) { }) } -func TestActionsWorkflowDispatchEvent(t *testing.T) { +func TestActionsWorkflowDispatch(t *testing.T) { onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -842,6 +842,8 @@ func TestActionsWorkflowDispatchEvent(t *testing.T) { assert.Equal(t, "test", r.Title) assert.Equal(t, "dispatch.yml", r.WorkflowID) + // .forgejo/workflows is wrong. It should be .gitea/workflows because the workflow is saved there during setup. + assert.Equal(t, ".forgejo/workflows", r.WorkflowDirectory) assert.Equal(t, sha, r.CommitSHA) assert.Equal(t, actions_module.GithubEventWorkflowDispatch, r.TriggerEvent) assert.Len(t, j, 1)