mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	Add workflow_run api + webhook (#33964)
Implements - https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28#list-jobs-for-a-workflow-run--code-samples - https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28#get-a-job-for-a-workflow-run--code-samples - https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository - https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#get-a-workflow-run - `/actions/runs` for global + user + org (Gitea only) - `/actions/jobs` for global + user + org + repository (Gitea only) - workflow_run webhook + action trigger - limitations - workflow id is assigned to a string, this may result into problems in strongly typed clients Fixes - workflow_job webhook url to no longer contain the `runs/<run>` part to align with api - workflow instance does now use it's name inside the file instead of filename if set Refactoring - Moved a lot of logic from workflows/workflow_job into a shared module used by both webhook and api TODO - [x] Verify Keda Compatibility - [x] Edit Webhook API bug is resolved Closes https://github.com/go-gitea/gitea/issues/23670 Closes https://github.com/go-gitea/gitea/issues/23796 Closes https://github.com/go-gitea/gitea/issues/24898 Replaces https://github.com/go-gitea/gitea/pull/28047 and is much more complete --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -166,6 +166,17 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err | |||||||
| 	return nil, fmt.Errorf("event %s is not a pull request event", run.Event) | 	return nil, fmt.Errorf("event %s is not a pull request event", run.Event) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) { | ||||||
|  | 	if run.Event == webhook_module.HookEventWorkflowRun { | ||||||
|  | 		var payload api.WorkflowRunPayload | ||||||
|  | 		if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return &payload, nil | ||||||
|  | 	} | ||||||
|  | 	return nil, fmt.Errorf("event %s is not a workflow run event", run.Event) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (run *ActionRun) IsSchedule() bool { | func (run *ActionRun) IsSchedule() bool { | ||||||
| 	return run.ScheduleID > 0 | 	return run.ScheduleID > 0 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -80,22 +80,31 @@ type FindRunJobOptions struct { | |||||||
| func (opts FindRunJobOptions) ToConds() builder.Cond { | func (opts FindRunJobOptions) ToConds() builder.Cond { | ||||||
| 	cond := builder.NewCond() | 	cond := builder.NewCond() | ||||||
| 	if opts.RunID > 0 { | 	if opts.RunID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"run_id": opts.RunID}) | 		cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID}) | ||||||
| 	} | 	} | ||||||
| 	if opts.RepoID > 0 { | 	if opts.RepoID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | 		cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) | ||||||
| 	} |  | ||||||
| 	if opts.OwnerID > 0 { |  | ||||||
| 		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) |  | ||||||
| 	} | 	} | ||||||
| 	if opts.CommitSHA != "" { | 	if opts.CommitSHA != "" { | ||||||
| 		cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) | 		cond = cond.And(builder.Eq{"`action_run_job`.commit_sha": opts.CommitSHA}) | ||||||
| 	} | 	} | ||||||
| 	if len(opts.Statuses) > 0 { | 	if len(opts.Statuses) > 0 { | ||||||
| 		cond = cond.And(builder.In("status", opts.Statuses)) | 		cond = cond.And(builder.In("`action_run_job`.status", opts.Statuses)) | ||||||
| 	} | 	} | ||||||
| 	if opts.UpdatedBefore > 0 { | 	if opts.UpdatedBefore > 0 { | ||||||
| 		cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) | 		cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) | ||||||
| 	} | 	} | ||||||
| 	return cond | 	return cond | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (opts FindRunJobOptions) ToJoins() []db.JoinFunc { | ||||||
|  | 	if opts.OwnerID > 0 { | ||||||
|  | 		return []db.JoinFunc{ | ||||||
|  | 			func(sess db.Engine) error { | ||||||
|  | 				sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) | ||||||
|  | 				return nil | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -72,39 +72,50 @@ type FindRunOptions struct { | |||||||
| 	TriggerEvent  webhook_module.HookEventType | 	TriggerEvent  webhook_module.HookEventType | ||||||
| 	Approved      bool // not util.OptionalBool, it works only when it's true | 	Approved      bool // not util.OptionalBool, it works only when it's true | ||||||
| 	Status        []Status | 	Status        []Status | ||||||
|  | 	CommitSHA     string | ||||||
| } | } | ||||||
|  |  | ||||||
| func (opts FindRunOptions) ToConds() builder.Cond { | func (opts FindRunOptions) ToConds() builder.Cond { | ||||||
| 	cond := builder.NewCond() | 	cond := builder.NewCond() | ||||||
| 	if opts.RepoID > 0 { | 	if opts.RepoID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | 		cond = cond.And(builder.Eq{"`action_run`.repo_id": opts.RepoID}) | ||||||
| 	} |  | ||||||
| 	if opts.OwnerID > 0 { |  | ||||||
| 		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) |  | ||||||
| 	} | 	} | ||||||
| 	if opts.WorkflowID != "" { | 	if opts.WorkflowID != "" { | ||||||
| 		cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID}) | 		cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID}) | ||||||
| 	} | 	} | ||||||
| 	if opts.TriggerUserID > 0 { | 	if opts.TriggerUserID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID}) | 		cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID}) | ||||||
| 	} | 	} | ||||||
| 	if opts.Approved { | 	if opts.Approved { | ||||||
| 		cond = cond.And(builder.Gt{"approved_by": 0}) | 		cond = cond.And(builder.Gt{"`action_run`.approved_by": 0}) | ||||||
| 	} | 	} | ||||||
| 	if len(opts.Status) > 0 { | 	if len(opts.Status) > 0 { | ||||||
| 		cond = cond.And(builder.In("status", opts.Status)) | 		cond = cond.And(builder.In("`action_run`.status", opts.Status)) | ||||||
| 	} | 	} | ||||||
| 	if opts.Ref != "" { | 	if opts.Ref != "" { | ||||||
| 		cond = cond.And(builder.Eq{"ref": opts.Ref}) | 		cond = cond.And(builder.Eq{"`action_run`.ref": opts.Ref}) | ||||||
| 	} | 	} | ||||||
| 	if opts.TriggerEvent != "" { | 	if opts.TriggerEvent != "" { | ||||||
| 		cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) | 		cond = cond.And(builder.Eq{"`action_run`.trigger_event": opts.TriggerEvent}) | ||||||
|  | 	} | ||||||
|  | 	if opts.CommitSHA != "" { | ||||||
|  | 		cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) | ||||||
| 	} | 	} | ||||||
| 	return cond | 	return cond | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (opts FindRunOptions) ToJoins() []db.JoinFunc { | ||||||
|  | 	if opts.OwnerID > 0 { | ||||||
|  | 		return []db.JoinFunc{func(sess db.Engine) error { | ||||||
|  | 			sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) | ||||||
|  | 			return nil | ||||||
|  | 		}} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (opts FindRunOptions) ToOrders() string { | func (opts FindRunOptions) ToOrders() string { | ||||||
| 	return "`id` DESC" | 	return "`action_run`.`id` DESC" | ||||||
| } | } | ||||||
|  |  | ||||||
| type StatusInfo struct { | type StatusInfo struct { | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ | |||||||
|   ref: "refs/heads/master" |   ref: "refs/heads/master" | ||||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|   event: "push" |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|   is_fork_pull_request: 0 |   is_fork_pull_request: 0 | ||||||
|   status: 1 |   status: 1 | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
| @@ -28,6 +29,7 @@ | |||||||
|   ref: "refs/heads/master" |   ref: "refs/heads/master" | ||||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|   event: "push" |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|   is_fork_pull_request: 0 |   is_fork_pull_request: 0 | ||||||
|   status: 1 |   status: 1 | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
| @@ -47,6 +49,7 @@ | |||||||
|   ref: "refs/heads/master" |   ref: "refs/heads/master" | ||||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|   event: "push" |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|   is_fork_pull_request: 0 |   is_fork_pull_request: 0 | ||||||
|   status: 6 # running |   status: 6 # running | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
| @@ -66,6 +69,47 @@ | |||||||
|   ref: "refs/heads/test" |   ref: "refs/heads/test" | ||||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|   event: "push" |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|  |   is_fork_pull_request: 0 | ||||||
|  |   status: 1 | ||||||
|  |   started: 1683636528 | ||||||
|  |   stopped: 1683636626 | ||||||
|  |   created: 1683636108 | ||||||
|  |   updated: 1683636626 | ||||||
|  |   need_approval: 0 | ||||||
|  |   approved_by: 0 | ||||||
|  | - | ||||||
|  |   id: 802 | ||||||
|  |   title: "workflow run list" | ||||||
|  |   repo_id: 5 | ||||||
|  |   owner_id: 3 | ||||||
|  |   workflow_id: "test.yaml" | ||||||
|  |   index: 191 | ||||||
|  |   trigger_user_id: 1 | ||||||
|  |   ref: "refs/heads/test" | ||||||
|  |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|  |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|  |   is_fork_pull_request: 0 | ||||||
|  |   status: 1 | ||||||
|  |   started: 1683636528 | ||||||
|  |   stopped: 1683636626 | ||||||
|  |   created: 1683636108 | ||||||
|  |   updated: 1683636626 | ||||||
|  |   need_approval: 0 | ||||||
|  |   approved_by: 0 | ||||||
|  | - | ||||||
|  |   id: 803 | ||||||
|  |   title: "workflow run list for user" | ||||||
|  |   repo_id: 2 | ||||||
|  |   owner_id: 0 | ||||||
|  |   workflow_id: "test.yaml" | ||||||
|  |   index: 192 | ||||||
|  |   trigger_user_id: 1 | ||||||
|  |   ref: "refs/heads/test" | ||||||
|  |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|  |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|   is_fork_pull_request: 0 |   is_fork_pull_request: 0 | ||||||
|   status: 1 |   status: 1 | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
| @@ -86,6 +130,7 @@ | |||||||
|   ref: "refs/heads/test" |   ref: "refs/heads/test" | ||||||
|   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" |   commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" | ||||||
|   event: "push" |   event: "push" | ||||||
|  |   trigger_event: "push" | ||||||
|   is_fork_pull_request: 0 |   is_fork_pull_request: 0 | ||||||
|   status: 2 |   status: 2 | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
|   | |||||||
| @@ -99,3 +99,33 @@ | |||||||
|   status: 2 |   status: 2 | ||||||
|   started: 1683636528 |   started: 1683636528 | ||||||
|   stopped: 1683636626 |   stopped: 1683636626 | ||||||
|  | - | ||||||
|  |   id: 203 | ||||||
|  |   run_id: 802 | ||||||
|  |   repo_id: 5 | ||||||
|  |   owner_id: 0 | ||||||
|  |   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 | ||||||
|  | - | ||||||
|  |   id: 204 | ||||||
|  |   run_id: 803 | ||||||
|  |   repo_id: 2 | ||||||
|  |   owner_id: 0 | ||||||
|  |   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 | ||||||
|   | |||||||
| @@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) { | |||||||
| 		"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", | 		"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", | ||||||
| 		"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", | 		"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", | 		"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release", | ||||||
| 		"package", "status", "workflow_job", | 		"package", "status", "workflow_run", "workflow_job", | ||||||
| 	}, | 	}, | ||||||
| 		(&Webhook{ | 		(&Webhook{ | ||||||
| 			HookEvent: &webhook_module.HookEvent{SendEverything: true}, | 			HookEvent: &webhook_module.HookEvent{SendEverything: true}, | ||||||
|   | |||||||
| @@ -246,6 +246,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web | |||||||
| 		webhook_module.HookEventPackage: | 		webhook_module.HookEventPackage: | ||||||
| 		return matchPackageEvent(payload.(*api.PackagePayload), evt) | 		return matchPackageEvent(payload.(*api.PackagePayload), evt) | ||||||
|  |  | ||||||
|  | 	case // workflow_run | ||||||
|  | 		webhook_module.HookEventWorkflowRun: | ||||||
|  | 		return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) | ||||||
|  |  | ||||||
| 	default: | 	default: | ||||||
| 		log.Warn("unsupported event %q", triggedEvent) | 		log.Warn("unsupported event %q", triggedEvent) | ||||||
| 		return false | 		return false | ||||||
| @@ -691,3 +695,53 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool { | |||||||
| 	} | 	} | ||||||
| 	return matchTimes == len(evt.Acts()) | 	return matchTimes == len(evt.Acts()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool { | ||||||
|  | 	// with no special filter parameters | ||||||
|  | 	if len(evt.Acts()) == 0 { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	matchTimes := 0 | ||||||
|  | 	// all acts conditions should be satisfied | ||||||
|  | 	for cond, vals := range evt.Acts() { | ||||||
|  | 		switch cond { | ||||||
|  | 		case "types": | ||||||
|  | 			action := payload.Action | ||||||
|  | 			for _, val := range vals { | ||||||
|  | 				if glob.MustCompile(val, '/').Match(action) { | ||||||
|  | 					matchTimes++ | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		case "workflows": | ||||||
|  | 			workflow := payload.Workflow | ||||||
|  | 			patterns, err := workflowpattern.CompilePatterns(vals...) | ||||||
|  | 			if err != nil { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) { | ||||||
|  | 				matchTimes++ | ||||||
|  | 			} | ||||||
|  | 		case "branches": | ||||||
|  | 			patterns, err := workflowpattern.CompilePatterns(vals...) | ||||||
|  | 			if err != nil { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { | ||||||
|  | 				matchTimes++ | ||||||
|  | 			} | ||||||
|  | 		case "branches-ignore": | ||||||
|  | 			patterns, err := workflowpattern.CompilePatterns(vals...) | ||||||
|  | 			if err != nil { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { | ||||||
|  | 				matchTimes++ | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			log.Warn("workflow run event unsupported condition %q", cond) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return matchTimes == len(evt.Acts()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -470,6 +470,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) { | |||||||
| 	return json.MarshalIndent(p, "", "  ") | 	return json.MarshalIndent(p, "", "  ") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // WorkflowRunPayload represents a payload information of workflow run event. | ||||||
|  | type WorkflowRunPayload struct { | ||||||
|  | 	Action       string             `json:"action"` | ||||||
|  | 	Workflow     *ActionWorkflow    `json:"workflow"` | ||||||
|  | 	WorkflowRun  *ActionWorkflowRun `json:"workflow_run"` | ||||||
|  | 	PullRequest  *PullRequest       `json:"pull_request,omitempty"` | ||||||
|  | 	Organization *Organization      `json:"organization,omitempty"` | ||||||
|  | 	Repo         *Repository        `json:"repository"` | ||||||
|  | 	Sender       *User              `json:"sender"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // JSONPayload implements Payload | ||||||
|  | func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) { | ||||||
|  | 	return json.MarshalIndent(p, "", "  ") | ||||||
|  | } | ||||||
|  |  | ||||||
| // WorkflowJobPayload represents a payload information of workflow job event. | // WorkflowJobPayload represents a payload information of workflow job event. | ||||||
| type WorkflowJobPayload struct { | type WorkflowJobPayload struct { | ||||||
| 	Action       string             `json:"action"` | 	Action       string             `json:"action"` | ||||||
|   | |||||||
| @@ -86,9 +86,39 @@ type ActionArtifact struct { | |||||||
|  |  | ||||||
| // ActionWorkflowRun represents a WorkflowRun | // ActionWorkflowRun represents a WorkflowRun | ||||||
| type ActionWorkflowRun struct { | type ActionWorkflowRun struct { | ||||||
| 	ID           int64  `json:"id"` | 	ID             int64       `json:"id"` | ||||||
| 	RepositoryID int64  `json:"repository_id"` | 	URL            string      `json:"url"` | ||||||
| 	HeadSha      string `json:"head_sha"` | 	HTMLURL        string      `json:"html_url"` | ||||||
|  | 	DisplayTitle   string      `json:"display_title"` | ||||||
|  | 	Path           string      `json:"path"` | ||||||
|  | 	Event          string      `json:"event"` | ||||||
|  | 	RunAttempt     int64       `json:"run_attempt"` | ||||||
|  | 	RunNumber      int64       `json:"run_number"` | ||||||
|  | 	RepositoryID   int64       `json:"repository_id,omitempty"` | ||||||
|  | 	HeadSha        string      `json:"head_sha"` | ||||||
|  | 	HeadBranch     string      `json:"head_branch,omitempty"` | ||||||
|  | 	Status         string      `json:"status"` | ||||||
|  | 	Actor          *User       `json:"actor,omitempty"` | ||||||
|  | 	TriggerActor   *User       `json:"trigger_actor,omitempty"` | ||||||
|  | 	Repository     *Repository `json:"repository,omitempty"` | ||||||
|  | 	HeadRepository *Repository `json:"head_repository,omitempty"` | ||||||
|  | 	Conclusion     string      `json:"conclusion,omitempty"` | ||||||
|  | 	// swagger:strfmt date-time | ||||||
|  | 	StartedAt time.Time `json:"started_at"` | ||||||
|  | 	// swagger:strfmt date-time | ||||||
|  | 	CompletedAt time.Time `json:"completed_at"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ActionWorkflowRunsResponse returns ActionWorkflowRuns | ||||||
|  | type ActionWorkflowRunsResponse struct { | ||||||
|  | 	Entries    []*ActionWorkflowRun `json:"workflow_runs"` | ||||||
|  | 	TotalCount int64                `json:"total_count"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ActionWorkflowJobsResponse returns ActionWorkflowJobs | ||||||
|  | type ActionWorkflowJobsResponse struct { | ||||||
|  | 	Entries    []*ActionWorkflowJob `json:"jobs"` | ||||||
|  | 	TotalCount int64                `json:"total_count"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // ActionArtifactsResponse returns ActionArtifacts | // ActionArtifactsResponse returns ActionArtifacts | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ const ( | |||||||
| 	HookEventPullRequestReview HookEventType = "pull_request_review" | 	HookEventPullRequestReview HookEventType = "pull_request_review" | ||||||
| 	// Actions event only | 	// Actions event only | ||||||
| 	HookEventSchedule    HookEventType = "schedule" | 	HookEventSchedule    HookEventType = "schedule" | ||||||
|  | 	HookEventWorkflowRun HookEventType = "workflow_run" | ||||||
| 	HookEventWorkflowJob HookEventType = "workflow_job" | 	HookEventWorkflowJob HookEventType = "workflow_job" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -67,6 +68,7 @@ func AllEvents() []HookEventType { | |||||||
| 		HookEventRelease, | 		HookEventRelease, | ||||||
| 		HookEventPackage, | 		HookEventPackage, | ||||||
| 		HookEventStatus, | 		HookEventStatus, | ||||||
|  | 		HookEventWorkflowRun, | ||||||
| 		HookEventWorkflowJob, | 		HookEventWorkflowJob, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2403,6 +2403,8 @@ settings.event_pull_request_review_request_desc = Pull request review requested | |||||||
| settings.event_pull_request_approvals = Pull Request Approvals | settings.event_pull_request_approvals = Pull Request Approvals | ||||||
| settings.event_pull_request_merge = Pull Request Merge | settings.event_pull_request_merge = Pull Request Merge | ||||||
| settings.event_header_workflow = Workflow Events | settings.event_header_workflow = Workflow Events | ||||||
|  | settings.event_workflow_run = Workflow Run | ||||||
|  | settings.event_workflow_run_desc = Gitea Actions Workflow run queued, waiting, in progress, or completed. | ||||||
| settings.event_workflow_job = Workflow Jobs | settings.event_workflow_job = Workflow Jobs | ||||||
| settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed. | settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed. | ||||||
| settings.event_package = Package | settings.event_package = Package | ||||||
|   | |||||||
							
								
								
									
										93
									
								
								routers/api/v1/admin/action.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								routers/api/v1/admin/action.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package admin | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"code.gitea.io/gitea/routers/api/v1/shared" | ||||||
|  | 	"code.gitea.io/gitea/services/context" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ListWorkflowJobs Lists all jobs | ||||||
|  | func ListWorkflowJobs(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Lists all jobs | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJobsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	shared.ListJobs(ctx, 0, 0, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ListWorkflowRuns Lists all runs | ||||||
|  | func ListWorkflowRuns(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Lists all runs | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: event | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow event name | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: branch | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow branch | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: actor | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggered by user | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: head_sha | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggering sha of the workflow run | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowRunsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	shared.ListRuns(ctx, 0, 0) | ||||||
|  | } | ||||||
| @@ -942,6 +942,8 @@ func Routes() *web.Router { | |||||||
| 				m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) | 				m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) | ||||||
| 				m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) | 				m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) | ||||||
| 			}) | 			}) | ||||||
|  | 			m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns) | ||||||
|  | 			m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -1078,6 +1080,9 @@ func Routes() *web.Router { | |||||||
| 					m.Get("/{runner_id}", reqToken(), user.GetRunner) | 					m.Get("/{runner_id}", reqToken(), user.GetRunner) | ||||||
| 					m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) | 					m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) | ||||||
| 				}) | 				}) | ||||||
|  |  | ||||||
|  | 				m.Get("/runs", reqToken(), user.ListWorkflowRuns) | ||||||
|  | 				m.Get("/jobs", reqToken(), user.ListWorkflowJobs) | ||||||
| 			}) | 			}) | ||||||
|  |  | ||||||
| 			m.Get("/followers", user.ListMyFollowers) | 			m.Get("/followers", user.ListMyFollowers) | ||||||
| @@ -1202,6 +1207,7 @@ func Routes() *web.Router { | |||||||
| 				}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) | 				}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) | ||||||
|  |  | ||||||
| 				m.Group("/actions/jobs", func() { | 				m.Group("/actions/jobs", func() { | ||||||
|  | 					m.Get("/{job_id}", repo.GetWorkflowJob) | ||||||
| 					m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) | 					m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) | ||||||
| 				}, reqToken(), reqRepoReader(unit.TypeActions)) | 				}, reqToken(), reqRepoReader(unit.TypeActions)) | ||||||
|  |  | ||||||
| @@ -1280,9 +1286,13 @@ func Routes() *web.Router { | |||||||
| 				}, reqToken(), reqAdmin()) | 				}, reqToken(), reqAdmin()) | ||||||
| 				m.Group("/actions", func() { | 				m.Group("/actions", func() { | ||||||
| 					m.Get("/tasks", repo.ListActionTasks) | 					m.Get("/tasks", repo.ListActionTasks) | ||||||
| 					m.Group("/runs/{run}", func() { | 					m.Group("/runs", func() { | ||||||
| 						m.Get("/artifacts", repo.GetArtifactsOfRun) | 						m.Group("/{run}", func() { | ||||||
| 						m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) | 							m.Get("", repo.GetWorkflowRun) | ||||||
|  | 							m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) | ||||||
|  | 							m.Get("/jobs", repo.ListWorkflowRunJobs) | ||||||
|  | 							m.Get("/artifacts", repo.GetArtifactsOfRun) | ||||||
|  | 						}) | ||||||
| 					}) | 					}) | ||||||
| 					m.Get("/artifacts", repo.GetArtifacts) | 					m.Get("/artifacts", repo.GetArtifacts) | ||||||
| 					m.Group("/artifacts/{artifact_id}", func() { | 					m.Group("/artifacts/{artifact_id}", func() { | ||||||
| @@ -1734,11 +1744,15 @@ func Routes() *web.Router { | |||||||
| 					Patch(bind(api.EditHookOption{}), admin.EditHook). | 					Patch(bind(api.EditHookOption{}), admin.EditHook). | ||||||
| 					Delete(admin.DeleteHook) | 					Delete(admin.DeleteHook) | ||||||
| 			}) | 			}) | ||||||
| 			m.Group("/actions/runners", func() { | 			m.Group("/actions", func() { | ||||||
| 				m.Get("", admin.ListRunners) | 				m.Group("/runners", func() { | ||||||
| 				m.Post("/registration-token", admin.CreateRegistrationToken) | 					m.Get("", admin.ListRunners) | ||||||
| 				m.Get("/{runner_id}", admin.GetRunner) | 					m.Post("/registration-token", admin.CreateRegistrationToken) | ||||||
| 				m.Delete("/{runner_id}", admin.DeleteRunner) | 					m.Get("/{runner_id}", admin.GetRunner) | ||||||
|  | 					m.Delete("/{runner_id}", admin.DeleteRunner) | ||||||
|  | 				}) | ||||||
|  | 				m.Get("/runs", admin.ListWorkflowRuns) | ||||||
|  | 				m.Get("/jobs", admin.ListWorkflowJobs) | ||||||
| 			}) | 			}) | ||||||
| 			m.Group("/runners", func() { | 			m.Group("/runners", func() { | ||||||
| 				m.Get("/registration-token", admin.GetRegistrationToken) | 				m.Get("/registration-token", admin.GetRegistrationToken) | ||||||
|   | |||||||
| @@ -570,6 +570,96 @@ func (Action) DeleteRunner(ctx *context.APIContext) { | |||||||
| 	shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) | 	shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (Action) ListWorkflowJobs(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Get org-level workflow jobs | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: org | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the organization | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJobsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  | 	shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (Action) ListWorkflowRuns(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Get org-level workflow runs | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: org | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the organization | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: event | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow event name | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: branch | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow branch | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: actor | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggered by user | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: head_sha | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggering sha of the workflow run | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowRunsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  | 	shared.ListRuns(ctx, ctx.Org.Organization.ID, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
| var _ actions_service.API = new(Action) | var _ actions_service.API = new(Action) | ||||||
|  |  | ||||||
| // Action implements actions_service.API | // Action implements actions_service.API | ||||||
|   | |||||||
| @@ -650,6 +650,114 @@ func (Action) DeleteRunner(ctx *context.APIContext) { | |||||||
| 	shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) | 	shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetWorkflowRunJobs Lists all jobs for a workflow run. | ||||||
|  | func (Action) ListWorkflowJobs(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Lists all jobs for a repository | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the owner | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repository | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJobsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	repoID := ctx.Repo.Repository.ID | ||||||
|  |  | ||||||
|  | 	shared.ListJobs(ctx, 0, repoID, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ListWorkflowRuns Lists all runs for a repository run. | ||||||
|  | func (Action) ListWorkflowRuns(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Lists all runs for a repository run | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the owner | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repository | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: event | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow event name | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: branch | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow branch | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: actor | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggered by user | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: head_sha | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggering sha of the workflow run | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/ArtifactsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	repoID := ctx.Repo.Repository.ID | ||||||
|  |  | ||||||
|  | 	shared.ListRuns(ctx, 0, repoID) | ||||||
|  | } | ||||||
|  |  | ||||||
| var _ actions_service.API = new(Action) | var _ actions_service.API = new(Action) | ||||||
|  |  | ||||||
| // Action implements actions_service.API | // Action implements actions_service.API | ||||||
| @@ -756,7 +864,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) { | |||||||
| 	//   "500": | 	//   "500": | ||||||
| 	//     "$ref": "#/responses/error" | 	//     "$ref": "#/responses/error" | ||||||
|  |  | ||||||
| 	workflows, err := actions_service.ListActionWorkflows(ctx) | 	workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.APIErrorInternal(err) | 		ctx.APIErrorInternal(err) | ||||||
| 		return | 		return | ||||||
| @@ -802,7 +910,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) { | |||||||
| 	//     "$ref": "#/responses/error" | 	//     "$ref": "#/responses/error" | ||||||
|  |  | ||||||
| 	workflowID := ctx.PathParam("workflow_id") | 	workflowID := ctx.PathParam("workflow_id") | ||||||
| 	workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) | 	workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, util.ErrNotExist) { | 		if errors.Is(err, util.ErrNotExist) { | ||||||
| 			ctx.APIError(http.StatusNotFound, err) | 			ctx.APIError(http.StatusNotFound, err) | ||||||
| @@ -992,6 +1100,157 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { | |||||||
| 	ctx.Status(http.StatusNoContent) | 	ctx.Status(http.StatusNoContent) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetWorkflowRun Gets a specific workflow run. | ||||||
|  | func GetWorkflowRun(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Gets a specific workflow run | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the owner | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repository | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: run | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: id of the run | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowRun" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	runID := ctx.PathParamInt64("run") | ||||||
|  | 	job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID) | ||||||
|  |  | ||||||
|  | 	if err != nil || job.RepoID != ctx.Repo.Repository.ID { | ||||||
|  | 		ctx.APIError(http.StatusNotFound, util.ErrNotExist) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.APIErrorInternal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.JSON(http.StatusOK, convertedArtifact) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ListWorkflowRunJobs Lists all jobs for a workflow run. | ||||||
|  | func ListWorkflowRunJobs(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Lists all jobs for a workflow run | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the owner | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repository | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: run | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: runid of the workflow run | ||||||
|  | 	//   type: integer | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJobsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	repoID := ctx.Repo.Repository.ID | ||||||
|  |  | ||||||
|  | 	runID := ctx.PathParamInt64("run") | ||||||
|  |  | ||||||
|  | 	// Avoid the list all jobs functionality for this api route to be used with a runID == 0. | ||||||
|  | 	if runID <= 0 { | ||||||
|  | 		ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer")) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID. | ||||||
|  | 	// no additional checks for runID are needed here | ||||||
|  | 	shared.ListJobs(ctx, 0, repoID, runID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetWorkflowJob Gets a specific workflow job for a workflow run. | ||||||
|  | func GetWorkflowJob(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Gets a specific workflow job for a workflow run | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: owner | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the owner | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: repo | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: name of the repository | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// - name: job_id | ||||||
|  | 	//   in: path | ||||||
|  | 	//   description: id of the job | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: true | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJob" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	jobID := ctx.PathParamInt64("job_id") | ||||||
|  | 	job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID) | ||||||
|  |  | ||||||
|  | 	if err != nil || job.RepoID != ctx.Repo.Repository.ID { | ||||||
|  | 		ctx.APIError(http.StatusNotFound, util.ErrNotExist) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.APIErrorInternal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.JSON(http.StatusOK, convertedWorkflowJob) | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetArtifacts Lists all artifacts for a repository. | // GetArtifacts Lists all artifacts for a repository. | ||||||
| func GetArtifactsOfRun(ctx *context.APIContext) { | func GetArtifactsOfRun(ctx *context.APIContext) { | ||||||
| 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun | 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun | ||||||
|   | |||||||
							
								
								
									
										187
									
								
								routers/api/v1/shared/action.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								routers/api/v1/shared/action.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package shared | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/modules/webhook" | ||||||
|  | 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||||
|  | 	"code.gitea.io/gitea/services/context" | ||||||
|  | 	"code.gitea.io/gitea/services/convert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ListJobs lists jobs for api route validated ownerID and repoID | ||||||
|  | // ownerID == 0 and repoID == 0 means all jobs | ||||||
|  | // ownerID == 0 and repoID != 0 means all jobs for the given repo | ||||||
|  | // ownerID != 0 and repoID == 0 means all jobs for the given user/org | ||||||
|  | // ownerID != 0 and repoID != 0 undefined behavior | ||||||
|  | // runID == 0 means all jobs | ||||||
|  | // runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run | ||||||
|  | // Access rights are checked at the API route level | ||||||
|  | func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { | ||||||
|  | 	if ownerID != 0 && repoID != 0 { | ||||||
|  | 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||||
|  | 	} | ||||||
|  | 	opts := actions_model.FindRunJobOptions{ | ||||||
|  | 		OwnerID:     ownerID, | ||||||
|  | 		RepoID:      repoID, | ||||||
|  | 		RunID:       runID, | ||||||
|  | 		ListOptions: utils.GetListOptions(ctx), | ||||||
|  | 	} | ||||||
|  | 	for _, status := range ctx.FormStrings("status") { | ||||||
|  | 		values, err := convertToInternal(status) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		opts.Statuses = append(opts.Statuses, values...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.APIErrorInternal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res := new(api.ActionWorkflowJobsResponse) | ||||||
|  | 	res.TotalCount = total | ||||||
|  |  | ||||||
|  | 	res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) | ||||||
|  |  | ||||||
|  | 	isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID | ||||||
|  | 	for i := range jobs { | ||||||
|  | 		var repository *repo_model.Repository | ||||||
|  | 		if isRepoLevel { | ||||||
|  | 			repository = ctx.Repo.Repository | ||||||
|  | 		} else { | ||||||
|  | 			repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.APIErrorInternal(err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.APIErrorInternal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		res.Entries[i] = convertedWorkflowJob | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(http.StatusOK, &res) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertToInternal(s string) ([]actions_model.Status, error) { | ||||||
|  | 	switch s { | ||||||
|  | 	case "pending", "waiting", "requested", "action_required": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusBlocked}, nil | ||||||
|  | 	case "queued": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusWaiting}, nil | ||||||
|  | 	case "in_progress": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusRunning}, nil | ||||||
|  | 	case "completed": | ||||||
|  | 		return []actions_model.Status{ | ||||||
|  | 			actions_model.StatusSuccess, | ||||||
|  | 			actions_model.StatusFailure, | ||||||
|  | 			actions_model.StatusSkipped, | ||||||
|  | 			actions_model.StatusCancelled, | ||||||
|  | 		}, nil | ||||||
|  | 	case "failure": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusFailure}, nil | ||||||
|  | 	case "success": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusSuccess}, nil | ||||||
|  | 	case "skipped", "neutral": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusSkipped}, nil | ||||||
|  | 	case "cancelled", "timed_out": | ||||||
|  | 		return []actions_model.Status{actions_model.StatusCancelled}, nil | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("invalid status %s", s) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ListRuns lists jobs for api route validated ownerID and repoID | ||||||
|  | // ownerID == 0 and repoID == 0 means all runs | ||||||
|  | // ownerID == 0 and repoID != 0 means all runs for the given repo | ||||||
|  | // ownerID != 0 and repoID == 0 means all runs for the given user/org | ||||||
|  | // ownerID != 0 and repoID != 0 undefined behavior | ||||||
|  | // Access rights are checked at the API route level | ||||||
|  | func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { | ||||||
|  | 	if ownerID != 0 && repoID != 0 { | ||||||
|  | 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||||
|  | 	} | ||||||
|  | 	opts := actions_model.FindRunOptions{ | ||||||
|  | 		OwnerID:     ownerID, | ||||||
|  | 		RepoID:      repoID, | ||||||
|  | 		ListOptions: utils.GetListOptions(ctx), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if event := ctx.FormString("event"); event != "" { | ||||||
|  | 		opts.TriggerEvent = webhook.HookEventType(event) | ||||||
|  | 	} | ||||||
|  | 	if branch := ctx.FormString("branch"); branch != "" { | ||||||
|  | 		opts.Ref = string(git.RefNameFromBranch(branch)) | ||||||
|  | 	} | ||||||
|  | 	for _, status := range ctx.FormStrings("status") { | ||||||
|  | 		values, err := convertToInternal(status) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		opts.Status = append(opts.Status, values...) | ||||||
|  | 	} | ||||||
|  | 	if actor := ctx.FormString("actor"); actor != "" { | ||||||
|  | 		user, err := user_model.GetUserByName(ctx, actor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.APIErrorInternal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		opts.TriggerUserID = user.ID | ||||||
|  | 	} | ||||||
|  | 	if headSHA := ctx.FormString("head_sha"); headSHA != "" { | ||||||
|  | 		opts.CommitSHA = headSHA | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.APIErrorInternal(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res := new(api.ActionWorkflowRunsResponse) | ||||||
|  | 	res.TotalCount = total | ||||||
|  |  | ||||||
|  | 	res.Entries = make([]*api.ActionWorkflowRun, len(runs)) | ||||||
|  | 	isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID | ||||||
|  | 	for i := range runs { | ||||||
|  | 		var repository *repo_model.Repository | ||||||
|  | 		if isRepoLevel { | ||||||
|  | 			repository = ctx.Repo.Repository | ||||||
|  | 		} else { | ||||||
|  | 			repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.APIErrorInternal(err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.APIErrorInternal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		res.Entries[i] = convertedRun | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.JSON(http.StatusOK, &res) | ||||||
|  | } | ||||||
| @@ -443,6 +443,34 @@ type swaggerRepoTasksList struct { | |||||||
| 	Body api.ActionTaskResponse `json:"body"` | 	Body api.ActionTaskResponse `json:"body"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // WorkflowRunsList | ||||||
|  | // swagger:response WorkflowRunsList | ||||||
|  | type swaggerActionWorkflowRunsResponse struct { | ||||||
|  | 	// in:body | ||||||
|  | 	Body api.ActionWorkflowRunsResponse `json:"body"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WorkflowRun | ||||||
|  | // swagger:response WorkflowRun | ||||||
|  | type swaggerWorkflowRun struct { | ||||||
|  | 	// in:body | ||||||
|  | 	Body api.ActionWorkflowRun `json:"body"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WorkflowJobsList | ||||||
|  | // swagger:response WorkflowJobsList | ||||||
|  | type swaggerActionWorkflowJobsResponse struct { | ||||||
|  | 	// in:body | ||||||
|  | 	Body api.ActionWorkflowJobsResponse `json:"body"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WorkflowJob | ||||||
|  | // swagger:response WorkflowJob | ||||||
|  | type swaggerWorkflowJob struct { | ||||||
|  | 	// in:body | ||||||
|  | 	Body api.ActionWorkflowJob `json:"body"` | ||||||
|  | } | ||||||
|  |  | ||||||
| // ArtifactsList | // ArtifactsList | ||||||
| // swagger:response ArtifactsList | // swagger:response ArtifactsList | ||||||
| type swaggerRepoArtifactsList struct { | type swaggerRepoArtifactsList struct { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import ( | |||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
|  | 	"code.gitea.io/gitea/routers/api/v1/shared" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||||
| 	actions_service "code.gitea.io/gitea/services/actions" | 	actions_service "code.gitea.io/gitea/services/actions" | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| @@ -356,3 +357,86 @@ func ListVariables(ctx *context.APIContext) { | |||||||
| 	ctx.SetTotalCountHeader(count) | 	ctx.SetTotalCountHeader(count) | ||||||
| 	ctx.JSON(http.StatusOK, variables) | 	ctx.JSON(http.StatusOK, variables) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ListWorkflowRuns lists workflow runs | ||||||
|  | func ListWorkflowRuns(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /user/actions/runs user getUserWorkflowRuns | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Get workflow runs | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: event | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow event name | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: branch | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow branch | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: actor | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggered by user | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: head_sha | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: triggering sha of the workflow run | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowRunsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  | 	shared.ListRuns(ctx, ctx.Doer.ID, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ListWorkflowJobs lists workflow jobs | ||||||
|  | func ListWorkflowJobs(ctx *context.APIContext) { | ||||||
|  | 	// swagger:operation GET /user/actions/jobs user getUserWorkflowJobs | ||||||
|  | 	// --- | ||||||
|  | 	// summary: Get workflow jobs | ||||||
|  | 	// parameters: | ||||||
|  | 	// - name: status | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: workflow status (pending, queued, in_progress, failure, success, skipped) | ||||||
|  | 	//   type: string | ||||||
|  | 	//   required: false | ||||||
|  | 	// - name: page | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page number of results to return (1-based) | ||||||
|  | 	//   type: integer | ||||||
|  | 	// - name: limit | ||||||
|  | 	//   in: query | ||||||
|  | 	//   description: page size of results | ||||||
|  | 	//   type: integer | ||||||
|  | 	// produces: | ||||||
|  | 	// - application/json | ||||||
|  | 	// responses: | ||||||
|  | 	//   "200": | ||||||
|  | 	//     "$ref": "#/responses/WorkflowJobsList" | ||||||
|  | 	//   "400": | ||||||
|  | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
|  | 	shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -173,6 +173,7 @@ func updateHookEvents(events []string) webhook_module.HookEvents { | |||||||
| 	hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true) | 	hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true) | ||||||
| 	hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true) | 	hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true) | ||||||
| 	hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true) | 	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.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true) | ||||||
|  |  | ||||||
| 	// Issues | 	// Issues | ||||||
|   | |||||||
| @@ -304,7 +304,7 @@ func ViewPost(ctx *context_module.Context) { | |||||||
| 	if task != nil { | 	if task != nil { | ||||||
| 		steps, logs, err := convertToViewModel(ctx, req.LogCursors, task) | 		steps, logs, err := convertToViewModel(ctx, req.LogCursors, task) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("convertToViewModel", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...) | 		resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...) | ||||||
| @@ -408,7 +408,7 @@ func Rerun(ctx *context_module.Context) { | |||||||
|  |  | ||||||
| 	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) | 	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("GetRunByIndex", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -426,7 +426,7 @@ func Rerun(ctx *context_module.Context) { | |||||||
| 		run.Started = 0 | 		run.Started = 0 | ||||||
| 		run.Stopped = 0 | 		run.Stopped = 0 | ||||||
| 		if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { | 		if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("UpdateRun", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -441,7 +441,7 @@ func Rerun(ctx *context_module.Context) { | |||||||
| 			// if the job has needs, it should be set to "blocked" status to wait for other jobs | 			// if the job has needs, it should be set to "blocked" status to wait for other jobs | ||||||
| 			shouldBlock := len(j.Needs) > 0 | 			shouldBlock := len(j.Needs) > 0 | ||||||
| 			if err := rerunJob(ctx, j, shouldBlock); err != nil { | 			if err := rerunJob(ctx, j, shouldBlock); err != nil { | ||||||
| 				ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 				ctx.ServerError("RerunJob", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -455,7 +455,7 @@ func Rerun(ctx *context_module.Context) { | |||||||
| 		// jobs other than the specified one should be set to "blocked" status | 		// jobs other than the specified one should be set to "blocked" status | ||||||
| 		shouldBlock := j.JobID != job.JobID | 		shouldBlock := j.JobID != job.JobID | ||||||
| 		if err := rerunJob(ctx, j, shouldBlock); err != nil { | 		if err := rerunJob(ctx, j, shouldBlock); err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("RerunJob", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -485,7 +485,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	actions_service.CreateCommitStatus(ctx, job) | 	actions_service.CreateCommitStatus(ctx, job) | ||||||
| 	_ = job.LoadAttributes(ctx) | 	actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) | ||||||
| 	notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 	notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -547,7 +547,7 @@ func Cancel(ctx *context_module.Context) { | |||||||
| 		} | 		} | ||||||
| 		return nil | 		return nil | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("StopTask", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -557,7 +557,11 @@ func Cancel(ctx *context_module.Context) { | |||||||
| 		_ = job.LoadAttributes(ctx) | 		_ = job.LoadAttributes(ctx) | ||||||
| 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
| 	} | 	} | ||||||
|  | 	if len(updatedjobs) > 0 { | ||||||
|  | 		job := updatedjobs[0] | ||||||
|  | 		actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) | ||||||
|  | 		notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | 	} | ||||||
| 	ctx.JSON(http.StatusOK, struct{}{}) | 	ctx.JSON(http.StatusOK, struct{}{}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -593,12 +597,18 @@ func Approve(ctx *context_module.Context) { | |||||||
| 		} | 		} | ||||||
| 		return nil | 		return nil | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("UpdateRunJob", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	actions_service.CreateCommitStatus(ctx, jobs...) | 	actions_service.CreateCommitStatus(ctx, jobs...) | ||||||
|  |  | ||||||
|  | 	if len(updatedjobs) > 0 { | ||||||
|  | 		job := updatedjobs[0] | ||||||
|  | 		actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) | ||||||
|  | 		notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, job := range updatedjobs { | 	for _, job := range updatedjobs { | ||||||
| 		_ = job.LoadAttributes(ctx) | 		_ = job.LoadAttributes(ctx) | ||||||
| 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
| @@ -680,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { | 	if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("SetArtifactNeedDelete", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusOK, struct{}{}) | 	ctx.JSON(http.StatusOK, struct{}{}) | ||||||
| @@ -696,7 +706,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 			ctx.HTTPError(http.StatusNotFound, err.Error()) | 			ctx.HTTPError(http.StatusNotFound, err.Error()) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("GetRunByIndex", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -705,7 +715,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 		ArtifactName: artifactName, | 		ArtifactName: artifactName, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 		ctx.ServerError("FindArtifacts", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if len(artifacts) == 0 { | 	if len(artifacts) == 0 { | ||||||
| @@ -726,7 +736,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 	if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { | 	if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { | ||||||
| 		err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) | 		err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("DownloadArtifactV4", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		return | 		return | ||||||
| @@ -739,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 	for _, art := range artifacts { | 	for _, art := range artifacts { | ||||||
| 		f, err := storage.ActionsArtifacts.Open(art.StoragePath) | 		f, err := storage.ActionsArtifacts.Open(art.StoragePath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("ActionsArtifacts.Open", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -747,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 		if art.ContentEncoding == "gzip" { | 		if art.ContentEncoding == "gzip" { | ||||||
| 			r, err = gzip.NewReader(f) | 			r, err = gzip.NewReader(f) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 				ctx.ServerError("gzip.NewReader", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| @@ -757,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
|  |  | ||||||
| 		w, err := writer.Create(art.ArtifactPath) | 		w, err := writer.Create(art.ArtifactPath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("writer.Create", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if _, err := io.Copy(w, r); err != nil { | 		if _, err := io.Copy(w, r); err != nil { | ||||||
| 			ctx.HTTPError(http.StatusInternalServerError, err.Error()) | 			ctx.ServerError("io.Copy", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { | |||||||
| 			webhook_module.HookEventRepository:               form.Repository, | 			webhook_module.HookEventRepository:               form.Repository, | ||||||
| 			webhook_module.HookEventPackage:                  form.Package, | 			webhook_module.HookEventPackage:                  form.Package, | ||||||
| 			webhook_module.HookEventStatus:                   form.Status, | 			webhook_module.HookEventStatus:                   form.Status, | ||||||
|  | 			webhook_module.HookEventWorkflowRun:              form.WorkflowRun, | ||||||
| 			webhook_module.HookEventWorkflowJob:              form.WorkflowJob, | 			webhook_module.HookEventWorkflowJob:              form.WorkflowJob, | ||||||
| 		}, | 		}, | ||||||
| 		BranchFilter: form.BranchFilter, | 		BranchFilter: form.BranchFilter, | ||||||
|   | |||||||
| @@ -42,6 +42,10 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac | |||||||
| 			_ = job.LoadAttributes(ctx) | 			_ = job.LoadAttributes(ctx) | ||||||
| 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
| 		} | 		} | ||||||
|  | 		if len(jobs) > 0 { | ||||||
|  | 			job := jobs[0] | ||||||
|  | 			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -123,7 +127,7 @@ func CancelAbandonedJobs(ctx context.Context) error { | |||||||
| 		} | 		} | ||||||
| 		CreateCommitStatus(ctx, job) | 		CreateCommitStatus(ctx, job) | ||||||
| 		if updated { | 		if updated { | ||||||
| 			_ = job.LoadAttributes(ctx) | 			NotifyWorkflowRunStatusUpdateWithReload(ctx, job) | ||||||
| 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -33,4 +33,8 @@ type API interface { | |||||||
| 	GetRunner(*context.APIContext) | 	GetRunner(*context.APIContext) | ||||||
| 	// DeleteRunner delete runner | 	// DeleteRunner delete runner | ||||||
| 	DeleteRunner(*context.APIContext) | 	DeleteRunner(*context.APIContext) | ||||||
|  | 	// ListWorkflowJobs list jobs | ||||||
|  | 	ListWorkflowJobs(*context.APIContext) | ||||||
|  | 	// ListWorkflowRuns list runs | ||||||
|  | 	ListWorkflowRuns(*context.APIContext) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import ( | |||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/modules/graceful" | 	"code.gitea.io/gitea/modules/graceful" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/queue" | 	"code.gitea.io/gitea/modules/queue" | ||||||
| 	notify_service "code.gitea.io/gitea/services/notify" | 	notify_service "code.gitea.io/gitea/services/notify" | ||||||
|  |  | ||||||
| @@ -78,9 +79,30 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { | |||||||
| 		_ = job.LoadAttributes(ctx) | 		_ = job.LoadAttributes(ctx) | ||||||
| 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) | ||||||
| 	} | 	} | ||||||
|  | 	if len(jobs) > 0 { | ||||||
|  | 		runUpdated := true | ||||||
|  | 		for _, job := range jobs { | ||||||
|  | 			if !job.Status.IsDone() { | ||||||
|  | 				runUpdated = false | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if runUpdated { | ||||||
|  | 			NotifyWorkflowRunStatusUpdateWithReload(ctx, jobs[0]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) { | ||||||
|  | 	job.Run = nil | ||||||
|  | 	if err := job.LoadAttributes(ctx); err != nil { | ||||||
|  | 		log.Error("LoadAttributes: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | } | ||||||
|  |  | ||||||
| type jobStatusResolver struct { | type jobStatusResolver struct { | ||||||
| 	statuses map[int64]actions_model.Status | 	statuses map[int64]actions_model.Status | ||||||
| 	needs    map[int64][]int64 | 	needs    map[int64][]int64 | ||||||
|   | |||||||
| @@ -6,13 +6,16 @@ package actions | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  |  | ||||||
|  | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
|  | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	packages_model "code.gitea.io/gitea/models/packages" | 	packages_model "code.gitea.io/gitea/models/packages" | ||||||
| 	perm_model "code.gitea.io/gitea/models/perm" | 	perm_model "code.gitea.io/gitea/models/perm" | ||||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | 	access_model "code.gitea.io/gitea/models/perm/access" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/gitrepo" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/repository" | 	"code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| @@ -762,3 +765,41 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m | |||||||
| 		Sender:       convert.ToUser(ctx, doer, nil), | 		Sender:       convert.ToUser(ctx, doer, nil), | ||||||
| 	}).Notify(ctx) | 	}).Notify(ctx) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { | ||||||
|  | 	ctx = withMethod(ctx, "WorkflowRunStatusUpdate") | ||||||
|  |  | ||||||
|  | 	var org *api.Organization | ||||||
|  | 	if repo.Owner.IsOrganization() { | ||||||
|  | 		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	status := convert.ToWorkflowRunAction(run.Status) | ||||||
|  |  | ||||||
|  | 	gitRepo, err := gitrepo.OpenRepository(ctx, repo) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("OpenRepository: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer gitRepo.Close() | ||||||
|  |  | ||||||
|  | 	convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetActionWorkflow: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("ToActionWorkflowRun: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ | ||||||
|  | 		Action:       status, | ||||||
|  | 		Workflow:     convertedWorkflow, | ||||||
|  | 		WorkflowRun:  convertedRun, | ||||||
|  | 		Organization: org, | ||||||
|  | 		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), | ||||||
|  | 		Sender:       convert.ToUser(ctx, sender, nil), | ||||||
|  | 	}).Notify(ctx) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -178,7 +178,7 @@ func notify(ctx context.Context, input *notifyInput) error { | |||||||
| 		return fmt.Errorf("gitRepo.GetCommit: %w", err) | 		return fmt.Errorf("gitRepo.GetCommit: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if skipWorkflows(input, commit) { | 	if skipWorkflows(ctx, input, commit) { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -243,7 +243,7 @@ func notify(ctx context.Context, input *notifyInput) error { | |||||||
| 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String()) | 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func skipWorkflows(input *notifyInput, commit *git.Commit) bool { | func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool { | ||||||
| 	// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync) | 	// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync) | ||||||
| 	// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs | 	// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs | ||||||
| 	skipWorkflowEvents := []webhook_module.HookEventType{ | 	skipWorkflowEvents := []webhook_module.HookEventType{ | ||||||
| @@ -263,6 +263,27 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	if input.Event == webhook_module.HookEventWorkflowRun { | ||||||
|  | 		wrun, ok := input.Payload.(*api.WorkflowRunPayload) | ||||||
|  | 		for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ { | ||||||
|  | 			if wrun.WorkflowRun.Event != "workflow_run" { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			r, err := actions_model.GetRunByRepoAndID(ctx, input.Repo.ID, wrun.WorkflowRun.ID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("GetRunByRepoAndID: %v", err) | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			wrun, err = r.GetWorkflowRunEventPayload() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("GetWorkflowRunEventPayload: %v", err) | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		// skip workflow runs events exceeding the maxiumum of 5 recursive events | ||||||
|  | 		log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RepoPath()) | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -372,6 +393,15 @@ func handleWorkflows( | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		CreateCommitStatus(ctx, alljobs...) | 		CreateCommitStatus(ctx, alljobs...) | ||||||
|  | 		if len(alljobs) > 0 { | ||||||
|  | 			job := alljobs[0] | ||||||
|  | 			err := job.LoadRun(ctx) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("LoadRun: %v", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | 		} | ||||||
| 		for _, job := range alljobs { | 		for _, job := range alljobs { | ||||||
| 			notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) | 			notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -157,6 +157,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("LoadAttributes: %v", err) | 		log.Error("LoadAttributes: %v", err) | ||||||
| 	} | 	} | ||||||
|  | 	notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) | ||||||
| 	for _, job := range allJobs { | 	for _, job := range allJobs { | ||||||
| 		notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) | 		notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -5,9 +5,6 @@ package actions | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" |  | ||||||
| 	"path" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| @@ -31,51 +28,8 @@ import ( | |||||||
| 	"github.com/nektos/act/pkg/model" | 	"github.com/nektos/act/pkg/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { |  | ||||||
| 	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) |  | ||||||
| 	cfg := cfgUnit.ActionsConfig() |  | ||||||
|  |  | ||||||
| 	defaultBranch, _ := commit.GetBranchName() |  | ||||||
|  |  | ||||||
| 	workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name())) |  | ||||||
| 	workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name())) |  | ||||||
| 	badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch)) |  | ||||||
|  |  | ||||||
| 	// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow |  | ||||||
| 	// State types: |  | ||||||
| 	// - active |  | ||||||
| 	// - deleted |  | ||||||
| 	// - disabled_fork |  | ||||||
| 	// - disabled_inactivity |  | ||||||
| 	// - disabled_manually |  | ||||||
| 	state := "active" |  | ||||||
| 	if cfg.IsWorkflowDisabled(entry.Name()) { |  | ||||||
| 		state = "disabled_manually" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined |  | ||||||
| 	// by retrieving the first and last commits for the file history. The first commit would indicate the creation date, |  | ||||||
| 	// while the last commit would represent the modification date. The DeletedAt could be determined by identifying |  | ||||||
| 	// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely |  | ||||||
| 	// cause a significant performance degradation. |  | ||||||
| 	createdAt := commit.Author.When |  | ||||||
| 	updatedAt := commit.Author.When |  | ||||||
|  |  | ||||||
| 	return &api.ActionWorkflow{ |  | ||||||
| 		ID:        entry.Name(), |  | ||||||
| 		Name:      entry.Name(), |  | ||||||
| 		Path:      path.Join(folder, entry.Name()), |  | ||||||
| 		State:     state, |  | ||||||
| 		CreatedAt: createdAt, |  | ||||||
| 		UpdatedAt: updatedAt, |  | ||||||
| 		URL:       workflowURL, |  | ||||||
| 		HTMLURL:   workflowRepoURL, |  | ||||||
| 		BadgeURL:  badgeURL, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { | func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { | ||||||
| 	workflow, err := GetActionWorkflow(ctx, workflowID) | 	workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -92,42 +46,6 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl | |||||||
| 	return repo_model.UpdateRepoUnit(ctx, cfgUnit) | 	return repo_model.UpdateRepoUnit(ctx, cfgUnit) | ||||||
| } | } | ||||||
|  |  | ||||||
| func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) { |  | ||||||
| 	defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.APIErrorInternal(err) |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	folder, entries, err := actions.ListWorkflows(defaultBranchCommit) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.APIError(http.StatusNotFound, err.Error()) |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	workflows := make([]*api.ActionWorkflow, len(entries)) |  | ||||||
| 	for i, entry := range entries { |  | ||||||
| 		workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return workflows, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) { |  | ||||||
| 	entries, err := ListActionWorkflows(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, entry := range entries { |  | ||||||
| 		if entry.Name == workflowID { |  | ||||||
| 			return entry, nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error { | func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error { | ||||||
| 	if workflowID == "" { | 	if workflowID == "" { | ||||||
| 		return util.ErrorWrapLocale( | 		return util.ErrorWrapLocale( | ||||||
| @@ -285,6 +203,15 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re | |||||||
| 		log.Error("FindRunJobs: %v", err) | 		log.Error("FindRunJobs: %v", err) | ||||||
| 	} | 	} | ||||||
| 	CreateCommitStatus(ctx, allJobs...) | 	CreateCommitStatus(ctx, allJobs...) | ||||||
|  | 	if len(allJobs) > 0 { | ||||||
|  | 		job := allJobs[0] | ||||||
|  | 		err := job.LoadRun(ctx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("LoadRun: %v", err) | ||||||
|  | 		} else { | ||||||
|  | 			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	for _, job := range allJobs { | 	for _, job := range allJobs { | ||||||
| 		notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) | 		notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| // FormString returns the first value matching the provided key in the form as a string | // FormString returns the first value matching the provided key in the form as a string | ||||||
|  | // It works the same as http.Request.FormValue: | ||||||
|  | // try urlencoded request body first, then query string, then multipart form body | ||||||
| func (b *Base) FormString(key string, def ...string) string { | func (b *Base) FormString(key string, def ...string) string { | ||||||
| 	s := b.Req.FormValue(key) | 	s := b.Req.FormValue(key) | ||||||
| 	if s == "" { | 	if s == "" { | ||||||
| @@ -20,7 +22,7 @@ func (b *Base) FormString(key string, def ...string) string { | |||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
|  |  | ||||||
| // FormStrings returns a string slice for the provided key from the form | // FormStrings returns a values for the key in the form (including query parameters), similar to FormString | ||||||
| func (b *Base) FormStrings(key string) []string { | func (b *Base) FormStrings(key string) []string { | ||||||
| 	if b.Req.Form == nil { | 	if b.Req.Form == nil { | ||||||
| 		if err := b.Req.ParseMultipartForm(32 << 20); err != nil { | 		if err := b.Req.ParseMultipartForm(32 << 20); err != nil { | ||||||
|   | |||||||
| @@ -5,8 +5,11 @@ | |||||||
| package convert | package convert | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -14,6 +17,7 @@ import ( | |||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/auth" | 	"code.gitea.io/gitea/models/auth" | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
| 	git_model "code.gitea.io/gitea/models/git" | 	git_model "code.gitea.io/gitea/models/git" | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| @@ -22,6 +26,7 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/actions" | ||||||
| 	"code.gitea.io/gitea/modules/container" | 	"code.gitea.io/gitea/modules/container" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| @@ -32,6 +37,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/services/gitdiff" | 	"code.gitea.io/gitea/services/gitdiff" | ||||||
|  |  | ||||||
| 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1" | 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1" | ||||||
|  | 	"github.com/nektos/act/pkg/model" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ToEmail convert models.EmailAddress to api.Email | // ToEmail convert models.EmailAddress to api.Email | ||||||
| @@ -241,6 +247,242 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { | ||||||
|  | 	err := run.LoadAttributes(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	status, conclusion := ToActionsStatus(run.Status) | ||||||
|  | 	return &api.ActionWorkflowRun{ | ||||||
|  | 		ID:           run.ID, | ||||||
|  | 		URL:          fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), | ||||||
|  | 		HTMLURL:      run.HTMLURL(), | ||||||
|  | 		RunNumber:    run.Index, | ||||||
|  | 		StartedAt:    run.Started.AsLocalTime(), | ||||||
|  | 		CompletedAt:  run.Stopped.AsLocalTime(), | ||||||
|  | 		Event:        string(run.Event), | ||||||
|  | 		DisplayTitle: run.Title, | ||||||
|  | 		HeadBranch:   git.RefName(run.Ref).BranchName(), | ||||||
|  | 		HeadSha:      run.CommitSHA, | ||||||
|  | 		Status:       status, | ||||||
|  | 		Conclusion:   conclusion, | ||||||
|  | 		Path:         fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), | ||||||
|  | 		Repository:   ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}), | ||||||
|  | 		TriggerActor: ToUser(ctx, run.TriggerUser, nil), | ||||||
|  | 		// We do not have a way to get a different User for the actor than the trigger user | ||||||
|  | 		Actor: ToUser(ctx, run.TriggerUser, nil), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ToWorkflowRunAction(status actions_model.Status) string { | ||||||
|  | 	var action string | ||||||
|  | 	switch status { | ||||||
|  | 	case actions_model.StatusWaiting, actions_model.StatusBlocked: | ||||||
|  | 		action = "requested" | ||||||
|  | 	case actions_model.StatusRunning: | ||||||
|  | 		action = "in_progress" | ||||||
|  | 	} | ||||||
|  | 	if status.IsDone() { | ||||||
|  | 		action = "completed" | ||||||
|  | 	} | ||||||
|  | 	return action | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ToActionsStatus(status actions_model.Status) (string, string) { | ||||||
|  | 	var action string | ||||||
|  | 	var conclusion string | ||||||
|  | 	switch status { | ||||||
|  | 	// This is a naming conflict of the webhook between Gitea and GitHub Actions | ||||||
|  | 	case actions_model.StatusWaiting: | ||||||
|  | 		action = "queued" | ||||||
|  | 	case actions_model.StatusBlocked: | ||||||
|  | 		action = "waiting" | ||||||
|  | 	case actions_model.StatusRunning: | ||||||
|  | 		action = "in_progress" | ||||||
|  | 	} | ||||||
|  | 	if status.IsDone() { | ||||||
|  | 		action = "completed" | ||||||
|  | 		switch status { | ||||||
|  | 		case actions_model.StatusSuccess: | ||||||
|  | 			conclusion = "success" | ||||||
|  | 		case actions_model.StatusCancelled: | ||||||
|  | 			conclusion = "cancelled" | ||||||
|  | 		case actions_model.StatusFailure: | ||||||
|  | 			conclusion = "failure" | ||||||
|  | 		case actions_model.StatusSkipped: | ||||||
|  | 			conclusion = "skipped" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return action, conclusion | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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) { | ||||||
|  | 	err := job.LoadAttributes(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	jobIndex := 0 | ||||||
|  | 	jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	for i, j := range jobs { | ||||||
|  | 		if j.ID == job.ID { | ||||||
|  | 			jobIndex = i | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	status, conclusion := ToActionsStatus(job.Status) | ||||||
|  | 	var runnerID int64 | ||||||
|  | 	var runnerName string | ||||||
|  | 	var steps []*api.ActionWorkflowStep | ||||||
|  |  | ||||||
|  | 	if job.TaskID != 0 { | ||||||
|  | 		if task == nil { | ||||||
|  | 			task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		runnerID = task.RunnerID | ||||||
|  | 		if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { | ||||||
|  | 			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(), | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &api.ActionWorkflowJob{ | ||||||
|  | 		ID: job.ID, | ||||||
|  | 		// missing api endpoint for this location | ||||||
|  | 		URL:     fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID), | ||||||
|  | 		HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex), | ||||||
|  | 		RunID:   job.RunID, | ||||||
|  | 		// Missing api endpoint for this location, artifacts are available under a nested url | ||||||
|  | 		RunURL:      fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID), | ||||||
|  | 		Name:        job.Name, | ||||||
|  | 		Labels:      job.RunsOn, | ||||||
|  | 		RunAttempt:  job.Attempt, | ||||||
|  | 		HeadSha:     job.Run.CommitSHA, | ||||||
|  | 		HeadBranch:  git.RefName(job.Run.Ref).BranchName(), | ||||||
|  | 		Status:      status, | ||||||
|  | 		Conclusion:  conclusion, | ||||||
|  | 		RunnerID:    runnerID, | ||||||
|  | 		RunnerName:  runnerName, | ||||||
|  | 		Steps:       steps, | ||||||
|  | 		CreatedAt:   job.Created.AsTime().UTC(), | ||||||
|  | 		StartedAt:   job.Started.AsTime().UTC(), | ||||||
|  | 		CompletedAt: job.Stopped.AsTime().UTC(), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { | ||||||
|  | 	cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) | ||||||
|  | 	cfg := cfgUnit.ActionsConfig() | ||||||
|  |  | ||||||
|  | 	defaultBranch, _ := commit.GetBranchName() | ||||||
|  |  | ||||||
|  | 	workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name())) | ||||||
|  | 	workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) | ||||||
|  | 	badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch)) | ||||||
|  |  | ||||||
|  | 	// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow | ||||||
|  | 	// State types: | ||||||
|  | 	// - active | ||||||
|  | 	// - deleted | ||||||
|  | 	// - disabled_fork | ||||||
|  | 	// - disabled_inactivity | ||||||
|  | 	// - disabled_manually | ||||||
|  | 	state := "active" | ||||||
|  | 	if cfg.IsWorkflowDisabled(entry.Name()) { | ||||||
|  | 		state = "disabled_manually" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined | ||||||
|  | 	// by retrieving the first and last commits for the file history. The first commit would indicate the creation date, | ||||||
|  | 	// while the last commit would represent the modification date. The DeletedAt could be determined by identifying | ||||||
|  | 	// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely | ||||||
|  | 	// cause a significant performance degradation. | ||||||
|  | 	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:      name, | ||||||
|  | 		Path:      path.Join(folder, entry.Name()), | ||||||
|  | 		State:     state, | ||||||
|  | 		CreatedAt: createdAt, | ||||||
|  | 		UpdatedAt: updatedAt, | ||||||
|  | 		URL:       workflowURL, | ||||||
|  | 		HTMLURL:   workflowRepoURL, | ||||||
|  | 		BadgeURL:  badgeURL, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) { | ||||||
|  | 	defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	folder, entries, err := actions.ListWorkflows(defaultBranchCommit) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	workflows := make([]*api.ActionWorkflow, len(entries)) | ||||||
|  | 	for i, entry := range entries { | ||||||
|  | 		workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, folder, entry) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return workflows, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) { | ||||||
|  | 	entries, err := ListActionWorkflows(ctx, gitrepo, repo) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, entry := range entries { | ||||||
|  | 		if entry.ID == workflowID { | ||||||
|  | 			return entry, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) | ||||||
|  | } | ||||||
|  |  | ||||||
| // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact | // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact | ||||||
| func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { | func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { | ||||||
| 	url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) | 	url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) | ||||||
|   | |||||||
| @@ -234,6 +234,7 @@ type WebhookForm struct { | |||||||
| 	Release                  bool | 	Release                  bool | ||||||
| 	Package                  bool | 	Package                  bool | ||||||
| 	Status                   bool | 	Status                   bool | ||||||
|  | 	WorkflowRun              bool | ||||||
| 	WorkflowJob              bool | 	WorkflowJob              bool | ||||||
| 	Active                   bool | 	Active                   bool | ||||||
| 	BranchFilter             string `binding:"GlobPattern"` | 	BranchFilter             string `binding:"GlobPattern"` | ||||||
|   | |||||||
| @@ -79,5 +79,7 @@ type Notifier interface { | |||||||
|  |  | ||||||
| 	CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) | 	CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) | ||||||
|  |  | ||||||
|  | 	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) | 	WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -376,6 +376,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { | ||||||
|  | 	for _, notifier := range notifiers { | ||||||
|  | 		notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { | func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { | ||||||
| 	for _, notifier := range notifiers { | 	for _, notifier := range notifiers { | ||||||
| 		notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task) | 		notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task) | ||||||
|   | |||||||
| @@ -214,5 +214,8 @@ func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.R | |||||||
| func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { | func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (*NullNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { | ||||||
|  | } | ||||||
|  |  | ||||||
| func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { | func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { | ||||||
| } | } | ||||||
|   | |||||||
| @@ -176,6 +176,12 @@ func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, | |||||||
| 	return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil | 	return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) { | func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -278,6 +278,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er | |||||||
| 	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil | 	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) { | ||||||
|  | 	text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false) | ||||||
|  |  | ||||||
|  | 	return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) { | func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) { | ||||||
| 	text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) | 	text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -172,6 +172,12 @@ func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, err | |||||||
| 	return newFeishuTextPayload(text), nil | 	return newFeishuTextPayload(text), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return newFeishuTextPayload(text), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) { | func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -327,6 +327,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte | |||||||
| 	return text, color | 	return text, color | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { | ||||||
|  | 	description := p.WorkflowRun.Conclusion | ||||||
|  | 	if description == "" { | ||||||
|  | 		description = p.WorkflowRun.Status | ||||||
|  | 	} | ||||||
|  | 	refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description) | ||||||
|  |  | ||||||
|  | 	text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink) | ||||||
|  | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
| func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { | func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { | ||||||
| 	description := p.WorkflowJob.Conclusion | 	description := p.WorkflowJob.Conclusion | ||||||
| 	if description == "" { | 	if description == "" { | ||||||
|   | |||||||
| @@ -252,6 +252,12 @@ func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, erro | |||||||
| 	return m.newPayload(text) | 	return m.newPayload(text) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return m.newPayload(text) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) { | func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -318,6 +318,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er | |||||||
| 	), nil | 	), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) { | ||||||
|  | 	title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false) | ||||||
|  |  | ||||||
|  | 	return createMSTeamsPayload( | ||||||
|  | 		p.Repo, | ||||||
|  | 		p.Sender, | ||||||
|  | 		title, | ||||||
|  | 		"", | ||||||
|  | 		p.WorkflowRun.HTMLURL, | ||||||
|  | 		color, | ||||||
|  | 		&MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle}, | ||||||
|  | 	), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) { | func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) { | ||||||
| 	title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) | 	title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,10 +5,8 @@ package webhook | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	actions_model "code.gitea.io/gitea/models/actions" | 	actions_model "code.gitea.io/gitea/models/actions" | ||||||
| 	"code.gitea.io/gitea/models/db" |  | ||||||
| 	git_model "code.gitea.io/gitea/models/git" | 	git_model "code.gitea.io/gitea/models/git" | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| @@ -18,6 +16,7 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/gitrepo" | ||||||
| 	"code.gitea.io/gitea/modules/httplib" | 	"code.gitea.io/gitea/modules/httplib" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/repository" | 	"code.gitea.io/gitea/modules/repository" | ||||||
| @@ -956,72 +955,17 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ | |||||||
| 		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) | 		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := job.LoadAttributes(ctx) | 	status, _ := convert.ToActionsStatus(job.Status) | ||||||
|  |  | ||||||
|  | 	convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Error loading job attributes: %v", err) | 		log.Error("ToActionWorkflowJob: %v", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	jobIndex := 0 |  | ||||||
| 	jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("Error loading getting run jobs: %v", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	for i, j := range jobs { |  | ||||||
| 		if j.ID == job.ID { |  | ||||||
| 			jobIndex = i |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	status, conclusion := toActionStatus(job.Status) |  | ||||||
| 	var runnerID int64 |  | ||||||
| 	var runnerName string |  | ||||||
| 	var steps []*api.ActionWorkflowStep |  | ||||||
|  |  | ||||||
| 	if task != nil { |  | ||||||
| 		runnerID = task.RunnerID |  | ||||||
| 		if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { |  | ||||||
| 			runnerName = runner.Name |  | ||||||
| 		} |  | ||||||
| 		for i, step := range task.Steps { |  | ||||||
| 			stepStatus, stepConclusion := toActionStatus(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(), |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ | 	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ | ||||||
| 		Action: status, | 		Action:       status, | ||||||
| 		WorkflowJob: &api.ActionWorkflowJob{ | 		WorkflowJob:  convertedJob, | ||||||
| 			ID: job.ID, |  | ||||||
| 			// missing api endpoint for this location |  | ||||||
| 			URL:     fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.APIURL(), job.RunID, job.ID), |  | ||||||
| 			HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex), |  | ||||||
| 			RunID:   job.RunID, |  | ||||||
| 			// Missing api endpoint for this location, artifacts are available under a nested url |  | ||||||
| 			RunURL:      fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID), |  | ||||||
| 			Name:        job.Name, |  | ||||||
| 			Labels:      job.RunsOn, |  | ||||||
| 			RunAttempt:  job.Attempt, |  | ||||||
| 			HeadSha:     job.Run.CommitSHA, |  | ||||||
| 			HeadBranch:  git.RefName(job.Run.Ref).BranchName(), |  | ||||||
| 			Status:      status, |  | ||||||
| 			Conclusion:  conclusion, |  | ||||||
| 			RunnerID:    runnerID, |  | ||||||
| 			RunnerName:  runnerName, |  | ||||||
| 			Steps:       steps, |  | ||||||
| 			CreatedAt:   job.Created.AsTime().UTC(), |  | ||||||
| 			StartedAt:   job.Started.AsTime().UTC(), |  | ||||||
| 			CompletedAt: job.Stopped.AsTime().UTC(), |  | ||||||
| 		}, |  | ||||||
| 		Organization: org, | 		Organization: org, | ||||||
| 		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), | 		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), | ||||||
| 		Sender:       convert.ToUser(ctx, sender, nil), | 		Sender:       convert.ToUser(ctx, sender, nil), | ||||||
| @@ -1030,28 +974,46 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func toActionStatus(status actions_model.Status) (string, string) { | func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { | ||||||
| 	var action string | 	source := EventSource{ | ||||||
| 	var conclusion string | 		Repository: repo, | ||||||
| 	switch status { | 		Owner:      repo.Owner, | ||||||
| 	// This is a naming conflict of the webhook between Gitea and GitHub Actions |  | ||||||
| 	case actions_model.StatusWaiting: |  | ||||||
| 		action = "queued" |  | ||||||
| 	case actions_model.StatusBlocked: |  | ||||||
| 		action = "waiting" |  | ||||||
| 	case actions_model.StatusRunning: |  | ||||||
| 		action = "in_progress" |  | ||||||
| 	} | 	} | ||||||
| 	if status.IsDone() { |  | ||||||
| 		action = "completed" | 	var org *api.Organization | ||||||
| 		switch status { | 	if repo.Owner.IsOrganization() { | ||||||
| 		case actions_model.StatusSuccess: | 		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) | ||||||
| 			conclusion = "success" | 	} | ||||||
| 		case actions_model.StatusCancelled: |  | ||||||
| 			conclusion = "cancelled" | 	status := convert.ToWorkflowRunAction(run.Status) | ||||||
| 		case actions_model.StatusFailure: |  | ||||||
| 			conclusion = "failure" | 	gitRepo, err := gitrepo.OpenRepository(ctx, repo) | ||||||
| 		} | 	if err != nil { | ||||||
|  | 		log.Error("OpenRepository: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer gitRepo.Close() | ||||||
|  |  | ||||||
|  | 	convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetActionWorkflow: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("ToActionWorkflowRun: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{ | ||||||
|  | 		Action:       status, | ||||||
|  | 		Workflow:     convertedWorkflow, | ||||||
|  | 		WorkflowRun:  convertedRun, | ||||||
|  | 		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) | ||||||
| 	} | 	} | ||||||
| 	return action, conclusion |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -114,6 +114,10 @@ func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayloa | |||||||
| 	return PackagistPayload{}, nil | 	return PackagistPayload{}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) { | ||||||
|  | 	return PackagistPayload{}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) { | func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) { | ||||||
| 	return PackagistPayload{}, nil | 	return PackagistPayload{}, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ type payloadConvertor[T any] interface { | |||||||
| 	Wiki(*api.WikiPayload) (T, error) | 	Wiki(*api.WikiPayload) (T, error) | ||||||
| 	Package(*api.PackagePayload) (T, error) | 	Package(*api.PackagePayload) (T, error) | ||||||
| 	Status(*api.CommitStatusPayload) (T, error) | 	Status(*api.CommitStatusPayload) (T, error) | ||||||
|  | 	WorkflowRun(*api.WorkflowRunPayload) (T, error) | ||||||
| 	WorkflowJob(*api.WorkflowJobPayload) (T, error) | 	WorkflowJob(*api.WorkflowJobPayload) (T, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -81,6 +82,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module | |||||||
| 		return convertUnmarshalledJSON(rc.Package, data) | 		return convertUnmarshalledJSON(rc.Package, data) | ||||||
| 	case webhook_module.HookEventStatus: | 	case webhook_module.HookEventStatus: | ||||||
| 		return convertUnmarshalledJSON(rc.Status, data) | 		return convertUnmarshalledJSON(rc.Status, data) | ||||||
|  | 	case webhook_module.HookEventWorkflowRun: | ||||||
|  | 		return convertUnmarshalledJSON(rc.WorkflowRun, data) | ||||||
| 	case webhook_module.HookEventWorkflowJob: | 	case webhook_module.HookEventWorkflowJob: | ||||||
| 		return convertUnmarshalledJSON(rc.WorkflowJob, data) | 		return convertUnmarshalledJSON(rc.WorkflowJob, data) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -173,6 +173,12 @@ func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) | |||||||
| 	return s.createPayload(text, nil), nil | 	return s.createPayload(text, nil), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return s.createPayload(text, nil), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) { | func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -180,6 +180,12 @@ func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, | |||||||
| 	return createTelegramPayloadHTML(text), nil | 	return createTelegramPayloadHTML(text), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return createTelegramPayloadHTML(text), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) { | func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -181,6 +181,12 @@ func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayl | |||||||
| 	return newWechatworkMarkdownPayload(text), nil | 	return newWechatworkMarkdownPayload(text), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) { | ||||||
|  | 	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|  | 	return newWechatworkMarkdownPayload(text), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) { | func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) { | ||||||
| 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -263,6 +263,16 @@ | |||||||
| 		<div class="fourteen wide column"> | 		<div class="fourteen wide column"> | ||||||
| 			<label>{{ctx.Locale.Tr "repo.settings.event_header_workflow"}}</label> | 			<label>{{ctx.Locale.Tr "repo.settings.event_header_workflow"}}</label> | ||||||
| 		</div> | 		</div> | ||||||
|  | 		<!-- Workflow Run Event --> | ||||||
|  | 		<div class="seven wide column"> | ||||||
|  | 			<div class="field"> | ||||||
|  | 				<div class="ui checkbox"> | ||||||
|  | 					<input name="workflow_run" type="checkbox" {{if .Webhook.HookEvents.Get "workflow_run"}}checked{{end}}> | ||||||
|  | 					<label>{{ctx.Locale.Tr "repo.settings.event_workflow_run"}}</label> | ||||||
|  | 					<span class="help">{{ctx.Locale.Tr "repo.settings.event_workflow_run_desc"}}</span> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
| 		<!-- Workflow Job Event --> | 		<!-- Workflow Job Event --> | ||||||
| 		<div class="seven wide column"> | 		<div class="seven wide column"> | ||||||
| 			<div class="field"> | 			<div class="field"> | ||||||
|   | |||||||
							
								
								
									
										892
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										892
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -75,6 +75,49 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/admin/actions/jobs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "admin" | ||||||
|  |         ], | ||||||
|  |         "summary": "Lists all jobs", | ||||||
|  |         "operationId": "listAdminWorkflowJobs", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJobsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/admin/actions/runners": { |     "/admin/actions/runners": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -177,6 +220,73 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/admin/actions/runs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "admin" | ||||||
|  |         ], | ||||||
|  |         "summary": "Lists all runs", | ||||||
|  |         "operationId": "listAdminWorkflowRuns", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow event name", | ||||||
|  |             "name": "event", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow branch", | ||||||
|  |             "name": "branch", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggered by user", | ||||||
|  |             "name": "actor", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggering sha of the workflow run", | ||||||
|  |             "name": "head_sha", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowRunsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/admin/cron": { |     "/admin/cron": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -1799,6 +1909,56 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/orgs/{org}/actions/jobs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "organization" | ||||||
|  |         ], | ||||||
|  |         "summary": "Get org-level workflow jobs", | ||||||
|  |         "operationId": "getOrgWorkflowJobs", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the organization", | ||||||
|  |             "name": "org", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJobsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/orgs/{org}/actions/runners": { |     "/orgs/{org}/actions/runners": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -1957,6 +2117,80 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/orgs/{org}/actions/runs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "organization" | ||||||
|  |         ], | ||||||
|  |         "summary": "Get org-level workflow runs", | ||||||
|  |         "operationId": "getOrgWorkflowRuns", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the organization", | ||||||
|  |             "name": "org", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow event name", | ||||||
|  |             "name": "event", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow branch", | ||||||
|  |             "name": "branch", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggered by user", | ||||||
|  |             "name": "actor", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggering sha of the workflow run", | ||||||
|  |             "name": "head_sha", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowRunsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/orgs/{org}/actions/secrets": { |     "/orgs/{org}/actions/secrets": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -4519,6 +4753,109 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/repos/{owner}/{repo}/actions/jobs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "Lists all jobs for a repository", | ||||||
|  |         "operationId": "listWorkflowJobs", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the owner", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repository", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJobsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "/repos/{owner}/{repo}/actions/jobs/{job_id}": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "Gets a specific workflow job for a workflow run", | ||||||
|  |         "operationId": "getWorkflowJob", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the owner", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repository", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "id of the job", | ||||||
|  |             "name": "job_id", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJob" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": { |     "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -4758,7 +5095,132 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/repos/{owner}/{repo}/actions/runs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "Lists all runs for a repository run", | ||||||
|  |         "operationId": "getWorkflowRuns", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the owner", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repository", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow event name", | ||||||
|  |             "name": "event", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow branch", | ||||||
|  |             "name": "branch", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggered by user", | ||||||
|  |             "name": "actor", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggering sha of the workflow run", | ||||||
|  |             "name": "head_sha", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/ArtifactsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/repos/{owner}/{repo}/actions/runs/{run}": { |     "/repos/{owner}/{repo}/actions/runs/{run}": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "Gets a specific workflow run", | ||||||
|  |         "operationId": "GetWorkflowRun", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the owner", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repository", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "id of the run", | ||||||
|  |             "name": "run", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowRun" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|       "delete": { |       "delete": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
|           "application/json" |           "application/json" | ||||||
| @@ -4856,6 +5318,70 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/repos/{owner}/{repo}/actions/runs/{run}/jobs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "repository" | ||||||
|  |         ], | ||||||
|  |         "summary": "Lists all jobs for a workflow run", | ||||||
|  |         "operationId": "listWorkflowRunJobs", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the owner", | ||||||
|  |             "name": "owner", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "name of the repository", | ||||||
|  |             "name": "repo", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "runid of the workflow run", | ||||||
|  |             "name": "run", | ||||||
|  |             "in": "path", | ||||||
|  |             "required": true | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJobsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/repos/{owner}/{repo}/actions/secrets": { |     "/repos/{owner}/{repo}/actions/secrets": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -17584,6 +18110,49 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/user/actions/jobs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "user" | ||||||
|  |         ], | ||||||
|  |         "summary": "Get workflow jobs", | ||||||
|  |         "operationId": "getUserWorkflowJobs", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowJobsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/user/actions/runners": { |     "/user/actions/runners": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @@ -17701,6 +18270,73 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/user/actions/runs": { | ||||||
|  |       "get": { | ||||||
|  |         "produces": [ | ||||||
|  |           "application/json" | ||||||
|  |         ], | ||||||
|  |         "tags": [ | ||||||
|  |           "user" | ||||||
|  |         ], | ||||||
|  |         "summary": "Get workflow runs", | ||||||
|  |         "operationId": "getUserWorkflowRuns", | ||||||
|  |         "parameters": [ | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow event name", | ||||||
|  |             "name": "event", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow branch", | ||||||
|  |             "name": "branch", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", | ||||||
|  |             "name": "status", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggered by user", | ||||||
|  |             "name": "actor", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "triggering sha of the workflow run", | ||||||
|  |             "name": "head_sha", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page number of results to return (1-based)", | ||||||
|  |             "name": "page", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "integer", | ||||||
|  |             "description": "page size of results", | ||||||
|  |             "name": "limit", | ||||||
|  |             "in": "query" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "$ref": "#/responses/WorkflowRunsList" | ||||||
|  |           }, | ||||||
|  |           "400": { | ||||||
|  |             "$ref": "#/responses/error" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/user/actions/secrets/{secretname}": { |     "/user/actions/secrets/{secretname}": { | ||||||
|       "put": { |       "put": { | ||||||
|         "consumes": [ |         "consumes": [ | ||||||
| @@ -20440,23 +21076,251 @@ | |||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|     }, |     }, | ||||||
|     "ActionWorkflowRun": { |     "ActionWorkflowJob": { | ||||||
|       "description": "ActionWorkflowRun represents a WorkflowRun", |       "description": "ActionWorkflowJob represents a WorkflowJob", | ||||||
|       "type": "object", |       "type": "object", | ||||||
|       "properties": { |       "properties": { | ||||||
|  |         "completed_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "CompletedAt" | ||||||
|  |         }, | ||||||
|  |         "conclusion": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Conclusion" | ||||||
|  |         }, | ||||||
|  |         "created_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "CreatedAt" | ||||||
|  |         }, | ||||||
|  |         "head_branch": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "HeadBranch" | ||||||
|  |         }, | ||||||
|         "head_sha": { |         "head_sha": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "HeadSha" |           "x-go-name": "HeadSha" | ||||||
|         }, |         }, | ||||||
|  |         "html_url": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "HTMLURL" | ||||||
|  |         }, | ||||||
|         "id": { |         "id": { | ||||||
|           "type": "integer", |           "type": "integer", | ||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "ID" |           "x-go-name": "ID" | ||||||
|         }, |         }, | ||||||
|  |         "labels": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "Labels" | ||||||
|  |         }, | ||||||
|  |         "name": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Name" | ||||||
|  |         }, | ||||||
|  |         "run_attempt": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "RunAttempt" | ||||||
|  |         }, | ||||||
|  |         "run_id": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "RunID" | ||||||
|  |         }, | ||||||
|  |         "run_url": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "RunURL" | ||||||
|  |         }, | ||||||
|  |         "runner_id": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "RunnerID" | ||||||
|  |         }, | ||||||
|  |         "runner_name": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "RunnerName" | ||||||
|  |         }, | ||||||
|  |         "started_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "StartedAt" | ||||||
|  |         }, | ||||||
|  |         "status": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Status" | ||||||
|  |         }, | ||||||
|  |         "steps": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "$ref": "#/definitions/ActionWorkflowStep" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "Steps" | ||||||
|  |         }, | ||||||
|  |         "url": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "URL" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|  |     }, | ||||||
|  |     "ActionWorkflowJobsResponse": { | ||||||
|  |       "description": "ActionWorkflowJobsResponse returns ActionWorkflowJobs", | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { | ||||||
|  |         "jobs": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "$ref": "#/definitions/ActionWorkflowJob" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "Entries" | ||||||
|  |         }, | ||||||
|  |         "total_count": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "TotalCount" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|  |     }, | ||||||
|  |     "ActionWorkflowRun": { | ||||||
|  |       "description": "ActionWorkflowRun represents a WorkflowRun", | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { | ||||||
|  |         "actor": { | ||||||
|  |           "$ref": "#/definitions/User" | ||||||
|  |         }, | ||||||
|  |         "completed_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "CompletedAt" | ||||||
|  |         }, | ||||||
|  |         "conclusion": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Conclusion" | ||||||
|  |         }, | ||||||
|  |         "display_title": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "DisplayTitle" | ||||||
|  |         }, | ||||||
|  |         "event": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Event" | ||||||
|  |         }, | ||||||
|  |         "head_branch": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "HeadBranch" | ||||||
|  |         }, | ||||||
|  |         "head_repository": { | ||||||
|  |           "$ref": "#/definitions/Repository" | ||||||
|  |         }, | ||||||
|  |         "head_sha": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "HeadSha" | ||||||
|  |         }, | ||||||
|  |         "html_url": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "HTMLURL" | ||||||
|  |         }, | ||||||
|  |         "id": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "ID" | ||||||
|  |         }, | ||||||
|  |         "path": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Path" | ||||||
|  |         }, | ||||||
|  |         "repository": { | ||||||
|  |           "$ref": "#/definitions/Repository" | ||||||
|  |         }, | ||||||
|         "repository_id": { |         "repository_id": { | ||||||
|           "type": "integer", |           "type": "integer", | ||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "RepositoryID" |           "x-go-name": "RepositoryID" | ||||||
|  |         }, | ||||||
|  |         "run_attempt": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "RunAttempt" | ||||||
|  |         }, | ||||||
|  |         "run_number": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "RunNumber" | ||||||
|  |         }, | ||||||
|  |         "started_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "StartedAt" | ||||||
|  |         }, | ||||||
|  |         "status": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Status" | ||||||
|  |         }, | ||||||
|  |         "trigger_actor": { | ||||||
|  |           "$ref": "#/definitions/User" | ||||||
|  |         }, | ||||||
|  |         "url": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "URL" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|  |     }, | ||||||
|  |     "ActionWorkflowRunsResponse": { | ||||||
|  |       "description": "ActionWorkflowRunsResponse returns ActionWorkflowRuns", | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { | ||||||
|  |         "total_count": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "TotalCount" | ||||||
|  |         }, | ||||||
|  |         "workflow_runs": { | ||||||
|  |           "type": "array", | ||||||
|  |           "items": { | ||||||
|  |             "$ref": "#/definitions/ActionWorkflowRun" | ||||||
|  |           }, | ||||||
|  |           "x-go-name": "Entries" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
|  |     }, | ||||||
|  |     "ActionWorkflowStep": { | ||||||
|  |       "description": "ActionWorkflowStep represents a step of a WorkflowJob", | ||||||
|  |       "type": "object", | ||||||
|  |       "properties": { | ||||||
|  |         "completed_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "CompletedAt" | ||||||
|  |         }, | ||||||
|  |         "conclusion": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Conclusion" | ||||||
|  |         }, | ||||||
|  |         "name": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Name" | ||||||
|  |         }, | ||||||
|  |         "number": { | ||||||
|  |           "type": "integer", | ||||||
|  |           "format": "int64", | ||||||
|  |           "x-go-name": "Number" | ||||||
|  |         }, | ||||||
|  |         "started_at": { | ||||||
|  |           "type": "string", | ||||||
|  |           "format": "date-time", | ||||||
|  |           "x-go-name": "StartedAt" | ||||||
|  |         }, | ||||||
|  |         "status": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Status" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
| @@ -28615,6 +29479,30 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "WorkflowJob": { | ||||||
|  |       "description": "WorkflowJob", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/ActionWorkflowJob" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "WorkflowJobsList": { | ||||||
|  |       "description": "WorkflowJobsList", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/ActionWorkflowJobsResponse" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "WorkflowRun": { | ||||||
|  |       "description": "WorkflowRun", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/ActionWorkflowRun" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "WorkflowRunsList": { | ||||||
|  |       "description": "WorkflowRunsList", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/ActionWorkflowRunsResponse" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "conflict": { |     "conflict": { | ||||||
|       "description": "APIConflict is a conflict empty response" |       "description": "APIConflict is a conflict empty response" | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -720,7 +720,7 @@ func TestWorkflowDispatchPublicApi(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch |   workflow_dispatch | ||||||
| jobs: | jobs: | ||||||
| @@ -800,7 +800,7 @@ func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } |   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } | ||||||
| jobs: | jobs: | ||||||
| @@ -891,7 +891,7 @@ func TestWorkflowDispatchPublicApiJSON(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } |   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } | ||||||
| jobs: | jobs: | ||||||
| @@ -977,7 +977,7 @@ func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } |   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } | ||||||
| jobs: | jobs: | ||||||
| @@ -1071,7 +1071,7 @@ func TestWorkflowDispatchPublicApiWithInputsNonDefaultBranchJSON(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch |   workflow_dispatch | ||||||
| jobs: | jobs: | ||||||
| @@ -1107,7 +1107,7 @@ jobs: | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "update", | 					Operation: "update", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } |   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } | ||||||
| jobs: | jobs: | ||||||
| @@ -1209,7 +1209,7 @@ func TestWorkflowApi(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Operation: "create", | 					Operation: "create", | ||||||
| 					TreePath:  ".gitea/workflows/dispatch.yml", | 					TreePath:  ".gitea/workflows/dispatch.yml", | ||||||
| 					ContentReader: strings.NewReader(`name: test | 					ContentReader: strings.NewReader(` | ||||||
| on: | on: | ||||||
|   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } |   workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } | ||||||
| jobs: | jobs: | ||||||
|   | |||||||
| @@ -910,8 +910,7 @@ jobs: | |||||||
| 		assert.Equal(t, commitID, payloads[3].WorkflowJob.HeadSha) | 		assert.Equal(t, commitID, payloads[3].WorkflowJob.HeadSha) | ||||||
| 		assert.Equal(t, "repo1", payloads[3].Repo.Name) | 		assert.Equal(t, "repo1", payloads[3].Repo.Name) | ||||||
| 		assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName) | 		assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName) | ||||||
| 		assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[3].WorkflowJob.RunID, payloads[3].WorkflowJob.ID)) | 		assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID)) | ||||||
| 		assert.Contains(t, payloads[3].WorkflowJob.URL, payloads[3].WorkflowJob.RunURL) |  | ||||||
| 		assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0)) | 		assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0)) | ||||||
| 		assert.Len(t, payloads[3].WorkflowJob.Steps, 1) | 		assert.Len(t, payloads[3].WorkflowJob.Steps, 1) | ||||||
|  |  | ||||||
| @@ -947,9 +946,207 @@ jobs: | |||||||
| 		assert.Equal(t, commitID, payloads[6].WorkflowJob.HeadSha) | 		assert.Equal(t, commitID, payloads[6].WorkflowJob.HeadSha) | ||||||
| 		assert.Equal(t, "repo1", payloads[6].Repo.Name) | 		assert.Equal(t, "repo1", payloads[6].Repo.Name) | ||||||
| 		assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName) | 		assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName) | ||||||
| 		assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[6].WorkflowJob.RunID, payloads[6].WorkflowJob.ID)) | 		assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID)) | ||||||
| 		assert.Contains(t, payloads[6].WorkflowJob.URL, payloads[6].WorkflowJob.RunURL) |  | ||||||
| 		assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1)) | 		assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1)) | ||||||
| 		assert.Len(t, payloads[6].WorkflowJob.Steps, 2) | 		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, | ||||||
|  | 	} | ||||||
|  | 	runner.execTask(t, task, outcome) | ||||||
|  |  | ||||||
|  | 	task = runner.fetchTask(t) | ||||||
|  | 	outcome = &mockTaskOutcome{ | ||||||
|  | 		result: runnerv1.Result_RESULT_FAILURE, | ||||||
|  | 	} | ||||||
|  | 	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 := range 6 { | ||||||
|  | 		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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										167
									
								
								tests/integration/workflow_run_api_check_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								tests/integration/workflow_run_api_check_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package integration | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	auth_model "code.gitea.io/gitea/models/auth" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/tests" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestAPIWorkflowRun(t *testing.T) { | ||||||
|  | 	t.Run("AdminRuns", func(t *testing.T) { | ||||||
|  | 		testAPIWorkflowRunBasic(t, "/api/v1/admin/actions", "User1", 802, auth_model.AccessTokenScopeReadAdmin, auth_model.AccessTokenScopeReadRepository) | ||||||
|  | 	}) | ||||||
|  | 	t.Run("UserRuns", func(t *testing.T) { | ||||||
|  | 		testAPIWorkflowRunBasic(t, "/api/v1/user/actions", "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository) | ||||||
|  | 	}) | ||||||
|  | 	t.Run("OrgRuns", func(t *testing.T) { | ||||||
|  | 		testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository) | ||||||
|  | 	}) | ||||||
|  | 	t.Run("RepoRuns", func(t *testing.T) { | ||||||
|  | 		testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testAPIWorkflowRunBasic(t *testing.T, apiRootURL, userUsername string, runID int64, scope ...auth_model.AccessTokenScope) { | ||||||
|  | 	defer tests.PrepareTestEnv(t)() | ||||||
|  | 	token := getUserToken(t, userUsername, scope...) | ||||||
|  |  | ||||||
|  | 	apiRunsURL := fmt.Sprintf("%s/%s", apiRootURL, "runs") | ||||||
|  | 	req := NewRequest(t, "GET", apiRunsURL).AddTokenAuth(token) | ||||||
|  | 	runnerListResp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	runnerList := api.ActionWorkflowRunsResponse{} | ||||||
|  | 	DecodeJSON(t, runnerListResp, &runnerList) | ||||||
|  |  | ||||||
|  | 	foundRun := false | ||||||
|  |  | ||||||
|  | 	for _, run := range runnerList.Entries { | ||||||
|  | 		// Verify filtering works | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", run.Status, "", "", "", "") | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, run.Conclusion, "", "", "", "", "") | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", run.HeadBranch, "", "") | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", run.Event, "", "", "") | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, "") | ||||||
|  | 		verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, run.HeadSha) | ||||||
|  |  | ||||||
|  | 		// Verify run url works | ||||||
|  | 		req := NewRequest(t, "GET", run.URL).AddTokenAuth(token) | ||||||
|  | 		runResp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 		apiRun := api.ActionWorkflowRun{} | ||||||
|  | 		DecodeJSON(t, runResp, &apiRun) | ||||||
|  | 		assert.Equal(t, run.ID, apiRun.ID) | ||||||
|  | 		assert.Equal(t, run.Status, apiRun.Status) | ||||||
|  | 		assert.Equal(t, run.Conclusion, apiRun.Conclusion) | ||||||
|  | 		assert.Equal(t, run.Event, apiRun.Event) | ||||||
|  |  | ||||||
|  | 		// Verify jobs list works | ||||||
|  | 		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s", run.URL, "jobs")).AddTokenAuth(token) | ||||||
|  | 		jobsResp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 		jobList := api.ActionWorkflowJobsResponse{} | ||||||
|  | 		DecodeJSON(t, jobsResp, &jobList) | ||||||
|  |  | ||||||
|  | 		if run.ID == runID { | ||||||
|  | 			foundRun = true | ||||||
|  | 			assert.Len(t, jobList.Entries, 1) | ||||||
|  | 			for _, job := range jobList.Entries { | ||||||
|  | 				// Check the jobs list of the run | ||||||
|  | 				verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", run.URL, "jobs"), token, job.ID, "", job.Status) | ||||||
|  | 				verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", run.URL, "jobs"), token, job.ID, job.Conclusion, "") | ||||||
|  | 				// Check the run independent job list | ||||||
|  | 				verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", apiRootURL, "jobs"), token, job.ID, "", job.Status) | ||||||
|  | 				verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", apiRootURL, "jobs"), token, job.ID, job.Conclusion, "") | ||||||
|  |  | ||||||
|  | 				// Verify job url works | ||||||
|  | 				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.True(t, foundRun, "Expected to find run with ID %d", runID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func verifyWorkflowRunCanbeFoundWithStatusFilter(t *testing.T, runAPIURL, token string, id int64, conclusion, status, event, branch, actor, headSHA string) { | ||||||
|  | 	filter := url.Values{} | ||||||
|  | 	if conclusion != "" { | ||||||
|  | 		filter.Add("status", conclusion) | ||||||
|  | 	} | ||||||
|  | 	if status != "" { | ||||||
|  | 		filter.Add("status", status) | ||||||
|  | 	} | ||||||
|  | 	if event != "" { | ||||||
|  | 		filter.Set("event", event) | ||||||
|  | 	} | ||||||
|  | 	if branch != "" { | ||||||
|  | 		filter.Set("branch", branch) | ||||||
|  | 	} | ||||||
|  | 	if actor != "" { | ||||||
|  | 		filter.Set("actor", actor) | ||||||
|  | 	} | ||||||
|  | 	if headSHA != "" { | ||||||
|  | 		filter.Set("head_sha", headSHA) | ||||||
|  | 	} | ||||||
|  | 	req := NewRequest(t, "GET", runAPIURL+"?"+filter.Encode()).AddTokenAuth(token) | ||||||
|  | 	runResp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	runList := api.ActionWorkflowRunsResponse{} | ||||||
|  | 	DecodeJSON(t, runResp, &runList) | ||||||
|  |  | ||||||
|  | 	found := false | ||||||
|  | 	for _, run := range runList.Entries { | ||||||
|  | 		if conclusion != "" { | ||||||
|  | 			assert.Equal(t, conclusion, run.Conclusion) | ||||||
|  | 		} | ||||||
|  | 		if status != "" { | ||||||
|  | 			assert.Equal(t, status, run.Status) | ||||||
|  | 		} | ||||||
|  | 		if event != "" { | ||||||
|  | 			assert.Equal(t, event, run.Event) | ||||||
|  | 		} | ||||||
|  | 		if branch != "" { | ||||||
|  | 			assert.Equal(t, branch, run.HeadBranch) | ||||||
|  | 		} | ||||||
|  | 		if actor != "" { | ||||||
|  | 			assert.Equal(t, actor, run.Actor.UserName) | ||||||
|  | 		} | ||||||
|  | 		found = found || run.ID == id | ||||||
|  | 	} | ||||||
|  | 	assert.True(t, found, "Expected to find run with ID %d", id) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func verifyWorkflowJobCanbeFoundWithStatusFilter(t *testing.T, runAPIURL, token string, id int64, conclusion, status string) { | ||||||
|  | 	filter := conclusion | ||||||
|  | 	if filter == "" { | ||||||
|  | 		filter = status | ||||||
|  | 	} | ||||||
|  | 	if filter == "" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	req := NewRequest(t, "GET", runAPIURL+"?status="+filter).AddTokenAuth(token) | ||||||
|  | 	jobListResp := MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	jobList := api.ActionWorkflowJobsResponse{} | ||||||
|  | 	DecodeJSON(t, jobListResp, &jobList) | ||||||
|  |  | ||||||
|  | 	found := false | ||||||
|  | 	for _, job := range jobList.Entries { | ||||||
|  | 		if conclusion != "" { | ||||||
|  | 			assert.Equal(t, conclusion, job.Conclusion) | ||||||
|  | 		} else { | ||||||
|  | 			assert.Equal(t, status, job.Status) | ||||||
|  | 		} | ||||||
|  | 		found = found || job.ID == id | ||||||
|  | 	} | ||||||
|  | 	assert.True(t, found, "Expected to find job with ID %d", id) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user