Files
progcode/editorial/agent-rewrites/130.json
T

8 lines
24 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 130,
"slug": "editorial-2024-05-field-platform-templates",
"title": "Платформенный шаблон без ловушки fork: как провести границу решения",
"excerpt": "Шаблон ускоряет повторяемый путь, но не заменяет архитектурное решение. Разбираем симптомы слишком широкой формы, проверяем контракт, безопасное расширение и корректный отказ до создания репозитория.",
"contentHtml": "<p>Новая команда просит создать сервис, а платформа предлагает одну форму: имя, владелец, runtime, репозиторий и несколько флагов. В первый день это похоже на хороший стандарт. Через месяц у сервиса появляется особый healthcheck, другой срок хранения данных, ручное исключение в CI и локальный скрипт для деплоя. Ещё через месяц команда просит добавить в форму «универсальный adapter», чтобы не делать отдельное решение.</p>\n<p>Так шаблон превращается в каталог скрытых архитектурных решений. Кнопка Create всё ещё выглядит простой, но за ней уже нет единого контракта: разные команды получают разные права, жизненные циклы и обязанности поддержки. Тезис статьи простой: автоматизировать стоит повторяемую задачу, а несовпадение нужно увидеть до генерации репозитория. Полное совпадение ведёт в golden path, одно явно ограниченное отличие — на review расширения, изменение инварианта — к отказу от шаблона.</p>\n<h2>Симптомы шаблона, который стал слишком широким</h2>\n<p>Первый симптом — просьба добавить поле без заранее определённого множества значений. «Пусть команда сама укажет runtime», «выберем хранилище позже», «разрешим любой внешний adapter» звучит как небольшая доработка формы. На деле каждое такое поле переносит решение из архитектурного обсуждения в момент генерации. Пользователь заполняет значение, но не получает ответа, кто отвечает за его безопасность, обновление и удаление.</p>\n<p>Второй симптом — patch сразу после создания проекта. Команда выключает обязательную проверку, переписывает pipeline или меняет видимость репозитория, а затем обещает вернуть полезную часть в общий шаблон. Если различие нельзя назвать, назначить ему владельца, очертить границу и описать обратный ход, это не расширение базового пути. Это самостоятельный проект, который временно маскируется под стандартный.</p>\n<p>Третий симптом — шаблон создаёт код раньше, чем зафиксированы класс данных и модель доступа. Skeleton уже содержит Dockerfile, workflow и manifest, поэтому команда начинает подгонять требования под готовую структуру. Платформа помогла быстро получить файлы, но незаметно стала источником политики. Удобная форма не должна принимать решение за владельца данных или службы безопасности.</p>\n<h2>Что именно делает шаблон, а чего он не делает</h2>\n<p>В Backstage Software Templates пользователь вводит параметры, после чего Scaffolder выполняет последовательность шагов. В документации среди таких шагов показаны загрузка skeleton, подстановка значений и публикация результата в GitHub или GitLab. Это механизм автоматизации, а не доказательство того, что любое сочетание параметров разрешено. Допустимые значения, обязательные поля и проверки должны быть частью контракта вашей платформы.</p>\n<p>GitHub template repository решает другую задачу: создаёт новый репозиторий с той же структурой, ветками и файлами. GitHub отдельно предупреждает, что ветки из шаблона имеют несвязанные истории; новый fork, напротив, сохраняет историю родительского репозитория. Поэтому template — удобный старт нового проекта, но не канал доставки обновлений во все уже созданные проекты. Ошибка начинается, когда копию называют fork-ом и обещают ей автоматическое наследование.</p>\n<table><caption>Решение до генерации: симптом, проверка и действие</caption><thead><tr><th>Наблюдение</th><th>Что проверяем</th><th>Вердикт</th><th>Следующий шаг</th></tr></thead><tbody><tr><td>Все входы входят в закрытый контракт</td><td>Тип сервиса, владелец, runtime, класс данных и видимость имеют допустимые значения</td><td>Golden path</td><td>Запустить стандартные шаги и записать версию контракта</td></tr><tr><td>Есть одно отличие от базы</td><td>У отличия есть идентификатор, owner, граница, срок действия и rollback</td><td>Review extension</td><td>Рассмотреть отдельную ветку; не добавлять свободное поле в форму</td></tr><tr><td>Меняется инвариант</td><td>Появляется новый класс данных, модель доступа или жизненный цикл</td><td>Decline template</td><td>Остановить генерацию и провести отдельное архитектурное решение</td></tr><tr><td>Просят синхронизировать копии с базой</td><td>Есть ли реальный канал поставки обновлений и владелец миграций</td><td>Не обещать наследование</td><td>Выбрать upstream-механику или зафиксировать независимое владение</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2024/platform-templates-2024-escape-hatch.svg\" alt=\"Схема развилки платформенного шаблона: совпадение ведёт в golden path, ограниченное расширение — на review, изменение инварианта — к остановке\"><figcaption>Развилка должна быть видна до генерации проекта: свободный параметр скрывает отличие, а именованное расширение оставляет его проверяемым.</figcaption></figure>\n<h2>Контракт: входные факты, инварианты и результат</h2>\n<p>Начните не с YAML, а с короткой заявки. Запишите тип компонента, владельца, runtime, класс данных, видимость репозитория, требования к доступу и срок хранения. Затем отделите инварианты от вариантов. Например, для внутреннего HTTP-сервиса инвариантами могут быть подтверждённый владелец, закрытый класс данных и один из поддерживаемых runtime. Название проекта и регион могут быть вариантами, если они не меняют модель риска.</p>\n<p>У контракта должны быть три явных результата. <strong>Golden path</strong> применяет стандарт без ручного решения. <strong>Расширение</strong> добавляет ровно одну заранее названную возможность и проходит review до создания артефакта. <strong>Отказ</strong> не означает «никогда»: он говорит, что текущая форма не является правильным местом для нового инварианта. Следующий вопрос должен вести к отдельному design path — с владельцем и проверками.</p>\n<p>Поле допустимо только тогда, когда его множество значений закрыто или его проверка определена отдельно. Удобное правило: если для значения нельзя сразу назвать owner, policy и способ отката, значение не должно попадать в golden path. Неизвестный runtime нельзя сделать безопасным значением с помощью default, а внешний data class нельзя превратить во внутренний одним текстом подсказки.</p>\n<h2>Учебный шаблон Backstage с закрытыми значениями</h2>\n<p>Ниже приведён минимальный учебный фрагмент для Backstage Scaffolder. Он показывает форму и порядок шагов, но не является готовой конфигурацией вашей инсталляции. В реальном проекте замените URL skeleton, организацию, группу владельца и action публикации на разрешённые вашей платформой значения.</p>\n<pre><code>apiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n name: internal-http-service\nspec:\n owner: group:platform\n type: service\n parameters:\n - title: Service contract\n required:\n - name\n - owner\n - runtime\n - dataClass\n - repoVisibility\n properties:\n name:\n title: Service name\n type: string\n pattern: '^[a-z0-9-]+$'\n owner:\n title: Owning group\n type: string\n runtime:\n title: Runtime\n type: string\n enum:\n - node20\n - go122\n dataClass:\n title: Data class\n type: string\n enum:\n - internal\n repoVisibility:\n title: Repository visibility\n type: string\n enum:\n - private\n - internal\n steps:\n - id: fetchBase\n name: Fetch approved skeleton\n action: fetch:template\n input:\n url: ./template\n values:\n name: ${{ parameters.name }}\n runtime: ${{ parameters.runtime }}\n - id: publish\n name: Publish repository\n action: publish:github\n input:\n repoUrl: 'github.com?owner=acme&amp;repo=${{ parameters.name }}'\n repoVisibility: ${{ parameters.repoVisibility }}</code></pre>\n<p>В этом примере enum ограничивает runtime, класс данных и видимость, а pattern отсекает часть случайных имён. Это полезная граница формы, но не полноценная авторизация. Строка owner всё ещё должна разрешаться по каталогу групп, action публикации требует настроенной интеграции и credentials, а закрытый класс данных не отменяет проверку содержимого skeleton. Backstage описывает owner шаблона как ответственную сущность, но прямо отделяет это поле от runtime-авторизации. Проверки прав и секретов остаются обязанностью конфигурации вашей платформы.</p>\n<h2>Воспроизводимая проверка разницы между template и fork</h2>\n<p>Проверить обещание «проект наследует шаблон» можно без спора о терминах. Создайте тестовый репозиторий из шаблона, клонируйте оба репозитория и сравните корневые коммиты. В команде ниже замените <code>acme/service-template</code> и <code>acme/orders-api</code> на доступные вам репозитории. Команда создания изменяет удалённый GitHub и поэтому предназначена только для тестового владельца, у которого есть право создавать репозитории.</p>\n<pre><code>gh repo create acme/orders-api --private --template acme/service-template\ngh repo clone acme/service-template /tmp/service-template\ngh repo clone acme/orders-api /tmp/orders-api\nprintf 'template roots: '\ngit -C /tmp/service-template rev-list --max-parents=0 HEAD\nprintf 'created roots: '\ngit -C /tmp/orders-api rev-list --max-parents=0 HEAD\ngit -C /tmp/orders-api log --oneline --decorate -5</code></pre>\n<p>Ожидаемый результат для GitHub template — новый репозиторий с собственной начальной историей. Это не измерение качества шаблона и не проверка того, что файлы совпадают навсегда. Для проверки содержимого зафиксируйте commit шаблона, сравните нужные файлы и отдельно решите, как доставлять будущие изменения: pull request из upstream, пакет, генератор миграций или ручное владение. Если такого канала нет, в документации проекта нужно прямо написать: после создания репозиторий самостоятельный.</p>\n<h2>Как оформить узкое расширение</h2>\n<p>Расширение — это не поле «custom». Сначала дайте ему имя, например <code>observability-adapter-v1</code>, и опишите, что оно добавляет и чего не меняет. Затем назначьте owner, перечислите затронутые файлы и разрешения, задайте условие включения, срок пересмотра и rollback. Владелец должен быть способен принять инцидент и удалить расширение, а не только согласовать его в каталоге.</p>\n<p>Проверка расширения должна включать положительный и отрицательный случаи. Положительный случай доказывает, что базовый сервис создаётся и получает adapter. Отрицательный — что произвольное значение или другой класс данных останавливают задачу до публикации. Если action уже создал репозиторий, а затем обнаружил несовместимость на deploy, граница стоит слишком поздно: потребуется cleanup, а часть риска уже прошла.</p>\n<p>Не включайте редкое расширение в общий шаблон только потому, что так короче обсуждение. Каждая новая ветка увеличивает число состояний, которые нужно тестировать и поддерживать. Если один и тот же запрос повторяется и сохраняет общий инвариант, соберите доказательства и включите его в следующую версию контракта. Если запрос одноразовый или меняет модель ответственности, оставьте его отдельным проектом.</p>\n<h2>Отрицательный путь: когда шаблон обязан остановиться</h2>\n<ol><li>Зафиксируйте заявку до создания файлов: тип компонента, owner, runtime, data class, видимость, access и retention.</li><li>Сверьте каждое поле с версией контракта. Неизвестное значение пометьте как несовместимое, а не как «временное».</li><li>Проверьте владельца по реальному каталогу групп и убедитесь, что он отвечает и за результат, и за последующие изменения.</li><li>Запустите dry-run или тестовый Scaffolder task, если такая возможность включена в вашей Backstage-инсталляции. Убедитесь, что отказ происходит до publish.</li><li>Для расширения проверьте owner, границу, permissions, rollback и срок пересмотра. Отсутствующий пункт — причина вернуть запрос на review.</li><li>После создания проверьте не только наличие файлов, но и visibility, branch protection, CI, секреты, healthcheck и наблюдаемость в вашей среде.</li><li>Запишите commit шаблона и версию контракта рядом с созданным проектом. Это позволяет понять, от какой исходной формы он стартовал, даже если история репозиториев несвязана.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Описанный подход не заменяет threat model, классификацию данных, согласование доступа и правила вашей организации. Backstage может проверять схему параметров и выполнять actions, но конкретный набор integrations, permissions, secrets и custom actions зависит от инсталляции. Учебный YAML не доказывает безопасность публикации и не должен переноситься в production без адаптации и review.</p>\n<p>Команда <code>gh repo create</code> требует установленного GitHub CLI, входа в нужный аккаунт и права создания репозитория; выполнение команды изменяет внешний GitHub. Сравнение корневых коммитов доказывает различие историй только для конкретной пары репозиториев. Оно не проверяет актуальность файлов, policy или pipeline. Для GitLab, другой forge или внутреннего шаблонизатора действуют аналогичные вопросы, но точные команды и семантика могут отличаться.</p>\n<h2>Критерий готовности решения</h2>\n<p>Шаблон готов к использованию, когда другая команда может взять ту же заявку и получить тот же вердикт по тем же фактам. Для golden path разрешены только значения закрытого контракта. Для extension есть owner, boundary, version, проверка отрицательного пути и rollback. Для отказа указана причина и следующий владелец отдельного решения. Ни один неизвестный параметр не создаёт репозиторий «на авось».</p>\n<p>Минимальный набор доказательств — четыре записи: стандартная заявка, разрешённое расширение, несовместимая заявка и повтор стандартной заявки. Первая и четвёртая дают один результат, вторая не меняет базовых инвариантов, третья останавливается до publish. После этого отдельно проверяются права, secrets, CI и ручные шаги окружения. Такой результат честнее, чем обещание, что одна форма поддерживает все будущие варианты.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://backstage.io/docs/features/software-templates/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Software Templates</a> — параметры, последовательность шагов, dry-run и поведение задач Scaffolder.</li><li><a href=\"https://backstage.io/docs/features/software-templates/writing-templates/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Writing Templates</a> — структура <code>spec.parameters</code>, <code>spec.steps</code>, actions, условия шагов и обработка ошибок.</li><li><a href=\"https://backstage.io/docs/features/software-catalog/descriptor-format/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Descriptor Format of Catalog Entities</a> — семантика <code>kind: Template</code> и owner сущности.</li><li><a href=\"https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Creating a repository from a template</a> — отличие template от fork и несвязанные истории.</li><li><a href=\"https://cli.github.com/manual/gh_repo_create\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub CLI: gh repo create</a> — флаг <code>--template</code> и требования команды создания репозитория.</li></ul>"
}