Files
progcode/editorial/agent-rewrites/131.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

2 lines
18 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":131,"slug":"editorial-2024-05-mechanism-platform-templates","title":"Платформенный шаблон как контракт: базовый путь, расширение и отказ","excerpt":"Шаблон ускоряет повторяемый старт, если ограничивает вход, называет владельца и умеет остановиться. Разбираем контракт параметров, extension point, отрицательный путь и границы repository templates.","contentHtml":"<p>Новая команда просит создать сервис. Форма платформы принимает имя, владельца, runtime и несколько флагов. Через месяц в созданном репозитории появляются ручные исключения в CI, другой healthcheck и особая политика хранения. Команда называет это вариантом шаблона, хотя базовый путь уже не описывает результат. Симптом виден в первом локальном patch после генерации.</p><p>Цена ошибки складывается из трёх частей. Reviewer восстанавливает исходное решение по разрозненным изменениям. Platform team поддерживает комбинации, для которых никто не назначил владельца. Команда-потребитель ждёт обновлений от шаблона, но получает самостоятельную копию. Чем больше полей добавляет форма, тем дороже становится неизвестность.</p><p>Тезис статьи простой: платформенный шаблон должен принимать закрытый класс повторяемых задач. Совпадение с базовым контрактом ведёт в golden path. Одно заранее названное отличие ведёт на review расширения. Изменение инварианта или неизвестная политика должны остановить шаблон. Право сказать нет — часть механизма, а не неудобная ошибка интерфейса.</p><h2>Механизм: вход, решение, результат</h2><p>Разделите шаблон на три слоя. Первый слой принимает факты: тип компонента, owner, runtime, класс данных и требования к доставке. Второй слой проверяет контракт: допустимы ли значения, совпадают ли они с версией base path, есть ли у отличия имя и владелец. Третий слой выдаёт один из трёх результатов: golden path, review extension или decline.</p><p>Такой порядок не делает архитектуру автоматической. Он не выбирает policy за доменную команду. Он не доказывает, что generated repository можно отправить в production. Он лишь не даёт форме превратить незакрытый вопрос в якобы безопасный default.</p><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Просят универсальный флаг</td><td>Поле меняет runtime, owner, data class, доступ или retention</td><td>Сравнить поле с базовым инвариантом и назвать owner решения</td><td>Убрать поле из golden path; вынести запрос на design path</td></tr><tr><td>Сразу после создания нужен ручной patch</td><td>Различие не оформлено как versioned extension</td><td>Проверить identifier, owner, boundary и rollback</td><td>Остановить расширение; отправить его на review</td></tr><tr><td>Шаблон используют для одноразовой миграции</td><td>Нет повторяемого типа компонента и понятного lifecycle</td><td>Проверить, повторится ли тот же контракт для другой заявки</td><td>Отказать шаблону и провести отдельное решение</td></tr><tr><td>Копию считают fork-ом</td><td>Смешаны копирование файлов и наследование изменений</td><td>Проверить историю и реальный канал обновлений</td><td>Зафиксировать самостоятельное владение или выбрать другой механизм</td></tr></tbody></table><figure><img src=\"/assets/editorial/2024/platform-templates-2024-adoption-loop.svg\" alt=\"Развилка платформенного шаблона: базовый контракт, ограниченное расширение или отказ до отдельного решения\"><figcaption>Схема выбора: вход сначала сравнивается с контрактом. Иллюстрация объясняет логику маршрута и не показывает метрики adoption, состояние CI или production-результат.</figcaption></figure><h2>Закрытый контракт параметров</h2><p>У формы должен быть конечный список допустимых значений. В учебном примере базовый путь создаёт только внутренний HTTP-сервис. Для него известны owner, runtime и класс данных. Расширение добавляет observability adapter, но не меняет runtime, owner или data class. Любое лишнее поле шаблон отклоняет до создания репозитория.</p><pre><code># Учебный пример, не production-конфигурация: apiVersion: scaffolder.backstage.io/v1beta3; kind: Template; metadata.name: internal-http-service; spec.parameters.required: [name, owner, runtime, dataClass]; spec.parameters.properties.runtime.enum: [node20, go122]; spec.parameters.properties.dataClass.enum: [internal]; spec.parameters.properties.extension.enum: [none, observability-adapter]; spec.steps: [fetch:template, publish:github]</code></pre><p>Фрагмент только показывает границу входа. Enum ограничивает форму, но не заменяет проверку. Значение owner должно ссылаться на существующую группу. Action публикации требует прав и credentials. Нужны отдельные проверки secrets, branch protection, pipeline, healthcheck и rollback. Учебный пример не подтверждает пригодность runtime и не создаёт policy организации.</p><p>Главное правило — не передавать через форму то, что меняет смысл базового сервиса. Поле <code>runtime</code> с двумя разрешёнными значениями может быть частью закрытого контракта. Поле <code>customRuntimeConfig</code> открывает неограниченное пространство решений. Поле <code>retentionPolicy</code> нельзя считать безобидным расширением, если оно меняет требования к данным и доступу.</p><h2>Base path и extension point</h2><p>Base path фиксирует повторяемую часть: компонент известного типа, назначенного владельца, одобренный runtime и заранее определённый класс данных. Он может создать skeleton, metadata и список обязательных проверок. Он не должен принимать вопрос «какой runtime выбрать позже». Отложенное решение не становится безопасным оттого, что его записали в параметр.</p><p>Extension point нужен для узкого отличия, которое не ломает базовые инварианты. У расширения должны быть identifier, owner, версия, граница изменений и условие удаления. Observability adapter подходит как учебный пример, если он добавляет один известный слой наблюдаемости. Новый класс данных, внешний доступ или другая модель retention уже меняют тип решения. Их нельзя спрятать за словом extension.</p><p>Проверяйте расширение до генерации. Сначала найдите canonical record для типа заявки. Затем сравните с ним все входы. Если запись не найдена, результатом должен быть decline. Если совпал base contract и добавлено одно разрешённое отличие, создайте review record. Только после review запускайте action, который публикует файлы.</p><h2>Почему repository template не равен fork</h2><p>GitHub repository template создаёт новый репозиторий с той же структурой и файлами. GitHub отдельно указывает, что ветви такого репозитория имеют несвязанные истории. Это полезный старт, но не канал автоматической синхронизации с шаблоном. Команда получает начальный набор файлов и дальше принимает собственный жизненный цикл, если другой договор не определён отдельно.</p><p>Fork решает другой вопрос. Он сохраняет историю родительского репозитория и подходит для совместной работы с исходным проектом. Нельзя обещать потребителю updates от template только потому, что интерфейс запуска называется похожим образом. Перед внедрением зафиксируйте, что именно распространяется: копия файлов, pull request-ы, пакет, action или самостоятельная версия. У механизма должен быть владелец доставки изменений.</p><p>Backstage Software Templates тоже не заменяют этот договор. Scaffolder принимает параметры, выполняет шаги, подставляет переменные и может публиковать результат в GitHub или GitLab. Эти возможности описывают способ создания компонента. Они не доказывают, что любой action разрешён, что вход соответствует policy или что созданные репозитории будут синхронизироваться. Контракт команды остаётся отдельным слоем.</p><h2>Порядок действий</h2><ol><li><strong>Опишите класс задачи.</strong> Запишите component type, owner, runtime, data class, access boundary и срок жизни. Если ответ неизвестен, не маскируйте пробел новым флагом.</li><li><strong>Зафиксируйте base contract.</strong> Назовите обязательные входы, допустимые значения, результат и список non-goals. Версия контракта меняется вместе с этим договором.</li><li><strong>Закройте схему.</strong> Отклоняйте неизвестные keys и неполные комбинации. Не принимайте raw policy, credential, произвольный path или неподтверждённую метрику как вход.</li><li><strong>Разведите результаты.</strong> Полное совпадение даёт golden path. Одно разрешённое отличие даёт review extension. Несовместимое или неизвестное значение даёт decline.</li><li><strong>Проверьте отрицательный путь.</strong> Подайте неизвестный runtime, внешний data class, пустой owner и неподдерживаемое extension. Каждый случай должен остановиться до создания репозитория.</li><li><strong>Проведите review расширения.</strong> Проверьте owner, boundary, права, version, rollback и условие удаления. Если хотя бы одного поля нет, верните запрос на design path.</li><li><strong>Запустите публикацию только после проверки.</strong> В рабочей среде отдельно подтвердите credentials, результат pipeline, healthcheck, наблюдаемость и откат. Учебный пример выше этого не делает.</li><li><strong>Пересмотрите решение.</strong> Если расширение повторяется и сохраняет инварианты, включите его в новую версию контракта. Одноразовый случай не превращайте в обязательную опцию.</li></ol><h2>Ограничения и отрицательный путь</h2><p>Шаблон не выбирает owner за команду, не проводит threat modeling, legal review или capacity planning. Он не устанавливает SLO и не определяет правила хранения данных. Backstage и GitHub документируют свои механизмы, но не знают сетевые права и требования конкретной организации. Эти проверки нельзя заменить несколькими полями формы.</p><p>Отказ должен возвращать причину и следующий вопрос. Например: «decline-template: нет стабильного component type; назначьте owner и определите data class». Артефакт не создаётся. Команда получает design record и может вернуться к шаблону после решения. Такой отказ дешевле fork-а, который выглядит стандартным только до первой ручной переделки.</p><p>Rollback должен быть коротким. Для не прошедшего review удаляется draft contract или extension proposal, а не чужой repository. Для ошибочной версии возвращаются к предыдущему base contract. Если шаблон уже публикует внешние ресурсы, отдельно документируйте компенсационные действия; форма сама по себе не делает их обратимыми.</p><h2>Проверяемый критерий готовности</h2><p>Механизм готов, если другая команда получает тот же вердикт по тем же фактам. Допустимая заявка проходит базовый путь. Узкое расширение содержит identifier, owner, boundary, version и rollback. Несовместимая заявка останавливается до публикации и возвращает понятную причину. Повторный запуск не создаёт дублирующие обязательные действия.</p><p>Проверка состоит из четырёх записей: базовая заявка, разрешённое расширение, несовместимая заявка и повтор базовой заявки. Сравните решения, созданные артефакты и отрицательные ветки. Готовность есть, если инварианты base path не меняются, extension нельзя активировать без review, decline не создаёт repository, а повторный запуск имеет явно определённое поведение. Это проверяемый контракт, а не обещание универсальной платформы.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://backstage.io/docs/features/software-templates/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Software Templates</a> — параметры, шаги, выполнение задач и публикация результата.</li><li><a href=\"https://backstage.io/docs/features/software-templates/writing-templates/\" target=\"_blank\" rel=\"noopener noreferrer\">Backstage: Writing Templates</a> — официальная схема template и входных переменных.</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> — структура, файлы и несвязанные истории созданных ветвей.</li></ul>"}