Skip to content

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

330 linesCodeBlameRaw
1//! The webhooks service: events, delivered to the addresses a repository or
2//! a workspace registers.
3//!
4//! Every event g1t publishes can be delivered: an HTTPS `POST` of JSON,
5//! signed with the webhook's secret in `X-G1t-Signature-256`, and retried
6//! with growing waits when the receiver does not answer with a 2xx. Each
7//! delivery is kept, with what was sent and what came back, and can be sent
8//! again.
9//!
10//! Mirrors `packages/contracts/src/webhooks.ts`.
11
12use serde::{Deserialize, Serialize};
13
14use crate::repos::RepoPath;
15use crate::{User, Viewer};
16
17/// Every event a webhook can be sent, in the order people are shown them.
18pub const EVENT_TYPES: [&str; 131] = [
19 "git.push",
20 "git.deleted",
21 "branch.renamed",
22 "repo.created",
23 "repo.forked",
24 "repo.detached",
25 "repo.updated",
26 "repo.visibility_changed",
27 "repo.renamed",
28 "repo.transferred",
29 "repo.default_branch_changed",
30 "repo.archived",
31 "repo.unarchived",
32 "repo.deleted",
33 "repo.restored",
34 "repo.purged",
35 "repo.collaborator_added",
36 "repo.collaborator_removed",
37 "repo.collaborator_role_changed",
38 "team.created",
39 "team.edited",
40 "team.deleted",
41 "team.member_added",
42 "team.member_role_changed",
43 "team.member_removed",
44 "team.repo_added",
45 "team.repo_role_changed",
46 "team.repo_removed",
47 "workspace.member_joined",
48 "ruleset.created",
49 "ruleset.updated",
50 "ruleset.deleted",
51 "work_item.opened",
52 "work_item.updated",
53 "work_item.assigned",
54 "work_item.labeled",
55 "work_item.unlabeled",
56 "work_item.milestoned",
57 "work_item.demilestoned",
58 "work_item.planned",
59 "work_item.closed",
60 "work_item.reopened",
61 "work_item.linked",
62 "work_item.unlinked",
63 "work_item.landed",
64 "comment.created",
65 "comment.edited",
66 "comment.deleted",
67 "commit_comment.created",
68 "commit_comment.edited",
69 "commit_comment.deleted",
70 "pull.opened",
71 "pull.ready",
72 "pull.updated",
73 "pull.merge_requested",
74 "pull.merged",
75 "pull.closed",
76 "pull.reopened",
77 "pull.converted_to_draft",
78 "pull.assigned",
79 "pull.review_requested",
80 "pull.review_request_removed",
81 "pull.labeled",
82 "pull.unlabeled",
83 "pull.milestoned",
84 "pull.demilestoned",
85 "pull.base_changed",
86 "pull.auto_merge_enabled",
87 "pull.auto_merge_disabled",
88 "pull.head_branch_deleted",
89 "pull.head_branch_restored",
90 "pull.stalled",
91 "pull.resumed",
92 "release.created",
93 "release.published",
94 "release.released",
95 "release.prereleased",
96 "release.edited",
97 "release.unpublished",
98 "release.deleted",
99 "checks.completed",
100 "status.created",
101 "check_run.created",
102 "check_run.completed",
103 "check_run.rerequested",
104 "check_run.requested_action",
105 "check_suite.completed",
106 "check_suite.rerequested",
107 "workflow.completed",
108 "deployment.succeeded",
109 "deployment.failed",
110 "deployment.created",
111 "deployment_status.created",
112 "queue.changed",
113 "integration.failed",
114 "integration.passed",
115 "decision.recorded",
116 "session.appended",
117 "session.started",
118 "session.ended",
119 "session.progressed",
120 "work.claimed",
121 "work.released",
122 "work.claim_expired",
123 "work.handoff_requested",
124 "dependency.changed",
125 "overlap.detected",
126 "package.published",
127 "package.version_deleted",
128 "package.deleted",
129 "package.visibility_changed",
130 "secret_scanning_alert.created",
131 "secret_scanning_alert.fixed",
132 "secret_scanning_alert.dismissed",
133 "secret_scanning_alert.reopened",
134 "secret_scanning.bypass_requested",
135 "secret_scanning.bypass_reviewed",
136 "code_scanning_alert.created",
137 "code_scanning_alert.fixed",
138 "code_scanning_alert.dismissed",
139 "code_scanning_alert.reopened",
140 "vulnerability_alert.created",
141 "vulnerability_alert.fixed",
142 "vulnerability_alert.dismissed",
143 "secret_scanning_alert.summary",
144 "code_scanning_alert.summary",
145 "vulnerability_alert.reopened",
146 "vulnerability_alert.summary",
147 "security_advisory.published",
148 "security_advisory.withdrawn",
149 "security_advisory.reported",
150];
151
152/// Event types webhooks were once offered that g1t no longer sends: they
153/// came from g1t's own agents, since removed. A webhook saved with one
154/// still reads, and saving its list again leaves them out.
155pub const RETIRED_EVENT_TYPES: [&str; 2] = ["agent.asked", "review.completed"];
156
157/// What a webhook belongs to.
158#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum HookScope {
161 /// One repository's events.
162 Repo,
163 /// The events of every repository in a workspace.
164 Workspace,
165}
166
167#[derive(Clone, Debug, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct Hook {
170 pub id: String,
171 pub scope: HookScope,
172 pub workspace: String,
173 /// For a repository's webhook: `owner/name`.
174 pub repo: Option<String>,
175 pub url: String,
176 /// The event types it is sent, or `["*"]` for all.
177 pub events: Vec<String>,
178 pub active: bool,
179 /// The last four characters of its secret.
180 pub secret_hint: String,
181 pub created_by: String,
182 /// RFC 3339.
183 pub created_at: String,
184 /// How its latest delivery went: `delivered`, `pending` or `failed`.
185 pub last_status: Option<String>,
186 pub last_delivered_at: Option<String>,
187}
188
189/// One event sent, or being sent, to a webhook.
190#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct HookDelivery {
193 pub id: String,
194 pub hook_id: String,
195 /// The event's id, or empty for a ping.
196 pub event_id: String,
197 pub event: String,
198 /// `pending` while it will be tried again, `delivered`, or `failed` once
199 /// it has been tried as often as it will be.
200 pub status: String,
201 pub attempts: u32,
202 /// The receiver's HTTP status, the last time it answered.
203 pub response_status: Option<u16>,
204 /// The start of what it answered.
205 pub response_body: Option<String>,
206 /// Why the last attempt failed, when the receiver could not be reached.
207 pub error: Option<String>,
208 pub duration_ms: Option<u32>,
209 /// The JSON that was sent.
210 pub payload: String,
211 /// RFC 3339.
212 pub created_at: String,
213 pub delivered_at: Option<String>,
214 pub next_attempt_at: Option<String>,
215}
216
217/// Which webhooks a call is about: a repository's, or with `repo` left out,
218/// the workspace's own.
219#[derive(Clone, Debug, Serialize, Deserialize)]
220pub struct HookOwner {
221 pub workspace: String,
222 #[serde(default)]
223 pub repo: Option<RepoPath>,
224}
225
226/// `list`. Returns `Outcome<Vec<Hook>>`. Members of the workspace only.
227#[derive(Debug, Serialize, Deserialize)]
228pub struct ListArgs {
229 pub viewer: Viewer,
230 #[serde(flatten)]
231 pub owner: HookOwner,
232}
233
234/// `create`. Returns `Outcome<CreatedHook>`. Members, for a repository's
235/// webhooks; owners, for the workspace's.
236#[derive(Debug, Serialize, Deserialize)]
237pub struct CreateArgs {
238 pub actor: User,
239 #[serde(flatten)]
240 pub owner: HookOwner,
241 pub url: String,
242 /// Event types, or `["*"]` for all. All when empty.
243 #[serde(default)]
244 pub events: Vec<String>,
245 /// Made by g1t when left out.
246 #[serde(default)]
247 pub secret: Option<String>,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
251pub struct CreatedHook {
252 pub hook: Hook,
253 /// The secret, when g1t made it: shown this once.
254 pub secret: Option<String>,
255}
256
257/// `update`: only the fields given change. Returns `Outcome<Hook>`.
258#[derive(Debug, Serialize, Deserialize)]
259pub struct UpdateArgs {
260 pub actor: User,
261 #[serde(flatten)]
262 pub owner: HookOwner,
263 pub id: String,
264 #[serde(default)]
265 pub url: Option<String>,
266 #[serde(default)]
267 pub events: Option<Vec<String>>,
268 #[serde(default)]
269 pub active: Option<bool>,
270}
271
272/// `delete` (returns `Outcome<bool>`) and `ping` (sends a `ping` event and
273/// returns `Outcome<HookDelivery>`).
274#[derive(Debug, Serialize, Deserialize)]
275pub struct HookArgs {
276 pub actor: User,
277 #[serde(flatten)]
278 pub owner: HookOwner,
279 pub id: String,
280}
281
282/// `deliveries`: a webhook's latest deliveries, newest first. Returns
283/// `Outcome<Vec<HookDelivery>>`.
284#[derive(Debug, Serialize, Deserialize)]
285pub struct DeliveriesArgs {
286 pub viewer: Viewer,
287 #[serde(flatten)]
288 pub owner: HookOwner,
289 pub id: String,
290}
291
292/// `redeliver`: sends a delivery's payload again, as a new delivery.
293/// Returns `Outcome<HookDelivery>`.
294#[derive(Debug, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct RedeliverArgs {
297 pub actor: User,
298 #[serde(flatten)]
299 pub owner: HookOwner,
300 pub delivery_id: String,
301}
302
303#[cfg(test)]
304mod tests {
305 use super::{EVENT_TYPES, RETIRED_EVENT_TYPES};
306
307 #[test]
308 fn retired_events_are_not_offered() {
309 for kind in RETIRED_EVENT_TYPES {
310 assert!(!EVENT_TYPES.contains(&kind), "{kind}");
311 }
312 }
313
314 /// The TypeScript mirror lists the same events, in the same order.
315 #[test]
316 fn the_typescript_mirror_lists_the_same_events() {
317 let ts = include_str!("../../../packages/contracts/src/webhooks.ts");
318 let list = ts
319 .split_once("export const EVENT_TYPES = [")
320 .and_then(|(_, rest)| rest.split_once("] as const"))
321 .map(|(list, _)| list)
322 .expect("EVENT_TYPES in webhooks.ts");
323 let mirrored: Vec<&str> = list
324 .split(',')
325 .map(|item| item.trim().trim_matches('"'))
326 .filter(|item| !item.is_empty())
327 .collect();
328 assert_eq!(mirrored, EVENT_TYPES);
329 }
330}