Skip to content

g1t/crates/contracts/src/events.rs

839 lines31,301 bytesCodeBlame
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/// What a publisher supplies; the bus fills in the id and time.
7#[derive(Debug, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct NewEvent<T: Serialize> {
10 #[serde(rename = "type")]
11 pub kind: &'static str,
12 /// The service that published it.
13 pub source: &'static str,
14 /// The repo the event concerns.
15 pub repo_id: Option<String>,
16 /// The user or agent that caused it, if any.
17 pub actor: Option<String>,
18 pub data: T,
19}
20
21#[derive(Debug, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct RepoCreated {
24 pub repo_id: String,
25 pub namespace: String,
26 pub name: String,
27 pub is_private: bool,
28}
29
30/// The payload of `repo.collaborator_added`, `repo.collaborator_removed`
31/// and `repo.collaborator_role_changed`: a person's own role on one
32/// repository (see `access`). `role` is the role they have now (null once
33/// removed); `previous_role` what they had before (null when added).
34#[derive(Debug, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct RepoCollaborator {
37 pub repo_id: String,
38 pub namespace: String,
39 pub name: String,
40 pub username: String,
41 pub role: Option<crate::access::RepoRole>,
42 pub previous_role: Option<crate::access::RepoRole>,
43}
44
45#[derive(Debug, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct RepoForked {
48 pub repo_id: String,
49 pub source_repo_id: String,
50 pub pull_id: String,
51}
52
53/// One branch or tag moved by a push. `after` is the commit it points to now.
54#[derive(Debug, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct GitPush {
57 pub repo_id: String,
58 /// The full ref, such as `refs/heads/main`.
59 #[serde(rename = "ref")]
60 pub git_ref: String,
61 /// Where it pointed before; absent for a new branch or tag.
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub before: Option<String>,
64 pub after: String,
65 /// Whether the ref is the repository's default branch.
66 pub default_branch: bool,
67 /// Set when the push was too large to scan for secrets before it was
68 /// stored, and was let through: the security service scans
69 /// `before..after` after it landed. Absent otherwise.
70 #[serde(skip_serializing_if = "std::ops::Not::not")]
71 pub unscanned: bool,
72}
73
74/// The payload of `issue.opened`, `issue.updated`, `issue.assigned`,
75/// `issue.labeled`, `issue.unlabeled`, `issue.milestoned`,
76/// `issue.demilestoned`, `issue.closed` and `issue.reopened`; each uses
77/// the fields that apply to it.
78#[derive(Debug, Default, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct IssueEvent {
81 pub issue_id: String,
82 pub repo_id: String,
83 pub number: u32,
84 /// Who opened it: g1t, for one its agent filed while at work.
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub author: Option<crate::credentials::Principal>,
87 /// For an issue g1t's agent filed: the person it was working for.
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub requested_by: Option<crate::credentials::Principal>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub title: Option<String>,
92 /// On close: `completed` or `not_planned`.
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub reason: Option<&'static str>,
95 /// On close: the number of the pull request whose merge closed it.
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub resolved_by: Option<u32>,
98 /// On `issue.assigned`: the people it is now assigned to.
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub assignees: Option<Vec<String>>,
101 /// On `issue.assigned`: those of them who were not before.
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub added: Option<Vec<String>>,
104 /// On `issue.labeled` and `issue.unlabeled`: the label put on or taken
105 /// off. One event for each.
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub label: Option<EventLabel>,
108 /// On `issue.milestoned`: the milestone it was put in; on
109 /// `issue.demilestoned`, the one it was taken out of.
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub milestone: Option<crate::work::MilestoneRef>,
112}
113
114/// A label, as `issue.labeled`, `pull.labeled` and their `unlabeled` say.
115#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
116pub struct EventLabel {
117 pub name: String,
118 /// Six hex digits.
119 pub color: String,
120}
121
122/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
123/// moved), `pull.closed`, `pull.merged`, `pull.assigned`,
124/// `pull.review_requested` and `pull.review_request_removed` (reviewers
125/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
126/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
127/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
128/// through until a person steps in) and `pull.resumed` (it picked back
129/// up); each uses the fields that apply to it.
130#[derive(Debug, Default, Serialize)]
131#[serde(rename_all = "camelCase")]
132pub struct PullEvent {
133 pub pull_id: String,
134 pub repo_id: String,
135 pub number: u32,
136 /// Who opened it: g1t, for a change g1t made.
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub author: Option<crate::credentials::Principal>,
139 /// For a change g1t made: the person who asked for it.
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub requested_by: Option<crate::credentials::Principal>,
142 /// The number of the issue it is for.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub issue: Option<u32>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub agent: Option<String>,
147 /// On merge: the commit the branch now points to. On update and when
148 /// marked ready: the head of the change.
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub commit: Option<String>,
151 /// On close: the pull request that was merged instead.
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub superseded_by: Option<u32>,
154 /// How sure g1t is of a g1t agent's change, once it has worked that out.
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub confidence: Option<crate::work::Confidence>,
157 /// On `pull.assigned`: the people it is now assigned to.
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub assignees: Option<Vec<String>>,
160 /// On `pull.assigned`: those newly assigned.
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub added: Option<Vec<String>>,
163 /// On `pull.review_requested`: the reviewers newly asked; on
164 /// `pull.review_request_removed`, those no longer asked.
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub reviewers: Option<Vec<String>>,
167 /// On `pull.review_requested` and `pull.review_request_removed`: the
168 /// teams newly asked, or no longer, each with the people it asks.
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub teams: Option<Vec<TeamRequested>>,
171 /// On `pull.review_requested`: asked because they own files it changes
172 /// (its CODEOWNERS file), not by a person.
173 #[serde(skip_serializing_if = "std::ops::Not::not")]
174 pub code_owners: bool,
175 /// On `pull.stalled`: why g1t stopped, and what would start it again.
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub detail: Option<String>,
178 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub label: Option<EventLabel>,
181 /// On `pull.milestoned`: the milestone it was put in; on
182 /// `pull.demilestoned`, the one it was taken out of.
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub milestone: Option<crate::work::MilestoneRef>,
185 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub base: Option<String>,
188}
189
190/// A team asked to review a pull request.
191#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct TeamRequested {
194 /// `workspace/slug`.
195 pub team: String,
196 /// Everyone in it who is told: the whole team, or, with review
197 /// assignment, the people picked (and the rest when it says to tell
198 /// them). Never the pull request's author.
199 pub notified: Vec<String>,
200 /// With review assignment: the people picked, who are asked as
201 /// reviewers themselves.
202 #[serde(default)]
203 pub assigned: Vec<String>,
204}
205
206/// The payload of every `team.*` event: `team.created`, `team.edited`,
207/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
208/// `team.member_removed` (with `username`, `role` and `previousRole`);
209/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
210/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
211/// name the repository as the event's own.
212#[derive(Clone, Debug, Default, Serialize)]
213#[serde(rename_all = "camelCase")]
214pub struct TeamChanged {
215 pub workspace: String,
216 pub team_id: String,
217 /// The team's slug, as it is now.
218 pub team: String,
219 pub name: String,
220 pub visibility: Option<crate::teams::TeamVisibility>,
221 /// The parent's slug.
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub parent: Option<String>,
224 /// On `team.edited`: what changed, such as `name` or `parent`.
225 #[serde(skip_serializing_if = "Vec::is_empty")]
226 pub changes: Vec<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub username: Option<String>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub role: Option<crate::teams::TeamRole>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub previous_role: Option<crate::teams::TeamRole>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub repo_id: Option<String>,
235 /// `workspace/name`.
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub repo: Option<String>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub repo_role: Option<crate::access::RepoRole>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub previous_repo_role: Option<crate::access::RepoRole>,
242}
243
244/// `deployment.succeeded` and `deployment.failed`: a build of a project
245/// finished, for production or for one pull request's preview.
246#[derive(Debug, Serialize)]
247#[serde(rename_all = "camelCase")]
248pub struct DeploymentEvent {
249 pub deployment_id: String,
250 pub project_id: String,
251 pub repo_id: String,
252 pub workspace: String,
253 /// The project's slug.
254 pub project: String,
255 /// `production` or `preview`.
256 pub kind: String,
257 pub branch: Option<String>,
258 /// For a preview: its pull request.
259 pub number: Option<u32>,
260 pub commit: String,
261 /// Where the deployment is on the site, such as
262 /// `/acme/rocket/deployments/dpl_1`.
263 pub path: String,
264 /// For a failure: what went wrong.
265 pub error: Option<String>,
266 /// For a success: whether the deployment before it, of the same app, failed.
267 pub recovered: bool,
268 /// Who started it, by username, or `g1t`.
269 pub triggered_by: String,
270}
271
272/// `checks.completed`: a run of an issue's acceptance checks against a pull
273/// request finished.
274#[derive(Debug, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct ChecksEvent {
277 pub pull_id: String,
278 pub repo_id: String,
279 pub number: u32,
280 /// `passed`, `failed` or `errored`.
281 pub status: &'static str,
282 /// The commit that was checked.
283 pub commit: String,
284}
285
286/// `workflow.completed`: a GitHub Actions run finished.
287#[derive(Debug, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct WorkflowEvent {
290 pub run_id: String,
291 pub repo_id: String,
292 /// The workflow's name, and its file.
293 pub workflow: String,
294 pub path: String,
295 /// The run's number among the workflow's runs.
296 pub number: u64,
297 /// The GitHub event that started it, such as `push`.
298 pub event: String,
299 /// `success`, `failure`, `cancelled` or `skipped`.
300 pub conclusion: String,
301 #[serde(rename = "ref")]
302 pub git_ref: String,
303 pub sha: String,
304 /// The pull request it ran for, if any.
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub pull: Option<u32>,
307}
308
309/// `review.completed`: a g1t agent finished reviewing a pull request, or
310/// could not.
311#[derive(Debug, Serialize)]
312#[serde(rename_all = "camelCase")]
313pub struct ReviewEvent {
314 pub pull_id: String,
315 pub repo_id: String,
316 pub number: u32,
317 /// `approve` or `request_changes`; absent when no review was written.
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub verdict: Option<&'static str>,
320}
321
322/// `comment.created`. `number` is the issue or pull request commented on.
323#[derive(Debug, Serialize)]
324#[serde(rename_all = "camelCase")]
325pub struct CommentCreated {
326 pub comment_id: String,
327 pub repo_id: String,
328 pub number: u32,
329 /// Set when the comment is on a pull request.
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub pull_id: Option<String>,
332 /// Set when the comment is a review: approve or request changes.
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub verdict: Option<crate::work::Verdict>,
335}
336
337#[derive(Debug, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct SessionAppended {
340 pub pull_id: String,
341 pub repo_id: String,
342 pub number: u32,
343 pub count: u32,
344}
345
346/// An event as stored in the log and delivered to subscribers. `data` is
347/// left as JSON; each reader decodes the types it cares about.
348#[derive(Clone, Debug, Serialize, serde::Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct Event {
351 /// Sorts by the time the event was published.
352 pub id: String,
353 #[serde(rename = "type")]
354 pub kind: String,
355 /// The service that published it.
356 pub source: String,
357 /// RFC 3339.
358 pub time: String,
359 /// The repo the event concerns.
360 pub repo_id: Option<String>,
361 /// The user or agent that caused it, if any.
362 pub actor: Option<String>,
363 pub data: serde_json::Value,
364}
365
366/// `publish`, as a publisher sends it. Returns nothing.
367#[derive(Debug, Serialize)]
368pub struct Publish<T: Serialize> {
369 pub events: Vec<NewEvent<T>>,
370}
371
372/// `publish`, as the events service reads it.
373#[derive(Debug, serde::Deserialize)]
374pub struct PublishArgs {
375 pub events: Vec<Published>,
376}
377
378/// A [`NewEvent`] of any type, as received.
379#[derive(Debug, serde::Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub struct Published {
382 #[serde(rename = "type")]
383 pub kind: String,
384 pub source: String,
385 #[serde(default)]
386 pub repo_id: Option<String>,
387 #[serde(default)]
388 pub actor: Option<String>,
389 pub data: serde_json::Value,
390}
391
392/// `list`: events from the log, newest first. Returns `Vec<Event>`.
393#[derive(Debug, Default, Serialize, serde::Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct ListArgs {
396 #[serde(default)]
397 pub repo_id: Option<String>,
398 /// Only these types; all types when empty.
399 #[serde(default)]
400 pub types: Vec<String>,
401 /// Only events older than this event id.
402 #[serde(default)]
403 pub before: Option<String>,
404 #[serde(default)]
405 pub limit: Option<u32>,
406}
407
408/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
409/// Every service that stores a slug moves its rows to the workspace's
410/// *current* slug (ask identity by `workspace_id`), so that a repeated or
411/// late delivery after a second rename still lands in the right place.
412#[derive(Clone, Debug, Serialize, serde::Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct WorkspaceRenamed {
415 pub workspace_id: String,
416 pub from: String,
417 pub to: String,
418}
419
420impl WorkspaceRenamed {
421 /// The slugs whose rows move to `current`: the two this rename names,
422 /// minus `current` itself. Moving rows keyed by either converges on the
423 /// current slug whatever order renames are delivered in.
424 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
425 let mut slugs: Vec<String> = Vec::new();
426 for slug in [&self.from, &self.to] {
427 if slug != current && !slugs.contains(slug) {
428 slugs.push(slug.clone());
429 }
430 }
431 slugs
432 }
433}
434
435/// `repo.updated`: a repository's description, topics or visibility
436/// changed. `visibility_changed` says whether it went public or private,
437/// which `repo.visibility_changed` also announces on its own.
438#[derive(Debug, Serialize, serde::Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct RepoUpdated {
441 pub repo_id: String,
442 pub namespace: String,
443 pub name: String,
444 pub is_private: bool,
445 #[serde(default)]
446 pub visibility_changed: bool,
447}
448
449/// `repo.visibility_changed`: a repository went public or private.
450#[derive(Debug, Serialize, serde::Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct RepoVisibilityChanged {
453 pub repo_id: String,
454 pub is_private: bool,
455}
456
457/// `repo.renamed`: a repository's name changed within its workspace,
458/// keeping its id and its git store key. Like `repo.transferred`, a path
459/// change: every service that keeps rows under the repository's path moves
460/// them to its *current* path (ask repos `path_by_id`), so a repeated or
461/// late delivery after a second rename or a transfer still lands in the
462/// right place. `g1t_kit::transfer::on_event` handles both.
463#[derive(Clone, Debug, Serialize, serde::Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct RepoRenamed {
466 pub repo_id: String,
467 /// The workspace it is in.
468 pub namespace: String,
469 /// Its old name.
470 pub from: String,
471 /// Its new name.
472 pub to: String,
473}
474
475impl RepoRenamed {
476 /// The paths whose rows move to `current` (`namespace/name`): the two
477 /// this rename names, minus `current`.
478 pub fn stale_paths(&self, current: &str) -> Vec<String> {
479 let mut paths: Vec<String> = Vec::new();
480 for name in [&self.from, &self.to] {
481 let path = format!("{}/{name}", self.namespace);
482 if path != current && !paths.contains(&path) {
483 paths.push(path);
484 }
485 }
486 paths
487 }
488}
489
490/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
491/// git refuses it, but it can be restored until `purge_after`, so services
492/// stop what runs for it (agents, workflows, deployments, indexing,
493/// webhook deliveries) and hide it, and keep what they hold until
494/// `repo.purged`. `repo.restored` brings it back.
495#[derive(Clone, Debug, Serialize, serde::Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct RepoDeleted {
498 pub repo_id: String,
499 #[serde(default)]
500 pub namespace: String,
501 #[serde(default)]
502 pub name: String,
503 #[serde(default)]
504 pub is_private: bool,
505 /// RFC 3339: when it is purged unless restored first.
506 #[serde(default)]
507 pub purge_after: String,
508 /// It went with its workspace (`workspace.deleting`). Services that
509 /// handle the workspace as a whole (deployments pauses its apps rather
510 /// than taking them down) leave this one to that.
511 #[serde(default)]
512 pub with_workspace: bool,
513}
514
515/// `repo.restored`: a deleted repository is back, at its path, as it was.
516/// Services start again what `repo.deleted` stopped: index it, deploy its
517/// production, show it.
518#[derive(Clone, Debug, Serialize, serde::Deserialize)]
519#[serde(rename_all = "camelCase")]
520pub struct RepoRestored {
521 pub repo_id: String,
522 pub namespace: String,
523 pub name: String,
524 pub is_private: bool,
525 /// It came back with its workspace (`workspace.restored`).
526 #[serde(default)]
527 pub with_workspace: bool,
528}
529
530/// `repo.purged`: a deleted repository is gone for good, its git data
531/// with it. Services drop every row they keep for it by `repo_id`, except
532/// history that belongs to its workspace: ledgers, invoices and the audit
533/// log. Its path is free for a new repository.
534#[derive(Clone, Debug, Serialize, serde::Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct RepoPurged {
537 pub repo_id: String,
538 pub namespace: String,
539 pub name: String,
540}
541
542/// `repo.archived` and `repo.unarchived`: a repository became read-only,
543/// or writable again. While archived, pushes are refused, issues and pull
544/// requests are locked, and agents and workflows do not run for it; its
545/// deployments keep serving.
546#[derive(Clone, Debug, Serialize, serde::Deserialize)]
547#[serde(rename_all = "camelCase")]
548pub struct RepoArchived {
549 pub repo_id: String,
550 pub namespace: String,
551 pub name: String,
552 pub archived: bool,
553}
554
555/// `repo.default_branch_changed`: the branch everything lands on is now
556/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
557/// requests into it now target `to`) rather than another branch chosen.
558#[derive(Clone, Debug, Serialize, serde::Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct RepoDefaultBranchChanged {
561 pub repo_id: String,
562 pub from: String,
563 pub to: String,
564 #[serde(default)]
565 pub renamed: bool,
566}
567
568/// `branch.renamed`: a branch was renamed. Pull requests from or into
569/// `from` follow it to `to`, and web addresses that name `from` redirect.
570#[derive(Clone, Debug, Serialize, serde::Deserialize)]
571#[serde(rename_all = "camelCase")]
572pub struct BranchRenamed {
573 pub repo_id: String,
574 pub from: String,
575 pub to: String,
576 /// Whether it is the default branch.
577 #[serde(default)]
578 pub default_branch: bool,
579}
580
581/// `repo.transferred`: a repository moved from one workspace to another,
582/// keeping its id and its name. Every service that keeps a repository
583/// under its path (`namespace/name`) or its workspace's slug moves those
584/// rows to the repository's *current* path (ask repos `path_by_id`), so a
585/// repeated or late delivery after a second transfer still lands in the
586/// right place. What was charged or recorded before the transfer stays
587/// with the workspace it happened in.
588#[derive(Clone, Debug, Serialize, serde::Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct RepoTransferred {
591 pub repo_id: String,
592 pub name: String,
593 /// The workspace it left.
594 pub from: String,
595 /// The workspace it went to.
596 pub to: String,
597}
598
599impl RepoTransferred {
600 /// The paths whose rows move to `current` (`namespace/name`): the two
601 /// this transfer names, minus `current`. Moving rows keyed by either
602 /// converges whatever order transfers are delivered in.
603 pub fn stale_paths(&self, current: &str) -> Vec<String> {
604 let mut paths: Vec<String> = Vec::new();
605 for namespace in [&self.from, &self.to] {
606 let path = format!("{namespace}/{}", self.name);
607 if path != current && !paths.contains(&path) {
608 paths.push(path);
609 }
610 }
611 paths
612 }
613}
614
615/// `workspace.deleted`: a workspace is gone. Services drop what they keep
616/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
617/// agent queue) and keep what is history: ledgers, invoices and the audit
618/// log stay under its slug, which is never given to another workspace.
619#[derive(Clone, Debug, Serialize, serde::Deserialize)]
620#[serde(rename_all = "camelCase")]
621pub struct WorkspaceDeleted {
622 pub workspace_id: String,
623 pub slug: String,
624}
625
626/// `workspace.deleting`: an owner deleted a workspace, and it can be
627/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
628/// meantime. Services hide what they keep for it and stop what runs for it,
629/// keeping their rows: repos deletes its repositories softly (each with a
630/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
631/// apps, search drops it from results. `workspace.restored` undoes exactly
632/// that; once `purge_after` passes, `workspace.deleted` follows and
633/// services purge as for any deleted workspace.
634#[derive(Clone, Debug, Serialize, serde::Deserialize)]
635#[serde(rename_all = "camelCase")]
636pub struct WorkspaceDeleting {
637 pub workspace_id: String,
638 pub slug: String,
639 /// The username of the owner who deleted it.
640 pub by: String,
641 /// RFC 3339: when it is purged unless restored first.
642 pub purge_after: String,
643}
644
645/// `workspace.restored`: staff brought a deleted workspace back, with its
646/// members and tokens. Services undo what they did on `workspace.deleting`,
647/// and only that: a repository deleted on its own before stays deleted.
648#[derive(Clone, Debug, Serialize, serde::Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct WorkspaceRestored {
651 pub workspace_id: String,
652 pub slug: String,
653}
654
655/// `user.updated`: an account was made, or changed what its profile shows
656/// (name, bio, avatar). Nothing private: ask identity for the profile.
657#[derive(Debug, Serialize, serde::Deserialize)]
658#[serde(rename_all = "camelCase")]
659pub struct UserUpdated {
660 pub username: String,
661}
662
663/// `user.email_added`, `user.email_verified`, `user.email_removed` and
664/// `user.primary_email_changed`: an account's addresses changed. Never the
665/// address itself; ask identity, as the person, for that.
666#[derive(Debug, Serialize, serde::Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub struct UserEmailChanged {
669 pub user_id: String,
670 /// Whether g1t staff made the change.
671 #[serde(default)]
672 pub by_staff: bool,
673}
674
675/// `workspace.updated`: a workspace was made, or its name, description or
676/// icon changed. Ask identity for it by slug.
677#[derive(Debug, Serialize, serde::Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct WorkspaceUpdated {
680 pub workspace_id: String,
681 pub slug: String,
682}
683
684/// `invite.created`: someone (or staff) made an invite. Never the code or
685/// the address it is for.
686#[derive(Debug, Serialize, serde::Deserialize)]
687#[serde(rename_all = "camelCase")]
688pub struct InviteCreated {
689 pub invite_id: String,
690 /// The account that made it; null when staff did.
691 pub inviter_id: Option<String>,
692 /// The workspace it joins.
693 pub workspace_id: Option<String>,
694 /// Whether it is bound to one email address.
695 pub bound: bool,
696}
697
698/// `invite.redeemed`: an invite was used, by a new account or by an
699/// existing one joining a workspace.
700#[derive(Debug, Serialize, serde::Deserialize)]
701#[serde(rename_all = "camelCase")]
702pub struct InviteRedeemed {
703 pub invite_id: String,
704 pub user_id: String,
705 pub inviter_id: Option<String>,
706 pub workspace_id: Option<String>,
707 /// Whether it made the account.
708 pub created_account: bool,
709}
710
711/// `waitlist.requested`: someone asked for access. Ask identity's staff
712/// methods for the entry; the address is not in the event.
713#[derive(Debug, Serialize, serde::Deserialize)]
714#[serde(rename_all = "camelCase")]
715pub struct WaitlistRequested {
716 pub entry_id: String,
717}
718
719/// The payload of `package.published`, `package.version_deleted`,
720/// `package.deleted` and `package.visibility_changed`; each uses the fields
721/// that apply to it. `repo_id` is the repository the package is linked to.
722#[derive(Clone, Debug, Default, Serialize)]
723#[serde(rename_all = "camelCase")]
724pub struct PackageEvent {
725 pub package_id: String,
726 pub workspace: String,
727 pub ecosystem: String,
728 pub name: String,
729 pub repo_id: Option<String>,
730 /// The version published or deleted: for a container image, its
731 /// manifest's digest.
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub version: Option<String>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub digest: Option<String>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub size: Option<u64>,
738 /// On publish: the tags that now point to the version.
739 #[serde(skip_serializing_if = "Option::is_none")]
740 pub tags: Option<Vec<String>>,
741 /// On `package.visibility_changed`: `public` or `private`.
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub visibility: Option<String>,
744}
745
746/// `queue.changed`: a repository's merge queue gained, lost or settled an
747/// entry, so the next batch may be ready to test.
748#[derive(Debug, Serialize)]
749#[serde(rename_all = "camelCase")]
750pub struct QueueChanged {
751 pub repo_id: String,
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn a_push_says_it_was_unscanned_only_when_it_was() {
760 let push = |unscanned| GitPush {
761 repo_id: "rep_1".into(),
762 git_ref: "refs/heads/import".into(),
763 before: None,
764 after: "abc".into(),
765 default_branch: false,
766 unscanned,
767 };
768 let quiet = serde_json::to_value(push(false)).unwrap();
769 assert!(quiet.get("unscanned").is_none());
770 let flagged = serde_json::to_value(push(true)).unwrap();
771 assert_eq!(flagged["unscanned"], true);
772 assert_eq!(flagged["ref"], "refs/heads/import");
773 }
774
775 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
776 WorkspaceRenamed {
777 workspace_id: "wsp_1".into(),
778 from: from.into(),
779 to: to.into(),
780 }
781 }
782
783 #[test]
784 fn stale_slugs_leave_out_the_current_one() {
785 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
786 // Delivered after a second rename, b → c: both move to c.
787 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
788 // Renamed back: a → b → a.
789 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
790 }
791
792 fn transferred(from: &str, to: &str) -> RepoTransferred {
793 RepoTransferred {
794 repo_id: "rep_1".into(),
795 name: "rocket".into(),
796 from: from.into(),
797 to: to.into(),
798 }
799 }
800
801 #[test]
802 fn stale_paths_leave_out_the_current_one() {
803 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
804 // Delivered after a second transfer, b → c: both move to c.
805 assert_eq!(
806 transferred("a", "b").stale_paths("c/rocket"),
807 vec!["a/rocket", "b/rocket"]
808 );
809 // Transferred back: a → b → a.
810 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
811 }
812
813 #[test]
814 fn a_transfer_reads_as_published() {
815 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
816 let event: RepoTransferred = serde_json::from_value(data).unwrap();
817 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
818 }
819
820 #[test]
821 fn a_rename_names_both_paths_in_its_workspace() {
822 let renamed = RepoRenamed {
823 repo_id: "rep_1".into(),
824 namespace: "acme".into(),
825 from: "old".into(),
826 to: "new".into(),
827 };
828 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
829 // Delivered after a transfer: both names in acme move.
830 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
831 }
832
833 #[test]
834 fn reads_the_published_payload() {
835 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
836 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
837 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
838 }
839}