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.

1,624 linesCodeBlameRaw
1//! Events published on the bus, and the events service that carries them.
2//! Mirrors `packages/contracts/src/events.ts`.
3
4use serde::Serialize;
5
6/// The key in an event's `data` that marks what a workflow job's own token
7/// (`G1T_TOKEN`) did, with the run's id as its value. The actions service
8/// starts no workflows for such an event, as GitHub starts none for what
9/// its `GITHUB_TOKEN` does, so a workflow cannot set itself off.
10pub const CAUSED_BY_JOB: &str = "causedByJob";
11
12/// The run whose job's token caused an event, if one did.
13pub fn caused_by_job(data: &serde_json::Value) -> Option<&str> {
14 data[CAUSED_BY_JOB].as_str().filter(|run| !run.is_empty())
15}
16
17/// The run whose job's token `actor` is acting with, if it is one.
18pub fn job_run_of(actor: &crate::User) -> Option<&str> {
19 actor.token.as_deref().and_then(|token| token.job.as_ref()).map(|job| job.run_id.as_str())
20}
21
22/// `data` as JSON, marked as a workflow job's doing when `actor` acted
23/// with a job's token (see [`CAUSED_BY_JOB`]).
24pub fn marked<T: Serialize>(data: T, actor: Option<&crate::User>) -> serde_json::Value {
25 let mut value = serde_json::to_value(data).unwrap_or(serde_json::Value::Null);
26 if let (Some(run), serde_json::Value::Object(map)) = (actor.and_then(job_run_of), &mut value) {
27 map.insert(CAUSED_BY_JOB.to_owned(), serde_json::Value::String(run.to_owned()));
28 }
29 value
30}
31
32/// `data` as JSON, marked as a workflow job's doing when the event it
33/// follows from (`cause`, its data) was: a push by a job's token moves its
34/// pull request, and that starts no workflows either.
35pub fn carried<T: Serialize>(data: T, cause: &serde_json::Value) -> serde_json::Value {
36 let mut value = serde_json::to_value(data).unwrap_or(serde_json::Value::Null);
37 if let (Some(run), serde_json::Value::Object(map)) = (caused_by_job(cause), &mut value) {
38 map.insert(CAUSED_BY_JOB.to_owned(), serde_json::Value::String(run.to_owned()));
39 }
40 value
41}
42
43/// What a publisher supplies; the bus fills in the id and time.
44#[derive(Debug, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct NewEvent<T: Serialize> {
47 #[serde(rename = "type")]
48 pub kind: &'static str,
49 /// The service that published it.
50 pub source: &'static str,
51 /// The repo the event concerns.
52 pub repo_id: Option<String>,
53 /// The user or agent that caused it, if any.
54 pub actor: Option<String>,
55 pub data: T,
56}
57
58#[derive(Debug, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct RepoCreated {
61 pub repo_id: String,
62 pub namespace: String,
63 pub name: String,
64 pub is_private: bool,
65 /// The repository it was forked from, when it is a fork.
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub parent_id: Option<String>,
68}
69
70/// The payload of `repo.collaborator_added`, `repo.collaborator_removed`
71/// and `repo.collaborator_role_changed`: a person's own role on one
72/// repository (see `access`). `role` is the role they have now (null once
73/// removed); `previous_role` what they had before (null when added).
74#[derive(Debug, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct RepoCollaborator {
77 pub repo_id: String,
78 pub namespace: String,
79 pub name: String,
80 pub username: String,
81 pub role: Option<crate::access::RepoRole>,
82 pub previous_role: Option<crate::access::RepoRole>,
83}
84
85#[derive(Debug, Serialize)]
86#[serde(rename_all = "camelCase")]
87pub struct RepoForked {
88 pub repo_id: String,
89 pub source_repo_id: String,
90 /// The pull request a working copy was made for; absent for a fork
91 /// someone made into a workspace.
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub pull_id: Option<String>,
94}
95
96/// The payload of `repo.detached`: a fork left its fork network, because
97/// it was detached or its parent changed who can see it.
98#[derive(Debug, Serialize)]
99#[serde(rename_all = "camelCase")]
100pub struct RepoDetached {
101 pub repo_id: String,
102 pub parent_id: String,
103}
104
105/// One branch or tag moved by a push. `after` is the commit it points to now.
106#[derive(Debug, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct GitPush {
109 pub repo_id: String,
110 /// The full ref, such as `refs/heads/main`.
111 #[serde(rename = "ref")]
112 pub git_ref: String,
113 /// Where it pointed before; absent for a new branch or tag.
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub before: Option<String>,
116 pub after: String,
117 /// Whether the ref is the repository's default branch.
118 pub default_branch: bool,
119 /// Set when the push was too large to scan for secrets before it was
120 /// stored, and was let through: the security service scans
121 /// `before..after` after it landed. Absent otherwise.
122 #[serde(skip_serializing_if = "std::ops::Not::not")]
123 pub unscanned: bool,
124 /// Set when a workflow job's token pushed: the run's id (see
125 /// [`CAUSED_BY_JOB`]). Absent otherwise.
126 #[serde(rename = "causedByJob", skip_serializing_if = "Option::is_none")]
127 pub caused_by_job: Option<String>,
128 /// How many commits the push brought to the branch, counted along its
129 /// first-parent line from `after` back to `before` (a merge commit is
130 /// one), at most 50; one for a new branch. Absent when it was not
131 /// counted: a tag, a branch deleted, a pull request landed, a push the
132 /// store could not read back, or one from before this was recorded.
133 /// Home's digest (`activity_digest`) sums it.
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub commits: Option<u32>,
136 /// Set when landing a pull request moved the branch (repos `land`): its
137 /// number, so that the pull requests the push merged are told apart
138 /// from the one whose merge is recorded by whoever landed it.
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub landed_pull: Option<u32>,
141 /// Set when an agent pushed on the person's behalf (a session g1t
142 /// started for them): the agent's name. The event's actor is still the
143 /// person; what the push closes is written as the agent's, for them.
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub agent: Option<String>,
146}
147
148/// A branch or tag deleted by a push. `before` is the commit it pointed to.
149#[derive(Debug, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct GitDeleted {
152 pub repo_id: String,
153 /// The full ref, such as `refs/heads/feature`.
154 #[serde(rename = "ref")]
155 pub git_ref: String,
156 pub before: String,
157 /// Set when a workflow job's token deleted it: the run's id (see
158 /// [`CAUSED_BY_JOB`]). Absent otherwise.
159 #[serde(rename = "causedByJob", skip_serializing_if = "Option::is_none")]
160 pub caused_by_job: Option<String>,
161}
162
163/// The work item events the work service publishes. Each one's payload is
164/// a [`WorkItemEvent`]; the event's `repoId` is the work item's origin,
165/// when it has one.
166pub const WORK_ITEM_EVENTS: [&str; 13] = [
167 "work_item.opened",
168 "work_item.updated",
169 "work_item.assigned",
170 "work_item.labeled",
171 "work_item.unlabeled",
172 "work_item.milestoned",
173 "work_item.demilestoned",
174 "work_item.planned",
175 "work_item.closed",
176 "work_item.reopened",
177 "work_item.linked",
178 "work_item.unlinked",
179 "work_item.landed",
180];
181
182/// The payload of every `work_item.*` event ([`WORK_ITEM_EVENTS`]); each
183/// uses the fields that apply to it. A repository's subscribers hear of
184/// work items linked to it (`repo_ids`); a workspace's, of all of its.
185#[derive(Debug, Default, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct WorkItemEvent {
188 pub item_id: String,
189 /// The workspace's slug.
190 pub workspace: String,
191 pub number: u32,
192 /// Every repository it is linked to, its origin first.
193 pub repo_ids: Vec<String>,
194 /// Who opened it: g1t, for one its agent filed while at work.
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub author: Option<crate::credentials::Principal>,
197 /// For one g1t's agent filed: the person it was working for.
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub requested_by: Option<crate::credentials::Principal>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub title: Option<String>,
202 /// On close: `completed` or `not_planned`.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub reason: Option<&'static str>,
205 /// On `work_item.landed` and on a close a landing caused: what landed,
206 /// in which repository.
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub resolution: Option<crate::work::Resolution>,
209 /// On `work_item.landed` and on close: the targets it has not landed
210 /// in yet, by path. Empty on close.
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub missing: Option<Vec<String>>,
213 /// On close: the session it was closed in, when it was: the one the
214 /// closing commit names (`G1t-Session: ses_…`), or the caller's own
215 /// session on the work item when it closed it with a token.
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub session: Option<String>,
218 /// On `work_item.assigned`: the people it is now assigned to.
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub assignees: Option<Vec<String>>,
221 /// On `work_item.assigned`: those of them who were not before.
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub added: Option<Vec<String>>,
224 /// On `work_item.labeled` and `work_item.unlabeled`: the label put on
225 /// or taken off. One event for each.
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub label: Option<EventLabel>,
228 /// On `work_item.milestoned`: the milestone it was put in; on
229 /// `work_item.demilestoned`, the one it was taken out of.
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub milestone: Option<crate::work::MilestoneRef>,
232 /// On `work_item.linked` and `work_item.unlinked`: the repository
233 /// (`owner/name`) and how it is linked now, or was.
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub repo: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub role: Option<crate::work::LinkRole>,
238 /// On `work_item.planned`: where it stands in the plan now.
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub horizon: Option<crate::work::Horizon>,
241 /// On `work_item.planned`, when it moved to another horizon: where it
242 /// was.
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub from_horizon: Option<crate::work::Horizon>,
245 /// On `work_item.planned`: how much the work is worth, if said.
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub appetite: Option<crate::work::Appetite>,
248 /// On `work_item.planned`: whether it is urgent.
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub urgent: Option<bool>,
251}
252
253/// `work.migrated`: a page of where repositories' issues went when they
254/// became work items, for services that stored issue numbers to rewrite
255/// them. Sent once per page by the migration job, then with `done`.
256#[derive(Clone, Debug, Default, Serialize, serde::Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct WorkMigrated {
259 /// Counts from 1.
260 pub page: u32,
261 pub items: Vec<crate::work::MigratedItem>,
262 /// The last page.
263 pub done: bool,
264}
265
266/// A label, as `work_item.labeled`, `pull.labeled` and their `unlabeled`
267/// say.
268#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
269pub struct EventLabel {
270 pub name: String,
271 /// Six hex digits.
272 pub color: String,
273}
274
275/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
276/// moved), `pull.closed`, `pull.reopened`, `pull.converted_to_draft`,
277/// `pull.merged`, `pull.assigned`,
278/// `pull.review_requested` and `pull.review_request_removed` (reviewers
279/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
280/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
281/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
282/// through until a person steps in) and `pull.resumed` (it picked back
283/// up); each uses the fields that apply to it.
284#[derive(Debug, Default, Serialize)]
285#[serde(rename_all = "camelCase")]
286pub struct PullEvent {
287 pub pull_id: String,
288 pub repo_id: String,
289 pub number: u32,
290 /// Who opened it: g1t, for a change g1t made.
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub author: Option<crate::credentials::Principal>,
293 /// For a change g1t made: the person who asked for it.
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub requested_by: Option<crate::credentials::Principal>,
296 /// The number of the work item it is for, in its workspace.
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub item: Option<u32>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub agent: Option<String>,
301 /// On merge: the commit the branch now points to. On update, when
302 /// marked ready and when reopened: the head of the change.
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub commit: Option<String>,
305 /// On close: the pull request that was merged instead.
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub superseded_by: Option<u32>,
308 /// On `pull.assigned`: the people it is now assigned to.
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub assignees: Option<Vec<String>>,
311 /// On `pull.assigned`: those newly assigned.
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub added: Option<Vec<String>>,
314 /// On `pull.review_requested`: the reviewers newly asked; on
315 /// `pull.review_request_removed`, those no longer asked.
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub reviewers: Option<Vec<String>>,
318 /// On `pull.review_requested` and `pull.review_request_removed`: the
319 /// teams newly asked, or no longer, each with the people it asks.
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub teams: Option<Vec<TeamRequested>>,
322 /// On `pull.review_requested`: asked because they own files it changes
323 /// (its CODEOWNERS file), not by a person.
324 #[serde(skip_serializing_if = "std::ops::Not::not")]
325 pub code_owners: bool,
326 /// On `pull.stalled`: why g1t stopped, and what would start it again.
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub detail: Option<String>,
329 /// On `pull.auto_merge_disabled`: the username of who had turned
330 /// auto-merge on, who is told it is off.
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub auto_merge_by: Option<String>,
333 /// On `pull.auto_merge_disabled`: g1t turned it off because the pull
334 /// request joined the merge queue, which lands it. Nothing is left for
335 /// anyone to do, so nobody's inbox is told.
336 #[serde(skip_serializing_if = "std::ops::Not::not")]
337 pub queued: bool,
338 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub label: Option<EventLabel>,
341 /// On `pull.milestoned`: the milestone it was put in; on
342 /// `pull.demilestoned`, the one it was taken out of.
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub milestone: Option<crate::work::MilestoneRef>,
345 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub base: Option<String>,
348}
349
350/// A team asked to review a pull request.
351#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct TeamRequested {
354 /// `workspace/slug`.
355 pub team: String,
356 /// Everyone in it who is told: the whole team, or, with review
357 /// assignment, the people picked (and the rest when it says to tell
358 /// them). Never the pull request's author.
359 pub notified: Vec<String>,
360 /// With review assignment: the people picked, who are asked as
361 /// reviewers themselves.
362 #[serde(default)]
363 pub assigned: Vec<String>,
364}
365
366/// The payload of every `team.*` event: `team.created`, `team.edited`,
367/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
368/// `team.member_removed` (with `username`, `role` and `previousRole`);
369/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
370/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
371/// name the repository as the event's own.
372#[derive(Clone, Debug, Default, Serialize)]
373#[serde(rename_all = "camelCase")]
374pub struct TeamChanged {
375 pub workspace: String,
376 pub team_id: String,
377 /// The team's slug, as it is now.
378 pub team: String,
379 pub name: String,
380 pub visibility: Option<crate::teams::TeamVisibility>,
381 /// The parent's slug.
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub parent: Option<String>,
384 /// On `team.edited`: what changed, such as `name` or `parent`.
385 #[serde(skip_serializing_if = "Vec::is_empty")]
386 pub changes: Vec<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub username: Option<String>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub role: Option<crate::teams::TeamRole>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub previous_role: Option<crate::teams::TeamRole>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub repo_id: Option<String>,
395 /// `workspace/name`.
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub repo: Option<String>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub repo_role: Option<crate::access::RepoRole>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub previous_repo_role: Option<crate::access::RepoRole>,
402}
403
404/// `deployment.succeeded` and `deployment.failed`: a build of a project
405/// finished, for production or for one pull request's preview.
406#[derive(Debug, Serialize)]
407#[serde(rename_all = "camelCase")]
408pub struct DeploymentEvent {
409 pub deployment_id: String,
410 pub project_id: String,
411 pub repo_id: String,
412 pub workspace: String,
413 /// The project's slug.
414 pub project: String,
415 /// `production` or `preview`.
416 pub kind: String,
417 pub branch: Option<String>,
418 /// For a preview: its pull request.
419 pub number: Option<u32>,
420 pub commit: String,
421 /// Where the deployment is on the site, such as
422 /// `/acme/rocket/deployments/dpl_1`.
423 pub path: String,
424 /// For a failure: what went wrong.
425 pub error: Option<String>,
426 /// For a success: whether the deployment before it, of the same app, failed.
427 pub recovered: bool,
428 /// Who started it, by username, or `g1t`.
429 pub triggered_by: String,
430}
431
432/// `checks.completed`: a run of a work item's acceptance checks against a pull
433/// request finished.
434#[derive(Debug, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub struct ChecksEvent {
437 pub pull_id: String,
438 pub repo_id: String,
439 pub number: u32,
440 /// `passed`, `failed` or `errored`.
441 pub status: &'static str,
442 /// The commit that was checked.
443 pub commit: String,
444}
445
446/// `workflow.completed`: a GitHub Actions run finished.
447#[derive(Debug, Serialize)]
448#[serde(rename_all = "camelCase")]
449pub struct WorkflowEvent {
450 pub run_id: String,
451 pub repo_id: String,
452 /// The workflow's name, and its file.
453 pub workflow: String,
454 pub path: String,
455 /// The run's number among the workflow's runs.
456 pub number: u64,
457 /// The GitHub event that started it, such as `push`.
458 pub event: String,
459 /// `success`, `failure`, `cancelled` or `skipped`.
460 pub conclusion: String,
461 #[serde(rename = "ref")]
462 pub git_ref: String,
463 pub sha: String,
464 /// The pull request it ran for, if any.
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub pull: Option<u32>,
467}
468
469/// Where a comment is, as the `comment.*` events say: a pull request of a
470/// repository (`repo_id`, `pull_id`), or a work item of a workspace
471/// (`workspace`, `item_id`, and the repositories it is linked to in
472/// `repo_ids`). `number` is the pull request's or the work item's.
473#[derive(Debug, Default, Serialize)]
474#[serde(rename_all = "camelCase")]
475pub struct CommentOn {
476 /// The pull request's repository; a work item's origin, if any.
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub repo_id: Option<String>,
479 pub number: u32,
480 /// Set when the comment is on a pull request.
481 #[serde(skip_serializing_if = "Option::is_none")]
482 pub pull_id: Option<String>,
483 /// Set when the comment is on a work item.
484 #[serde(skip_serializing_if = "Option::is_none")]
485 pub item_id: Option<String>,
486 #[serde(skip_serializing_if = "Option::is_none")]
487 pub workspace: Option<String>,
488 #[serde(skip_serializing_if = "Vec::is_empty")]
489 pub repo_ids: Vec<String>,
490}
491
492/// `comment.created`.
493#[derive(Debug, Default, Serialize)]
494#[serde(rename_all = "camelCase")]
495pub struct CommentCreated {
496 pub comment_id: String,
497 #[serde(flatten)]
498 pub on: CommentOn,
499 /// Set when the comment is a review: approve or request changes.
500 #[serde(skip_serializing_if = "Option::is_none")]
501 pub verdict: Option<crate::work::Verdict>,
502 /// Set when one of the workspace's agents wrote it, as itself. The
503 /// event's actor is then the person it acted for (`actingFor`).
504 #[serde(skip_serializing_if = "Option::is_none")]
505 pub agent: Option<crate::work::AgentRef>,
506 /// Who the agent acted for, set with `agent`.
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub acting_for: Option<crate::credentials::Principal>,
509 /// An agent's review, advisory: its verdict counts toward nothing.
510 #[serde(skip_serializing_if = "std::ops::Not::not")]
511 pub advisory: bool,
512}
513
514/// `comment.edited`: a comment's text changed. `changes.body.from` is what
515/// it said before; the comment as it is now is read by `commentId`.
516#[derive(Debug, Serialize)]
517#[serde(rename_all = "camelCase")]
518pub struct CommentEdited {
519 pub comment_id: String,
520 #[serde(flatten)]
521 pub on: CommentOn,
522 pub changes: CommentChanges,
523}
524
525/// What an edit changed, as `comment.edited` says it.
526#[derive(Debug, Serialize)]
527pub struct CommentChanges {
528 pub body: ChangedFrom,
529}
530
531/// A field's value before a change.
532#[derive(Debug, Serialize)]
533pub struct ChangedFrom {
534 pub from: String,
535}
536
537/// `comment.deleted`: a comment was deleted. It no longer exists when the
538/// event is read, so `comment` is the comment as it was.
539#[derive(Debug, Serialize)]
540#[serde(rename_all = "camelCase")]
541pub struct CommentDeleted {
542 pub comment_id: String,
543 #[serde(flatten)]
544 pub on: CommentOn,
545 pub comment: DeletedComment,
546}
547
548/// A deleted comment, as it was.
549#[derive(Debug, Serialize)]
550#[serde(rename_all = "camelCase")]
551pub struct DeletedComment {
552 pub id: String,
553 pub body: String,
554 pub author: crate::credentials::Principal,
555 /// RFC 3339.
556 pub created_at: String,
557 /// For a comment on one line of a pull request's change: the file.
558 pub path: Option<String>,
559 /// And the line, as numbered after the change.
560 pub line: Option<u32>,
561}
562
563/// The payload of `commit_comment.created`, `commit_comment.edited` and
564/// `commit_comment.deleted`. `comment` is the comment as it is now, or as
565/// it was when deleted; `changes.body.from` is what an edit replaced.
566#[derive(Debug, Serialize)]
567#[serde(rename_all = "camelCase")]
568pub struct CommitCommentEvent {
569 pub comment_id: String,
570 pub repo_id: String,
571 /// The commit's full hash.
572 pub commit: String,
573 pub comment: crate::work::CommitComment,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub changes: Option<CommentChanges>,
576 /// Where it is on g1t.sh: `/owner/name/commit/<hash>`.
577 pub link: String,
578 /// For the inbox, on `commit_comment.created`: the first line it says
579 /// something on, the people it mentions by name (never `g1t`), the
580 /// teams it mentions with the people each tells, and the commit's
581 /// author when their address is a g1t account's (a username).
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub excerpt: Option<String>,
584 #[serde(skip_serializing_if = "Vec::is_empty")]
585 pub mentions: Vec<String>,
586 #[serde(skip_serializing_if = "Vec::is_empty")]
587 pub team_mentions: Vec<crate::inbox::TeamMentioned>,
588 #[serde(skip_serializing_if = "Option::is_none")]
589 pub commit_author: Option<String>,
590}
591
592/// The payload of every `release.*` event: `release.created`,
593/// `release.published`, `release.released`, `release.prereleased`,
594/// `release.edited`, `release.unpublished` and `release.deleted`. One change
595/// to a release can be several of them (see [`release_actions`]).
596#[derive(Debug, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct ReleaseEvent {
599 pub release_id: String,
600 pub repo_id: String,
601 pub tag_name: String,
602 /// As it is now; as it was, for `release.deleted`.
603 pub release: crate::about::Release,
604 /// On `release.edited`: `{ "name": { "from" }, "body": { "from" } }`
605 /// for what changed.
606 #[serde(skip_serializing_if = "Option::is_none")]
607 pub changes: Option<serde_json::Value>,
608}
609
610/// A release's state, for [`release_actions`]: whether it is a draft, and
611/// whether it is a prerelease.
612#[derive(Clone, Copy, Debug, PartialEq, Eq)]
613pub struct ReleaseState {
614 pub draft: bool,
615 pub prerelease: bool,
616}
617
618/// What a change to a release is, as GitHub's release activity types, in
619/// the order they are sent: from `before` (None: it did not exist) to
620/// `after` (None: deleted). A release made published is `created`,
621/// `published` and `released` (or `prereleased`); a draft published is
622/// `published` and `released` (or `prereleased`); made a draft again,
623/// `unpublished`; anything else `edited`, and a prerelease made a full
624/// release is `released` too.
625pub fn release_actions(before: Option<ReleaseState>, after: Option<ReleaseState>) -> Vec<&'static str> {
626 let published = |state: ReleaseState| if state.prerelease { "prereleased" } else { "released" };
627 match (before, after) {
628 (None, None) => Vec::new(),
629 (None, Some(after)) if after.draft => vec!["created"],
630 (None, Some(after)) => vec!["created", "published", published(after)],
631 (Some(_), None) => vec!["deleted"],
632 (Some(before), Some(after)) if before.draft && !after.draft => vec!["published", published(after)],
633 (Some(before), Some(after)) if !before.draft && after.draft => vec!["unpublished"],
634 (Some(before), Some(after)) if !after.draft && before.prerelease && !after.prerelease => vec!["edited", "released"],
635 (Some(_), Some(_)) => vec!["edited"],
636 }
637}
638
639/// The event kind for a release activity type from [`release_actions`].
640pub fn release_kind(action: &str) -> Option<&'static str> {
641 Some(match action {
642 "created" => "release.created",
643 "published" => "release.published",
644 "released" => "release.released",
645 "prereleased" => "release.prereleased",
646 "edited" => "release.edited",
647 "unpublished" => "release.unpublished",
648 "deleted" => "release.deleted",
649 _ => return None,
650 })
651}
652
653#[derive(Debug, Serialize)]
654#[serde(rename_all = "camelCase")]
655pub struct SessionAppended {
656 pub pull_id: String,
657 pub repo_id: String,
658 pub number: u32,
659 pub count: u32,
660}
661
662/// An event as stored in the log and delivered to subscribers. `data` is
663/// left as JSON; each reader decodes the types it cares about.
664#[derive(Clone, Debug, Serialize, serde::Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct Event {
667 /// Sorts by the time the event was published.
668 pub id: String,
669 #[serde(rename = "type")]
670 pub kind: String,
671 /// The service that published it.
672 pub source: String,
673 /// RFC 3339.
674 pub time: String,
675 /// The repo the event concerns.
676 pub repo_id: Option<String>,
677 /// The user or agent that caused it, if any.
678 pub actor: Option<String>,
679 pub data: serde_json::Value,
680}
681
682/// `publish`, as a publisher sends it. Returns nothing.
683#[derive(Debug, Serialize)]
684pub struct Publish<T: Serialize> {
685 pub events: Vec<NewEvent<T>>,
686}
687
688/// `publish`, as the events service reads it.
689#[derive(Debug, serde::Deserialize)]
690pub struct PublishArgs {
691 pub events: Vec<Published>,
692}
693
694/// A [`NewEvent`] of any type, as received.
695#[derive(Debug, serde::Deserialize)]
696#[serde(rename_all = "camelCase")]
697pub struct Published {
698 #[serde(rename = "type")]
699 pub kind: String,
700 pub source: String,
701 #[serde(default)]
702 pub repo_id: Option<String>,
703 #[serde(default)]
704 pub actor: Option<String>,
705 pub data: serde_json::Value,
706}
707
708/// `list`: events from the log, newest first, or with `after` oldest first.
709/// Returns `Vec<Event>`.
710#[derive(Debug, Default, Serialize, serde::Deserialize)]
711#[serde(rename_all = "camelCase")]
712pub struct ListArgs {
713 #[serde(default)]
714 pub repo_id: Option<String>,
715 /// Only events of these repositories, by id: a workspace's activity
716 /// across its repositories. The caller has checked the viewer may read
717 /// each. At most [`MAX_LIST_REPOS`] are read.
718 #[serde(default, skip_serializing_if = "Vec::is_empty")]
719 pub repo_ids: Vec<String>,
720 /// Only these types; all types when empty.
721 #[serde(default)]
722 pub types: Vec<String>,
723 /// Only events older than this event id.
724 #[serde(default)]
725 pub before: Option<String>,
726 /// Only events the log recorded after this event id, and then oldest
727 /// first: a cursor to follow the log with. Events are ordered by when
728 /// the log wrote them, not by their ids, so one published a moment
729 /// before another but written after it is not skipped.
730 #[serde(default)]
731 pub after: Option<String>,
732 /// Only events by this account id.
733 #[serde(default)]
734 pub actor: Option<String>,
735 /// Only events about these work items or pull requests: their
736 /// `number`, or the `item` a comment, review or link is on. All when
737 /// empty.
738 #[serde(default)]
739 pub numbers: Vec<u32>,
740 /// Only events at or after this RFC 3339 time.
741 #[serde(default)]
742 pub since: Option<String>,
743 #[serde(default)]
744 pub limit: Option<u32>,
745}
746
747/// The most repositories one `list` reads with `repo_ids`.
748pub const MAX_LIST_REPOS: usize = 100;
749
750/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
751/// Every service that stores a slug moves its rows to the workspace's
752/// *current* slug (ask identity by `workspace_id`), so that a repeated or
753/// late delivery after a second rename still lands in the right place.
754#[derive(Clone, Debug, Serialize, serde::Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct WorkspaceRenamed {
757 pub workspace_id: String,
758 pub from: String,
759 pub to: String,
760}
761
762impl WorkspaceRenamed {
763 /// The slugs whose rows move to `current`: the two this rename names,
764 /// minus `current` itself. Moving rows keyed by either converges on the
765 /// current slug whatever order renames are delivered in.
766 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
767 let mut slugs: Vec<String> = Vec::new();
768 for slug in [&self.from, &self.to] {
769 if slug != current && !slugs.contains(slug) {
770 slugs.push(slug.clone());
771 }
772 }
773 slugs
774 }
775}
776
777/// `repo.updated`: a repository's website, topics or visibility
778/// changed. `visibility_changed` says whether it went public or private,
779/// which `repo.visibility_changed` also announces on its own.
780#[derive(Debug, Serialize, serde::Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct RepoUpdated {
783 pub repo_id: String,
784 pub namespace: String,
785 pub name: String,
786 pub is_private: bool,
787 #[serde(default)]
788 pub visibility_changed: bool,
789}
790
791/// `project.updated`: a project's name or description changed. `primary`
792/// says whether it is its repository's own project, whose description
793/// search and lists of repositories show.
794#[derive(Debug, Serialize, serde::Deserialize)]
795#[serde(rename_all = "camelCase")]
796pub struct ProjectUpdated {
797 pub project_id: String,
798 pub workspace: String,
799 pub slug: String,
800 pub repo_id: String,
801 #[serde(default)]
802 pub primary: bool,
803 #[serde(default)]
804 pub description: Option<String>,
805}
806
807/// `repo.visibility_changed`: a repository went public or private.
808#[derive(Debug, Serialize, serde::Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct RepoVisibilityChanged {
811 pub repo_id: String,
812 pub is_private: bool,
813}
814
815/// `repo.renamed`: a repository's name changed within its workspace,
816/// keeping its id and its git store key. Like `repo.transferred`, a path
817/// change: every service that keeps rows under the repository's path moves
818/// them to its *current* path (ask repos `path_by_id`), so a repeated or
819/// late delivery after a second rename or a transfer still lands in the
820/// right place. `g1t_kit::transfer::on_event` handles both.
821#[derive(Clone, Debug, Serialize, serde::Deserialize)]
822#[serde(rename_all = "camelCase")]
823pub struct RepoRenamed {
824 pub repo_id: String,
825 /// The workspace it is in.
826 pub namespace: String,
827 /// Its old name.
828 pub from: String,
829 /// Its new name.
830 pub to: String,
831}
832
833impl RepoRenamed {
834 /// The paths whose rows move to `current` (`namespace/name`): the two
835 /// this rename names, minus `current`.
836 pub fn stale_paths(&self, current: &str) -> Vec<String> {
837 let mut paths: Vec<String> = Vec::new();
838 for name in [&self.from, &self.to] {
839 let path = format!("{}/{name}", self.namespace);
840 if path != current && !paths.contains(&path) {
841 paths.push(path);
842 }
843 }
844 paths
845 }
846}
847
848/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
849/// git refuses it, but it can be restored until `purge_after`, so services
850/// stop what runs for it (agents, workflows, deployments, indexing,
851/// webhook deliveries) and hide it, and keep what they hold until
852/// `repo.purged`. `repo.restored` brings it back.
853#[derive(Clone, Debug, Serialize, serde::Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct RepoDeleted {
856 pub repo_id: String,
857 #[serde(default)]
858 pub namespace: String,
859 #[serde(default)]
860 pub name: String,
861 #[serde(default)]
862 pub is_private: bool,
863 /// RFC 3339: when it is purged unless restored first.
864 #[serde(default)]
865 pub purge_after: String,
866 /// It went with its workspace (`workspace.deleting`). Services that
867 /// handle the workspace as a whole (deployments pauses its apps rather
868 /// than taking them down) leave this one to that.
869 #[serde(default)]
870 pub with_workspace: bool,
871}
872
873/// `repo.restored`: a deleted repository is back, at its path, as it was.
874/// Services start again what `repo.deleted` stopped: index it, deploy its
875/// production, show it.
876#[derive(Clone, Debug, Serialize, serde::Deserialize)]
877#[serde(rename_all = "camelCase")]
878pub struct RepoRestored {
879 pub repo_id: String,
880 pub namespace: String,
881 pub name: String,
882 pub is_private: bool,
883 /// It came back with its workspace (`workspace.restored`).
884 #[serde(default)]
885 pub with_workspace: bool,
886}
887
888/// `repo.purged`: a deleted repository is gone for good, its git data
889/// with it. Services drop every row they keep for it by `repo_id`, except
890/// history that belongs to its workspace: ledgers, invoices and the audit
891/// log. Its path is free for a new repository.
892#[derive(Clone, Debug, Serialize, serde::Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct RepoPurged {
895 pub repo_id: String,
896 pub namespace: String,
897 pub name: String,
898}
899
900/// `repo.archived` and `repo.unarchived`: a repository became read-only,
901/// or writable again. While archived, pushes are refused, its pull
902/// requests are locked, no work items are filed from it, and agents and
903/// workflows do not run for it; its deployments keep serving.
904#[derive(Clone, Debug, Serialize, serde::Deserialize)]
905#[serde(rename_all = "camelCase")]
906pub struct RepoArchived {
907 pub repo_id: String,
908 pub namespace: String,
909 pub name: String,
910 pub archived: bool,
911}
912
913/// `repo.default_branch_changed`: the branch everything lands on is now
914/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
915/// requests into it now target `to`) rather than another branch chosen.
916#[derive(Clone, Debug, Serialize, serde::Deserialize)]
917#[serde(rename_all = "camelCase")]
918pub struct RepoDefaultBranchChanged {
919 pub repo_id: String,
920 pub from: String,
921 pub to: String,
922 #[serde(default)]
923 pub renamed: bool,
924}
925
926/// `branch.renamed`: a branch was renamed. Pull requests from or into
927/// `from` follow it to `to`, and web addresses that name `from` redirect.
928#[derive(Clone, Debug, Serialize, serde::Deserialize)]
929#[serde(rename_all = "camelCase")]
930pub struct BranchRenamed {
931 pub repo_id: String,
932 pub from: String,
933 pub to: String,
934 /// Whether it is the default branch.
935 #[serde(default)]
936 pub default_branch: bool,
937}
938
939/// `repo.transferred`: a repository moved from one workspace to another,
940/// keeping its id and its name. Every service that keeps a repository
941/// under its path (`namespace/name`) or its workspace's slug moves those
942/// rows to the repository's *current* path (ask repos `path_by_id`), so a
943/// repeated or late delivery after a second transfer still lands in the
944/// right place. What was charged or recorded before the transfer stays
945/// with the workspace it happened in.
946#[derive(Clone, Debug, Serialize, serde::Deserialize)]
947#[serde(rename_all = "camelCase")]
948pub struct RepoTransferred {
949 pub repo_id: String,
950 pub name: String,
951 /// The workspace it left.
952 pub from: String,
953 /// The workspace it went to.
954 pub to: String,
955}
956
957impl RepoTransferred {
958 /// The paths whose rows move to `current` (`namespace/name`): the two
959 /// this transfer names, minus `current`. Moving rows keyed by either
960 /// converges whatever order transfers are delivered in.
961 pub fn stale_paths(&self, current: &str) -> Vec<String> {
962 let mut paths: Vec<String> = Vec::new();
963 for namespace in [&self.from, &self.to] {
964 let path = format!("{namespace}/{}", self.name);
965 if path != current && !paths.contains(&path) {
966 paths.push(path);
967 }
968 }
969 paths
970 }
971}
972
973/// `workspace.deleted`: a workspace is gone. Services drop what they keep
974/// for it alone (its webhooks, integrations, secrets, guardrails,
975/// agent queue) and keep what is history: ledgers, invoices and the audit
976/// log stay under its slug, which is never given to another workspace.
977#[derive(Clone, Debug, Serialize, serde::Deserialize)]
978#[serde(rename_all = "camelCase")]
979pub struct WorkspaceDeleted {
980 pub workspace_id: String,
981 pub slug: String,
982}
983
984/// `workspace.deleting`: an owner deleted a workspace, and it can be
985/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
986/// meantime. Services hide what they keep for it and stop what runs for it,
987/// keeping their rows: repos deletes its repositories softly (each with a
988/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
989/// apps, search drops it from results. `workspace.restored` undoes exactly
990/// that; once `purge_after` passes, `workspace.deleted` follows and
991/// services purge as for any deleted workspace.
992#[derive(Clone, Debug, Serialize, serde::Deserialize)]
993#[serde(rename_all = "camelCase")]
994pub struct WorkspaceDeleting {
995 pub workspace_id: String,
996 pub slug: String,
997 /// The username of the owner who deleted it.
998 pub by: String,
999 /// RFC 3339: when it is purged unless restored first.
1000 pub purge_after: String,
1001}
1002
1003/// `workspace.restored`: staff brought a deleted workspace back, with its
1004/// members and tokens. Services undo what they did on `workspace.deleting`,
1005/// and only that: a repository deleted on its own before stays deleted.
1006#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1007#[serde(rename_all = "camelCase")]
1008pub struct WorkspaceRestored {
1009 pub workspace_id: String,
1010 pub slug: String,
1011}
1012
1013/// `user.deleting`: an account was deleted, by the person or by g1t's
1014/// staff, and can be restored by staff until `purge_after`. Its sessions,
1015/// tokens and keys have ended and it has left every workspace; services
1016/// stop what they do for it (search drops its profile, nothing more is
1017/// sent to it) and keep their rows. `user.restored` undoes that; once
1018/// `purge_after` passes, `user.deleted` follows.
1019#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1020#[serde(rename_all = "camelCase")]
1021pub struct UserDeleting {
1022 pub user_id: String,
1023 pub username: String,
1024 /// Whether g1t's staff deleted it rather than the person.
1025 #[serde(default)]
1026 pub by_staff: bool,
1027 /// RFC 3339: when it is purged unless restored first.
1028 pub purge_after: String,
1029}
1030
1031/// `user.restored`: staff brought a deleted account back. It signs in
1032/// again with its password; its old sessions and tokens stay ended.
1033/// Services undo what they did on `user.deleting`.
1034#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1035#[serde(rename_all = "camelCase")]
1036pub struct UserRestored {
1037 pub user_id: String,
1038 pub username: String,
1039}
1040
1041/// `user.deleted`: an account is gone for good. Services drop what they
1042/// keep for it alone (its inbox, subscriptions, settings) and show what it
1043/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
1044/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
1045/// its username. The username is never given to anyone again.
1046#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct UserDeleted {
1049 pub user_id: String,
1050 pub username: String,
1051}
1052
1053/// `user.updated`: an account was made, or changed what its profile shows
1054/// (name, bio, avatar). Nothing private: ask identity for the profile.
1055#[derive(Debug, Serialize, serde::Deserialize)]
1056#[serde(rename_all = "camelCase")]
1057pub struct UserUpdated {
1058 pub username: String,
1059}
1060
1061/// `user.email_added`, `user.email_verified`, `user.email_removed` and
1062/// `user.primary_email_changed`: an account's addresses changed. Never the
1063/// address itself; ask identity, as the person, for that.
1064#[derive(Debug, Serialize, serde::Deserialize)]
1065#[serde(rename_all = "camelCase")]
1066pub struct UserEmailChanged {
1067 pub user_id: String,
1068 /// Whether g1t staff made the change.
1069 #[serde(default)]
1070 pub by_staff: bool,
1071}
1072
1073/// `workspace.updated`: a workspace was made, or its name, description or
1074/// icon changed. Ask identity for it by slug.
1075#[derive(Debug, Serialize, serde::Deserialize)]
1076#[serde(rename_all = "camelCase")]
1077pub struct WorkspaceUpdated {
1078 pub workspace_id: String,
1079 pub slug: String,
1080}
1081
1082/// How someone came to be a member of a workspace (`workspace.member_joined`).
1083#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
1084#[serde(rename_all = "snake_case")]
1085pub enum JoinedHow {
1086 /// They accepted a workspace invitation sent to them by name.
1087 Invitation,
1088 /// They made the workspace, and are its first owner.
1089 Created,
1090 /// They used an invite code or link that named the workspace.
1091 Joined,
1092}
1093
1094/// `workspace.member_joined`: a person became a member of a workspace,
1095/// published by identity as the membership is written. `how` says by
1096/// which door; the workspace's creator is `created`, so a listener that
1097/// greets new members can leave them out (the agents service's @g1t
1098/// welcomes everyone else, once). Never published for an agent or a
1099/// token: only a person joins.
1100#[derive(Clone, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
1101#[serde(rename_all = "camelCase")]
1102pub struct MemberJoined {
1103 pub workspace_id: String,
1104 /// The workspace's slug.
1105 pub workspace: String,
1106 pub user_id: String,
1107 pub username: String,
1108 pub role: crate::Role,
1109 pub how: JoinedHow,
1110}
1111
1112/// `invite.created`: someone (or staff) made an invite. Never the code or
1113/// the address it is for.
1114#[derive(Debug, Serialize, serde::Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116pub struct InviteCreated {
1117 pub invite_id: String,
1118 /// The account that made it; null when staff did.
1119 pub inviter_id: Option<String>,
1120 /// The workspace it joins.
1121 pub workspace_id: Option<String>,
1122 /// Whether it is bound to one email address.
1123 pub bound: bool,
1124}
1125
1126/// `invite.redeemed`: an invite was used, by a new account or by an
1127/// existing one joining a workspace.
1128#[derive(Debug, Serialize, serde::Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct InviteRedeemed {
1131 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
1132 /// the account.
1133 pub invite_id: String,
1134 pub user_id: String,
1135 pub inviter_id: Option<String>,
1136 pub workspace_id: Option<String>,
1137 /// Whether it made the account.
1138 pub created_account: bool,
1139}
1140
1141/// The payload of `package.published`, `package.version_deleted`,
1142/// `package.deleted` and `package.visibility_changed`; each uses the fields
1143/// that apply to it. `repo_id` is the repository the package is linked to.
1144#[derive(Clone, Debug, Default, Serialize)]
1145#[serde(rename_all = "camelCase")]
1146pub struct PackageEvent {
1147 pub package_id: String,
1148 pub workspace: String,
1149 pub ecosystem: String,
1150 pub name: String,
1151 pub repo_id: Option<String>,
1152 /// The version published or deleted: for a container image, its
1153 /// manifest's digest.
1154 #[serde(skip_serializing_if = "Option::is_none")]
1155 pub version: Option<String>,
1156 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub digest: Option<String>,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub size: Option<u64>,
1160 /// On publish: the tags that now point to the version.
1161 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub tags: Option<Vec<String>>,
1163 /// On `package.visibility_changed`: `public` or `private`.
1164 #[serde(skip_serializing_if = "Option::is_none")]
1165 pub visibility: Option<String>,
1166}
1167
1168/// `queue.changed`: a repository's merge queue gained, lost or settled an
1169/// entry, so the next batch may be ready to test.
1170#[derive(Debug, Serialize)]
1171#[serde(rename_all = "camelCase")]
1172pub struct QueueChanged {
1173 pub repo_id: String,
1174}
1175
1176/// The `doc.page.*` types the artifacts service (services/artifacts, TypeScript)
1177/// publishes, with no `repoId` on the event: a page may be in a private
1178/// space, so it never reaches a repository's timeline or webhooks.
1179pub const DOC_PAGE_EVENTS: [&str; 4] = ["doc.page.created", "doc.page.updated", "doc.page.archived", "doc.page.stale"];
1180
1181/// The `folio.*` types (Artifacts mode), with their payloads, live in
1182/// [`crate::folios`]; nothing publishes or subscribes to them yet.
1183pub use crate::folios::FOLIO_EVENTS;
1184
1185/// What every `doc.page.*` event carries (`DocPageEventData` in
1186/// events.ts). `doc.page.updated` adds `versionId`, `kind` and `authors`;
1187/// `doc.page.stale` adds `repoId`, `repo`, `commit`, `pull`, `paths` and
1188/// `owners` ([`DocPageStale`]).
1189#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1190#[serde(rename_all = "camelCase")]
1191pub struct DocPageEvent {
1192 pub workspace: String,
1193 pub workspace_id: String,
1194 pub page_id: String,
1195 pub space_id: String,
1196 pub title: String,
1197 /// The page's address on the site.
1198 pub path: String,
1199}
1200
1201/// `doc.page.stale`: code a page cites changed.
1202#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1203#[serde(rename_all = "camelCase")]
1204pub struct DocPageStale {
1205 #[serde(flatten)]
1206 pub page: DocPageEvent,
1207 pub repo_id: String,
1208 /// `owner/name`.
1209 pub repo: String,
1210 pub commit: String,
1211 /// The merged pull request's number, when a pull request made the change.
1212 pub pull: Option<u32>,
1213 pub paths: Vec<String>,
1214 /// Member keys: `user:<id>`, `agent:<id>`.
1215 pub owners: Vec<String>,
1216}
1217
1218/// `activity_digest`: what happened in some repositories, and to a
1219/// workspace's artifacts, over a span, counted. Returns [`ActivityDigest`].
1220/// Home reads it for what people and agents did since you were last there.
1221/// Fields are snake_case, as every service's arguments are.
1222///
1223/// The span is `[from, until)` in RFC 3339. Event ids sort by time, so the
1224/// service reads each repository's events of each type it counts as one
1225/// range of the `(repo_id, type, id)` index, from `id_floor(from)` to
1226/// `id_floor(until)`; a workspace's folio events (which carry no
1227/// repository) are the same range under a null repository, narrowed to
1228/// the workspace. That is indexed range scans per repository and type, and
1229/// it suits a workspace of tens of repositories. When a workspace has
1230/// hundreds, the next step is a daily rollup per workspace that this
1231/// method reads instead of the log; it is not built yet.
1232#[derive(Debug, Default, Serialize, serde::Deserialize)]
1233pub struct ActivityDigestArgs {
1234 /// The repositories to count, by id. The caller has checked the
1235 /// viewer may read each. At most `MAX_DIGEST_REPOS` are read.
1236 #[serde(default)]
1237 pub repo_ids: Vec<String>,
1238 /// The workspace's slug, for its `folio.*` events; none leaves
1239 /// artifacts out.
1240 #[serde(default)]
1241 pub workspace: Option<String>,
1242 /// The start of the span, inclusive. RFC 3339.
1243 pub from: String,
1244 /// The end of the span, exclusive. RFC 3339.
1245 pub until: String,
1246}
1247
1248/// The most repositories one digest reads.
1249pub const MAX_DIGEST_REPOS: usize = 50;
1250/// The most events one digest reads of repositories, and of folios; past
1251/// either the digest says it is not complete.
1252pub const MAX_DIGEST_EVENTS: u32 = 5000;
1253/// The most releases, packages and edited folios a digest lists.
1254pub const MAX_DIGEST_LISTED: usize = 50;
1255
1256/// What `activity_digest` returns.
1257#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1258pub struct ActivityDigest {
1259 pub from: String,
1260 pub until: String,
1261 /// One per repository that had any event counted; none for a quiet one.
1262 pub repos: Vec<RepoDigest>,
1263 /// The workspace's artifacts, when a workspace was named.
1264 pub folios: Option<FolioDigest>,
1265 /// False when more events fell in the span than were read, or more
1266 /// repositories were named than are read, so counts are low.
1267 pub complete: bool,
1268}
1269
1270/// How many times one actor did something. The actor is a member key:
1271/// `user:<id>` for a person (or g1t, `user:usr_g1t_agent`), `agent:<id>`
1272/// for one of the workspace's agents acting as itself, or empty when the
1273/// event named nobody.
1274#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1275pub struct ActorCount {
1276 pub actor: String,
1277 pub count: u32,
1278}
1279
1280/// Pushes by one actor, with the commits they brought.
1281#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1282pub struct PushesBy {
1283 pub actor: String,
1284 pub count: u32,
1285 pub commits: u32,
1286}
1287
1288/// Pushes to a repository's branches (`git.push` events for `refs/heads/`;
1289/// tags are not pushes here). A push whose commits were not counted
1290/// counts as one commit.
1291#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1292pub struct PushDigest {
1293 pub count: u32,
1294 pub commits: u32,
1295 /// The branches pushed to, by name, sorted.
1296 pub branches: Vec<String>,
1297 /// Most pushes first.
1298 pub by: Vec<PushesBy>,
1299 /// Of those, the pushes straight to the default branch: what landed
1300 /// without a pull request.
1301 pub default_branch: DefaultBranchPushes,
1302}
1303
1304#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1305pub struct DefaultBranchPushes {
1306 pub count: u32,
1307 pub commits: u32,
1308 pub by: Vec<PushesBy>,
1309 /// When the newest of them was. RFC 3339.
1310 pub last_at: Option<String>,
1311}
1312
1313/// Pull requests opened, merged and closed without merging, each by who did it.
1314#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1315pub struct PullDigest {
1316 pub opened: Vec<ActorCount>,
1317 pub merged: Vec<ActorCount>,
1318 pub closed: Vec<ActorCount>,
1319}
1320
1321/// Work items opened and closed, each by who did it.
1322#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1323pub struct ItemDigest {
1324 pub opened: Vec<ActorCount>,
1325 pub closed: Vec<ActorCount>,
1326}
1327
1328/// Builds that finished, by who set them off, and how many of the
1329/// successes were production.
1330#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1331pub struct DeploymentDigest {
1332 pub succeeded: Vec<ActorCount>,
1333 pub failed: Vec<ActorCount>,
1334 pub production: u32,
1335}
1336
1337/// A release published in the span.
1338#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1339pub struct ReleaseDigest {
1340 pub tag: String,
1341 pub name: Option<String>,
1342 pub actor: String,
1343 /// RFC 3339.
1344 pub at: String,
1345}
1346
1347/// A package version published in the span.
1348#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1349pub struct PackageDigest {
1350 pub ecosystem: String,
1351 pub name: String,
1352 pub version: String,
1353 pub actor: String,
1354 /// RFC 3339.
1355 pub at: String,
1356}
1357
1358/// One repository's span.
1359#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1360pub struct RepoDigest {
1361 pub repo_id: String,
1362 pub pushes: PushDigest,
1363 pub pulls: PullDigest,
1364 pub items: ItemDigest,
1365 /// Reviews: `review.completed` (g1t's), and a comment with a verdict (a person's).
1366 pub reviews: Vec<ActorCount>,
1367 /// Comments without a verdict; one by an agent as itself is the agent's.
1368 pub comments: Vec<ActorCount>,
1369 pub deployments: DeploymentDigest,
1370 /// Newest first, at most `MAX_DIGEST_LISTED`.
1371 pub releases: Vec<ReleaseDigest>,
1372 /// Newest first, at most `MAX_DIGEST_LISTED`.
1373 pub packages: Vec<PackageDigest>,
1374}
1375
1376/// One folio (an artifact: a doc, slides, a design, a dashboard) whose
1377/// content changed, with everyone whose changes are in it, as member
1378/// keys. Never its title.
1379#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1380pub struct FolioEdited {
1381 pub folio_id: String,
1382 pub kind: String,
1383 pub authors: Vec<String>,
1384}
1385
1386/// A workspace's artifacts over the span: `folio.created` by who made
1387/// them, and the folios whose content changed (`folio.updated`).
1388#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1389pub struct FolioDigest {
1390 pub created: Vec<ActorCount>,
1391 /// Each folio once, at most `MAX_DIGEST_LISTED`.
1392 pub edited: Vec<FolioEdited>,
1393 /// How many folios changed, counting those past the cap.
1394 pub edited_count: u32,
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use super::*;
1400
1401 #[test]
1402 fn comment_edits_and_deletes_say_what_it_was() {
1403 let edited = CommentEdited {
1404 comment_id: "cmt_1".into(),
1405 on: CommentOn { repo_id: Some("rep_1".into()), number: 7, pull_id: Some("pr_1".into()), ..CommentOn::default() },
1406 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
1407 };
1408 assert_eq!(
1409 serde_json::to_value(edited).unwrap(),
1410 serde_json::json!({
1411 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
1412 "changes": { "body": { "from": "Before" } },
1413 })
1414 );
1415 // On a work item: its workspace and every repository it is linked to.
1416 let deleted = CommentDeleted {
1417 comment_id: "cmt_1".into(),
1418 on: CommentOn {
1419 repo_id: Some("rep_1".into()),
1420 number: 7,
1421 item_id: Some("wki_1".into()),
1422 workspace: Some("acme".into()),
1423 repo_ids: vec!["rep_1".into(), "rep_2".into()],
1424 ..CommentOn::default()
1425 },
1426 comment: DeletedComment {
1427 id: "cmt_1".into(),
1428 body: "Gone".into(),
1429 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1430 created_at: "2026-10-08T00:00:00Z".into(),
1431 path: None,
1432 line: None,
1433 },
1434 };
1435 assert_eq!(
1436 serde_json::to_value(deleted).unwrap(),
1437 serde_json::json!({
1438 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "itemId": "wki_1", "workspace": "acme",
1439 "repoIds": ["rep_1", "rep_2"],
1440 "comment": {
1441 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1442 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1443 },
1444 })
1445 );
1446 }
1447
1448 #[test]
1449 fn a_release_change_is_githubs_activity_types() {
1450 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1451 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1452 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1453 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1454 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1455 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1456 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1457 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1458 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1459 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1460 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1461 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1462 }
1463 assert_eq!(release_kind("archived"), None);
1464 }
1465
1466 #[test]
1467 fn what_a_job_token_did_is_marked_and_carried_on() {
1468 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1469 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1470 assert_eq!(caused_by_job(&plain), None);
1471 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1472 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1473 ..Default::default()
1474 }));
1475 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1476 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1477 assert_eq!(by_job["number"], 4);
1478 // A push by the job's token, and the pull request it moves.
1479 let push = GitPush {
1480 repo_id: "rep_1".into(),
1481 git_ref: "refs/heads/fix".into(),
1482 before: Some("a".into()),
1483 after: "b".into(),
1484 default_branch: false,
1485 unscanned: false,
1486 caused_by_job: job_run_of(&actor).map(str::to_owned),
1487 commits: Some(3),
1488 landed_pull: None,
1489 agent: None,
1490 };
1491 let push = serde_json::to_value(push).unwrap();
1492 assert_eq!(caused_by_job(&push), Some("run_9"));
1493 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1494 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1495 }
1496
1497 #[test]
1498 fn a_push_says_it_was_unscanned_only_when_it_was() {
1499 let push = |unscanned| GitPush {
1500 repo_id: "rep_1".into(),
1501 git_ref: "refs/heads/import".into(),
1502 before: None,
1503 after: "abc".into(),
1504 default_branch: false,
1505 unscanned,
1506 caused_by_job: None,
1507 commits: None,
1508 landed_pull: None,
1509 agent: None,
1510 };
1511 let quiet = serde_json::to_value(push(false)).unwrap();
1512 assert!(quiet.get("unscanned").is_none());
1513 let flagged = serde_json::to_value(push(true)).unwrap();
1514 assert_eq!(flagged["unscanned"], true);
1515 assert_eq!(flagged["ref"], "refs/heads/import");
1516 assert!(quiet.get("landedPull").is_none());
1517 }
1518
1519 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1520 WorkspaceRenamed {
1521 workspace_id: "wsp_1".into(),
1522 from: from.into(),
1523 to: to.into(),
1524 }
1525 }
1526
1527 #[test]
1528 fn stale_slugs_leave_out_the_current_one() {
1529 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1530 // Delivered after a second rename, b → c: both move to c.
1531 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1532 // Renamed back: a → b → a.
1533 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1534 }
1535
1536 fn transferred(from: &str, to: &str) -> RepoTransferred {
1537 RepoTransferred {
1538 repo_id: "rep_1".into(),
1539 name: "rocket".into(),
1540 from: from.into(),
1541 to: to.into(),
1542 }
1543 }
1544
1545 #[test]
1546 fn stale_paths_leave_out_the_current_one() {
1547 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1548 // Delivered after a second transfer, b → c: both move to c.
1549 assert_eq!(
1550 transferred("a", "b").stale_paths("c/rocket"),
1551 vec!["a/rocket", "b/rocket"]
1552 );
1553 // Transferred back: a → b → a.
1554 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1555 }
1556
1557 #[test]
1558 fn a_transfer_reads_as_published() {
1559 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1560 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1561 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1562 }
1563
1564 #[test]
1565 fn a_rename_names_both_paths_in_its_workspace() {
1566 let renamed = RepoRenamed {
1567 repo_id: "rep_1".into(),
1568 namespace: "acme".into(),
1569 from: "old".into(),
1570 to: "new".into(),
1571 };
1572 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1573 // Delivered after a transfer: both names in acme move.
1574 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1575 }
1576
1577 #[test]
1578 fn a_member_joining_says_by_which_door() {
1579 let joined = MemberJoined {
1580 workspace_id: "wsp_1".into(),
1581 workspace: "acme".into(),
1582 user_id: "usr_ada".into(),
1583 username: "ada".into(),
1584 role: crate::Role::Member,
1585 how: JoinedHow::Invitation,
1586 };
1587 let data = serde_json::to_value(&joined).unwrap();
1588 assert_eq!(
1589 data,
1590 serde_json::json!({
1591 "workspaceId": "wsp_1",
1592 "workspace": "acme",
1593 "userId": "usr_ada",
1594 "username": "ada",
1595 "role": "member",
1596 "how": "invitation",
1597 })
1598 );
1599 let back: MemberJoined = serde_json::from_value(data).unwrap();
1600 assert_eq!(back, joined);
1601 assert_eq!(serde_json::to_value(JoinedHow::Created).unwrap(), "created");
1602 assert_eq!(serde_json::to_value(JoinedHow::Joined).unwrap(), "joined");
1603 }
1604
1605 #[test]
1606 fn reads_the_published_payload() {
1607 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1608 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1609 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1610 }
1611
1612 #[test]
1613 fn a_stale_page_reads_as_published() {
1614 let data = serde_json::json!({
1615 "workspace": "acme", "workspaceId": "wsp_1", "pageId": "pag_1", "spaceId": "spc_1",
1616 "title": "Exports", "path": "/acme/-/docs/general/exports-pag_1",
1617 "repoId": "rep_1", "repo": "acme/web", "commit": "abc", "pull": 431,
1618 "paths": ["src/export.ts"], "owners": ["user:usr_1"]
1619 });
1620 let event: DocPageStale = serde_json::from_value(data).unwrap();
1621 assert_eq!((event.page.page_id.as_str(), event.pull), ("pag_1", Some(431)));
1622 assert!(DOC_PAGE_EVENTS.contains(&"doc.page.stale"));
1623 }
1624}