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