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,441 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.

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

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