Skip to content

g1t/crates/contracts/src/webhooks.rs

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