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