Skip to content

g1t/packages/contracts/src/events.ts

416 lines17,855 bytesCodeBlame
1/**
2 * Every state change in g1t is published as an event. Services react to
3 * each other through events rather than direct calls, and the same stream
4 * feeds timelines, webhooks and workflows.
5 *
6 * The envelope follows CloudEvents: `type` says what happened, `subject`
7 * says to what, `data` is the type-specific payload.
8 */
9
10import type { RepoRole } from "./access";
11import type { CheckRunEventData, CheckSuiteEventData, StatusEventData } from "./checks";
12import type { DeploymentStatus, RepoDeployment } from "./deployments";
13import type { TeamRole, TeamVisibility } from "./teams";
14import type { Confidence, Verdict } from "./work";
15
16/** What every `package.*` event names. */
17export type PackageEventData = {
18 packageId: string;
19 workspace: string;
20 ecosystem: string;
21 name: string;
22 repoId: string | null;
23};
24
25/** What `deployment.succeeded` and `deployment.failed` carry. */
26export type DeploymentEventData = {
27 deploymentId: string;
28 projectId: string;
29 repoId: string;
30 workspace: string;
31 project: string;
32 kind: "production" | "preview";
33 branch: string | null;
34 number: number | null;
35 commit: string;
36 path: string;
37 error: string | null;
38 recovered: boolean;
39 /** A username, or `g1t`. */
40 triggeredBy: string;
41};
42
43/** The payload of the `repo.collaborator_*` events. */
44export type RepoCollaboratorData = {
45 repoId: string;
46 namespace: string;
47 name: string;
48 username: string;
49 role: RepoRole | null;
50 previousRole: RepoRole | null;
51};
52/** A team asked to review a pull request. */
53export type TeamRequested = { team: string; notified: string[]; assigned: string[] };
54
55/** The payload of the `team.*` events; each sets the fields that apply. */
56export type TeamChangedData = {
57 workspace: string;
58 teamId: string;
59 team: string;
60 name: string;
61 visibility: TeamVisibility | null;
62 parent?: string;
63 changes?: string[];
64 username?: string;
65 role?: TeamRole;
66 previousRole?: TeamRole;
67 repoId?: string;
68 repo?: string;
69 repoRole?: RepoRole;
70 previousRepoRole?: RepoRole;
71};
72
73export type EventPayloads = {
74 "repo.created": { repoId: string; namespace: string; name: string; isPrivate: boolean };
75 "repo.forked": { repoId: string; sourceRepoId: string; pullId: string };
76 /**
77 * A repository's description, topics or visibility changed.
78 * `visibilityChanged` says whether it went public or private, which
79 * `repo.visibility_changed` also announces on its own.
80 */
81 "repo.updated": { repoId: string; namespace: string; name: string; isPrivate: boolean; visibilityChanged: boolean };
82 "repo.visibility_changed": { repoId: string; isPrivate: boolean };
83 /**
84 * A repository's name changed within its workspace `namespace`, from
85 * `from` to `to`, keeping its id. A path change like `repo.transferred`:
86 * services move rows kept under its path to its *current* path (see
87 * `repoMove`, `currentMovedPath`).
88 */
89 "repo.renamed": { repoId: string; namespace: string; from: string; to: string };
90 /**
91 * A repository was deleted. It is hidden and git refuses it, but it can
92 * be restored until `purgeAfter`: services stop what runs for it and hide
93 * it, and keep their rows until `repo.purged`.
94 */
95 "repo.deleted": {
96 repoId: string;
97 namespace: string;
98 name: string;
99 isPrivate: boolean;
100 purgeAfter: string;
101 /** It went with its workspace (`workspace.deleting`); deployments leaves it to that. */
102 withWorkspace?: boolean;
103 };
104 /** A deleted repository is back, as it was. Services start again what they stopped. */
105 "repo.restored": {
106 repoId: string;
107 namespace: string;
108 name: string;
109 isPrivate: boolean;
110 /** It came back with its workspace (`workspace.restored`). */
111 withWorkspace?: boolean;
112 };
113 /**
114 * A deleted repository is gone for good, its git data with it. Services
115 * drop every row they keep for it, except a workspace's history (ledgers,
116 * invoices, the audit log).
117 */
118 "repo.purged": { repoId: string; namespace: string; name: string };
119 /**
120 * A repository was archived (read-only: pushes, merges, agents and
121 * workflows refused; issues and pull requests locked; deployments keep
122 * serving), or unarchived.
123 */
124 "repo.archived": { repoId: string; namespace: string; name: string; archived: true };
125 "repo.unarchived": { repoId: string; namespace: string; name: string; archived: false };
126 /** The default branch is now `to`; `renamed` when `from` was renamed to it. */
127 "repo.default_branch_changed": { repoId: string; from: string; to: string; renamed: boolean };
128 /** A branch was renamed. Pull requests from it follow. */
129 "branch.renamed": { repoId: string; from: string; to: string; defaultBranch: boolean };
130 /** An account was made, or changed what its profile shows. Ask identity for the profile. */
131 "user.updated": { username: string };
132 /** A workspace was made, or its name, description or icon changed. */
133 "workspace.updated": { workspaceId: string; slug: string };
134 /** Someone, or g1t staff, made an invite. Never the code or the address. */
135 "invite.created": { inviteId: string; inviterId: string | null; workspaceId: string | null; bound: boolean };
136 /** An invite was used: by a new account, or by an account joining a workspace. */
137 "invite.redeemed": {
138 inviteId: string;
139 userId: string;
140 inviterId: string | null;
141 workspaceId: string | null;
142 createdAccount: boolean;
143 };
144 /** Someone asked for access while registration is invite-only. The address is not in the event. */
145 "waitlist.requested": { entryId: string };
146 /**
147 * One branch moved by a push. `ref` is the full ref, `after` the commit it
148 * points to now, and `defaultBranch` whether it is the default branch.
149 */
150 /** `before` is where the ref pointed before; absent for a new branch or tag. */
151 "git.push": { repoId: string; ref: string; before?: string; after: string; defaultBranch: boolean };
152 /**
153 * `author` is who opened it: g1t, for one its agent filed while at work,
154 * with `requestedBy` the person it was working for. Every issue and pull
155 * request event carries both.
156 */
157 "issue.opened": {
158 issueId: string;
159 repoId: string;
160 number: number;
161 title: string;
162 author?: { id: string; username: string };
163 requestedBy?: { id: string; username: string };
164 };
165 "issue.updated": { issueId: string; repoId: string; number: number };
166 /** The people an issue is assigned to changed; `assignees` is the new set, `added` those newly assigned. */
167 "issue.assigned": { issueId: string; repoId: string; number: number; assignees: string[]; added?: string[] };
168 /** `resolvedBy` is the number of the pull request whose merge closed it. */
169 "issue.closed": {
170 issueId: string;
171 repoId: string;
172 number: number;
173 reason: "completed" | "not_planned";
174 resolvedBy?: number;
175 };
176 "issue.reopened": { issueId: string; repoId: string; number: number };
177 /**
178 * `issue` is the number of the issue the pull request is for. `author` is
179 * who opened it: g1t, for a change g1t made, with `requestedBy` the person
180 * who asked for it.
181 */
182 "pull.opened": {
183 pullId: string;
184 repoId: string;
185 number: number;
186 issue?: number;
187 agent: string;
188 author?: { id: string; username: string };
189 requestedBy?: { id: string; username: string };
190 };
191 /** `confidence`, on a g1t agent's change once g1t has worked it out, is on every pull request event. */
192 "pull.ready": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence };
193 /** A push moved the head of a pull request that is ready for review. */
194 "pull.updated": { pullId: string; repoId: string; number: number; issue?: number; commit: string; confidence?: Confidence };
195 /** A merge was asked for while the pull request was behind; it has to catch up first. */
196 "pull.merge_requested": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence };
197 "pull.closed": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence };
198 /** People were assigned to a pull request: `assignees` is the new set, `added` those newly assigned. */
199 "pull.assigned": { pullId: string; repoId: string; number: number; issue?: number; assignees: string[]; added: string[] };
200 /** Reviewers were asked for a pull request (`reviewers`), or no longer are. */
201 "pull.review_requested": {
202 pullId: string;
203 repoId: string;
204 number: number;
205 issue?: number;
206 reviewers?: string[];
207 /** Teams asked: `team` is `workspace/slug`, `notified` who is told, `assigned` who review assignment picked. */
208 teams?: TeamRequested[];
209 /** Asked because they own files it changes (its CODEOWNERS file). */
210 codeOwners?: boolean;
211 };
212 "pull.review_request_removed": {
213 pullId: string;
214 repoId: string;
215 number: number;
216 issue?: number;
217 reviewers?: string[];
218 teams?: TeamRequested[];
219 };
220 /** g1t stopped seeing a pull request through until a person steps in; `detail` says why. */
221 "pull.stalled": { pullId: string; repoId: string; number: number; issue?: number; detail: string };
222 /** A pull request g1t had stopped on is going again. */
223 "pull.resumed": { pullId: string; repoId: string; number: number; issue?: number };
224 /**
225 * The pull request's head or its target moved and the files both changed
226 * overlap: a sandbox should find out whether it still merges cleanly.
227 * `commit` is its head.
228 */
229 "pull.mergecheck": { pullId: string; repoId: string; number: number; issue?: number; commit: string };
230 /** Whether the pull request merges cleanly was settled. */
231 "pull.mergeability": { pullId: string; repoId: string; number: number; issue?: number };
232 /** Another agent asked the agent on a pull request, which was not at work, a question or handed it work. */
233 "agent.asked": { pullId: string; repoId: string; number: number; issue?: number };
234 "pull.merged": { pullId: string; repoId: string; number: number; issue?: number; commit: string; confidence?: Confidence };
235 /** A run of the acceptance checks finished. `commit` is what was checked. */
236 "checks.completed": {
237 pullId: string;
238 repoId: string;
239 number: number;
240 status: "passed" | "failed" | "errored";
241 commit: string;
242 };
243 /** A status was set on a commit through the API. */
244 "status.created": StatusEventData;
245 /** A check run was reported on a commit through the API, completed, asked to run again, or had one of its buttons pressed (`requestedAction`). */
246 "check_run.created": CheckRunEventData;
247 "check_run.completed": CheckRunEventData;
248 "check_run.rerequested": CheckRunEventData;
249 "check_run.requested_action": CheckRunEventData;
250 /** A reporter's check runs on a commit all completed, or it was asked to run them again. */
251 "check_suite.completed": CheckSuiteEventData;
252 "check_suite.rerequested": CheckSuiteEventData;
253 /** A g1t agent finished reviewing a pull request; no verdict if it could not. */
254 "review.completed": {
255 pullId: string;
256 repoId: string;
257 number: number;
258 verdict?: "approve" | "request_changes";
259 };
260 /** A person was given a role on one repository directly, had it changed, or lost it. `role` is null once removed. */
261 "repo.collaborator_added": RepoCollaboratorData;
262 "repo.collaborator_removed": RepoCollaboratorData;
263 "repo.collaborator_role_changed": RepoCollaboratorData;
264 /** A team of a workspace was created, changed (`changes` says what) or deleted. */
265 "team.created": TeamChangedData;
266 "team.edited": TeamChangedData;
267 "team.deleted": TeamChangedData;
268 /** Someone joined a team, had their role in it changed, or left it. */
269 "team.member_added": TeamChangedData;
270 "team.member_role_changed": TeamChangedData;
271 "team.member_removed": TeamChangedData;
272 /** A team was given a role on a repository, had it changed, or lost it. */
273 "team.repo_added": TeamChangedData;
274 "team.repo_role_changed": TeamChangedData;
275 "team.repo_removed": TeamChangedData;
276 /** A repository's merge queue gained, lost or settled an entry. */
277 "queue.changed": { repoId: string };
278 /** `number` is the issue or pull request commented on. */
279 "comment.created": {
280 commentId: string;
281 repoId: string;
282 number: number;
283 /** Set when the comment is on a pull request. */
284 pullId?: string;
285 /** Set when the comment is a review. */
286 verdict?: Verdict;
287 };
288 "session.appended": { pullId: string; repoId: string; number: number; count: number };
289 /**
290 * A build of a project finished, for production or one pull request's
291 * preview (`number`). `path` is the deployment's page on the site;
292 * `recovered`, on a success, says the build before it failed.
293 */
294 "deployment.succeeded": DeploymentEventData;
295 "deployment.failed": DeploymentEventData;
296 /**
297 * A deployment was made, wherever it runs: reported through the API, by
298 * a g1t Actions job with an `environment:`, or a g1t.page build. Its
299 * `payload` is left out: read the deployment for it.
300 */
301 "deployment.created": { repoId: string; deployment: Omit<RepoDeployment, "payload"> };
302 /** A deployment has a new status; `deployment` is as it is now. */
303 "deployment_status.created": {
304 repoId: string;
305 deployment: Omit<RepoDeployment, "payload">;
306 deploymentStatus: DeploymentStatus;
307 };
308 /**
309 * A package version was published, such as an image pushed by
310 * `docker push`. `tags` are the tags that now point to it; `repoId` (and
311 * the event's) is the repository the package is linked to, if any.
312 */
313 "package.published": PackageEventData & { version: string; digest: string; size: number; tags: string[] };
314 /** One version of a package was deleted, with the tags that pointed to it. */
315 "package.version_deleted": PackageEventData & { version: string; digest: string };
316 /** A package was deleted with every version it had. */
317 "package.deleted": PackageEventData;
318 /** A package became public or private. */
319 "package.visibility_changed": PackageEventData & { visibility: "public" | "private" };
320 /**
321 * A workspace's slug changed from `from` to `to`. Services that store a
322 * slug move their rows to the workspace's *current* slug (see
323 * `currentWorkspaceSlug`), so a repeated or late delivery after a second
324 * rename still lands in the right place.
325 */
326 "workspace.renamed": { workspaceId: string; from: string; to: string };
327 /**
328 * A repository moved from workspace `from` to `to`, keeping its id and
329 * name. Services that store its path or its workspace's slug move those
330 * rows to its *current* path (ask repos `path_by_id`), so a repeated or
331 * late delivery after a second transfer still lands in the right place.
332 */
333 "repo.transferred": { repoId: string; name: string; from: string; to: string };
334 /**
335 * A workspace is gone. Services drop what they keep for it alone; ledgers,
336 * invoices and the audit log stay under its slug, which is never reused.
337 */
338 "workspace.deleted": { workspaceId: string; slug: string };
339 /**
340 * An owner deleted a workspace; staff can restore it until `purgeAfter`,
341 * and nobody can reach it meanwhile. Services hide what they keep for it
342 * and stop what runs for it, keeping their rows: repos deletes its
343 * repositories softly (`repo.deleted` with `withWorkspace`), deployments
344 * pauses its apps, search drops it. `workspace.restored` undoes exactly
345 * that; `workspace.deleted` follows once `purgeAfter` passes. `by` is the
346 * owner's username.
347 */
348 "workspace.deleting": { workspaceId: string; slug: string; by: string; purgeAfter: string };
349 /**
350 * Staff brought a deleted workspace back with its members and tokens.
351 * Services undo what they did on `workspace.deleting`, and only that.
352 */
353 "workspace.restored": { workspaceId: string; slug: string };
354 /**
355 * A memory was added, changed, reviewed or forgotten. No text: ask the
356 * work service for it by id. `repoId` is the project's, or null for the
357 * workspace's memory. `status` is `deleted` once forgotten.
358 */
359 "memory.changed": { memoryId: string; workspace: string; status: "candidate" | "kept" | "dismissed" | "deleted" };
360 /**
361 * A sandbox was stopped because it looked like it was mining: CPU pinned
362 * with little I/O and no progress, or a miner seen by name. For g1t's
363 * staff, in sudo; published with no `repoId` so it never reaches a
364 * repository's timeline or webhooks. `metrics` is what the sandbox
365 * measured (`Verdict` in crates/runner abuse.rs), or null if it could
366 * not say.
367 */
368 "abuse.flagged": {
369 workspace: string;
370 repo: string | null;
371 /** The agent run, when the sandbox had one. */
372 run: string | null;
373 /** What the sandbox was for: agent, checks, queue, actions, deploy... */
374 kind: string;
375 sandbox: string;
376 metrics: Record<string, unknown> | null;
377 };
378};
379
380export type EventType = keyof EventPayloads;
381
382export type G1tEvent<T extends EventType = EventType> = {
383 [K in T]: {
384 id: string;
385 type: K;
386 /** The service that published it. */
387 source: string;
388 /** RFC 3339. */
389 time: string;
390 /** The repo the event concerns, used to scope timelines and deliveries. */
391 repoId: string | null;
392 /** The user or agent that caused it, if any. */
393 actor: string | null;
394 data: EventPayloads[K];
395 };
396}[T];
397
398/** What a publisher supplies; the bus fills in `id` and `time`. */
399export type NewEvent<T extends EventType = EventType> = {
400 [K in T]: Omit<G1tEvent<K>, "id" | "time">;
401}[T];
402
403export type EventQuery = {
404 repoId?: string;
405 types?: EventType[];
406 /** Return events older than this event id. */
407 before?: string;
408 limit?: number;
409};
410
411/** The event bus and its durable log. */
412export interface EventsApi {
413 publish(events: NewEvent[]): Promise<void>;
414 /** Newest first. */
415 list(query: EventQuery): Promise<G1tEvent[]>;
416}