0047. Продвижение артефактов: стадии и перенос между репозиториями
Статус: accepted. Дата: 2026-09-30.
Контекст
Сборка проходит путь от CI до production: её проверяют, одобряют и выкатывают. Агенту развёртывания нужен вопрос «какая версия сейчас в prod», а не идентификатор файла. Свободные метки (labels) для этого не подходят: их меняет любой с правом annotation.write, у них нет истории и автора, а удаление не фиксируется. Владелец продукта выбрал две совместимые модели: артефакт может переходить между репозиториями, выделенными под этапы (например dev → staging → prod), или получать promote-метки (стадии); у одного артефакта может быть несколько стадий.
Решение
- Стадия — управляемая метка артефакта:
^[a-z0-9][a-z0-9_.-]{0,31}$, не больше 16 на артефакт. Стадии хранятся отдельно от свободных меток (arkvory_artifact_stages) и меняются только через promotion API с правомartifact.promote(для file keys и групп —read+write). Каждое добавление и снятие пишется в журналarkvory_promotion_eventsс автором, временем и комментарием. Операции идемпотентны: повторное добавление существующей стадии не меняет время и не пишет событие. - Перенос в репозиторий —
POST R/artifacts/{id}/promoteс{target, mode, stages, comment}. Режимcopyоставляет исходный артефакт,moveлогически удаляет его в той же транзакции, что и публикация копии. Байты не передаются заново: локальное хранилище создаёт жёсткую ссылку, опубликованный blob неизменяем. Копия получает метки, metadata и коллекции источника, идентичность UPack и указанные стадии. Нужныartifact.readиcontent.readв источнике,artifact.promoteв цели и, дляmove,artifact.promoteв источнике. - Повторный promote того же источника в тот же репозиторий возвращает уже опубликованную копию. Если в цели уже есть пакет с той же group/name/version и другим SHA-256 — 409; с тем же SHA-256 возвращается существующий артефакт. Прерванный promote оставляет pending-резерв, который обычная очистка удаляет после истечения.
moveотклоняется, если у источника есть блокеры удаления (внешние ссылки, история файлов, вложения). Стадии источника переходят в копию.- Артефакт со стадией защищён от retention: блокер
promotion_stage. - Разрешение версии —
GET R/packages/resolveиGET|HEAD R/packages/contentпринимаютname,group, ровно одно изversion(точная) илиrange(SemVer-диапазон:1.2.3,^1.2,~1.2.3,1.x,>=1.0.0 <2.0.0,||), необязательнуюstageиprerelease=true. Безversionprerelease-версии исключаются, пока их не разрешили явно. Приstageиorder=promotedвыбирается последняя продвинутая версия (откат = повторный promote старой версии), иначе — старшая SemVer. GET R/promotions— постраничный журнал продвижений репозитория для CI/CD;GET R/artifacts/{id}/promotions— история артефакта.
Альтернативы
- Только репозитории-этапы: требует копии на каждый этап и не позволяет отметить одну сборку сразу как
qaиbeta. - Только стадии: не подходит для раздельных прав и квот по окружениям.
- Свободные метки с аудитом: смешивает управляемое состояние с произвольными тегами и ломает существующую семантику
annotation.write. - Физическое копирование байтов: удваивает место и время; жёсткая ссылка сохраняет неизменяемость и независимость жизненного цикла каждой копии.
Последствия
- Логическая квота целевого репозитория учитывает копию; физически байты делят один inode до удаления последней ссылки. Глобальная
ARKVORY_CAPACITY_BYTESсчитает каждую копию, это консервативная оценка. - Промежуточное состояние после сбоя — pending-строка и, возможно, жёсткая ссылка; их удаляет обычная очистка.
- Новое действие
artifact.promoteдобавлено в managed-ключи и в отображение coarse-прав (read+write).
Проверка
- Unit: имена стадий, запросы promote, разбор и сопоставление SemVer-диапазонов, отбор prerelease.
- PostgreSQL/HTTP: стадии с историей и идемпотентностью; copy и move между репозиториями с правами, конфликтом идентичности, повтором и блокерами; resolve по version/range/stage/order; retention-блокер; байты копии совпадают, удаление источника не затрагивает копию.
- SDK/CLI/браузер: те же сценарии через публичные клиенты.