g1t/crates/contracts/src/webhooks.rs
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 retried | 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. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 18 | pub const EVENT_TYPES: [&str; 35] = [ |
| Webhooks: every event, to your own addresses, signed and retried | 19 | "git.push", |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 20 | "branch.renamed", |
| Webhooks: every event, to your own addresses, signed and retried | 21 | "repo.created", |
| 22 | "repo.forked", | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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", | |
| Webhooks: every event, to your own addresses, signed and retried | 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", | |
| Agents asked while not at work are woken to answer | 48 | "agent.asked", |
| Webhooks: every event, to your own addresses, signed and retried | 49 | "checks.completed", |
| 50 | "review.completed", | |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 51 | "workflow.completed", |
| Webhooks: every event, to your own addresses, signed and retried | 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 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 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 | } |