github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/github/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 github converts GitHub pipelines to Harness pipelines. 16 package github 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "os" 23 "regexp" 24 "strconv" 25 "strings" 26 "time" 27 28 github "github.com/drone/go-convert/convert/github/yaml" 29 harness "github.com/drone/spec/dist/go" 30 31 "github.com/drone/go-convert/internal/store" 32 "github.com/ghodss/yaml" 33 ) 34 35 // conversion context 36 type context struct { 37 pipeline *github.Pipeline 38 } 39 40 // Converter converts a GitHub pipeline to a Harness 41 // v1 pipeline. 42 type Converter struct { 43 kubeEnabled bool 44 kubeNamespace string 45 kubeConnector string 46 dockerhubConn string 47 identifiers *store.Identifiers 48 49 // // as we walk the yaml, we store a 50 // // a snapshot of the current node and 51 // // its parents. 52 // config *github.Pipeline 53 // stage *github.Stage 54 } 55 56 // New creates a new Converter that converts a GitHub 57 // pipeline to a Harness v1 pipeline. 58 func New(options ...Option) *Converter { 59 d := new(Converter) 60 61 // create the unique identifier store. this store 62 // is used for registering unique identifiers to 63 // prevent duplicate names, unique index violations. 64 d.identifiers = store.New() 65 66 // loop through and apply the options. 67 for _, option := range options { 68 option(d) 69 } 70 71 // set the default kubernetes namespace. 72 if d.kubeNamespace == "" { 73 d.kubeNamespace = "default" 74 } 75 76 // set the runtime to kubernetes if the kubernetes 77 // connector is configured. 78 if d.kubeConnector != "" { 79 d.kubeEnabled = true 80 } 81 82 return d 83 } 84 85 // Convert downgrades a v1 pipeline. 86 func (d *Converter) Convert(r io.Reader) ([]byte, error) { 87 src, err := github.Parse(r) 88 if err != nil { 89 return nil, err 90 } 91 return d.convert(&context{ 92 pipeline: src, 93 }) 94 } 95 96 // ConvertBytes downgrades a v1 pipeline. 97 func (d *Converter) ConvertBytes(b []byte) ([]byte, error) { 98 return d.Convert( 99 bytes.NewBuffer(b), 100 ) 101 } 102 103 // ConvertString downgrades a v1 pipeline. 104 func (d *Converter) ConvertString(s string) ([]byte, error) { 105 return d.Convert( 106 bytes.NewBufferString(s), 107 ) 108 } 109 110 // ConvertFile downgrades a v1 pipeline. 111 func (d *Converter) ConvertFile(p string) ([]byte, error) { 112 f, err := os.Open(p) 113 if err != nil { 114 return nil, err 115 } 116 defer f.Close() 117 return d.Convert(f) 118 } 119 120 // converts a GitHub pipeline to Harness pipeline. 121 func (d *Converter) convert(ctx *context) ([]byte, error) { 122 123 // create the harness pipeline spec 124 pipeline := &harness.Pipeline{ 125 Stages: []*harness.Stage{}, 126 } 127 128 // create the harness pipeline resource 129 config := &harness.Config{ 130 Version: 1, 131 Kind: "pipeline", 132 Spec: pipeline, 133 } 134 135 // TODO pipeline.name removed from spec 136 // pipeline.Name = ctx.pipeline.Name 137 138 if ctx.pipeline.Env != nil { 139 pipeline.Options = &harness.Default{ 140 Envs: ctx.pipeline.Env, 141 } 142 } 143 144 //pipeline.When = convertOn(from.On) //GAP 145 146 if ctx.pipeline.Jobs != nil { 147 for name, job := range ctx.pipeline.Jobs { 148 // skip nil jobs to avoid nil-pointer 149 if job == nil { 150 continue 151 } 152 153 var cloneStage *harness.CloneStage 154 for _, step := range job.Steps { 155 cloneStage = convertClone(step) 156 if cloneStage != nil { 157 break 158 } 159 } 160 161 pipeline.Stages = append(pipeline.Stages, &harness.Stage{ 162 Name: name, 163 Type: "ci", 164 Strategy: convertStrategy(job.Strategy), 165 When: convertIf(job.If), 166 Spec: &harness.StageCI{ 167 Clone: cloneStage, 168 Envs: job.Env, 169 Platform: convertRunsOn(job.RunsOn), 170 Runtime: &harness.Runtime{ 171 Type: "cloud", 172 Spec: &harness.RuntimeCloud{}, 173 }, 174 Steps: convertSteps(job), 175 //Volumes: convertVolumes(from.Volumes), 176 177 // TODO support for delegate.selectors from.Node 178 // TODO support for stage.variables 179 }, 180 }) 181 } 182 } 183 184 // marshal the harness yaml 185 out, err := yaml.Marshal(config) 186 if err != nil { 187 return nil, err 188 } 189 190 return out, nil 191 } 192 193 func convertClone(src *github.Step) *harness.CloneStage { 194 if src == nil || !isCheckoutAction(src.Uses) { 195 return nil 196 } 197 dst := new(harness.CloneStage) 198 if src.With != nil { 199 if depth, ok := src.With["fetch-depth"]; ok { 200 dst.Depth, _ = toInt64(depth) 201 } 202 } 203 return dst 204 } 205 206 func convertOn(src *github.On) *harness.When { 207 if src == nil || isTriggersEmpty(src) { 208 return nil 209 } 210 211 exprs := map[string]*harness.Expr{} 212 213 for eventName, eventCondition := range getEventConditions(src) { 214 if expr := convertEventCondition(eventCondition); expr != nil { 215 exprs[eventName] = expr 216 } 217 } 218 219 dst := new(harness.When) 220 dst.Cond = []map[string]*harness.Expr{exprs} 221 return dst 222 } 223 224 func convertIf(i string) *harness.When { 225 if i == "" { 226 return nil 227 } 228 229 i = githubExprToJexlExpr(i) 230 231 dst := new(harness.When) 232 dst.Eval = i 233 return dst 234 } 235 236 func githubExprToJexlExpr(githubExpr string) string { 237 // Replace functions 238 githubExpr = strings.Replace(githubExpr, "!contains(", "!~ ", -1) 239 githubExpr = strings.Replace(githubExpr, "contains(", "=~ ", -1) 240 githubExpr = strings.Replace(githubExpr, "startsWith(", "=^ ", -1) 241 githubExpr = strings.Replace(githubExpr, "endsWith(", "=$ ", -1) 242 243 // Replace variables 244 githubExpr = strings.Replace(githubExpr, "github.event_name", "<+trigger.event>", -1) 245 githubExpr = strings.Replace(githubExpr, "github.ref", "<+trigger.payload.ref>", -1) 246 githubExpr = strings.Replace(githubExpr, "github.head_ref", "<+trigger.sourceBranch>", -1) 247 githubExpr = strings.Replace(githubExpr, "github.event.ref", "<+trigger.payload.ref>", -1) 248 githubExpr = strings.Replace(githubExpr, "github.base_ref", "<+trigger.targetBranch>", -1) 249 githubExpr = strings.Replace(githubExpr, "github.event.number", "<+trigger.prNumber>", -1) 250 githubExpr = strings.Replace(githubExpr, "github.event.pull_request.title", "<+trigger.prTitle>", -1) 251 githubExpr = strings.Replace(githubExpr, "github.event.pull_request.body", "<+trigger.payload.pull_request.body>", -1) 252 githubExpr = strings.Replace(githubExpr, "github.event.pull_request.html_url", "<+trigger.payload.pull_request.html_url>", -1) 253 githubExpr = strings.Replace(githubExpr, "github.event.repository.html_url", "<+trigger.repoUrl>", -1) 254 githubExpr = strings.Replace(githubExpr, "github.actor", "<+trigger.gitUser>", -1) 255 githubExpr = strings.Replace(githubExpr, "github.actor_email", "<+codebase.gitUserEmail>", -1) 256 257 return githubExpr 258 } 259 260 func getEventConditions(src *github.On) map[string][]string { 261 eventConditions := make(map[string][]string) 262 263 if src.Push != nil { 264 eventConditions["push"] = src.Push.Branches 265 } 266 if src.PullRequest != nil { 267 eventConditions["pull_request"] = src.PullRequest.Branches 268 } 269 return eventConditions 270 } 271 272 func convertEventCondition(src []string) *harness.Expr { 273 if len(src) != 0 { 274 return &harness.Expr{In: src} 275 } 276 return nil 277 } 278 279 func isTriggersEmpty(src *github.On) bool { 280 return (src.Push == nil || len(src.Push.Branches) == 0) && 281 (src.PullRequest == nil || len(src.PullRequest.Branches) == 0) 282 } 283 284 func toInt64(value interface{}) (int64, error) { 285 switch v := value.(type) { 286 case int: 287 return int64(v), nil 288 case int64: 289 return v, nil 290 case float64: 291 return int64(v), nil 292 case string: 293 intValue, err := strconv.Atoi(v) 294 return int64(intValue), err 295 default: 296 return 0, fmt.Errorf("unsupported type for conversion to int64") 297 } 298 } 299 300 func isCheckoutAction(action string) bool { 301 matched, _ := regexp.MatchString(`^actions/checkout@`, action) 302 return matched 303 } 304 305 func convertRunsOn(src string) *harness.Platform { 306 if src == "" { 307 return nil 308 } 309 dst := new(harness.Platform) 310 switch { 311 case strings.Contains(src, "windows"), strings.Contains(src, "win"): 312 dst.Os = harness.OSWindows.String() 313 case strings.Contains(src, "darwin"), strings.Contains(src, "macos"), strings.Contains(src, "mac"): 314 dst.Os = harness.OSDarwin.String() 315 default: 316 dst.Os = harness.OSLinux.String() 317 } 318 dst.Arch = harness.ArchAmd64.String() // we assume amd64 for now 319 return dst 320 } 321 322 // copyEnv returns a copy of the environment variable map. 323 func copyEnv(src map[string]string) map[string]string { 324 dst := map[string]string{} 325 for k, v := range src { 326 dst[k] = v 327 } 328 return dst 329 } 330 331 func convertSteps(src *github.Job) []*harness.Step { 332 var steps []*harness.Step 333 for serviceName, service := range src.Services { 334 if service != nil { 335 steps = append(steps, convertServices(service, serviceName)) 336 } 337 } 338 for _, step := range src.Steps { 339 if isCheckoutAction(step.Uses) { 340 continue 341 } 342 dst := &harness.Step{ 343 Name: step.Name, 344 } 345 346 if step.ContinueOnErr { 347 dst.Failure = convertContinueOnError(step) 348 } 349 350 if step.Timeout != 0 { 351 dst.Timeout = convertTimeout(step) 352 } 353 354 if step.Uses != "" { 355 dst.Name = step.Name 356 dst.Spec = convertAction(step) 357 dst.Type = "action" 358 } else { 359 dst.Name = step.Name 360 dst.Spec = convertRun(step, src.Container) 361 dst.Type = "script" 362 } 363 steps = append(steps, dst) 364 } 365 return steps 366 } 367 368 func convertAction(src *github.Step) *harness.StepAction { 369 if src == nil { 370 return nil 371 } 372 dst := &harness.StepAction{ 373 Uses: src.Uses, 374 With: make(map[string]interface{}), 375 Envs: src.Env, 376 } 377 for key, value := range src.With { 378 switch v := value.(type) { 379 case float64: 380 dst.With[key] = fmt.Sprintf("%v", v) 381 case bool: 382 dst.With[key] = value 383 case string: 384 if strings.HasPrefix(v, "'") && strings.HasSuffix(v, "'") { 385 dst.With[key] = value 386 } else { 387 dst.With[key] = fmt.Sprintf("%s", v) 388 } 389 default: 390 dst.With[key] = value 391 } 392 } 393 return dst 394 } 395 396 func convertContinueOnError(src *github.Step) *harness.FailureList { 397 if !src.ContinueOnErr { 398 return nil 399 } 400 401 return &harness.FailureList{ 402 Items: []*harness.Failure{ 403 { 404 Errors: []string{"all"}, 405 Action: &harness.FailureAction{ 406 Type: "ignore", 407 Spec: &harness.Ignore{}, 408 }, 409 }, 410 }, 411 } 412 } 413 414 func convertRun(src *github.Step, container *github.Container) *harness.StepExec { 415 if src == nil { 416 return nil 417 } 418 dst := &harness.StepExec{ 419 Run: src.Run, 420 Envs: src.Env, 421 } 422 if container != nil { 423 dst.Image = container.Image 424 } 425 return dst 426 } 427 428 func convertServices(service *github.Service, serviceName string) *harness.Step { 429 if service == nil { 430 return nil 431 } 432 return &harness.Step{ 433 Name: serviceName, 434 Type: "background", 435 Spec: &harness.StepBackground{ 436 Image: service.Image, 437 Envs: service.Env, 438 Mount: convertMounts(service.Volumes), 439 Ports: service.Ports, 440 Args: service.Options, 441 }, 442 } 443 } 444 445 func convertTimeout(src *github.Step) string { 446 if src == nil || src.Timeout == 0 { 447 return "0" 448 } 449 return fmt.Sprint(time.Duration(src.Timeout * int(time.Minute))) 450 } 451 452 func convertMounts(volumes []string) []*harness.Mount { 453 if len(volumes) == 0 { 454 return nil 455 } 456 var dst []*harness.Mount 457 458 for _, volume := range volumes { 459 parts := strings.Split(volume, ":") 460 461 var mount harness.Mount 462 if len(parts) > 1 { 463 mount.Name = parts[0] 464 mount.Path = parts[1] 465 } else { 466 mount.Path = parts[0] 467 } 468 469 dst = append(dst, &mount) 470 } 471 472 return dst 473 } 474 475 func convertStrategy(src *github.Strategy) *harness.Strategy { 476 if src == nil || src.Matrix == nil { 477 return nil 478 } 479 480 matrix := src.Matrix 481 482 includeMaps := convertInterfaceMapsToStringMaps(matrix.Include) 483 excludeMaps := convertInterfaceMapsToStringMaps(matrix.Exclude) 484 dst := &harness.Strategy{ 485 Type: "matrix", 486 Spec: &harness.Matrix{ 487 Axis: matrix.Matrix, 488 Include: includeMaps, 489 Exclude: excludeMaps, 490 }, 491 } 492 return dst 493 } 494 495 func convertInterfaceMapsToStringMaps(maps []map[string]interface{}) []map[string]string { 496 convertedMaps := make([]map[string]string, len(maps)) 497 for i, originalMap := range maps { 498 convertedMap := make(map[string]string) 499 for key, value := range originalMap { 500 convertedMap[key] = fmt.Sprintf("%v", value) 501 } 502 convertedMaps[i] = convertedMap 503 } 504 return convertedMaps 505 }