Skip to content

g1t/packages/contracts/src/events.ts

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