Перейти к содержанию

Окружения

Окружение — это именованная копия управляемой модели организации. Физически копия представляет собой отдельную схему PostgreSQL — не колонку-дискриминатор, не префикс, а настоящую схему, — поэтому любой существующий запрос репозитория корректен внутри окружения без единой переписанной строки, а строки одного окружения не могут попасть в чтение другого из-за забытого предиката (REQ-1487, REQ-1488). [tool-verified: environments.py module docstring; org_schema() at environments.py lines 86-96]

Каждая организация начинается с одного окружения по имени prod. Его нельзя удалить или переименовать. Запрос, не называющий окружения, обслуживается prod; запрос, называющий несуществующее окружение, отклоняется. [tool-verified: PROD = "prod" at environments.py line 44; select_environment() at env_routing.py lines 93-129]

Окружения доступны организациям на платном тарифе. [inferred: REQ-1507]

Имена окружений

Имя должно соответствовать [a-z][a-z0-9_]{1,31} — от двух до тридцати двух символов из строчных букв, цифр и подчёркиваний, начиная с буквы. prod и имена, начинающиеся с pg_, отклоняются. Максимальная длина для конкретной организации зависит от идентификатора самой организации: PostgreSQL молча обрезает идентификатор длиннее 63 байт, и самое длинное имя схемы, выводимое окружением, — это то, от чего защищает ограничение. [tool-verified: ENV_NAME_PATTERN at environments.py line 59; validate_env_name() at environments.py lines 119-142; max_env_name_length() at environments.py lines 108-116]

Что несёт копия

Каждая таблица в схеме организации попадает ровно в один класс (REQ-1489). Классификация — это разрешительный список, а не список исключений: добавленная позже таблица не путешествует, пока кто-нибудь не назовёт здесь её класс, так что режим отказа для забытой таблицы — красный тест. [tool-verified: CLASSIFIED constant and module docstring, env_classes.py lines 19-22]

Класс Таблицы Что происходит при копировании
CARRIED domains, naming_rules, registered_tables, table_columns, relationships, metrics, roles, rls_rules, tags, tag_param_values, tag_assignments, glossary terms, materialized_views, calendars, api_endpoints, tracked_functions, tracked_webhooks, table_meta_links Копируется целиком
IDENTITY_ONLY sources, api_sources, kafka_sources, kafka_sinks Поля идентичности и управления путешествуют; значения подключения остаются на месте (см. «Привязки»)
SEEDED_AT_CREATION roles, user_role_assignments Копируется только при первом создании окружения; последующие слияния их не трогают
PARTIAL org_settings Копируется по ключам: настройки управления путешествуют, ключи, называющие внешнюю цель или среду выполнения конкретного окружения, остаются на месте
NEVER_SENSITIVE org_secrets, user_directory Не копируется никогда
NEVER_RUNTIME mv_refresh_log, relationship_candidates, admin_audit_log и другие Не копируется никогда

[tool-verified: CARRIED, IDENTITY_ONLY, SEEDED_AT_CREATION, PARTIAL, NEVER_SENSITIVE, NEVER_RUNTIME frozensets, env_classes.py lines 29-113]

SEEDED_AT_CREATION существует, чтобы решить одну конкретную проблему. Новому окружению нужны роли и назначения, иначе оно откроется без единого, кто мог бы действовать. Но последующее слияние, принёсшее строку developer из prod, перезаписало бы ограниченную версию, которая может быть нужна ограниченной ветке, — и тогда путь ревью стал бы маршрутом эскалации привилегий. Поэтому роли и назначения путешествуют один раз, при создании, а дальше остаются собственным ответом каждого окружения. [tool-verified: env_classes.py lines 65-71; env_copy.py lines 41-44]

Привязки

Привязки — это колонки, которые говорят, куда источник на самом деле указывает: host, port, database, username и остальные. Они не путешествуют ни в одной копии. Окружение, которое ещё не привязано, помечается как unbound, а не оставляется пустым: пустой хост — это не отсутствующий хост, и построитель подключения прочитал бы его как localhost:5432. [tool-verified: BOUND_COLUMN = "bound" at env_classes.py line 143; BINDING_COLUMNS dict at env_classes.py lines 155-172]

Источники окружения разрешаются одним из двух способов.

База — окружение несёт собственные учётные данные. org_admin создаёт базу и затем привязывает каждый источник явно. [tool-verified: CreateEnvBody.inherit_connections = False (default) at environments_router.py line 227; "binding a base is an org_admin's act" comment at line 358]

Ветка — окружение наследует учётные данные базы по ссылке. Ничего не копируется. Когда запросу нужно подключение, разрешение поднимается по цепочке branched_from и останавливается на первом окружении, чья строка привязана. Ротация учётных данных на базе распространяется на каждую её ветку без каких-либо действий. Отзыв отзывает их у всех сразу. Ни один секрет нигде не материализуется так, чтобы ветка, экспорт или репозиторий могли его унести. [tool-verified: resolve() at env_bindings.py lines 114-151; lineage() at env_bindings.py lines 74-102; env_bindings.py module docstring lines 11-33]

Чтобы создать ветку, включите Inherit connections в панели Environments. По умолчанию выключено. [tool-verified: environmentsTab.json key inheritConnections; inheritHelp2 string]

Git-проекция

Каждая запись в модель фиксирует результат в git-ветке окружения. Репозиторий — проекция модели, но никогда не её авторитет: Provisa читает и пишет плоскость управления; репозиторий — это запись, а не источник. Развёртывание дерева требует явного вызова — слитый pull request на git-хосте сам себя не развёртывает (REQ-1524, REQ-1526). [tool-verified: deploy endpoint docstring at environments_router.py lines 777-791]

Каждая сущность получает один файл. Путь — это URI из REQ-1385 со снятыми схемой и организацией: provisa://acme/sales/tables/Order становится sales/tables/Order.yaml. Источники попадают в sources/, команды — в commands/, метрики — в metrics/. Дочерние строки, каскадирующие от родителя — колонки, связи, правила RLS, — пишутся внутри родительского файла, а не отдельными файлами. [tool-verified: table_path() at env_files.py line 109-115; kind_path() at env_files.py lines 118-120; COMMANDS_DIR = "commands" at env_project.py line 71; env_files.py module docstring lines 17-24]

Команды и назначения их тегов переживают полный круг. Тег на команде направляется в собственный файл команды (commands/<name>.yaml); тег, не принадлежащий никакому файлу, исчезает из проекции и был бы удалён при следующем развёртывании этого дерева. [tool-verified: env_project.py lines 346-364; owner_command_name routing in _assignments_for() at env_project.py lines 137-164]

Ни один суррогатный ключ не доходит до файла. registered_tables.id — автоинкрементное целое: одна и та же модель в двух окружениях получает разные целые, поэтому наивный дамп даёт различия сам с собой. Каждый суррогат отбрасывается, а каждая ссылка на него записывается как путь цели. [tool-verified: STORAGE_COLUMNS and _model_columns() at env_files.py lines 62-128; env_project.py docstring lines 26-27]

Сериализация детерминирована. Ключи выводятся по алфавиту, дочерние коллекции упорядочены по своему адресу, стиль YAML зафиксирован. Два окружения, содержащие одну и ту же модель, дают побайтово одинаковые деревья. [tool-verified: dump() at env_files.py lines 131-143]

Слияние

Слияние модели одного окружения в другое обновляет по идентичности: каждый объект, который есть у источника, создаётся или обновляется в цели. Объекты, которых у источника больше нет, удаляются только тогда, когда вызывающая сторона явно запросила удаления. Слияние, оборвавшееся на полпути, оставляет цель такой, какой она была, — одна транзакция. [tool-verified: copy_model() at env_copy.py lines 216-234; REQ-1490 description]

Перед применением вызовите эндпоинт предпросмотра (GET /{name}/merge-preview) или передайте dry_run: true. Предпросмотр проходит тем же путём по коду, что и слияние; это эндпоинт GET, поэтому CI-скрипт, перепутавший флаг, не может случайно применить слияние, которое собирался лишь осмотреть. [tool-verified: preview_merge() docstring at environments_router.py lines 1086-1095]

Слияние оставляет привязки, роли и секреты цели ровно такими, какими они были. Окружение разработки не теряет собственных подключений к базам данных, взяв более новую модель из prod. Prod не приобретает грантов dev. [tool-verified: env_copy.py lines 269-287; REQ-1490 scenario]

Что называет отчёт

Отчёт о слиянии перечисляет по путям, что было добавлено, изменено, удалено и оставлено без изменений. Он также называет любые конфликты — объекты, изменённые обеими сторонами с момента последнего общего коммита. Конфликт сообщается, но не разрешается: побеждает источник, а именно это и означает слияние в цель. Provisa не предлагает ни разрешения конфликтов, ни маркеров слияния, ни выбора по объектам. Ценность списка конфликтов — в сигнале: двое правили один объект, не зная об этом (REQ-1555). [tool-verified: CopyReport.conflicts at env_copy.py lines 151-165; detect_conflicts() called at env_copy.py lines 261-263; REQ-1555 description]

Объект, который обе стороны изменили к одному и тому же значению, — это согласие, а не конфликт. Когда два окружения вовсе не имеют общего предка, база в отчёте равна None, и пустой список конфликтов означает, что ничего не сравнивалось, а не что ничего не столкнулось. [tool-verified: CopyReport.compared property at env_copy.py lines 164-166; env_copy.py lines 255-264]

Слияние ложится одним сжатым коммитом в ветку цели. Сообщение коммита обязательно и не должно быть пустым — это единственный отчёт о том объёме работы, за который стоит этот squash. Коммиты источника остаются на месте и после этого по-прежнему развёртываемы по SHA. [tool-verified: _squash() docstring at environments_router.py lines 663-680; MergeBody.message comment at environments_router.py lines 258-260]

Pull

Pull берёт то, что удалённый репозиторий содержит для окружения, и делает это моделью. Он не перематывает локальную ветку напрямую; он применяет полученное дерево обычным путём развёртывания, поэтому те же валидация и аудит, что управляют ручным развёртыванием, управляют и pull. [tool-verified: pull_environment() docstring at environments_router.py lines 1450-1462]

Как и слияние, pull сообщает, что он перезаписал, — объекты, изменённые входящим деревом, которые локальное окружение тоже изменило с момента последнего общего коммита двух линий. Незакоммиченное локальное изменение — это дрейфующее окружение (см. «История» ниже); pull называет его в отчёте обычным изменением. [tool-verified: REQ-1556 description; pull_environment() at environments_router.py lines 1485-1519]

Pull отклоняется, когда две линии разошлись — у каждой есть коммиты, которых нет у другой. Отказ несёт список объектов, затронутых обеими сторонами, чтобы тот, кому теперь предстоит решить, чья работа выживет, знал, на какие объекты смотреть. [tool-verified: state["diverged"] check at environments_router.py lines 1491-1503; _collisions() at environments_router.py lines 1581-1602]

История

Каждое развёртывание двигает курсор окружения вперёд по его собственной линии коммитов. Undo делает шаг назад на один коммит; redo снова шагает вперёд к позиции, откуда undo ушёл. Ни одна из операций не удаляет коммит — шаг назад добавляет позицию, а не переписывает историю. [tool-verified: _move() docstring at environments_router.py lines 854-868]

Ветка засевается на вершине окружения, из которого она создана, поэтому undo останавливается в этой точке засева и не уходит на коммиты родительского окружения. [tool-verified: origin_sha comment at environments_router.py lines 428-448; _move() at environments_router.py lines 907-916]

Флаги can_undo и can_redo едут вместе с ответом на список окружений. Оба сообщают false, когда проекция не содержит коммита, который называет плоскость управления, — состояние, допускаемое дизайном и называемое дрейфующим. Узел, чьё хранилище репозитория так и не получило конкретный коммит, всё равно перечисляет свои окружения; меняются только ответы об истории (REQ-1561). [tool-verified: _with_history() at environments_router.py lines 316-344; REQ-1561 description]

Авторизация

Окружениями управляют два права. Ни одно из них по умолчанию не принадлежит аналитику (REQ-1573). [tool-verified: REQ-1573 description; MANAGE_CAPABILITY = "environment_management" and SWITCH_CAPABILITY = "environment_switch" at environments_router.py line 110 and env_routing.py line 53]

Право Кто им обладает (при засеве) Чем оно управляет
environment_management org_admin, developer Создание и удаление окружений
environment_switch org_admin, developer Обслуживание любым окружением, кроме prod

prod не требует права — им обслуживается запрос, не называющий ничего, и отказ в нём означал бы отказ каждому запросу.

Применение происходит в точке выбора, до того как будет достигнут любой маршрут. Участнику, у которого нет environment_switch, отказывают сразу на всех поверхностях — HTTP, GraphQL, SQL и проводных протоколах, — потому что окружение привязывается в middleware, а не в отдельных обработчиках. [tool-verified: select_environment() at env_routing.py lines 93-129; env_routing.py module docstring lines 28-34]

Аналитик, не имеющий никакого права на окружения, может запрашивать prod и не видит переключателя окружений. Подрядчик, которому выдана роль аналитика, не видит поверхности окружений и не может создать окружение или переключиться в какое-либо, кроме production. [tool-verified: REQ-1573 use_case and scenario]

Полномочия владельца окружения

Создание окружения — единственный путь, которым участник с доступом только на чтение приобретает права на редактирование модели (REQ-1528). Внутри созданного им окружения создатель обладает возможностями роли developer — минус права на данные (write, full_results, usage). Права на построение модели, а не права на данные. [tool-verified: ENVIRONMENT_OWNER_CAPABILITIES at env_authority.py lines 75-77; _DATA_RIGHTS at env_authority.py lines 74-77; env_authority.py module docstring lines 14-38]

Этот грант выводится из environments.created_by во время авторизации и никогда не пишется в таблицу грантов. Удаление окружения снимает его тем же действием. [tool-verified: env_authority.py module docstring lines 39-42; environment_owner() at env_authority.py lines 84-98]

Членство в домене по-прежнему ограничивает то, что владелец может менять. Ветвление меняет, что участник может делать; оно никогда не меняет, к каким доменам он может это делать (REQ-1530). [tool-verified: domains_within() at env_authority.py lines 121-145]

Защищённые окружения (REQ-1504)

Окружение можно сделать защищённым. Слияние или развёртывание в защищённое окружение не применяется по запросу; оно предлагается, и кто-то, кроме запросившего, должен его одобрить.

prod становится защищённым автоматически, как только в организации больше одного участника. Организация из одного участника не может удовлетворить условию «кто-то, кроме запросившего», поэтому там правило не применяется — иначе prod стал бы неслиянным. Любое окружение может быть помечено защищённым администратором организации. [tool-verified: is_protected() at env_approvals.py lines 79-96; protectedHelp2 UI string in environmentsTab.json line 28]

Запрос на слияние — это строка, а не диалог подтверждения. Одобряющий по определению другой человек, нежели запросивший, и в момент запроса не присутствует; эфемерное подтверждение вынудило бы одобрять внутри сессии запросившего, а это ровно то устройство, которое требование запрещает. [tool-verified: env_approvals.py module docstring lines 11-17]

Строка запроса несёт отчёт о слиянии рядом с сообщением запросившего. Устаревание выводится во время чтения и никогда не хранится: перепланирование во время чтения и сравнение с сохранённым отчётом — единственный вариант, который не может ошибиться. Устаревший запрос нужно запросить заново. Запросивший не может одобрить собственный запрос. [tool-verified: STALE constant and effective_state() at env_approvals.py lines 53, 215-243; decide() lines 265-268]

Состояния жизненного цикла запроса: requestedapproved/rejectedapplied. stale выводится. [tool-verified: REQUESTED, APPROVED, REJECTED, APPLIED, STALE at env_approvals.py lines 47-53]

Та же дверь обслуживает развёртывания из ref репозитория: запрос закрепляет SHA в момент предложения. Если ref сдвинулся между предложением и решением, одобряющий читает отчёт по закреплённому коммиту, а не по новому. [tool-verified: request_deploy() at env_approvals.py lines 150-189; env_approvals.py docstring lines 26-27]

Note

UI запросов на слияние находится во вкладке Merge requests панели Environments. Колонка Report показывает, что изменилось бы, по количеству; строка разворачивается, показывая подробности по объектам. [tool-verified: environmentsTab.json keys requestsTitle, colReport, approve, reject]

Команды CLI env

provisa env deploy отправляет модель по ref в окружение. Она завершается с кодом 0, когда развёртывание применено или было пробным, и с кодом 2, когда окружение защищено и развёртывание было только предложено: конвейер, принявший ожидающее одобрение за выпущенное развёртывание, был бы неправ, и код выхода об этом говорит. [tool-verified: _cmd_env_deploy() at cli.py lines 389-411]

provisa env deploy --org acme --env prod --ref main --token <token> --api <url>

provisa env fetch приносит удалённые ветки организации в локальный репозиторий. После этого развёртывание может назвать origin/<branch>. [tool-verified: _cmd_env_fetch() at cli.py lines 414-426]

provisa env fetch --org acme --api <url> --token <token>

Обе команды принимают --api (URL API Provisa) и --token (bearer-токен). Задайте PROVISA_API_URL и PROVISA_API_TOKEN в окружении, чтобы не передавать их при каждом вызове. [inferred: shared _api_call() helper]

Типичный CI-конвейер для рабочего процесса с репозиторием:

provisa env fetch --org acme --api "$PROVISA_API_URL" --token "$PROVISA_API_TOKEN"
provisa env deploy --org acme --env prod --ref "origin/main" \
  --message "release: $GIT_COMMIT_MSG" \
  --api "$PROVISA_API_URL" --token "$PROVISA_API_TOKEN"

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

  • Развёртывание — как поднять плоскость управления, к которой подключаются окружения
  • Команды — отслеживаемые функции и вебхуки, появляющиеся в дереве каждого окружения