Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,478 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01//! Events published on the bus, and the events service that carries them.
2//! Mirrors `packages/contracts/src/events.ts`.
3
4use serde::Serialize;
5
Merge branch 'worktree-agent-a3abfcce648e87dca'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
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.043/// 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,
Merge branch 'worktree-agent-a3abfcce648e87dca'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>,
Merge branch 'mirroring' into artifacts-mode113 /// Set when the push was copied in from the remote a mirror follows,
114 /// not made on g1t. Absent otherwise.
115 #[serde(skip_serializing_if = "std::ops::Not::not")]
116 pub mirrored: bool,
117 /// The repository's mirror state when the push landed (see
118 /// [`crate::mirrors`]): workflows and deployments follow it. Absent for
119 /// a repository that leads.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub mirror: Option<crate::mirrors::RepoMirror>,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.122 /// How many commits the push brought to the branch, counted along its
123 /// first-parent line from `after` back to `before` (a merge commit is
124 /// one), at most 50; one for a new branch. Absent when it was not
125 /// counted: a tag, a branch deleted, a pull request landed, a push the
126 /// store could not read back, or one from before this was recorded.
127 /// Home's digest (`activity_digest`) sums it.
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub commits: Option<u32>,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0130}
131
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r9132/// A branch or tag deleted by a push. `before` is the commit it pointed to.
133#[derive(Debug, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct GitDeleted {
136 pub repo_id: String,
137 /// The full ref, such as `refs/heads/feature`.
138 #[serde(rename = "ref")]
139 pub git_ref: String,
140 pub before: String,
141 /// Set when a workflow job's token deleted it: the run's id (see
142 /// [`CAUSED_BY_JOB`]). Absent otherwise.
143 #[serde(rename = "causedByJob", skip_serializing_if = "Option::is_none")]
144 pub caused_by_job: Option<String>,
145}
146
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0147/// The payload of `issue.opened`, `issue.updated`, `issue.assigned`,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar148/// `issue.labeled`, `issue.unlabeled`, `issue.milestoned`,
149/// `issue.demilestoned`, `issue.closed` and `issue.reopened`; each uses
150/// the fields that apply to it.
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0151#[derive(Debug, Default, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct IssueEvent {
154 pub issue_id: String,
155 pub repo_id: String,
156 pub number: u32,
157 /// Who opened it: g1t, for one its agent filed while at work.
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub author: Option<crate::credentials::Principal>,
160 /// For an issue g1t's agent filed: the person it was working for.
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub requested_by: Option<crate::credentials::Principal>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub title: Option<String>,
165 /// On close: `completed` or `not_planned`.
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub reason: Option<&'static str>,
168 /// On close: the number of the pull request whose merge closed it.
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub resolved_by: Option<u32>,
171 /// On `issue.assigned`: the people it is now assigned to.
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub assignees: Option<Vec<String>>,
Events: review requests, assignments, stops and deployments are published174 /// On `issue.assigned`: those of them who were not before.
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub added: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar177 /// On `issue.labeled` and `issue.unlabeled`: the label put on or taken
178 /// off. One event for each.
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub label: Option<EventLabel>,
181 /// On `issue.milestoned`: the milestone it was put in; on
182 /// `issue.demilestoned`, the one it was taken out of.
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub milestone: Option<crate::work::MilestoneRef>,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0185}
186
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar187/// A label, as `issue.labeled`, `pull.labeled` and their `unlabeled` say.
188#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
189pub struct EventLabel {
190 pub name: String,
191 /// Six hex digits.
192 pub color: String,
193}
194
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0195/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts196/// moved), `pull.closed`, `pull.reopened`, `pull.converted_to_draft`,
197/// `pull.merged`, `pull.assigned`,
Events: review requests, assignments, stops and deployments are published198/// `pull.review_requested` and `pull.review_request_removed` (reviewers
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar199/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
200/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
201/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
202/// through until a person steps in) and `pull.resumed` (it picked back
203/// up); each uses the fields that apply to it.
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0204#[derive(Debug, Default, Serialize)]
205#[serde(rename_all = "camelCase")]
206pub struct PullEvent {
207 pub pull_id: String,
208 pub repo_id: String,
209 pub number: u32,
210 /// Who opened it: g1t, for a change g1t made.
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub author: Option<crate::credentials::Principal>,
213 /// For a change g1t made: the person who asked for it.
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub requested_by: Option<crate::credentials::Principal>,
216 /// The number of the issue it is for.
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub issue: Option<u32>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub agent: Option<String>,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts221 /// On merge: the commit the branch now points to. On update, when
222 /// marked ready and when reopened: the head of the change.
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub commit: Option<String>,
225 /// On close: the pull request that was merged instead.
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub superseded_by: Option<u32>,
228 /// How sure g1t is of a g1t agent's change, once it has worked that out.
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub confidence: Option<crate::work::Confidence>,
Events: review requests, assignments, stops and deployments are published231 /// On `pull.assigned`: the people it is now assigned to.
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub assignees: Option<Vec<String>>,
234 /// On `pull.assigned`: those newly assigned.
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub added: Option<Vec<String>>,
237 /// On `pull.review_requested`: the reviewers newly asked; on
238 /// `pull.review_request_removed`, those no longer asked.
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub reviewers: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar241 /// On `pull.review_requested` and `pull.review_request_removed`: the
242 /// teams newly asked, or no longer, each with the people it asks.
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub teams: Option<Vec<TeamRequested>>,
245 /// On `pull.review_requested`: asked because they own files it changes
246 /// (its CODEOWNERS file), not by a person.
247 #[serde(skip_serializing_if = "std::ops::Not::not")]
248 pub code_owners: bool,
Events: review requests, assignments, stops and deployments are published249 /// On `pull.stalled`: why g1t stopped, and what would start it again.
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub detail: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar252 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub label: Option<EventLabel>,
255 /// On `pull.milestoned`: the milestone it was put in; on
256 /// `pull.demilestoned`, the one it was taken out of.
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub milestone: Option<crate::work::MilestoneRef>,
259 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub base: Option<String>,
262}
263
264/// A team asked to review a pull request.
265#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct TeamRequested {
268 /// `workspace/slug`.
269 pub team: String,
270 /// Everyone in it who is told: the whole team, or, with review
271 /// assignment, the people picked (and the rest when it says to tell
272 /// them). Never the pull request's author.
273 pub notified: Vec<String>,
274 /// With review assignment: the people picked, who are asked as
275 /// reviewers themselves.
276 #[serde(default)]
277 pub assigned: Vec<String>,
278}
279
280/// The payload of every `team.*` event: `team.created`, `team.edited`,
281/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
282/// `team.member_removed` (with `username`, `role` and `previousRole`);
283/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
284/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
285/// name the repository as the event's own.
286#[derive(Clone, Debug, Default, Serialize)]
287#[serde(rename_all = "camelCase")]
288pub struct TeamChanged {
289 pub workspace: String,
290 pub team_id: String,
291 /// The team's slug, as it is now.
292 pub team: String,
293 pub name: String,
294 pub visibility: Option<crate::teams::TeamVisibility>,
295 /// The parent's slug.
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub parent: Option<String>,
298 /// On `team.edited`: what changed, such as `name` or `parent`.
299 #[serde(skip_serializing_if = "Vec::is_empty")]
300 pub changes: Vec<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub username: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub role: Option<crate::teams::TeamRole>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub previous_role: Option<crate::teams::TeamRole>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub repo_id: Option<String>,
309 /// `workspace/name`.
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub repo: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub repo_role: Option<crate::access::RepoRole>,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub previous_repo_role: Option<crate::access::RepoRole>,
Events: review requests, assignments, stops and deployments are published316}
317
318/// `deployment.succeeded` and `deployment.failed`: a build of a project
319/// finished, for production or for one pull request's preview.
320#[derive(Debug, Serialize)]
321#[serde(rename_all = "camelCase")]
322pub struct DeploymentEvent {
323 pub deployment_id: String,
324 pub project_id: String,
325 pub repo_id: String,
326 pub workspace: String,
327 /// The project's slug.
328 pub project: String,
329 /// `production` or `preview`.
330 pub kind: String,
331 pub branch: Option<String>,
332 /// For a preview: its pull request.
333 pub number: Option<u32>,
334 pub commit: String,
335 /// Where the deployment is on the site, such as
336 /// `/acme/rocket/deployments/dpl_1`.
337 pub path: String,
338 /// For a failure: what went wrong.
339 pub error: Option<String>,
340 /// For a success: whether the deployment before it, of the same app, failed.
341 pub recovered: bool,
342 /// Who started it, by username, or `g1t`.
343 pub triggered_by: String,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0344}
345
346/// `checks.completed`: a run of an issue's acceptance checks against a pull
347/// request finished.
348#[derive(Debug, Serialize)]
349#[serde(rename_all = "camelCase")]
350pub struct ChecksEvent {
351 pub pull_id: String,
352 pub repo_id: String,
353 pub number: u32,
354 /// `passed`, `failed` or `errored`.
355 pub status: &'static str,
356 /// The commit that was checked.
357 pub commit: String,
358}
359
360/// `workflow.completed`: a GitHub Actions run finished.
361#[derive(Debug, Serialize)]
362#[serde(rename_all = "camelCase")]
363pub struct WorkflowEvent {
364 pub run_id: String,
365 pub repo_id: String,
366 /// The workflow's name, and its file.
367 pub workflow: String,
368 pub path: String,
369 /// The run's number among the workflow's runs.
370 pub number: u64,
371 /// The GitHub event that started it, such as `push`.
372 pub event: String,
373 /// `success`, `failure`, `cancelled` or `skipped`.
374 pub conclusion: String,
375 #[serde(rename = "ref")]
376 pub git_ref: String,
377 pub sha: String,
378 /// The pull request it ran for, if any.
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub pull: Option<u32>,
381}
382
383/// `review.completed`: a g1t agent finished reviewing a pull request, or
384/// could not.
385#[derive(Debug, Serialize)]
386#[serde(rename_all = "camelCase")]
387pub struct ReviewEvent {
388 pub pull_id: String,
389 pub repo_id: String,
390 pub number: u32,
391 /// `approve` or `request_changes`; absent when no review was written.
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub verdict: Option<&'static str>,
394}
395
396/// `comment.created`. `number` is the issue or pull request commented on.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar397#[derive(Debug, Default, Serialize)]
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0398#[serde(rename_all = "camelCase")]
399pub struct CommentCreated {
400 pub comment_id: String,
401 pub repo_id: String,
402 pub number: u32,
403 /// Set when the comment is on a pull request.
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub pull_id: Option<String>,
406 /// Set when the comment is a review: approve or request changes.
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub verdict: Option<crate::work::Verdict>,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar409 /// Set when one of the workspace's agents wrote it, as itself. The
410 /// event's actor is then the person it acted for (`actingFor`).
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub agent: Option<crate::work::AgentRef>,
413 /// Who the agent acted for, set with `agent`.
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub acting_for: Option<crate::credentials::Principal>,
416 /// An agent's review, advisory: its verdict counts toward nothing.
417 #[serde(skip_serializing_if = "std::ops::Not::not")]
418 pub advisory: bool,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0419}
420
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts421/// `comment.edited`: a comment's text changed. `changes.body.from` is what
422/// it said before; the comment as it is now is read by `commentId`.
423#[derive(Debug, Serialize)]
424#[serde(rename_all = "camelCase")]
425pub struct CommentEdited {
426 pub comment_id: String,
427 pub repo_id: String,
428 pub number: u32,
429 /// Set when the comment is on a pull request.
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub pull_id: Option<String>,
432 pub changes: CommentChanges,
433}
434
435/// What an edit changed, as `comment.edited` says it.
436#[derive(Debug, Serialize)]
437pub struct CommentChanges {
438 pub body: ChangedFrom,
439}
440
441/// A field's value before a change.
442#[derive(Debug, Serialize)]
443pub struct ChangedFrom {
444 pub from: String,
445}
446
447/// `comment.deleted`: a comment was deleted. It no longer exists when the
448/// event is read, so `comment` is the comment as it was.
449#[derive(Debug, Serialize)]
450#[serde(rename_all = "camelCase")]
451pub struct CommentDeleted {
452 pub comment_id: String,
453 pub repo_id: String,
454 pub number: u32,
455 /// Set when the comment was on a pull request.
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub pull_id: Option<String>,
458 pub comment: DeletedComment,
459}
460
461/// A deleted comment, as it was.
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0462#[derive(Debug, Serialize)]
463#[serde(rename_all = "camelCase")]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts464pub struct DeletedComment {
465 pub id: String,
466 pub body: String,
467 pub author: crate::credentials::Principal,
468 /// RFC 3339.
469 pub created_at: String,
470 /// For a comment on one line of a pull request's change: the file.
471 pub path: Option<String>,
472 /// And the line, as numbered after the change.
473 pub line: Option<u32>,
474}
475
476/// The payload of every `release.*` event: `release.created`,
477/// `release.published`, `release.released`, `release.prereleased`,
478/// `release.edited`, `release.unpublished` and `release.deleted`. One change
479/// to a release can be several of them (see [`release_actions`]).
480#[derive(Debug, Serialize)]
481#[serde(rename_all = "camelCase")]
482pub struct ReleaseEvent {
483 pub release_id: String,
484 pub repo_id: String,
485 pub tag_name: String,
486 /// As it is now; as it was, for `release.deleted`.
487 pub release: crate::about::Release,
488 /// On `release.edited`: `{ "name": { "from" }, "body": { "from" } }`
489 /// for what changed.
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub changes: Option<serde_json::Value>,
492}
493
494/// A release's state, for [`release_actions`]: whether it is a draft, and
495/// whether it is a prerelease.
496#[derive(Clone, Copy, Debug, PartialEq, Eq)]
497pub struct ReleaseState {
498 pub draft: bool,
499 pub prerelease: bool,
500}
501
502/// What a change to a release is, as GitHub's release activity types, in
503/// the order they are sent: from `before` (None: it did not exist) to
504/// `after` (None: deleted). A release made published is `created`,
505/// `published` and `released` (or `prereleased`); a draft published is
506/// `published` and `released` (or `prereleased`); made a draft again,
507/// `unpublished`; anything else `edited`, and a prerelease made a full
508/// release is `released` too.
509pub fn release_actions(before: Option<ReleaseState>, after: Option<ReleaseState>) -> Vec<&'static str> {
510 let published = |state: ReleaseState| if state.prerelease { "prereleased" } else { "released" };
511 match (before, after) {
512 (None, None) => Vec::new(),
513 (None, Some(after)) if after.draft => vec!["created"],
514 (None, Some(after)) => vec!["created", "published", published(after)],
515 (Some(_), None) => vec!["deleted"],
516 (Some(before), Some(after)) if before.draft && !after.draft => vec!["published", published(after)],
517 (Some(before), Some(after)) if !before.draft && after.draft => vec!["unpublished"],
518 (Some(before), Some(after)) if !after.draft && before.prerelease && !after.prerelease => vec!["edited", "released"],
519 (Some(_), Some(_)) => vec!["edited"],
520 }
521}
522
523/// The event kind for a release activity type from [`release_actions`].
524pub fn release_kind(action: &str) -> Option<&'static str> {
525 Some(match action {
526 "created" => "release.created",
527 "published" => "release.published",
528 "released" => "release.released",
529 "prereleased" => "release.prereleased",
530 "edited" => "release.edited",
531 "unpublished" => "release.unpublished",
532 "deleted" => "release.deleted",
533 _ => return None,
534 })
535}
536
537#[derive(Debug, Serialize)]
538#[serde(rename_all = "camelCase")]
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0539pub struct SessionAppended {
540 pub pull_id: String,
541 pub repo_id: String,
542 pub number: u32,
543 pub count: u32,
544}
545
546/// An event as stored in the log and delivered to subscribers. `data` is
547/// left as JSON; each reader decodes the types it cares about.
548#[derive(Clone, Debug, Serialize, serde::Deserialize)]
549#[serde(rename_all = "camelCase")]
550pub struct Event {
551 /// Sorts by the time the event was published.
552 pub id: String,
553 #[serde(rename = "type")]
554 pub kind: String,
555 /// The service that published it.
556 pub source: String,
557 /// RFC 3339.
558 pub time: String,
559 /// The repo the event concerns.
560 pub repo_id: Option<String>,
561 /// The user or agent that caused it, if any.
562 pub actor: Option<String>,
563 pub data: serde_json::Value,
564}
565
566/// `publish`, as a publisher sends it. Returns nothing.
567#[derive(Debug, Serialize)]
568pub struct Publish<T: Serialize> {
569 pub events: Vec<NewEvent<T>>,
570}
571
572/// `publish`, as the events service reads it.
573#[derive(Debug, serde::Deserialize)]
574pub struct PublishArgs {
575 pub events: Vec<Published>,
576}
577
578/// A [`NewEvent`] of any type, as received.
579#[derive(Debug, serde::Deserialize)]
580#[serde(rename_all = "camelCase")]
581pub struct Published {
582 #[serde(rename = "type")]
583 pub kind: String,
584 pub source: String,
585 #[serde(default)]
586 pub repo_id: Option<String>,
587 #[serde(default)]
588 pub actor: Option<String>,
589 pub data: serde_json::Value,
590}
591
592/// `list`: events from the log, newest first. Returns `Vec<Event>`.
593#[derive(Debug, Default, Serialize, serde::Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct ListArgs {
596 #[serde(default)]
597 pub repo_id: Option<String>,
598 /// Only these types; all types when empty.
599 #[serde(default)]
600 pub types: Vec<String>,
601 /// Only events older than this event id.
602 #[serde(default)]
603 pub before: Option<String>,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)604 /// Only events by this account id.
605 #[serde(default)]
606 pub actor: Option<String>,
607 /// Only events about these issues or pull requests: their `number`, or
608 /// the `issue` a comment, review or link is on. All when empty.
609 #[serde(default)]
610 pub numbers: Vec<u32>,
611 /// Only events at or after this RFC 3339 time.
612 #[serde(default)]
613 pub since: Option<String>,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0614 #[serde(default)]
615 pub limit: Option<u32>,
616}
617
618/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
619/// Every service that stores a slug moves its rows to the workspace's
620/// *current* slug (ask identity by `workspace_id`), so that a repeated or
621/// late delivery after a second rename still lands in the right place.
622#[derive(Clone, Debug, Serialize, serde::Deserialize)]
623#[serde(rename_all = "camelCase")]
624pub struct WorkspaceRenamed {
625 pub workspace_id: String,
626 pub from: String,
627 pub to: String,
628}
629
630impl WorkspaceRenamed {
631 /// The slugs whose rows move to `current`: the two this rename names,
632 /// minus `current` itself. Moving rows keyed by either converges on the
633 /// current slug whatever order renames are delivered in.
634 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
635 let mut slugs: Vec<String> = Vec::new();
636 for slug in [&self.from, &self.to] {
637 if slug != current && !slugs.contains(slug) {
638 slugs.push(slug.clone());
639 }
640 }
641 slugs
642 }
643}
644
645/// `repo.updated`: a repository's description, topics or visibility
646/// changed. `visibility_changed` says whether it went public or private,
647/// which `repo.visibility_changed` also announces on its own.
648#[derive(Debug, Serialize, serde::Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct RepoUpdated {
651 pub repo_id: String,
652 pub namespace: String,
653 pub name: String,
654 pub is_private: bool,
655 #[serde(default)]
656 pub visibility_changed: bool,
657}
658
659/// `repo.visibility_changed`: a repository went public or private.
660#[derive(Debug, Serialize, serde::Deserialize)]
661#[serde(rename_all = "camelCase")]
662pub struct RepoVisibilityChanged {
663 pub repo_id: String,
664 pub is_private: bool,
665}
666
667/// `repo.renamed`: a repository's name changed within its workspace,
668/// keeping its id and its git store key. Like `repo.transferred`, a path
669/// change: every service that keeps rows under the repository's path moves
670/// them to its *current* path (ask repos `path_by_id`), so a repeated or
671/// late delivery after a second rename or a transfer still lands in the
672/// right place. `g1t_kit::transfer::on_event` handles both.
673#[derive(Clone, Debug, Serialize, serde::Deserialize)]
674#[serde(rename_all = "camelCase")]
675pub struct RepoRenamed {
676 pub repo_id: String,
677 /// The workspace it is in.
678 pub namespace: String,
679 /// Its old name.
680 pub from: String,
681 /// Its new name.
682 pub to: String,
683}
684
685impl RepoRenamed {
686 /// The paths whose rows move to `current` (`namespace/name`): the two
687 /// this rename names, minus `current`.
688 pub fn stale_paths(&self, current: &str) -> Vec<String> {
689 let mut paths: Vec<String> = Vec::new();
690 for name in [&self.from, &self.to] {
691 let path = format!("{}/{name}", self.namespace);
692 if path != current && !paths.contains(&path) {
693 paths.push(path);
694 }
695 }
696 paths
697 }
698}
699
700/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
701/// git refuses it, but it can be restored until `purge_after`, so services
702/// stop what runs for it (agents, workflows, deployments, indexing,
703/// webhook deliveries) and hide it, and keep what they hold until
704/// `repo.purged`. `repo.restored` brings it back.
705#[derive(Clone, Debug, Serialize, serde::Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct RepoDeleted {
708 pub repo_id: String,
709 #[serde(default)]
710 pub namespace: String,
711 #[serde(default)]
712 pub name: String,
713 #[serde(default)]
714 pub is_private: bool,
715 /// RFC 3339: when it is purged unless restored first.
716 #[serde(default)]
717 pub purge_after: String,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member718 /// It went with its workspace (`workspace.deleting`). Services that
719 /// handle the workspace as a whole (deployments pauses its apps rather
720 /// than taking them down) leave this one to that.
721 #[serde(default)]
722 pub with_workspace: bool,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0723}
724
725/// `repo.restored`: a deleted repository is back, at its path, as it was.
726/// Services start again what `repo.deleted` stopped: index it, deploy its
727/// production, show it.
728#[derive(Clone, Debug, Serialize, serde::Deserialize)]
729#[serde(rename_all = "camelCase")]
730pub struct RepoRestored {
731 pub repo_id: String,
732 pub namespace: String,
733 pub name: String,
734 pub is_private: bool,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member735 /// It came back with its workspace (`workspace.restored`).
736 #[serde(default)]
737 pub with_workspace: bool,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0738}
739
740/// `repo.purged`: a deleted repository is gone for good, its git data
741/// with it. Services drop every row they keep for it by `repo_id`, except
742/// history that belongs to its workspace: ledgers, invoices and the audit
743/// log. Its path is free for a new repository.
744#[derive(Clone, Debug, Serialize, serde::Deserialize)]
745#[serde(rename_all = "camelCase")]
746pub struct RepoPurged {
747 pub repo_id: String,
748 pub namespace: String,
749 pub name: String,
750}
751
752/// `repo.archived` and `repo.unarchived`: a repository became read-only,
753/// or writable again. While archived, pushes are refused, issues and pull
754/// requests are locked, and agents and workflows do not run for it; its
755/// deployments keep serving.
756#[derive(Clone, Debug, Serialize, serde::Deserialize)]
757#[serde(rename_all = "camelCase")]
758pub struct RepoArchived {
759 pub repo_id: String,
760 pub namespace: String,
761 pub name: String,
762 pub archived: bool,
763}
764
765/// `repo.default_branch_changed`: the branch everything lands on is now
766/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
767/// requests into it now target `to`) rather than another branch chosen.
768#[derive(Clone, Debug, Serialize, serde::Deserialize)]
769#[serde(rename_all = "camelCase")]
770pub struct RepoDefaultBranchChanged {
771 pub repo_id: String,
772 pub from: String,
773 pub to: String,
774 #[serde(default)]
775 pub renamed: bool,
776}
777
778/// `branch.renamed`: a branch was renamed. Pull requests from or into
779/// `from` follow it to `to`, and web addresses that name `from` redirect.
780#[derive(Clone, Debug, Serialize, serde::Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct BranchRenamed {
783 pub repo_id: String,
784 pub from: String,
785 pub to: String,
786 /// Whether it is the default branch.
787 #[serde(default)]
788 pub default_branch: bool,
789}
790
791/// `repo.transferred`: a repository moved from one workspace to another,
792/// keeping its id and its name. Every service that keeps a repository
793/// under its path (`namespace/name`) or its workspace's slug moves those
794/// rows to the repository's *current* path (ask repos `path_by_id`), so a
795/// repeated or late delivery after a second transfer still lands in the
796/// right place. What was charged or recorded before the transfer stays
797/// with the workspace it happened in.
798#[derive(Clone, Debug, Serialize, serde::Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct RepoTransferred {
801 pub repo_id: String,
802 pub name: String,
803 /// The workspace it left.
804 pub from: String,
805 /// The workspace it went to.
806 pub to: String,
807}
808
809impl RepoTransferred {
810 /// The paths whose rows move to `current` (`namespace/name`): the two
811 /// this transfer names, minus `current`. Moving rows keyed by either
812 /// converges whatever order transfers are delivered in.
813 pub fn stale_paths(&self, current: &str) -> Vec<String> {
814 let mut paths: Vec<String> = Vec::new();
815 for namespace in [&self.from, &self.to] {
816 let path = format!("{namespace}/{}", self.name);
817 if path != current && !paths.contains(&path) {
818 paths.push(path);
819 }
820 }
821 paths
822 }
823}
824
825/// `workspace.deleted`: a workspace is gone. Services drop what they keep
826/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
827/// agent queue) and keep what is history: ledgers, invoices and the audit
828/// log stay under its slug, which is never given to another workspace.
829#[derive(Clone, Debug, Serialize, serde::Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct WorkspaceDeleted {
832 pub workspace_id: String,
833 pub slug: String,
834}
835
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member836/// `workspace.deleting`: an owner deleted a workspace, and it can be
837/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
838/// meantime. Services hide what they keep for it and stop what runs for it,
839/// keeping their rows: repos deletes its repositories softly (each with a
840/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
841/// apps, search drops it from results. `workspace.restored` undoes exactly
842/// that; once `purge_after` passes, `workspace.deleted` follows and
843/// services purge as for any deleted workspace.
844#[derive(Clone, Debug, Serialize, serde::Deserialize)]
845#[serde(rename_all = "camelCase")]
846pub struct WorkspaceDeleting {
847 pub workspace_id: String,
848 pub slug: String,
849 /// The username of the owner who deleted it.
850 pub by: String,
851 /// RFC 3339: when it is purged unless restored first.
852 pub purge_after: String,
853}
854
855/// `workspace.restored`: staff brought a deleted workspace back, with its
856/// members and tokens. Services undo what they did on `workspace.deleting`,
857/// and only that: a repository deleted on its own before stays deleted.
858#[derive(Clone, Debug, Serialize, serde::Deserialize)]
859#[serde(rename_all = "camelCase")]
860pub struct WorkspaceRestored {
861 pub workspace_id: String,
862 pub slug: String,
863}
864
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)865/// `user.deleting`: an account was deleted, by the person or by g1t's
866/// staff, and can be restored by staff until `purge_after`. Its sessions,
867/// tokens and keys have ended and it has left every workspace; services
868/// stop what they do for it (search drops its profile, nothing more is
869/// sent to it) and keep their rows. `user.restored` undoes that; once
870/// `purge_after` passes, `user.deleted` follows.
871#[derive(Clone, Debug, Serialize, serde::Deserialize)]
872#[serde(rename_all = "camelCase")]
873pub struct UserDeleting {
874 pub user_id: String,
875 pub username: String,
876 /// Whether g1t's staff deleted it rather than the person.
877 #[serde(default)]
878 pub by_staff: bool,
879 /// RFC 3339: when it is purged unless restored first.
880 pub purge_after: String,
881}
882
883/// `user.restored`: staff brought a deleted account back. It signs in
884/// again with its password; its old sessions and tokens stay ended.
885/// Services undo what they did on `user.deleting`.
886#[derive(Clone, Debug, Serialize, serde::Deserialize)]
887#[serde(rename_all = "camelCase")]
888pub struct UserRestored {
889 pub user_id: String,
890 pub username: String,
891}
892
893/// `user.deleted`: an account is gone for good. Services drop what they
894/// keep for it alone (its inbox, subscriptions, settings) and show what it
895/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
896/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
897/// its username. The username is never given to anyone again.
898#[derive(Clone, Debug, Serialize, serde::Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct UserDeleted {
901 pub user_id: String,
902 pub username: String,
903}
904
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0905/// `user.updated`: an account was made, or changed what its profile shows
906/// (name, bio, avatar). Nothing private: ask identity for the profile.
907#[derive(Debug, Serialize, serde::Deserialize)]
908#[serde(rename_all = "camelCase")]
909pub struct UserUpdated {
910 pub username: String,
911}
912
913/// `user.email_added`, `user.email_verified`, `user.email_removed` and
914/// `user.primary_email_changed`: an account's addresses changed. Never the
915/// address itself; ask identity, as the person, for that.
916#[derive(Debug, Serialize, serde::Deserialize)]
917#[serde(rename_all = "camelCase")]
918pub struct UserEmailChanged {
919 pub user_id: String,
920 /// Whether g1t staff made the change.
921 #[serde(default)]
922 pub by_staff: bool,
923}
924
925/// `workspace.updated`: a workspace was made, or its name, description or
926/// icon changed. Ask identity for it by slug.
927#[derive(Debug, Serialize, serde::Deserialize)]
928#[serde(rename_all = "camelCase")]
929pub struct WorkspaceUpdated {
930 pub workspace_id: String,
931 pub slug: String,
932}
933
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.934/// How someone came to be a member of a workspace (`workspace.member_joined`).
935#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
936#[serde(rename_all = "snake_case")]
937pub enum JoinedHow {
938 /// They accepted a workspace invitation sent to them by name.
939 Invitation,
940 /// They made the workspace, and are its first owner.
941 Created,
942 /// They used an invite code or link that named the workspace.
943 Joined,
944}
945
946/// `workspace.member_joined`: a person became a member of a workspace,
947/// published by identity as the membership is written. `how` says by
948/// which door; the workspace's creator is `created`, so a listener that
949/// greets new members can leave them out (the agents service's @g1t
950/// welcomes everyone else, once). Never published for an agent or a
951/// token: only a person joins.
952#[derive(Clone, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
953#[serde(rename_all = "camelCase")]
954pub struct MemberJoined {
955 pub workspace_id: String,
956 /// The workspace's slug.
957 pub workspace: String,
958 pub user_id: String,
959 pub username: String,
960 pub role: crate::Role,
961 pub how: JoinedHow,
962}
963
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0964/// `invite.created`: someone (or staff) made an invite. Never the code or
965/// the address it is for.
966#[derive(Debug, Serialize, serde::Deserialize)]
967#[serde(rename_all = "camelCase")]
968pub struct InviteCreated {
969 pub invite_id: String,
970 /// The account that made it; null when staff did.
971 pub inviter_id: Option<String>,
972 /// The workspace it joins.
973 pub workspace_id: Option<String>,
974 /// Whether it is bound to one email address.
975 pub bound: bool,
976}
977
978/// `invite.redeemed`: an invite was used, by a new account or by an
979/// existing one joining a workspace.
980#[derive(Debug, Serialize, serde::Deserialize)]
981#[serde(rename_all = "camelCase")]
982pub struct InviteRedeemed {
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)983 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
984 /// the account.
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0985 pub invite_id: String,
986 pub user_id: String,
987 pub inviter_id: Option<String>,
988 pub workspace_id: Option<String>,
989 /// Whether it made the account.
990 pub created_account: bool,
991}
992
993/// `waitlist.requested`: someone asked for access. Ask identity's staff
994/// methods for the entry; the address is not in the event.
995#[derive(Debug, Serialize, serde::Deserialize)]
996#[serde(rename_all = "camelCase")]
997pub struct WaitlistRequested {
998 pub entry_id: String,
999}
1000
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1001/// The payload of `package.published`, `package.version_deleted`,
1002/// `package.deleted` and `package.visibility_changed`; each uses the fields
1003/// that apply to it. `repo_id` is the repository the package is linked to.
1004#[derive(Clone, Debug, Default, Serialize)]
1005#[serde(rename_all = "camelCase")]
1006pub struct PackageEvent {
1007 pub package_id: String,
1008 pub workspace: String,
1009 pub ecosystem: String,
1010 pub name: String,
1011 pub repo_id: Option<String>,
1012 /// The version published or deleted: for a container image, its
1013 /// manifest's digest.
1014 #[serde(skip_serializing_if = "Option::is_none")]
1015 pub version: Option<String>,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub digest: Option<String>,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1019 pub size: Option<u64>,
1020 /// On publish: the tags that now point to the version.
1021 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub tags: Option<Vec<String>>,
1023 /// On `package.visibility_changed`: `public` or `private`.
1024 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub visibility: Option<String>,
1026}
1027
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01028/// `queue.changed`: a repository's merge queue gained, lost or settled an
1029/// entry, so the next batch may be ready to test.
1030#[derive(Debug, Serialize)]
1031#[serde(rename_all = "camelCase")]
1032pub struct QueueChanged {
1033 pub repo_id: String,
1034}
1035
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.1036/// The `doc.page.*` types the artifacts service (services/artifacts, TypeScript)
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1037/// publishes, with no `repoId` on the event: a page may be in a private
1038/// space, so it never reaches a repository's timeline or webhooks.
1039pub const DOC_PAGE_EVENTS: [&str; 4] = ["doc.page.created", "doc.page.updated", "doc.page.archived", "doc.page.stale"];
1040
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1041/// The `folio.*` types (Artifacts mode), with their payloads, live in
1042/// [`crate::folios`]; nothing publishes or subscribes to them yet.
1043pub use crate::folios::FOLIO_EVENTS;
1044
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1045/// What every `doc.page.*` event carries (`DocPageEventData` in
1046/// events.ts). `doc.page.updated` adds `versionId`, `kind` and `authors`;
1047/// `doc.page.stale` adds `repoId`, `repo`, `commit`, `pull`, `paths` and
1048/// `owners` ([`DocPageStale`]).
1049#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1050#[serde(rename_all = "camelCase")]
1051pub struct DocPageEvent {
1052 pub workspace: String,
1053 pub workspace_id: String,
1054 pub page_id: String,
1055 pub space_id: String,
1056 pub title: String,
1057 /// The page's address on the site.
1058 pub path: String,
1059}
1060
1061/// `doc.page.stale`: code a page cites changed.
1062#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1063#[serde(rename_all = "camelCase")]
1064pub struct DocPageStale {
1065 #[serde(flatten)]
1066 pub page: DocPageEvent,
1067 pub repo_id: String,
1068 /// `owner/name`.
1069 pub repo: String,
1070 pub commit: String,
1071 /// The merged pull request's number, when a pull request made the change.
1072 pub pull: Option<u32>,
1073 pub paths: Vec<String>,
1074 /// Member keys: `user:<id>`, `agent:<id>`.
1075 pub owners: Vec<String>,
1076}
1077
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1078/// `activity_digest`: what happened in some repositories, and to a
1079/// workspace's artifacts, over a span, counted. Returns [`ActivityDigest`].
1080/// Home reads it for what people and agents did since you were last there.
1081/// Fields are snake_case, as every service's arguments are.
1082///
1083/// The span is `[from, until)` in RFC 3339. Event ids sort by time, so the
1084/// service reads each repository's events of each type it counts as one
1085/// range of the `(repo_id, type, id)` index, from `id_floor(from)` to
1086/// `id_floor(until)`; a workspace's folio events (which carry no
1087/// repository) are the same range under a null repository, narrowed to
1088/// the workspace. That is indexed range scans per repository and type, and
1089/// it suits a workspace of tens of repositories. When a workspace has
1090/// hundreds, the next step is a daily rollup per workspace that this
1091/// method reads instead of the log; it is not built yet.
1092#[derive(Debug, Default, Serialize, serde::Deserialize)]
1093pub struct ActivityDigestArgs {
1094 /// The repositories to count, by id. The caller has checked the
1095 /// viewer may read each. At most `MAX_DIGEST_REPOS` are read.
1096 #[serde(default)]
1097 pub repo_ids: Vec<String>,
1098 /// The workspace's slug, for its `folio.*` events; none leaves
1099 /// artifacts out.
1100 #[serde(default)]
1101 pub workspace: Option<String>,
1102 /// The start of the span, inclusive. RFC 3339.
1103 pub from: String,
1104 /// The end of the span, exclusive. RFC 3339.
1105 pub until: String,
1106}
1107
1108/// The most repositories one digest reads.
1109pub const MAX_DIGEST_REPOS: usize = 50;
1110/// The most events one digest reads of repositories, and of folios; past
1111/// either the digest says it is not complete.
1112pub const MAX_DIGEST_EVENTS: u32 = 5000;
1113/// The most releases, packages and edited folios a digest lists.
1114pub const MAX_DIGEST_LISTED: usize = 50;
1115
1116/// What `activity_digest` returns.
1117#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1118pub struct ActivityDigest {
1119 pub from: String,
1120 pub until: String,
1121 /// One per repository that had any event counted; none for a quiet one.
1122 pub repos: Vec<RepoDigest>,
1123 /// The workspace's artifacts, when a workspace was named.
1124 pub folios: Option<FolioDigest>,
1125 /// False when more events fell in the span than were read, or more
1126 /// repositories were named than are read, so counts are low.
1127 pub complete: bool,
1128}
1129
1130/// How many times one actor did something. The actor is a member key:
1131/// `user:<id>` for a person (or g1t, `user:usr_g1t_agent`), `agent:<id>`
1132/// for one of the workspace's agents acting as itself, or empty when the
1133/// event named nobody (a push copied in by a mirror).
1134#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1135pub struct ActorCount {
1136 pub actor: String,
1137 pub count: u32,
1138}
1139
1140/// Pushes by one actor, with the commits they brought.
1141#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1142pub struct PushesBy {
1143 pub actor: String,
1144 pub count: u32,
1145 pub commits: u32,
1146}
1147
1148/// Pushes to a repository's branches (`git.push` events for `refs/heads/`;
1149/// tags are not pushes here). A push whose commits were not counted
1150/// counts as one commit.
1151#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1152pub struct PushDigest {
1153 pub count: u32,
1154 pub commits: u32,
1155 /// The branches pushed to, by name, sorted.
1156 pub branches: Vec<String>,
1157 /// Most pushes first.
1158 pub by: Vec<PushesBy>,
1159 /// Of those, the pushes straight to the default branch: what landed
1160 /// without a pull request.
1161 pub default_branch: DefaultBranchPushes,
1162}
1163
1164#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1165pub struct DefaultBranchPushes {
1166 pub count: u32,
1167 pub commits: u32,
1168 pub by: Vec<PushesBy>,
1169 /// When the newest of them was. RFC 3339.
1170 pub last_at: Option<String>,
1171}
1172
1173/// Pull requests opened, merged and closed without merging, each by who did it.
1174#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1175pub struct PullDigest {
1176 pub opened: Vec<ActorCount>,
1177 pub merged: Vec<ActorCount>,
1178 pub closed: Vec<ActorCount>,
1179}
1180
1181/// Issues opened and closed, each by who did it.
1182#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1183pub struct IssueDigest {
1184 pub opened: Vec<ActorCount>,
1185 pub closed: Vec<ActorCount>,
1186}
1187
1188/// Builds that finished, by who set them off, and how many of the
1189/// successes were production.
1190#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1191pub struct DeploymentDigest {
1192 pub succeeded: Vec<ActorCount>,
1193 pub failed: Vec<ActorCount>,
1194 pub production: u32,
1195}
1196
1197/// A release published in the span.
1198#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1199pub struct ReleaseDigest {
1200 pub tag: String,
1201 pub name: Option<String>,
1202 pub actor: String,
1203 /// RFC 3339.
1204 pub at: String,
1205}
1206
1207/// A package version published in the span.
1208#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1209pub struct PackageDigest {
1210 pub ecosystem: String,
1211 pub name: String,
1212 pub version: String,
1213 pub actor: String,
1214 /// RFC 3339.
1215 pub at: String,
1216}
1217
1218/// One repository's span.
1219#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1220pub struct RepoDigest {
1221 pub repo_id: String,
1222 pub pushes: PushDigest,
1223 pub pulls: PullDigest,
1224 pub issues: IssueDigest,
1225 /// Reviews: `review.completed` (g1t's), and a comment with a verdict (a person's).
1226 pub reviews: Vec<ActorCount>,
1227 /// Comments without a verdict; one by an agent as itself is the agent's.
1228 pub comments: Vec<ActorCount>,
1229 pub deployments: DeploymentDigest,
1230 /// Newest first, at most `MAX_DIGEST_LISTED`.
1231 pub releases: Vec<ReleaseDigest>,
1232 /// Newest first, at most `MAX_DIGEST_LISTED`.
1233 pub packages: Vec<PackageDigest>,
1234}
1235
1236/// One folio (an artifact: a doc, slides, a design, a dashboard) whose
1237/// content changed, with everyone whose changes are in it, as member
1238/// keys. Never its title.
1239#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1240pub struct FolioEdited {
1241 pub folio_id: String,
1242 pub kind: String,
1243 pub authors: Vec<String>,
1244}
1245
1246/// A workspace's artifacts over the span: `folio.created` by who made
1247/// them, and the folios whose content changed (`folio.updated`).
1248#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1249pub struct FolioDigest {
1250 pub created: Vec<ActorCount>,
1251 /// Each folio once, at most `MAX_DIGEST_LISTED`.
1252 pub edited: Vec<FolioEdited>,
1253 /// How many folios changed, counting those past the cap.
1254 pub edited_count: u32,
1255}
1256
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01257#[cfg(test)]
1258mod tests {
1259 use super::*;
1260
1261 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1262 fn comment_edits_and_deletes_say_what_it_was() {
1263 let edited = CommentEdited {
1264 comment_id: "cmt_1".into(),
1265 repo_id: "rep_1".into(),
1266 number: 7,
1267 pull_id: Some("pr_1".into()),
1268 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
1269 };
1270 assert_eq!(
1271 serde_json::to_value(edited).unwrap(),
1272 serde_json::json!({
1273 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
1274 "changes": { "body": { "from": "Before" } },
1275 })
1276 );
1277 let deleted = CommentDeleted {
1278 comment_id: "cmt_1".into(),
1279 repo_id: "rep_1".into(),
1280 number: 7,
1281 pull_id: None,
1282 comment: DeletedComment {
1283 id: "cmt_1".into(),
1284 body: "Gone".into(),
1285 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1286 created_at: "2026-10-08T00:00:00Z".into(),
1287 path: None,
1288 line: None,
1289 },
1290 };
1291 assert_eq!(
1292 serde_json::to_value(deleted).unwrap(),
1293 serde_json::json!({
1294 "commentId": "cmt_1", "repoId": "rep_1", "number": 7,
1295 "comment": {
1296 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1297 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1298 },
1299 })
1300 );
1301 }
1302
1303 #[test]
1304 fn a_release_change_is_githubs_activity_types() {
1305 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1306 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1307 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1308 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1309 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1310 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1311 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1312 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1313 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1314 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1315 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1316 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1317 }
1318 assert_eq!(release_kind("archived"), None);
1319 }
1320
1321 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1322 fn what_a_job_token_did_is_marked_and_carried_on() {
1323 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1324 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1325 assert_eq!(caused_by_job(&plain), None);
1326 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1327 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1328 ..Default::default()
1329 }));
1330 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1331 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1332 assert_eq!(by_job["number"], 4);
1333 // A push by the job's token, and the pull request it moves.
1334 let push = GitPush {
1335 repo_id: "rep_1".into(),
1336 git_ref: "refs/heads/fix".into(),
1337 before: Some("a".into()),
1338 after: "b".into(),
1339 default_branch: false,
1340 unscanned: false,
1341 caused_by_job: job_run_of(&actor).map(str::to_owned),
Merge branch 'mirroring' into artifacts-mode1342 mirrored: false,
1343 mirror: None,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1344 commits: Some(3),
Merge branch 'worktree-agent-a3abfcce648e87dca'1345 };
1346 let push = serde_json::to_value(push).unwrap();
1347 assert_eq!(caused_by_job(&push), Some("run_9"));
1348 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1349 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1350 }
1351
1352 #[test]
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01353 fn a_push_says_it_was_unscanned_only_when_it_was() {
1354 let push = |unscanned| GitPush {
1355 repo_id: "rep_1".into(),
1356 git_ref: "refs/heads/import".into(),
1357 before: None,
1358 after: "abc".into(),
1359 default_branch: false,
1360 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1361 caused_by_job: None,
Merge branch 'mirroring' into artifacts-mode1362 mirrored: false,
1363 mirror: None,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1364 commits: None,
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01365 };
1366 let quiet = serde_json::to_value(push(false)).unwrap();
1367 assert!(quiet.get("unscanned").is_none());
1368 let flagged = serde_json::to_value(push(true)).unwrap();
1369 assert_eq!(flagged["unscanned"], true);
1370 assert_eq!(flagged["ref"], "refs/heads/import");
1371 }
1372
1373 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1374 WorkspaceRenamed {
1375 workspace_id: "wsp_1".into(),
1376 from: from.into(),
1377 to: to.into(),
1378 }
1379 }
1380
1381 #[test]
1382 fn stale_slugs_leave_out_the_current_one() {
1383 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1384 // Delivered after a second rename, b → c: both move to c.
1385 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1386 // Renamed back: a → b → a.
1387 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1388 }
1389
1390 fn transferred(from: &str, to: &str) -> RepoTransferred {
1391 RepoTransferred {
1392 repo_id: "rep_1".into(),
1393 name: "rocket".into(),
1394 from: from.into(),
1395 to: to.into(),
1396 }
1397 }
1398
1399 #[test]
1400 fn stale_paths_leave_out_the_current_one() {
1401 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1402 // Delivered after a second transfer, b → c: both move to c.
1403 assert_eq!(
1404 transferred("a", "b").stale_paths("c/rocket"),
1405 vec!["a/rocket", "b/rocket"]
1406 );
1407 // Transferred back: a → b → a.
1408 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1409 }
1410
1411 #[test]
1412 fn a_transfer_reads_as_published() {
1413 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1414 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1415 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1416 }
1417
1418 #[test]
1419 fn a_rename_names_both_paths_in_its_workspace() {
1420 let renamed = RepoRenamed {
1421 repo_id: "rep_1".into(),
1422 namespace: "acme".into(),
1423 from: "old".into(),
1424 to: "new".into(),
1425 };
1426 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1427 // Delivered after a transfer: both names in acme move.
1428 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1429 }
1430
1431 #[test]
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.1432 fn a_member_joining_says_by_which_door() {
1433 let joined = MemberJoined {
1434 workspace_id: "wsp_1".into(),
1435 workspace: "acme".into(),
1436 user_id: "usr_ada".into(),
1437 username: "ada".into(),
1438 role: crate::Role::Member,
1439 how: JoinedHow::Invitation,
1440 };
1441 let data = serde_json::to_value(&joined).unwrap();
1442 assert_eq!(
1443 data,
1444 serde_json::json!({
1445 "workspaceId": "wsp_1",
1446 "workspace": "acme",
1447 "userId": "usr_ada",
1448 "username": "ada",
1449 "role": "member",
1450 "how": "invitation",
1451 })
1452 );
1453 let back: MemberJoined = serde_json::from_value(data).unwrap();
1454 assert_eq!(back, joined);
1455 assert_eq!(serde_json::to_value(JoinedHow::Created).unwrap(), "created");
1456 assert_eq!(serde_json::to_value(JoinedHow::Joined).unwrap(), "joined");
1457 }
1458
1459 #[test]
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01460 fn reads_the_published_payload() {
1461 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1462 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1463 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1464 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1465
1466 #[test]
1467 fn a_stale_page_reads_as_published() {
1468 let data = serde_json::json!({
1469 "workspace": "acme", "workspaceId": "wsp_1", "pageId": "pag_1", "spaceId": "spc_1",
1470 "title": "Exports", "path": "/acme/-/docs/general/exports-pag_1",
1471 "repoId": "rep_1", "repo": "acme/web", "commit": "abc", "pull": 431,
1472 "paths": ["src/export.ts"], "owners": ["user:usr_1"]
1473 });
1474 let event: DocPageStale = serde_json::from_value(data).unwrap();
1475 assert_eq!((event.page.page_id.as_str(), event.pull), ("pag_1", Some(431)));
1476 assert!(DOC_PAGE_EVENTS.contains(&"doc.page.stale"));
1477 }
Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.01478}

This file's history is long; its oldest lines are credited to the oldest commit read.