Skip to content

g1t/crates/contracts/src/webhooks.rs

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

This file's history is long; its oldest lines are credited to the oldest commit read.