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