Перейти к содержимому
GitRiverGitRiver
EN
Навигация

Обещание совместимости /api/v1

Что означает /api/v1, что в ответах считается договором, что менять запрещено, как объявляется устаревание и сколько живёт устаревший вызов

Собственный интерфейс GitRiver отвечает по адресу /api/v1. Этот документ — обязательство: что в ответах считается договором и не меняется, что менять запрещено, как объявляется устаревание и сколько живёт устаревший вызов.

Обязательство нужно тем, кто пишет по интерфейсу один раз и живёт с написанным годами: скриптам выгрузки и переноса, шаблонам отчётов, службам развёртывания, сторонним переходникам.

Обещание действует с версии 1.1.0 (25.09.2026) — первого выпуска, который его несёт. Всё, что отвечает в 1.1.0, договором и является. До неё вышла одна версия — 1.0.0 (29.03.2026); что меняется при переходе с неё, названо в конце документа поимённо.

Что означает v1

Номер в пути — версия договора, а не версия продукта. Продукт растёт (1.0 → 1.1 → дальше), путь остаётся /api/v1: новая минорная версия сервера не требует от клиента ни строчки правок.

Версия сервера приходит отдельно и запрашивается явно:

Где Что отдаёт
GET /health версия сборки и состояние базы: status (ok/error) и version
GET /api/openapi.json, поле info.version версия сборки, отдавшей спецификацию
GET /api/v1/auth/me редакция установки, в которой работает вызывающий: instance_is_pro, instance_is_max, is_pro, license_grace_until

Редакцию узнаёт только вызывающий с токеном. GET /api/v1/server-info отвечает без токена и отдаёт настройки, нужные интерфейсу до входа, — внешний адрес, включён ли SSH и приставку адреса клонирования, видны ли профили анониму; версии и редакции в нём нет.

Часть договора

Свойство Пример
Путь вызова и метод PATCH /api/v1/repos/{owner}/{name}/issues/{number}
Код успешного ответа 201 у создания, 204 у удаления, 200 у чтения и действия, 202 у принятой в работу
Обязательные поля тела ответа и их типы id, number, status у задачи
Форма конверта отказа {"error": "<текст>", "code": "<машинный код>"} — оба поля есть всегда
Значения машинных кодов отказа not_found, ref_not_found, unauthorized, forbidden, conflict, validation, internal, bad_gateway, too_large, license_required, rate_limited, registration_closed
Имена параметров страницы и полей выдачи у конкретного вызова per_page, after в запросе; items, next_cursor, total в ответе
Требование к токену вызов, отвечавший анониму, продолжает отвечать анониму
Принадлежность к редакции вызов, работавший в Community, не становится платным; вызов, которому хватало лицензии установки, не начинает требовать места Pro; вызов не начинает требовать более старшей редакции. Правило действует с 1.1.0; ужесточение, сделанное при переходе с 1.0.0, названо в конце документа поимённо

Имена мест подстановки в пути ({owner}, {name}) адрес клиента не меняют, но попадают в спецификацию и оттуда в сгенерированный по ней клиент — поэтому мы не переименовываем и их.

Форма ответов и правила пагинации описаны в руководстве пользователя; здесь сказано не как они устроены, а что из этого мы обязуемся не менять.

Договор — про интерфейс, а не про настройки установки. Владелец установки вправе закрыть профили, завести ограничения по сетевому происхождению или отозвать права: аноним увидит меньше, но это его решение, а не смена договора.

Не часть договора

  • Порядок ключей в объектах JSON.
  • Порядок элементов в списках, для которых сортировка не объявлена. Объявленная — часть договора (обсуждение идёт от старых к новым).
  • Тексты: поле error, описания операций в спецификации, надписи интерфейса. Текст переводится и уточняется; программа опирается на code. Язык текста выбирается по заголовку Accept-Language запроса (сейчас доступны ru и en); без заголовка или с недоступным языком — язык установки (настройка language). Язык ответа об отказе назван в Content-Language.
  • Устройство непрозрачных строк. next_cursor — непрозрачная строка: её нужно вернуть следующим запросом как есть. Разбирать её содержимое нельзя, кодирование может измениться.
  • Значения пределов: размер страницы, пороги частоты, предельный размер тела. Они настраиваются владельцем установки и от установки к установке различаются. Договор — поведение на границе, а не число: размер страницы и смещение усекаются к допустимому диапазону (?limit=-1 отдаёт одну запись, а не 400), тело сверх предела получает 413, запрос сверх частоты — 429 с Retry-After.
  • Время ответа, номера идентификаторов, содержимое журналов.

Где конверта отказа нет

Конверт {"error", "code"} приходит у отказов вызовов API — в том числе на несуществующем адресе под /api/v1 (404 с кодом not_found) и при превышении частоты (429, там же поле retry_after и заголовки Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining).

Без конверта могут прийти три отказа: 405 — путь есть, метод другой; 504 — истёк предельный срок ответа; обрыв слишком большого тела при выгрузке файла составным телом (multipart/form-data). Тела у них может не быть вовсе, и опираться клиенту следует на код ответа. Договор здесь — сам код ответа, а не то, что лежит (или не лежит) в теле.

При приёме архива артефактов и кеша и вложения выпуска 413 приходит обычным конвертом с кодом too_large. Отказы разбора тела в формате JSON, включая 413, тоже приходят общим конвертом (см. ниже).

Что менять запрещено

Каждая строка — то, что молча ломает уже написанного клиента.

Запрещено Что видит клиент
Снять путь 404 на вызове, который работал
Снять метод у пути, включая устаревший псевдоним 405 на вызове, который работал
Переименовать или убрать обязательное поле ответа отсутствующий ключ, KeyError в скрипте
Сменить тип поля разбор падает либо считает не то
Сменить код успешного ответа клиент, сгенерированный по спецификации, считает ответ неожиданным
Переименовать машинный код отказа или сменить код ответа у рода отказа ветка обработки перестаёт выбираться
Потребовать токен там, где отвечали анониму 401 вместо данных
Сделать обязательным параметр, который был необязательным 400 на прежнем запросе
Сузить допустимые значения входа, ужесточить проверку принимавшееся перестаёт приниматься
Сделать платным вызов, работавший в Community 403 с license_required там, где работало в Community
Потребовать место Pro там, где хватало лицензии установки 403 с license_required у учётной записи без места
Потребовать более старшую редакцию, чем требовалось 403 с license_required у установки с прежней лицензией

Как соблюдается обещание

Путь вызова, метод, код успешного ответа, требование к токену и принадлежность к редакции не меняются незаметно: выпуск, в котором вызов снят или одно из этих свойств изменено, не выходит. Новый вызов обещания не нарушает.

Обязательные поля тела ответа — тоже часть договора, и пропавшее поле — слом обещания. Заметив, что поле, на которое опирается ваш код, исчезло из ответа, сообщайте об этом как о дефекте, а не считайте побочным эффектом.

Что разрешено и сломом не считается

  • Новый путь; новый метод у существующего пути.

  • Новое необязательное поле в успешном ответе. Образец такого добавления уже есть: поле partial — оговорка «данные неполные», сегодня с единственным значением "truncated" (выдача обрезана пределом). Клиент, не знающий о поле, читает ответ как раньше; отсутствие поля означает полный ответ.

    Оговорка о неполноте бывает и НЕ конвертной. У файла в сравнении (files[].truncated) и у просмотра файла (truncated) она относится к ОДНОМУ объекту, а не к выдаче целиком: обрезан не список, а содержимое конкретного файла, перешагнувшего предел размера (сейчас — 100 КиБ на файл в сравнении и 10 МБ при просмотре файла). Признак поднят только по причине РАЗМЕРА и не смешивается с is_binary: крупный текстовый файл двоичным не называется. Третья причина пустого патча — содержимое не менялось (переименование, смена прав) — не поднимает ни одного из двух признаков.

  • Новый необязательный параметр запроса, если без него поведение прежнее.

  • Новое значение в перечислении — машинного кода отказа или причины частичности. Отсюда требование к клиенту: незнакомое значение обязано разбираться по коду ответа HTTP и не ронять обработку. Пополнение объявлено заранее: причин частичности станет больше.

  • Ослабление проверки входа, расширение допустимых значений.

  • Смена текста, порядка ключей, порядка неотсортированной выдачи.

  • Отказ на неизвестное поле в теле запроса. В 1.0.0 поле, которого вызов не знает, отбрасывалось молча: запрос создания репозитория с "visibility": "public" вместо "is_private": false возвращал 201 и создавал репозиторий приватным. Клиент получал успех и не тот результат, о котором просил. Ужесточением договора это не считается ровно по этой причине: запросы с неизвестными полями и раньше не работали так, как ожидал их автор, — теперь они об этом сообщают. Отказ — 400 с кодом validation; текст называет нераспознанные поля полным путём от корня тела.

    Правило действует в собственном интерфейсе /api/v1 — включая адреса, которые обслуживает мастер установки. Слои совместимости (/api/v3, /api/v4), SCIM, OIDC, Git LFS и протоколы реестров пакетов принимают незнакомые поля молча: форму запроса там задаёт внешняя спецификация.

    Вне правила и протокол исполнителя (/api/v1/runner/**): исполнитель новее сервера продолжает с ним работать.

Отказ разбора тела приходит общим конвертом

Нечитаемое или не той формы тело запроса /api/v1 отвечает 400 с конвертом {"error", "code": "validation"} — как и всякая другая непройденная проверка данных: сломанный JSON, нет обязательного поля, не тот тип значения, повторённый ключ. Если отказ связан с конкретным полем, текст его называет.

В 1.0.0 разобранное, но не той формы тело отвечало 422, и ни один из отказов разбора не нёс машинного кода. Сменой кода у рода отказа это не является: рода отказа «тело не разбирается» в договоре не было вовсе — не было и кода, по которому его можно опознать. Клиент, различавший этот случай по 422, опознаёт его теперь по коду validation вместе с остальными проверками входа.

Отдельный код остался у одного случая: тело, присланное без Content-Type: application/json, отвечает 415 — сервер до содержимого не дошёл. Конверт и код у него те же.

Отказы разбора ПАРАМЕТРОВ адреса и строки запроса приходят тем же конвертом с кодом validation и текстом, называющим параметр. Код ответа — 400, как и в 1.0.0; добавился машинный код.

Состав строки запроса строгим НЕ стал и не станет: незнакомый параметр пропускается молча. Ссылка, к которой по дороге приписали свои параметры (метки переходов, счётчики, возврат посредника), продолжает работать.

Ужесточения, сделанные намеренно

Таблица запретов называет «ужесточить проверку» сломом — и это правило. Исключения из него делаются поимённо и только там, где прежнее поведение ОТВЕЧАЛО УСПЕХОМ НА НЕВЫПОЛНЕННОЕ, то есть договор и так не соблюдался:

Вызов Было Стало Почему это не слом
PATCH /api/v1/users/{username} 200 и запись без изменений, если прислать is_admin или is_pro_seat без прав 403 с перечнем полей Поля и раньше не применялись; клиент считал действие выполненным
PUT /api/v1/repos/{owner}/{name}/environments/{env} имя среды из тела не применялось, бралось из адреса 400, если имя в теле не совпадает с адресом Запрос с другим именем правил СРЕДУ ИЗ АДРЕСА, а клиент считал, что создал вторую
POST/PATCH поставщика SAML пустой sp_entity_id принимался 400 Пустой идентификатор ломает вход, и обнаруживалось это при первой попытке войти, а не при сохранении
POST /api/v1/repos/{owner}/{name}/packages/retention, POST /api/v1/repos/{owner}/{name}/packages/tag-rules пустая маска и отрицательное «хранить последних»/«удалять старше» принимались 400 Правило с пустой маской не накрывает ни одного образа и ни одного тега, а отрицательное число трактуется как «ограничения нет». Запись создавалась, отвечала 201 и не делала ничего: клиент считал хранение настроенным
POST /api/v1/admin/k8s-runners пропуск runner_image принимался и давал gitriver-runner:latest 400 Короткое имя кластер ищет в Docker Hub, не находит и оставляет под в ImagePullBackOff. Настройка создавалась, отвечала 201 и не могла поднять исполнителя НИ РАЗУ: клиент считал автомасштабирование настроенным. Обновлению (PUT) поле по-прежнему необязательно — там пропуск значит «не менять», а переданный пустой адрес отвечает 400 по той же причине
POST/PUT /api/v1/admin/k8s-runners пустое или недопустимое для Kubernetes namespace и пустой match_label принимались 400 С пустым пространством имён контроллер обращался к /namespaces//jobs, с пустой меткой не находил ни одного задания. Настройка отвечала 201, а исполнитель не поднимался ни разу

Общее у всех: запрос, который перестал приниматься, и раньше не делал того, чего от него ждали. Ужесточение, у которого такого объяснения нет, по-прежнему запрещено.

Устаревание: как объявляется и что видит клиент

Устаревшая операция помечена в спецификации /api/openapi.json признаком deprecated: true и описанием, куда переходить. Страница /api/docs показывает пометку, а генератор клиента по спецификации переносит её в сгенерированный код.

Так помечены все PUT-псевдонимы частичного обновления: канонический метод — PATCH, а PUT на том же пути оставлен работающим ради уже написанных скриптов. Например:

PATCH /api/v1/users/{username}                                  канонический
PUT   /api/v1/users/{username}                                  устаревший
PATCH /api/v1/repos/{owner}/{name}/issues/{number}              канонический
PUT   /api/v1/repos/{owner}/{name}/issues/{number}              устаревший
PATCH /api/v1/repos/{owner}/{name}/pull_requests/{number}       канонический
PUT   /api/v1/repos/{owner}/{name}/pull_requests/{number}       устаревший

Так же помечен GET /api/v1/import/remote-repos — по другой причине. Список репозиториев внешней системы запрашивается с токеном доступа к ней, а токен в адресе оседает в журнале каждого посредника на пути: строку запроса целиком пишут обратный прокси, балансировщик и сервер доступа, и живут эти журналы дольше самого переноса. Канонический вызов — тот же путь методом POST, токен в теле:

POST  /api/v1/import/remote-repos                               канонический
GET   /api/v1/import/remote-repos?auth_token=…                  устаревший

Второй канал — заметки о выпуске и руководство пользователя.

Заголовков Deprecation (RFC 9745) и Sunset (RFC 8594) в ответе нет. Они предупреждают о сроке отключения, а внутри v1 устаревший вызов не отключается (см. ниже) — срока, о котором стоило бы предупреждать, нет.

Сколько живёт устаревший вызов

Внутри v1 — столько же, сколько сам v1. Пометка deprecated означает «есть канонический способ, новый код пишите им», а не отсчёт до отключения: снятие метода запрещено правилом выше, и на устаревшие псевдонимы оно распространяется наравне с остальными.

Снять что-либо можно только вместе со сменой номера договора — заведением /api/v2. При этом:

  1. v2 объявляется в заметках о выпуске и в этом документе, с перечнем отличий и порядком перехода;
  2. /api/v1 продолжает отвечать не менее двенадцати месяцев с даты выхода версии, в которой появился /api/v2;
  3. в течение этого срока v1 получает исправления безопасности по общему правилу security.md; новые возможности в него не добавляются;
  4. дата прекращения v1 называется в момент объявления v2, а не позже.

Установка, обновляющаяся раз в год, застаёт обе версии работающими хотя бы однажды. Этот срок длиннее срока поддержки предыдущей минорной версии (полгода).

Что живёт по чужим номерам версий

Обещание распространяется только на /api/v1. Ниже — то, чей договор пишем не мы; там мы обязуемся следовать чужой спецификации, а если она поменяется — пойти за ней.

Адрес Чей договор
/api/v3 интерфейс ГитХаба (покрытие)
/api/v4 интерфейс ГитЛаба (покрытие)
/scim/v2 RFC 7644 (внешнее управление учётными записями)
/v2 OCI Distribution Spec (реестр образов)
/oauth/*, /.well-known/* RFC 6749, RFC 8414, OpenID Connect
/{owner}/{repo} по git и LFS протоколы git и Git LFS

Слои совместимости живут по номерам чужих платформ: v3 и v4 в этих путях — их версии, не наши. Наше обещание на них не распространяется, а их собственное — «ответ совпадает с ответом чужой платформы»; что поддержано, а что нет, названо в документах покрытия.

Отдельный случай — нативные протоколы пакетов (npm, PyPI, Cargo, Maven, NuGet, Composer, Generic). Они лежат ПОД /api/v1, например GET /api/v1/packages/{owner}/{name}/cargo/index/config.json. Пути наши, и правило «путь не снимают» на них распространяется. А вот форму тела — и успешного ответа, и отказа — задаёт пакетный менеджер: она меняется вслед за его спецификацией, и общий конверт {"error", "code"} там не действует.

Разделение редакций в 1.1.0

Что изменилось. КОРПОРАТИВНЫЕ возможности — вход по SAML SSO и его настройки, внешнее управление учётными записями по SCIM, LDAP, журнал аудита, квоты, правила по IP-адресу, оформление, пределы черновиков знания — открывает теперь редакция Max. В 1.0.0 их открывала любая действующая лицензия Pro. Задетые вызовы:

Возможность Вызовы
Вход по SAML SSO /api/v1/admin/saml/providers и /api/v1/admin/saml/providers/{id} (все методы); все вызовы под /api/v1/auth/saml/
SCIM все вызовы под /api/v1/admin/scim/
LDAP /api/v1/admin/ldap (все методы), POST /api/v1/admin/ldap/test
Журнал аудита GET /api/v1/admin/audit-log
Квоты PUT /api/v1/admin/quotas, PUT /api/v1/admin/quotas/users/{id}, PUT /api/v1/admin/quotas/groups/{id} — чтение и удаление квот остались без ограничения редакцией
Правила по IP-адресу POST /api/v1/admin/ip_rules, PATCH и PUT /api/v1/admin/ip_rules/{id}
Оформление PUT /api/v1/admin/branding
Пределы черновиков знания PUT /api/v1/admin/knowledge/limits/users/{id}, PUT /api/v1/admin/knowledge/limits/groups/{id}

Настройки помощника остались в редакции Pro.

Кого это задевает. Установку с лицензией Pro, которая пользовалась чем-то из этого списка. Вызовы отвечают 403 с кодом license_required, пока редакция не изменена на Max. Всё остальное платное — проверки безопасности, лицензионная чистота, метрики DORA, указатель по коду, свои роли и переменные группы, помощник целиком вместе с его настройками — редакция Pro открывает по-прежнему, и для него не изменилось ничего.

Что это значит при покупке лицензии. Число купленных мест на доступность корпоративных возможностей не влияет: их открывает только редакция Max.

Почему это названо здесь. Правило «принадлежность к редакции не ужесточается» действует с 1.1.0, а это ужесточение сделано при переходе с 1.0.0 — до того, как правило вступило в силу. Правка, отнимающая у оплаченной установки возможность, всё равно называется поимённо, вместе с перечнем задетых вызовов и порядком перехода. Молча такие правки не делаются.

Порядок перехода. Владельцу установки с лицензией Pro, которому нужны корпоративные возможности, издатель выдаёт файл активации редакции Max с тем же идентификатором лицензии — срок при этом двигать не обязательно. Установка принимает такой файл и отвечает «Лицензия обновлена: редакция повышена с Pro до Max»; мест не удваивается, вторая строка в списке лицензий не появляется. Вводится он там же, где продление (лицензирование), и тем же путём доезжает автоматически с сервера лицензий.

Редакцию установка берёт из подписи, а не из того, что и когда куплено. Поэтому переоформление, сделанное на стороне издателя, доезжает само — очередной отметкой, без действий администратора, — и ручной ввод файла нужен там, где отметка не ходит (закрытый контур), либо когда переход нужен немедленно. Условия продажи — кому какая редакция полагается и с какого дня — здесь не описываются: установка исполняет то, что подписано.

Подтверждение права в 1.1.0

Что изменилось. Подписанный ответ активации годен теперь ограниченное время (lease_until), а не до конца срока лицензии. Пока подписи доходят — отметкой к серверу лицензий или файлом, вставленным вручную, — не меняется ничего. Когда подписи перестают доходить, платные вызовы начинают отвечать 403 с кодом license_required, хотя срок лицензии ещё идёт.

Почему это не слом договора. Ни один путь, метод, параметр и ни одно поле ответа не изменились и не исчезли; код и конверт отказа — прежние (license_required, 403), и клиент, который уже умеет их читать, читает их так же. Изменилось УСЛОВИЕ, при котором платная возможность открыта, а условия лицензирования договором /api/v1 не были никогда: он обещает форму вызовов, а не то, что лицензия у установки есть.

Кого это задевает. Клиентов, написанных в расчёте на то, что 403 license_required невозможен у установки с действующей лицензией. Такого обещания не было и раньше — лицензию можно снять из администрирования в любой момент, — но теперь появился второй способ в него упереться. Лечение то же: показать пользователю текст отказа, а не считать его сбоем.

Что добавилось в ответы. /api/v1/admin/license отдаёт поля lease_until, lease_days_left, lease_expired и db_moved у каждой лицензии и lease_warning, lease_expired, db_moved, db_fingerprint в сводке; в ленте активности появилось значение license_lease_expiring. И то и другое — добавления, разрешённые списком выше; клиент, который о них не знает, читает ответ как раньше.

Подробности — лицензирование.

Что меняется при переходе с 1.0.0 на 1.1.0

1.0.0 вышла до этого обещания, и переход на 1.1.0 — единственное место, где меняется то, что дальше объявлено неизменным. Поэтому здесь названо всё сразу: и то, что видит клиент интерфейса, и то, что видит владелец оплаченной установки. Дальше этот список не растёт.

Форма ответа стала страницей в трёх местах. Вместо массива верхнего уровня приходит {"items": [...], "next_cursor": ...}:

  • комментарии задачи, комментарии и обзоры запроса на слияние;
  • пакеты репозитория и версии одного пакета;
  • сводные списки квот — GET /api/v1/admin/quotas/users и GET /api/v1/admin/quotas/groups.

Подробности и порядок перехода — в руководстве пользователя.

Впредь так не делаем: форма существующего ответа не меняется, а постраничная выдача, если она понадобится ещё где-то, заводится отдельным вызовом.

Корпоративные возможности ушли в редакцию Max — разделение редакций выше: перечень задетых вызовов и порядок перехода там же.

Право подтверждается ограниченное время — подтверждение права: установка, до которой перестали доходить подписи, получает 403 license_required раньше конца срока лицензии.

Места считаются. Установка с лицензией НА ЧИСЛО МЕСТ обнаружит, что платные разделы открыты только тем участникам, у кого признак места проставлен, — лицензирование. Установок с лицензией без ограничения мест это не касается.

null в правке снимает значение. В частичной правке пропущенное поле не меняется, а null снимает значение необязательного поля: веху задачи, срок и описание вехи, описание метки, выпуска и репозитория, сертификат кластера, учётную запись службы в настройке исполнителей Kubernetes. В 1.0.0 null в этих полях не делал ничего. У PUT /api/v1/admin/k8s-runners/{id} пропуск service_account теперь сохраняет прежнее значение, как у остальных полей, — в 1.0.0 он её стирал; снять её — передать null или пустую строку.