Удаление артефактов и retention
Реализован законченный сценарий логического удаления неиспользуемого опубликованного артефакта: API, SDK, предварительный просмотр очистки, выборочная обработка и действие в карточке консоли. OpenAPI 0.11.0, 127 HTTP-операции; миграции 13 и 14. Этот документ описывает ручной сценарий. Автоматизация описана в STORAGE_POLICIES; фоновая физическая очистка — в ONLINE_CLEANUP.
Права и API
Новый managed permission artifact.delete выдаётся на точный репозиторий. Старые read/write, пользовательская группа, administrator и bootstrap-флаг не дают удаления автоматически; парольные сессии, персональные токены, файловые ключи и ключ восстановления это право не получают вовсе — только управляемый сервисный ключ. Bootstrap-оператор может выдать отдельный managed credential через уже существующий service API. Для полноценного просмотра карточки консоли ключу дополнительно нужны обычные права каталога и аннотаций. Право удаления само по себе разрешает только инспекцию жизненного цикла, без bytes, публикации и управления ключами.
| Метод | URL относительно /api/v1/repositories/{repository} | Назначение |
|---|---|---|
| GET/HEAD | /artifacts/{id}/deletion | Проверить опубликованный артефакт и блокирующие зависимости |
| DELETE | /artifacts/{id} | Удалить конкретный артефакт; JSON body {expectedAnnotationRevision} обязателен |
| POST | /retention/preview | Прочитать ограниченную страницу кандидатов, без изменений |
| POST | /retention/apply | Повторно проверить и обработать только перечисленные IDs |
GET /operations показывает новые действия только ключу с соответствующим правом. Reader разрешает GET/HEAD инспекции, остальные методы возвращают 405. Поэтому preview с JSON-фильтрами выполняется на writer, несмотря на отсутствие изменений. CORS уже поддерживает DELETE с JSON и preflight. В capabilities: artifactDeletion=true, retentionPreview=true, onlineGarbageCollection=true.
Все параметры проходят runtime validation. Неизвестные поля и query-параметры отклоняются. Размеры — десятичные строки. Для несуществующей инспекции — 404. DELETE/apply возвращают явно различимые результаты, поэтому HTTP 200 не означает, что каждый файл удалён.
Предварительный просмотр
{
"criteria": {
"publishedBefore": "2026-09-01T00:00:00.000Z",
"protectedLabels": ["bse", "release"]
},
"limit": 50
}Дата обязательна: канонический UTC с миллисекундами, не в будущем. Сравнение строгое publishedAt < publishedBefore; это время публикации, а не начала загрузки. Список защищённых меток обязателен (до 32, точное сравнение регистра). Пустой список явно отключает защиту меток для этой команды. Метки test, staging, release не имеют скрытых системных прав.
Ответ: {items, next}; элемент содержит id, name, size, publishedAt, annotationRevision, blockers. До 100 элементов, по умолчанию 50. next — exclusive UUID cursor; следующая страница должна сохранять те же критерии. Заблокированные кандидаты тоже видны, чтобы оператор понимал причину. Пагинация не создаёт снимок и не резервирует право удаления.
Блокирующие причины:
reference: артефакт закреплён внешним сервисом; снять свою ссылку может её владелец через reference API.asset_history: артефакт используется текущей либо любой исторической ревизией файла.attachment_history: артефакт используется любой сохранённой ревизией вложений.protected_label: текущие аннотации, либо исходные метки при отсутствии аннотаций, совпадают с protectedLabels.
Удаление ссылки из текущего набора вложений не уничтожает её историю и не освобождает исторический target. Удаление основного build также сохраняет его snapshots и пины. Опубликованный UPack binding сам по себе не блокирует удаление; его immutable identity сохраняется как tombstone.
Применение и повтор запроса
const repository = client.inRepository('releases');
const criteria = {
publishedBefore: '2026-09-01T00:00:00.000Z',
protectedLabels: ['bse', 'release'],
};
const page = await repository.retention.preview({ criteria, limit: 50 }, signal);
// Интерфейс показывает кандидатов; оператор выбирает конкретные строки.
const selectedIds = new Set(reviewedIds);
const items = page.items
.filter((entry) => selectedIds.has(entry.id) && entry.blockers.length === 0)
.map((entry) => ({ id: entry.id, expectedAnnotationRevision: entry.annotationRevision }));
if (items.length) {
const result = await repository.retention.apply({ criteria, items }, signal);
// Проверить outcome каждой строки, а не только HTTP status.
}Apply принимает 1–100 уникальных IDs с ожидаемой ревизией аннотаций, в одной короткой транзакции. Критерии, ссылки и актуальность ключа проверяются заново. Состав выборки не расширяется сервером. Результаты:
| outcome | Значение |
|---|---|
deleted | Метаданные удаления и аудит зафиксированы |
already_deleted | Этот immutable ID уже удалён; повтор не добавляет аудит |
protected | Появилась/сохранилась блокирующая зависимость или метка; причины в blockers |
changed | Изменилась ревизия аннотаций; нужен повторный просмотр |
not_eligible | Время публикации не соответствует критерию |
not_found | В данном репозитории нет такого опубликованного артефакта |
Непредвиденная SQL/аудит-ошибка откатывает весь batch, включая receipts; защищённые строки являются обычным результатом и не отменяют удаление остальных выбранных строк. Утрата HTTP-ответа допускает повтор той же команды: immutable ID и receipt предотвращают повторное действие. SDK не повторяет mutation скрыто. Истечение/отзыв ключа закрывает новую команду и проверяется повторно внутри транзакции.
Прямое repository.artifacts.delete(id, expectedAnnotationRevision, signal) не применяет retention-фильтр меток: оператор явно выбрал конкретный объект. Зависимости и CAS обязательны всегда, обхода защиты истории нет. Консоль объясняет это, показывает blockers и требует вставить ID. Обновление темы/языка сохраняет подтверждение, переключение объекта/credential сбрасывает его. Устаревшая ревизия требует новой проверки.
Байты, скачивания и восстановление
После commit артефакт исчезает из artifact/search/package listings. Exact download получает 404, latest выбирает оставшуюся опубликованную версию. Уже открытый поток продолжает читать сохранённые байты. Upload status старого ID становится cancelled; complete не может воскресить его. Package group/name/version остаются занятыми: повторная публикация той же версии с другими байтами запрещена.
HTTP удаление не освобождает резерв ёмкости. Online cleanup после настраиваемого grace period (по умолчанию 24 часа) удаляет байты и помечает резерв reclaimed без остановки сервисов. Активные читатели и writers откладывают конкретный объект. Политика включается отдельно; UI не обещает немедленного уменьшения занятого диска. Прежний npm run gc сохранён как offline repair tool.
Восстановление удалённого артефакта в API не реализовано. Grace period нужен для обслуживания, а не является контрактом корзины. Backup PostgreSQL и blob storage следует восстанавливать согласованно. Receipts, immutable package identities, audit и исторические пины сохраняются; автоматической компактации tombstones пока нет, они учитываются в пределе строк каталога.
Обновление и границы
Миграция 13 добавляет published_at, публикационный trigger и deletion receipts. Для старых published rows baseline времени равен времени миграции — старые данные не становятся кандидатами задним числом. Trigger фиксирует переход pending → available и сохраняет время при replay complete. Миграция 14 строит индексы ссылок assets через существующий resumable CREATE INDEX CONCURRENTLY. Readiness требует обе миграции.
Перед выдачей нового permission обновить writer, readers, workers и SDK: старые permission parsers не знают artifact.delete, старые package listings не фильтруют tombstones. Существующая инструкция обновления со stop/backup/migrate/start сохраняется. Откат на runtime без защиты удаления после начала использования этого API не поддерживается.
Сериализация коротких catalog mutations выполняется по репозиторию; транзакции не держат byte streams. Pin, annotation, asset, package и attachment записи повторно проверяют published state под тем же gate. Обоснование: ADR 0025.
Следующие самостоятельные этапы: архивирование/удаление asset paths и управляемая политика хранения истории, восстановление удалённых объектов, реестр репозиториев. Репликация и глобальная очередь не добавлены этим изменением; стенд двух серверов остаётся отложенным.
Автоматизация последних N и общие квоты реализованы отдельной revisioned storage policy: STORAGE_POLICIES. Ручной preview/apply сохраняет прежний контракт.