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

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

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

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

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

Обещание действует с версии 1.1.0 (18.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 отвечает без токена, и редакция в его ответе объявляла бы состояние лицензии установки любому, кто открыл адрес узла. Сам 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.
  • Устройство непрозрачных строк. 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 — истёк предельный срок ответа; обрыв слишком большого тела там, где предел считает слой, а не обработчик (выгрузка файла составным телом). Тела у них может не быть вовсе, и опираться клиенту следует на код состояния. Договор здесь — сам код состояния, а не то, что лежит (или не лежит) в теле.

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

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

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

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

Как это проверяется

Четыре свойства из этого списка — путь, метод, код успеха, требование к токену — плюс шлюз редакций записаны снимком в дереве. Колонка шлюза называет ОБЕ его оси: редакцию и меру — без-шлюза, шлюз-pro (редакция Pro, местами не меряется), шлюз-pro-место (редакция Pro и место у вызывающего), шлюз-max (редакция Max). Снимок лежит в crates/gitriver-api/src/routes/manifest/api_v1_surface.txt, по строке на операцию. Правка, снимающая вызов или меняющая эти свойства, роняет сборку и называет, что именно снято или изменено. Добавление вызова обещания не нарушает, но снимок обязан оставаться полным — иначе завтрашний вызов не защищён ничем, поэтому сборка требует дописать в него новую строку.

Границы проверки называются прямо: состав полей тела ответа снимок не видит. Убранное поле — слом обещания, но ловится он ревизией, а не сборкой.

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

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

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

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

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

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

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

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

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

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

    По той же причине вне правила и протокол исполнителя (/api/v1/runner/**), хотя адреса у него наши: исполнитель обновляется отдельно от узла, и исполнитель новее узла получал бы отказ на каждое обновление состояния. Тело там составляет наш же код, опечатке в имени поля взяться неоткуда.

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

Нечитаемое или не той формы тело запроса /api/v1 отвечает 400 с конвертом {"error", "code": "validation"} — как и всякая другая непройденная проверка данных. Прежде код зависел от рода отказа: сломанный JSON давал 400, а разобранный, но не той формы (нет обязательного поля, не тот тип значения, повторённый ключ) — 422; и то и другое приходило с англоязычным текстом библиотеки (Failed to deserialize the JSON body into the target type: missing field ...), то есть без машинного кода. Клиент не мог ни выбрать по нему ветку, ни перевести сообщение.

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

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

Так же изменились отказы разбора ПАРАМЕТРОВ адреса и строки запроса: раньше нечитаемый идентификатор отвечал 400 с текстом библиотеки («Invalid URL: Cannot parse abc to a Uuid»), теперь — тем же конвертом с кодом validation и текстом, называющим параметр. Код прежний, добавился машинный код.

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

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

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

Вызов Было Стало Почему это не слом
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 Пустой идентификатор ломает вход, и обнаруживалось это при первой попытке войти, а не при сохранении

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

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

Устаревшая операция помечена в спецификации /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, журнал аудита, квоты, правила по адресу узла, оформление, пределы черновиков знания — открывает теперь редакция Max. В 1.0.0 их открывала любая действующая лицензия Pro. В снимке поверхности у этих вызовов колонка шлюза сменилась с шлюз-pro на шлюз-max.

Признак корпоративной возможности проверяемый: можно ли, купив одно место, получить её для всей компании. Настройки помощника под него не подходят — каждое обращение к модели меряется местом, — и остались в редакции Pro (колонка шлюз-pro, как и была).

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

Почему это сделано. Лицензия на ОДНО место включала корпоративный вход, провижининг и журнал аудита всей установке: компания на триста человек платила за одно место и получала SAML для всех. Место меряет работу участника, а вход по SAML происходит до того, как участник появляется, — минимум мест при покупке эту дыру не закрывает. Закрывает её только редакция.

Почему это названо здесь. Правило «принадлежность к редакции не ужесточается» действует с 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 /admin/quotas/users и /groups.

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

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

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

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

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