Skip to content

g1t/crates/contracts/src/events.rs

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