Go · 3523 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package repo
4
5 import (
6 "context"
7 "errors"
8 "net/url"
9 "os"
10 "path"
11 "sort"
12
13 "github.com/tenseleyFlow/shithub/internal/actions/trigger"
14 "github.com/tenseleyFlow/shithub/internal/actions/workflow"
15 "github.com/tenseleyFlow/shithub/internal/auth/policy"
16 repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
17 reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
18 "github.com/tenseleyFlow/shithub/internal/web/middleware"
19 )
20
21 type actionsDispatchWorkflowView struct {
22 File string
23 Name string
24 DispatchHref string
25 Inputs []actionsDispatchInputView
26 }
27
28 type actionsDispatchInputView struct {
29 Name string
30 Description string
31 Type string
32 Default string
33 Required bool
34 IsBoolean bool
35 IsChoice bool
36 Options []actionsDispatchOptionView
37 }
38
39 type actionsDispatchOptionView struct {
40 Value string
41 Selected bool
42 }
43
44 func (h *Handlers) actionsDispatchWorkflowViews(ctx context.Context, row reposdb.Repo, owner string) ([]actionsDispatchWorkflowView, error) {
45 viewer := middleware.CurrentUserFromContext(ctx)
46 if viewer.IsAnonymous() {
47 return nil, nil
48 }
49 dec := policy.Can(ctx, policy.Deps{Pool: h.d.Pool}, viewer.PolicyActor(), policy.ActionRepoWrite, policy.NewRepoRefFromRepo(row))
50 if !dec.Allow {
51 return nil, nil
52 }
53 if row.DefaultBranch == "" {
54 return nil, nil
55 }
56 gitDir, err := h.d.RepoFS.RepoPath(owner, row.Name)
57 if err != nil {
58 return nil, err
59 }
60 if _, err := os.Stat(gitDir); err != nil {
61 if errors.Is(err, os.ErrNotExist) {
62 return nil, nil
63 }
64 return nil, err
65 }
66 headSHA, err := repogit.ResolveRefOID(ctx, gitDir, row.DefaultBranch)
67 if err != nil {
68 if errors.Is(err, repogit.ErrRefNotFound) {
69 return nil, nil
70 }
71 return nil, err
72 }
73 files, _, err := trigger.Discover(ctx, gitDir, headSHA)
74 if err != nil {
75 return nil, err
76 }
77
78 views := make([]actionsDispatchWorkflowView, 0, len(files))
79 for _, file := range files {
80 wf, diags, err := workflow.Parse(file.Bytes)
81 if err != nil || workflowHasErrorDiagnostics(diags) || wf.On.WorkflowDispatch == nil {
82 continue
83 }
84 views = append(views, actionsDispatchWorkflowView{
85 File: file.Path,
86 Name: workflowDisplayName(wf.Name, file.Path),
87 DispatchHref: "/" + owner + "/" + row.Name + "/actions/workflows/" + url.PathEscape(path.Base(file.Path)) + "/dispatches",
88 Inputs: actionsDispatchInputViews(wf.On.WorkflowDispatch.Inputs),
89 })
90 }
91 sort.SliceStable(views, func(i, j int) bool {
92 if views[i].Name == views[j].Name {
93 return views[i].File < views[j].File
94 }
95 return views[i].Name < views[j].Name
96 })
97 return views, nil
98 }
99
100 func workflowHasErrorDiagnostics(diags []workflow.Diagnostic) bool {
101 for _, d := range diags {
102 if d.Severity == workflow.Error {
103 return true
104 }
105 }
106 return false
107 }
108
109 func actionsDispatchInputViews(inputs []workflow.DispatchInput) []actionsDispatchInputView {
110 views := make([]actionsDispatchInputView, 0, len(inputs))
111 for _, input := range inputs {
112 view := actionsDispatchInputView{
113 Name: input.Name,
114 Description: input.Description,
115 Type: input.Type,
116 Default: input.Default,
117 Required: input.Required,
118 IsBoolean: input.Type == "boolean",
119 IsChoice: input.Type == "choice",
120 }
121 for _, option := range input.Options {
122 view.Options = append(view.Options, actionsDispatchOptionView{
123 Value: option,
124 Selected: input.Default == option,
125 })
126 }
127 views = append(views, view)
128 }
129 return views
130 }
131