Skip to content

feat: универсальный GitLab webhook endpoint с фильтрами, шаблонами и роутингом по чатам#19

Merged
lavr merged 28 commits into
mainfrom
gitlab-mr-express-notifications
Jul 8, 2026
Merged

feat: универсальный GitLab webhook endpoint с фильтрами, шаблонами и роутингом по чатам#19
lavr merged 28 commits into
mainfrom
gitlab-mr-express-notifications

Conversation

@lavr

@lavr lavr commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Новый эндпоинт POST /api/v1/gitlab, принимающий group/project webhooks GitLab и
рассылающий сообщения в чаты eXpress. Две связанные части:

1. Универсальный приёмник событий

  • Generic-decode payload → деривация event-key (kind / kind.subtype) и
    нормализация полей (project, user, title, url, branch).
  • Фильтрация на уровне конфига: events.only / events.exclude с wildcard
    (kind, kind.subtype, kind.*).
  • Реестр шаблонов event → template со встроенными дефолтами и генерик-fallback;
    любой ключ переопределяется в конфиге (inline или template_files).
  • error_events → сообщения уходят с status=error (BotX notification.status).
  • Аутентификация по X-Gitlab-Token (subtle.ConstantTimeCompare), т.к. GitLab
    не умеет Authorization/X-API-Key.

2. Роутинг событий по чатам (server.gitlab.routes)

  • Упорядоченный список правил; срабатывают все совпавшие (их чаты
    объединяются + дедуплицируются), флаг stop обрывает перебор.
  • Контекст матчинга — нормализованные поля поверх сырого payload (dotted-путь):
    reserved-ключи kind/event/action/project/branch/user/title/url + произвольные
    пути в Raw.
  • Паттерны: glob по умолчанию, /regex/ для регэкспа (RE2, компиляция на старте);
    event — через тот же матчер, что only/exclude.
  • Приоритет чатов: ?chat_id → routes → default_chat_id → global default →
    единственный чат → 200 ignored.
  • Фан-аут best-effort: 200 с по-чатовыми results/errors если доставлено ≥1;
    502 если упали все (GitLab ретрайнет без дублей в успешных чатах).

Секции server.gitlab.* полностью опциональны; без них поведение не меняется.

Docs & examples

  • docs/integrations.md, docs/configuration.md, README.md, OpenAPI (/gitlab).
  • examples/gitlab/ — конфиги (базовый + с роутингом) и примеры payload (MR, note,
    push, pipeline, issue).

Tests

  • go build ./... — OK
  • go test ./... — все пакеты зелёные
  • Линт: без нового долга (56 замечаний против 60 на main — преэкзистинг репозитория)

🤖 Generated with Claude Code

lavr and others added 28 commits July 7, 2026 20:39
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- config: register server.gitlab in knownKeys so config validate no longer
  emits a spurious unknown-key warning and validates keys inside the block
- server: reject empty X-Gitlab-Token outright (defense-in-depth for direct
  WithGitlab construction with an empty SecretToken)
- server: collapse duplicated open/merge branches in classifyGitlab
- serve: drop redundant gitlab endpoint-enabled log (server.New already logs it)
- docs/commands.md: document /gitlab endpoint, X-Gitlab-Token auth exception,
  and gitlab cross-reference validation
- tests: cover 502 send failure, MR comment without nested merge_request,
  author username fallback, global-default/single-chat/query chat resolution;
  render the default template in buildGitlabConfig test instead of asserting
  on an internal field; assert no spurious unknown-key warning for server.gitlab

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1: заменён типизированный GitlabWebhook на generic map[string]any decode;
новый gitlabView{Kind,Action,EventKey,Project,User,Title,URL,Raw}; deriveEventKey
по правилам object_kind, normalizeGitlab best-effort, template-helper get/.Get;
generic default-шаблон; хендлер и тесты переписаны под новую модель.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Обновлён /gitlab под generic-модель: любой object_kind, event-ключ
kind/kind.subtype, фильтры only/exclude, реестр шаблонов, error_events.

- openapi.yaml: описание /gitlab, generic GitlabWebhook, GitlabIgnoredResponse,
  200 sent/ignored oneOf
- docs/integrations.md: деривация субтипов, фильтры, шаблоны+переменные+хелперы,
  error_events
- docs/configuration.md: таблица полей server.gitlab (events/templates/
  template_files/error_events)
- README: краткое упоминание универсального приёмника
- examples/gitlab: конфиг с фильтрами/шаблонами + payload pipeline/push/issue

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…я gitlab-хендлера

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- selectTemplate: match kind.* wildcard template keys at runtime, so
  validated/documented wildcard keys actually take effect (previously dead)
- untrack express-botx.yaml (default auto-loaded config that leaked a live
  BotX token) and gitignore it; token must be rotated separately
- remove committed docs/.DS_Store; gitignore .DS_Store
- docs/commands.md: /gitlab is universal (any event), not MR-only
- tests: kind.* wildcard selection, runtime template-execution 400 path,
  gitlabIsEmpty non-string branch, secret-resolve-error startup guard;
  drop weak duplicate of the endpoint-not-registered test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Guard against posting a blank message: a payload without object_kind
  renders the generic default to an empty string; treat it as ignored
  instead of delivering an empty notification.
- deriveEventKey: fall back to a flat top-level status (e.g. deployment
  hooks have no object_attributes), so deployment.success/failed can be
  targeted by only/exclude/error_events like build_status.
- examples/gitlab/config.yaml: add build.failed to only so the
  build.failed error_events entry is actually reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- openapi.yaml: GitlabFanoutResponse schema, routes in /gitlab description,
  oneOf results/errors on 200/502, updated chat-resolution priority
- docs/integrations.md: "Роутинг событий по чатам" section (all-match+stop,
  selectors, branch normalization, glob vs /regex/, fan-out codes)
- docs/configuration.md: routes in example + fields table + chat priority
- examples/gitlab/config-routing.yaml: multi-rule fan-out + stop example
  (passes `config validate`); README entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- gitlab routing: match reserved 'project' selector against
  project.path_with_namespace (fallback project.name) per the design spec,
  so namespaced globs like 'group/backend/*' route correctly against real
  GitLab payloads; keep template .Project as the short name for display
- add gitlabProject helper decoupling routing (namespace) from display (name)
- tests: cover config->Routes wiring in buildGitlabConfig, namespaced-project
  routing end-to-end through normalizeGitlab, and error_events status on the
  fan-out path; update unit-test views to carry project in Raw
- docs/examples: correct project-selector precedence, add path_with_namespace
  to sample payloads, align config-routing example, document kind/kind.* template
  catch-all ambiguity, broaden GitlabIgnoredResponse description

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "build" (job hook) case to gitlabBranch so branch selectors match
failed jobs on protected branches. GitLab job hooks carry the branch in a
top-level ref field, which was previously ignored, causing build.failed
events to resolve to an empty branch and silently miss the branch-filtered
route in the shipped example (routing failures to default_chat_id instead
of oncall). Align the doc comment and integrations.md normalization table,
and add gitlabBranch test coverage for build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- gitlab routing: preserve namespace of bare slashed branch on build/job
  events (was reduced to basename, breaking feature/* rules)
- design spec: correct non-working object_attributes.labels example
Keep spec files locally as working artifacts; they should not ship in the
repo. docs/superpowers/ now gitignored alongside docs/plans/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lavr lavr merged commit a6ad6bb into main Jul 8, 2026
7 checks passed
@lavr lavr deleted the gitlab-mr-express-notifications branch July 8, 2026 19:24
lavr added a commit that referenced this pull request Jul 8, 2026
Универсальный приём событий (only/exclude, шаблоны, error_events) и роутинг по чатам с фан-аутом.
@lavr lavr self-assigned this Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant