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