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.

328 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.visibility_changed",
40 "repo.default_branch_changed",
41 "pull.merged",
42 "comment.created",
43 ],
44 ),
45 // Pull requests g1t is seeing through, mentions, queued work items and the
46 // merge queue (runner/src/index.ts queue).
47 (
48 "SUBSCRIBER_RUNNER",
49 &[
50 "pull.opened",
51 "pull.ready",
52 "pull.updated",
53 "pull.mergeability",
54 "pull.mergecheck",
55 "pull.merge_requested",
56 "pull.merged",
57 "pull.closed",
58 "checks.completed",
59 "queue.changed",
60 "comment.created",
61 "work_item.opened",
62 "work_item.updated",
63 "work_item.closed",
64 "repo.archived",
65 ],
66 ),
67 // Write-back to linked trackers (integrations/src lib.rs), and g1t
68 // taking on what it is assigned or asked in a thread (summon.rs).
69 // `work.migrated` rewrites links kept to issue numbers.
70 ("SUBSCRIBER_INTEGRATIONS", &["work_item.closed", "pull.opened", "work_item.assigned", "comment.created", "work.migrated"]),
71 // A hook may ask for any type, or for every one.
72 ("SUBSCRIBER_WEBHOOKS", &["*"]),
73 // Every event a workflow can run on (g1t_actions::events::github_events),
74 // and runs stopped in archived repositories.
75 (
76 "SUBSCRIBER_ACTIONS",
77 &[
78 "git.push",
79 "git.deleted",
80 "pull.*",
81 "work_item.*",
82 "comment.*",
83 "release.*",
84 "deployment.created",
85 "deployment_status.created",
86 "workflow.completed",
87 "repo.archived",
88 ],
89 ),
90 // Previews and production deploys (deployments/src/index.ts queue),
91 // and what each production deployment shipped (deployments/src/shipped.ts).
92 (
93 "SUBSCRIBER_DEPLOYMENTS",
94 &[
95 "git.push",
96 "deployment_status.created",
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 "work_item.opened",
118 "work_item.closed",
119 "work_item.reopened",
120 "pull.opened",
121 "pull.updated",
122 "pull.merged",
123 "pull.closed",
124 "comment.created",
125 "deployment.succeeded",
126 "deployment.failed",
127 "deployment_status.created",
128 "package.published",
129 "package.deleted",
130 "package.version_deleted",
131 "workspace.deleting",
132 "workspace.restored",
133 ],
134 ),
135 // Pull request working copies, workspaces' repositories, contributors
136 // shown as ghost (repos/src/lib.rs handle_event).
137 (
138 "SUBSCRIBER_REPOS",
139 &[
140 "pull.merged",
141 "pull.closed",
142 "pull.reopened",
143 "workspace.deleting",
144 "workspace.restored",
145 "user.deleting",
146 "user.restored",
147 ],
148 ),
149 // Usage rows that follow a renamed workspace or repository.
150 ("SUBSCRIBER_BILLING", &[]),
151 // Secret and dependency scans, and version update pull requests
152 // (security/src/lib.rs on_event).
153 (
154 "SUBSCRIBER_SECURITY",
155 &[
156 "git.push",
157 "pull.opened",
158 "pull.updated",
159 "pull.ready",
160 "pull.merged",
161 "pull.closed",
162 "comment.created",
163 "repo.created",
164 "repo.visibility_changed",
165 ],
166 ),
167 // The site-wide search index (search/src/index.rs).
168 ("SUBSCRIBER_SEARCH", &["git.push", "repo.*", "work_item.*", "work.migrated", "pull.*", "user.*", "workspace.*"]),
169 // Composer packages read from repositories, and packages that follow
170 // their workspace, team or repository (packages/src/lib.rs on_event).
171 (
172 "SUBSCRIBER_PACKAGES",
173 &[
174 "git.push",
175 "repo.visibility_changed",
176 "workspace.deleting",
177 "workspace.restored",
178 "team.edited",
179 "team.deleted",
180 ],
181 ),
182 // Pages that cite code a merge or a push to the default branch
183 // changed become possibly out of date, and projects' docs folders shown
184 // in Docs are read again (docs/src/staleness.ts, repo-spaces.ts).
185 ("SUBSCRIBER_DOCS", &["git.push", "pull.merged"]),
186];
187
188/// Whether `pattern` (see the module's notes) matches the type `kind`.
189fn matches(pattern: &str, kind: &str) -> bool {
190 match pattern.strip_suffix('*') {
191 Some(prefix) => kind.starts_with(prefix),
192 None => pattern == kind,
193 }
194}
195
196/// Whether the subscriber behind `binding` is sent events of type `kind`.
197pub fn routed(binding: &str, kind: &str) -> bool {
198 match ROUTES.iter().find(|(name, _)| *name == binding) {
199 Some((_, patterns)) => {
200 LIFECYCLE.contains(&kind) || patterns.iter().any(|pattern| matches(pattern, kind))
201 }
202 None => true,
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 /// Types published on the bus that hooks are not offered.
211 const UNOFFERED: [&str; 29] = [
212 // The one-off move of issues into work items.
213 "work.migrated",
214 "pull.mergecheck",
215 "pull.mergeability",
216 "deployment.review_requested",
217 "abuse.flagged",
218 "invite.created",
219 "invite.redeemed",
220 "user.updated",
221 "user.deleting",
222 "user.restored",
223 "user.deleted",
224 "user.email_added",
225 "workspace.updated",
226 "workspace.renamed",
227 "workspace.deleting",
228 "workspace.restored",
229 "workspace.deleted",
230 // Docs pages: no repository, so no repository's hooks.
231 "doc.page.created",
232 "doc.page.updated",
233 "doc.page.archived",
234 "doc.page.stale",
235 "doc.space.deleted",
236 // Artifacts (folios): no repository either, and a folio may be private.
237 "folio.created",
238 "folio.updated",
239 "folio.trashed",
240 "folio.restored",
241 "folio.deleted",
242 "folio.shared",
243 "folio.stale",
244 ];
245
246 fn published(kind: &str) -> bool {
247 crate::webhooks::EVENT_TYPES.contains(&kind) || UNOFFERED.contains(&kind)
248 }
249
250 #[test]
251 fn every_exact_route_names_a_type_that_is_published() {
252 for (binding, patterns) in ROUTES {
253 for pattern in patterns.iter().chain(LIFECYCLE.iter()) {
254 if pattern.ends_with('*') {
255 let prefix = pattern.trim_end_matches('*');
256 assert!(
257 prefix.is_empty()
258 || crate::webhooks::EVENT_TYPES.iter().chain(UNOFFERED.iter()).any(|kind| kind.starts_with(prefix)),
259 "{binding}: {pattern} matches nothing"
260 );
261 } else {
262 assert!(published(pattern), "{binding}: {pattern} is not a type anything publishes");
263 }
264 }
265 }
266 }
267
268 #[test]
269 fn every_route_is_a_subscriber_binding_named_once() {
270 let mut names: Vec<&str> = ROUTES.iter().map(|(name, _)| *name).collect();
271 assert!(names.iter().all(|name| name.starts_with("SUBSCRIBER_")));
272 let count = names.len();
273 names.sort_unstable();
274 names.dedup();
275 assert_eq!(names.len(), count);
276 assert_eq!(count, 13);
277 }
278
279 #[test]
280 fn subscribers_hear_what_they_act_on_and_not_the_rest() {
281 assert!(routed("SUBSCRIBER_WEBHOOKS", "secret_scanning_alert.created"));
282 assert!(routed("SUBSCRIBER_WEBHOOKS", "anything.new"));
283 assert!(routed("SUBSCRIBER_ACTIONS", "pull.labeled"));
284 assert!(routed("SUBSCRIBER_ACTIONS", "release.published"));
285 assert!(!routed("SUBSCRIBER_ACTIONS", "check_run.completed"));
286 assert!(routed("SUBSCRIBER_RUNNER", "comment.created"));
287 assert!(!routed("SUBSCRIBER_RUNNER", "status.created"));
288 assert!(routed("SUBSCRIBER_INTEGRATIONS", "work_item.closed"));
289 assert!(routed("SUBSCRIBER_INTEGRATIONS", "work_item.assigned"));
290 assert!(routed("SUBSCRIBER_INTEGRATIONS", "comment.created"));
291 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "git.push"));
292 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "check_run.completed"));
293 assert!(routed("SUBSCRIBER_BILLING", "workspace.renamed"));
294 assert!(!routed("SUBSCRIBER_BILLING", "git.push"));
295 assert!(routed("SUBSCRIBER_SEARCH", "user.updated"));
296 assert!(routed("SUBSCRIBER_REPOS", "user.deleting"));
297 assert!(!routed("SUBSCRIBER_REPOS", "git.push"));
298 assert!(routed("SUBSCRIBER_DOCS", "pull.merged"));
299 assert!(!routed("SUBSCRIBER_DOCS", "pull.opened"));
300 assert!(routed("SUBSCRIBER_PROJECTS", "deployment_status.created"));
301 assert!(!routed("SUBSCRIBER_PROJECTS", "deployment.created"));
302 // A production deployment's success writes down what it shipped.
303 assert!(routed("SUBSCRIBER_DEPLOYMENTS", "deployment_status.created"));
304 assert!(!routed("SUBSCRIBER_DEPLOYMENTS", "deployment.created"));
305 // Every subscriber follows what moves or removes a repository.
306 for (binding, _) in ROUTES {
307 for kind in LIFECYCLE {
308 assert!(routed(binding, kind), "{binding} misses {kind}");
309 }
310 }
311 }
312
313 #[test]
314 fn a_subscriber_not_in_the_table_hears_everything() {
315 assert!(routed("SUBSCRIBER_NEW", "git.push"));
316 assert!(routed("SUBSCRIBER_NEW", "status.created"));
317 }
318
319 #[test]
320 fn a_family_matches_only_its_own_types() {
321 assert!(matches("pull.*", "pull.opened"));
322 assert!(!matches("pull.*", "pulls.opened"));
323 assert!(!matches("pull.*", "pull"));
324 assert!(matches("*", "x"));
325 assert!(matches("git.push", "git.push"));
326 assert!(!matches("git.push", "git.pushed"));
327 }
328}