Go · 4413 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package trigger
4
5 import (
6 "github.com/tenseleyFlow/shithub/internal/actions/workflow"
7 )
8
9 // Match reports whether the workflow's `on:` predicates accept the
10 // given event. Pure function: no I/O, no DB. Cheap to fuzz.
11 //
12 // The four event kinds:
13 //
14 // - push → on.push present, branch/tag classification
15 // passes the appropriate sub-filter, paths
16 // filter (when set) hits at least one
17 // changed path
18 // - pull_request → on.pull_request present, action is in the
19 // configured types: list (default ["opened",
20 // "synchronize", "reopened"]), base branch
21 // passes branches: filter, paths filter hits
22 // - schedule → on.schedule has any entry whose cron string
23 // equals event.Cron (the sweep tells us which
24 // cron fired; we just verify the workflow
25 // declared it)
26 // - workflow_dispatch → on.workflow_dispatch present
27 //
28 // Anything else returns false silently — strict-allowlist v1 posture.
29 //
30 // A workflow that doesn't declare the trigger kind at all (e.g.,
31 // only `on: push` but the event is a pull_request) returns false.
32 func Match(w *workflow.Workflow, ev Event) bool {
33 if w == nil {
34 return false
35 }
36 switch ev.Kind {
37 case EventPush:
38 return matchPush(w.On.Push, ev)
39 case EventPullRequest:
40 return matchPullRequest(w.On.PullRequest, ev)
41 case EventSchedule:
42 return matchSchedule(w.On.Schedule, ev)
43 case EventWorkflowDispatch:
44 return w.On.WorkflowDispatch != nil
45 }
46 return false
47 }
48
49 func matchPush(pt *workflow.PushTrigger, ev Event) bool {
50 if pt == nil {
51 return false
52 }
53 // Branch vs tag classification:
54 // - When event has a Branch set, only the branches: filter applies
55 // (a tags-only filter wouldn't accept a branch push, and vice
56 // versa). Match GHA semantics.
57 // - Same for Tag.
58 switch {
59 case ev.Branch != "":
60 // If neither branches nor tags is configured, match-all.
61 // If only tags is configured, this branch push doesn't match.
62 if len(pt.Branches) == 0 && len(pt.Tags) == 0 {
63 return matchPaths(pt.Paths, ev.ChangedPaths)
64 }
65 if len(pt.Branches) == 0 {
66 return false
67 }
68 if !matchAny(pt.Branches, ev.Branch) {
69 return false
70 }
71 case ev.Tag != "":
72 if len(pt.Branches) == 0 && len(pt.Tags) == 0 {
73 return matchPaths(pt.Paths, ev.ChangedPaths)
74 }
75 if len(pt.Tags) == 0 {
76 return false
77 }
78 if !matchAny(pt.Tags, ev.Tag) {
79 return false
80 }
81 default:
82 // Push to a non-branch, non-tag ref (e.g., refs/notes/*). v1
83 // doesn't surface those.
84 return false
85 }
86 return matchPaths(pt.Paths, ev.ChangedPaths)
87 }
88
89 func matchPullRequest(prt *workflow.PullRequestTrigger, ev Event) bool {
90 if prt == nil {
91 return false
92 }
93 // Default activity types per GHA when `types:` is omitted.
94 types := prt.Types
95 if len(types) == 0 {
96 types = defaultPRTypes
97 }
98 if !containsString(types, ev.Action) {
99 return false
100 }
101 // branches: filter applies to the BASE ref (the destination), per
102 // GHA docs — that's the branch the PR is targeting.
103 if len(prt.Branches) > 0 && !matchAny(prt.Branches, ev.BaseRef) {
104 return false
105 }
106 return matchPaths(prt.Paths, ev.ChangedPaths)
107 }
108
109 func matchSchedule(entries []workflow.ScheduleTrigger, ev Event) bool {
110 if len(entries) == 0 {
111 return false
112 }
113 // We require an exact cron-expression match against the entry the
114 // sweep fired. The sweep is the source of truth for which cron is
115 // firing; we just gate on the workflow having declared that
116 // expression. Avoids interpreting cron semantics in two places.
117 for _, e := range entries {
118 if e.Cron == ev.Cron {
119 return true
120 }
121 }
122 return false
123 }
124
125 // matchPaths returns true when paths is empty (no filter) or at least
126 // one changed path matches the filter list. Empty changed-paths +
127 // non-empty filter is a miss — we have a filter and nothing to match.
128 func matchPaths(filter, changed []string) bool {
129 if len(filter) == 0 {
130 return true
131 }
132 for _, c := range changed {
133 if matchAny(filter, c) {
134 return true
135 }
136 }
137 return false
138 }
139
140 func containsString(haystack []string, needle string) bool {
141 for _, s := range haystack {
142 if s == needle {
143 return true
144 }
145 }
146 return false
147 }
148
149 // defaultPRTypes mirrors GHA's default for on.pull_request when no
150 // types: list is given. Workflow authors who specify types: opt out
151 // of this default.
152 var defaultPRTypes = []string{"opened", "synchronize", "reopened"}
153