Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 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. | |
| 17 | const 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`]. | |
| 29 | pub 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 and write-back to linked trackers (integrations/src). | |
| 69 | ("SUBSCRIBER_INTEGRATIONS", &["git.push", "issue.closed", "pull.opened"]), | |
| 70 | // A hook may ask for any type, or for every one. | |
| 71 | ("SUBSCRIBER_WEBHOOKS", &["*"]), | |
| 72 | // Every event a workflow can run on (g1t_actions::events::github_events), | |
| 73 | // and runs stopped in archived repositories. | |
| 74 | ( | |
| 75 | "SUBSCRIBER_ACTIONS", | |
| 76 | &[ | |
| 77 | "git.push", | |
| 78 | "pull.*", | |
| 79 | "issue.*", | |
| 80 | "comment.*", | |
| 81 | "release.*", | |
| 82 | "deployment.created", | |
| 83 | "deployment_status.created", | |
| 84 | "review.completed", | |
| 85 | "workflow.completed", | |
| 86 | "repo.archived", | |
| 87 | ], | |
| 88 | ), | |
| 89 | // Previews and production deploys (deployments/src/index.ts queue). | |
| 90 | ( | |
| 91 | "SUBSCRIBER_DEPLOYMENTS", | |
| 92 | &[ | |
| 93 | "git.push", | |
| 94 | "branch.renamed", | |
| 95 | "repo.default_branch_changed", | |
| 96 | "pull.opened", | |
| 97 | "pull.ready", | |
| 98 | "pull.updated", | |
| 99 | "pull.reopened", | |
| 100 | "pull.closed", | |
| 101 | "pull.merged", | |
| 102 | "workspace.deleting", | |
| 103 | "workspace.restored", | |
| 104 | ], | |
| 105 | ), | |
| 106 | // Projects follow their repositories, and wake on activity | |
| 107 | // (projects/src/index.ts, activity.ts). | |
| 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 | "review.completed", | |
| 121 | "comment.created", | |
| 122 | "deployment.succeeded", | |
| 123 | "deployment.failed", | |
| 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 | "checks.completed", | |
| 159 | "comment.created", | |
| 160 | "repo.created", | |
| 161 | "repo.visibility_changed", | |
| 162 | ], | |
| 163 | ), | |
| 164 | // What agents are told about a repository (context/src/index.ts). | |
| 165 | ( | |
| 166 | "SUBSCRIBER_CONTEXT", | |
| 167 | &[ | |
| 168 | "git.push", | |
| 169 | "issue.opened", | |
| 170 | "issue.updated", | |
| 171 | "issue.closed", | |
| 172 | "pull.ready", | |
| 173 | "pull.merged", | |
| 174 | "memory.changed", | |
| 175 | ], | |
| 176 | ), | |
| 177 | // The site-wide search index (search/src/index.rs). | |
| 178 | ("SUBSCRIBER_SEARCH", &["git.push", "repo.*", "issue.*", "pull.*", "user.*", "workspace.*"]), | |
| 179 | // Composer packages read from repositories, and packages that follow | |
| 180 | // their workspace, team or repository (packages/src/lib.rs on_event). | |
| 181 | ( | |
| 182 | "SUBSCRIBER_PACKAGES", | |
| 183 | &[ | |
| 184 | "git.push", | |
| 185 | "repo.visibility_changed", | |
| 186 | "workspace.deleting", | |
| 187 | "workspace.restored", | |
| 188 | "team.edited", | |
| 189 | "team.deleted", | |
| 190 | ], | |
| 191 | ), | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 192 | // Pages that cite code a merge or a push to the default branch |
| 193 | // changed become possibly out of date, and projects' docs folders shown | |
| 194 | // in Docs are read again (docs/src/staleness.ts, repo-spaces.ts). | |
| 195 | ("SUBSCRIBER_DOCS", &["git.push", "pull.merged"]), | |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 196 | // Agents' routines that run on events: a pull request ready for |
| 197 | // review or merged, checks or a deploy failing, an issue opened | |
| @g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so. | 198 | // (agents/src/triggers.ts); pushes, which skill libraries that |
| 199 | // follow a repository read again (agents/src/skill-library.ts); and | |
| 200 | // a person joining a workspace, whom its @g1t welcomes | |
| 201 | // (agents/src/welcome.ts). | |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 202 | ( |
| 203 | "SUBSCRIBER_AGENTS", | |
| 204 | &[ | |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 205 | "git.push", |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 206 | "pull.opened", |
| 207 | "pull.ready", | |
| 208 | "pull.merged", | |
| 209 | "checks.completed", | |
| 210 | "issue.opened", | |
| 211 | "deployment.failed", | |
| @g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so. | 212 | "workspace.member_joined", |
| Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened | 213 | ], |
| 214 | ), | |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 215 | ]; |
| 216 | ||
| 217 | /// Whether `pattern` (see the module's notes) matches the type `kind`. | |
| 218 | fn matches(pattern: &str, kind: &str) -> bool { | |
| 219 | match pattern.strip_suffix('*') { | |
| 220 | Some(prefix) => kind.starts_with(prefix), | |
| 221 | None => pattern == kind, | |
| 222 | } | |
| 223 | } | |
| 224 | ||
| 225 | /// Whether the subscriber behind `binding` is sent events of type `kind`. | |
| 226 | pub fn routed(binding: &str, kind: &str) -> bool { | |
| 227 | match ROUTES.iter().find(|(name, _)| *name == binding) { | |
| 228 | Some((_, patterns)) => { | |
| 229 | LIFECYCLE.contains(&kind) || patterns.iter().any(|pattern| matches(pattern, kind)) | |
| 230 | } | |
| 231 | None => true, | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | #[cfg(test)] | |
| 236 | mod tests { | |
| 237 | use super::*; | |
| 238 | ||
| 239 | /// Types published on the bus that hooks are not offered. | |
| The artifacts plan records what Phase 1 decided, self-hosting and the deploy list name the g1t-folios index and the trash cron, and folio events are listed as published but never offered to webhooks. | 240 | const UNOFFERED: [&str; 28] = [ |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 241 | "pull.mergecheck", |
| 242 | "pull.mergeability", | |
| 243 | "deployment.review_requested", | |
| 244 | "memory.changed", | |
| 245 | "abuse.flagged", | |
| 246 | "invite.created", | |
| 247 | "invite.redeemed", | |
| 248 | "waitlist.requested", | |
| 249 | "user.updated", | |
| 250 | "user.deleting", | |
| 251 | "user.restored", | |
| 252 | "user.deleted", | |
| 253 | "user.email_added", | |
| 254 | "workspace.updated", | |
| 255 | "workspace.renamed", | |
| 256 | "workspace.deleting", | |
| 257 | "workspace.restored", | |
| 258 | "workspace.deleted", | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 259 | // Docs pages: no repository, so no repository's hooks. |
| 260 | "doc.page.created", | |
| 261 | "doc.page.updated", | |
| 262 | "doc.page.archived", | |
| 263 | "doc.page.stale", | |
| The artifacts plan records what Phase 1 decided, self-hosting and the deploy list name the g1t-folios index and the trash cron, and folio events are listed as published but never offered to webhooks. | 264 | // Artifacts (folios): no repository either, and a folio may be private. |
| 265 | "folio.created", | |
| 266 | "folio.updated", | |
| 267 | "folio.trashed", | |
| 268 | "folio.restored", | |
| 269 | "folio.shared", | |
| 270 | "folio.stale", | |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 271 | ]; |
| 272 | ||
| 273 | fn published(kind: &str) -> bool { | |
| 274 | crate::webhooks::EVENT_TYPES.contains(&kind) || UNOFFERED.contains(&kind) | |
| 275 | } | |
| 276 | ||
| 277 | #[test] | |
| 278 | fn every_exact_route_names_a_type_that_is_published() { | |
| 279 | for (binding, patterns) in ROUTES { | |
| 280 | for pattern in patterns.iter().chain(LIFECYCLE.iter()) { | |
| 281 | if pattern.ends_with('*') { | |
| 282 | let prefix = pattern.trim_end_matches('*'); | |
| 283 | assert!( | |
| 284 | prefix.is_empty() | |
| 285 | || crate::webhooks::EVENT_TYPES.iter().chain(UNOFFERED.iter()).any(|kind| kind.starts_with(prefix)), | |
| 286 | "{binding}: {pattern} matches nothing" | |
| 287 | ); | |
| 288 | } else { | |
| 289 | assert!(published(pattern), "{binding}: {pattern} is not a type anything publishes"); | |
| 290 | } | |
| 291 | } | |
| 292 | } | |
| 293 | } | |
| 294 | ||
| 295 | #[test] | |
| 296 | fn every_route_is_a_subscriber_binding_named_once() { | |
| 297 | let mut names: Vec<&str> = ROUTES.iter().map(|(name, _)| *name).collect(); | |
| 298 | assert!(names.iter().all(|name| name.starts_with("SUBSCRIBER_"))); | |
| 299 | let count = names.len(); | |
| 300 | names.sort_unstable(); | |
| 301 | names.dedup(); | |
| 302 | assert_eq!(names.len(), count); | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 303 | assert_eq!(count, 15); |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 304 | } |
| 305 | ||
| 306 | #[test] | |
| 307 | fn subscribers_hear_what_they_act_on_and_not_the_rest() { | |
| 308 | assert!(routed("SUBSCRIBER_WEBHOOKS", "secret_scanning_alert.created")); | |
| 309 | assert!(routed("SUBSCRIBER_WEBHOOKS", "anything.new")); | |
| 310 | assert!(routed("SUBSCRIBER_ACTIONS", "pull.labeled")); | |
| 311 | assert!(routed("SUBSCRIBER_ACTIONS", "release.published")); | |
| 312 | assert!(!routed("SUBSCRIBER_ACTIONS", "check_run.completed")); | |
| 313 | assert!(routed("SUBSCRIBER_RUNNER", "comment.created")); | |
| 314 | assert!(!routed("SUBSCRIBER_RUNNER", "status.created")); | |
| 315 | assert!(routed("SUBSCRIBER_BILLING", "workspace.renamed")); | |
| 316 | assert!(!routed("SUBSCRIBER_BILLING", "git.push")); | |
| 317 | assert!(routed("SUBSCRIBER_SEARCH", "user.updated")); | |
| 318 | assert!(routed("SUBSCRIBER_REPOS", "user.deleting")); | |
| 319 | assert!(!routed("SUBSCRIBER_REPOS", "git.push")); | |
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 320 | assert!(routed("SUBSCRIBER_DOCS", "pull.merged")); |
| 321 | assert!(!routed("SUBSCRIBER_DOCS", "pull.opened")); | |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 322 | assert!(routed("SUBSCRIBER_AGENTS", "git.push")); |
| @g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so. | 323 | assert!(routed("SUBSCRIBER_AGENTS", "workspace.member_joined")); |
| Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how. | 324 | assert!(!routed("SUBSCRIBER_AGENTS", "comment.created")); |
| @g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so. | 325 | assert!(!routed("SUBSCRIBER_AGENTS", "workspace.updated")); |
| Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails | 326 | // Every subscriber follows what moves or removes a repository. |
| 327 | for (binding, _) in ROUTES { | |
| 328 | for kind in LIFECYCLE { | |
| 329 | assert!(routed(binding, kind), "{binding} misses {kind}"); | |
| 330 | } | |
| 331 | } | |
| 332 | } | |
| 333 | ||
| 334 | #[test] | |
| 335 | fn a_subscriber_not_in_the_table_hears_everything() { | |
| 336 | assert!(routed("SUBSCRIBER_NEW", "git.push")); | |
| 337 | assert!(routed("SUBSCRIBER_NEW", "status.created")); | |
| 338 | } | |
| 339 | ||
| 340 | #[test] | |
| 341 | fn a_family_matches_only_its_own_types() { | |
| 342 | assert!(matches("pull.*", "pull.opened")); | |
| 343 | assert!(!matches("pull.*", "pulls.opened")); | |
| 344 | assert!(!matches("pull.*", "pull")); | |
| 345 | assert!(matches("*", "x")); | |
| 346 | assert!(matches("git.push", "git.push")); | |
| 347 | assert!(!matches("git.push", "git.pushed")); | |
| 348 | } | |
| 349 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.