Skip to content

g1t/crates/contracts/src/webhooks.rs

233 lines6,816 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Webhooks: every event, to your own addresses, signed and retried1//! 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.
Events: review requests, assignments, stops and deployments are published18pub const EVENT_TYPES: [&str; 46] = [
Webhooks: every event, to your own addresses, signed and retried19 "git.push",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look20 "branch.renamed",
Webhooks: every event, to your own addresses, signed and retried21 "repo.created",
22 "repo.forked",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 "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",
Webhooks: every event, to your own addresses, signed and retried36 "issue.opened",
37 "issue.updated",
38 "issue.assigned",
39 "issue.closed",
40 "issue.reopened",
41 "comment.created",
42 "pull.opened",
43 "pull.ready",
44 "pull.updated",
45 "pull.merge_requested",
46 "pull.merged",
47 "pull.closed",
Events: review requests, assignments, stops and deployments are published48 "pull.assigned",
49 "pull.review_requested",
50 "pull.review_request_removed",
51 "pull.stalled",
52 "pull.resumed",
Agents asked while not at work are woken to answer53 "agent.asked",
Webhooks: every event, to your own addresses, signed and retried54 "checks.completed",
55 "review.completed",
Actions: workflow_run, workflow.completed, artifacts on the run page, Node 2456 "workflow.completed",
Events: review requests, assignments, stops and deployments are published57 "deployment.succeeded",
58 "deployment.failed",
Webhooks: every event, to your own addresses, signed and retried59 "queue.changed",
60 "session.appended",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member61 "package.published",
62 "package.version_deleted",
63 "package.deleted",
64 "package.visibility_changed",
Webhooks: every event, to your own addresses, signed and retried65];
66
67/// What a webhook belongs to.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum HookScope {
71 /// One repository's events.
72 Repo,
73 /// The events of every repository in a workspace.
74 Workspace,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct Hook {
80 pub id: String,
81 pub scope: HookScope,
82 pub workspace: String,
83 /// For a repository's webhook: `owner/name`.
84 pub repo: Option<String>,
85 pub url: String,
86 /// The event types it is sent, or `["*"]` for all.
87 pub events: Vec<String>,
88 pub active: bool,
89 /// The last four characters of its secret.
90 pub secret_hint: String,
91 pub created_by: String,
92 /// RFC 3339.
93 pub created_at: String,
94 /// How its latest delivery went: `delivered`, `pending` or `failed`.
95 pub last_status: Option<String>,
96 pub last_delivered_at: Option<String>,
97}
98
99/// One event sent, or being sent, to a webhook.
100#[derive(Clone, Debug, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct HookDelivery {
103 pub id: String,
104 pub hook_id: String,
105 /// The event's id, or empty for a ping.
106 pub event_id: String,
107 pub event: String,
108 /// `pending` while it will be tried again, `delivered`, or `failed` once
109 /// it has been tried as often as it will be.
110 pub status: String,
111 pub attempts: u32,
112 /// The receiver's HTTP status, the last time it answered.
113 pub response_status: Option<u16>,
114 /// The start of what it answered.
115 pub response_body: Option<String>,
116 /// Why the last attempt failed, when the receiver could not be reached.
117 pub error: Option<String>,
118 pub duration_ms: Option<u32>,
119 /// The JSON that was sent.
120 pub payload: String,
121 /// RFC 3339.
122 pub created_at: String,
123 pub delivered_at: Option<String>,
124 pub next_attempt_at: Option<String>,
125}
126
127/// Which webhooks a call is about: a repository's, or with `repo` left out,
128/// the workspace's own.
129#[derive(Clone, Debug, Serialize, Deserialize)]
130pub struct HookOwner {
131 pub workspace: String,
132 #[serde(default)]
133 pub repo: Option<RepoPath>,
134}
135
136/// `list`. Returns `Outcome<Vec<Hook>>`. Members of the workspace only.
137#[derive(Debug, Serialize, Deserialize)]
138pub struct ListArgs {
139 pub viewer: Viewer,
140 #[serde(flatten)]
141 pub owner: HookOwner,
142}
143
144/// `create`. Returns `Outcome<CreatedHook>`. Members, for a repository's
145/// webhooks; owners, for the workspace's.
146#[derive(Debug, Serialize, Deserialize)]
147pub struct CreateArgs {
148 pub actor: User,
149 #[serde(flatten)]
150 pub owner: HookOwner,
151 pub url: String,
152 /// Event types, or `["*"]` for all. All when empty.
153 #[serde(default)]
154 pub events: Vec<String>,
155 /// Made by g1t when left out.
156 #[serde(default)]
157 pub secret: Option<String>,
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct CreatedHook {
162 pub hook: Hook,
163 /// The secret, when g1t made it: shown this once.
164 pub secret: Option<String>,
165}
166
167/// `update`: only the fields given change. Returns `Outcome<Hook>`.
168#[derive(Debug, Serialize, Deserialize)]
169pub struct UpdateArgs {
170 pub actor: User,
171 #[serde(flatten)]
172 pub owner: HookOwner,
173 pub id: String,
174 #[serde(default)]
175 pub url: Option<String>,
176 #[serde(default)]
177 pub events: Option<Vec<String>>,
178 #[serde(default)]
179 pub active: Option<bool>,
180}
181
182/// `delete` (returns `Outcome<bool>`) and `ping` (sends a `ping` event and
183/// returns `Outcome<HookDelivery>`).
184#[derive(Debug, Serialize, Deserialize)]
185pub struct HookArgs {
186 pub actor: User,
187 #[serde(flatten)]
188 pub owner: HookOwner,
189 pub id: String,
190}
191
192/// `deliveries`: a webhook's latest deliveries, newest first. Returns
193/// `Outcome<Vec<HookDelivery>>`.
194#[derive(Debug, Serialize, Deserialize)]
195pub struct DeliveriesArgs {
196 pub viewer: Viewer,
197 #[serde(flatten)]
198 pub owner: HookOwner,
199 pub id: String,
200}
201
202/// `redeliver`: sends a delivery's payload again, as a new delivery.
203/// Returns `Outcome<HookDelivery>`.
204#[derive(Debug, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct RedeliverArgs {
207 pub actor: User,
208 #[serde(flatten)]
209 pub owner: HookOwner,
210 pub delivery_id: String,
211}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look212
213#[cfg(test)]
214mod tests {
215 use super::EVENT_TYPES;
216
217 /// The TypeScript mirror lists the same events, in the same order.
218 #[test]
219 fn the_typescript_mirror_lists_the_same_events() {
220 let ts = include_str!("../../../packages/contracts/src/webhooks.ts");
221 let list = ts
222 .split_once("export const EVENT_TYPES = [")
223 .and_then(|(_, rest)| rest.split_once("] as const"))
224 .map(|(list, _)| list)
225 .expect("EVENT_TYPES in webhooks.ts");
226 let mirrored: Vec<&str> = list
227 .split(',')
228 .map(|item| item.trim().trim_matches('"'))
229 .filter(|item| !item.is_empty())
230 .collect();
231 assert_eq!(mirrored, EVENT_TYPES);
232 }
233}