cmd/ci/ci.go (120 lines of code) (raw):

package ci import ( ctx "context" "fmt" "os" "slices" "strings" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "gitlab.com/gitlab-org/step-runner/pkg/di" "gitlab.com/gitlab-org/step-runner/pkg/report" "gitlab.com/gitlab-org/step-runner/pkg/runner" "gitlab.com/gitlab-org/step-runner/proto" "gitlab.com/gitlab-org/step-runner/schema/v1" ) type Options struct { WriteStepResultsFile bool Steps string WorkDir string JobVariables map[string]string } func NewCmd() *cobra.Command { options := &Options{ Steps: os.Getenv("STEPS"), WorkDir: findWorkDir(), JobVariables: findJobVariables(), } cmd := &cobra.Command{ Use: "ci", Short: "Run steps in a CI environment variable STEPS", RunE: func(cmd *cobra.Command, args []string) error { return run(options) }, } argUsage := "write step-results.json file, note this file may contain secrets" cmd.Flags().BoolVar(&options.WriteStepResultsFile, "write-steps-results", runner.RunningInDebugMode, argUsage) return cmd } func run(options *Options) error { fmt.Println("***** WARNING *****") fmt.Println("As of GitLab Runner 17.11, the legacy step-runner image registry.gitlab.com/gitlab-org/step-runner:v0" + " is replaced by the image defined by the job.") fmt.Println("The legacy step-runner image will be supported until GitLab 18.0.") fmt.Println("See https://docs.gitlab.com/ci/steps/ for information on how to configure CI/CD Steps.") fmt.Println("***** END WARNING *****") def, err := wrapStepsInSpecDef(options.Steps) if err != nil { return fmt.Errorf("reading STEPS %q: %w", options.Steps, err) } protoDef, err := def.Compile() if err != nil { return fmt.Errorf("compiling STEPS: %w", err) } spec := &proto.Spec{Spec: &proto.Spec_Content{}} specDef := runner.NewSpecDefinition(spec, protoDef, "") diContainer := di.NewContainer() osEnv, err := runner.NewEnvironmentFromOSWithKnownVars() if err != nil { return err } globalEnv, err := runner.GlobalEnvironment(osEnv, options.JobVariables) if err != nil { return err } // Step runner should have no concept of "CI_BUILDS_DIR". // However entire `ci` command is a workaround hack because // steps are not yet plumbed through runner. Once we receive // steps from runner over gRPC we will receive "work_dir" // explicitly (set to CI_BUILDS_DIR by runner). Then we can // delete this whole command. // Add all CI_, GITLAB_ and DOCKER_ environment variables as a // workaround until we get an explicit list in the Run gRPC // call. globalCtx := runner.NewGlobalContext(options.WorkDir, options.JobVariables, globalEnv, os.Stdout, os.Stderr) params := &runner.Params{} stepParser, err := diContainer.StepParser() if err != nil { return err } step, err := stepParser.Parse(globalCtx, specDef, params, runner.StepDefinedInGitLabJob) if err != nil { return fmt.Errorf("failed to run steps: %w", err) } env := globalCtx.EnvWithLexicalScope(params.Env) inputs := params.NewInputsWithDefault(specDef.SpecInputs()) stepsCtx, err := runner.NewStepsContext(globalCtx, specDef.Dir(), inputs, env) if err != nil { return fmt.Errorf("creating steps context: %w", err) } defer stepsCtx.Cleanup() result, err := step.Run(ctx.Background(), stepsCtx) if options.WriteStepResultsFile { reptErr := report.NewStepResultReport("", report.FormatJSON).Write(result) if reptErr != nil { fmt.Println(reptErr) } } if err != nil { return fmt.Errorf("failed to run steps: %w", err) } return nil } func wrapStepsInSpecDef(steps string) (*schema.Step, error) { def := &schema.Step{} err := yaml.Unmarshal([]byte(steps), &def.Run) if err != nil { return nil, fmt.Errorf("unmarshalling steps: %w", err) } return def, nil } func findWorkDir() string { workDir := os.Getenv("CI_PROJECT_DIR") if workDir == "" { workDir, _ = os.Getwd() } return workDir } func findJobVariables() map[string]string { variables := map[string]string{} prefixes := []string{"CI_", "GITLAB_", "DOCKER_"} for _, e := range os.Environ() { k, v, ok := strings.Cut(e, "=") if !ok || !slices.ContainsFunc(prefixes, func(prefix string) bool { return strings.HasPrefix(k, prefix) }) { continue } variables[k] = v } return variables }