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 }