github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/travis/convert.go (about)

     1  // Copyright 2022 Harness, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package travis converts Travis pipelines to Harness pipelines.
    16  package travis
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"strings"
    24  
    25  	travis "github.com/drone/go-convert/convert/travis/yaml"
    26  	harness "github.com/drone/spec/dist/go"
    27  
    28  	"github.com/drone/go-convert/internal/store"
    29  	"github.com/ghodss/yaml"
    30  )
    31  
    32  // as we walk the yaml, we store a
    33  // a snapshot of the current node and
    34  // its parents.
    35  type context struct {
    36  	config *travis.Pipeline
    37  }
    38  
    39  // Converter converts a Travis pipeline to a Harness
    40  // v1 pipeline.
    41  type Converter struct {
    42  	kubeEnabled   bool
    43  	kubeNamespace string
    44  	kubeConnector string
    45  	dockerhubConn string
    46  	identifiers   *store.Identifiers
    47  }
    48  
    49  // New creates a new Converter that converts a Travis
    50  // pipeline to a Harness v1 pipeline.
    51  func New(options ...Option) *Converter {
    52  	d := new(Converter)
    53  
    54  	// create the unique identifier store. this store
    55  	// is used for registering unique identifiers to
    56  	// prevent duplicate names, unique index violations.
    57  	d.identifiers = store.New()
    58  
    59  	// loop through and apply the options.
    60  	for _, option := range options {
    61  		option(d)
    62  	}
    63  
    64  	// set the default kubernetes namespace.
    65  	if d.kubeNamespace == "" {
    66  		d.kubeNamespace = "default"
    67  	}
    68  
    69  	// set the runtime to kubernetes if the kubernetes
    70  	// connector is configured.
    71  	if d.kubeConnector != "" {
    72  		d.kubeEnabled = true
    73  	}
    74  
    75  	return d
    76  }
    77  
    78  // Convert downgrades a v1 pipeline.
    79  func (d *Converter) Convert(r io.Reader) ([]byte, error) {
    80  	config, err := travis.Parse(r)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	return d.convert(&context{
    85  		config: config,
    86  	})
    87  }
    88  
    89  // ConvertString downgrades a v1 pipeline.
    90  func (d *Converter) ConvertBytes(b []byte) ([]byte, error) {
    91  	return d.Convert(
    92  		bytes.NewBuffer(b),
    93  	)
    94  }
    95  
    96  // ConvertString downgrades a v1 pipeline.
    97  func (d *Converter) ConvertString(s string) ([]byte, error) {
    98  	return d.Convert(
    99  		bytes.NewBufferString(s),
   100  	)
   101  }
   102  
   103  // ConvertFile downgrades a v1 pipeline.
   104  func (d *Converter) ConvertFile(p string) ([]byte, error) {
   105  	f, err := os.Open(p)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	defer f.Close()
   110  	return d.Convert(f)
   111  }
   112  
   113  // converts converts a Travis pipeline to a Harness pipeline.
   114  func (d *Converter) convert(ctx *context) ([]byte, error) {
   115  
   116  	// create the harness pipeline spec
   117  	pipeline := &harness.Pipeline{}
   118  
   119  	// create the harness pipeline resource
   120  	config := &harness.Config{
   121  		Version: 1,
   122  		Kind:    "pipeline",
   123  		Spec:    pipeline,
   124  	}
   125  
   126  	// convert the clone
   127  	if v := convertGit(ctx); v != nil {
   128  		pipeline.Options = new(harness.Default)
   129  		pipeline.Options.Clone = v
   130  	}
   131  
   132  	// conver pipeilne stages
   133  	pipeline.Stages = append(pipeline.Stages, &harness.Stage{
   134  		Name:     "pipeline",
   135  		Desc:     "converted from travis.yml",
   136  		Type:     "ci",
   137  		Delegate: nil, // No Travis equivalent
   138  		Failure:  nil, // No Travis equivalent
   139  		Strategy: convertStrategy(ctx),
   140  		When:     nil, // TODO convert travis condition (if, branches)
   141  		Spec: &harness.StageCI{
   142  			Cache: convertCache(ctx),
   143  			// TODO support for other env variabes, like TRAVIS_RETHINKDB_VERSION
   144  			Envs:     createMatrixEnvs(ctx),
   145  			Platform: convertPlatform(ctx),
   146  			Runtime:  nil, // TODO convert runtime
   147  			Steps:    d.convertSteps(ctx),
   148  		},
   149  	})
   150  
   151  	// marshal the harness yaml
   152  	out, err := yaml.Marshal(config)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	return out, nil
   158  }
   159  
   160  func (d *Converter) convertSteps(ctx *context) []*harness.Step {
   161  	var steps []*harness.Step
   162  
   163  	// convert addon steps
   164  	steps = append(steps, d.convertAddons(ctx)...)
   165  
   166  	// convert services to background steps
   167  	steps = append(steps, d.convertServices(ctx)...)
   168  
   169  	// from the job lifecycle documentation
   170  	// https://docs.travis-ci.com/user/job-lifecycle/#the-job-lifecycle
   171  	for _, script := range ctx.config.BeforeInstall {
   172  		steps = append(steps, d.convertStep(ctx, "before_install", script))
   173  	}
   174  	for _, script := range ctx.config.Install {
   175  		steps = append(steps, d.convertStep(ctx, "install", script))
   176  	}
   177  	if len(ctx.config.Install) == 0 {
   178  		// when no install is defined, travis may automatically
   179  		// provide the install based on langauge.
   180  		if script, ok := defaultInstall[strings.ToLower(ctx.config.Language)]; ok {
   181  			steps = append(steps, d.convertStep(ctx, "install", script))
   182  		}
   183  	}
   184  	for _, script := range ctx.config.BeforeScript {
   185  		steps = append(steps, d.convertStep(ctx, "before_script", script))
   186  	}
   187  	for _, script := range ctx.config.Script {
   188  		steps = append(steps, d.convertStep(ctx, "script", script))
   189  	}
   190  	if len(ctx.config.Script) == 0 {
   191  		// when no script is defined, travis may automatically
   192  		// provide the script based on langauge.
   193  		if script, ok := defaultScript[strings.ToLower(ctx.config.Language)]; ok {
   194  			steps = append(steps, d.convertStep(ctx, "script", script))
   195  		}
   196  	}
   197  	for _, script := range ctx.config.BeforeCache {
   198  		steps = append(steps, d.convertStep(ctx, "before_cache", script))
   199  	}
   200  	for _, script := range ctx.config.AfterSuccess {
   201  		steps = append(steps, d.convertStep(ctx, "after_success", script))
   202  	}
   203  	for _, script := range ctx.config.AfterFailure {
   204  		steps = append(steps, d.convertStep(ctx, "after_failure", script))
   205  	}
   206  	for _, script := range ctx.config.BeforeDeploy {
   207  		steps = append(steps, d.convertStep(ctx, "before_deploy", script))
   208  	}
   209  	//
   210  	// TODO support deploy steps
   211  	//
   212  	for _, script := range ctx.config.AfterDeploy {
   213  		steps = append(steps, d.convertStep(ctx, "after_deploy", script))
   214  	}
   215  	for _, script := range ctx.config.AfterScript {
   216  		steps = append(steps, d.convertStep(ctx, "after_script", script))
   217  	}
   218  	return steps
   219  }
   220  
   221  func (d *Converter) convertStep(ctx *context, section, command string) *harness.Step {
   222  	return &harness.Step{
   223  		Name: d.identifiers.Generate(section),
   224  		// Desc: "",
   225  		Type: "script",
   226  		// Timeout: 0,
   227  		// When: convertCond(src.When),
   228  		// On: nil,
   229  		Spec: &harness.StepExec{
   230  			Image:     convertImageMaybe(ctx, d.kubeEnabled),
   231  			Connector: d.dockerhubConn,
   232  			// Mount:      convertMounts(src.Volumes),
   233  			// Privileged: src.Privileged,
   234  			// Pull:       convertPull(src.Pull),
   235  			// Shell:      convertShell(),
   236  			// User:       src.User,
   237  			// Group:      src.Group,
   238  			// Network:    "",
   239  			// Entrypoint: convertEntrypoint(src.Entrypoint),
   240  			// Args:       convertArgs(src.Entrypoint, src.Command),
   241  			Run: command,
   242  			// Envs:       convertVariables(src.Environment),
   243  			// Resources:  convertResourceLimits(&src.Resource),
   244  			// Reports:    nil,
   245  		},
   246  	}
   247  }
   248  
   249  func convertStrategy(ctx *context) *harness.Strategy {
   250  	// TODO env.matrix
   251  	// TODO jobs
   252  	// TODO dart_tasks
   253  
   254  	// https://config.travis-ci.com/matrix_expansion
   255  	spec := &harness.Matrix{}
   256  	spec.Axis = map[string][]string{}
   257  
   258  	// helper function to append the axis
   259  	// to the matrix definition.
   260  	appendAxis := func(name string, items []string) {
   261  		// ignore empty matrix
   262  		if len(items) > 0 {
   263  			var temp []string
   264  			for _, item := range items {
   265  				item = strings.ReplaceAll(item, "1.x", "1")
   266  				temp = append(temp, item)
   267  			}
   268  			spec.Axis[name] = temp
   269  		}
   270  	}
   271  
   272  	appendAxis("compiler", ctx.config.Compiler)
   273  	appendAxis("crystal", ctx.config.Crystal)
   274  	appendAxis("d", ctx.config.D)
   275  	appendAxis("dart", ctx.config.Dart)
   276  	appendAxis("dotnet", ctx.config.Dotnet)
   277  	appendAxis("mono", ctx.config.DotnetMono)
   278  	appendAxis("solution", ctx.config.DotnetSolution)
   279  	appendAxis("elixir", ctx.config.Elixir)
   280  	appendAxis("elm", ctx.config.Elm)
   281  	appendAxis("otp_release", ctx.config.ErlangOTP)
   282  	appendAxis("go", ctx.config.Go)
   283  	appendAxis("hhvm", ctx.config.HHVM)
   284  	appendAxis("haxe", ctx.config.Haxe)
   285  	appendAxis("ghc", ctx.config.GHC)
   286  	appendAxis("jdk", ctx.config.JDK)
   287  	appendAxis("node_js", ctx.config.Node)
   288  	appendAxis("julia", ctx.config.Julia)
   289  	appendAxis("matlab", ctx.config.Matlab)
   290  	appendAxis("nix", ctx.config.Nix)
   291  	appendAxis("xcode_scheme", ctx.config.XcodeScheme)
   292  	appendAxis("xcode_sdk", ctx.config.XcodeSDK)
   293  	appendAxis("php", ctx.config.PHP)
   294  	appendAxis("perl", ctx.config.Perl)
   295  	appendAxis("perl6", ctx.config.Perl6)
   296  	appendAxis("python", ctx.config.Python)
   297  	appendAxis("r", ctx.config.R)
   298  	appendAxis("rvm", append(ctx.config.RubyRVM, append(ctx.config.Ruby, ctx.config.RubyRBenv...)...)) // ruby, rvm, rbenv
   299  	appendAxis("gemfile", append(ctx.config.RubyGemfile, ctx.config.RubyGemfiles...))                  // gemfile, gemfiles
   300  	appendAxis("rust", ctx.config.Rust)
   301  	appendAxis("scala", ctx.config.Scala)
   302  	appendAxis("smalltalk", ctx.config.Smalltalk)
   303  	appendAxis("smalltalk_config", ctx.config.SmalltalkConfig)
   304  	appendAxis("smalltalk_vm", ctx.config.SmalltalkVM)
   305  	appendAxis("os", ctx.config.OS)
   306  	appendAxis("arch", ctx.config.Arch)
   307  	if len(spec.Axis) == 0 {
   308  		return nil
   309  	}
   310  	return &harness.Strategy{
   311  		Type: "matrix",
   312  		Spec: spec,
   313  	}
   314  }
   315  
   316  func createMatrixEnvs(ctx *context) map[string]string {
   317  	// https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
   318  	envs := map[string]string{}
   319  
   320  	appendEnvs := func(name, env string, slice []string) {
   321  		switch len(slice) {
   322  		case 0:
   323  		case 1:
   324  			if s := slice[0]; s != "" {
   325  				envs[env] = slice[0]
   326  			}
   327  		default:
   328  			envs[env] = fmt.Sprintf("<+matrix.%s>", name)
   329  		}
   330  	}
   331  
   332  	appendEnvs("compiler", "TRAVIS_COMPILER", ctx.config.Compiler)
   333  	appendEnvs("crystal", "TRAVIS_CRYSTAL_VERSION", ctx.config.Crystal)
   334  	appendEnvs("d", "TRAVIS_D_VERSION", ctx.config.D)
   335  	appendEnvs("dart", "TRAVIS_DART_VERSION", ctx.config.Dart)
   336  	appendEnvs("dotnet", "TRAVIS_DOTNET_VERSION", ctx.config.Dotnet)
   337  	appendEnvs("mono", "TRAVIS_MONO_VERSION", ctx.config.DotnetMono)
   338  	appendEnvs("solution", "TRAVIS_SOLUTION_VERSION", ctx.config.DotnetSolution)
   339  	appendEnvs("elixir", "TRAVIS_ELIXIR_VERSION", ctx.config.Elixir)
   340  	appendEnvs("elm", "TRAVIS_ELM_VERSION", ctx.config.Elm)
   341  	appendEnvs("otp_release", "TRAVIS_OTP_RELEASE", ctx.config.ErlangOTP)
   342  	appendEnvs("go", "TRAVIS_GO_VERSION", ctx.config.Go)
   343  	appendEnvs("hhvm", "TRAVIS_HHVM_VERSION", ctx.config.HHVM)
   344  	appendEnvs("haxe", "TRAVIS_HAXE_VERSION", ctx.config.Haxe)
   345  	appendEnvs("gemfile", "TRAVIS_GEMFILE_VERSION", append(ctx.config.RubyGemfile, ctx.config.RubyGemfiles...))
   346  	appendEnvs("ghc", "TRAVIS_GHC_VERSION", ctx.config.GHC)
   347  	appendEnvs("jdk", "TRAVIS_JDK_VERSION", ctx.config.JDK)
   348  	appendEnvs("node_js", "TRAVIS_NODE_VERSION", ctx.config.Node)
   349  	appendEnvs("julia", "TRAVIS_JULIA_VERSION", ctx.config.Julia)
   350  	appendEnvs("matlab", "TRAVIS_MATLAB_VERSION", ctx.config.Matlab)
   351  	appendEnvs("nix", "TRAVIS_NIX_VERSION", ctx.config.Nix)
   352  	appendEnvs("xcode_scheme", "TRAVIS_XCODE_SCHEME", ctx.config.XcodeScheme)
   353  	appendEnvs("xcode_sdk", "TRAVIS_XCODE_SDK", ctx.config.XcodeSDK)
   354  	appendEnvs("php", "TRAVIS_PHP_VERSION", ctx.config.PHP)
   355  	appendEnvs("perl", "TRAVIS_PERL_VERSION", ctx.config.Perl)
   356  	appendEnvs("perl6", "TRAVIS_PERL6_VERSION", ctx.config.Perl6)
   357  	appendEnvs("python", "TRAVIS_PYTHON_VERSION", ctx.config.Python)
   358  	appendEnvs("r", "TRAVIS_R_VERSION", ctx.config.R)
   359  	appendEnvs("rust", "TRAVIS_RUST_VERSION", ctx.config.Rust)
   360  	appendEnvs("rvm", "TRAVIS_RUBY_VERSION", append(ctx.config.Ruby, append(ctx.config.RubyRVM, ctx.config.RubyRBenv...)...))
   361  	appendEnvs("scala", "TRAVIS_SCALA_VERSION", ctx.config.Scala)
   362  	appendEnvs("smalltalk", "TRAVIS_SMALLTALK_VERSION", ctx.config.Smalltalk)
   363  	appendEnvs("smalltalk_config", "TRAVIS_SMALLTALK_CONFIG", ctx.config.SmalltalkConfig)
   364  	appendEnvs("smalltalk_vm", "TRAVIS_SMALLTALK_VM", ctx.config.SmalltalkVM)
   365  	appendEnvs("xcode_project", "TRAVIS_XCODE_PROJECT", []string{ctx.config.XcodeProject})
   366  
   367  	// append ruby alias
   368  	if env, ok := envs["TRAVIS_RUBY_VERSION"]; ok {
   369  		envs["TRAVIS_RVM_VERSION"] = env
   370  	}
   371  
   372  	if len(envs) == 0 {
   373  		return nil
   374  	}
   375  	return envs
   376  }
   377  
   378  func convertPlatform(ctx *context) *harness.Platform {
   379  	var os, arch string
   380  
   381  	switch len(ctx.config.OS) {
   382  	case 0:
   383  	case 1:
   384  		os = ctx.config.OS[0]
   385  	default:
   386  		os = "<+matrix.os>"
   387  	}
   388  
   389  	switch len(ctx.config.Arch) {
   390  	case 0:
   391  	case 1:
   392  		arch = ctx.config.Arch[0]
   393  	default:
   394  		arch = "<+matrix.arch>"
   395  	}
   396  
   397  	// normalize os
   398  	switch os {
   399  	case "mac", "ios", "osx":
   400  		os = "macos"
   401  	}
   402  
   403  	// return a nil platform if empty which instructs
   404  	// harness to use the platform defaults.
   405  	if os == "" && arch == "" {
   406  		return nil
   407  	}
   408  	// return &harness.Platform{
   409  	// 	Os: os, Arch: arch,
   410  	// }
   411  	return nil // TODO `os` and `arch` cannot be enums to support matrix
   412  }
   413  
   414  func convertGit(ctx *context) *harness.Clone {
   415  	src := ctx.config.Git
   416  	if src == nil {
   417  		return nil
   418  	}
   419  	dst := new(harness.Clone)
   420  	if src.Depth != nil {
   421  		dst.Depth = int64(src.Depth.Value)
   422  	}
   423  	// TODO git support for submodules
   424  	// TODO git support for submodules_depth
   425  	// TODO git support for lfs_skip_smudge
   426  	// TODO git support for sparse_checkout
   427  	// TODO git support for autocrlf
   428  	return dst
   429  }
   430  
   431  func convertCache(ctx *context) *harness.Cache {
   432  	src := ctx.config.Cache
   433  	if src == nil {
   434  		return nil
   435  	}
   436  	dst := new(harness.Cache)
   437  	dst.Enabled = true
   438  	dst.Paths = append(dst.Paths, src.Directories...)
   439  
   440  	if src.Apt {
   441  		// behavior not documented
   442  		// https://docs.travis-ci.com/user/caching/
   443  	}
   444  	if src.Bundler {
   445  		dst.Paths = append(dst.Paths, "~/.rvm")
   446  		dst.Paths = append(dst.Paths, "vendor/bundle")
   447  	}
   448  	if src.Cargo {
   449  		dst.Paths = append(dst.Paths, "target")
   450  		dst.Paths = append(dst.Paths, "~/.cargo")
   451  	}
   452  	if src.Ccache {
   453  		dst.Paths = append(dst.Paths, "~/.ccache")
   454  	}
   455  	if src.Cocoapods {
   456  		// paths not documented
   457  		// https://docs.travis-ci.com/user/caching/
   458  	}
   459  	if src.Edge {
   460  		// behavior not documented
   461  		// https://docs.travis-ci.com/user/caching/
   462  	}
   463  	if src.Npm {
   464  		dst.Paths = append(dst.Paths, "~/.npm")
   465  		dst.Paths = append(dst.Paths, "node_modules")
   466  	}
   467  	if src.Packages {
   468  		dst.Paths = append(dst.Paths, "~/R/Library")
   469  	}
   470  	if src.Pip {
   471  		dst.Paths = append(dst.Paths, "~/.cache/pip")
   472  	}
   473  	if src.Yarn {
   474  		dst.Paths = append(dst.Paths, "~/.cache/yarn")
   475  	}
   476  
   477  	// TODO when caching R packages, set R_LIB_USER=~/R/Library
   478  	// TODO cache support for `branch`
   479  	// TODO cache support for `timeout`
   480  
   481  	return dst
   482  }