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

Fixes from the page sweep1//! 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
Fixes from the page sweep43/// 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>,
Fixes from the page sweep121}
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
Fixes from the page sweep138/// 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.
Fixes from the page sweep142#[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>,
Fixes from the page sweep176}
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
Fixes from the page sweep186/// 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.
Fixes from the page sweep195#[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.
Fixes from the page sweep214 #[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,
Fixes from the page sweep332}
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)]
Fixes from the page sweep373#[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,
Fixes from the page sweep394}
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.
Fixes from the page sweep437#[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")]
Fixes from the page sweep514pub 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
Merge branch 'worktree-agent-ad531323a21f4c975'567/// `list`: events from the log, newest first, or with `after` oldest first.
568/// Returns `Vec<Event>`.
Fixes from the page sweep569#[derive(Debug, Default, Serialize, serde::Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct ListArgs {
572 #[serde(default)]
573 pub repo_id: Option<String>,
574 /// Only these types; all types when empty.
575 #[serde(default)]
576 pub types: Vec<String>,
577 /// Only events older than this event id.
578 #[serde(default)]
579 pub before: Option<String>,
Merge branch 'worktree-agent-ad531323a21f4c975'580 /// Only events the log recorded after this event id, and then oldest
581 /// first: a cursor to follow the log with. Events are ordered by when
582 /// the log wrote them, not by their ids, so one published a moment
583 /// before another but written after it is not skipped.
584 #[serde(default)]
585 pub after: Option<String>,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)586 /// Only events by this account id.
587 #[serde(default)]
588 pub actor: Option<String>,
589 /// Only events about these issues or pull requests: their `number`, or
590 /// the `issue` a comment, review or link is on. All when empty.
591 #[serde(default)]
592 pub numbers: Vec<u32>,
593 /// Only events at or after this RFC 3339 time.
594 #[serde(default)]
595 pub since: Option<String>,
Fixes from the page sweep596 #[serde(default)]
597 pub limit: Option<u32>,
598}
599
600/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
601/// Every service that stores a slug moves its rows to the workspace's
602/// *current* slug (ask identity by `workspace_id`), so that a repeated or
603/// late delivery after a second rename still lands in the right place.
604#[derive(Clone, Debug, Serialize, serde::Deserialize)]
605#[serde(rename_all = "camelCase")]
606pub struct WorkspaceRenamed {
607 pub workspace_id: String,
608 pub from: String,
609 pub to: String,
610}
611
612impl WorkspaceRenamed {
613 /// The slugs whose rows move to `current`: the two this rename names,
614 /// minus `current` itself. Moving rows keyed by either converges on the
615 /// current slug whatever order renames are delivered in.
616 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
617 let mut slugs: Vec<String> = Vec::new();
618 for slug in [&self.from, &self.to] {
619 if slug != current && !slugs.contains(slug) {
620 slugs.push(slug.clone());
621 }
622 }
623 slugs
624 }
625}
626
627/// `repo.updated`: a repository's description, topics or visibility
628/// changed. `visibility_changed` says whether it went public or private,
629/// which `repo.visibility_changed` also announces on its own.
630#[derive(Debug, Serialize, serde::Deserialize)]
631#[serde(rename_all = "camelCase")]
632pub struct RepoUpdated {
633 pub repo_id: String,
634 pub namespace: String,
635 pub name: String,
636 pub is_private: bool,
637 #[serde(default)]
638 pub visibility_changed: bool,
639}
640
641/// `repo.visibility_changed`: a repository went public or private.
642#[derive(Debug, Serialize, serde::Deserialize)]
643#[serde(rename_all = "camelCase")]
644pub struct RepoVisibilityChanged {
645 pub repo_id: String,
646 pub is_private: bool,
647}
648
649/// `repo.renamed`: a repository's name changed within its workspace,
650/// keeping its id and its git store key. Like `repo.transferred`, a path
651/// change: every service that keeps rows under the repository's path moves
652/// them to its *current* path (ask repos `path_by_id`), so a repeated or
653/// late delivery after a second rename or a transfer still lands in the
654/// right place. `g1t_kit::transfer::on_event` handles both.
655#[derive(Clone, Debug, Serialize, serde::Deserialize)]
656#[serde(rename_all = "camelCase")]
657pub struct RepoRenamed {
658 pub repo_id: String,
659 /// The workspace it is in.
660 pub namespace: String,
661 /// Its old name.
662 pub from: String,
663 /// Its new name.
664 pub to: String,
665}
666
667impl RepoRenamed {
668 /// The paths whose rows move to `current` (`namespace/name`): the two
669 /// this rename names, minus `current`.
670 pub fn stale_paths(&self, current: &str) -> Vec<String> {
671 let mut paths: Vec<String> = Vec::new();
672 for name in [&self.from, &self.to] {
673 let path = format!("{}/{name}", self.namespace);
674 if path != current && !paths.contains(&path) {
675 paths.push(path);
676 }
677 }
678 paths
679 }
680}
681
682/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
683/// git refuses it, but it can be restored until `purge_after`, so services
684/// stop what runs for it (agents, workflows, deployments, indexing,
685/// webhook deliveries) and hide it, and keep what they hold until
686/// `repo.purged`. `repo.restored` brings it back.
687#[derive(Clone, Debug, Serialize, serde::Deserialize)]
688#[serde(rename_all = "camelCase")]
689pub struct RepoDeleted {
690 pub repo_id: String,
691 #[serde(default)]
692 pub namespace: String,
693 #[serde(default)]
694 pub name: String,
695 #[serde(default)]
696 pub is_private: bool,
697 /// RFC 3339: when it is purged unless restored first.
698 #[serde(default)]
699 pub purge_after: String,
700 /// It went with its workspace (`workspace.deleting`). Services that
701 /// handle the workspace as a whole (deployments pauses its apps rather
702 /// than taking them down) leave this one to that.
703 #[serde(default)]
704 pub with_workspace: bool,
705}
706
707/// `repo.restored`: a deleted repository is back, at its path, as it was.
708/// Services start again what `repo.deleted` stopped: index it, deploy its
709/// production, show it.
710#[derive(Clone, Debug, Serialize, serde::Deserialize)]
711#[serde(rename_all = "camelCase")]
712pub struct RepoRestored {
713 pub repo_id: String,
714 pub namespace: String,
715 pub name: String,
716 pub is_private: bool,
717 /// It came back with its workspace (`workspace.restored`).
718 #[serde(default)]
719 pub with_workspace: bool,
720}
721
722/// `repo.purged`: a deleted repository is gone for good, its git data
723/// with it. Services drop every row they keep for it by `repo_id`, except
724/// history that belongs to its workspace: ledgers, invoices and the audit
725/// log. Its path is free for a new repository.
726#[derive(Clone, Debug, Serialize, serde::Deserialize)]
727#[serde(rename_all = "camelCase")]
728pub struct RepoPurged {
729 pub repo_id: String,
730 pub namespace: String,
731 pub name: String,
732}
733
734/// `repo.archived` and `repo.unarchived`: a repository became read-only,
735/// or writable again. While archived, pushes are refused, issues and pull
736/// requests are locked, and agents and workflows do not run for it; its
737/// deployments keep serving.
738#[derive(Clone, Debug, Serialize, serde::Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct RepoArchived {
741 pub repo_id: String,
742 pub namespace: String,
743 pub name: String,
744 pub archived: bool,
745}
746
747/// `repo.default_branch_changed`: the branch everything lands on is now
748/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
749/// requests into it now target `to`) rather than another branch chosen.
750#[derive(Clone, Debug, Serialize, serde::Deserialize)]
751#[serde(rename_all = "camelCase")]
752pub struct RepoDefaultBranchChanged {
753 pub repo_id: String,
754 pub from: String,
755 pub to: String,
756 #[serde(default)]
757 pub renamed: bool,
758}
759
760/// `branch.renamed`: a branch was renamed. Pull requests from or into
761/// `from` follow it to `to`, and web addresses that name `from` redirect.
762#[derive(Clone, Debug, Serialize, serde::Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct BranchRenamed {
765 pub repo_id: String,
766 pub from: String,
767 pub to: String,
768 /// Whether it is the default branch.
769 #[serde(default)]
770 pub default_branch: bool,
771}
772
773/// `repo.transferred`: a repository moved from one workspace to another,
774/// keeping its id and its name. Every service that keeps a repository
775/// under its path (`namespace/name`) or its workspace's slug moves those
776/// rows to the repository's *current* path (ask repos `path_by_id`), so a
777/// repeated or late delivery after a second transfer still lands in the
778/// right place. What was charged or recorded before the transfer stays
779/// with the workspace it happened in.
780#[derive(Clone, Debug, Serialize, serde::Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct RepoTransferred {
783 pub repo_id: String,
784 pub name: String,
785 /// The workspace it left.
786 pub from: String,
787 /// The workspace it went to.
788 pub to: String,
789}
790
791impl RepoTransferred {
792 /// The paths whose rows move to `current` (`namespace/name`): the two
793 /// this transfer names, minus `current`. Moving rows keyed by either
794 /// converges whatever order transfers are delivered in.
795 pub fn stale_paths(&self, current: &str) -> Vec<String> {
796 let mut paths: Vec<String> = Vec::new();
797 for namespace in [&self.from, &self.to] {
798 let path = format!("{namespace}/{}", self.name);
799 if path != current && !paths.contains(&path) {
800 paths.push(path);
801 }
802 }
803 paths
804 }
805}
806
807/// `workspace.deleted`: a workspace is gone. Services drop what they keep
808/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
809/// agent queue) and keep what is history: ledgers, invoices and the audit
810/// log stay under its slug, which is never given to another workspace.
811#[derive(Clone, Debug, Serialize, serde::Deserialize)]
812#[serde(rename_all = "camelCase")]
813pub struct WorkspaceDeleted {
814 pub workspace_id: String,
815 pub slug: String,
816}
817
818/// `workspace.deleting`: an owner deleted a workspace, and it can be
819/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
820/// meantime. Services hide what they keep for it and stop what runs for it,
821/// keeping their rows: repos deletes its repositories softly (each with a
822/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
823/// apps, search drops it from results. `workspace.restored` undoes exactly
824/// that; once `purge_after` passes, `workspace.deleted` follows and
825/// services purge as for any deleted workspace.
826#[derive(Clone, Debug, Serialize, serde::Deserialize)]
827#[serde(rename_all = "camelCase")]
828pub struct WorkspaceDeleting {
829 pub workspace_id: String,
830 pub slug: String,
831 /// The username of the owner who deleted it.
832 pub by: String,
833 /// RFC 3339: when it is purged unless restored first.
834 pub purge_after: String,
835}
836
837/// `workspace.restored`: staff brought a deleted workspace back, with its
838/// members and tokens. Services undo what they did on `workspace.deleting`,
839/// and only that: a repository deleted on its own before stays deleted.
840#[derive(Clone, Debug, Serialize, serde::Deserialize)]
841#[serde(rename_all = "camelCase")]
842pub struct WorkspaceRestored {
843 pub workspace_id: String,
844 pub slug: String,
845}
846
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)847/// `user.deleting`: an account was deleted, by the person or by g1t's
848/// staff, and can be restored by staff until `purge_after`. Its sessions,
849/// tokens and keys have ended and it has left every workspace; services
850/// stop what they do for it (search drops its profile, nothing more is
851/// sent to it) and keep their rows. `user.restored` undoes that; once
852/// `purge_after` passes, `user.deleted` follows.
853#[derive(Clone, Debug, Serialize, serde::Deserialize)]
854#[serde(rename_all = "camelCase")]
855pub struct UserDeleting {
856 pub user_id: String,
857 pub username: String,
858 /// Whether g1t's staff deleted it rather than the person.
859 #[serde(default)]
860 pub by_staff: bool,
861 /// RFC 3339: when it is purged unless restored first.
862 pub purge_after: String,
863}
864
865/// `user.restored`: staff brought a deleted account back. It signs in
866/// again with its password; its old sessions and tokens stay ended.
867/// Services undo what they did on `user.deleting`.
868#[derive(Clone, Debug, Serialize, serde::Deserialize)]
869#[serde(rename_all = "camelCase")]
870pub struct UserRestored {
871 pub user_id: String,
872 pub username: String,
873}
874
875/// `user.deleted`: an account is gone for good. Services drop what they
876/// keep for it alone (its inbox, subscriptions, settings) and show what it
877/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
878/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
879/// its username. The username is never given to anyone again.
880#[derive(Clone, Debug, Serialize, serde::Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct UserDeleted {
883 pub user_id: String,
884 pub username: String,
885}
886
Fixes from the page sweep887/// `user.updated`: an account was made, or changed what its profile shows
888/// (name, bio, avatar). Nothing private: ask identity for the profile.
889#[derive(Debug, Serialize, serde::Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub struct UserUpdated {
892 pub username: String,
893}
894
895/// `user.email_added`, `user.email_verified`, `user.email_removed` and
896/// `user.primary_email_changed`: an account's addresses changed. Never the
897/// address itself; ask identity, as the person, for that.
898#[derive(Debug, Serialize, serde::Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct UserEmailChanged {
901 pub user_id: String,
902 /// Whether g1t staff made the change.
903 #[serde(default)]
904 pub by_staff: bool,
905}
906
907/// `workspace.updated`: a workspace was made, or its name, description or
908/// icon changed. Ask identity for it by slug.
909#[derive(Debug, Serialize, serde::Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct WorkspaceUpdated {
912 pub workspace_id: String,
913 pub slug: String,
914}
915
@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.916/// How someone came to be a member of a workspace (`workspace.member_joined`).
917#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
918#[serde(rename_all = "snake_case")]
919pub enum JoinedHow {
920 /// They accepted a workspace invitation sent to them by name.
921 Invitation,
922 /// They made the workspace, and are its first owner.
923 Created,
924 /// They used an invite code or link that named the workspace.
925 Joined,
926}
927
928/// `workspace.member_joined`: a person became a member of a workspace,
929/// published by identity as the membership is written. `how` says by
930/// which door; the workspace's creator is `created`, so a listener that
931/// greets new members can leave them out (the agents service's @g1t
932/// welcomes everyone else, once). Never published for an agent or a
933/// token: only a person joins.
934#[derive(Clone, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
935#[serde(rename_all = "camelCase")]
936pub struct MemberJoined {
937 pub workspace_id: String,
938 /// The workspace's slug.
939 pub workspace: String,
940 pub user_id: String,
941 pub username: String,
942 pub role: crate::Role,
943 pub how: JoinedHow,
944}
945
Fixes from the page sweep946/// `invite.created`: someone (or staff) made an invite. Never the code or
947/// the address it is for.
948#[derive(Debug, Serialize, serde::Deserialize)]
949#[serde(rename_all = "camelCase")]
950pub struct InviteCreated {
951 pub invite_id: String,
952 /// The account that made it; null when staff did.
953 pub inviter_id: Option<String>,
954 /// The workspace it joins.
955 pub workspace_id: Option<String>,
956 /// Whether it is bound to one email address.
957 pub bound: bool,
958}
959
960/// `invite.redeemed`: an invite was used, by a new account or by an
961/// existing one joining a workspace.
962#[derive(Debug, Serialize, serde::Deserialize)]
963#[serde(rename_all = "camelCase")]
964pub struct InviteRedeemed {
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)965 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
966 /// the account.
Fixes from the page sweep967 pub invite_id: String,
968 pub user_id: String,
969 pub inviter_id: Option<String>,
970 pub workspace_id: Option<String>,
971 /// Whether it made the account.
972 pub created_account: bool,
973}
974
975/// The payload of `package.published`, `package.version_deleted`,
976/// `package.deleted` and `package.visibility_changed`; each uses the fields
977/// that apply to it. `repo_id` is the repository the package is linked to.
978#[derive(Clone, Debug, Default, Serialize)]
979#[serde(rename_all = "camelCase")]
980pub struct PackageEvent {
981 pub package_id: String,
982 pub workspace: String,
983 pub ecosystem: String,
984 pub name: String,
985 pub repo_id: Option<String>,
986 /// The version published or deleted: for a container image, its
987 /// manifest's digest.
988 #[serde(skip_serializing_if = "Option::is_none")]
989 pub version: Option<String>,
990 #[serde(skip_serializing_if = "Option::is_none")]
991 pub digest: Option<String>,
992 #[serde(skip_serializing_if = "Option::is_none")]
993 pub size: Option<u64>,
994 /// On publish: the tags that now point to the version.
995 #[serde(skip_serializing_if = "Option::is_none")]
996 pub tags: Option<Vec<String>>,
997 /// On `package.visibility_changed`: `public` or `private`.
998 #[serde(skip_serializing_if = "Option::is_none")]
999 pub visibility: Option<String>,
1000}
1001
1002/// `queue.changed`: a repository's merge queue gained, lost or settled an
1003/// entry, so the next batch may be ready to test.
1004#[derive(Debug, Serialize)]
1005#[serde(rename_all = "camelCase")]
1006pub struct QueueChanged {
1007 pub repo_id: String,
1008}
1009
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.1010/// 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 store1011/// publishes, with no `repoId` on the event: a page may be in a private
1012/// space, so it never reaches a repository's timeline or webhooks.
1013pub const DOC_PAGE_EVENTS: [&str; 4] = ["doc.page.created", "doc.page.updated", "doc.page.archived", "doc.page.stale"];
1014
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 yet1015/// The `folio.*` types (Artifacts mode), with their payloads, live in
1016/// [`crate::folios`]; nothing publishes or subscribes to them yet.
1017pub use crate::folios::FOLIO_EVENTS;
1018
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1019/// What every `doc.page.*` event carries (`DocPageEventData` in
1020/// events.ts). `doc.page.updated` adds `versionId`, `kind` and `authors`;
1021/// `doc.page.stale` adds `repoId`, `repo`, `commit`, `pull`, `paths` and
1022/// `owners` ([`DocPageStale`]).
1023#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct DocPageEvent {
1026 pub workspace: String,
1027 pub workspace_id: String,
1028 pub page_id: String,
1029 pub space_id: String,
1030 pub title: String,
1031 /// The page's address on the site.
1032 pub path: String,
1033}
1034
1035/// `doc.page.stale`: code a page cites changed.
1036#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1037#[serde(rename_all = "camelCase")]
1038pub struct DocPageStale {
1039 #[serde(flatten)]
1040 pub page: DocPageEvent,
1041 pub repo_id: String,
1042 /// `owner/name`.
1043 pub repo: String,
1044 pub commit: String,
1045 /// The merged pull request's number, when a pull request made the change.
1046 pub pull: Option<u32>,
1047 pub paths: Vec<String>,
1048 /// Member keys: `user:<id>`, `agent:<id>`.
1049 pub owners: Vec<String>,
1050}
1051
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.1052/// `activity_digest`: what happened in some repositories, and to a
1053/// workspace's artifacts, over a span, counted. Returns [`ActivityDigest`].
1054/// Home reads it for what people and agents did since you were last there.
1055/// Fields are snake_case, as every service's arguments are.
1056///
1057/// The span is `[from, until)` in RFC 3339. Event ids sort by time, so the
1058/// service reads each repository's events of each type it counts as one
1059/// range of the `(repo_id, type, id)` index, from `id_floor(from)` to
1060/// `id_floor(until)`; a workspace's folio events (which carry no
1061/// repository) are the same range under a null repository, narrowed to
1062/// the workspace. That is indexed range scans per repository and type, and
1063/// it suits a workspace of tens of repositories. When a workspace has
1064/// hundreds, the next step is a daily rollup per workspace that this
1065/// method reads instead of the log; it is not built yet.
1066#[derive(Debug, Default, Serialize, serde::Deserialize)]
1067pub struct ActivityDigestArgs {
1068 /// The repositories to count, by id. The caller has checked the
1069 /// viewer may read each. At most `MAX_DIGEST_REPOS` are read.
1070 #[serde(default)]
1071 pub repo_ids: Vec<String>,
1072 /// The workspace's slug, for its `folio.*` events; none leaves
1073 /// artifacts out.
1074 #[serde(default)]
1075 pub workspace: Option<String>,
1076 /// The start of the span, inclusive. RFC 3339.
1077 pub from: String,
1078 /// The end of the span, exclusive. RFC 3339.
1079 pub until: String,
1080}
1081
1082/// The most repositories one digest reads.
1083pub const MAX_DIGEST_REPOS: usize = 50;
1084/// The most events one digest reads of repositories, and of folios; past
1085/// either the digest says it is not complete.
1086pub const MAX_DIGEST_EVENTS: u32 = 5000;
1087/// The most releases, packages and edited folios a digest lists.
1088pub const MAX_DIGEST_LISTED: usize = 50;
1089
1090/// What `activity_digest` returns.
1091#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1092pub struct ActivityDigest {
1093 pub from: String,
1094 pub until: String,
1095 /// One per repository that had any event counted; none for a quiet one.
1096 pub repos: Vec<RepoDigest>,
1097 /// The workspace's artifacts, when a workspace was named.
1098 pub folios: Option<FolioDigest>,
1099 /// False when more events fell in the span than were read, or more
1100 /// repositories were named than are read, so counts are low.
1101 pub complete: bool,
1102}
1103
1104/// How many times one actor did something. The actor is a member key:
1105/// `user:<id>` for a person (or g1t, `user:usr_g1t_agent`), `agent:<id>`
1106/// 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.1107/// 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.1108#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1109pub struct ActorCount {
1110 pub actor: String,
1111 pub count: u32,
1112}
1113
1114/// Pushes by one actor, with the commits they brought.
1115#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1116pub struct PushesBy {
1117 pub actor: String,
1118 pub count: u32,
1119 pub commits: u32,
1120}
1121
1122/// Pushes to a repository's branches (`git.push` events for `refs/heads/`;
1123/// tags are not pushes here). A push whose commits were not counted
1124/// counts as one commit.
1125#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1126pub struct PushDigest {
1127 pub count: u32,
1128 pub commits: u32,
1129 /// The branches pushed to, by name, sorted.
1130 pub branches: Vec<String>,
1131 /// Most pushes first.
1132 pub by: Vec<PushesBy>,
1133 /// Of those, the pushes straight to the default branch: what landed
1134 /// without a pull request.
1135 pub default_branch: DefaultBranchPushes,
1136}
1137
1138#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1139pub struct DefaultBranchPushes {
1140 pub count: u32,
1141 pub commits: u32,
1142 pub by: Vec<PushesBy>,
1143 /// When the newest of them was. RFC 3339.
1144 pub last_at: Option<String>,
1145}
1146
1147/// Pull requests opened, merged and closed without merging, each by who did it.
1148#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1149pub struct PullDigest {
1150 pub opened: Vec<ActorCount>,
1151 pub merged: Vec<ActorCount>,
1152 pub closed: Vec<ActorCount>,
1153}
1154
1155/// Issues opened and closed, each by who did it.
1156#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1157pub struct IssueDigest {
1158 pub opened: Vec<ActorCount>,
1159 pub closed: Vec<ActorCount>,
1160}
1161
1162/// Builds that finished, by who set them off, and how many of the
1163/// successes were production.
1164#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1165pub struct DeploymentDigest {
1166 pub succeeded: Vec<ActorCount>,
1167 pub failed: Vec<ActorCount>,
1168 pub production: u32,
1169}
1170
1171/// A release published in the span.
1172#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1173pub struct ReleaseDigest {
1174 pub tag: String,
1175 pub name: Option<String>,
1176 pub actor: String,
1177 /// RFC 3339.
1178 pub at: String,
1179}
1180
1181/// A package version published in the span.
1182#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1183pub struct PackageDigest {
1184 pub ecosystem: String,
1185 pub name: String,
1186 pub version: String,
1187 pub actor: String,
1188 /// RFC 3339.
1189 pub at: String,
1190}
1191
1192/// One repository's span.
1193#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1194pub struct RepoDigest {
1195 pub repo_id: String,
1196 pub pushes: PushDigest,
1197 pub pulls: PullDigest,
1198 pub issues: IssueDigest,
1199 /// Reviews: `review.completed` (g1t's), and a comment with a verdict (a person's).
1200 pub reviews: Vec<ActorCount>,
1201 /// Comments without a verdict; one by an agent as itself is the agent's.
1202 pub comments: Vec<ActorCount>,
1203 pub deployments: DeploymentDigest,
1204 /// Newest first, at most `MAX_DIGEST_LISTED`.
1205 pub releases: Vec<ReleaseDigest>,
1206 /// Newest first, at most `MAX_DIGEST_LISTED`.
1207 pub packages: Vec<PackageDigest>,
1208}
1209
1210/// One folio (an artifact: a doc, slides, a design, a dashboard) whose
1211/// content changed, with everyone whose changes are in it, as member
1212/// keys. Never its title.
1213#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1214pub struct FolioEdited {
1215 pub folio_id: String,
1216 pub kind: String,
1217 pub authors: Vec<String>,
1218}
1219
1220/// A workspace's artifacts over the span: `folio.created` by who made
1221/// them, and the folios whose content changed (`folio.updated`).
1222#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1223pub struct FolioDigest {
1224 pub created: Vec<ActorCount>,
1225 /// Each folio once, at most `MAX_DIGEST_LISTED`.
1226 pub edited: Vec<FolioEdited>,
1227 /// How many folios changed, counting those past the cap.
1228 pub edited_count: u32,
1229}
1230
Fixes from the page sweep1231#[cfg(test)]
1232mod tests {
1233 use super::*;
1234
1235 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1236 fn comment_edits_and_deletes_say_what_it_was() {
1237 let edited = CommentEdited {
1238 comment_id: "cmt_1".into(),
1239 repo_id: "rep_1".into(),
1240 number: 7,
1241 pull_id: Some("pr_1".into()),
1242 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
1243 };
1244 assert_eq!(
1245 serde_json::to_value(edited).unwrap(),
1246 serde_json::json!({
1247 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
1248 "changes": { "body": { "from": "Before" } },
1249 })
1250 );
1251 let deleted = CommentDeleted {
1252 comment_id: "cmt_1".into(),
1253 repo_id: "rep_1".into(),
1254 number: 7,
1255 pull_id: None,
1256 comment: DeletedComment {
1257 id: "cmt_1".into(),
1258 body: "Gone".into(),
1259 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1260 created_at: "2026-10-08T00:00:00Z".into(),
1261 path: None,
1262 line: None,
1263 },
1264 };
1265 assert_eq!(
1266 serde_json::to_value(deleted).unwrap(),
1267 serde_json::json!({
1268 "commentId": "cmt_1", "repoId": "rep_1", "number": 7,
1269 "comment": {
1270 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1271 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1272 },
1273 })
1274 );
1275 }
1276
1277 #[test]
1278 fn a_release_change_is_githubs_activity_types() {
1279 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1280 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1281 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1282 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1283 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1284 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1285 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1286 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1287 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1288 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1289 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1290 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1291 }
1292 assert_eq!(release_kind("archived"), None);
1293 }
1294
1295 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1296 fn what_a_job_token_did_is_marked_and_carried_on() {
1297 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1298 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1299 assert_eq!(caused_by_job(&plain), None);
1300 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1301 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1302 ..Default::default()
1303 }));
1304 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1305 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1306 assert_eq!(by_job["number"], 4);
1307 // A push by the job's token, and the pull request it moves.
1308 let push = GitPush {
1309 repo_id: "rep_1".into(),
1310 git_ref: "refs/heads/fix".into(),
1311 before: Some("a".into()),
1312 after: "b".into(),
1313 default_branch: false,
1314 unscanned: false,
1315 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.1316 commits: Some(3),
Merge branch 'worktree-agent-a3abfcce648e87dca'1317 };
1318 let push = serde_json::to_value(push).unwrap();
1319 assert_eq!(caused_by_job(&push), Some("run_9"));
1320 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1321 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1322 }
1323
1324 #[test]
Fixes from the page sweep1325 fn a_push_says_it_was_unscanned_only_when_it_was() {
1326 let push = |unscanned| GitPush {
1327 repo_id: "rep_1".into(),
1328 git_ref: "refs/heads/import".into(),
1329 before: None,
1330 after: "abc".into(),
1331 default_branch: false,
1332 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1333 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.1334 commits: None,
Fixes from the page sweep1335 };
1336 let quiet = serde_json::to_value(push(false)).unwrap();
1337 assert!(quiet.get("unscanned").is_none());
1338 let flagged = serde_json::to_value(push(true)).unwrap();
1339 assert_eq!(flagged["unscanned"], true);
1340 assert_eq!(flagged["ref"], "refs/heads/import");
1341 }
1342
1343 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1344 WorkspaceRenamed {
1345 workspace_id: "wsp_1".into(),
1346 from: from.into(),
1347 to: to.into(),
1348 }
1349 }
1350
1351 #[test]
1352 fn stale_slugs_leave_out_the_current_one() {
1353 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1354 // Delivered after a second rename, b → c: both move to c.
1355 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1356 // Renamed back: a → b → a.
1357 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1358 }
1359
1360 fn transferred(from: &str, to: &str) -> RepoTransferred {
1361 RepoTransferred {
1362 repo_id: "rep_1".into(),
1363 name: "rocket".into(),
1364 from: from.into(),
1365 to: to.into(),
1366 }
1367 }
1368
1369 #[test]
1370 fn stale_paths_leave_out_the_current_one() {
1371 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1372 // Delivered after a second transfer, b → c: both move to c.
1373 assert_eq!(
1374 transferred("a", "b").stale_paths("c/rocket"),
1375 vec!["a/rocket", "b/rocket"]
1376 );
1377 // Transferred back: a → b → a.
1378 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1379 }
1380
1381 #[test]
1382 fn a_transfer_reads_as_published() {
1383 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1384 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1385 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1386 }
1387
1388 #[test]
1389 fn a_rename_names_both_paths_in_its_workspace() {
1390 let renamed = RepoRenamed {
1391 repo_id: "rep_1".into(),
1392 namespace: "acme".into(),
1393 from: "old".into(),
1394 to: "new".into(),
1395 };
1396 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1397 // Delivered after a transfer: both names in acme move.
1398 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1399 }
1400
1401 #[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.1402 fn a_member_joining_says_by_which_door() {
1403 let joined = MemberJoined {
1404 workspace_id: "wsp_1".into(),
1405 workspace: "acme".into(),
1406 user_id: "usr_ada".into(),
1407 username: "ada".into(),
1408 role: crate::Role::Member,
1409 how: JoinedHow::Invitation,
1410 };
1411 let data = serde_json::to_value(&joined).unwrap();
1412 assert_eq!(
1413 data,
1414 serde_json::json!({
1415 "workspaceId": "wsp_1",
1416 "workspace": "acme",
1417 "userId": "usr_ada",
1418 "username": "ada",
1419 "role": "member",
1420 "how": "invitation",
1421 })
1422 );
1423 let back: MemberJoined = serde_json::from_value(data).unwrap();
1424 assert_eq!(back, joined);
1425 assert_eq!(serde_json::to_value(JoinedHow::Created).unwrap(), "created");
1426 assert_eq!(serde_json::to_value(JoinedHow::Joined).unwrap(), "joined");
1427 }
1428
1429 #[test]
Fixes from the page sweep1430 fn reads_the_published_payload() {
1431 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1432 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1433 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1434 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1435
1436 #[test]
1437 fn a_stale_page_reads_as_published() {
1438 let data = serde_json::json!({
1439 "workspace": "acme", "workspaceId": "wsp_1", "pageId": "pag_1", "spaceId": "spc_1",
1440 "title": "Exports", "path": "/acme/-/docs/general/exports-pag_1",
1441 "repoId": "rep_1", "repo": "acme/web", "commit": "abc", "pull": 431,
1442 "paths": ["src/export.ts"], "owners": ["user:usr_1"]
1443 });
1444 let event: DocPageStale = serde_json::from_value(data).unwrap();
1445 assert_eq!((event.page.page_id.as_str(), event.pull), ("pag_1", Some(431)));
1446 assert!(DOC_PAGE_EVENTS.contains(&"doc.page.stale"));
1447 }
Fixes from the page sweep1448}

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