Навигация
Обещание совместимости /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. При этом:
v2объявляется в заметках о выпуске и в этом документе, с перечнем отличий и порядком перехода;/api/v1продолжает отвечать не менее двенадцати месяцев с даты выхода версии, в которой появился/api/v2;- в течение этого срока
v1получает исправления безопасности по общему правилу security.md; новые возможности в него не добавляются; - дата прекращения
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 или пустую строку.