schema/v1/step.go (226 lines of code) (raw):

package schema import ( "encoding/json" "fmt" "google.golang.org/protobuf/types/known/structpb" "gopkg.in/yaml.v3" "gitlab.com/gitlab-org/step-runner/proto" ) var ( _ yaml.Unmarshaler = &Step{} _ json.Unmarshaler = &Step{} ) // Inputs is a map of step input names to structured values. type StepInputs map[string]interface{} // Outputs are the output values for a sequence. They can reference the outputs of // sub-steps. type StepOutputs map[string]interface{} // Step is a unit of execution. type Step struct { // Action is a GitHub action to run. Action *string `json:"action,omitempty" yaml:"action,omitempty" mapstructure:"action,omitempty"` // Delegate selects a step by name which will produce the outputs a run. Delegate *string `json:"delegate,omitempty" yaml:"delegate,omitempty" mapstructure:"delegate,omitempty"` // Env is a map of environment variable names to string values. Env map[string]string `json:"env,omitempty" yaml:"env,omitempty" mapstructure:"env,omitempty"` // Exec is a command to run. Exec *Exec `json:"exec,omitempty" yaml:"exec,omitempty" mapstructure:"exec,omitempty"` // Inputs is a map of step input names to structured values. Inputs StepInputs `json:"inputs,omitempty" yaml:"inputs,omitempty" mapstructure:"inputs,omitempty"` // Name is a unique identifier for this step. Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` // Outputs are the output values for a sequence. They can reference the outputs of // sub-steps. Outputs StepOutputs `json:"outputs,omitempty" yaml:"outputs,omitempty" mapstructure:"outputs,omitempty"` // Script is a shell script to evaluate. Script *string `json:"script,omitempty" yaml:"script,omitempty" mapstructure:"script,omitempty"` // Step is a reference to another step to invoke. Step any `json:"step,omitempty" yaml:"step,omitempty" mapstructure:"step,omitempty"` // Run is a list of sub-steps to run. Run []Step `json:"run,omitempty" yaml:"run,omitempty" mapstructure:"run,omitempty"` } func (s *Step) UnmarshalYAML(value *yaml.Node) error { type Default Step d := (*Default)(s) err := value.Decode(d) if err != nil { return err } return s.unmarshalStep() } func (s *Step) UnmarshalJSON(data []byte) error { type Default Step d := (*Default)(s) err := json.Unmarshal(data, d) if err != nil { return err } return s.unmarshalStep() } func (s *Step) unmarshalStep() error { if s.Step == nil { return nil } switch v := s.Step.(type) { case string: return nil case map[string]any: data, err := json.Marshal(v) if err != nil { return fmt.Errorf("reifying step: %w", err) } ref := &Reference{} err = json.Unmarshal(data, ref) if err != nil { return fmt.Errorf("reifying step: %w", err) } s.Step = ref return nil default: return fmt.Errorf("unsupported type: %T", v) } } func (s *Step) Compile() (*proto.Definition, error) { err := s.verifyOneTypeProvided() if err != nil { return nil, err } return s.compileToDefinitionProto() } func (s *Step) verifyOneTypeProvided() error { have := 0 if s.Exec != nil { // Exec type step have++ } if s.Run != nil { // Run type step have++ } if have == 0 { return fmt.Errorf("at least one of `script, `action`, `run` or `exec` must be provided") } if have > 1 { return fmt.Errorf("only one of `script`, `action`, `run` or `exec` may be provided. have %v", have) } return nil } func (s *Step) compileToDefinitionProto() (*proto.Definition, error) { protoDef := &proto.Definition{} switch { case s.Exec != nil: protoDef.Type = proto.DefinitionType_exec protoDef.Exec = &proto.Definition_Exec{ Command: s.Exec.Command, } if s.Exec.WorkDir != nil { protoDef.Exec.WorkDir = *s.Exec.WorkDir } case s.Run != nil: protoDef.Type = proto.DefinitionType_steps protoDef.Steps = make([]*proto.Step, len(s.Run)) for i, ss := range s.Run { protoStep, err := (&ss).CompileStep() if err != nil { return nil, fmt.Errorf("compiling run[%v]: %v: %w", i, s.Name, err) } protoDef.Steps[i] = protoStep } protoDef.Outputs = map[string]*structpb.Value{} for k, v := range s.Outputs { protoV, err := (&valueCompiler{v}).compile() if err != nil { return nil, fmt.Errorf("compiling output[%q]: %v: %w", k, v, err) } protoDef.Outputs[k] = protoV } default: return nil, fmt.Errorf("could not determine step type") } protoDef.Env = s.Env if s.Delegate != nil { protoDef.Delegate = *s.Delegate } return protoDef, nil } func (s *Step) CompileStep() (*proto.Step, error) { err := s.compileScriptKeywordToStep() if err != nil { return nil, err } err = s.compileActionKeywordToStep() if err != nil { return nil, err } return s.compileToStepProto() } func (s *Step) compileScriptKeywordToStep() error { if s.Script == nil || *s.Script == "" { return nil } if s.Step != nil { return fmt.Errorf("the `script` keyword cannot be used with the `step` keyword") } if s.Action != nil && *s.Action != "" { return fmt.Errorf("the `script` keyword cannot be used with the `action` keyword") } if len(s.Inputs) != 0 { return fmt.Errorf("the `script` keyword cannot be used with `inputs`") } s.Step = &proto.Step_Reference{ Protocol: proto.StepReferenceProtocol_dist, Path: []string{"script"}, Filename: "step.yml", } s.Inputs = map[string]any{ "script": s.Script, } s.Script = nil return nil } func (s *Step) compileActionKeywordToStep() error { if s.Action == nil || *s.Action == "" { return nil } if s.Step != nil { return fmt.Errorf("the `action` keyword cannot be used with the `step` keyword") } if s.Script != nil && *s.Script != "" { return fmt.Errorf("the `action` keyword cannot be used with the `script` keyword") } s.Step = &Reference{Git: NewGitReference("https://gitlab.com/components/action-runner", "main")} s.Inputs = map[string]any{ "action": s.Action, "inputs": s.Inputs, } s.Action = nil return nil } func (s *Step) compileToStepProto() (*proto.Step, error) { protoInputs := map[string]*structpb.Value{} for k, v := range (map[string]any)(s.Inputs) { protoValue, err := (&valueCompiler{v}).compile() if err != nil { return nil, err } protoInputs[k] = protoValue } name := "" if s.Name != nil { name = *s.Name } var ( ref *proto.Step_Reference err error ) switch v := s.Step.(type) { case *proto.Step_Reference: ref = v case string: ref, err = shortReference(v).compile() case *Reference: ref, err = v.compile(name, protoInputs, s.Env) default: err = fmt.Errorf("unsupported type: %T", v) } if err != nil { return nil, fmt.Errorf("compiling reference: %w", err) } if ref.Protocol == proto.StepReferenceProtocol_spec_def { // Inputs are not returned in the StepReference because there is no guarantee they will match those defined by the SpecDef // Each step has inputs when executed, so the step is free to inline these inputs into the returned SpecDef return &proto.Step{ Name: name, Step: ref, Env: s.Env, Inputs: nil, }, nil } return &proto.Step{ Name: name, Step: ref, Env: s.Env, Inputs: protoInputs, }, nil }