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