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