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.

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