Skip to content

Commit c79f050

Browse files
authored
Merge pull request #27 from chrisgavin/input-types
Handle different workflow input types.
2 parents 1c8f8e5 + 7e6da2e commit c79f050

File tree

5 files changed

+220
-34
lines changed

5 files changed

+220
-34
lines changed

cmd/root.go

+59-13
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111

1212
"github.com/AlecAivazis/survey/v2"
1313
"github.com/chrisgavin/gh-dispatch/internal/dispatcher"
14+
"github.com/chrisgavin/gh-dispatch/internal/environment"
1415
"github.com/chrisgavin/gh-dispatch/internal/local_repository"
1516
"github.com/chrisgavin/gh-dispatch/internal/locator"
1617
"github.com/chrisgavin/gh-dispatch/internal/run"
1718
"github.com/chrisgavin/gh-dispatch/internal/version"
19+
"github.com/chrisgavin/gh-dispatch/internal/workflow"
1820
"github.com/cli/go-gh"
1921
"github.com/go-git/go-git/v5"
2022
"github.com/pkg/errors"
@@ -44,6 +46,11 @@ var rootCmd = &cobra.Command{
4446
if err != nil {
4547
return errors.Wrap(err, "Unable to open git repository.")
4648
}
49+
currentRepository, err := gh.CurrentRepository()
50+
if err != nil {
51+
return errors.Wrap(err, "Unable to determine current repository. Has it got a remote on GitHub?")
52+
}
53+
4754
remoteReference, remoteReferenceWarnings, err := local_repository.GetCurrentRemoteHead(cmd.Context(), gitRepository)
4855
if err != nil {
4956
return err
@@ -99,15 +106,15 @@ var rootCmd = &cobra.Command{
99106
return errors.New("Too many arguments.")
100107
}
101108

102-
workflow := workflows[workflowName]
109+
workflowData := workflows[workflowName]
103110

104111
inputArguments := map[string]string{}
105112
for _, input := range rootFlags.inputs {
106113
inputParts := strings.SplitN(input, "=", 2)
107114
key := inputParts[0]
108115
value := inputParts[1]
109116
inputFound := false
110-
for _, input := range workflow.Inputs {
117+
for _, input := range workflowData.Inputs {
111118
if input.Name == key {
112119
inputFound = true
113120
}
@@ -118,32 +125,71 @@ var rootCmd = &cobra.Command{
118125
inputArguments[key] = value
119126
}
120127

128+
var environmentCache []string
121129
inputQuestions := []*survey.Question{}
122130
inputAnswers := map[string]interface{}{}
123-
for _, input := range workflow.Inputs {
131+
for _, input := range workflowData.Inputs {
124132
if inputValue, ok := inputArguments[input.Name]; ok {
125133
inputAnswers[input.Name] = inputValue
126134
} else if !rootFlags.noPromptInputs {
127-
inputQuestions = append(inputQuestions, &survey.Question{
135+
question := survey.Question{
128136
Name: input.Name,
129-
Prompt: &survey.Input{
130-
Message: fmt.Sprintf("Input for %s:", input.Name),
137+
}
138+
message := fmt.Sprintf("Input for %s:", input.Name)
139+
if input.Type == workflow.StringInput {
140+
question.Prompt = &survey.Input{
141+
Message: message,
142+
Help: input.Description,
143+
}
144+
} else if input.Type == workflow.BooleanInput {
145+
question.Prompt = &survey.Confirm{
146+
Message: message,
147+
Help: input.Description,
148+
}
149+
} else if input.Type == workflow.ChoiceInput {
150+
options := input.OptionProvider()
151+
question.Prompt = &survey.Select{
152+
Message: message,
131153
Help: input.Description,
132-
},
133-
})
154+
Options: options,
155+
}
156+
} else if input.Type == workflow.EnvironmentInput {
157+
if environmentCache == nil {
158+
environmentCache, err = environment.ListEnvironments(currentRepository)
159+
if err != nil {
160+
return err
161+
}
162+
}
163+
question.Prompt = &survey.Select{
164+
Message: message,
165+
Help: input.Description,
166+
Options: environmentCache,
167+
}
168+
} else {
169+
return errors.Errorf("Unhandled input type %s. This is a bug. :(", input.Type)
170+
}
171+
inputQuestions = append(inputQuestions, &question)
134172
}
135173
}
136174
if err := survey.Ask(inputQuestions, &inputAnswers); err != nil {
137175
return errors.Wrap(err, "Unable to ask for inputs.")
138176
}
139-
140-
currentRepository, err := gh.CurrentRepository()
141-
if err != nil {
142-
return errors.Wrap(err, "Unable to determine current repository. Has it got a remote on GitHub?")
177+
workflowInputs := map[string]string{}
178+
for key, value := range inputAnswers {
179+
switch typedValue := value.(type) {
180+
case string:
181+
workflowInputs[key] = typedValue
182+
case survey.OptionAnswer:
183+
workflowInputs[key] = typedValue.Value
184+
case bool:
185+
workflowInputs[key] = strconv.FormatBool(typedValue)
186+
default:
187+
return errors.Errorf("Unhandled option answer type %T. This is a bug. :(", value)
188+
}
143189
}
144190

145191
log.Info("Dispatching workflow...")
146-
err = dispatcher.DispatchWorkflow(currentRepository, remoteReference, workflowName, inputAnswers)
192+
err = dispatcher.DispatchWorkflow(currentRepository, remoteReference, workflowName, workflowInputs)
147193
if err != nil {
148194
return err
149195
}

internal/dispatcher/dispatcher.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/pkg/errors"
1111
)
1212

13-
func DispatchWorkflow(repository repository.Repository, reference string, workflowName string, inputs map[string]interface{}) error {
13+
func DispatchWorkflow(repository repository.Repository, reference string, workflowName string, inputs map[string]string) error {
1414
client, err := client.NewClient(repository.Host())
1515
if err != nil {
1616
return err

internal/environment/environment.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package environment
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/chrisgavin/gh-dispatch/internal/client"
7+
"github.com/cli/go-gh/pkg/api"
8+
"github.com/cli/go-gh/pkg/repository"
9+
"github.com/pkg/errors"
10+
log "github.com/sirupsen/logrus"
11+
)
12+
13+
type Environment struct {
14+
Name string `json:"name"`
15+
}
16+
17+
type Environments struct {
18+
Environments []Environment `json:"environments"`
19+
}
20+
21+
func ListEnvironments(repository repository.Repository) ([]string, error) {
22+
client, err := client.NewClient(repository.Host())
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
environments := Environments{}
28+
if err := client.Get(fmt.Sprintf("repos/%s/%s/environments", repository.Owner(), repository.Name()), &environments); err != nil {
29+
if httpError, ok := err.(api.HTTPError); ok {
30+
if httpError.StatusCode == 404 {
31+
log.Warn("Got a 404 when listing environments for the repository. Unfortunately the environments API is a limited to paid organization plans.")
32+
return nil, nil
33+
}
34+
}
35+
return nil, errors.Wrap(err, "Unable to get list of environments.")
36+
}
37+
38+
names := []string{}
39+
for _, environment := range environments.Environments {
40+
names = append(names, environment.Name)
41+
}
42+
43+
return names, nil
44+
}

internal/workflow/workflow.go

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
package workflow
22

33
import (
4+
"fmt"
5+
46
"github.com/pkg/errors"
7+
log "github.com/sirupsen/logrus"
58
"gopkg.in/yaml.v2"
69
)
710

11+
type InputType string
12+
13+
const (
14+
StringInput InputType = "string"
15+
BooleanInput InputType = "boolean"
16+
ChoiceInput InputType = "choice"
17+
EnvironmentInput InputType = "environment"
18+
)
19+
20+
var inputTypesMap = map[string]InputType{
21+
string(StringInput): StringInput,
22+
string(BooleanInput): BooleanInput,
23+
string(ChoiceInput): ChoiceInput,
24+
string(EnvironmentInput): EnvironmentInput,
25+
}
26+
827
type Input struct {
9-
Name string
10-
Description string
28+
Name string
29+
Description string
30+
Type InputType
31+
OptionProvider func() []string
1132
}
1233

1334
type Workflow struct {
@@ -79,6 +100,34 @@ func ReadWorkflow(name string, rawWorkflow []byte) (*Workflow, error) {
79100
return nil, errors.Errorf("Input description for %s had unexpected type %T.", inputName, inputDescription)
80101
}
81102
}
103+
input.Type = StringInput
104+
if inputType, ok := mapInputConfiguration["type"]; ok {
105+
typedInputType, ok := inputType.(string)
106+
if !ok {
107+
return nil, errors.Errorf("Input type for %s had unexpected type %T.", inputName, inputType)
108+
}
109+
if input.Type, ok = inputTypesMap[typedInputType]; !ok {
110+
log.Warnf("Input %s has unknown type %s.", input.Name, inputType)
111+
} else {
112+
if input.Type == ChoiceInput {
113+
if inputOptions, ok := mapInputConfiguration["options"]; ok {
114+
if typedInputOptions, ok := inputOptions.([]interface{}); ok {
115+
input.OptionProvider = func() []string {
116+
choices := []string{}
117+
for _, inputOption := range typedInputOptions {
118+
choices = append(choices, fmt.Sprintf("%v", inputOption))
119+
}
120+
return choices
121+
}
122+
} else {
123+
return nil, errors.Errorf("Input options for %s had unexpected type %T.", input.Name, inputOptions)
124+
}
125+
} else {
126+
return nil, errors.Errorf("Input %s is a choice input but has no options property.", input.Name)
127+
}
128+
}
129+
}
130+
}
82131
workflow.Inputs = append(workflow.Inputs, input)
83132
}
84133
}

internal/workflow/workflow_test.go

+65-18
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,27 @@ import (
88

99
func parseTestWorkflow(t *testing.T, workflowContent string) *Workflow {
1010
const workflowName = "test.yml"
11-
workflow, err := ReadWorkflow(workflowName, []byte(workflowContent))
11+
workflowData, err := ReadWorkflow(workflowName, []byte(workflowContent))
1212
require.NoError(t, err)
13-
require.Equal(t, workflowName, workflow.Name)
14-
return workflow
13+
require.Equal(t, workflowName, workflowData.Name)
14+
return workflowData
1515
}
1616

1717
func TestReadNonDispatchableWorkflow(t *testing.T) {
1818
const workflowContent = `
1919
on: push
2020
`
21-
workflow := parseTestWorkflow(t, workflowContent)
22-
require.False(t, workflow.Dispatchable)
21+
workflowData := parseTestWorkflow(t, workflowContent)
22+
require.False(t, workflowData.Dispatchable)
2323
}
2424

2525
func TestReadDispatchableWorkflowSingletonStyle(t *testing.T) {
2626
const workflowContent = `
2727
on: workflow_dispatch
2828
`
29-
workflow := parseTestWorkflow(t, workflowContent)
30-
require.True(t, workflow.Dispatchable)
31-
require.Empty(t, workflow.Inputs)
29+
workflowData := parseTestWorkflow(t, workflowContent)
30+
require.True(t, workflowData.Dispatchable)
31+
require.Empty(t, workflowData.Inputs)
3232
}
3333

3434
func TestReadDispatchableWorkflowListStyle(t *testing.T) {
@@ -38,9 +38,9 @@ on:
3838
- pull_request
3939
- workflow_dispatch
4040
`
41-
workflow := parseTestWorkflow(t, workflowContent)
42-
require.True(t, workflow.Dispatchable)
43-
require.Empty(t, workflow.Inputs)
41+
workflowData := parseTestWorkflow(t, workflowContent)
42+
require.True(t, workflowData.Dispatchable)
43+
require.Empty(t, workflowData.Inputs)
4444
}
4545

4646
func TestReadDispatchableWorkflowMapStyle(t *testing.T) {
@@ -50,9 +50,9 @@ on:
5050
pull_request: {}
5151
workflow_dispatch: {}
5252
`
53-
workflow := parseTestWorkflow(t, workflowContent)
54-
require.True(t, workflow.Dispatchable)
55-
require.Empty(t, workflow.Inputs)
53+
workflowData := parseTestWorkflow(t, workflowContent)
54+
require.True(t, workflowData.Dispatchable)
55+
require.Empty(t, workflowData.Inputs)
5656
}
5757

5858
func TestReadWorkflowWithInputs(t *testing.T) {
@@ -64,16 +64,63 @@ on:
6464
some_input_with_description:
6565
description: "Some input description."
6666
`
67-
workflow := parseTestWorkflow(t, workflowContent)
68-
require.True(t, workflow.Dispatchable)
69-
require.Equal(t, workflow.Inputs, []Input{
67+
workflowData := parseTestWorkflow(t, workflowContent)
68+
require.True(t, workflowData.Dispatchable)
69+
require.Equal(t, []Input{
7070
{
7171
Name: "some_input",
7272
Description: "",
73+
Type: StringInput,
7374
},
7475
{
7576
Name: "some_input_with_description",
7677
Description: "Some input description.",
78+
Type: StringInput,
7779
},
78-
})
80+
}, workflowData.Inputs)
81+
}
82+
83+
func TestReadWorkflowWithBooleanInputs(t *testing.T) {
84+
const workflowContent = `
85+
on:
86+
workflow_dispatch:
87+
inputs:
88+
some_input:
89+
type: boolean
90+
`
91+
workflowData := parseTestWorkflow(t, workflowContent)
92+
require.True(t, workflowData.Dispatchable)
93+
require.Equal(t, 1, len(workflowData.Inputs))
94+
require.Equal(t, BooleanInput, workflowData.Inputs[0].Type)
95+
}
96+
97+
func TestReadWorkflowWithChoiceInputs(t *testing.T) {
98+
const workflowContent = `
99+
on:
100+
workflow_dispatch:
101+
inputs:
102+
some_input:
103+
type: choice
104+
options: [foo, bar]
105+
`
106+
workflowData := parseTestWorkflow(t, workflowContent)
107+
require.True(t, workflowData.Dispatchable)
108+
require.Equal(t, 1, len(workflowData.Inputs))
109+
require.Equal(t, ChoiceInput, workflowData.Inputs[0].Type)
110+
require.Equal(t, []string{"foo", "bar"}, workflowData.Inputs[0].OptionProvider())
111+
}
112+
113+
func TestReadWorkflowWithEnvironmentInputs(t *testing.T) {
114+
const workflowContent = `
115+
on:
116+
workflow_dispatch:
117+
inputs:
118+
some_input:
119+
type: environment
120+
`
121+
workflowData, err := ReadWorkflow("test.yml", []byte(workflowContent))
122+
require.NoError(t, err)
123+
require.True(t, workflowData.Dispatchable)
124+
require.Equal(t, 1, len(workflowData.Inputs))
125+
require.Equal(t, EnvironmentInput, workflowData.Inputs[0].Type)
79126
}

0 commit comments

Comments
 (0)