Help us learn about your current experience with the documentation. Take the survey.

Webhook 开发者指南

本文档是 GitLab webhooks 的开发者指南。

Webhook 会将 GitLab 中发生的事件或变更的 JSON 数据发送到 webhook 接收器。
使用 webhook 后,当特定变更发生时,系统会主动通知客户,无需轮询 API。

Webhook 流程

以下描述了 webhook 被触发和执行时的高层级流程:

sequenceDiagram
    Web or API node->>+Database: 获取 payload 数据
    Database-->>-Web or API node: 构建 payload
    Note over Web or API node,Database: Webhook 已触发
    Web or API node->>Sidekiq: 将 webhook 执行加入队列
    Sidekiq->>+Remote webhook receiver: POST webhook payload
    Remote webhook receiver-)-Database: 将响应保存到 WebHookLog
    Note over Database,Remote webhook receiver: Webhook 已执行

添加新 Webhook

Webhook 采用资源导向设计。例如,“emoji” webhook 在表情符号被授予或撤销时触发。

为资源添加 webhook 支持的步骤:

  1. web_hooks 表中添加新列。新列必须满足:

    • 为布尔类型
    • 不允许为空
    • 命名格式为 <resource>_events
    • 默认值为 false

    迁移文件中 #change 方法的示例:

    def change
      add_column :web_hooks, :emoji_events, :boolean, null: false, default: false
    end
  2. TriggerableHooks.available_triggers 中添加对新 webhook 的支持。

  3. 根据 webhook 应配置在项目、群组还是 GitLab 实例级别,将其添加到 ProjectHookGroupHookSystemHooktriggerable_hooks 列表中。
    参见 项目、群组和系统 webhook 获取指导。

  4. app/views/shared/web_hooks/_form.html.haml 中为 webhook 设置表单添加新的复选框前端支持。

  5. TestHooks::ProjectService 和/或 TestHooks::SystemService 中添加对新 webhook 的测试支持。
    TestHooks::GroupService 无需更新,因为它仅 执行 ProjectService

  6. 定义 webhook payload

  7. 更新 GitLab 以 触发 webhook

  8. 添加 webhook 文档

  9. 添加 REST API 支持:

    1. 更新 API::ProjectHooksAPI::GroupHooks 和/或 API::SystemHooks 以支持该参数。
    2. 更新 API::Entities::ProjectHook 和/或 API::Entities::GroupHook 以支持新字段(系统 hook 使用通用 API::Entities::Hook)。
    3. 更新 项目 webhook群组 webhook 和/或 系统 webhook 的 API 文档。

决策:项目、群组和系统 Webhook

使用以下标准决定你的 webhook 应配置在项目、群组还是 GitLab 实例级别:

  • 与特定层级资源相关的 webhook 应在该层级可配置。
    例如:issue webhook 可在项目级别配置,群组成员关系 webhook 可在群组级别配置,用户登录失败 webhook 可在 GitLab 实例级别配置。
  • 可在项目级别配置的 webhook 通常也应支持群组级别配置,因为群组所有者常配置群组 webhook 以接收该群组下所有项目的事件,且群组 webhook 在项目 webhook 触发时会 自动执行
  • 通常,仅项目或群组(或两者)可配置的 webhook,只有在实例管理员明确需要接收时才应支持实例级别配置。
    许多现有的项目/群组 webhook 不支持实例级别配置。

仅 EE 版注意事项

群组 webhook 是高级版(Premium)功能。所有与触发群组 webhook 或构建仅群组可配置 webhook payload 相关的代码,必须位于 ee/ 目录

触发 Webhook

触发项目和群组 Webhook

项目和群组 webhook 通过在项目或群组上调用 #execute_hooks 触发。

#execute_hooks 方法接收:

  • 要 POST 到 webhook 接收器的 webhook payload
  • webhook 类型名称。

例如:

project.execute_hooks(payload, :emoji_hooks)

当在单个项目或群组上调用 #execute_hooks 时,触发会自动向上冒泡到祖先群组并执行。这允许群组配置为接收其任何子群组或项目事件的 webhook。

当方法调用对象为:

  • 项目:除执行该项目配置的同类型 webhook 外,项目群组和祖先群组配置的同类型 webhook 也会 执行。任何配置的实例(系统)同类型 webhook 也会 执行
  • 群组:除执行该群组配置的同类型 webhook 外,祖先群组配置的同类型 webhook 也会 执行

构建 payload 可能很耗时,因为它通常需要从数据库加载更多记录,因此在触发 webhook 前检查项目的 #has_active_hooks?(群组的类似方法支持在 issue #517890 中跟踪)。

当以下任一条件满足时,方法返回 true

  • 项目或群组(或任何祖先群组)配置了指定类型的 webhook,因此至少应执行一个 webhook。
  • 在项目上调用时,为该类型 配置了系统 webhook

示例:

def execute_emoji_hooks
  return unless project.has_active_hooks?(:emoji_hooks)

  payload = Gitlab::DataBuilder::Emoji.build(emoji)
  project.execute_hooks(payload, :emoji_hooks)
end

触发实例(系统)Webhook

当项目 webhook 被触发时,配置了该类型的系统 webhook 会 自动执行

如果 webhook 不支持项目级别配置,也可通过 SystemHooksService 触发系统 hook。

示例:

SystemHooksService.new.execute_hooks_for(user, :create)

需要更新 SystemHooksService 以构建资源数据。

使用精确的 Payload 触发

Webhook payload 必须准确反映事件发生时的数据状态。
需注意避免因竞态条件或并发进程改变数据状态导致的问题,这会导致发送给接收器的 payload 不准确。

实现方法:

  • 尽量避免在构建 payload 前重新加载对象,因为其他进程可能已更改其状态。
  • 事件发生后立即构建 payload,这样为 payload 加载的任何额外数据也反映事件发生时的状态。

这两点意味着 payload 通常必须在请求内构建,而不能使用 Sidekiq 异步处理。

例外情况是 payload 仅包含不可变数据,但这种情况很少见。

Webhook Payload

Webhook payload 是 POST 到 webhook 接收器的 JSON 数据。

现有 webhook payload 参见 webhook 事件文档

Payload 中不应包含什么?

敏感数据绝不能包含在 payload 中,包括密钥和非公开用户邮箱(私有邮箱通过 User#hook_attrs 自动脱敏)。

构建 webhook payload 必须 高性能,因此添加到 payload 的每个新属性都必须证明其从数据库检索的开销是合理的。

权衡考虑:让少数客户在收到较小的 webhook 后通过 API 获取对象的部分数据,是否比让所有客户在 payload 中接收该数据更好?
此场景下,构建 webhook 和客户检索额外数据之间存在时间差,延迟可能导致 API 数据和 webhook 数据代表不同时间点的状态。

定义 Payload

对象应定义 #hook_attrs 方法返回 webhook payload 的对象属性。

#hook_attrs 中的属性必须使用静态键定义。方法必须返回特定属性集,而非直接返回 #attributes#as_json 的所有属性,否则模型所有未来属性都会包含在 payload 中(参见 issue 440384)。

Gitlab::DataBuilder:: 下的模块或类应组合完整 payload。完整 payload 通常包含关联对象。

完整 payload 结构参见 payload schema

示例:

# 对象定义 #hook_attrs:
class Car < ApplicationRecord
  def hook_attrs
    {
      make: make,
      color: color
    }
  end
end

# Gitlab::DataBuilder 模块组合完整 webhook payload:
module Gitlab
  module DataBuilder
    module Car
      extend self

      def build(car, action)
        {
          object_kind: 'car',
          action: action,
          object_attributes: car.hook_attrs,
          driver: car.driver.hook_attrs # 调用关联数据的 #hook_attrs
        }
      end
    end
  end
end

# 构建 payload:
Gitlab::DataBuilder::Car.build(car, 'start')

Payload Schema

历史上不同类型 webhook 的 payload schema 存在大量不一致性。

未来,除非新类型 webhook 的 payload 因一致性需求(如新 issuable 的 webhook)应模仿现有 schema,否则新 webhook schema 必须遵循以下规则:

  • 新 webhook schema 必须包含以下必需属性:
    • "object_kind":对象类型(小写下划线格式)。示例:"merge_request"
    • "action":描述刚发生的领域特定动词(现在时)。示例:"create""assign""update""revoke"。这有助于接收器识别和处理对象生命周期不同阶段触发 webhook 时的变更。
    • "object_attributes":包含事件后对象的属性,这些属性由 #hook_attrs 生成。
  • 关联数据必须是 payload 的顶级属性,不能嵌套在 "object_attributes" 中。
  • 如果 payload 包含 变更属性值记录,必须位于顶级 "changes" 对象中。

上述结构的 JSON schema 描述:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "推荐 GitLab webhook payload schema",
  "type": "object",
  "properties": {
    "object_kind": {
      "type": "string",
      "description": "对象类型(小写下划线格式)。示例:merge_request",
      "pattern": "^([a-zA-Z]+(_[a-zA-Z]+)*)$"
    },
    "action": {
      "type": "string",
      "description": "对象刚发生的领域特定动词(现在时)。示例:create, revoke",
    },
    "object_attributes": {
      "type": "object",
      "description": "事件后对象的属性"
    },
    "changes": {
      "type": "object",
      "description": "事件期间变更的可选属性",
      "patternProperties": {
        ".+" : {
          "type" : "object",
          "properties": {
            "previous": {
              "description": "事件前的属性值"
            },
            "current": {
              "description": "事件后的属性值"
            }
          },
          "required": ["previous", "current"]
        }
      }
    }
  },
  "required": ["object_kind", "action", "object_attributes"]
}

遵循上述 schema 的虚构 Car 对象的 webhook payload 示例:

{
  "object_kind": "car",
  "action": "start",
  "object_attributes": {
    "make": "Toyota",
    "color": "grey"
  },
  "driver": {
    "name": "Kaya",
    "age": 18
  }
}

包含变更对象

如果 payload 需包含对象属性变更列表,将 ReportableChanges 模块添加到模型中。
该模块收集从对象加载到后续所有保存操作期间的属性值变更。这在给定请求上下文中对象有多次保存操作且最终 hook 需要访问累积变更(而非最近一次保存的变更)时很有用。

如何将属性变更包含在 payload 中,参见 payload schema

最小化数据库请求

某些类型的 webhook 在 GitLab.com 上每天触发数百万次。

为 webhook payload 加载额外数据必须高性能,因为我们需要 在请求内构建 payload 而非通过 Sidekiq。在 GitLab.com 上,这也意味着 payload 的额外数据从 PostgreSQL 主库加载,因为 webhook 在数据库写入后触发。

构建 webhook payload 时最小化数据请求的方法:

可能需要预加载已加载记录的关联数据。此时可使用 ActiveRecord::Associations::Preloader

如果关联数据仅用于构建 payload,仅在 #has_active_hooks? 检查 通过后预加载该数据。

代码库中的良好实践示例是 Gitlab::DataBuilder::Pipeline

示例:

# 处理已加载的 issue
issue = Issue.first

# 假设已通过 #has_active_hooks? 检查,现在构建 webhook payload。
# 以下操作会导致 N+1 数据库查询:
# issue.notes.map(&:author).map(&:name)
#
# 改为先预加载关联数据避免 N+1
ActiveRecord::Associations::Preloader.new(records: [issue], associations: { notes: :author }).call;
issue.notes.map(&:author).map(&:name)

破坏性变更

我们不能对 webhook payload 进行破坏性变更。

如果 webhook 接收器可能因 payload 变更遇到错误,则该变更为破坏性变更。

只能进行增量变更(添加新属性)。

破坏性变更包括:

  • 删除属性
  • 重命名属性
  • "object_kind" 属性值的变更
  • "action" 属性值的变更

如果 "object_kind""action" 以外的属性值必须变更(如因功能移除),将值设为 null{}[] 而非删除属性。

测试

DataBuilder 编写单元测试时,需断言:

  • 使用 QueryRecorder 断言数据库请求数量固定。
    可通过 QueryRecorder 测量查询数,然后在测试中比较该数值,确保查询数不会在未明确同意的情况下变更。另见 数据预加载
  • payload 包含预期属性。

还需测试 webhook 应触发(或不触发)的场景,断言其正确触发。

变更 QA

可将 webhook URL 配置为 https://webhook.site 提供的地址,以查看 webhook 触发时生成的完整请求头和 payload。

变更审核

代码审核的常规审核者 外,webhook 变更还需由 Import & Integrate 组的后端团队成员审核。