Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

319 linesCodeBlameRaw
1//! Which events each subscribing service's queue is sent.
2//!
3//! The events service passes each batch on to one queue per subscriber
4//! (`SUBSCRIBER_*` bindings). Each is sent only the types its consumer acts
5//! on, so an event costs a queue write for the services that read it, not
6//! for all of them. A consumer that starts acting on a new type adds it
7//! here in the same change, or it never hears of it; the tests below and
8//! beside each consumer check what they can.
9//!
10//! A pattern is an exact type (`git.push`), a family (`pull.*`, every type
11//! that starts `pull.`), or `*`, every type. A binding not named here is
12//! sent everything, so a new subscriber is never left out.
13
14/// What the shared helpers in `g1t_kit` act on (`rename`, `transfer`,
15/// `lifecycle`, `deleted`, `user_deleted`): rows that follow a workspace
16/// or repository, or go with it. Rare, so every subscriber is sent them.
17const LIFECYCLE: [&str; 8] = [
18 "workspace.renamed",
19 "workspace.deleted",
20 "repo.renamed",
21 "repo.transferred",
22 "repo.deleted",
23 "repo.restored",
24 "repo.purged",
25 "user.deleted",
26];
27
28/// Each subscriber's binding and the types it is sent beside [`LIFECYCLE`].
29pub const ROUTES: &[(&str, &[&str])] = &[
30 // Mentions, pull request bases, runs stopped in archived repositories
31 // (work/src/lib.rs queue).
32 (
33 "SUBSCRIBER_WORK",
34 &[
35 "git.push",
36 "branch.renamed",
37 "repo.created",
38 "repo.archived",
39 "repo.default_branch_changed",
40 "pull.merged",
41 "comment.created",
42 ],
43 ),
44 // Pull requests g1t is seeing through, mentions, queued issues and the
45 // merge queue (runner/src/index.ts queue).
46 (
47 "SUBSCRIBER_RUNNER",
48 &[
49 "pull.opened",
50 "pull.ready",
51 "pull.updated",
52 "pull.mergeability",
53 "pull.mergecheck",
54 "pull.merge_requested",
55 "pull.merged",
56 "pull.closed",
57 "checks.completed",
58 "queue.changed",
59 "comment.created",
60 "issue.opened",
61 "issue.updated",
62 "issue.closed",
63 "repo.archived",
64 ],
65 ),
66 // Write-back to linked trackers (integrations/src lib.rs), and g1t
67 // taking on what it is assigned or asked in a thread (summon.rs).
68 ("SUBSCRIBER_INTEGRATIONS", &["issue.closed", "pull.opened", "issue.assigned", "comment.created"]),
69 // A hook may ask for any type, or for every one.
70 ("SUBSCRIBER_WEBHOOKS", &["*"]),
71 // Every event a workflow can run on (g1t_actions::events::github_events),
72 // and runs stopped in archived repositories.
73 (
74 "SUBSCRIBER_ACTIONS",
75 &[
76 "git.push",
77 "git.deleted",
78 "pull.*",
79 "issue.*",
80 "comment.*",
81 "release.*",
82 "deployment.created",
83 "deployment_status.created",
84 "workflow.completed",
85 "repo.archived",
86 ],
87 ),
88 // Previews and production deploys (deployments/src/index.ts queue).
89 (
90 "SUBSCRIBER_DEPLOYMENTS",
91 &[
92 "git.push",
93 "branch.renamed",
94 "repo.default_branch_changed",
95 "pull.opened",
96 "pull.ready",
97 "pull.updated",
98 "pull.reopened",
99 "pull.closed",
100 "pull.merged",
101 "workspace.deleting",
102 "workspace.restored",
103 ],
104 ),
105 // Projects follow their repositories, and wake on activity
106 // (projects/src/index.ts, activity.ts). A production deployment
107 // reported from elsewhere asks for its screenshot.
108 (
109 "SUBSCRIBER_PROJECTS",
110 &[
111 "git.push",
112 "repo.*",
113 "issue.opened",
114 "issue.closed",
115 "issue.reopened",
116 "pull.opened",
117 "pull.updated",
118 "pull.merged",
119 "pull.closed",
120 "comment.created",
121 "deployment.succeeded",
122 "deployment.failed",
123 "deployment_status.created",
124 "package.published",
125 "package.deleted",
126 "package.version_deleted",
127 "workspace.deleting",
128 "workspace.restored",
129 ],
130 ),
131 // Pull request working copies, workspaces' repositories, contributors
132 // shown as ghost (repos/src/lib.rs handle_event).
133 (
134 "SUBSCRIBER_REPOS",
135 &[
136 "pull.merged",
137 "pull.closed",
138 "pull.reopened",
139 "workspace.deleting",
140 "workspace.restored",
141 "user.deleting",
142 "user.restored",
143 ],
144 ),
145 // Usage rows that follow a renamed workspace or repository.
146 ("SUBSCRIBER_BILLING", &[]),
147 // Secret and dependency scans, and version update pull requests
148 // (security/src/lib.rs on_event).
149 (
150 "SUBSCRIBER_SECURITY",
151 &[
152 "git.push",
153 "pull.opened",
154 "pull.updated",
155 "pull.ready",
156 "pull.merged",
157 "pull.closed",
158 "comment.created",
159 "repo.created",
160 "repo.visibility_changed",
161 ],
162 ),
163 // The site-wide search index (search/src/index.rs).
164 ("SUBSCRIBER_SEARCH", &["git.push", "repo.*", "issue.*", "pull.*", "user.*", "workspace.*"]),
165 // Composer packages read from repositories, and packages that follow
166 // their workspace, team or repository (packages/src/lib.rs on_event).
167 (
168 "SUBSCRIBER_PACKAGES",
169 &[
170 "git.push",
171 "repo.visibility_changed",
172 "workspace.deleting",
173 "workspace.restored",
174 "team.edited",
175 "team.deleted",
176 ],
177 ),
178 // Pages that cite code a merge or a push to the default branch
179 // changed become possibly out of date, and projects' docs folders shown
180 // in Docs are read again (docs/src/staleness.ts, repo-spaces.ts).
181 ("SUBSCRIBER_DOCS", &["git.push", "pull.merged"]),
182];
183
184/// Whether `pattern` (see the module's notes) matches the type `kind`.
185fn matches(pattern: &str, kind: &str) -> bool {
186 match pattern.strip_suffix('*') {
187 Some(prefix) => kind.starts_with(prefix),
188 None => pattern == kind,
189 }
190}
191
192/// Whether the subscriber behind `binding` is sent events of type `kind`.
193pub fn routed(binding: &str, kind: &str) -> bool {
194 match ROUTES.iter().find(|(name, _)| *name == binding) {
195 Some((_, patterns)) => {
196 LIFECYCLE.contains(&kind) || patterns.iter().any(|pattern| matches(pattern, kind))
197 }
198 None => true,
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 /// Types published on the bus that hooks are not offered.
207 const UNOFFERED: [&str; 28] = [
208 "pull.mergecheck",
209 "pull.mergeability",
210 "deployment.review_requested",
211 "abuse.flagged",
212 "invite.created",
213 "invite.redeemed",
214 "user.updated",
215 "user.deleting",
216 "user.restored",
217 "user.deleted",
218 "user.email_added",
219 "workspace.updated",
220 "workspace.renamed",
221 "workspace.deleting",
222 "workspace.restored",
223 "workspace.deleted",
224 // Docs pages: no repository, so no repository's hooks.
225 "doc.page.created",
226 "doc.page.updated",
227 "doc.page.archived",
228 "doc.page.stale",
229 "doc.space.deleted",
230 // Artifacts (folios): no repository either, and a folio may be private.
231 "folio.created",
232 "folio.updated",
233 "folio.trashed",
234 "folio.restored",
235 "folio.deleted",
236 "folio.shared",
237 "folio.stale",
238 ];
239
240 fn published(kind: &str) -> bool {
241 crate::webhooks::EVENT_TYPES.contains(&kind) || UNOFFERED.contains(&kind)
242 }
243
244 #[test]
245 fn every_exact_route_names_a_type_that_is_published() {
246 for (binding, patterns) in ROUTES {
247 for pattern in patterns.iter().chain(LIFECYCLE.iter()) {
248 if pattern.ends_with('*') {
249 let prefix = pattern.trim_end_matches('*');
250 assert!(
251 prefix.is_empty()
252 || crate::webhooks::EVENT_TYPES.iter().chain(UNOFFERED.iter()).any(|kind| kind.starts_with(prefix)),
253 "{binding}: {pattern} matches nothing"
254 );
255 } else {
256 assert!(published(pattern), "{binding}: {pattern} is not a type anything publishes");
257 }
258 }
259 }
260 }
261
262 #[test]
263 fn every_route_is_a_subscriber_binding_named_once() {
264 let mut names: Vec<&str> = ROUTES.iter().map(|(name, _)| *name).collect();
265 assert!(names.iter().all(|name| name.starts_with("SUBSCRIBER_")));
266 let count = names.len();
267 names.sort_unstable();
268 names.dedup();
269 assert_eq!(names.len(), count);
270 assert_eq!(count, 13);
271 }
272
273 #[test]
274 fn subscribers_hear_what_they_act_on_and_not_the_rest() {
275 assert!(routed("SUBSCRIBER_WEBHOOKS", "secret_scanning_alert.created"));
276 assert!(routed("SUBSCRIBER_WEBHOOKS", "anything.new"));
277 assert!(routed("SUBSCRIBER_ACTIONS", "pull.labeled"));
278 assert!(routed("SUBSCRIBER_ACTIONS", "release.published"));
279 assert!(!routed("SUBSCRIBER_ACTIONS", "check_run.completed"));
280 assert!(routed("SUBSCRIBER_RUNNER", "comment.created"));
281 assert!(!routed("SUBSCRIBER_RUNNER", "status.created"));
282 assert!(routed("SUBSCRIBER_INTEGRATIONS", "issue.closed"));
283 assert!(routed("SUBSCRIBER_INTEGRATIONS", "issue.assigned"));
284 assert!(routed("SUBSCRIBER_INTEGRATIONS", "comment.created"));
285 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "git.push"));
286 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "check_run.completed"));
287 assert!(routed("SUBSCRIBER_BILLING", "workspace.renamed"));
288 assert!(!routed("SUBSCRIBER_BILLING", "git.push"));
289 assert!(routed("SUBSCRIBER_SEARCH", "user.updated"));
290 assert!(routed("SUBSCRIBER_REPOS", "user.deleting"));
291 assert!(!routed("SUBSCRIBER_REPOS", "git.push"));
292 assert!(routed("SUBSCRIBER_DOCS", "pull.merged"));
293 assert!(!routed("SUBSCRIBER_DOCS", "pull.opened"));
294 assert!(routed("SUBSCRIBER_PROJECTS", "deployment_status.created"));
295 assert!(!routed("SUBSCRIBER_PROJECTS", "deployment.created"));
296 // Every subscriber follows what moves or removes a repository.
297 for (binding, _) in ROUTES {
298 for kind in LIFECYCLE {
299 assert!(routed(binding, kind), "{binding} misses {kind}");
300 }
301 }
302 }
303
304 #[test]
305 fn a_subscriber_not_in_the_table_hears_everything() {
306 assert!(routed("SUBSCRIBER_NEW", "git.push"));
307 assert!(routed("SUBSCRIBER_NEW", "status.created"));
308 }
309
310 #[test]
311 fn a_family_matches_only_its_own_types() {
312 assert!(matches("pull.*", "pull.opened"));
313 assert!(!matches("pull.*", "pulls.opened"));
314 assert!(!matches("pull.*", "pull"));
315 assert!(matches("*", "x"));
316 assert!(matches("git.push", "git.push"));
317 assert!(!matches("git.push", "git.pushed"));
318 }
319}