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