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  }