mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 07:48:48 +01:00
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>
This commit is contained in:
@@ -205,3 +205,10 @@ type ActionRunnersResponse struct {
|
|||||||
Entries []*ActionRunner `json:"runners"`
|
Entries []*ActionRunner `json:"runners"`
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunDetails returns workflow_dispatch runid and url
|
||||||
|
type RunDetails struct {
|
||||||
|
WorkflowRunID int64 `json:"workflow_run_id"`
|
||||||
|
RunURL string `json:"run_url"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1004,9 +1004,15 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
|
|||||||
// in: body
|
// in: body
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
|
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
|
||||||
|
// - name: return_run_details
|
||||||
|
// description: Whether the response should include the workflow run ID and URLs.
|
||||||
|
// in: query
|
||||||
|
// type: boolean
|
||||||
// responses:
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/RunDetails"
|
||||||
// "204":
|
// "204":
|
||||||
// description: No Content
|
// description: No Content, if return_run_details is missing or false
|
||||||
// "400":
|
// "400":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "403":
|
// "403":
|
||||||
@@ -1023,7 +1029,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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 {
|
||||||
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
|
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
|
||||||
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
|
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
|
||||||
// So we have to manually read the `inputs[key]` from the form
|
// So we have to manually read the `inputs[key]` from the form
|
||||||
@@ -1054,7 +1060,22 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Status(http.StatusNoContent)
|
if !ctx.FormBool("return_run_details") {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowRun, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, &api.RunDetails{
|
||||||
|
WorkflowRunID: runID,
|
||||||
|
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), workflowRun.Index),
|
||||||
|
RunURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.APIURL(), runID),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ActionsEnableWorkflow(ctx *context.APIContext) {
|
func ActionsEnableWorkflow(ctx *context.APIContext) {
|
||||||
|
|||||||
@@ -46,3 +46,10 @@ type swaggerResponseActionWorkflowList struct {
|
|||||||
// in:body
|
// in:body
|
||||||
Body api.ActionWorkflowResponse `json:"body"`
|
Body api.ActionWorkflowResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunDetails
|
||||||
|
// swagger:response RunDetails
|
||||||
|
type swaggerResponseRunDetails struct {
|
||||||
|
// in:body
|
||||||
|
Body api.RunDetails `json:"body"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -936,7 +936,7 @@ func Run(ctx *context_module.Context) {
|
|||||||
ctx.ServerError("ref", nil)
|
ctx.ServerError("ref", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
|
_, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
|
||||||
for name, config := range workflowDispatch.Inputs {
|
for name, config := range workflowDispatch.Inputs {
|
||||||
value := ctx.Req.PostFormValue(name)
|
value := ctx.Req.PostFormValue(name)
|
||||||
if config.Type == "boolean" {
|
if config.Type == "boolean" {
|
||||||
|
|||||||
@@ -44,16 +44,16 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
|
|||||||
return repo_model.UpdateRepoUnit(ctx, cfgUnit)
|
return repo_model.UpdateRepoUnit(ctx, cfgUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) (runID int64, _ error) {
|
||||||
if workflowID == "" {
|
if workflowID == "" {
|
||||||
return util.ErrorWrapTranslatable(
|
return 0, util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("workflowID is empty"),
|
util.NewNotExistErrorf("workflowID is empty"),
|
||||||
"actions.workflow.not_found", workflowID,
|
"actions.workflow.not_found", workflowID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ref == "" {
|
if ref == "" {
|
||||||
return util.ErrorWrapTranslatable(
|
return 0, util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("ref is empty"),
|
util.NewNotExistErrorf("ref is empty"),
|
||||||
"form.target_ref_not_exist", ref,
|
"form.target_ref_not_exist", ref,
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||||
cfg := cfgUnit.ActionsConfig()
|
cfg := cfgUnit.ActionsConfig()
|
||||||
if cfg.IsWorkflowDisabled(workflowID) {
|
if cfg.IsWorkflowDisabled(workflowID) {
|
||||||
return util.ErrorWrapTranslatable(
|
return 0, util.ErrorWrapTranslatable(
|
||||||
util.NewPermissionDeniedErrorf("workflow is disabled"),
|
util.NewPermissionDeniedErrorf("workflow is disabled"),
|
||||||
"actions.workflow.disabled",
|
"actions.workflow.disabled",
|
||||||
)
|
)
|
||||||
@@ -82,7 +82,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
|
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorWrapTranslatable(
|
return 0, util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("ref %q doesn't exist", ref),
|
util.NewNotExistErrorf("ref %q doesn't exist", ref),
|
||||||
"form.target_ref_not_exist", ref,
|
"form.target_ref_not_exist", ref,
|
||||||
)
|
)
|
||||||
@@ -91,7 +91,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
// get workflow entry from runTargetCommit
|
// get workflow entry from runTargetCommit
|
||||||
_, entries, err := actions.ListWorkflows(runTargetCommit)
|
_, entries, err := actions.ListWorkflows(runTargetCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// find workflow from commit
|
// find workflow from commit
|
||||||
@@ -122,7 +122,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return util.ErrorWrapTranslatable(
|
return 0, util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||||
"actions.workflow.not_found", workflowID,
|
"actions.workflow.not_found", workflowID,
|
||||||
)
|
)
|
||||||
@@ -130,12 +130,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
|
|
||||||
content, err := actions.GetContentFromEntry(entry)
|
content, err := actions.GetContentFromEntry(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
singleWorkflow := &jobparser.SingleWorkflow{}
|
singleWorkflow := &jobparser.SingleWorkflow{}
|
||||||
if err := yaml.Unmarshal(content, singleWorkflow); err != nil {
|
if err := yaml.Unmarshal(content, singleWorkflow); err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal workflow content: %w", err)
|
return 0, fmt.Errorf("failed to unmarshal workflow content: %w", err)
|
||||||
}
|
}
|
||||||
// get inputs from post
|
// get inputs from post
|
||||||
workflow := &model.Workflow{
|
workflow := &model.Workflow{
|
||||||
@@ -144,7 +144,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
inputsWithDefaults := make(map[string]any)
|
inputsWithDefaults := make(map[string]any)
|
||||||
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
|
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
|
||||||
if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil {
|
if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +161,13 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
|
|
||||||
var eventPayload []byte
|
var eventPayload []byte
|
||||||
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
||||||
return fmt.Errorf("JSONPayload: %w", err)
|
return 0, fmt.Errorf("JSONPayload: %w", err)
|
||||||
}
|
}
|
||||||
run.EventPayload = string(eventPayload)
|
run.EventPayload = string(eventPayload)
|
||||||
|
|
||||||
// Insert the action run and its associated jobs into the database
|
// Insert the action run and its associated jobs into the database
|
||||||
if err := PrepareRunAndInsert(ctx, content, run, inputsWithDefaults); err != nil {
|
if err := PrepareRunAndInsert(ctx, content, run, inputsWithDefaults); err != nil {
|
||||||
return fmt.Errorf("PrepareRun: %w", err)
|
return 0, fmt.Errorf("PrepareRun: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return run.ID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
37
templates/swagger/v1_json.tmpl
generated
37
templates/swagger/v1_json.tmpl
generated
@@ -6031,11 +6031,20 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/CreateActionWorkflowDispatch"
|
"$ref": "#/definitions/CreateActionWorkflowDispatch"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the response should include the workflow run ID and URLs.",
|
||||||
|
"name": "return_run_details",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/RunDetails"
|
||||||
|
},
|
||||||
"204": {
|
"204": {
|
||||||
"description": "No Content"
|
"description": "No Content, if return_run_details is missing or false"
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"$ref": "#/responses/error"
|
"$ref": "#/responses/error"
|
||||||
@@ -28319,6 +28328,26 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"RunDetails": {
|
||||||
|
"description": "RunDetails returns workflow_dispatch runid and url",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"html_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
|
"run_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "RunURL"
|
||||||
|
},
|
||||||
|
"workflow_run_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "WorkflowRunID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"SearchResults": {
|
"SearchResults": {
|
||||||
"description": "SearchResults results of a successful search",
|
"description": "SearchResults results of a successful search",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -30192,6 +30221,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RunDetails": {
|
||||||
|
"description": "RunDetails",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/RunDetails"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Runner": {
|
"Runner": {
|
||||||
"description": "Runner",
|
"description": "Runner",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPullRequestTargetEvent(t *testing.T) {
|
func TestPullRequestTargetEvent(t *testing.T) {
|
||||||
@@ -906,6 +907,27 @@ jobs:
|
|||||||
CommitSHA: branch.CommitID,
|
CommitSHA: branch.CommitID,
|
||||||
})
|
})
|
||||||
assert.NotNil(t, run)
|
assert.NotNil(t, run)
|
||||||
|
|
||||||
|
// Now trigger with rundetails
|
||||||
|
values.Set("return_run_details", "true")
|
||||||
|
|
||||||
|
req = NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
runDetails := &api.RunDetails{}
|
||||||
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(runDetails))
|
||||||
|
assert.NotEqual(t, 0, runDetails.WorkflowRunID)
|
||||||
|
|
||||||
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||||
|
ID: runDetails.WorkflowRunID,
|
||||||
|
Title: "add workflow",
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Event: "workflow_dispatch",
|
||||||
|
Ref: "refs/heads/main",
|
||||||
|
WorkflowID: "dispatch.yml",
|
||||||
|
CommitSHA: branch.CommitID,
|
||||||
|
})
|
||||||
|
assert.NotNil(t, run)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user