Skip to content
359 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 // Memory captured from merges and comments, mentions, pull request
31 // bases, runs stopped in archived repositories (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 "review.completed",
59 "queue.changed",
60 "comment.created",
61 "issue.opened",
62 "issue.updated",
63 "issue.closed",
64 "agent.asked",
65 "repo.archived",
66 ],
67 ),
68 // Push mirrors, write-back to linked trackers, and commit statuses
69 // reported to a GitHub mirror's commits (integrations/src lib.rs,
70 // remotes.rs on_status).
71 ("SUBSCRIBER_INTEGRATIONS", &["git.push", "issue.closed", "pull.opened", "status.created"]),
72 // A hook may ask for any type, or for every one.
73 ("SUBSCRIBER_WEBHOOKS", &["*"]),
74 // Every event a workflow can run on (g1t_actions::events::github_events),
75 // and runs stopped in archived repositories.
76 (
77 "SUBSCRIBER_ACTIONS",
78 &[
79 "git.push",
80 "git.deleted",
81 "pull.*",
82 "issue.*",
83 "comment.*",
84 "release.*",
85 "deployment.created",
86 "deployment_status.created",
87 "review.completed",
88 "workflow.completed",
89 "repo.archived",
90 ],
91 ),
92 // Previews and production deploys (deployments/src/index.ts queue).
93 (
94 "SUBSCRIBER_DEPLOYMENTS",
95 &[
96 "git.push",
97 "branch.renamed",
98 "repo.default_branch_changed",
99 "pull.opened",
100 "pull.ready",
101 "pull.updated",
102 "pull.reopened",
103 "pull.closed",
104 "pull.merged",
105 "workspace.deleting",
106 "workspace.restored",
107 ],
108 ),
109 // Projects follow their repositories, and wake on activity
110 // (projects/src/index.ts, activity.ts). A production deployment
111 // reported from elsewhere asks for its screenshot.
112 (
113 "SUBSCRIBER_PROJECTS",
114 &[
115 "git.push",
116 "repo.*",
117 "issue.opened",
118 "issue.closed",
119 "issue.reopened",
120 "pull.opened",
121 "pull.updated",
122 "pull.merged",
123 "pull.closed",
124 "review.completed",
125 "comment.created",
126 "deployment.succeeded",
127 "deployment.failed",
128 "deployment_status.created",
129 "package.published",
130 "package.deleted",
131 "package.version_deleted",
132 "workspace.deleting",
133 "workspace.restored",
134 ],
135 ),
136 // Pull request working copies, workspaces' repositories, contributors
137 // shown as ghost (repos/src/lib.rs handle_event).
138 (
139 "SUBSCRIBER_REPOS",
140 &[
141 "pull.merged",
142 "pull.closed",
143 "pull.reopened",
144 "workspace.deleting",
145 "workspace.restored",
146 "user.deleting",
147 "user.restored",
148 ],
149 ),
150 // Usage rows that follow a renamed workspace or repository.
151 ("SUBSCRIBER_BILLING", &[]),
152 // Secret and dependency scans, and version update pull requests
153 // (security/src/lib.rs on_event).
154 (
155 "SUBSCRIBER_SECURITY",
156 &[
157 "git.push",
158 "pull.opened",
159 "pull.updated",
160 "pull.ready",
161 "pull.merged",
162 "pull.closed",
163 "comment.created",
164 "repo.created",
165 "repo.visibility_changed",
166 ],
167 ),
168 // What agents are told about a repository (context/src/index.ts).
169 (
170 "SUBSCRIBER_CONTEXT",
171 &[
172 "git.push",
173 "issue.opened",
174 "issue.updated",
175 "issue.closed",
176 "pull.ready",
177 "pull.merged",
178 "memory.changed",
179 ],
180 ),
181 // The site-wide search index (search/src/index.rs).
182 ("SUBSCRIBER_SEARCH", &["git.push", "repo.*", "issue.*", "pull.*", "user.*", "workspace.*"]),
183 // Composer packages read from repositories, and packages that follow
184 // their workspace, team or repository (packages/src/lib.rs on_event).
185 (
186 "SUBSCRIBER_PACKAGES",
187 &[
188 "git.push",
189 "repo.visibility_changed",
190 "workspace.deleting",
191 "workspace.restored",
192 "team.edited",
193 "team.deleted",
194 ],
195 ),
196 // Pages that cite code a merge or a push to the default branch
197 // changed become possibly out of date, and projects' docs folders shown
198 // in Docs are read again (docs/src/staleness.ts, repo-spaces.ts).
199 ("SUBSCRIBER_DOCS", &["git.push", "pull.merged"]),
200 // Agents' routines that run on events: a pull request ready for
201 // review or merged, checks or a deploy failing, an issue opened
202 // (agents/src/triggers.ts); pushes, which skill libraries that
203 // follow a repository read again (agents/src/skill-library.ts); and
204 // a person joining a workspace, whom its @g1t welcomes
205 // (agents/src/welcome.ts).
206 (
207 "SUBSCRIBER_AGENTS",
208 &[
209 "git.push",
210 "pull.opened",
211 "pull.ready",
212 "pull.merged",
213 "checks.completed",
214 "issue.opened",
215 "deployment.failed",
216 "workspace.member_joined",
217 ],
218 ),
219];
220
221/// Whether `pattern` (see the module's notes) matches the type `kind`.
222fn matches(pattern: &str, kind: &str) -> bool {
223 match pattern.strip_suffix('*') {
224 Some(prefix) => kind.starts_with(prefix),
225 None => pattern == kind,
226 }
227}
228
229/// Whether the subscriber behind `binding` is sent events of type `kind`.
230pub fn routed(binding: &str, kind: &str) -> bool {
231 match ROUTES.iter().find(|(name, _)| *name == binding) {
232 Some((_, patterns)) => {
233 LIFECYCLE.contains(&kind) || patterns.iter().any(|pattern| matches(pattern, kind))
234 }
235 None => true,
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 /// Types published on the bus that hooks are not offered.
244 const UNOFFERED: [&str; 30] = [
245 "pull.mergecheck",
246 "pull.mergeability",
247 "deployment.review_requested",
248 "memory.changed",
249 "abuse.flagged",
250 "invite.created",
251 "invite.redeemed",
252 "waitlist.requested",
253 "user.updated",
254 "user.deleting",
255 "user.restored",
256 "user.deleted",
257 "user.email_added",
258 "workspace.updated",
259 "workspace.renamed",
260 "workspace.deleting",
261 "workspace.restored",
262 "workspace.deleted",
263 // Docs pages: no repository, so no repository's hooks.
264 "doc.page.created",
265 "doc.page.updated",
266 "doc.page.archived",
267 "doc.page.stale",
268 "doc.space.deleted",
269 // Artifacts (folios): no repository either, and a folio may be private.
270 "folio.created",
271 "folio.updated",
272 "folio.trashed",
273 "folio.restored",
274 "folio.deleted",
275 "folio.shared",
276 "folio.stale",
277 ];
278
279 fn published(kind: &str) -> bool {
280 crate::webhooks::EVENT_TYPES.contains(&kind) || UNOFFERED.contains(&kind)
281 }
282
283 #[test]
284 fn every_exact_route_names_a_type_that_is_published() {
285 for (binding, patterns) in ROUTES {
286 for pattern in patterns.iter().chain(LIFECYCLE.iter()) {
287 if pattern.ends_with('*') {
288 let prefix = pattern.trim_end_matches('*');
289 assert!(
290 prefix.is_empty()
291 || crate::webhooks::EVENT_TYPES.iter().chain(UNOFFERED.iter()).any(|kind| kind.starts_with(prefix)),
292 "{binding}: {pattern} matches nothing"
293 );
294 } else {
295 assert!(published(pattern), "{binding}: {pattern} is not a type anything publishes");
296 }
297 }
298 }
299 }
300
301 #[test]
302 fn every_route_is_a_subscriber_binding_named_once() {
303 let mut names: Vec<&str> = ROUTES.iter().map(|(name, _)| *name).collect();
304 assert!(names.iter().all(|name| name.starts_with("SUBSCRIBER_")));
305 let count = names.len();
306 names.sort_unstable();
307 names.dedup();
308 assert_eq!(names.len(), count);
309 assert_eq!(count, 15);
310 }
311
312 #[test]
313 fn subscribers_hear_what_they_act_on_and_not_the_rest() {
314 assert!(routed("SUBSCRIBER_WEBHOOKS", "secret_scanning_alert.created"));
315 assert!(routed("SUBSCRIBER_WEBHOOKS", "anything.new"));
316 assert!(routed("SUBSCRIBER_ACTIONS", "pull.labeled"));
317 assert!(routed("SUBSCRIBER_ACTIONS", "release.published"));
318 assert!(!routed("SUBSCRIBER_ACTIONS", "check_run.completed"));
319 assert!(routed("SUBSCRIBER_RUNNER", "comment.created"));
320 assert!(!routed("SUBSCRIBER_RUNNER", "status.created"));
321 assert!(routed("SUBSCRIBER_INTEGRATIONS", "status.created"));
322 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "check_run.completed"));
323 assert!(routed("SUBSCRIBER_BILLING", "workspace.renamed"));
324 assert!(!routed("SUBSCRIBER_BILLING", "git.push"));
325 assert!(routed("SUBSCRIBER_SEARCH", "user.updated"));
326 assert!(routed("SUBSCRIBER_REPOS", "user.deleting"));
327 assert!(!routed("SUBSCRIBER_REPOS", "git.push"));
328 assert!(routed("SUBSCRIBER_DOCS", "pull.merged"));
329 assert!(!routed("SUBSCRIBER_DOCS", "pull.opened"));
330 assert!(routed("SUBSCRIBER_AGENTS", "git.push"));
331 assert!(routed("SUBSCRIBER_AGENTS", "workspace.member_joined"));
332 assert!(!routed("SUBSCRIBER_AGENTS", "comment.created"));
333 assert!(!routed("SUBSCRIBER_AGENTS", "workspace.updated"));
334 assert!(routed("SUBSCRIBER_PROJECTS", "deployment_status.created"));
335 assert!(!routed("SUBSCRIBER_PROJECTS", "deployment.created"));
336 // Every subscriber follows what moves or removes a repository.
337 for (binding, _) in ROUTES {
338 for kind in LIFECYCLE {
339 assert!(routed(binding, kind), "{binding} misses {kind}");
340 }
341 }
342 }
343
344 #[test]
345 fn a_subscriber_not_in_the_table_hears_everything() {
346 assert!(routed("SUBSCRIBER_NEW", "git.push"));
347 assert!(routed("SUBSCRIBER_NEW", "status.created"));
348 }
349
350 #[test]
351 fn a_family_matches_only_its_own_types() {
352 assert!(matches("pull.*", "pull.opened"));
353 assert!(!matches("pull.*", "pulls.opened"));
354 assert!(!matches("pull.*", "pull"));
355 assert!(matches("*", "x"));
356 assert!(matches("git.push", "git.push"));
357 assert!(!matches("git.push", "git.pushed"));
358 }
359}