Skip to content

g1t/crates/contracts/src/webhooks.rs

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