Skip to content
1,148 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}
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.reopened`, `pull.converted_to_draft`,
165/// `pull.merged`, `pull.assigned`,
166/// `pull.review_requested` and `pull.review_request_removed` (reviewers
167/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
168/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
169/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
170/// through until a person steps in) and `pull.resumed` (it picked back
171/// up); each uses the fields that apply to it.
172#[derive(Debug, Default, Serialize)]
173#[serde(rename_all = "camelCase")]
174pub struct PullEvent {
175 pub pull_id: String,
176 pub repo_id: String,
177 pub number: u32,
178 /// Who opened it: g1t, for a change g1t made.
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub author: Option<crate::credentials::Principal>,
181 /// For a change g1t made: the person who asked for it.
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub requested_by: Option<crate::credentials::Principal>,
184 /// The number of the issue it is for.
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub issue: Option<u32>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub agent: Option<String>,
189 /// On merge: the commit the branch now points to. On update, when
190 /// marked ready and when reopened: the head of the change.
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub commit: Option<String>,
193 /// On close: the pull request that was merged instead.
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub superseded_by: Option<u32>,
196 /// How sure g1t is of a g1t agent's change, once it has worked that out.
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub confidence: Option<crate::work::Confidence>,
199 /// On `pull.assigned`: the people it is now assigned to.
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub assignees: Option<Vec<String>>,
202 /// On `pull.assigned`: those newly assigned.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub added: Option<Vec<String>>,
205 /// On `pull.review_requested`: the reviewers newly asked; on
206 /// `pull.review_request_removed`, those no longer asked.
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub reviewers: Option<Vec<String>>,
209 /// On `pull.review_requested` and `pull.review_request_removed`: the
210 /// teams newly asked, or no longer, each with the people it asks.
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub teams: Option<Vec<TeamRequested>>,
213 /// On `pull.review_requested`: asked because they own files it changes
214 /// (its CODEOWNERS file), not by a person.
215 #[serde(skip_serializing_if = "std::ops::Not::not")]
216 pub code_owners: bool,
217 /// On `pull.stalled`: why g1t stopped, and what would start it again.
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub detail: Option<String>,
220 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub label: Option<EventLabel>,
223 /// On `pull.milestoned`: the milestone it was put in; on
224 /// `pull.demilestoned`, the one it was taken out of.
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub milestone: Option<crate::work::MilestoneRef>,
227 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub base: Option<String>,
230}
231
232/// A team asked to review a pull request.
233#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct TeamRequested {
236 /// `workspace/slug`.
237 pub team: String,
238 /// Everyone in it who is told: the whole team, or, with review
239 /// assignment, the people picked (and the rest when it says to tell
240 /// them). Never the pull request's author.
241 pub notified: Vec<String>,
242 /// With review assignment: the people picked, who are asked as
243 /// reviewers themselves.
244 #[serde(default)]
245 pub assigned: Vec<String>,
246}
247
248/// The payload of every `team.*` event: `team.created`, `team.edited`,
249/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
250/// `team.member_removed` (with `username`, `role` and `previousRole`);
251/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
252/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
253/// name the repository as the event's own.
254#[derive(Clone, Debug, Default, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub struct TeamChanged {
257 pub workspace: String,
258 pub team_id: String,
259 /// The team's slug, as it is now.
260 pub team: String,
261 pub name: String,
262 pub visibility: Option<crate::teams::TeamVisibility>,
263 /// The parent's slug.
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub parent: Option<String>,
266 /// On `team.edited`: what changed, such as `name` or `parent`.
267 #[serde(skip_serializing_if = "Vec::is_empty")]
268 pub changes: Vec<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub username: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub role: Option<crate::teams::TeamRole>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub previous_role: Option<crate::teams::TeamRole>,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub repo_id: Option<String>,
277 /// `workspace/name`.
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub repo: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub repo_role: Option<crate::access::RepoRole>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub previous_repo_role: Option<crate::access::RepoRole>,
284}
285
286/// `deployment.succeeded` and `deployment.failed`: a build of a project
287/// finished, for production or for one pull request's preview.
288#[derive(Debug, Serialize)]
289#[serde(rename_all = "camelCase")]
290pub struct DeploymentEvent {
291 pub deployment_id: String,
292 pub project_id: String,
293 pub repo_id: String,
294 pub workspace: String,
295 /// The project's slug.
296 pub project: String,
297 /// `production` or `preview`.
298 pub kind: String,
299 pub branch: Option<String>,
300 /// For a preview: its pull request.
301 pub number: Option<u32>,
302 pub commit: String,
303 /// Where the deployment is on the site, such as
304 /// `/acme/rocket/deployments/dpl_1`.
305 pub path: String,
306 /// For a failure: what went wrong.
307 pub error: Option<String>,
308 /// For a success: whether the deployment before it, of the same app, failed.
309 pub recovered: bool,
310 /// Who started it, by username, or `g1t`.
311 pub triggered_by: String,
312}
313
314/// `checks.completed`: a run of an issue's acceptance checks against a pull
315/// request finished.
316#[derive(Debug, Serialize)]
317#[serde(rename_all = "camelCase")]
318pub struct ChecksEvent {
319 pub pull_id: String,
320 pub repo_id: String,
321 pub number: u32,
322 /// `passed`, `failed` or `errored`.
323 pub status: &'static str,
324 /// The commit that was checked.
325 pub commit: String,
326}
327
328/// `workflow.completed`: a GitHub Actions run finished.
329#[derive(Debug, Serialize)]
330#[serde(rename_all = "camelCase")]
331pub struct WorkflowEvent {
332 pub run_id: String,
333 pub repo_id: String,
334 /// The workflow's name, and its file.
335 pub workflow: String,
336 pub path: String,
337 /// The run's number among the workflow's runs.
338 pub number: u64,
339 /// The GitHub event that started it, such as `push`.
340 pub event: String,
341 /// `success`, `failure`, `cancelled` or `skipped`.
342 pub conclusion: String,
343 #[serde(rename = "ref")]
344 pub git_ref: String,
345 pub sha: String,
346 /// The pull request it ran for, if any.
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub pull: Option<u32>,
349}
350
351/// `review.completed`: a g1t agent finished reviewing a pull request, or
352/// could not.
353#[derive(Debug, Serialize)]
354#[serde(rename_all = "camelCase")]
355pub struct ReviewEvent {
356 pub pull_id: String,
357 pub repo_id: String,
358 pub number: u32,
359 /// `approve` or `request_changes`; absent when no review was written.
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub verdict: Option<&'static str>,
362}
363
364/// `comment.created`. `number` is the issue or pull request commented on.
365#[derive(Debug, Default, Serialize)]
366#[serde(rename_all = "camelCase")]
367pub struct CommentCreated {
368 pub comment_id: String,
369 pub repo_id: String,
370 pub number: u32,
371 /// Set when the comment is on a pull request.
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub pull_id: Option<String>,
374 /// Set when the comment is a review: approve or request changes.
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub verdict: Option<crate::work::Verdict>,
377 /// Set when one of the workspace's agents wrote it, as itself. The
378 /// event's actor is then the person it acted for (`actingFor`).
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub agent: Option<crate::work::AgentRef>,
381 /// Who the agent acted for, set with `agent`.
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub acting_for: Option<crate::credentials::Principal>,
384 /// An agent's review, advisory: its verdict counts toward nothing.
385 #[serde(skip_serializing_if = "std::ops::Not::not")]
386 pub advisory: bool,
387}
388
389/// `comment.edited`: a comment's text changed. `changes.body.from` is what
390/// it said before; the comment as it is now is read by `commentId`.
391#[derive(Debug, Serialize)]
392#[serde(rename_all = "camelCase")]
393pub struct CommentEdited {
394 pub comment_id: String,
395 pub repo_id: String,
396 pub number: u32,
397 /// Set when the comment is on a pull request.
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub pull_id: Option<String>,
400 pub changes: CommentChanges,
401}
402
403/// What an edit changed, as `comment.edited` says it.
404#[derive(Debug, Serialize)]
405pub struct CommentChanges {
406 pub body: ChangedFrom,
407}
408
409/// A field's value before a change.
410#[derive(Debug, Serialize)]
411pub struct ChangedFrom {
412 pub from: String,
413}
414
415/// `comment.deleted`: a comment was deleted. It no longer exists when the
416/// event is read, so `comment` is the comment as it was.
417#[derive(Debug, Serialize)]
418#[serde(rename_all = "camelCase")]
419pub struct CommentDeleted {
420 pub comment_id: String,
421 pub repo_id: String,
422 pub number: u32,
423 /// Set when the comment was on a pull request.
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub pull_id: Option<String>,
426 pub comment: DeletedComment,
427}
428
429/// A deleted comment, as it was.
430#[derive(Debug, Serialize)]
431#[serde(rename_all = "camelCase")]
432pub struct DeletedComment {
433 pub id: String,
434 pub body: String,
435 pub author: crate::credentials::Principal,
436 /// RFC 3339.
437 pub created_at: String,
438 /// For a comment on one line of a pull request's change: the file.
439 pub path: Option<String>,
440 /// And the line, as numbered after the change.
441 pub line: Option<u32>,
442}
443
444/// The payload of every `release.*` event: `release.created`,
445/// `release.published`, `release.released`, `release.prereleased`,
446/// `release.edited`, `release.unpublished` and `release.deleted`. One change
447/// to a release can be several of them (see [`release_actions`]).
448#[derive(Debug, Serialize)]
449#[serde(rename_all = "camelCase")]
450pub struct ReleaseEvent {
451 pub release_id: String,
452 pub repo_id: String,
453 pub tag_name: String,
454 /// As it is now; as it was, for `release.deleted`.
455 pub release: crate::about::Release,
456 /// On `release.edited`: `{ "name": { "from" }, "body": { "from" } }`
457 /// for what changed.
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub changes: Option<serde_json::Value>,
460}
461
462/// A release's state, for [`release_actions`]: whether it is a draft, and
463/// whether it is a prerelease.
464#[derive(Clone, Copy, Debug, PartialEq, Eq)]
465pub struct ReleaseState {
466 pub draft: bool,
467 pub prerelease: bool,
468}
469
470/// What a change to a release is, as GitHub's release activity types, in
471/// the order they are sent: from `before` (None: it did not exist) to
472/// `after` (None: deleted). A release made published is `created`,
473/// `published` and `released` (or `prereleased`); a draft published is
474/// `published` and `released` (or `prereleased`); made a draft again,
475/// `unpublished`; anything else `edited`, and a prerelease made a full
476/// release is `released` too.
477pub fn release_actions(before: Option<ReleaseState>, after: Option<ReleaseState>) -> Vec<&'static str> {
478 let published = |state: ReleaseState| if state.prerelease { "prereleased" } else { "released" };
479 match (before, after) {
480 (None, None) => Vec::new(),
481 (None, Some(after)) if after.draft => vec!["created"],
482 (None, Some(after)) => vec!["created", "published", published(after)],
483 (Some(_), None) => vec!["deleted"],
484 (Some(before), Some(after)) if before.draft && !after.draft => vec!["published", published(after)],
485 (Some(before), Some(after)) if !before.draft && after.draft => vec!["unpublished"],
486 (Some(before), Some(after)) if !after.draft && before.prerelease && !after.prerelease => vec!["edited", "released"],
487 (Some(_), Some(_)) => vec!["edited"],
488 }
489}
490
491/// The event kind for a release activity type from [`release_actions`].
492pub fn release_kind(action: &str) -> Option<&'static str> {
493 Some(match action {
494 "created" => "release.created",
495 "published" => "release.published",
496 "released" => "release.released",
497 "prereleased" => "release.prereleased",
498 "edited" => "release.edited",
499 "unpublished" => "release.unpublished",
500 "deleted" => "release.deleted",
501 _ => return None,
502 })
503}
504
505#[derive(Debug, Serialize)]
506#[serde(rename_all = "camelCase")]
507pub struct SessionAppended {
508 pub pull_id: String,
509 pub repo_id: String,
510 pub number: u32,
511 pub count: u32,
512}
513
514/// An event as stored in the log and delivered to subscribers. `data` is
515/// left as JSON; each reader decodes the types it cares about.
516#[derive(Clone, Debug, Serialize, serde::Deserialize)]
517#[serde(rename_all = "camelCase")]
518pub struct Event {
519 /// Sorts by the time the event was published.
520 pub id: String,
521 #[serde(rename = "type")]
522 pub kind: String,
523 /// The service that published it.
524 pub source: String,
525 /// RFC 3339.
526 pub time: String,
527 /// The repo the event concerns.
528 pub repo_id: Option<String>,
529 /// The user or agent that caused it, if any.
530 pub actor: Option<String>,
531 pub data: serde_json::Value,
532}
533
534/// `publish`, as a publisher sends it. Returns nothing.
535#[derive(Debug, Serialize)]
536pub struct Publish<T: Serialize> {
537 pub events: Vec<NewEvent<T>>,
538}
539
540/// `publish`, as the events service reads it.
541#[derive(Debug, serde::Deserialize)]
542pub struct PublishArgs {
543 pub events: Vec<Published>,
544}
545
546/// A [`NewEvent`] of any type, as received.
547#[derive(Debug, serde::Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct Published {
550 #[serde(rename = "type")]
551 pub kind: String,
552 pub source: String,
553 #[serde(default)]
554 pub repo_id: Option<String>,
555 #[serde(default)]
556 pub actor: Option<String>,
557 pub data: serde_json::Value,
558}
559
560/// `list`: events from the log, newest first. Returns `Vec<Event>`.
561#[derive(Debug, Default, Serialize, serde::Deserialize)]
562#[serde(rename_all = "camelCase")]
563pub struct ListArgs {
564 #[serde(default)]
565 pub repo_id: Option<String>,
566 /// Only these types; all types when empty.
567 #[serde(default)]
568 pub types: Vec<String>,
569 /// Only events older than this event id.
570 #[serde(default)]
571 pub before: Option<String>,
572 /// Only events by this account id.
573 #[serde(default)]
574 pub actor: Option<String>,
575 /// Only events about these issues or pull requests: their `number`, or
576 /// the `issue` a comment, review or link is on. All when empty.
577 #[serde(default)]
578 pub numbers: Vec<u32>,
579 /// Only events at or after this RFC 3339 time.
580 #[serde(default)]
581 pub since: Option<String>,
582 #[serde(default)]
583 pub limit: Option<u32>,
584}
585
586/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
587/// Every service that stores a slug moves its rows to the workspace's
588/// *current* slug (ask identity by `workspace_id`), so that a repeated or
589/// late delivery after a second rename still lands in the right place.
590#[derive(Clone, Debug, Serialize, serde::Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub struct WorkspaceRenamed {
593 pub workspace_id: String,
594 pub from: String,
595 pub to: String,
596}
597
598impl WorkspaceRenamed {
599 /// The slugs whose rows move to `current`: the two this rename names,
600 /// minus `current` itself. Moving rows keyed by either converges on the
601 /// current slug whatever order renames are delivered in.
602 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
603 let mut slugs: Vec<String> = Vec::new();
604 for slug in [&self.from, &self.to] {
605 if slug != current && !slugs.contains(slug) {
606 slugs.push(slug.clone());
607 }
608 }
609 slugs
610 }
611}
612
613/// `repo.updated`: a repository's description, topics or visibility
614/// changed. `visibility_changed` says whether it went public or private,
615/// which `repo.visibility_changed` also announces on its own.
616#[derive(Debug, Serialize, serde::Deserialize)]
617#[serde(rename_all = "camelCase")]
618pub struct RepoUpdated {
619 pub repo_id: String,
620 pub namespace: String,
621 pub name: String,
622 pub is_private: bool,
623 #[serde(default)]
624 pub visibility_changed: bool,
625}
626
627/// `repo.visibility_changed`: a repository went public or private.
628#[derive(Debug, Serialize, serde::Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct RepoVisibilityChanged {
631 pub repo_id: String,
632 pub is_private: bool,
633}
634
635/// `repo.renamed`: a repository's name changed within its workspace,
636/// keeping its id and its git store key. Like `repo.transferred`, a path
637/// change: every service that keeps rows under the repository's path moves
638/// them to its *current* path (ask repos `path_by_id`), so a repeated or
639/// late delivery after a second rename or a transfer still lands in the
640/// right place. `g1t_kit::transfer::on_event` handles both.
641#[derive(Clone, Debug, Serialize, serde::Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub struct RepoRenamed {
644 pub repo_id: String,
645 /// The workspace it is in.
646 pub namespace: String,
647 /// Its old name.
648 pub from: String,
649 /// Its new name.
650 pub to: String,
651}
652
653impl RepoRenamed {
654 /// The paths whose rows move to `current` (`namespace/name`): the two
655 /// this rename names, minus `current`.
656 pub fn stale_paths(&self, current: &str) -> Vec<String> {
657 let mut paths: Vec<String> = Vec::new();
658 for name in [&self.from, &self.to] {
659 let path = format!("{}/{name}", self.namespace);
660 if path != current && !paths.contains(&path) {
661 paths.push(path);
662 }
663 }
664 paths
665 }
666}
667
668/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
669/// git refuses it, but it can be restored until `purge_after`, so services
670/// stop what runs for it (agents, workflows, deployments, indexing,
671/// webhook deliveries) and hide it, and keep what they hold until
672/// `repo.purged`. `repo.restored` brings it back.
673#[derive(Clone, Debug, Serialize, serde::Deserialize)]
674#[serde(rename_all = "camelCase")]
675pub struct RepoDeleted {
676 pub repo_id: String,
677 #[serde(default)]
678 pub namespace: String,
679 #[serde(default)]
680 pub name: String,
681 #[serde(default)]
682 pub is_private: bool,
683 /// RFC 3339: when it is purged unless restored first.
684 #[serde(default)]
685 pub purge_after: String,
686 /// It went with its workspace (`workspace.deleting`). Services that
687 /// handle the workspace as a whole (deployments pauses its apps rather
688 /// than taking them down) leave this one to that.
689 #[serde(default)]
690 pub with_workspace: bool,
691}
692
693/// `repo.restored`: a deleted repository is back, at its path, as it was.
694/// Services start again what `repo.deleted` stopped: index it, deploy its
695/// production, show it.
696#[derive(Clone, Debug, Serialize, serde::Deserialize)]
697#[serde(rename_all = "camelCase")]
698pub struct RepoRestored {
699 pub repo_id: String,
700 pub namespace: String,
701 pub name: String,
702 pub is_private: bool,
703 /// It came back with its workspace (`workspace.restored`).
704 #[serde(default)]
705 pub with_workspace: bool,
706}
707
708/// `repo.purged`: a deleted repository is gone for good, its git data
709/// with it. Services drop every row they keep for it by `repo_id`, except
710/// history that belongs to its workspace: ledgers, invoices and the audit
711/// log. Its path is free for a new repository.
712#[derive(Clone, Debug, Serialize, serde::Deserialize)]
713#[serde(rename_all = "camelCase")]
714pub struct RepoPurged {
715 pub repo_id: String,
716 pub namespace: String,
717 pub name: String,
718}
719
720/// `repo.archived` and `repo.unarchived`: a repository became read-only,
721/// or writable again. While archived, pushes are refused, issues and pull
722/// requests are locked, and agents and workflows do not run for it; its
723/// deployments keep serving.
724#[derive(Clone, Debug, Serialize, serde::Deserialize)]
725#[serde(rename_all = "camelCase")]
726pub struct RepoArchived {
727 pub repo_id: String,
728 pub namespace: String,
729 pub name: String,
730 pub archived: bool,
731}
732
733/// `repo.default_branch_changed`: the branch everything lands on is now
734/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
735/// requests into it now target `to`) rather than another branch chosen.
736#[derive(Clone, Debug, Serialize, serde::Deserialize)]
737#[serde(rename_all = "camelCase")]
738pub struct RepoDefaultBranchChanged {
739 pub repo_id: String,
740 pub from: String,
741 pub to: String,
742 #[serde(default)]
743 pub renamed: bool,
744}
745
746/// `branch.renamed`: a branch was renamed. Pull requests from or into
747/// `from` follow it to `to`, and web addresses that name `from` redirect.
748#[derive(Clone, Debug, Serialize, serde::Deserialize)]
749#[serde(rename_all = "camelCase")]
750pub struct BranchRenamed {
751 pub repo_id: String,
752 pub from: String,
753 pub to: String,
754 /// Whether it is the default branch.
755 #[serde(default)]
756 pub default_branch: bool,
757}
758
759/// `repo.transferred`: a repository moved from one workspace to another,
760/// keeping its id and its name. Every service that keeps a repository
761/// under its path (`namespace/name`) or its workspace's slug moves those
762/// rows to the repository's *current* path (ask repos `path_by_id`), so a
763/// repeated or late delivery after a second transfer still lands in the
764/// right place. What was charged or recorded before the transfer stays
765/// with the workspace it happened in.
766#[derive(Clone, Debug, Serialize, serde::Deserialize)]
767#[serde(rename_all = "camelCase")]
768pub struct RepoTransferred {
769 pub repo_id: String,
770 pub name: String,
771 /// The workspace it left.
772 pub from: String,
773 /// The workspace it went to.
774 pub to: String,
775}
776
777impl RepoTransferred {
778 /// The paths whose rows move to `current` (`namespace/name`): the two
779 /// this transfer names, minus `current`. Moving rows keyed by either
780 /// converges whatever order transfers are delivered in.
781 pub fn stale_paths(&self, current: &str) -> Vec<String> {
782 let mut paths: Vec<String> = Vec::new();
783 for namespace in [&self.from, &self.to] {
784 let path = format!("{namespace}/{}", self.name);
785 if path != current && !paths.contains(&path) {
786 paths.push(path);
787 }
788 }
789 paths
790 }
791}
792
793/// `workspace.deleted`: a workspace is gone. Services drop what they keep
794/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
795/// agent queue) and keep what is history: ledgers, invoices and the audit
796/// log stay under its slug, which is never given to another workspace.
797#[derive(Clone, Debug, Serialize, serde::Deserialize)]
798#[serde(rename_all = "camelCase")]
799pub struct WorkspaceDeleted {
800 pub workspace_id: String,
801 pub slug: String,
802}
803
804/// `workspace.deleting`: an owner deleted a workspace, and it can be
805/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
806/// meantime. Services hide what they keep for it and stop what runs for it,
807/// keeping their rows: repos deletes its repositories softly (each with a
808/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
809/// apps, search drops it from results. `workspace.restored` undoes exactly
810/// that; once `purge_after` passes, `workspace.deleted` follows and
811/// services purge as for any deleted workspace.
812#[derive(Clone, Debug, Serialize, serde::Deserialize)]
813#[serde(rename_all = "camelCase")]
814pub struct WorkspaceDeleting {
815 pub workspace_id: String,
816 pub slug: String,
817 /// The username of the owner who deleted it.
818 pub by: String,
819 /// RFC 3339: when it is purged unless restored first.
820 pub purge_after: String,
821}
822
823/// `workspace.restored`: staff brought a deleted workspace back, with its
824/// members and tokens. Services undo what they did on `workspace.deleting`,
825/// and only that: a repository deleted on its own before stays deleted.
826#[derive(Clone, Debug, Serialize, serde::Deserialize)]
827#[serde(rename_all = "camelCase")]
828pub struct WorkspaceRestored {
829 pub workspace_id: String,
830 pub slug: String,
831}
832
833/// `user.deleting`: an account was deleted, by the person or by g1t's
834/// staff, and can be restored by staff until `purge_after`. Its sessions,
835/// tokens and keys have ended and it has left every workspace; services
836/// stop what they do for it (search drops its profile, nothing more is
837/// sent to it) and keep their rows. `user.restored` undoes that; once
838/// `purge_after` passes, `user.deleted` follows.
839#[derive(Clone, Debug, Serialize, serde::Deserialize)]
840#[serde(rename_all = "camelCase")]
841pub struct UserDeleting {
842 pub user_id: String,
843 pub username: String,
844 /// Whether g1t's staff deleted it rather than the person.
845 #[serde(default)]
846 pub by_staff: bool,
847 /// RFC 3339: when it is purged unless restored first.
848 pub purge_after: String,
849}
850
851/// `user.restored`: staff brought a deleted account back. It signs in
852/// again with its password; its old sessions and tokens stay ended.
853/// Services undo what they did on `user.deleting`.
854#[derive(Clone, Debug, Serialize, serde::Deserialize)]
855#[serde(rename_all = "camelCase")]
856pub struct UserRestored {
857 pub user_id: String,
858 pub username: String,
859}
860
861/// `user.deleted`: an account is gone for good. Services drop what they
862/// keep for it alone (its inbox, subscriptions, settings) and show what it
863/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
864/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
865/// its username. The username is never given to anyone again.
866#[derive(Clone, Debug, Serialize, serde::Deserialize)]
867#[serde(rename_all = "camelCase")]
868pub struct UserDeleted {
869 pub user_id: String,
870 pub username: String,
871}
872
873/// `user.updated`: an account was made, or changed what its profile shows
874/// (name, bio, avatar). Nothing private: ask identity for the profile.
875#[derive(Debug, Serialize, serde::Deserialize)]
876#[serde(rename_all = "camelCase")]
877pub struct UserUpdated {
878 pub username: String,
879}
880
881/// `user.email_added`, `user.email_verified`, `user.email_removed` and
882/// `user.primary_email_changed`: an account's addresses changed. Never the
883/// address itself; ask identity, as the person, for that.
884#[derive(Debug, Serialize, serde::Deserialize)]
885#[serde(rename_all = "camelCase")]
886pub struct UserEmailChanged {
887 pub user_id: String,
888 /// Whether g1t staff made the change.
889 #[serde(default)]
890 pub by_staff: bool,
891}
892
893/// `workspace.updated`: a workspace was made, or its name, description or
894/// icon changed. Ask identity for it by slug.
895#[derive(Debug, Serialize, serde::Deserialize)]
896#[serde(rename_all = "camelCase")]
897pub struct WorkspaceUpdated {
898 pub workspace_id: String,
899 pub slug: String,
900}
901
902/// `invite.created`: someone (or staff) made an invite. Never the code or
903/// the address it is for.
904#[derive(Debug, Serialize, serde::Deserialize)]
905#[serde(rename_all = "camelCase")]
906pub struct InviteCreated {
907 pub invite_id: String,
908 /// The account that made it; null when staff did.
909 pub inviter_id: Option<String>,
910 /// The workspace it joins.
911 pub workspace_id: Option<String>,
912 /// Whether it is bound to one email address.
913 pub bound: bool,
914}
915
916/// `invite.redeemed`: an invite was used, by a new account or by an
917/// existing one joining a workspace.
918#[derive(Debug, Serialize, serde::Deserialize)]
919#[serde(rename_all = "camelCase")]
920pub struct InviteRedeemed {
921 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
922 /// the account.
923 pub invite_id: String,
924 pub user_id: String,
925 pub inviter_id: Option<String>,
926 pub workspace_id: Option<String>,
927 /// Whether it made the account.
928 pub created_account: bool,
929}
930
931/// `waitlist.requested`: someone asked for access. Ask identity's staff
932/// methods for the entry; the address is not in the event.
933#[derive(Debug, Serialize, serde::Deserialize)]
934#[serde(rename_all = "camelCase")]
935pub struct WaitlistRequested {
936 pub entry_id: String,
937}
938
939/// The payload of `package.published`, `package.version_deleted`,
940/// `package.deleted` and `package.visibility_changed`; each uses the fields
941/// that apply to it. `repo_id` is the repository the package is linked to.
942#[derive(Clone, Debug, Default, Serialize)]
943#[serde(rename_all = "camelCase")]
944pub struct PackageEvent {
945 pub package_id: String,
946 pub workspace: String,
947 pub ecosystem: String,
948 pub name: String,
949 pub repo_id: Option<String>,
950 /// The version published or deleted: for a container image, its
951 /// manifest's digest.
952 #[serde(skip_serializing_if = "Option::is_none")]
953 pub version: Option<String>,
954 #[serde(skip_serializing_if = "Option::is_none")]
955 pub digest: Option<String>,
956 #[serde(skip_serializing_if = "Option::is_none")]
957 pub size: Option<u64>,
958 /// On publish: the tags that now point to the version.
959 #[serde(skip_serializing_if = "Option::is_none")]
960 pub tags: Option<Vec<String>>,
961 /// On `package.visibility_changed`: `public` or `private`.
962 #[serde(skip_serializing_if = "Option::is_none")]
963 pub visibility: Option<String>,
964}
965
966/// `queue.changed`: a repository's merge queue gained, lost or settled an
967/// entry, so the next batch may be ready to test.
968#[derive(Debug, Serialize)]
969#[serde(rename_all = "camelCase")]
970pub struct QueueChanged {
971 pub repo_id: String,
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 #[test]
979 fn comment_edits_and_deletes_say_what_it_was() {
980 let edited = CommentEdited {
981 comment_id: "cmt_1".into(),
982 repo_id: "rep_1".into(),
983 number: 7,
984 pull_id: Some("pr_1".into()),
985 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
986 };
987 assert_eq!(
988 serde_json::to_value(edited).unwrap(),
989 serde_json::json!({
990 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
991 "changes": { "body": { "from": "Before" } },
992 })
993 );
994 let deleted = CommentDeleted {
995 comment_id: "cmt_1".into(),
996 repo_id: "rep_1".into(),
997 number: 7,
998 pull_id: None,
999 comment: DeletedComment {
1000 id: "cmt_1".into(),
1001 body: "Gone".into(),
1002 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1003 created_at: "2026-10-08T00:00:00Z".into(),
1004 path: None,
1005 line: None,
1006 },
1007 };
1008 assert_eq!(
1009 serde_json::to_value(deleted).unwrap(),
1010 serde_json::json!({
1011 "commentId": "cmt_1", "repoId": "rep_1", "number": 7,
1012 "comment": {
1013 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1014 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1015 },
1016 })
1017 );
1018 }
1019
1020 #[test]
1021 fn a_release_change_is_githubs_activity_types() {
1022 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1023 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1024 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1025 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1026 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1027 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1028 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1029 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1030 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1031 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1032 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1033 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1034 }
1035 assert_eq!(release_kind("archived"), None);
1036 }
1037
1038 #[test]
1039 fn what_a_job_token_did_is_marked_and_carried_on() {
1040 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1041 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1042 assert_eq!(caused_by_job(&plain), None);
1043 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1044 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1045 ..Default::default()
1046 }));
1047 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1048 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1049 assert_eq!(by_job["number"], 4);
1050 // A push by the job's token, and the pull request it moves.
1051 let push = GitPush {
1052 repo_id: "rep_1".into(),
1053 git_ref: "refs/heads/fix".into(),
1054 before: Some("a".into()),
1055 after: "b".into(),
1056 default_branch: false,
1057 unscanned: false,
1058 caused_by_job: job_run_of(&actor).map(str::to_owned),
1059 };
1060 let push = serde_json::to_value(push).unwrap();
1061 assert_eq!(caused_by_job(&push), Some("run_9"));
1062 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1063 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1064 }
1065
1066 #[test]
1067 fn a_push_says_it_was_unscanned_only_when_it_was() {
1068 let push = |unscanned| GitPush {
1069 repo_id: "rep_1".into(),
1070 git_ref: "refs/heads/import".into(),
1071 before: None,
1072 after: "abc".into(),
1073 default_branch: false,
1074 unscanned,
1075 caused_by_job: None,
1076 };
1077 let quiet = serde_json::to_value(push(false)).unwrap();
1078 assert!(quiet.get("unscanned").is_none());
1079 let flagged = serde_json::to_value(push(true)).unwrap();
1080 assert_eq!(flagged["unscanned"], true);
1081 assert_eq!(flagged["ref"], "refs/heads/import");
1082 }
1083
1084 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1085 WorkspaceRenamed {
1086 workspace_id: "wsp_1".into(),
1087 from: from.into(),
1088 to: to.into(),
1089 }
1090 }
1091
1092 #[test]
1093 fn stale_slugs_leave_out_the_current_one() {
1094 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1095 // Delivered after a second rename, b → c: both move to c.
1096 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1097 // Renamed back: a → b → a.
1098 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1099 }
1100
1101 fn transferred(from: &str, to: &str) -> RepoTransferred {
1102 RepoTransferred {
1103 repo_id: "rep_1".into(),
1104 name: "rocket".into(),
1105 from: from.into(),
1106 to: to.into(),
1107 }
1108 }
1109
1110 #[test]
1111 fn stale_paths_leave_out_the_current_one() {
1112 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1113 // Delivered after a second transfer, b → c: both move to c.
1114 assert_eq!(
1115 transferred("a", "b").stale_paths("c/rocket"),
1116 vec!["a/rocket", "b/rocket"]
1117 );
1118 // Transferred back: a → b → a.
1119 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1120 }
1121
1122 #[test]
1123 fn a_transfer_reads_as_published() {
1124 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1125 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1126 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1127 }
1128
1129 #[test]
1130 fn a_rename_names_both_paths_in_its_workspace() {
1131 let renamed = RepoRenamed {
1132 repo_id: "rep_1".into(),
1133 namespace: "acme".into(),
1134 from: "old".into(),
1135 to: "new".into(),
1136 };
1137 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1138 // Delivered after a transfer: both names in acme move.
1139 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1140 }
1141
1142 #[test]
1143 fn reads_the_published_payload() {
1144 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1145 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1146 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1147 }
1148}