Skip to content

g1t/crates/contracts/src/webhooks.rs

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