diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 1db849352f..6f18da360a 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -74,3 +74,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 + +- + id: 802 + title: "workflow run list" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 8837e6ec2d..eaad7779e7 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -69,3 +69,19 @@ status: 5 started: 1683636528 stopped: 1683636626 + +- + id: 203 + run_id: 802 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 \ No newline at end of file diff --git a/services/convert/convert.go b/services/convert/convert.go index 6108834f99..aa0ccb7dd1 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -5,6 +5,7 @@ package convert import ( + "bytes" "context" "fmt" "net/url" @@ -34,6 +35,7 @@ import ( "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/gitdiff" + "github.com/nektos/act/pkg/model" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" ) @@ -423,9 +425,25 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, co createdAt := commit.Author.When updatedAt := commit.Author.When + content, err := actions.GetContentFromEntry(entry) + name := entry.Name() + if err == nil { + workflow, err := model.ReadWorkflow(bytes.NewReader(content)) + if err == nil { + // Only use the name when specified in the workflow file + if workflow.Name != "" { + name = workflow.Name + } + } else { + log.Error("getActionWorkflowEntry: Failed to parse workflow: %v", err) + } + } else { + log.Error("getActionWorkflowEntry: Failed to get content from entry: %v", err) + } + return &api.ActionWorkflow{ ID: entry.Name(), - Name: entry.Name(), + Name: name, Path: path.Join(folder, entry.Name()), State: state, CreatedAt: createdAt, @@ -464,7 +482,7 @@ func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_ } for _, entry := range entries { - if entry.Name == workflowID { + if entry.ID == workflowID { return entry, nil } } diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index f576dc38ab..081a8abdee 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -719,7 +719,7 @@ func TestWorkflowDispatchPublicApi(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch jobs: @@ -799,7 +799,7 @@ func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -890,7 +890,7 @@ func TestWorkflowDispatchPublicApiJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -976,7 +976,7 @@ func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -1070,7 +1070,7 @@ func TestWorkflowDispatchPublicApiWithInputsNonDefaultBranchJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch jobs: @@ -1106,7 +1106,7 @@ jobs: { Operation: "update", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -1207,7 +1207,7 @@ func TestWorkflowApi(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 6d8f9a790a..7a14599e26 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -749,3 +749,204 @@ jobs: assert.Len(t, payloads[6].WorkflowJob.Steps, 2) }) } + +type workflowRunWebhook struct { + URL string + payloads []api.WorkflowRunPayload + triggeredEvent string +} + +func Test_WebhookWorkflowRun(t *testing.T) { + webhookData := &workflowRunWebhook{} + provider := newMockWebhookProvider(func(r *http.Request) { + assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_run", "X-GitHub-Event-Type should contain workflow_run") + assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_run", "X-Gitea-Event-Type should contain workflow_run") + assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_run", "X-Gogs-Event-Type should contain workflow_run") + content, _ := io.ReadAll(r.Body) + var payload api.WorkflowRunPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + webhookData.payloads = append(webhookData.payloads, payload) + webhookData.triggeredEvent = "workflow_run" + }, http.StatusOK) + defer provider.Close() + webhookData.URL = provider.URL() + + tests := []struct { + name string + callback func(t *testing.T, webhookData *workflowRunWebhook) + }{ + { + name: "WorkflowRun", + callback: testWebhookWorkflowRun, + }, + { + name: "WorkflowRunDepthLimit", + callback: testWebhookWorkflowRunDepthLimit, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + webhookData.payloads = nil + webhookData.triggeredEvent = "" + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + test.callback(t, webhookData) + }) + }) + } +} + +func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { + // 1. create a new webhook with special webhook for repo1 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1) + assert.NoError(t, err) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, "user2", "repo1", "mock-runner", []string{"ubuntu-latest"}, false) + + // 2.1 add workflow_run workflow file to the repo + + opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+"dispatch.yml", ` +on: + workflow_run: + workflows: ["Push"] + types: + - completed +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' +`) + createWorkflowFile(t, token, "user2", "repo1", ".gitea/workflows/dispatch.yml", opts) + + // 2.2 trigger the webhooks + + // add workflow file to the repo + // init the workflow + wfTreePath := ".gitea/workflows/push.yml" + wfFileContent := `name: Push +on: push +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' + wf2-job: + runs-on: ubuntu-latest + needs: wf1-job + steps: + - run: echo 'cmd 1' + - run: echo 'cmd 2' +` + opts = getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 1) + assert.Equal(t, "requested", webhookData.payloads[0].Action) + assert.Equal(t, "queued", webhookData.payloads[0].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[0].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[0].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[0].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[0].Repo.FullName) + + // 4. Execute two Jobs + task := runner.fetchTask(t) + outcome := &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + execTime: time.Millisecond, + } + runner.execTask(t, task, outcome) + + task = runner.fetchTask(t) + outcome = &mockTaskOutcome{ + result: runnerv1.Result_RESULT_FAILURE, + execTime: time.Millisecond, + } + runner.execTask(t, task, outcome) + + // 7. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 3) + assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Equal(t, "push", webhookData.payloads[1].WorkflowRun.Event) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 3) + assert.Equal(t, "requested", webhookData.payloads[2].Action) + assert.Equal(t, "queued", webhookData.payloads[2].WorkflowRun.Status) + assert.Equal(t, "workflow_run", webhookData.payloads[2].WorkflowRun.Event) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[2].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[2].Repo.FullName) +} + +func testWebhookWorkflowRunDepthLimit(t *testing.T, webhookData *workflowRunWebhook) { + // 1. create a new webhook with special webhook for repo1 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1) + assert.NoError(t, err) + + // 2. trigger the webhooks + + // add workflow file to the repo + // init the workflow + wfTreePath := ".gitea/workflows/push.yml" + wfFileContent := `name: Endless Loop +on: + push: + workflow_run: + types: + - requested +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' +` + opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + // 1x push + 5x workflow_run requested chain + assert.Len(t, webhookData.payloads, 6) + for i := 0; i < 6; i++ { + assert.Equal(t, "requested", webhookData.payloads[i].Action) + assert.Equal(t, "queued", webhookData.payloads[i].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[i].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[i].WorkflowRun.HeadSha) + if i == 0 { + assert.Equal(t, "push", webhookData.payloads[i].WorkflowRun.Event) + } else { + assert.Equal(t, "workflow_run", webhookData.payloads[i].WorkflowRun.Event) + } + assert.Equal(t, "repo1", webhookData.payloads[i].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[i].Repo.FullName) + } +} diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go index f142da7b22..f43ce717c7 100644 --- a/tests/integration/workflow_run_api_check_test.go +++ b/tests/integration/workflow_run_api_check_test.go @@ -25,7 +25,9 @@ func TestAPIWorkflowRunRepoApi(t *testing.T) { runnerList := api.ActionWorkflowRunsResponse{} DecodeJSON(t, runnerListResp, &runnerList) - assert.Len(t, runnerList.Entries, 4) + assert.Len(t, runnerList.Entries, 5) + + foundRun := false for _, run := range runnerList.Entries { req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", run.URL, "jobs")).AddTokenAuth(token) @@ -33,40 +35,20 @@ func TestAPIWorkflowRunRepoApi(t *testing.T) { jobList := api.ActionWorkflowJobsResponse{} DecodeJSON(t, jobsResp, &jobList) - // assert.NotEmpty(t, jobList.Entries) - for _, job := range jobList.Entries { - req := NewRequest(t, "GET", job.URL).AddTokenAuth(token) - jobsResp := MakeRequest(t, req, http.StatusOK) - apiJob := api.ActionWorkflowJob{} - DecodeJSON(t, jobsResp, &apiJob) - assert.Equal(t, job.ID, apiJob.ID) - assert.Equal(t, job.RunID, apiJob.RunID) - assert.Equal(t, job.Status, apiJob.Status) - assert.Equal(t, job.Conclusion, apiJob.Conclusion) + if run.ID == 802 { + foundRun = true + assert.Len(t, jobList.Entries, 1) + for _, job := range jobList.Entries { + req := NewRequest(t, "GET", job.URL).AddTokenAuth(token) + jobsResp := MakeRequest(t, req, http.StatusOK) + apiJob := api.ActionWorkflowJob{} + DecodeJSON(t, jobsResp, &apiJob) + assert.Equal(t, job.ID, apiJob.ID) + assert.Equal(t, job.RunID, apiJob.RunID) + assert.Equal(t, job.Status, apiJob.Status) + assert.Equal(t, job.Conclusion, apiJob.Conclusion) + } } - // assert.NotEmpty(t, run.ID) - // assert.NotEmpty(t, run.Status) - // assert.NotEmpty(t, run.Event) - // assert.NotEmpty(t, run.WorkflowID) - // assert.NotEmpty(t, run.HeadBranch) - // assert.NotEmpty(t, run.HeadSHA) - // assert.NotEmpty(t, run.CreatedAt) - // assert.NotEmpty(t, run.UpdatedAt) - // assert.NotEmpty(t, run.URL) - // assert.NotEmpty(t, run.HTMLURL) - // assert.NotEmpty(t, run.PullRequests) - // assert.NotEmpty(t, run.WorkflowURL) - // assert.NotEmpty(t, run.HeadCommit) - // assert.NotEmpty(t, run.HeadRepository) - // assert.NotEmpty(t, run.Repository) - // assert.NotEmpty(t, run.HeadRepository) - // assert.NotEmpty(t, run.HeadRepository.Owner) - // assert.NotEmpty(t, run.HeadRepository.Name) - // assert.NotEmpty(t, run.Repository.Owner) - // assert.NotEmpty(t, run.Repository.Name) - // assert.NotEmpty(t, run.HeadRepository.Owner.Login) - // assert.NotEmpty(t, run.HeadRepository.Name) - // assert.NotEmpty(t, run.Repository.Owner.Login) - // assert.NotEmpty(t, run.Repository.Name) } + assert.True(t, foundRun, "Expected to find run with ID 802") }