mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01: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  ```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 }} ```   --------- 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:
		| @@ -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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user