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