mirror of
https://github.com/go-gitea/gitea
synced 2024-12-23 00:54:28 +00:00
Actions support workflow dispatch event (#28163)
fix #23668 My plan: * In the `actions.list` method, if workflow is selected and IsAdmin, check whether the on event contains `workflow_dispatch`. If so, display a `Run workflow` button to allow the user to manually trigger the run. * Providing a form that allows users to select target brach or tag, and these parameters can be configured in yaml * Simple form validation, `required` input cannot be empty * Add a route `/actions/run`, and an `actions.Run` method to handle * Add `WorkflowDispatchPayload` struct to pass the Webhook event payload to the runner when triggered, this payload carries the `inputs` values and other fields, doc: [workflow_dispatch payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch) Other PRs * the `Workflow.WorkflowDispatchConfig()` method still return non-nil when workflow_dispatch is not defined. I submitted a PR https://gitea.com/gitea/act/pulls/85 to fix it. Still waiting for them to process. Behavior should be same with github, but may cause confusion. Here's a quick reminder. * [Doc](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) Said: This event will `only` trigger a workflow run if the workflow file is `on the default branch`. * If the workflow yaml file only exists in a non-default branch, it cannot be triggered. (It will not even show up in the workflow list) * If the same workflow yaml file exists in each branch at the same time, the version of the default branch is used. Even if `Use workflow from` selects another branch ![image](https://github.com/go-gitea/gitea/assets/3114995/4bf596f3-426b-48e8-9b8f-0f6d18defd79) ```yaml name: Docker Image CI on: workflow_dispatch: inputs: logLevel: description: 'Log level' required: true default: 'warning' type: choice options: - info - warning - debug tags: description: 'Test scenario tags' required: false type: boolean boolean_default_true: description: 'Test scenario tags' required: true type: boolean default: true boolean_default_false: description: 'Test scenario tags' required: false type: boolean default: false environment: description: 'Environment to run tests against' type: environment required: true default: 'environment values' number_required_1: description: 'number ' type: number required: true default: '100' number_required_2: description: 'number' type: number required: true default: '100' number_required_3: description: 'number' type: number required: true default: '100' number_1: description: 'number' type: number required: false number_2: description: 'number' type: number required: false number_3: description: 'number' type: number required: false env: inputs_logLevel: ${{ inputs.logLevel }} inputs_tags: ${{ inputs.tags }} inputs_boolean_default_true: ${{ inputs.boolean_default_true }} inputs_boolean_default_false: ${{ inputs.boolean_default_false }} inputs_environment: ${{ inputs.environment }} inputs_number_1: ${{ inputs.number_1 }} inputs_number_2: ${{ inputs.number_2 }} inputs_number_3: ${{ inputs.number_3 }} inputs_number_required_1: ${{ inputs.number_required_1 }} inputs_number_required_2: ${{ inputs.number_required_2 }} inputs_number_required_3: ${{ inputs.number_required_3 }} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: ls -la - run: env | grep inputs - run: echo ${{ inputs.logLevel }} - run: echo ${{ inputs.boolean_default_false }} ``` ![image](https://github.com/go-gitea/gitea/assets/3114995/a58a842d-a0ff-4618-bc6d-83a9596d07c8) ![image](https://github.com/go-gitea/gitea/assets/3114995/44a7cca5-7bd4-42a9-8723-91751a501c88) --------- Co-authored-by: TKaxv_7S <954067342@qq.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
561b5c504f
commit
36232b69db
@ -494,3 +494,17 @@ type PackagePayload struct {
|
|||||||
func (p *PackagePayload) JSONPayload() ([]byte, error) {
|
func (p *PackagePayload) JSONPayload() ([]byte, error) {
|
||||||
return json.MarshalIndent(p, "", " ")
|
return json.MarshalIndent(p, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorkflowDispatchPayload represents a workflow dispatch payload
|
||||||
|
type WorkflowDispatchPayload struct {
|
||||||
|
Workflow string `json:"workflow"`
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Inputs map[string]any `json:"inputs"`
|
||||||
|
Repository *Repository `json:"repository"`
|
||||||
|
Sender *User `json:"sender"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONPayload implements Payload
|
||||||
|
func (p *WorkflowDispatchPayload) JSONPayload() ([]byte, error) {
|
||||||
|
return json.MarshalIndent(p, "", " ")
|
||||||
|
}
|
||||||
|
@ -628,6 +628,7 @@ org_still_own_repo = "This organization still owns one or more repositories, del
|
|||||||
org_still_own_packages = "This organization still owns one or more packages, delete them first."
|
org_still_own_packages = "This organization still owns one or more packages, delete them first."
|
||||||
|
|
||||||
target_branch_not_exist = Target branch does not exist.
|
target_branch_not_exist = Target branch does not exist.
|
||||||
|
target_ref_not_exist = Target ref does not exist %s
|
||||||
|
|
||||||
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
|
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
|
||||||
|
|
||||||
@ -3701,6 +3702,11 @@ workflow.disable_success = Workflow '%s' disabled successfully.
|
|||||||
workflow.enable = Enable Workflow
|
workflow.enable = Enable Workflow
|
||||||
workflow.enable_success = Workflow '%s' enabled successfully.
|
workflow.enable_success = Workflow '%s' enabled successfully.
|
||||||
workflow.disabled = Workflow is disabled.
|
workflow.disabled = Workflow is disabled.
|
||||||
|
workflow.run = Run Workflow
|
||||||
|
workflow.not_found = Workflow '%s' not found.
|
||||||
|
workflow.run_success = Workflow '%s' run successfully.
|
||||||
|
workflow.from_ref = Use workflow from
|
||||||
|
workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger.
|
||||||
|
|
||||||
need_approval_desc = Need approval to run workflows for fork pull request.
|
need_approval_desc = Need approval to run workflows for fork pull request.
|
||||||
|
|
||||||
|
@ -7,22 +7,28 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
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"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"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/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/repo"
|
"code.gitea.io/gitea/routers/web/repo"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -58,8 +64,13 @@ func MustEnableActions(ctx *context.Context) {
|
|||||||
func List(ctx *context.Context) {
|
func List(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||||
ctx.Data["PageIsActions"] = true
|
ctx.Data["PageIsActions"] = true
|
||||||
|
workflowID := ctx.FormString("workflow")
|
||||||
|
actorID := ctx.FormInt64("actor")
|
||||||
|
status := ctx.FormInt("status")
|
||||||
|
ctx.Data["CurWorkflow"] = workflowID
|
||||||
|
|
||||||
var workflows []Workflow
|
var workflows []Workflow
|
||||||
|
var curWorkflow *model.Workflow
|
||||||
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
|
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
|
||||||
ctx.ServerError("IsEmpty", err)
|
ctx.ServerError("IsEmpty", err)
|
||||||
return
|
return
|
||||||
@ -140,6 +151,10 @@ func List(ctx *context.Context) {
|
|||||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||||
}
|
}
|
||||||
workflows = append(workflows, workflow)
|
workflows = append(workflows, workflow)
|
||||||
|
|
||||||
|
if workflow.Entry.Name() == workflowID {
|
||||||
|
curWorkflow = wf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Data["workflows"] = workflows
|
ctx.Data["workflows"] = workflows
|
||||||
@ -150,17 +165,46 @@ func List(ctx *context.Context) {
|
|||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
workflow := ctx.FormString("workflow")
|
|
||||||
actorID := ctx.FormInt64("actor")
|
|
||||||
status := ctx.FormInt("status")
|
|
||||||
ctx.Data["CurWorkflow"] = workflow
|
|
||||||
|
|
||||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||||
ctx.Data["ActionsConfig"] = actionsConfig
|
ctx.Data["ActionsConfig"] = actionsConfig
|
||||||
|
|
||||||
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
|
if len(workflowID) > 0 && ctx.Repo.IsAdmin() {
|
||||||
ctx.Data["AllowDisableOrEnableWorkflow"] = true
|
ctx.Data["AllowDisableOrEnableWorkflow"] = true
|
||||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
|
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
|
||||||
|
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
|
||||||
|
|
||||||
|
if !isWorkflowDisabled && curWorkflow != nil {
|
||||||
|
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
|
||||||
|
if workflowDispatchConfig != nil {
|
||||||
|
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
|
||||||
|
|
||||||
|
branchOpts := git_model.FindBranchOptions{
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
IsDeletedBranch: optional.Some(false),
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
ListAll: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FindBranchNames", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// always put default branch on the top if it exists
|
||||||
|
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
|
||||||
|
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||||
|
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||||
|
}
|
||||||
|
ctx.Data["Branches"] = branches
|
||||||
|
|
||||||
|
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Tags"] = tags
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
|
// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
|
||||||
@ -177,7 +221,7 @@ func List(ctx *context.Context) {
|
|||||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||||
},
|
},
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
WorkflowID: workflow,
|
WorkflowID: workflowID,
|
||||||
TriggerUserID: actorID,
|
TriggerUserID: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +258,7 @@ func List(ctx *context.Context) {
|
|||||||
|
|
||||||
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
|
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
|
||||||
pager.SetDefaultParams(ctx)
|
pager.SetDefaultParams(ctx)
|
||||||
pager.AddParamString("workflow", workflow)
|
pager.AddParamString("workflow", workflowID)
|
||||||
pager.AddParamString("actor", fmt.Sprint(actorID))
|
pager.AddParamString("actor", fmt.Sprint(actorID))
|
||||||
pager.AddParamString("status", fmt.Sprint(status))
|
pager.AddParamString("status", fmt.Sprint(status))
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
@ -222,3 +266,86 @@ func List(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplListActions)
|
ctx.HTML(http.StatusOK, tplListActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkflowDispatchInput struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Required bool `yaml:"required"`
|
||||||
|
Default string `yaml:"default"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Options []string `yaml:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkflowDispatch struct {
|
||||||
|
Inputs []WorkflowDispatchInput
|
||||||
|
}
|
||||||
|
|
||||||
|
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
|
||||||
|
switch w.RawOn.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
var val string
|
||||||
|
if !decodeNode(w.RawOn, &val) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val == "workflow_dispatch" {
|
||||||
|
return &WorkflowDispatch{}
|
||||||
|
}
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
var val []string
|
||||||
|
if !decodeNode(w.RawOn, &val) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, v := range val {
|
||||||
|
if v == "workflow_dispatch" {
|
||||||
|
return &WorkflowDispatch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case yaml.MappingNode:
|
||||||
|
var val map[string]yaml.Node
|
||||||
|
if !decodeNode(w.RawOn, &val) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowDispatchNode, found := val["workflow_dispatch"]
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var workflowDispatch WorkflowDispatch
|
||||||
|
var workflowDispatchVal map[string]yaml.Node
|
||||||
|
if !decodeNode(workflowDispatchNode, &workflowDispatchVal) {
|
||||||
|
return &workflowDispatch
|
||||||
|
}
|
||||||
|
|
||||||
|
inputsNode, found := workflowDispatchVal["inputs"]
|
||||||
|
if !found || inputsNode.Kind != yaml.MappingNode {
|
||||||
|
return &workflowDispatch
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
if i+1 >= len(inputsNode.Content) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var input WorkflowDispatchInput
|
||||||
|
if decodeNode(*inputsNode.Content[i+1], &input) {
|
||||||
|
input.Name = inputsNode.Content[i].Value
|
||||||
|
workflowDispatch.Inputs = append(workflowDispatch.Inputs, input)
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
return &workflowDispatch
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNode(node yaml.Node, out any) bool {
|
||||||
|
if err := node.Decode(out); err != nil {
|
||||||
|
log.Warn("Failed to decode node %v into %T: %v", node, out, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
156
routers/web/repo/actions/actions_test.go
Normal file
156
routers/web/repo/actions/actions_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
act_model "github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
|
||||||
|
yaml := `
|
||||||
|
name: local-action-docker-url
|
||||||
|
`
|
||||||
|
workflow, err := act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch := workflowDispatchConfig(workflow)
|
||||||
|
assert.Nil(t, workflowDispatch)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on: push
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.Nil(t, workflowDispatch)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on: workflow_dispatch
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.NotNil(t, workflowDispatch)
|
||||||
|
assert.Nil(t, workflowDispatch.Inputs)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on: [push, pull_request]
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.Nil(t, workflowDispatch)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.Nil(t, workflowDispatch)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on: [push, workflow_dispatch]
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.NotNil(t, workflowDispatch)
|
||||||
|
assert.Nil(t, workflowDispatch.Inputs)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
- push
|
||||||
|
- workflow_dispatch
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.NotNil(t, workflowDispatch)
|
||||||
|
assert.Nil(t, workflowDispatch.Inputs)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
`
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.NotNil(t, workflowDispatch)
|
||||||
|
assert.Nil(t, workflowDispatch.Inputs)
|
||||||
|
|
||||||
|
yaml = `
|
||||||
|
name: local-action-docker-url
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
logLevel:
|
||||||
|
description: 'Log level'
|
||||||
|
required: true
|
||||||
|
default: 'warning'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- info
|
||||||
|
- warning
|
||||||
|
- debug
|
||||||
|
boolean_default_true:
|
||||||
|
description: 'Test scenario tags'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
boolean_default_false:
|
||||||
|
description: 'Test scenario tags'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
`
|
||||||
|
|
||||||
|
workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
|
||||||
|
assert.NoError(t, err, "read workflow should succeed")
|
||||||
|
workflowDispatch = workflowDispatchConfig(workflow)
|
||||||
|
assert.NotNil(t, workflowDispatch)
|
||||||
|
assert.Equal(t, WorkflowDispatchInput{
|
||||||
|
Name: "logLevel",
|
||||||
|
Default: "warning",
|
||||||
|
Description: "Log level",
|
||||||
|
Options: []string{
|
||||||
|
"info",
|
||||||
|
"warning",
|
||||||
|
"debug",
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
Type: "choice",
|
||||||
|
}, workflowDispatch.Inputs[0])
|
||||||
|
assert.Equal(t, WorkflowDispatchInput{
|
||||||
|
Name: "boolean_default_true",
|
||||||
|
Default: "true",
|
||||||
|
Description: "Test scenario tags",
|
||||||
|
Required: true,
|
||||||
|
Type: "boolean",
|
||||||
|
}, workflowDispatch.Inputs[1])
|
||||||
|
assert.Equal(t, WorkflowDispatchInput{
|
||||||
|
Name: "boolean_default_false",
|
||||||
|
Default: "false",
|
||||||
|
Description: "Test scenario tags",
|
||||||
|
Required: true,
|
||||||
|
Type: "boolean",
|
||||||
|
}, workflowDispatch.Inputs[2])
|
||||||
|
}
|
@ -18,18 +18,26 @@ 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/models/perm"
|
||||||
|
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"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
actions_service "code.gitea.io/gitea/services/actions"
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
context_module "code.gitea.io/gitea/services/context"
|
context_module "code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -745,3 +753,164 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
|
|||||||
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
|
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
|
||||||
ctx.JSONRedirect(redirectURL)
|
ctx.JSONRedirect(redirectURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Run(ctx *context_module.Context) {
|
||||||
|
redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(ctx.FormString("workflow")),
|
||||||
|
url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
|
||||||
|
|
||||||
|
workflowID := ctx.FormString("workflow")
|
||||||
|
if len(workflowID) == 0 {
|
||||||
|
ctx.ServerError("workflow", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := ctx.FormString("ref")
|
||||||
|
if len(ref) == 0 {
|
||||||
|
ctx.ServerError("ref", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// can not rerun job when workflow is disabled
|
||||||
|
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||||
|
cfg := cfgUnit.ActionsConfig()
|
||||||
|
if cfg.IsWorkflowDisabled(workflowID) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("actions.workflow.disabled"))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get target commit of run from specified ref
|
||||||
|
refName := git.RefName(ref)
|
||||||
|
var runTargetCommit *git.Commit
|
||||||
|
var err error
|
||||||
|
if refName.IsTag() {
|
||||||
|
runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
|
||||||
|
} else if refName.IsBranch() {
|
||||||
|
runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get workflow entry from default branch commit
|
||||||
|
defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries, err := actions.ListWorkflows(defaultBranchCommit)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find workflow from commit
|
||||||
|
var workflows []*jobparser.SingleWorkflow
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name() == workflowID {
|
||||||
|
content, err := actions.GetContentFromEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
workflows, err = jobparser.Parse(content)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("workflow", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(workflows) == 0 {
|
||||||
|
ctx.Flash.Error(ctx.Tr("actions.workflow.not_found", workflowID))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get inputs from post
|
||||||
|
workflow := &model.Workflow{
|
||||||
|
RawOn: workflows[0].RawOn,
|
||||||
|
}
|
||||||
|
inputs := make(map[string]any)
|
||||||
|
if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
|
||||||
|
for name, config := range workflowDispatch.Inputs {
|
||||||
|
value := ctx.Req.PostForm.Get(name)
|
||||||
|
if config.Type == "boolean" {
|
||||||
|
// https://www.w3.org/TR/html401/interact/forms.html
|
||||||
|
// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
|
||||||
|
// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
|
||||||
|
// A switch is "on" when the control element's checked attribute is set.
|
||||||
|
// When a form is submitted, only "on" checkbox controls can become successful.
|
||||||
|
inputs[name] = strconv.FormatBool(value == "on")
|
||||||
|
} else if value != "" {
|
||||||
|
inputs[name] = value
|
||||||
|
} else {
|
||||||
|
inputs[name] = config.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
|
||||||
|
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||||
|
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
|
||||||
|
workflowDispatchPayload := &api.WorkflowDispatchPayload{
|
||||||
|
Workflow: workflowID,
|
||||||
|
Ref: ref,
|
||||||
|
Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
||||||
|
Inputs: inputs,
|
||||||
|
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
|
||||||
|
}
|
||||||
|
var eventPayload []byte
|
||||||
|
if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
|
||||||
|
ctx.ServerError("JSONPayload", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
run := &actions_model.ActionRun{
|
||||||
|
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||||
|
WorkflowID: workflowID,
|
||||||
|
TriggerUserID: ctx.Doer.ID,
|
||||||
|
Ref: ref,
|
||||||
|
CommitSHA: runTargetCommit.ID.String(),
|
||||||
|
IsForkPullRequest: false,
|
||||||
|
Event: "workflow_dispatch",
|
||||||
|
TriggerEvent: "workflow_dispatch",
|
||||||
|
EventPayload: string(eventPayload),
|
||||||
|
Status: actions_model.StatusWaiting,
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel running jobs of the same workflow
|
||||||
|
if err := actions_model.CancelPreviousJobs(
|
||||||
|
ctx,
|
||||||
|
run.RepoID,
|
||||||
|
run.Ref,
|
||||||
|
run.WorkflowID,
|
||||||
|
run.Event,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("CancelRunningJobs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the action run and its associated jobs into the database
|
||||||
|
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
||||||
|
ctx.ServerError("workflow", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("FindRunJobs: %v", err)
|
||||||
|
}
|
||||||
|
actions_service.CreateCommitStatus(ctx, alljobs...)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
|
||||||
|
ctx.Redirect(redirectURL)
|
||||||
|
}
|
||||||
|
@ -1384,6 +1384,7 @@ func registerRoutes(m *web.Router) {
|
|||||||
m.Get("", actions.List)
|
m.Get("", actions.List)
|
||||||
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
|
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
|
||||||
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
|
||||||
|
m.Post("/run", reqRepoAdmin, actions.Run)
|
||||||
|
|
||||||
m.Group("/runs/{run}", func() {
|
m.Group("/runs/{run}", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
|
@ -76,6 +76,11 @@
|
|||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .WorkflowDispatchConfig}}
|
||||||
|
{{template "repo/actions/workflow_dispatch" .}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/actions/runs_list" .}}
|
{{template "repo/actions/runs_list" .}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
78
templates/repo/actions/workflow_dispatch.tmpl
Normal file
78
templates/repo/actions/workflow_dispatch.tmpl
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<div class="ui blue info message tw-flex tw-justify-between tw-items-center">
|
||||||
|
<span class="ui text middle">{{ctx.Locale.Tr "actions.workflow.has_workflow_dispatch"}}</span>
|
||||||
|
<button class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
|
||||||
|
</div>
|
||||||
|
<div id="runWorkflowDispatchModal" class="ui tiny modal">
|
||||||
|
<div class="content">
|
||||||
|
<form id="runWorkflowDispatchForm" class="ui form" action="{{$.Link}}/run?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="ui inline field required tw-flex tw-items-center">
|
||||||
|
<span class="ui inline required field">
|
||||||
|
<label>{{ctx.Locale.Tr "actions.workflow.from_ref"}}:</label>
|
||||||
|
</span>
|
||||||
|
<div class="ui inline field dropdown button select-branch branch-selector-dropdown ellipsis-items-nowrap">
|
||||||
|
<input type="hidden" name="ref" value="refs/heads/{{index .Branches 0}}">
|
||||||
|
{{svg "octicon-git-branch" 14}}
|
||||||
|
<div class="default text">{{index .Branches 0}}</div>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu transition">
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-filter" 16}}</i>
|
||||||
|
<input name="search" type="text" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
|
||||||
|
</div>
|
||||||
|
<div class="branch-tag-tab">
|
||||||
|
<a class="branch-tag-item reference column muted active" href="#" data-target="#branch-list">
|
||||||
|
{{svg "octicon-git-branch" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.branches"}}
|
||||||
|
</a>
|
||||||
|
<a class="branch-tag-item reference column muted" href="#" data-target="#tag-list">
|
||||||
|
{{svg "octicon-tag" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.tags"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="branch-tag-divider"></div>
|
||||||
|
<div id="branch-list" class="scrolling menu reference-list-menu">
|
||||||
|
{{range .Branches}}
|
||||||
|
<div class="item" data-value="refs/heads/{{.}}" title="{{.}}">{{.}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
|
||||||
|
{{range .Tags}}
|
||||||
|
<div class="item" data-value="refs/tags/{{.}}" title="{{.}}">{{.}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
{{range $item := .WorkflowDispatchConfig.Inputs}}
|
||||||
|
<div class="ui field {{if .Required}}required{{end}}">
|
||||||
|
{{if eq .Type "choice"}}
|
||||||
|
<label>{{.Description}}:</label>
|
||||||
|
<select class="ui selection type dropdown" name="{{.Name}}">
|
||||||
|
{{range .Options}}
|
||||||
|
<option value="{{.}}" {{if eq $item.Default .}}selected{{end}} >{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
{{else if eq .Type "boolean"}}
|
||||||
|
<div class="ui inline checkbox">
|
||||||
|
<label>{{.Description}}</label>
|
||||||
|
<input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
{{else if eq .Type "number"}}
|
||||||
|
<label>{{.Description}}:</label>
|
||||||
|
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
|
||||||
|
{{else}}
|
||||||
|
<label>{{.Description}}:</label>
|
||||||
|
<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<button class="ui tiny primary button" type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -43,6 +43,19 @@ function reloadConfirmDraftComment() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initBranchSelectorTabs() {
|
||||||
|
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
|
||||||
|
if (!elSelectBranch) return;
|
||||||
|
|
||||||
|
$(elSelectBranch).find('.reference.column').on('click', function () {
|
||||||
|
hideElem($(elSelectBranch).find('.scrolling.reference-list-menu'));
|
||||||
|
showElem(this.getAttribute('data-target'));
|
||||||
|
queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function initRepoCommentForm() {
|
export function initRepoCommentForm() {
|
||||||
const $commentForm = $('.comment.form');
|
const $commentForm = $('.comment.form');
|
||||||
if (!$commentForm.length) return;
|
if (!$commentForm.length) return;
|
||||||
@ -81,13 +94,6 @@ export function initRepoCommentForm() {
|
|||||||
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$selectBranch.find('.reference.column').on('click', function () {
|
|
||||||
hideElem($selectBranch.find('.scrolling.reference-list-menu'));
|
|
||||||
showElem(this.getAttribute('data-target'));
|
|
||||||
queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
|
|
||||||
this.classList.add('active');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initBranchSelector();
|
initBranchSelector();
|
||||||
|
@ -60,7 +60,7 @@ import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
|
|||||||
import {initRepoBranchButton} from './features/repo-branch.ts';
|
import {initRepoBranchButton} from './features/repo-branch.ts';
|
||||||
import {initCommonOrganization} from './features/common-organization.ts';
|
import {initCommonOrganization} from './features/common-organization.ts';
|
||||||
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
||||||
import {initRepoCommentForm, initRepository} from './features/repo-legacy.ts';
|
import {initRepoCommentForm, initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
|
||||||
import {initCopyContent} from './features/copycontent.ts';
|
import {initCopyContent} from './features/copycontent.ts';
|
||||||
import {initCaptcha} from './features/captcha.ts';
|
import {initCaptcha} from './features/captcha.ts';
|
||||||
import {initRepositoryActionView} from './components/RepoActionView.vue';
|
import {initRepositoryActionView} from './components/RepoActionView.vue';
|
||||||
@ -182,6 +182,7 @@ onDomReady(() => {
|
|||||||
initRepoBranchButton,
|
initRepoBranchButton,
|
||||||
initRepoCodeView,
|
initRepoCodeView,
|
||||||
initRepoCommentForm,
|
initRepoCommentForm,
|
||||||
|
initBranchSelectorTabs,
|
||||||
initRepoEllipsisButton,
|
initRepoEllipsisButton,
|
||||||
initRepoDiffCommitBranchesAndTags,
|
initRepoDiffCommitBranchesAndTags,
|
||||||
initRepoEditor,
|
initRepoEditor,
|
||||||
|
Loading…
Reference in New Issue
Block a user