gitfoss | 9cec23485bd304126f7b1dd2f0d9398e90fc96b2 | packages/gitfoss-ci-runner/src/gitfoss-ci-runner.cr ∙ GitFOSS
.cr
Crystal
(text/x-crystal)
require "option_parser"

require "./project"
require "./pipeline"
require "./stage"
require "./command"

require "./logger"
require "./utils"

module Gitfoss::Ci::Runner
  # include Gitfoss::Ci::Runner::Logger

  VERSION = "0.1.0"
  DOCKER_API_VERSION = "v1.47" # docker 27.3.1

  def self.run()
    # Entry point
    options = {} of Symbol => String

    OptionParser.parse do |parser|
      parser.banner = "Usage: run_pipeline.cr [options] pipeline.yml"
      parser.on("-d", "--docker-host HOST", "Docker host (optional)") do |h|
        options[:docker_host] = h
      end
      parser.on("-f", "--file FILE", "Path to the YAML file") do |f|
        options[:yaml_file] = f
      end
      parser.on("-h", "--help", "Show this help") do
        puts parser
        exit 0
      end
      parser.invalid_option do |flag|
        on_failed_pipeline
        err_pipe "ERROR: #{flag} is not a valid option."
        err_pipe parser.to_s
        exit 1
      end
    end

    yaml_path = options[:yaml_file]? || ARGV[0]?
    unless yaml_path
      on_failed_pipeline
      err_pipe "No YAML file specified"
      exit 1
    end

    unless File.exists?(yaml_path.not_nil!)
      on_failed_pipeline
      err_pipe "File not found: #{yaml_path}"
      exit 1
    end

    yaml = load_yaml(yaml_path)

    # Instantiate Docker client (respect optional host)
    docker =
      if options[:docker_host]?
        # puts "Docker base url: #{options[:docker_host]}\n"
        Docker::Client.new(options[:docker_host], DOCKER_API_VERSION)
      else
        Docker::Client.new(nil, DOCKER_API_VERSION)
      end

    project = Project.new(orgSlug: "ethicdevs", repoSlug: "gitfoss")
    pipeline = Pipeline.new(id: "pipeline-1")

    # YAML expected top-level "stages" array
    stages_node = yaml["stages"]
    unless stages_node
      on_failed_pipeline
      err_pipe "YAML missing 'stages' top-level key"
      exit 1
    end

    # Iterate stages in order
    stages_node.as_a.each do |stage_node|
      stage_name = stage_node["name"].as_s
      log_pipe "Running stage: #{stage_name}"
      stage = Stage.new(stage_name)

      # The YAML could describe a single command per stage or multiple; support both.
      # If stage has "commands" at top-level, map stage->Command. Otherwise if it contains
      # a "steps" array with multiple commands, create one Command per step.
      if stage_node["steps"]?
        stage_node["steps"].as_a.each do |step_node|
          cmd_node =
            if step_node.is_a?(YAML::Any) && step_node["image"]
              step_node
            else
              # inherit common stage-level fields if step is a plain command string
              YAML.parse({
                "image" => stage_node["image"],
                "workdir" => stage_node["workdir"]? || "",
                "shell" => stage_node["shell"]? || "sh",
                "env" => stage_node["env"]? || {} of String => String,
                "commands" => [step_node]
              }.to_yaml)
            end

          begin
            command_name = "#{project.to_s}/#{pipeline.id}/stage_#{stage_name}_#{Time.utc.to_unix}".gsub('/', '_')
            command = map_to_command(command_name, cmd_node)
            exit_code = command.run(docker, project, pipeline, stage)
            if exit_code != 0
              on_failed_pipeline
            else
              on_passed_pipeline
            end
          rescue ex
            on_failed_pipeline
            err_pipe "Stage #{stage_name} step failed", ex
            exit 1
          end
        end
      else
        # single-command-per-stage case: map entire stage node to one Command
        begin
          command_name = "#{project.to_s}/#{pipeline.id}/stage_#{stage_name}_#{Time.utc.to_unix}".gsub('/', '_')
          command = map_to_command(command_name, stage_node)
          exit_code = command.run(docker, project, pipeline, stage)
          if exit_code != 0
            on_failed_pipeline
          else
            on_passed_pipeline
          end
        rescue ex
          on_failed_pipeline
          err_pipe "Stage #{stage_name} failed", ex
          exit 1
        end
      end
      puts "\n"
    end
  end
end


Gitfoss::Ci::Runner.run

# Example usage (commented out):
# docker = Docker.client
# pipeline = Pipeline.new
# stage = Stage.new("build")
# cmd = Command.new("alpine", {"FOO" => "$HOME"}, ["echo hello"], [], "", ".", {"out" => "/tmp/out"}, {"in" => "/tmp/in"})
# cmd.run(docker, pipeline, stage)