Skip to content

Commit bc9817b

Browse files
WorkflowDispatch api optionally return runid (#36706)
Implements https://github.blog/changelog/2026-02-19-workflow-dispatch-api-now-returns-run-ids --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent 553277b commit bc9817b

7 files changed

Lines changed: 110 additions & 18 deletions

File tree

modules/structs/repo_actions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,10 @@ type ActionRunnersResponse struct {
205205
Entries []*ActionRunner `json:"runners"`
206206
TotalCount int64 `json:"total_count"`
207207
}
208+
209+
// RunDetails returns workflow_dispatch runid and url
210+
type RunDetails struct {
211+
WorkflowRunID int64 `json:"workflow_run_id"`
212+
RunURL string `json:"run_url"`
213+
HTMLURL string `json:"html_url"`
214+
}

routers/api/v1/repo/action.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,9 +1004,15 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
10041004
// in: body
10051005
// schema:
10061006
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
1007+
// - name: return_run_details
1008+
// description: Whether the response should include the workflow run ID and URLs.
1009+
// in: query
1010+
// type: boolean
10071011
// responses:
1012+
// "200":
1013+
// "$ref": "#/responses/RunDetails"
10081014
// "204":
1009-
// description: No Content
1015+
// description: No Content, if return_run_details is missing or false
10101016
// "400":
10111017
// "$ref": "#/responses/error"
10121018
// "403":
@@ -1023,7 +1029,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
10231029
return
10241030
}
10251031

1026-
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
1032+
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
10271033
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
10281034
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
10291035
// So we have to manually read the `inputs[key]` from the form
@@ -1054,7 +1060,22 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
10541060
return
10551061
}
10561062

1057-
ctx.Status(http.StatusNoContent)
1063+
if !ctx.FormBool("return_run_details") {
1064+
ctx.Status(http.StatusNoContent)
1065+
return
1066+
}
1067+
1068+
workflowRun, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
1069+
if err != nil {
1070+
ctx.APIErrorInternal(err)
1071+
return
1072+
}
1073+
1074+
ctx.JSON(http.StatusOK, &api.RunDetails{
1075+
WorkflowRunID: runID,
1076+
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), workflowRun.Index),
1077+
RunURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.APIURL(), runID),
1078+
})
10581079
}
10591080

10601081
func ActionsEnableWorkflow(ctx *context.APIContext) {

routers/api/v1/swagger/action.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,10 @@ type swaggerResponseActionWorkflowList struct {
4646
// in:body
4747
Body api.ActionWorkflowResponse `json:"body"`
4848
}
49+
50+
// RunDetails
51+
// swagger:response RunDetails
52+
type swaggerResponseRunDetails struct {
53+
// in:body
54+
Body api.RunDetails `json:"body"`
55+
}

routers/web/repo/actions/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ func Run(ctx *context_module.Context) {
936936
ctx.ServerError("ref", nil)
937937
return
938938
}
939-
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
939+
_, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
940940
for name, config := range workflowDispatch.Inputs {
941941
value := ctx.Req.PostFormValue(name)
942942
if config.Type == "boolean" {

services/actions/workflow.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,16 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
4444
return repo_model.UpdateRepoUnit(ctx, cfgUnit)
4545
}
4646

47-
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 {
47+
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) (runID int64, _ error) {
4848
if workflowID == "" {
49-
return util.ErrorWrapTranslatable(
49+
return 0, util.ErrorWrapTranslatable(
5050
util.NewNotExistErrorf("workflowID is empty"),
5151
"actions.workflow.not_found", workflowID,
5252
)
5353
}
5454

5555
if ref == "" {
56-
return util.ErrorWrapTranslatable(
56+
return 0, util.ErrorWrapTranslatable(
5757
util.NewNotExistErrorf("ref is empty"),
5858
"form.target_ref_not_exist", ref,
5959
)
@@ -63,7 +63,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
6363
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
6464
cfg := cfgUnit.ActionsConfig()
6565
if cfg.IsWorkflowDisabled(workflowID) {
66-
return util.ErrorWrapTranslatable(
66+
return 0, util.ErrorWrapTranslatable(
6767
util.NewPermissionDeniedErrorf("workflow is disabled"),
6868
"actions.workflow.disabled",
6969
)
@@ -82,7 +82,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
8282
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
8383
}
8484
if err != nil {
85-
return util.ErrorWrapTranslatable(
85+
return 0, util.ErrorWrapTranslatable(
8686
util.NewNotExistErrorf("ref %q doesn't exist", ref),
8787
"form.target_ref_not_exist", ref,
8888
)
@@ -91,7 +91,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
9191
// get workflow entry from runTargetCommit
9292
_, entries, err := actions.ListWorkflows(runTargetCommit)
9393
if err != nil {
94-
return err
94+
return 0, err
9595
}
9696

9797
// find workflow from commit
@@ -122,20 +122,20 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
122122
}
123123

124124
if entry == nil {
125-
return util.ErrorWrapTranslatable(
125+
return 0, util.ErrorWrapTranslatable(
126126
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
127127
"actions.workflow.not_found", workflowID,
128128
)
129129
}
130130

131131
content, err := actions.GetContentFromEntry(entry)
132132
if err != nil {
133-
return err
133+
return 0, err
134134
}
135135

136136
singleWorkflow := &jobparser.SingleWorkflow{}
137137
if err := yaml.Unmarshal(content, singleWorkflow); err != nil {
138-
return fmt.Errorf("failed to unmarshal workflow content: %w", err)
138+
return 0, fmt.Errorf("failed to unmarshal workflow content: %w", err)
139139
}
140140
// get inputs from post
141141
workflow := &model.Workflow{
@@ -144,7 +144,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
144144
inputsWithDefaults := make(map[string]any)
145145
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
146146
if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil {
147-
return err
147+
return 0, err
148148
}
149149
}
150150

@@ -161,13 +161,13 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
161161

162162
var eventPayload []byte
163163
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
164-
return fmt.Errorf("JSONPayload: %w", err)
164+
return 0, fmt.Errorf("JSONPayload: %w", err)
165165
}
166166
run.EventPayload = string(eventPayload)
167167

168168
// Insert the action run and its associated jobs into the database
169169
if err := PrepareRunAndInsert(ctx, content, run, inputsWithDefaults); err != nil {
170-
return fmt.Errorf("PrepareRun: %w", err)
170+
return 0, fmt.Errorf("PrepareRun: %w", err)
171171
}
172-
return nil
172+
return run.ID, nil
173173
}

templates/swagger/v1_json.tmpl

Lines changed: 36 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/actions_trigger_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
files_service "code.gitea.io/gitea/services/repository/files"
3939

4040
"github.com/stretchr/testify/assert"
41+
"github.com/stretchr/testify/require"
4142
)
4243

4344
func TestPullRequestTargetEvent(t *testing.T) {
@@ -906,6 +907,27 @@ jobs:
906907
CommitSHA: branch.CommitID,
907908
})
908909
assert.NotNil(t, run)
910+
911+
// Now trigger with rundetails
912+
values.Set("return_run_details", "true")
913+
914+
req = NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
915+
AddTokenAuth(token)
916+
resp := MakeRequest(t, req, http.StatusOK)
917+
runDetails := &api.RunDetails{}
918+
require.NoError(t, json.NewDecoder(resp.Body).Decode(runDetails))
919+
assert.NotEqual(t, 0, runDetails.WorkflowRunID)
920+
921+
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
922+
ID: runDetails.WorkflowRunID,
923+
Title: "add workflow",
924+
RepoID: repo.ID,
925+
Event: "workflow_dispatch",
926+
Ref: "refs/heads/main",
927+
WorkflowID: "dispatch.yml",
928+
CommitSHA: branch.CommitID,
929+
})
930+
assert.NotNil(t, run)
909931
})
910932
}
911933

0 commit comments

Comments
 (0)