feat(webhook): add workflow_step webhook events for step-level CI notifications
Adds HookEventWorkflowStep event type that fires on every step state transition (queued -> in_progress -> completed). Follows the same pattern as the existing workflow_job events. - New WorkflowStepPayload struct with run/job/step context - WorkflowStepStatusUpdate notifier interface + dispatch - Step state change detection in UpdateTask runner endpoint - Fix: register workflow_step in updateHookEvents API mapping - Full test coverage mirroring workflow_job tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,9 +35,11 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestNewWebHookLink(t *testing.T) {
|
||||
@@ -1112,6 +1114,111 @@ jobs:
|
||||
})
|
||||
}
|
||||
|
||||
func Test_WebhookWorkflowStep(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
var payloads []api.WorkflowStepPayload
|
||||
var triggeredEvent string
|
||||
provider := newMockWebhookProvider(func(r *http.Request) {
|
||||
assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_step", "X-GitHub-Event-Type should contain workflow_step")
|
||||
assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_step", "X-Gitea-Event-Type should contain workflow_step")
|
||||
assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_step", "X-Gogs-Event-Type should contain workflow_step")
|
||||
content, _ := io.ReadAll(r.Body)
|
||||
var payload api.WorkflowStepPayload
|
||||
err := json.Unmarshal(content, &payload)
|
||||
assert.NoError(t, err)
|
||||
payloads = append(payloads, payload)
|
||||
triggeredEvent = "workflow_step"
|
||||
}, http.StatusOK)
|
||||
defer provider.Close()
|
||||
|
||||
// 1. create a webhook subscribed to workflow_step 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", provider.URL(), "workflow_step")
|
||||
|
||||
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. push a workflow with a single step
|
||||
wfTreePath := ".gitea/workflows/push.yml"
|
||||
wfFileContent := `name: Push
|
||||
on: push
|
||||
jobs:
|
||||
wf1-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'step one'
|
||||
`
|
||||
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. no step webhooks yet — only job-level events have fired at this point
|
||||
assert.Empty(t, payloads)
|
||||
|
||||
// 4. fetch the task so the runner picks it up (step rows created in DB)
|
||||
task := runner.fetchTask(t)
|
||||
assert.NotNil(t, task)
|
||||
|
||||
// 5. send an intermediate UpdateTask marking step 0 as in_progress
|
||||
_, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Steps: []*runnerv1.StepState{
|
||||
{Id: 0, StartedAt: timestamppb.Now()},
|
||||
},
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "workflow_step", triggeredEvent)
|
||||
require.Len(t, payloads, 1)
|
||||
assert.Equal(t, "in_progress", payloads[0].Action)
|
||||
assert.Equal(t, "in_progress", payloads[0].Step.Status)
|
||||
assert.Equal(t, commitID, payloads[0].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[0].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Repo.FullName)
|
||||
|
||||
// 6. send final UpdateTask marking the task as success and step 0 as completed
|
||||
_, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
StoppedAt: timestamppb.Now(),
|
||||
Steps: []*runnerv1.StepState{
|
||||
{
|
||||
Id: 0,
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
StartedAt: timestamppb.Now(),
|
||||
StoppedAt: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "workflow_step", triggeredEvent)
|
||||
require.Len(t, payloads, 2)
|
||||
assert.Equal(t, "completed", payloads[1].Action)
|
||||
assert.Equal(t, "completed", payloads[1].Step.Status)
|
||||
assert.Equal(t, "success", payloads[1].Step.Conclusion)
|
||||
assert.Equal(t, commitID, payloads[1].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[1].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[1].Repo.FullName)
|
||||
assert.NotNil(t, payloads[1].WorkflowRun)
|
||||
assert.NotNil(t, payloads[1].WorkflowJob)
|
||||
})
|
||||
}
|
||||
|
||||
type workflowRunWebhook struct {
|
||||
URL string
|
||||
payloads []api.WorkflowRunPayload
|
||||
|
||||
Reference in New Issue
Block a user