# Снапшот перед публикацией

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

Обзор хранилища и справочник эндпоинтов — [Хранилище исходного кода](/docs/source-storage).

## Гарантия наличия снапшота перед публикацией

`POST /v1/apps/:id/publish` проверяет, что у приложения есть сохранённый снапшот. Возраст снапшота на исход проверки не влияет — версия, сохранённая давно, подходит наравне с только что созданной. Отказ `409 SNAPSHOT_REQUIRED` приходит, когда пригодного снапшота нет: его не сохраняли либо файлы сохранённой версии удалены. В подсказке сказано, какой вызов нужно сделать перед повторной публикацией.

Проверка работает только если в портале включено сохранение исходников (по умолчанию включено, владелец портала может отключить — [Отключение для портала](/docs/source-storage/registry#отключение-для-портала)).

Пример отказа:

```json
{
  "success": false,
  "error": {
    "code": "SNAPSHOT_REQUIRED",
    "message": "Publish requires a saved source snapshot for this app. Call POST /v1/apps/:id/sources, or publish with sourceServerId set to the server you deployed to — a deploy auto-save is stored on that server, not on the app.",
    "hint": {
      "requiredAction": "POST /v1/apps/:id/sources",
      "reason": "app_snapshot_missing",
      "serverKeyedSources": {
        "requiredAction": "POST /v1/apps/:id/publish with sourceServerId (the server you deployed to)",
        "bodyField": "sourceServerId",
        "header": "x-source-server",
        "listEndpoint": "GET /v1/infra/servers/:serverId/sources",
        "registryEndpoint": "GET /v1/me/sources"
      },
      "toolName": "save_sources",
      "lastSnapshot": null
    }
  }
}
```

Блок `hint.lastSnapshot` приходит непустым, только когда платформа проверяет возраст снапшота. В нём `timestamp` — время создания версии, `presentedAt` — время, когда её последний раз предъявили сохранением, а `ageMinutes` считается от `presentedAt`: у дедуплицированного сохранения эти два значения расходятся. Поле `hint.freshnessWindowMinutes` приходит по тому же условию — это длина окна свежести в минутах.

Поле `hint.lastSnapshot` равно `null`, когда пригодного снапшота нет — его не сохраняли либо файлы сохранённой версии удалены. Пока возраст снапшота не проверяется, отказ приходит только по этой причине, поэтому поле в нём всегда `null`.

`hint.reason` называет причину отказа одним из двух значений: `app_snapshot_missing`, `server_snapshot_missing`. Ещё два значения — `app_snapshot_stale` и `server_snapshot_stale` — приходят, только когда платформа проверяет возраст снапшота. Блок `hint.serverKeyedSources` приходит только когда сервер в запросе не указан, — он объясняет, что делать, если исходники сохранил автосейв деплоя.

### Публикация исходников с сервера

Автосейв деплоя сохраняет исходники у того сервера, на который вы деплоили. Если сервер принадлежит не OAuth-ключу публикуемого приложения — например, деплой шёл под личным ключом `vibe_api_` — снапшот хранится у сервера (`appId` пустой), и проверка публикации, которая по умолчанию смотрит снапшоты приложения, его не находит. Деплой при этом честно отвечает `autoSaved: true`: байты сохранены.

Чтобы опубликовать именно их, назовите сервер:

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/apps/$APP_ID/publish" \
  -H "X-API-Key: $VIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sourceServerId": "'"$SERVER_ID"'"}'
```

Тот же смысл у заголовка `X-Source-Server: <serverId>`.

Что важно знать:

- **Сервер платформа не подбирает.** Идентификатор передаёте вы — это тот же `:id`, по которому вы вызывали деплой. Автоподбор «ближайшего» сервера опубликовал бы исходники, которых никто не называл.
- **Права на сервер проверяются отдельно от прав на приложение.** Публиковать исходники сервера может ключ-владелец сервера, личный ключ того же пользователя или администратор портала. Сервера нет, он из другого портала или удалён — `404 SERVER_NOT_FOUND`. Прав нет — `403 NOT_AUTHORIZED`.
- **Нумерация версий у сервера своя.** Вместе с `sourceServerId` поле `sourceVersionId` означает версию ЭТОГО сервера: `v3` сервера и `v3` приложения — разные версии.
- **Версию сервера публикация не помечает.** Своей версии приложения публикация ставит тег `published` и пишет отметку о публикации, а версии сервера — нет. У серверной версии те же поля хранят историю ДЕПЛОЯ (`GET /v1/infra/servers/:serverId/sources` их и показывает), а тег `published` — это бессрочное хранение и запрет на удаление. Публикация одного приложения не меняет ни то, ни другое у версии, которая ему не принадлежит. Что именно опубликовано, видно в журнале действий записью о публикации.
- **У опубликованной версии сервера нет бессрочного хранения.** Раз тега на ней нет, она подчиняется обычным правилам очистки: платформа всегда бережёт пять последних версий, плюс по одной версии на день за последние 14 дней и по одной на неделю за последние 4 недели. Новые деплои на тот же сервер вытесняют опубликованную версию из этих правил, и она может быть удалена, пока приложение остаётся опубликованным. Об этом же напоминает строка в `warnings` в ответе на публикацию. Чтобы сохранить версию навсегда, пометьте её сами — это ваше решение, потому что бессрочное хранение занимает место и запрещает удаление:

  ```bash
  curl -X PATCH "https://vibecode.bitrix24.tech/v1/infra/servers/$SERVER_ID/sources/v3" \
    -H "X-API-Key: $VIBE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"tags": ["manual"]}'
  ```

  Теги заменяются целиком: если у версии уже есть свои метки, перечислите их в том же массиве рядом с `manual`. Готовый список приходит в той самой строке `warnings`.
- **Без `sourceServerId` всё работает как раньше** — проверка ищет сохранённый снапшот приложения, ставит тег и отметку о публикации.
- **Заголовок проверяется так же, как поле, — но только когда поля нет.** Если `sourceServerId` пришёл в теле, он и решает: заголовок не читается, и что бы в нём ни лежало, на ответ это не влияет. Если поля в теле нет, заголовок проверяется строго: не в форме UUID или переданный дважды — `400 VALIDATION_ERROR`. Молча игнорировать его в этом случае нельзя, иначе публикация взяла бы снапшот приложения, пока вы думаете, что назвали сервер.
- **Хранилище исходников выключено для портала** — селектор не используется вовсе, публикация проходит, а в `warnings` приходит строка о том, что переданный `sourceServerId` не пригодился.

Где взять идентификатор сервера, если он не сохранился: `GET /v1/infra/servers/:serverId/sources` перечисляет версии конкретного сервера, а `GET /v1/me/sources` — сводный реестр всех источников, доступных ключу, по серверам и приложениям.

Параметры для повторной публикации той же версии (не самой свежей):

- `sourceVersionId` в теле — формат `v<N>`, например `"sourceVersionId": "v3"`.
- Заголовок `x-source-version: v3` — альтернатива телу.

## Коды ошибок

Отказы `POST /v1/apps/:id/publish`, специфичные для проверки снапшота:

| HTTP | Код | Когда возвращается |
|------|-----|---------------------|
| 409 | `SNAPSHOT_REQUIRED` | Сохранённого снапшота нет либо файлы сохранённой версии удалены (только при включённом сохранении). В ответе — поле `hint` с описанием действия. |

Полный справочник кодов — [Коды ошибок](/docs/errors).

## Смотрите также

- [Хранилище исходного кода](/docs/source-storage)
- [Автоматическое сохранение при деплое](/docs/source-storage/auto-save)
- [Сохранить снапшот](/docs/source-storage/save)
- [Срок жизни версий и очистка](/docs/source-storage/retention)
- [Опубликовать приложение](/docs/apps/publish)
