Skip to content

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

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