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.

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