g1t/packages/contracts/src/events.ts

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