1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import "strings"
6
7// Limits GitHub applies to push events.
8const (
9 // MaxFilesEvaluated is the number of changed files a paths or paths-ignore
10 // filter looks at; files after the 3000th are ignored. Recorded on
11 // github.com (filter-paths-3000-boundary): a push of exactly 3000 files
12 // whose only match is the last one runs the workflow, the same push with
13 // 3001 files does not. It is not 300 (filter-paths-300-files and
14 // filter-paths-1200-files: a match beyond the 300th file still runs).
15 MaxFilesEvaluated = 3000
16 // MaxTagsPerPush is the number of tags that can be pushed at once before
17 // GitHub creates no event for them.
18 MaxTagsPerPush = 3
19)
20
21// PushInfo describes a push to one ref.
22type PushInfo struct {
23 // Ref is the full name, "refs/heads/main" or "refs/tags/v1".
24 Ref string
25 // Created and Deleted say whether the push created or deleted the ref.
26 Created bool
27 Deleted bool
28 // ChangedFiles are the paths changed by the push, in the order of the
29 // diff. Only the first MaxFilesEvaluated are looked at.
30 ChangedFiles []string
31 // FilesTruncated is set when the list of changed files is not available:
32 // the push has more than 1000 commits, or the diff could not be
33 // computed. A paths filter is then considered satisfied.
34 FilesTruncated bool
35 // TagCount is the number of tags pushed in the same push.
36 TagCount int
37}
38
39// MatchPush reports whether a push triggers the workflow, applying the
40// "push" event's filters like GitHub:
41//
42// - the deletion of a ref triggers nothing;
43// - a push of more than three tags creates no event for its tags;
44// - a workflow with only branches or branches-ignore does not run for a tag,
45// and one with only tags or tags-ignore does not run for a branch; with
46// neither it runs for both;
47// - when both a branches and a tags filter exist, a ref is checked against
48// the filter for its kind;
49// - paths and paths-ignore apply to branch pushes only, against the first
50// 3000 changed files; when the list is unavailable (see
51// [PushInfo.FilesTruncated]) they pass;
52// - patterns are evaluated in order and the last one that matches wins.
53//
54// Commit-message directives are separate: see [SkipCI].
55//
56// Recorded on github.com: push-tags-limit, filter-tags, filter-branches-ignore,
57// filter-tag-branches-only, filter-paths-tag, push-delete-branch and
58// push-delete-tag, see TestMatchPushRecordedTags. Still assumed: the
59// behaviour on refs that are neither a branch nor a tag, and on a push that
60// changes no file.
61func (w *Workflow) MatchPush(p PushInfo) bool {
62 if w == nil || w.On == nil {
63 return false
64 }
65 ev := w.On.Event("push")
66 if ev == nil {
67 return false
68 }
69 if p.Deleted {
70 return false
71 }
72 switch {
73 case strings.HasPrefix(p.Ref, "refs/heads/"):
74 name := strings.TrimPrefix(p.Ref, "refs/heads/")
75 if !ev.refFilterAllows(true, name) {
76 return false
77 }
78 return ev.pathsAllow(p)
79 case strings.HasPrefix(p.Ref, "refs/tags/"):
80 if p.TagCount > MaxTagsPerPush {
81 return false
82 }
83 return ev.refFilterAllows(false, strings.TrimPrefix(p.Ref, "refs/tags/"))
84 }
85 return false
86}
87
88// refFilterAllows applies the branches or tags filters to the name of a
89// branch (branch is true) or a tag.
90func (e *Event) refFilterAllows(branch bool, name string) bool {
91 cf := e.filters()
92 inc, exc := cf.branches, cf.branchesIgnore
93 own, ownIgnore, other, otherIgnore := e.Branches, e.BranchesIgnore, e.Tags, e.TagsIgnore
94 if !branch {
95 inc, exc = cf.tags, cf.tagsIgnore
96 own, ownIgnore, other, otherIgnore = e.Tags, e.TagsIgnore, e.Branches, e.BranchesIgnore
97 }
98 if own == nil && ownIgnore == nil && (other != nil || otherIgnore != nil) {
99 return false
100 }
101 if own != nil && !matchList(inc, name) {
102 return false
103 }
104 if ownIgnore != nil && matchList(exc, name) {
105 return false
106 }
107 return true
108}
109
110// pathsAllow applies paths and paths-ignore to a branch push.
111func (e *Event) pathsAllow(p PushInfo) bool {
112 if e.Paths == nil && e.PathsIgnore == nil {
113 return true
114 }
115 if p.FilesTruncated {
116 return true
117 }
118 cf := e.filters()
119 files := p.ChangedFiles
120 if len(files) > MaxFilesEvaluated {
121 files = files[:MaxFilesEvaluated]
122 }
123 if e.Paths != nil {
124 any := false
125 for _, f := range files {
126 if matchList(cf.paths, f) {
127 any = true
128 break
129 }
130 }
131 if !any {
132 return false
133 }
134 }
135 if e.PathsIgnore != nil && len(files) > 0 {
136 any := false
137 for _, f := range files {
138 if !matchList(cf.pathsIgnore, f) {
139 any = true
140 break
141 }
142 }
143 if !any {
144 return false
145 }
146 }
147 return true
148}