code.gitea.io/gitea@v1.19.3/modules/actions/workflows.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package actions 5 6 import ( 7 "bytes" 8 "io" 9 "strings" 10 11 "code.gitea.io/gitea/modules/git" 12 "code.gitea.io/gitea/modules/log" 13 api "code.gitea.io/gitea/modules/structs" 14 webhook_module "code.gitea.io/gitea/modules/webhook" 15 16 "github.com/gobwas/glob" 17 "github.com/nektos/act/pkg/jobparser" 18 "github.com/nektos/act/pkg/model" 19 "github.com/nektos/act/pkg/workflowpattern" 20 "gopkg.in/yaml.v3" 21 ) 22 23 func init() { 24 model.OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) { 25 // Log the error instead of panic or fatal. 26 // It will be a big job to refactor act/pkg/model to return decode error, 27 // so we just log the error and return empty value, and improve it later. 28 log.Error("Failed to decode node %v into %T: %v", node, out, err) 29 } 30 } 31 32 func ListWorkflows(commit *git.Commit) (git.Entries, error) { 33 tree, err := commit.SubTree(".gitea/workflows") 34 if _, ok := err.(git.ErrNotExist); ok { 35 tree, err = commit.SubTree(".github/workflows") 36 } 37 if _, ok := err.(git.ErrNotExist); ok { 38 return nil, nil 39 } 40 if err != nil { 41 return nil, err 42 } 43 44 entries, err := tree.ListEntriesRecursiveFast() 45 if err != nil { 46 return nil, err 47 } 48 49 ret := make(git.Entries, 0, len(entries)) 50 for _, entry := range entries { 51 if strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml") { 52 ret = append(ret, entry) 53 } 54 } 55 return ret, nil 56 } 57 58 func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) { 59 entries, err := ListWorkflows(commit) 60 if err != nil { 61 return nil, err 62 } 63 64 workflows := make(map[string][]byte, len(entries)) 65 for _, entry := range entries { 66 f, err := entry.Blob().DataAsync() 67 if err != nil { 68 return nil, err 69 } 70 content, err := io.ReadAll(f) 71 _ = f.Close() 72 if err != nil { 73 return nil, err 74 } 75 workflow, err := model.ReadWorkflow(bytes.NewReader(content)) 76 if err != nil { 77 log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) 78 continue 79 } 80 events, err := jobparser.ParseRawOn(&workflow.RawOn) 81 if err != nil { 82 log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) 83 continue 84 } 85 for _, evt := range events { 86 log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) 87 if detectMatched(commit, triggedEvent, payload, evt) { 88 workflows[entry.Name()] = content 89 } 90 } 91 } 92 93 return workflows, nil 94 } 95 96 func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { 97 if !canGithubEventMatch(evt.Name, triggedEvent) { 98 return false 99 } 100 101 switch triggedEvent { 102 case // events with no activity types 103 webhook_module.HookEventCreate, 104 webhook_module.HookEventDelete, 105 webhook_module.HookEventFork, 106 // FIXME: `wiki` event should match `gollum` event 107 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum 108 webhook_module.HookEventWiki: 109 if len(evt.Acts()) != 0 { 110 log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) 111 } 112 // no special filter parameters for these events, just return true if name matched 113 return true 114 115 case // push 116 webhook_module.HookEventPush: 117 return matchPushEvent(commit, payload.(*api.PushPayload), evt) 118 119 case // issues 120 webhook_module.HookEventIssues, 121 webhook_module.HookEventIssueAssign, 122 webhook_module.HookEventIssueLabel, 123 webhook_module.HookEventIssueMilestone: 124 return matchIssuesEvent(commit, payload.(*api.IssuePayload), evt) 125 126 case // issue_comment 127 webhook_module.HookEventIssueComment, 128 // `pull_request_comment` is same as `issue_comment` 129 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment 130 webhook_module.HookEventPullRequestComment: 131 return matchIssueCommentEvent(commit, payload.(*api.IssueCommentPayload), evt) 132 133 case // pull_request 134 webhook_module.HookEventPullRequest, 135 webhook_module.HookEventPullRequestSync, 136 webhook_module.HookEventPullRequestAssign, 137 webhook_module.HookEventPullRequestLabel: 138 return matchPullRequestEvent(commit, payload.(*api.PullRequestPayload), evt) 139 140 case // pull_request_review 141 webhook_module.HookEventPullRequestReviewApproved, 142 webhook_module.HookEventPullRequestReviewRejected: 143 return matchPullRequestReviewEvent(commit, payload.(*api.PullRequestPayload), evt) 144 145 case // pull_request_review_comment 146 webhook_module.HookEventPullRequestReviewComment: 147 return matchPullRequestReviewCommentEvent(commit, payload.(*api.PullRequestPayload), evt) 148 149 case // release 150 webhook_module.HookEventRelease: 151 return matchReleaseEvent(commit, payload.(*api.ReleasePayload), evt) 152 153 case // registry_package 154 webhook_module.HookEventPackage: 155 return matchPackageEvent(commit, payload.(*api.PackagePayload), evt) 156 157 default: 158 log.Warn("unsupported event %q", triggedEvent) 159 return false 160 } 161 } 162 163 func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool { 164 // with no special filter parameters 165 if len(evt.Acts()) == 0 { 166 return true 167 } 168 169 matchTimes := 0 170 hasBranchFilter := false 171 hasTagFilter := false 172 refName := git.RefName(pushPayload.Ref) 173 // all acts conditions should be satisfied 174 for cond, vals := range evt.Acts() { 175 switch cond { 176 case "branches": 177 hasBranchFilter = true 178 if !refName.IsBranch() { 179 break 180 } 181 patterns, err := workflowpattern.CompilePatterns(vals...) 182 if err != nil { 183 break 184 } 185 if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 186 matchTimes++ 187 } 188 case "branches-ignore": 189 hasBranchFilter = true 190 if !refName.IsBranch() { 191 break 192 } 193 patterns, err := workflowpattern.CompilePatterns(vals...) 194 if err != nil { 195 break 196 } 197 if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 198 matchTimes++ 199 } 200 case "tags": 201 hasTagFilter = true 202 if !refName.IsTag() { 203 break 204 } 205 patterns, err := workflowpattern.CompilePatterns(vals...) 206 if err != nil { 207 break 208 } 209 if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 210 matchTimes++ 211 } 212 case "tags-ignore": 213 hasTagFilter = true 214 if !refName.IsTag() { 215 break 216 } 217 patterns, err := workflowpattern.CompilePatterns(vals...) 218 if err != nil { 219 break 220 } 221 if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 222 matchTimes++ 223 } 224 case "paths": 225 filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) 226 if err != nil { 227 log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) 228 } else { 229 patterns, err := workflowpattern.CompilePatterns(vals...) 230 if err != nil { 231 break 232 } 233 if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { 234 matchTimes++ 235 } 236 } 237 case "paths-ignore": 238 filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) 239 if err != nil { 240 log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) 241 } else { 242 patterns, err := workflowpattern.CompilePatterns(vals...) 243 if err != nil { 244 break 245 } 246 if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { 247 matchTimes++ 248 } 249 } 250 default: 251 log.Warn("push event unsupported condition %q", cond) 252 } 253 } 254 // if both branch and tag filter are defined in the workflow only one needs to match 255 if hasBranchFilter && hasTagFilter { 256 matchTimes++ 257 } 258 return matchTimes == len(evt.Acts()) 259 } 260 261 func matchIssuesEvent(commit *git.Commit, issuePayload *api.IssuePayload, evt *jobparser.Event) bool { 262 // with no special filter parameters 263 if len(evt.Acts()) == 0 { 264 return true 265 } 266 267 matchTimes := 0 268 // all acts conditions should be satisfied 269 for cond, vals := range evt.Acts() { 270 switch cond { 271 case "types": 272 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues 273 // Actions with the same name: 274 // opened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned 275 // Actions need to be converted: 276 // label_updated -> labeled 277 // label_cleared -> unlabeled 278 // Unsupported activity types: 279 // deleted, transferred, pinned, unpinned, locked, unlocked 280 281 action := issuePayload.Action 282 switch action { 283 case api.HookIssueLabelUpdated: 284 action = "labeled" 285 case api.HookIssueLabelCleared: 286 action = "unlabeled" 287 } 288 for _, val := range vals { 289 if glob.MustCompile(val, '/').Match(string(action)) { 290 matchTimes++ 291 break 292 } 293 } 294 default: 295 log.Warn("issue event unsupported condition %q", cond) 296 } 297 } 298 return matchTimes == len(evt.Acts()) 299 } 300 301 func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { 302 // with no special filter parameters 303 if len(evt.Acts()) == 0 { 304 // defaultly, only pull request `opened`, `reopened` and `synchronized` will trigger workflow 305 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 306 return prPayload.Action == api.HookIssueSynchronized || prPayload.Action == api.HookIssueOpened || prPayload.Action == api.HookIssueReOpened 307 } 308 309 matchTimes := 0 310 // all acts conditions should be satisfied 311 for cond, vals := range evt.Acts() { 312 switch cond { 313 case "types": 314 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 315 // Actions with the same name: 316 // opened, edited, closed, reopened, assigned, unassigned 317 // Actions need to be converted: 318 // synchronized -> synchronize 319 // label_updated -> labeled 320 // label_cleared -> unlabeled 321 // Unsupported activity types: 322 // converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled 323 324 action := prPayload.Action 325 switch action { 326 case api.HookIssueSynchronized: 327 action = "synchronize" 328 case api.HookIssueLabelUpdated: 329 action = "labeled" 330 case api.HookIssueLabelCleared: 331 action = "unlabeled" 332 } 333 log.Trace("matching pull_request %s with %v", action, vals) 334 for _, val := range vals { 335 if glob.MustCompile(val, '/').Match(string(action)) { 336 matchTimes++ 337 break 338 } 339 } 340 case "branches": 341 refName := git.RefName(prPayload.PullRequest.Base.Ref) 342 patterns, err := workflowpattern.CompilePatterns(vals...) 343 if err != nil { 344 break 345 } 346 if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 347 matchTimes++ 348 } 349 case "branches-ignore": 350 refName := git.RefName(prPayload.PullRequest.Base.Ref) 351 patterns, err := workflowpattern.CompilePatterns(vals...) 352 if err != nil { 353 break 354 } 355 if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { 356 matchTimes++ 357 } 358 case "paths": 359 filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) 360 if err != nil { 361 log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) 362 } else { 363 patterns, err := workflowpattern.CompilePatterns(vals...) 364 if err != nil { 365 break 366 } 367 if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { 368 matchTimes++ 369 } 370 } 371 case "paths-ignore": 372 filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) 373 if err != nil { 374 log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) 375 } else { 376 patterns, err := workflowpattern.CompilePatterns(vals...) 377 if err != nil { 378 break 379 } 380 if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { 381 matchTimes++ 382 } 383 } 384 default: 385 log.Warn("pull request event unsupported condition %q", cond) 386 } 387 } 388 return matchTimes == len(evt.Acts()) 389 } 390 391 func matchIssueCommentEvent(commit *git.Commit, issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool { 392 // with no special filter parameters 393 if len(evt.Acts()) == 0 { 394 return true 395 } 396 397 matchTimes := 0 398 // all acts conditions should be satisfied 399 for cond, vals := range evt.Acts() { 400 switch cond { 401 case "types": 402 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment 403 // Actions with the same name: 404 // created, edited, deleted 405 // Actions need to be converted: 406 // NONE 407 // Unsupported activity types: 408 // NONE 409 410 for _, val := range vals { 411 if glob.MustCompile(val, '/').Match(string(issueCommentPayload.Action)) { 412 matchTimes++ 413 break 414 } 415 } 416 default: 417 log.Warn("issue comment event unsupported condition %q", cond) 418 } 419 } 420 return matchTimes == len(evt.Acts()) 421 } 422 423 func matchPullRequestReviewEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { 424 // with no special filter parameters 425 if len(evt.Acts()) == 0 { 426 return true 427 } 428 429 matchTimes := 0 430 // all acts conditions should be satisfied 431 for cond, vals := range evt.Acts() { 432 switch cond { 433 case "types": 434 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review 435 // Activity types with the same name: 436 // NONE 437 // Activity types need to be converted: 438 // reviewed -> submitted 439 // reviewed -> edited 440 // Unsupported activity types: 441 // dismissed 442 443 actions := make([]string, 0) 444 if prPayload.Action == api.HookIssueReviewed { 445 // the `reviewed` HookIssueAction can match the two activity types: `submitted` and `edited` 446 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review 447 actions = append(actions, "submitted", "edited") 448 } 449 450 matched := false 451 for _, val := range vals { 452 for _, action := range actions { 453 if glob.MustCompile(val, '/').Match(action) { 454 matched = true 455 break 456 } 457 } 458 if matched { 459 break 460 } 461 } 462 if matched { 463 matchTimes++ 464 } 465 default: 466 log.Warn("pull request review event unsupported condition %q", cond) 467 } 468 } 469 return matchTimes == len(evt.Acts()) 470 } 471 472 func matchPullRequestReviewCommentEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { 473 // with no special filter parameters 474 if len(evt.Acts()) == 0 { 475 return true 476 } 477 478 matchTimes := 0 479 // all acts conditions should be satisfied 480 for cond, vals := range evt.Acts() { 481 switch cond { 482 case "types": 483 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review_comment 484 // Activity types with the same name: 485 // NONE 486 // Activity types need to be converted: 487 // reviewed -> created 488 // reviewed -> edited 489 // Unsupported activity types: 490 // deleted 491 492 actions := make([]string, 0) 493 if prPayload.Action == api.HookIssueReviewed { 494 // the `reviewed` HookIssueAction can match the two activity types: `created` and `edited` 495 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review_comment 496 actions = append(actions, "created", "edited") 497 } 498 499 matched := false 500 for _, val := range vals { 501 for _, action := range actions { 502 if glob.MustCompile(val, '/').Match(action) { 503 matched = true 504 break 505 } 506 } 507 if matched { 508 break 509 } 510 } 511 if matched { 512 matchTimes++ 513 } 514 default: 515 log.Warn("pull request review comment event unsupported condition %q", cond) 516 } 517 } 518 return matchTimes == len(evt.Acts()) 519 } 520 521 func matchReleaseEvent(commit *git.Commit, payload *api.ReleasePayload, evt *jobparser.Event) bool { 522 // with no special filter parameters 523 if len(evt.Acts()) == 0 { 524 return true 525 } 526 527 matchTimes := 0 528 // all acts conditions should be satisfied 529 for cond, vals := range evt.Acts() { 530 switch cond { 531 case "types": 532 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release 533 // Activity types with the same name: 534 // published 535 // Activity types need to be converted: 536 // updated -> edited 537 // Unsupported activity types: 538 // unpublished, created, deleted, prereleased, released 539 540 action := payload.Action 541 switch action { 542 case api.HookReleaseUpdated: 543 action = "edited" 544 } 545 for _, val := range vals { 546 if glob.MustCompile(val, '/').Match(string(action)) { 547 matchTimes++ 548 break 549 } 550 } 551 default: 552 log.Warn("release event unsupported condition %q", cond) 553 } 554 } 555 return matchTimes == len(evt.Acts()) 556 } 557 558 func matchPackageEvent(commit *git.Commit, payload *api.PackagePayload, evt *jobparser.Event) bool { 559 // with no special filter parameters 560 if len(evt.Acts()) == 0 { 561 return true 562 } 563 564 matchTimes := 0 565 // all acts conditions should be satisfied 566 for cond, vals := range evt.Acts() { 567 switch cond { 568 case "types": 569 // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#registry_package 570 // Activity types with the same name: 571 // NONE 572 // Activity types need to be converted: 573 // created -> published 574 // Unsupported activity types: 575 // updated 576 577 action := payload.Action 578 switch action { 579 case api.HookPackageCreated: 580 action = "published" 581 } 582 for _, val := range vals { 583 if glob.MustCompile(val, '/').Match(string(action)) { 584 matchTimes++ 585 break 586 } 587 } 588 default: 589 log.Warn("package event unsupported condition %q", cond) 590 } 591 } 592 return matchTimes == len(evt.Acts()) 593 }