Skip to content
1,216 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.

Get started on mission control1//! 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
Get started on mission control43/// 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
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67/// 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).
Get started on mission control71#[derive(Debug, Serialize)]
72#[serde(rename_all = "camelCase")]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73pub 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")]
Get started on mission control84pub struct RepoForked {
85 pub repo_id: String,
86 pub source_repo_id: String,
87 pub pull_id: String,
88}
89
GitHub Actions on g1t, part one: reading workflows90/// One branch or tag moved by a push. `after` is the commit it points to now.
Get started on mission control91#[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,
GitHub Actions on g1t, part one: reading workflows98 /// Where it pointed before; absent for a new branch or tag.
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub before: Option<String>,
Get started on mission control101 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 daily104 /// Set when the push was too large to scan for secrets before it was
105 /// stored, and was let through: the security service scans
106 /// `before..after` after it landed. Absent otherwise.
107 #[serde(skip_serializing_if = "std::ops::Not::not")]
108 pub unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'109 /// Set when a workflow job's token pushed: the run's id (see
110 /// [`CAUSED_BY_JOB`]). Absent otherwise.
111 #[serde(rename = "causedByJob", skip_serializing_if = "Option::is_none")]
112 pub caused_by_job: Option<String>,
Merge branch 'mirroring' into artifacts-mode113 /// Set when the push was copied in from the remote a mirror follows,
114 /// not made on g1t. Absent otherwise.
115 #[serde(skip_serializing_if = "std::ops::Not::not")]
116 pub mirrored: bool,
117 /// The repository's mirror state when the push landed (see
118 /// [`crate::mirrors`]): workflows and deployments follow it. Absent for
119 /// a repository that leads.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub mirror: Option<crate::mirrors::RepoMirror>,
Get started on mission control122}
123
124/// The payload of `issue.opened`, `issue.updated`, `issue.assigned`,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar125/// `issue.labeled`, `issue.unlabeled`, `issue.milestoned`,
126/// `issue.demilestoned`, `issue.closed` and `issue.reopened`; each uses
127/// the fields that apply to it.
Get started on mission control128#[derive(Debug, Default, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct IssueEvent {
131 pub issue_id: String,
132 pub repo_id: String,
133 pub number: u32,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights134 /// Who opened it: g1t, for one its agent filed while at work.
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub author: Option<crate::credentials::Principal>,
137 /// For an issue g1t's agent filed: the person it was working for.
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub requested_by: Option<crate::credentials::Principal>,
Get started on mission control140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub title: Option<String>,
142 /// On close: `completed` or `not_planned`.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub reason: Option<&'static str>,
145 /// On close: the number of the pull request whose merge closed it.
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub resolved_by: Option<u32>,
148 /// On `issue.assigned`: the people it is now assigned to.
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub assignees: Option<Vec<String>>,
Events: review requests, assignments, stops and deployments are published151 /// On `issue.assigned`: those of them who were not before.
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub added: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar154 /// On `issue.labeled` and `issue.unlabeled`: the label put on or taken
155 /// off. One event for each.
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub label: Option<EventLabel>,
158 /// On `issue.milestoned`: the milestone it was put in; on
159 /// `issue.demilestoned`, the one it was taken out of.
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub milestone: Option<crate::work::MilestoneRef>,
Get started on mission control162}
163
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar164/// A label, as `issue.labeled`, `pull.labeled` and their `unlabeled` say.
165#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
166pub struct EventLabel {
167 pub name: String,
168 /// Six hex digits.
169 pub color: String,
170}
171
Get started on mission control172/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts173/// moved), `pull.closed`, `pull.reopened`, `pull.converted_to_draft`,
174/// `pull.merged`, `pull.assigned`,
Events: review requests, assignments, stops and deployments are published175/// `pull.review_requested` and `pull.review_request_removed` (reviewers
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar176/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
177/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
178/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
179/// through until a person steps in) and `pull.resumed` (it picked back
180/// up); each uses the fields that apply to it.
Get started on mission control181#[derive(Debug, Default, Serialize)]
182#[serde(rename_all = "camelCase")]
183pub struct PullEvent {
184 pub pull_id: String,
185 pub repo_id: String,
186 pub number: u32,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights187 /// Who opened it: g1t, for a change g1t made.
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub author: Option<crate::credentials::Principal>,
190 /// For a change g1t made: the person who asked for it.
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub requested_by: Option<crate::credentials::Principal>,
Get started on mission control193 /// The number of the issue it is for.
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub issue: Option<u32>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub agent: Option<String>,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts198 /// On merge: the commit the branch now points to. On update, when
199 /// marked ready and when reopened: the head of the change.
Get started on mission control200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub commit: Option<String>,
202 /// On close: the pull request that was merged instead.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 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 step205 /// How sure g1t is of a g1t agent's change, once it has worked that out.
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub confidence: Option<crate::work::Confidence>,
Events: review requests, assignments, stops and deployments are published208 /// On `pull.assigned`: the people it is now assigned to.
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub assignees: Option<Vec<String>>,
211 /// On `pull.assigned`: those newly assigned.
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub added: Option<Vec<String>>,
214 /// On `pull.review_requested`: the reviewers newly asked; on
215 /// `pull.review_request_removed`, those no longer asked.
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub reviewers: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar218 /// On `pull.review_requested` and `pull.review_request_removed`: the
219 /// teams newly asked, or no longer, each with the people it asks.
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub teams: Option<Vec<TeamRequested>>,
222 /// On `pull.review_requested`: asked because they own files it changes
223 /// (its CODEOWNERS file), not by a person.
224 #[serde(skip_serializing_if = "std::ops::Not::not")]
225 pub code_owners: bool,
Events: review requests, assignments, stops and deployments are published226 /// On `pull.stalled`: why g1t stopped, and what would start it again.
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub detail: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar229 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub label: Option<EventLabel>,
232 /// On `pull.milestoned`: the milestone it was put in; on
233 /// `pull.demilestoned`, the one it was taken out of.
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub milestone: Option<crate::work::MilestoneRef>,
236 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub base: Option<String>,
239}
240
241/// A team asked to review a pull request.
242#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct TeamRequested {
245 /// `workspace/slug`.
246 pub team: String,
247 /// Everyone in it who is told: the whole team, or, with review
248 /// assignment, the people picked (and the rest when it says to tell
249 /// them). Never the pull request's author.
250 pub notified: Vec<String>,
251 /// With review assignment: the people picked, who are asked as
252 /// reviewers themselves.
253 #[serde(default)]
254 pub assigned: Vec<String>,
255}
256
257/// The payload of every `team.*` event: `team.created`, `team.edited`,
258/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
259/// `team.member_removed` (with `username`, `role` and `previousRole`);
260/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
261/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
262/// name the repository as the event's own.
263#[derive(Clone, Debug, Default, Serialize)]
264#[serde(rename_all = "camelCase")]
265pub struct TeamChanged {
266 pub workspace: String,
267 pub team_id: String,
268 /// The team's slug, as it is now.
269 pub team: String,
270 pub name: String,
271 pub visibility: Option<crate::teams::TeamVisibility>,
272 /// The parent's slug.
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub parent: Option<String>,
275 /// On `team.edited`: what changed, such as `name` or `parent`.
276 #[serde(skip_serializing_if = "Vec::is_empty")]
277 pub changes: Vec<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub username: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub role: Option<crate::teams::TeamRole>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub previous_role: Option<crate::teams::TeamRole>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub repo_id: Option<String>,
286 /// `workspace/name`.
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub repo: Option<String>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub repo_role: Option<crate::access::RepoRole>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub previous_repo_role: Option<crate::access::RepoRole>,
Events: review requests, assignments, stops and deployments are published293}
294
295/// `deployment.succeeded` and `deployment.failed`: a build of a project
296/// finished, for production or for one pull request's preview.
297#[derive(Debug, Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct DeploymentEvent {
300 pub deployment_id: String,
301 pub project_id: String,
302 pub repo_id: String,
303 pub workspace: String,
304 /// The project's slug.
305 pub project: String,
306 /// `production` or `preview`.
307 pub kind: String,
308 pub branch: Option<String>,
309 /// For a preview: its pull request.
310 pub number: Option<u32>,
311 pub commit: String,
312 /// Where the deployment is on the site, such as
313 /// `/acme/rocket/deployments/dpl_1`.
314 pub path: String,
315 /// For a failure: what went wrong.
316 pub error: Option<String>,
317 /// For a success: whether the deployment before it, of the same app, failed.
318 pub recovered: bool,
319 /// Who started it, by username, or `g1t`.
320 pub triggered_by: String,
Get started on mission control321}
322
323/// `checks.completed`: a run of an issue's acceptance checks against a pull
324/// request finished.
325#[derive(Debug, Serialize)]
326#[serde(rename_all = "camelCase")]
327pub struct ChecksEvent {
328 pub pull_id: String,
329 pub repo_id: String,
330 pub number: u32,
331 /// `passed`, `failed` or `errored`.
332 pub status: &'static str,
333 /// The commit that was checked.
334 pub commit: String,
335}
336
Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24337/// `workflow.completed`: a GitHub Actions run finished.
338#[derive(Debug, Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct WorkflowEvent {
341 pub run_id: String,
342 pub repo_id: String,
343 /// The workflow's name, and its file.
344 pub workflow: String,
345 pub path: String,
346 /// The run's number among the workflow's runs.
347 pub number: u64,
348 /// The GitHub event that started it, such as `push`.
349 pub event: String,
350 /// `success`, `failure`, `cancelled` or `skipped`.
351 pub conclusion: String,
352 #[serde(rename = "ref")]
353 pub git_ref: String,
354 pub sha: String,
355 /// The pull request it ran for, if any.
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub pull: Option<u32>,
358}
359
Get started on mission control360/// `review.completed`: a g1t agent finished reviewing a pull request, or
361/// could not.
362#[derive(Debug, Serialize)]
363#[serde(rename_all = "camelCase")]
364pub struct ReviewEvent {
365 pub pull_id: String,
366 pub repo_id: String,
367 pub number: u32,
368 /// `approve` or `request_changes`; absent when no review was written.
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub verdict: Option<&'static str>,
371}
372
373/// `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 calendar374#[derive(Debug, Default, Serialize)]
Get started on mission control375#[serde(rename_all = "camelCase")]
376pub struct CommentCreated {
377 pub comment_id: String,
378 pub repo_id: String,
379 pub number: u32,
380 /// Set when the comment is on a pull request.
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub pull_id: Option<String>,
383 /// Set when the comment is a review: approve or request changes.
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub verdict: Option<crate::work::Verdict>,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar386 /// Set when one of the workspace's agents wrote it, as itself. The
387 /// event's actor is then the person it acted for (`actingFor`).
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub agent: Option<crate::work::AgentRef>,
390 /// Who the agent acted for, set with `agent`.
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub acting_for: Option<crate::credentials::Principal>,
393 /// An agent's review, advisory: its verdict counts toward nothing.
394 #[serde(skip_serializing_if = "std::ops::Not::not")]
395 pub advisory: bool,
Get started on mission control396}
397
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts398/// `comment.edited`: a comment's text changed. `changes.body.from` is what
399/// it said before; the comment as it is now is read by `commentId`.
400#[derive(Debug, Serialize)]
401#[serde(rename_all = "camelCase")]
402pub struct CommentEdited {
403 pub comment_id: String,
404 pub repo_id: String,
405 pub number: u32,
406 /// Set when the comment is on a pull request.
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub pull_id: Option<String>,
409 pub changes: CommentChanges,
410}
411
412/// What an edit changed, as `comment.edited` says it.
413#[derive(Debug, Serialize)]
414pub struct CommentChanges {
415 pub body: ChangedFrom,
416}
417
418/// A field's value before a change.
419#[derive(Debug, Serialize)]
420pub struct ChangedFrom {
421 pub from: String,
422}
423
424/// `comment.deleted`: a comment was deleted. It no longer exists when the
425/// event is read, so `comment` is the comment as it was.
426#[derive(Debug, Serialize)]
427#[serde(rename_all = "camelCase")]
428pub struct CommentDeleted {
429 pub comment_id: String,
430 pub repo_id: String,
431 pub number: u32,
432 /// Set when the comment was on a pull request.
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub pull_id: Option<String>,
435 pub comment: DeletedComment,
436}
437
438/// A deleted comment, as it was.
Get started on mission control439#[derive(Debug, Serialize)]
440#[serde(rename_all = "camelCase")]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts441pub struct DeletedComment {
442 pub id: String,
443 pub body: String,
444 pub author: crate::credentials::Principal,
445 /// RFC 3339.
446 pub created_at: String,
447 /// For a comment on one line of a pull request's change: the file.
448 pub path: Option<String>,
449 /// And the line, as numbered after the change.
450 pub line: Option<u32>,
451}
452
453/// The payload of every `release.*` event: `release.created`,
454/// `release.published`, `release.released`, `release.prereleased`,
455/// `release.edited`, `release.unpublished` and `release.deleted`. One change
456/// to a release can be several of them (see [`release_actions`]).
457#[derive(Debug, Serialize)]
458#[serde(rename_all = "camelCase")]
459pub struct ReleaseEvent {
460 pub release_id: String,
461 pub repo_id: String,
462 pub tag_name: String,
463 /// As it is now; as it was, for `release.deleted`.
464 pub release: crate::about::Release,
465 /// On `release.edited`: `{ "name": { "from" }, "body": { "from" } }`
466 /// for what changed.
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub changes: Option<serde_json::Value>,
469}
470
471/// A release's state, for [`release_actions`]: whether it is a draft, and
472/// whether it is a prerelease.
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub struct ReleaseState {
475 pub draft: bool,
476 pub prerelease: bool,
477}
478
479/// What a change to a release is, as GitHub's release activity types, in
480/// the order they are sent: from `before` (None: it did not exist) to
481/// `after` (None: deleted). A release made published is `created`,
482/// `published` and `released` (or `prereleased`); a draft published is
483/// `published` and `released` (or `prereleased`); made a draft again,
484/// `unpublished`; anything else `edited`, and a prerelease made a full
485/// release is `released` too.
486pub fn release_actions(before: Option<ReleaseState>, after: Option<ReleaseState>) -> Vec<&'static str> {
487 let published = |state: ReleaseState| if state.prerelease { "prereleased" } else { "released" };
488 match (before, after) {
489 (None, None) => Vec::new(),
490 (None, Some(after)) if after.draft => vec!["created"],
491 (None, Some(after)) => vec!["created", "published", published(after)],
492 (Some(_), None) => vec!["deleted"],
493 (Some(before), Some(after)) if before.draft && !after.draft => vec!["published", published(after)],
494 (Some(before), Some(after)) if !before.draft && after.draft => vec!["unpublished"],
495 (Some(before), Some(after)) if !after.draft && before.prerelease && !after.prerelease => vec!["edited", "released"],
496 (Some(_), Some(_)) => vec!["edited"],
497 }
498}
499
500/// The event kind for a release activity type from [`release_actions`].
501pub fn release_kind(action: &str) -> Option<&'static str> {
502 Some(match action {
503 "created" => "release.created",
504 "published" => "release.published",
505 "released" => "release.released",
506 "prereleased" => "release.prereleased",
507 "edited" => "release.edited",
508 "unpublished" => "release.unpublished",
509 "deleted" => "release.deleted",
510 _ => return None,
511 })
512}
513
514#[derive(Debug, Serialize)]
515#[serde(rename_all = "camelCase")]
Get started on mission control516pub struct SessionAppended {
517 pub pull_id: String,
518 pub repo_id: String,
519 pub number: u32,
520 pub count: u32,
521}
522
523/// An event as stored in the log and delivered to subscribers. `data` is
524/// left as JSON; each reader decodes the types it cares about.
525#[derive(Clone, Debug, Serialize, serde::Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct Event {
528 /// Sorts by the time the event was published.
529 pub id: String,
530 #[serde(rename = "type")]
531 pub kind: String,
532 /// The service that published it.
533 pub source: String,
534 /// RFC 3339.
535 pub time: String,
536 /// The repo the event concerns.
537 pub repo_id: Option<String>,
538 /// The user or agent that caused it, if any.
539 pub actor: Option<String>,
540 pub data: serde_json::Value,
541}
542
543/// `publish`, as a publisher sends it. Returns nothing.
544#[derive(Debug, Serialize)]
545pub struct Publish<T: Serialize> {
546 pub events: Vec<NewEvent<T>>,
547}
548
549/// `publish`, as the events service reads it.
550#[derive(Debug, serde::Deserialize)]
551pub struct PublishArgs {
552 pub events: Vec<Published>,
553}
554
555/// A [`NewEvent`] of any type, as received.
556#[derive(Debug, serde::Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct Published {
559 #[serde(rename = "type")]
560 pub kind: String,
561 pub source: String,
562 #[serde(default)]
563 pub repo_id: Option<String>,
564 #[serde(default)]
565 pub actor: Option<String>,
566 pub data: serde_json::Value,
567}
568
569/// `list`: events from the log, newest first. Returns `Vec<Event>`.
570#[derive(Debug, Default, Serialize, serde::Deserialize)]
571#[serde(rename_all = "camelCase")]
572pub struct ListArgs {
573 #[serde(default)]
574 pub repo_id: Option<String>,
575 /// Only these types; all types when empty.
576 #[serde(default)]
577 pub types: Vec<String>,
578 /// Only events older than this event id.
579 #[serde(default)]
580 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)581 /// Only events by this account id.
582 #[serde(default)]
583 pub actor: Option<String>,
584 /// Only events about these issues or pull requests: their `number`, or
585 /// the `issue` a comment, review or link is on. All when empty.
586 #[serde(default)]
587 pub numbers: Vec<u32>,
588 /// Only events at or after this RFC 3339 time.
589 #[serde(default)]
590 pub since: Option<String>,
Get started on mission control591 #[serde(default)]
592 pub limit: Option<u32>,
593}
594
Agents and memory, checks and conflicts, profiles, slug renames, custom domains595/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
596/// Every service that stores a slug moves its rows to the workspace's
597/// *current* slug (ask identity by `workspace_id`), so that a repeated or
598/// late delivery after a second rename still lands in the right place.
599#[derive(Clone, Debug, Serialize, serde::Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct WorkspaceRenamed {
602 pub workspace_id: String,
603 pub from: String,
604 pub to: String,
605}
606
607impl WorkspaceRenamed {
608 /// The slugs whose rows move to `current`: the two this rename names,
609 /// minus `current` itself. Moving rows keyed by either converges on the
610 /// current slug whatever order renames are delivered in.
611 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
612 let mut slugs: Vec<String> = Vec::new();
613 for slug in [&self.from, &self.to] {
614 if slug != current && !slugs.contains(slug) {
615 slugs.push(slug.clone());
616 }
617 }
618 slugs
619 }
620}
621
Search across all of g1t, Explore, and a command palette622/// `repo.updated`: a repository's description, topics or visibility
623/// changed. `visibility_changed` says whether it went public or private,
624/// which `repo.visibility_changed` also announces on its own.
625#[derive(Debug, Serialize, serde::Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub struct RepoUpdated {
628 pub repo_id: String,
629 pub namespace: String,
630 pub name: String,
631 pub is_private: bool,
632 #[serde(default)]
633 pub visibility_changed: bool,
634}
635
636/// `repo.visibility_changed`: a repository went public or private.
637#[derive(Debug, Serialize, serde::Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct RepoVisibilityChanged {
640 pub repo_id: String,
641 pub is_private: bool,
642}
643
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look644/// `repo.renamed`: a repository's name changed within its workspace,
645/// keeping its id and its git store key. Like `repo.transferred`, a path
646/// change: every service that keeps rows under the repository's path moves
647/// them to its *current* path (ask repos `path_by_id`), so a repeated or
648/// late delivery after a second rename or a transfer still lands in the
649/// right place. `g1t_kit::transfer::on_event` handles both.
650#[derive(Clone, Debug, Serialize, serde::Deserialize)]
Search across all of g1t, Explore, and a command palette651#[serde(rename_all = "camelCase")]
652pub struct RepoRenamed {
653 pub repo_id: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look654 /// The workspace it is in.
Search across all of g1t, Explore, and a command palette655 pub namespace: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look656 /// Its old name.
657 pub from: String,
658 /// Its new name.
659 pub to: String,
660}
661
662impl RepoRenamed {
663 /// The paths whose rows move to `current` (`namespace/name`): the two
664 /// this rename names, minus `current`.
665 pub fn stale_paths(&self, current: &str) -> Vec<String> {
666 let mut paths: Vec<String> = Vec::new();
667 for name in [&self.from, &self.to] {
668 let path = format!("{}/{name}", self.namespace);
669 if path != current && !paths.contains(&path) {
670 paths.push(path);
671 }
672 }
673 paths
674 }
675}
676
677/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
678/// git refuses it, but it can be restored until `purge_after`, so services
679/// stop what runs for it (agents, workflows, deployments, indexing,
680/// webhook deliveries) and hide it, and keep what they hold until
681/// `repo.purged`. `repo.restored` brings it back.
682#[derive(Clone, Debug, Serialize, serde::Deserialize)]
683#[serde(rename_all = "camelCase")]
684pub struct RepoDeleted {
685 pub repo_id: String,
686 #[serde(default)]
687 pub namespace: String,
688 #[serde(default)]
Search across all of g1t, Explore, and a command palette689 pub name: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look690 #[serde(default)]
691 pub is_private: bool,
692 /// RFC 3339: when it is purged unless restored first.
693 #[serde(default)]
694 pub purge_after: String,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member695 /// It went with its workspace (`workspace.deleting`). Services that
696 /// handle the workspace as a whole (deployments pauses its apps rather
697 /// than taking them down) leave this one to that.
698 #[serde(default)]
699 pub with_workspace: bool,
Search across all of g1t, Explore, and a command palette700}
701
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look702/// `repo.restored`: a deleted repository is back, at its path, as it was.
703/// Services start again what `repo.deleted` stopped: index it, deploy its
704/// production, show it.
705#[derive(Clone, Debug, Serialize, serde::Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct RepoRestored {
708 pub repo_id: String,
709 pub namespace: String,
710 pub name: String,
711 pub is_private: bool,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member712 /// It came back with its workspace (`workspace.restored`).
713 #[serde(default)]
714 pub with_workspace: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look715}
716
717/// `repo.purged`: a deleted repository is gone for good, its git data
718/// with it. Services drop every row they keep for it by `repo_id`, except
719/// history that belongs to its workspace: ledgers, invoices and the audit
720/// log. Its path is free for a new repository.
721#[derive(Clone, Debug, Serialize, serde::Deserialize)]
722#[serde(rename_all = "camelCase")]
723pub struct RepoPurged {
724 pub repo_id: String,
725 pub namespace: String,
726 pub name: String,
727}
728
729/// `repo.archived` and `repo.unarchived`: a repository became read-only,
730/// or writable again. While archived, pushes are refused, issues and pull
731/// requests are locked, and agents and workflows do not run for it; its
732/// deployments keep serving.
733#[derive(Clone, Debug, Serialize, serde::Deserialize)]
734#[serde(rename_all = "camelCase")]
735pub struct RepoArchived {
736 pub repo_id: String,
737 pub namespace: String,
738 pub name: String,
739 pub archived: bool,
740}
741
742/// `repo.default_branch_changed`: the branch everything lands on is now
743/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
744/// requests into it now target `to`) rather than another branch chosen.
745#[derive(Clone, Debug, Serialize, serde::Deserialize)]
746#[serde(rename_all = "camelCase")]
747pub struct RepoDefaultBranchChanged {
748 pub repo_id: String,
749 pub from: String,
750 pub to: String,
751 #[serde(default)]
752 pub renamed: bool,
753}
754
755/// `branch.renamed`: a branch was renamed. Pull requests from or into
756/// `from` follow it to `to`, and web addresses that name `from` redirect.
757#[derive(Clone, Debug, Serialize, serde::Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct BranchRenamed {
760 pub repo_id: String,
761 pub from: String,
762 pub to: String,
763 /// Whether it is the default branch.
764 #[serde(default)]
765 pub default_branch: bool,
766}
767
768/// `repo.transferred`: a repository moved from one workspace to another,
769/// keeping its id and its name. Every service that keeps a repository
770/// under its path (`namespace/name`) or its workspace's slug moves those
771/// rows to the repository's *current* path (ask repos `path_by_id`), so a
772/// repeated or late delivery after a second transfer still lands in the
773/// right place. What was charged or recorded before the transfer stays
774/// with the workspace it happened in.
775#[derive(Clone, Debug, Serialize, serde::Deserialize)]
Search across all of g1t, Explore, and a command palette776#[serde(rename_all = "camelCase")]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look777pub struct RepoTransferred {
Search across all of g1t, Explore, and a command palette778 pub repo_id: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look779 pub name: String,
780 /// The workspace it left.
781 pub from: String,
782 /// The workspace it went to.
783 pub to: String,
Search across all of g1t, Explore, and a command palette784}
785
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look786impl RepoTransferred {
787 /// The paths whose rows move to `current` (`namespace/name`): the two
788 /// this transfer names, minus `current`. Moving rows keyed by either
789 /// converges whatever order transfers are delivered in.
790 pub fn stale_paths(&self, current: &str) -> Vec<String> {
791 let mut paths: Vec<String> = Vec::new();
792 for namespace in [&self.from, &self.to] {
793 let path = format!("{namespace}/{}", self.name);
794 if path != current && !paths.contains(&path) {
795 paths.push(path);
796 }
797 }
798 paths
799 }
800}
801
802/// `workspace.deleted`: a workspace is gone. Services drop what they keep
803/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
804/// agent queue) and keep what is history: ledgers, invoices and the audit
805/// log stay under its slug, which is never given to another workspace.
806#[derive(Clone, Debug, Serialize, serde::Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct WorkspaceDeleted {
809 pub workspace_id: String,
810 pub slug: String,
811}
812
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member813/// `workspace.deleting`: an owner deleted a workspace, and it can be
814/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
815/// meantime. Services hide what they keep for it and stop what runs for it,
816/// keeping their rows: repos deletes its repositories softly (each with a
817/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
818/// apps, search drops it from results. `workspace.restored` undoes exactly
819/// that; once `purge_after` passes, `workspace.deleted` follows and
820/// services purge as for any deleted workspace.
821#[derive(Clone, Debug, Serialize, serde::Deserialize)]
822#[serde(rename_all = "camelCase")]
823pub struct WorkspaceDeleting {
824 pub workspace_id: String,
825 pub slug: String,
826 /// The username of the owner who deleted it.
827 pub by: String,
828 /// RFC 3339: when it is purged unless restored first.
829 pub purge_after: String,
830}
831
832/// `workspace.restored`: staff brought a deleted workspace back, with its
833/// members and tokens. Services undo what they did on `workspace.deleting`,
834/// and only that: a repository deleted on its own before stays deleted.
835#[derive(Clone, Debug, Serialize, serde::Deserialize)]
836#[serde(rename_all = "camelCase")]
837pub struct WorkspaceRestored {
838 pub workspace_id: String,
839 pub slug: String,
840}
841
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)842/// `user.deleting`: an account was deleted, by the person or by g1t's
843/// staff, and can be restored by staff until `purge_after`. Its sessions,
844/// tokens and keys have ended and it has left every workspace; services
845/// stop what they do for it (search drops its profile, nothing more is
846/// sent to it) and keep their rows. `user.restored` undoes that; once
847/// `purge_after` passes, `user.deleted` follows.
848#[derive(Clone, Debug, Serialize, serde::Deserialize)]
849#[serde(rename_all = "camelCase")]
850pub struct UserDeleting {
851 pub user_id: String,
852 pub username: String,
853 /// Whether g1t's staff deleted it rather than the person.
854 #[serde(default)]
855 pub by_staff: bool,
856 /// RFC 3339: when it is purged unless restored first.
857 pub purge_after: String,
858}
859
860/// `user.restored`: staff brought a deleted account back. It signs in
861/// again with its password; its old sessions and tokens stay ended.
862/// Services undo what they did on `user.deleting`.
863#[derive(Clone, Debug, Serialize, serde::Deserialize)]
864#[serde(rename_all = "camelCase")]
865pub struct UserRestored {
866 pub user_id: String,
867 pub username: String,
868}
869
870/// `user.deleted`: an account is gone for good. Services drop what they
871/// keep for it alone (its inbox, subscriptions, settings) and show what it
872/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
873/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
874/// its username. The username is never given to anyone again.
875#[derive(Clone, Debug, Serialize, serde::Deserialize)]
876#[serde(rename_all = "camelCase")]
877pub struct UserDeleted {
878 pub user_id: String,
879 pub username: String,
880}
881
Search across all of g1t, Explore, and a command palette882/// `user.updated`: an account was made, or changed what its profile shows
883/// (name, bio, avatar). Nothing private: ask identity for the profile.
884#[derive(Debug, Serialize, serde::Deserialize)]
885#[serde(rename_all = "camelCase")]
886pub struct UserUpdated {
887 pub username: String,
888}
889
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look890/// `user.email_added`, `user.email_verified`, `user.email_removed` and
891/// `user.primary_email_changed`: an account's addresses changed. Never the
892/// address itself; ask identity, as the person, for that.
893#[derive(Debug, Serialize, serde::Deserialize)]
894#[serde(rename_all = "camelCase")]
895pub struct UserEmailChanged {
896 pub user_id: String,
897 /// Whether g1t staff made the change.
898 #[serde(default)]
899 pub by_staff: bool,
900}
901
Search across all of g1t, Explore, and a command palette902/// `workspace.updated`: a workspace was made, or its name, description or
903/// icon changed. Ask identity for it by slug.
904#[derive(Debug, Serialize, serde::Deserialize)]
905#[serde(rename_all = "camelCase")]
906pub struct WorkspaceUpdated {
907 pub workspace_id: String,
908 pub slug: String,
909}
910
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look911/// `invite.created`: someone (or staff) made an invite. Never the code or
912/// the address it is for.
913#[derive(Debug, Serialize, serde::Deserialize)]
914#[serde(rename_all = "camelCase")]
915pub struct InviteCreated {
916 pub invite_id: String,
917 /// The account that made it; null when staff did.
918 pub inviter_id: Option<String>,
919 /// The workspace it joins.
920 pub workspace_id: Option<String>,
921 /// Whether it is bound to one email address.
922 pub bound: bool,
923}
924
925/// `invite.redeemed`: an invite was used, by a new account or by an
926/// existing one joining a workspace.
927#[derive(Debug, Serialize, serde::Deserialize)]
928#[serde(rename_all = "camelCase")]
929pub struct InviteRedeemed {
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)930 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
931 /// the account.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look932 pub invite_id: String,
933 pub user_id: String,
934 pub inviter_id: Option<String>,
935 pub workspace_id: Option<String>,
936 /// Whether it made the account.
937 pub created_account: bool,
938}
939
940/// `waitlist.requested`: someone asked for access. Ask identity's staff
941/// methods for the entry; the address is not in the event.
942#[derive(Debug, Serialize, serde::Deserialize)]
943#[serde(rename_all = "camelCase")]
944pub struct WaitlistRequested {
945 pub entry_id: String,
946}
947
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member948/// The payload of `package.published`, `package.version_deleted`,
949/// `package.deleted` and `package.visibility_changed`; each uses the fields
950/// that apply to it. `repo_id` is the repository the package is linked to.
951#[derive(Clone, Debug, Default, Serialize)]
952#[serde(rename_all = "camelCase")]
953pub struct PackageEvent {
954 pub package_id: String,
955 pub workspace: String,
956 pub ecosystem: String,
957 pub name: String,
958 pub repo_id: Option<String>,
959 /// The version published or deleted: for a container image, its
960 /// manifest's digest.
961 #[serde(skip_serializing_if = "Option::is_none")]
962 pub version: Option<String>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 pub digest: Option<String>,
965 #[serde(skip_serializing_if = "Option::is_none")]
966 pub size: Option<u64>,
967 /// On publish: the tags that now point to the version.
968 #[serde(skip_serializing_if = "Option::is_none")]
969 pub tags: Option<Vec<String>>,
970 /// On `package.visibility_changed`: `public` or `private`.
971 #[serde(skip_serializing_if = "Option::is_none")]
972 pub visibility: Option<String>,
973}
974
Get started on mission control975/// `queue.changed`: a repository's merge queue gained, lost or settled an
976/// entry, so the next batch may be ready to test.
977#[derive(Debug, Serialize)]
978#[serde(rename_all = "camelCase")]
979pub struct QueueChanged {
980 pub repo_id: String,
981}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains982
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store983/// The `doc.page.*` types the docs service (services/docs, TypeScript)
984/// publishes, with no `repoId` on the event: a page may be in a private
985/// space, so it never reaches a repository's timeline or webhooks.
986pub const DOC_PAGE_EVENTS: [&str; 4] = ["doc.page.created", "doc.page.updated", "doc.page.archived", "doc.page.stale"];
987
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 yet988/// The `folio.*` types (Artifacts mode), with their payloads, live in
989/// [`crate::folios`]; nothing publishes or subscribes to them yet.
990pub use crate::folios::FOLIO_EVENTS;
991
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store992/// What every `doc.page.*` event carries (`DocPageEventData` in
993/// events.ts). `doc.page.updated` adds `versionId`, `kind` and `authors`;
994/// `doc.page.stale` adds `repoId`, `repo`, `commit`, `pull`, `paths` and
995/// `owners` ([`DocPageStale`]).
996#[derive(Clone, Debug, Serialize, serde::Deserialize)]
997#[serde(rename_all = "camelCase")]
998pub struct DocPageEvent {
999 pub workspace: String,
1000 pub workspace_id: String,
1001 pub page_id: String,
1002 pub space_id: String,
1003 pub title: String,
1004 /// The page's address on the site.
1005 pub path: String,
1006}
1007
1008/// `doc.page.stale`: code a page cites changed.
1009#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1010#[serde(rename_all = "camelCase")]
1011pub struct DocPageStale {
1012 #[serde(flatten)]
1013 pub page: DocPageEvent,
1014 pub repo_id: String,
1015 /// `owner/name`.
1016 pub repo: String,
1017 pub commit: String,
1018 /// The merged pull request's number, when a pull request made the change.
1019 pub pull: Option<u32>,
1020 pub paths: Vec<String>,
1021 /// Member keys: `user:<id>`, `agent:<id>`.
1022 pub owners: Vec<String>,
1023}
1024
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1029 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1030 fn comment_edits_and_deletes_say_what_it_was() {
1031 let edited = CommentEdited {
1032 comment_id: "cmt_1".into(),
1033 repo_id: "rep_1".into(),
1034 number: 7,
1035 pull_id: Some("pr_1".into()),
1036 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
1037 };
1038 assert_eq!(
1039 serde_json::to_value(edited).unwrap(),
1040 serde_json::json!({
1041 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
1042 "changes": { "body": { "from": "Before" } },
1043 })
1044 );
1045 let deleted = CommentDeleted {
1046 comment_id: "cmt_1".into(),
1047 repo_id: "rep_1".into(),
1048 number: 7,
1049 pull_id: None,
1050 comment: DeletedComment {
1051 id: "cmt_1".into(),
1052 body: "Gone".into(),
1053 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1054 created_at: "2026-10-08T00:00:00Z".into(),
1055 path: None,
1056 line: None,
1057 },
1058 };
1059 assert_eq!(
1060 serde_json::to_value(deleted).unwrap(),
1061 serde_json::json!({
1062 "commentId": "cmt_1", "repoId": "rep_1", "number": 7,
1063 "comment": {
1064 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1065 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1066 },
1067 })
1068 );
1069 }
1070
1071 #[test]
1072 fn a_release_change_is_githubs_activity_types() {
1073 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1074 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1075 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1076 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1077 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1078 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1079 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1080 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1081 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1082 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1083 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1084 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1085 }
1086 assert_eq!(release_kind("archived"), None);
1087 }
1088
1089 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1090 fn what_a_job_token_did_is_marked_and_carried_on() {
1091 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1092 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1093 assert_eq!(caused_by_job(&plain), None);
1094 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1095 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1096 ..Default::default()
1097 }));
1098 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1099 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1100 assert_eq!(by_job["number"], 4);
1101 // A push by the job's token, and the pull request it moves.
1102 let push = GitPush {
1103 repo_id: "rep_1".into(),
1104 git_ref: "refs/heads/fix".into(),
1105 before: Some("a".into()),
1106 after: "b".into(),
1107 default_branch: false,
1108 unscanned: false,
1109 caused_by_job: job_run_of(&actor).map(str::to_owned),
Merge branch 'mirroring' into artifacts-mode1110 mirrored: false,
1111 mirror: None,
Merge branch 'worktree-agent-a3abfcce648e87dca'1112 };
1113 let push = serde_json::to_value(push).unwrap();
1114 assert_eq!(caused_by_job(&push), Some("run_9"));
1115 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1116 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1117 }
1118
1119 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1120 fn a_push_says_it_was_unscanned_only_when_it_was() {
1121 let push = |unscanned| GitPush {
1122 repo_id: "rep_1".into(),
1123 git_ref: "refs/heads/import".into(),
1124 before: None,
1125 after: "abc".into(),
1126 default_branch: false,
1127 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1128 caused_by_job: None,
Merge branch 'mirroring' into artifacts-mode1129 mirrored: false,
1130 mirror: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1131 };
1132 let quiet = serde_json::to_value(push(false)).unwrap();
1133 assert!(quiet.get("unscanned").is_none());
1134 let flagged = serde_json::to_value(push(true)).unwrap();
1135 assert_eq!(flagged["unscanned"], true);
1136 assert_eq!(flagged["ref"], "refs/heads/import");
1137 }
1138
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1139 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1140 WorkspaceRenamed {
1141 workspace_id: "wsp_1".into(),
1142 from: from.into(),
1143 to: to.into(),
1144 }
1145 }
1146
1147 #[test]
1148 fn stale_slugs_leave_out_the_current_one() {
1149 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1150 // Delivered after a second rename, b → c: both move to c.
1151 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1152 // Renamed back: a → b → a.
1153 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1154 }
1155
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1156 fn transferred(from: &str, to: &str) -> RepoTransferred {
1157 RepoTransferred {
1158 repo_id: "rep_1".into(),
1159 name: "rocket".into(),
1160 from: from.into(),
1161 to: to.into(),
1162 }
1163 }
1164
1165 #[test]
1166 fn stale_paths_leave_out_the_current_one() {
1167 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1168 // Delivered after a second transfer, b → c: both move to c.
1169 assert_eq!(
1170 transferred("a", "b").stale_paths("c/rocket"),
1171 vec!["a/rocket", "b/rocket"]
1172 );
1173 // Transferred back: a → b → a.
1174 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1175 }
1176
1177 #[test]
1178 fn a_transfer_reads_as_published() {
1179 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1180 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1181 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1182 }
1183
1184 #[test]
1185 fn a_rename_names_both_paths_in_its_workspace() {
1186 let renamed = RepoRenamed {
1187 repo_id: "rep_1".into(),
1188 namespace: "acme".into(),
1189 from: "old".into(),
1190 to: "new".into(),
1191 };
1192 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1193 // Delivered after a transfer: both names in acme move.
1194 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1195 }
1196
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1197 #[test]
1198 fn reads_the_published_payload() {
1199 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1200 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1201 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1202 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1203
1204 #[test]
1205 fn a_stale_page_reads_as_published() {
1206 let data = serde_json::json!({
1207 "workspace": "acme", "workspaceId": "wsp_1", "pageId": "pag_1", "spaceId": "spc_1",
1208 "title": "Exports", "path": "/acme/-/docs/general/exports-pag_1",
1209 "repoId": "rep_1", "repo": "acme/web", "commit": "abc", "pull": 431,
1210 "paths": ["src/export.ts"], "owners": ["user:usr_1"]
1211 });
1212 let event: DocPageStale = serde_json::from_value(data).unwrap();
1213 assert_eq!((event.page.page_id.as_str(), event.pull), ("pag_1", Some(431)));
1214 assert!(DOC_PAGE_EVENTS.contains(&"doc.page.stale"));
1215 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1216}

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