This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
name: Build & Push Gitea image
|
||||
|
||||
# Builds and pushes a custom Gitea image to Artifact Registry.
|
||||
# Branch → image tag mapping:
|
||||
# dev → gitea:dev (inner dev loop)
|
||||
# stage → gitea:stage (staging verification)
|
||||
# main → gitea:latest + gitea:<sha> (production)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- stage
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Authenticate to GCP
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_SA_KEY }}
|
||||
|
||||
- name: Set up gcloud
|
||||
uses: google-github-actions/setup-gcloud@v2
|
||||
with:
|
||||
project_id: neuron-785695
|
||||
|
||||
- name: Configure Docker for Artifact Registry
|
||||
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
- name: Compute image tags
|
||||
id: tag
|
||||
run: |
|
||||
REGISTRY="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/gitea"
|
||||
SHA="${GITHUB_SHA:0:8}"
|
||||
BRANCH="${GITEA_REF_NAME:-${GITHUB_REF_NAME}}"
|
||||
case "$BRANCH" in
|
||||
main)
|
||||
TAGS="${REGISTRY}:latest ${REGISTRY}:${SHA}"
|
||||
ENV_TAG="latest"
|
||||
;;
|
||||
stage)
|
||||
TAGS="${REGISTRY}:stage ${REGISTRY}:stage-${SHA}"
|
||||
ENV_TAG="stage"
|
||||
;;
|
||||
dev)
|
||||
TAGS="${REGISTRY}:dev ${REGISTRY}:dev-${SHA}"
|
||||
ENV_TAG="dev"
|
||||
;;
|
||||
*)
|
||||
TAGS="${REGISTRY}:${SHA}"
|
||||
ENV_TAG="${SHA}"
|
||||
;;
|
||||
esac
|
||||
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
|
||||
echo "primary=${REGISTRY}:${ENV_TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install docker buildx
|
||||
run: |
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
BUILDX_URL="https://github.com/docker/buildx/releases/download/v0.20.1/buildx-v0.20.1.linux-amd64"
|
||||
curl -fsSL "$BUILDX_URL" -o ~/.docker/cli-plugins/docker-buildx
|
||||
chmod +x ~/.docker/cli-plugins/docker-buildx
|
||||
docker buildx version
|
||||
|
||||
- name: Build Gitea image
|
||||
run: |
|
||||
TAGS=""
|
||||
for t in ${{ steps.tag.outputs.tags }}; do
|
||||
TAGS="$TAGS -t $t"
|
||||
done
|
||||
docker buildx build \
|
||||
--load \
|
||||
--build-arg GITEA_VERSION="neuron-$(git rev-parse --short HEAD)" \
|
||||
--build-arg TAGS="sqlite sqlite_unlock_notify" \
|
||||
$TAGS \
|
||||
.
|
||||
|
||||
- name: Push Gitea image
|
||||
run: |
|
||||
for t in ${{ steps.tag.outputs.tags }}; do
|
||||
docker push "$t"
|
||||
echo "Pushed: $t"
|
||||
done
|
||||
|
||||
- name: Output
|
||||
run: echo "::notice title=Image::${{ steps.tag.outputs.primary }}"
|
||||
@@ -139,7 +139,7 @@ func TestWebhook_EventsArray(t *testing.T) {
|
||||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
|
||||
"package", "status", "workflow_run", "workflow_job",
|
||||
"package", "status", "workflow_run", "workflow_job", "workflow_step",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||
|
||||
@@ -648,3 +648,26 @@ type WorkflowJobPayload struct {
|
||||
func (p *WorkflowJobPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// WorkflowStepPayload represents a payload information of workflow step event.
|
||||
type WorkflowStepPayload struct {
|
||||
// The action performed on the workflow step
|
||||
Action string `json:"action"`
|
||||
// The workflow run that contains the step
|
||||
WorkflowRun *ActionWorkflowRun `json:"workflow_run"`
|
||||
// The workflow job that contains the step
|
||||
WorkflowJob *ActionWorkflowJob `json:"workflow_job"`
|
||||
// The workflow step that was acted upon
|
||||
Step *ActionWorkflowStep `json:"step"`
|
||||
// The organization that owns the repository (if applicable)
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
// The repository containing the workflow
|
||||
Repo *Repository `json:"repository"`
|
||||
// The user who triggered the workflow
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
// JSONPayload implements Payload
|
||||
func (p *WorkflowStepPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@ const (
|
||||
HookEventPullRequestReview HookEventType = "pull_request_review"
|
||||
// Actions event only
|
||||
HookEventSchedule HookEventType = "schedule"
|
||||
HookEventWorkflowRun HookEventType = "workflow_run"
|
||||
HookEventWorkflowJob HookEventType = "workflow_job"
|
||||
HookEventWorkflowRun HookEventType = "workflow_run"
|
||||
HookEventWorkflowJob HookEventType = "workflow_job"
|
||||
HookEventWorkflowStep HookEventType = "workflow_step"
|
||||
)
|
||||
|
||||
func AllEvents() []HookEventType {
|
||||
@@ -70,6 +71,7 @@ func AllEvents() []HookEventType {
|
||||
HookEventStatus,
|
||||
HookEventWorkflowRun,
|
||||
HookEventWorkflowJob,
|
||||
HookEventWorkflowStep,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2329,6 +2329,8 @@
|
||||
"repo.settings.event_workflow_run_desc": "Gitea Actions Workflow run queued, waiting, in progress, or completed.",
|
||||
"repo.settings.event_workflow_job": "Workflow Jobs",
|
||||
"repo.settings.event_workflow_job_desc": "Gitea Actions Workflow job queued, waiting, in progress, or completed.",
|
||||
"repo.settings.event_workflow_step": "Workflow Steps",
|
||||
"repo.settings.event_workflow_step_desc": "Gitea Actions Workflow step started or completed.",
|
||||
"repo.settings.event_package": "Package",
|
||||
"repo.settings.event_package_desc": "Package created or deleted in a repository.",
|
||||
"repo.settings.branch_filter": "Branch filter",
|
||||
|
||||
@@ -183,6 +183,16 @@ func (s *Service) UpdateTask(
|
||||
) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
runner := GetRunner(ctx)
|
||||
|
||||
// Snapshot step states before the update so we can detect which steps changed.
|
||||
oldStepStates := make(map[int64]actions_model.Status)
|
||||
if existingTask, err := actions_model.GetTaskByID(ctx, req.Msg.State.Id); err == nil {
|
||||
if err := existingTask.LoadAttributes(ctx); err == nil {
|
||||
for _, step := range existingTask.Steps {
|
||||
oldStepStates[step.Index] = step.Status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task, err := actions_model.UpdateTaskByState(ctx, runner.ID, req.Msg.State)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "update task: %v", err)
|
||||
@@ -222,6 +232,14 @@ func (s *Service) UpdateTask(
|
||||
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, task.Job.Run, task.Job)
|
||||
|
||||
// Emit step events for any step whose status changed during this update.
|
||||
for _, step := range task.Steps {
|
||||
oldStatus, seen := oldStepStates[step.Index]
|
||||
if !seen || oldStatus != step.Status {
|
||||
actions_service.NotifyWorkflowStepStatusUpdate(ctx, task.Job, task, step)
|
||||
}
|
||||
}
|
||||
|
||||
if task.Status.IsDone() {
|
||||
actions_service.NotifyWorkflowJobStatusUpdateWithTask(ctx, task.Job, task)
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ func updateHookEvents(events []string) webhook_module.HookEvents {
|
||||
hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true)
|
||||
hookEvents[webhook_module.HookEventWorkflowRun] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowRun), true)
|
||||
hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true)
|
||||
hookEvents[webhook_module.HookEventWorkflowStep] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowStep), true)
|
||||
|
||||
// Issues
|
||||
hookEvents[webhook_module.HookEventIssues] = issuesHook(events, "issues_only")
|
||||
|
||||
@@ -187,6 +187,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
|
||||
webhook_module.HookEventStatus: form.Status,
|
||||
webhook_module.HookEventWorkflowRun: form.WorkflowRun,
|
||||
webhook_module.HookEventWorkflowJob: form.WorkflowJob,
|
||||
webhook_module.HookEventWorkflowStep: form.WorkflowStep,
|
||||
},
|
||||
BranchFilter: form.BranchFilter,
|
||||
}
|
||||
|
||||
@@ -119,6 +119,30 @@ func NotifyWorkflowJobsStatusUpdate(ctx context.Context, jobs ...*actions_model.
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyWorkflowStepStatusUpdate notifies a single step status update when a concrete task is available.
|
||||
// Use it when a step transitions between queued, in_progress, and completed states.
|
||||
func NotifyWorkflowStepStatusUpdate(ctx context.Context, job *actions_model.ActionRunJob, task *actions_model.ActionTask, step *actions_model.ActionTaskStep) {
|
||||
if job.RunAttemptID == 0 {
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
log.Error("job.LoadAttributes: %v", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowStepStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, task, step)
|
||||
return
|
||||
}
|
||||
|
||||
attempt, err := actions_model.GetRunAttemptByRepoAndID(ctx, job.RepoID, job.RunAttemptID)
|
||||
if err != nil {
|
||||
log.Error("GetRunAttemptByRepoAndID: %v", err)
|
||||
return
|
||||
}
|
||||
if err := attempt.LoadAttributes(ctx); err != nil {
|
||||
log.Error("attempt.LoadAttributes: %v", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowStepStatusUpdate(ctx, attempt.Run.Repo, attempt.TriggerUser, job, task, step)
|
||||
}
|
||||
|
||||
// NotifyWorkflowJobStatusUpdateWithTask notifies a single job status update when a concrete task is available.
|
||||
// Use it for runner/task lifecycle callbacks so the notification includes the originating task context.
|
||||
func NotifyWorkflowJobStatusUpdateWithTask(ctx context.Context, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
|
||||
@@ -357,6 +357,20 @@ func ToActionsStatus(status actions_model.Status) (string, string) {
|
||||
return action, conclusion
|
||||
}
|
||||
|
||||
// ToActionWorkflowStep converts an actions_model.ActionTaskStep to an api.ActionWorkflowStep.
|
||||
// The step index (0-based position) must be passed by the caller because it is not stored on the model.
|
||||
func ToActionWorkflowStep(step *actions_model.ActionTaskStep, stepIndex int) *api.ActionWorkflowStep {
|
||||
stepStatus, stepConclusion := ToActionsStatus(step.Status)
|
||||
return &api.ActionWorkflowStep{
|
||||
Name: step.Name,
|
||||
Number: int64(stepIndex),
|
||||
Status: stepStatus,
|
||||
Conclusion: stepConclusion,
|
||||
StartedAt: step.Started.AsTime().UTC(),
|
||||
CompletedAt: step.Stopped.AsTime().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob
|
||||
// task is optional and can be nil
|
||||
func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) {
|
||||
@@ -391,15 +405,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
||||
runnerName = runner.Name
|
||||
}
|
||||
for i, step := range task.Steps {
|
||||
stepStatus, stepConclusion := ToActionsStatus(job.Status)
|
||||
steps = append(steps, &api.ActionWorkflowStep{
|
||||
Name: step.Name,
|
||||
Number: int64(i),
|
||||
Status: stepStatus,
|
||||
Conclusion: stepConclusion,
|
||||
StartedAt: step.Started.AsTime().UTC(),
|
||||
CompletedAt: step.Stopped.AsTime().UTC(),
|
||||
})
|
||||
steps = append(steps, ToActionWorkflowStep(step, i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@ type WebhookForm struct {
|
||||
Status bool
|
||||
WorkflowRun bool
|
||||
WorkflowJob bool
|
||||
WorkflowStep bool
|
||||
Active bool
|
||||
BranchFilter string `binding:"GlobPattern"`
|
||||
AuthorizationHeader string
|
||||
|
||||
@@ -82,4 +82,6 @@ type Notifier interface {
|
||||
WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun)
|
||||
|
||||
WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask)
|
||||
|
||||
WorkflowStepStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask, step *actions_model.ActionTaskStep)
|
||||
}
|
||||
|
||||
@@ -416,3 +416,10 @@ func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, s
|
||||
notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task)
|
||||
}
|
||||
}
|
||||
|
||||
// WorkflowStepStatusUpdate dispatches a workflow step status change to every registered notifier.
|
||||
func WorkflowStepStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask, step *actions_model.ActionTaskStep) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.WorkflowStepStatusUpdate(ctx, repo, sender, job, task, step)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,3 +219,6 @@ func (*NullNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_mod
|
||||
|
||||
func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
}
|
||||
|
||||
func (*NullNotifier) WorkflowStepStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask, step *actions_model.ActionTaskStep) {
|
||||
}
|
||||
|
||||
@@ -188,6 +188,12 @@ func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload
|
||||
return createDingtalkPayload(text, text, "Workflow Job", p.WorkflowJob.HTMLURL), nil
|
||||
}
|
||||
|
||||
func (dingtalkConvertor) WorkflowStep(p *api.WorkflowStepPayload) (DingtalkPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "Workflow Step", p.WorkflowJob.HTMLURL), nil
|
||||
}
|
||||
|
||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
|
||||
return DingtalkPayload{
|
||||
MsgType: "actionCard",
|
||||
|
||||
@@ -290,6 +290,12 @@ func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload
|
||||
return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) WorkflowStep(p *api.WorkflowStepPayload) (DiscordPayload, error) {
|
||||
text, color := getWorkflowStepPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
meta := &DiscordMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||
|
||||
@@ -280,6 +280,21 @@ func TestDiscordPayload(t *testing.T) {
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("WorkflowStep", func(t *testing.T) {
|
||||
p := workflowStepTestPayload()
|
||||
|
||||
pl, err := dc.WorkflowStep(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "Workflow Step completed: build / Run tests:success", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, p.WorkflowJob.HTMLURL, pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiscordJSONPayload(t *testing.T) {
|
||||
|
||||
@@ -191,6 +191,12 @@ func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, er
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (feishuConvertor) WorkflowStep(p *api.WorkflowStepPayload) (FeishuPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// feishuGenSign generates a signature for Feishu webhook
|
||||
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||
func feishuGenSign(secret string, timestamp int64) string {
|
||||
|
||||
@@ -389,6 +389,37 @@ func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkForm
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getWorkflowStepPayloadInfo(p *api.WorkflowStepPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
description := p.Step.Conclusion
|
||||
if description == "" {
|
||||
description = p.Step.Status
|
||||
}
|
||||
stepLink := linkFormatter(p.WorkflowJob.HTMLURL, fmt.Sprintf("%s / %s", p.WorkflowJob.Name, p.Step.Name)+":"+description)
|
||||
|
||||
text = fmt.Sprintf("Workflow Step %s: %s", p.Action, stepLink)
|
||||
switch description {
|
||||
case "waiting":
|
||||
color = orangeColor
|
||||
case "queued":
|
||||
color = orangeColorLight
|
||||
case "success":
|
||||
color = greenColor
|
||||
case "failure":
|
||||
color = redColor
|
||||
case "cancelled":
|
||||
color = yellowColor
|
||||
case "skipped":
|
||||
color = purpleColor
|
||||
default:
|
||||
color = greyColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
// ToHook convert models.Webhook to api.Hook
|
||||
// This function is not part of the convert package to prevent an import cycle
|
||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||
|
||||
@@ -610,6 +610,125 @@ func TestGetReleasePayloadInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func workflowJobTestPayload() *api.WorkflowJobPayload {
|
||||
return &api.WorkflowJobPayload{
|
||||
Action: "completed",
|
||||
WorkflowJob: &api.ActionWorkflowJob{
|
||||
ID: 1,
|
||||
Name: "build",
|
||||
Status: "completed",
|
||||
RunID: 42,
|
||||
HeadSha: "abc123",
|
||||
HTMLURL: "http://localhost:3000/test/repo/actions/runs/42/jobs/1",
|
||||
},
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func workflowStepTestPayload() *api.WorkflowStepPayload {
|
||||
return &api.WorkflowStepPayload{
|
||||
Action: "completed",
|
||||
WorkflowRun: &api.ActionWorkflowRun{
|
||||
ID: 42,
|
||||
DisplayTitle: "Push: main",
|
||||
},
|
||||
WorkflowJob: &api.ActionWorkflowJob{
|
||||
ID: 1,
|
||||
Name: "build",
|
||||
Status: "completed",
|
||||
RunID: 42,
|
||||
HeadSha: "abc123",
|
||||
HTMLURL: "http://localhost:3000/test/repo/actions/runs/42/jobs/1",
|
||||
},
|
||||
Step: &api.ActionWorkflowStep{
|
||||
Name: "Run tests",
|
||||
Number: 0,
|
||||
Status: "completed",
|
||||
Conclusion: "success",
|
||||
},
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWorkflowStepPayloadInfo(t *testing.T) {
|
||||
cases := []struct {
|
||||
action string
|
||||
status string
|
||||
conclusion string
|
||||
expectText string
|
||||
expectColor int
|
||||
}{
|
||||
{
|
||||
action: "queued",
|
||||
status: "queued",
|
||||
conclusion: "",
|
||||
expectText: "Workflow Step queued: build / Run tests:queued",
|
||||
expectColor: orangeColorLight,
|
||||
},
|
||||
{
|
||||
action: "in_progress",
|
||||
status: "in_progress",
|
||||
conclusion: "",
|
||||
expectText: "Workflow Step in_progress: build / Run tests:in_progress",
|
||||
expectColor: greyColor,
|
||||
},
|
||||
{
|
||||
action: "completed",
|
||||
status: "completed",
|
||||
conclusion: "success",
|
||||
expectText: "Workflow Step completed: build / Run tests:success",
|
||||
expectColor: greenColor,
|
||||
},
|
||||
{
|
||||
action: "completed",
|
||||
status: "completed",
|
||||
conclusion: "failure",
|
||||
expectText: "Workflow Step completed: build / Run tests:failure",
|
||||
expectColor: redColor,
|
||||
},
|
||||
{
|
||||
action: "completed",
|
||||
status: "completed",
|
||||
conclusion: "cancelled",
|
||||
expectText: "Workflow Step completed: build / Run tests:cancelled",
|
||||
expectColor: yellowColor,
|
||||
},
|
||||
{
|
||||
action: "completed",
|
||||
status: "completed",
|
||||
conclusion: "skipped",
|
||||
expectText: "Workflow Step completed: build / Run tests:skipped",
|
||||
expectColor: purpleColor,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p := workflowStepTestPayload()
|
||||
p.Action = c.action
|
||||
p.Step.Status = c.status
|
||||
p.Step.Conclusion = c.conclusion
|
||||
text, color := getWorkflowStepPayloadInfo(p, noneLinkFormatter, false)
|
||||
assert.Equal(t, c.expectText, text, "case %d", i)
|
||||
assert.Equal(t, c.expectColor, color, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssueCommentPayloadInfo(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
|
||||
@@ -265,6 +265,12 @@ func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload,
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) WorkflowStep(p *api.WorkflowStepPayload) (MatrixPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
|
||||
|
||||
func getMessageBody(htmlText string) string {
|
||||
|
||||
@@ -346,6 +346,20 @@ func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (msteamsConvertor) WorkflowStep(p *api.WorkflowStepPayload) (MSTeamsPayload, error) {
|
||||
title, color := getWorkflowStepPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.WorkflowJob.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"WorkflowStep:", p.Step.Name},
|
||||
), nil
|
||||
}
|
||||
|
||||
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
|
||||
facts := make([]MSTeamsFact, 0, 2)
|
||||
if r != nil {
|
||||
|
||||
@@ -984,6 +984,55 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
|
||||
}
|
||||
}
|
||||
|
||||
func (*webhookNotifier) WorkflowStepStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask, step *actions_model.ActionTaskStep) {
|
||||
source := EventSource{
|
||||
Repository: repo,
|
||||
Owner: repo.Owner,
|
||||
}
|
||||
|
||||
var org *api.Organization
|
||||
if repo.Owner.IsOrganization() {
|
||||
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
|
||||
}
|
||||
|
||||
stepStatus, _ := convert.ToActionsStatus(step.Status)
|
||||
|
||||
convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
|
||||
if err != nil {
|
||||
log.Error("ToActionWorkflowJob: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the step index within the task steps for the Number field.
|
||||
stepIndex := int(step.Index)
|
||||
|
||||
convertedStep := convert.ToActionWorkflowStep(step, stepIndex)
|
||||
|
||||
// Build a minimal WorkflowRun for the payload.
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
log.Error("job.LoadAttributes: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
convertedRun, err := convert.ToActionWorkflowRun(ctx, job.Run, nil)
|
||||
if err != nil {
|
||||
log.Error("ToActionWorkflowRun: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowStep, &api.WorkflowStepPayload{
|
||||
Action: stepStatus,
|
||||
WorkflowRun: convertedRun,
|
||||
WorkflowJob: convertedJob,
|
||||
Step: convertedStep,
|
||||
Organization: org,
|
||||
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, sender, nil),
|
||||
}); err != nil {
|
||||
log.Error("PrepareWebhooks: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
source := EventSource{
|
||||
Repository: repo,
|
||||
|
||||
@@ -122,6 +122,10 @@ func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPa
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) WorkflowStep(_ *api.WorkflowStepPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
meta := &PackagistMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||
|
||||
@@ -31,6 +31,7 @@ type payloadConvertor[T any] interface {
|
||||
Status(*api.CommitStatusPayload) (T, error)
|
||||
WorkflowRun(*api.WorkflowRunPayload) (T, error)
|
||||
WorkflowJob(*api.WorkflowJobPayload) (T, error)
|
||||
WorkflowStep(*api.WorkflowStepPayload) (T, error)
|
||||
}
|
||||
|
||||
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
|
||||
@@ -86,6 +87,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
|
||||
return convertUnmarshalledJSON(rc.WorkflowRun, data)
|
||||
case webhook_module.HookEventWorkflowJob:
|
||||
return convertUnmarshalledJSON(rc.WorkflowJob, data)
|
||||
case webhook_module.HookEventWorkflowStep:
|
||||
return convertUnmarshalledJSON(rc.WorkflowStep, data)
|
||||
}
|
||||
return t, fmt.Errorf("newPayload unsupported event: %s", event)
|
||||
}
|
||||
|
||||
@@ -185,6 +185,12 @@ func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, er
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) WorkflowStep(p *api.WorkflowStepPayload) (SlackPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// Push implements payloadConvertor Push method
|
||||
func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
|
||||
// n new commits
|
||||
|
||||
@@ -155,6 +155,15 @@ func TestSlackPayload(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("WorkflowStep", func(t *testing.T) {
|
||||
p := workflowStepTestPayload()
|
||||
|
||||
pl, err := sc.WorkflowStep(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Workflow Step completed: <http://localhost:3000/test/repo/actions/runs/42/jobs/1|build / Run tests:success> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlackJSONPayload(t *testing.T) {
|
||||
|
||||
@@ -192,6 +192,12 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (telegramConvertor) WorkflowStep(p *api.WorkflowStepPayload) (TelegramPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
|
||||
// https://core.telegram.org/bots/api#formatting-options
|
||||
return TelegramPayload{
|
||||
|
||||
@@ -193,6 +193,12 @@ func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (Wechatwork
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) WorkflowStep(p *api.WorkflowStepPayload) (WechatworkPayload, error) {
|
||||
text, _ := getWorkflowStepPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
|
||||
@@ -346,6 +346,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Workflow Step Event -->
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="workflow_step" type="checkbox" {{if .Webhook.HookEvents.Get "workflow_step"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_workflow_step"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_workflow_step_desc"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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