Skip to content

Commit

Permalink
Handle different workflow input types.
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisgavin committed Apr 10, 2022
1 parent 1c8f8e5 commit 7e6da2e
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 34 deletions.
72 changes: 59 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (

"github.com/AlecAivazis/survey/v2"
"github.com/chrisgavin/gh-dispatch/internal/dispatcher"
"github.com/chrisgavin/gh-dispatch/internal/environment"
"github.com/chrisgavin/gh-dispatch/internal/local_repository"
"github.com/chrisgavin/gh-dispatch/internal/locator"
"github.com/chrisgavin/gh-dispatch/internal/run"
"github.com/chrisgavin/gh-dispatch/internal/version"
"github.com/chrisgavin/gh-dispatch/internal/workflow"
"github.com/cli/go-gh"
"github.com/go-git/go-git/v5"
"github.com/pkg/errors"
Expand Down Expand Up @@ -44,6 +46,11 @@ var rootCmd = &cobra.Command{
if err != nil {
return errors.Wrap(err, "Unable to open git repository.")
}
currentRepository, err := gh.CurrentRepository()
if err != nil {
return errors.Wrap(err, "Unable to determine current repository. Has it got a remote on GitHub?")
}

remoteReference, remoteReferenceWarnings, err := local_repository.GetCurrentRemoteHead(cmd.Context(), gitRepository)
if err != nil {
return err
Expand Down Expand Up @@ -99,15 +106,15 @@ var rootCmd = &cobra.Command{
return errors.New("Too many arguments.")
}

workflow := workflows[workflowName]
workflowData := workflows[workflowName]

inputArguments := map[string]string{}
for _, input := range rootFlags.inputs {
inputParts := strings.SplitN(input, "=", 2)
key := inputParts[0]
value := inputParts[1]
inputFound := false
for _, input := range workflow.Inputs {
for _, input := range workflowData.Inputs {
if input.Name == key {
inputFound = true
}
Expand All @@ -118,32 +125,71 @@ var rootCmd = &cobra.Command{
inputArguments[key] = value
}

var environmentCache []string
inputQuestions := []*survey.Question{}
inputAnswers := map[string]interface{}{}
for _, input := range workflow.Inputs {
for _, input := range workflowData.Inputs {
if inputValue, ok := inputArguments[input.Name]; ok {
inputAnswers[input.Name] = inputValue
} else if !rootFlags.noPromptInputs {
inputQuestions = append(inputQuestions, &survey.Question{
question := survey.Question{
Name: input.Name,
Prompt: &survey.Input{
Message: fmt.Sprintf("Input for %s:", input.Name),
}
message := fmt.Sprintf("Input for %s:", input.Name)
if input.Type == workflow.StringInput {
question.Prompt = &survey.Input{
Message: message,
Help: input.Description,
}
} else if input.Type == workflow.BooleanInput {
question.Prompt = &survey.Confirm{
Message: message,
Help: input.Description,
}
} else if input.Type == workflow.ChoiceInput {
options := input.OptionProvider()
question.Prompt = &survey.Select{
Message: message,
Help: input.Description,
},
})
Options: options,
}
} else if input.Type == workflow.EnvironmentInput {
if environmentCache == nil {
environmentCache, err = environment.ListEnvironments(currentRepository)
if err != nil {
return err
}
}
question.Prompt = &survey.Select{
Message: message,
Help: input.Description,
Options: environmentCache,
}
} else {
return errors.Errorf("Unhandled input type %s. This is a bug. :(", input.Type)
}
inputQuestions = append(inputQuestions, &question)
}
}
if err := survey.Ask(inputQuestions, &inputAnswers); err != nil {
return errors.Wrap(err, "Unable to ask for inputs.")
}

currentRepository, err := gh.CurrentRepository()
if err != nil {
return errors.Wrap(err, "Unable to determine current repository. Has it got a remote on GitHub?")
workflowInputs := map[string]string{}
for key, value := range inputAnswers {
switch typedValue := value.(type) {
case string:
workflowInputs[key] = typedValue
case survey.OptionAnswer:
workflowInputs[key] = typedValue.Value
case bool:
workflowInputs[key] = strconv.FormatBool(typedValue)
default:
return errors.Errorf("Unhandled option answer type %T. This is a bug. :(", value)
}
}

log.Info("Dispatching workflow...")
err = dispatcher.DispatchWorkflow(currentRepository, remoteReference, workflowName, inputAnswers)
err = dispatcher.DispatchWorkflow(currentRepository, remoteReference, workflowName, workflowInputs)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/dispatcher/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/pkg/errors"
)

func DispatchWorkflow(repository repository.Repository, reference string, workflowName string, inputs map[string]interface{}) error {
func DispatchWorkflow(repository repository.Repository, reference string, workflowName string, inputs map[string]string) error {
client, err := client.NewClient(repository.Host())
if err != nil {
return err
Expand Down
44 changes: 44 additions & 0 deletions internal/environment/environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package environment

import (
"fmt"

"github.com/chrisgavin/gh-dispatch/internal/client"
"github.com/cli/go-gh/pkg/api"
"github.com/cli/go-gh/pkg/repository"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

type Environment struct {
Name string `json:"name"`
}

type Environments struct {
Environments []Environment `json:"environments"`
}

func ListEnvironments(repository repository.Repository) ([]string, error) {
client, err := client.NewClient(repository.Host())
if err != nil {
return nil, err
}

environments := Environments{}
if err := client.Get(fmt.Sprintf("repos/%s/%s/environments", repository.Owner(), repository.Name()), &environments); err != nil {
if httpError, ok := err.(api.HTTPError); ok {
if httpError.StatusCode == 404 {
log.Warn("Got a 404 when listing environments for the repository. Unfortunately the environments API is a limited to paid organization plans.")
return nil, nil
}
}
return nil, errors.Wrap(err, "Unable to get list of environments.")
}

names := []string{}
for _, environment := range environments.Environments {
names = append(names, environment.Name)
}

return names, nil
}
53 changes: 51 additions & 2 deletions internal/workflow/workflow.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
package workflow

import (
"fmt"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)

type InputType string

const (
StringInput InputType = "string"
BooleanInput InputType = "boolean"
ChoiceInput InputType = "choice"
EnvironmentInput InputType = "environment"
)

var inputTypesMap = map[string]InputType{
string(StringInput): StringInput,
string(BooleanInput): BooleanInput,
string(ChoiceInput): ChoiceInput,
string(EnvironmentInput): EnvironmentInput,
}

type Input struct {
Name string
Description string
Name string
Description string
Type InputType
OptionProvider func() []string
}

type Workflow struct {
Expand Down Expand Up @@ -79,6 +100,34 @@ func ReadWorkflow(name string, rawWorkflow []byte) (*Workflow, error) {
return nil, errors.Errorf("Input description for %s had unexpected type %T.", inputName, inputDescription)
}
}
input.Type = StringInput
if inputType, ok := mapInputConfiguration["type"]; ok {
typedInputType, ok := inputType.(string)
if !ok {
return nil, errors.Errorf("Input type for %s had unexpected type %T.", inputName, inputType)
}
if input.Type, ok = inputTypesMap[typedInputType]; !ok {
log.Warnf("Input %s has unknown type %s.", input.Name, inputType)
} else {
if input.Type == ChoiceInput {
if inputOptions, ok := mapInputConfiguration["options"]; ok {
if typedInputOptions, ok := inputOptions.([]interface{}); ok {
input.OptionProvider = func() []string {
choices := []string{}
for _, inputOption := range typedInputOptions {
choices = append(choices, fmt.Sprintf("%v", inputOption))
}
return choices
}
} else {
return nil, errors.Errorf("Input options for %s had unexpected type %T.", input.Name, inputOptions)
}
} else {
return nil, errors.Errorf("Input %s is a choice input but has no options property.", input.Name)
}
}
}
}
workflow.Inputs = append(workflow.Inputs, input)
}
}
Expand Down
83 changes: 65 additions & 18 deletions internal/workflow/workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ import (

func parseTestWorkflow(t *testing.T, workflowContent string) *Workflow {
const workflowName = "test.yml"
workflow, err := ReadWorkflow(workflowName, []byte(workflowContent))
workflowData, err := ReadWorkflow(workflowName, []byte(workflowContent))
require.NoError(t, err)
require.Equal(t, workflowName, workflow.Name)
return workflow
require.Equal(t, workflowName, workflowData.Name)
return workflowData
}

func TestReadNonDispatchableWorkflow(t *testing.T) {
const workflowContent = `
on: push
`
workflow := parseTestWorkflow(t, workflowContent)
require.False(t, workflow.Dispatchable)
workflowData := parseTestWorkflow(t, workflowContent)
require.False(t, workflowData.Dispatchable)
}

func TestReadDispatchableWorkflowSingletonStyle(t *testing.T) {
const workflowContent = `
on: workflow_dispatch
`
workflow := parseTestWorkflow(t, workflowContent)
require.True(t, workflow.Dispatchable)
require.Empty(t, workflow.Inputs)
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Empty(t, workflowData.Inputs)
}

func TestReadDispatchableWorkflowListStyle(t *testing.T) {
Expand All @@ -38,9 +38,9 @@ on:
- pull_request
- workflow_dispatch
`
workflow := parseTestWorkflow(t, workflowContent)
require.True(t, workflow.Dispatchable)
require.Empty(t, workflow.Inputs)
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Empty(t, workflowData.Inputs)
}

func TestReadDispatchableWorkflowMapStyle(t *testing.T) {
Expand All @@ -50,9 +50,9 @@ on:
pull_request: {}
workflow_dispatch: {}
`
workflow := parseTestWorkflow(t, workflowContent)
require.True(t, workflow.Dispatchable)
require.Empty(t, workflow.Inputs)
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Empty(t, workflowData.Inputs)
}

func TestReadWorkflowWithInputs(t *testing.T) {
Expand All @@ -64,16 +64,63 @@ on:
some_input_with_description:
description: "Some input description."
`
workflow := parseTestWorkflow(t, workflowContent)
require.True(t, workflow.Dispatchable)
require.Equal(t, workflow.Inputs, []Input{
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Equal(t, []Input{
{
Name: "some_input",
Description: "",
Type: StringInput,
},
{
Name: "some_input_with_description",
Description: "Some input description.",
Type: StringInput,
},
})
}, workflowData.Inputs)
}

func TestReadWorkflowWithBooleanInputs(t *testing.T) {
const workflowContent = `
on:
workflow_dispatch:
inputs:
some_input:
type: boolean
`
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Equal(t, 1, len(workflowData.Inputs))
require.Equal(t, BooleanInput, workflowData.Inputs[0].Type)
}

func TestReadWorkflowWithChoiceInputs(t *testing.T) {
const workflowContent = `
on:
workflow_dispatch:
inputs:
some_input:
type: choice
options: [foo, bar]
`
workflowData := parseTestWorkflow(t, workflowContent)
require.True(t, workflowData.Dispatchable)
require.Equal(t, 1, len(workflowData.Inputs))
require.Equal(t, ChoiceInput, workflowData.Inputs[0].Type)
require.Equal(t, []string{"foo", "bar"}, workflowData.Inputs[0].OptionProvider())
}

func TestReadWorkflowWithEnvironmentInputs(t *testing.T) {
const workflowContent = `
on:
workflow_dispatch:
inputs:
some_input:
type: environment
`
workflowData, err := ReadWorkflow("test.yml", []byte(workflowContent))
require.NoError(t, err)
require.True(t, workflowData.Dispatchable)
require.Equal(t, 1, len(workflowData.Inputs))
require.Equal(t, EnvironmentInput, workflowData.Inputs[0].Type)
}

0 comments on commit 7e6da2e

Please sign in to comment.