revise July 2024 release engineering articles
Build and deploy / deploy (push) Successful in 15s

This commit is contained in:
2026-07-31 16:04:34 +03:00
parent 48049ee0ca
commit 78dfdb6015
7 changed files with 952 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 232 из 358 созданных материалов. Остальные 126 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 235 из 358 созданных материалов. Остальные 123 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
+46
View File
@@ -0,0 +1,46 @@
# P77 · 2024-07 · Инженерия релиза — три самостоятельных прохода ревью
## Рамка sidecar-пакета
- Archive slugs: <code>editorial-2024-07-practice-release-engineering</code>, <code>editorial-2024-07-mechanism-release-engineering</code>, <code>editorial-2024-07-field-release-engineering</code>.
- Голос: M7, июль 2024. Каждая статья начинает с конкретной версии, artifact, migration plan и rollout card, называет цену расхождения и ведёт по короткой связке «симптом → причина → проверка → действие». Текст не объявляет fixed fixture реальной поставкой, registry, pipeline, CI run, deploy, облаком, журналом доставки или production result.
- Созданы ровно пять sidecar-файлов: этот review, один import-safe script и три локальных SVG. Overlay, README, очередь, <code>articles.json</code>, Git, staging, commit/push и чужие файлы не менялись. Пакет намеренно не интегрирован.
- Fixture хранит только versioned fixed synthetic JS objects в памяти Node: commit, artifact, migration plan, rollout card и rollback candidate. У него нет входа для URL, файлов, environment, clock, реального digest, credential, CI log, deploy target или сети. PASS означает только согласованность embedded teaching records и негативных веток.
## Проход 1 — источники, техника и модель
- Источники сверены 31.07.2026 и намеренно ограничены первичными официальными материалами, доступными к июлю 2024. [SLSA: SLSA v1.0 is now final!](https://slsa.dev/blog/2023/04/slsa-v1-final) датирован 19.04.2023 и фиксирует первый stable SLSA v1.0. В пакет взята узкая дисциплина: artifact, provenance и verification expectations — разные сущности; источник не даёт claim о конкретной сборке или policy команды.
- [GitHub Changelog: Artifact Attestations is generally available](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) датирован 25.06.2024. Он показывает, что attestation для build artifact и её verification — отдельные операции. В статьях это служит исторической опорой для разделения artifact, statement и approval, а не заявлением, что sidecar создаёт или проверяет attestation.
- [Kubernetes documentation: Deployments, snapshot 14.03.2024](https://github.com/kubernetes/website/blob/c889d9b2510bba78dbb527a79b8e2099b16f3d67/content/en/docs/concepts/workloads/controllers/deployment.md) — неизменяемый снимок первичной ветки release-1.30, существовавший до конца июля 2024. Из него использована одна граница: Deployment revision и rollback относятся к Pod template. Ни текст, ни fixture не приравнивают такой rollback к отмене migration, data effect или операции в чужой среде.
- Модель задаёт четыре fixed case. <code>fixed-aligned-release</code> связывает release <code>2024.07.0</code>, source commit, artifact digest, compatible migration и proposed rollout card. Три negative cases раздельно показывают artifact, который не binds commit; migration, нацеленную на <code>2024.07.1</code>; и rollout card с другим digest. Они не описывают реальную delivery system.
- <code>inspectSyntheticReleaseEvidence()</code> сравнивает exact relations: version commit/artifact/rollout, <code>artifact.sourceCommitId</code>, migration target и compatibility, requested digest и migration version rollout card, а также versioned return point. False relation ведёт к точному <code>stop-and-reconcile-records</code>, не к retry и не к deploy.
- Input contract закрыт: marker <code>synthetic</code>, exact scope, <code>fixed-memory-only</code> mode и known case id. CI-like/deploy-like extra fields, другой scope, режим чтения registry и unknown case отдельно отклоняются. Во всех report и plan явно записаны <code>not-read</code>, <code>not-run</code> и <code>not-attempted</code> для Git, registry, CI, deploy, cloud, production, delivery logs, filesystem и network.
- План повторно строит canonical report по embedded case и сравнивает весь object. Подмена decision, record или добавление deployment-like field дают отказ. <code>draftSyntheticControlledRollback()</code> выдаёт только versioned return draft для aligned case; чужой action и stop case не проходят. Rollback draft прямо хранит boundary <code>does-not-reverse-data-and-requires-separate-human-review</code>.
- Технические проверки: <code>node --check web/scripts/upgrade-2024-07.mjs</code> — PASS; <code>node web/scripts/upgrade-2024-07.mjs --verify-fixture</code> — PASS, 27/27 assertions. Это проверка модели в памяти, не CI test, deploy validation, artifact verification или production approval.
## Проход 2 — стиль, объём и голос
- Все три ревизии начинают с конкретного сценария и цены. Practice разбирает четыре совпадающие на вид карточки и цену потери source relation. Mechanism показывает, что общий tag скрывает разные objects и превращает retry в ложное действие. Field разбирает aligned case и три разных mismatch, чтобы не называть каждый stop «неудачным релизом».
- В каждом тексте присутствует короткий прагматичный маршрут: symptom — знакомая версия на разных стадиях; cause — свободные связи между records; check — exact predicate commit/artifact/migration/digest; action — stop, reconcile и только потом human review. Нет абстрактного требования «лучше документировать» без owner-а, predicate или следующего шага.
- <code>audit:draft</code> посчитал основной текст без раздела sources: practice — **9 243** знака, mechanism — **10 370** знаков, field — **9 759** знаков. Все три значения внутри обязательного диапазона 5 000–15 000 и целевого коридора 9–11 тыс.
- В каждой статье есть содержательная SVG figure с alt и caption, working table с заголовками, исполнимый Node example, ordered route, ограничения и отдельный следующий проверяемый шаг. Примеры запускают только fixture и прямо называют её synthetic boundary. Они не содержат YAML workflow, real registry path, cloud call, CI log, deploy command или production claim.
- Practice отвечает на вопрос, какие exact links нужны от commit до rollout intent. Mechanism отделяет provenance-like relation, decision draft и controlled rollback boundary. Field учит различать три negative outcomes и направлять вопрос правильному owner-у. Это три разные функции, а не один текст с переставленными заголовками.
- Формулировки вычитаны на M7: version, digest, compatibility, owner, stop condition и rollback появляются только как проверяемые элементы contract. Речь не обещает безопасность, скорость, zero downtime, автоматический data undo, успешный rollout или adoption. «Human deploy review» везде отделён от execution.
- Import-safe gate прошёл внутри <code>npm run audit:draft</code>: CLI <code>--print-revisions</code> и module export совпадают, в ревизиях нет fields <code>date</code> и <code>author</code>, все три stable slug уже существуют в archive, figures/tables/code/routes/sources и local assets найдены.
## Проход 3 — mobile visual, SVG safety и проверки
- <code>release-engineering-2024-delivery-chain.svg</code> показывает направленную цепочку commit → artifact → migration plan → rollout card и явную нижнюю границу human review / rollback draft. <code>release-engineering-2024-rollback-table.svg</code> разделяет code/artifact return point, migration review, rollout intent и evidence gate. <code>release-engineering-2024-evidence-gate.svg</code> ведёт по четырём predicates и показывает, что любое «нет» останавливает разбор.
- У каждого SVG есть собственные <code>title</code> и <code>desc</code>, а в статье — независимые alt и caption. Подписи отделяют static teaching contract от реального CI, registry, cloud, delivery log, production rollout и data recovery. В схемах нет внешнего input или поведенческих элементов.
- <code>xmllint --noout</code> для трёх SVG — PASS. Safety scan — PASS: отсутствуют <code>script</code>, <code>foreignObject</code>, <code>javascript:</code>, <code>data:image</code>, event-handler attributes, <code>image</code>, <code>href</code>, <code>url(...)</code> и внешние URL (кроме обязательного XML namespace).
- Sharp отрендерил все три final SVG на ширине **375 px**: delivery chain — 375×256, rollback table — 375×256, evidence gate — 375×269. Первый mobile review обнаружил clipping слишком длинного заголовка rollback table; заголовок сокращён до «Rollback: версия, artifact и migration», затем XML, safety scan и Sharp-render повторены. После исправления titles, cards, стрелки, stop branches, нижние boundaries и таблица читаются без clipping, overlap или горизонтального overflow.
- Финальный набор: <code>node --check</code> — PASS; <code>--verify-fixture</code> — PASS 27/27; <code>cd web &amp;&amp; npm run audit:draft -- scripts/upgrade-2024-07.mjs</code> — PASS для трёх slug и указанных объёмов; <code>xmllint</code> — PASS; SVG safety — PASS; Sharp 375 px — PASS. npm вывел существующие предупреждения о user config, но audit завершился code 0 и на проверку draft не повлиял.
- Пакет остаётся sidecar: overlay, README, queue, <code>articles.json</code> и Git не обновлялись; staging, commit и push не выполнялись. Реальные evidence collection, authorization, policy, migration rehearsal, CI, registry verification, deploy, rollout observation и data recovery требуют отдельного разрешённого процесса.
## Выпуск после трёх независимых проходов
1. **Факты и техника.** Редактор повторно проверил GitHub Changelog от 25.06.2024: создание и проверка artifact attestation разделены; SLSA v1.0 действительно опубликована как первая stable version 19.04.2023. Для Kubernetes найден технический дефект источника: публичный historical subdomain не проходит TLS-проверку. Ссылка заменена на неизменяемый первичный snapshot `c889d9b` от 14.03.2024 в официальном репозитории Kubernetes; он подтверждает, что revision создаётся при изменении Pod template, а rollback возвращает template, а не data effect. `node --check` и fixture — **PASS 27/27**.
2. **Редактура и голос.** Повторное чтение подтвердило три разные задачи: practice описывает связи релиза, mechanism — критерии и границу controlled rollback, field — диагностирует три вида mismatch. Служебный номер партии в публичный текст не попал. Основной текст проходит диапазон: **9 243**, **10 370** и **9 759** знаков; в каждом материале есть конкретная цена ошибки, таблица, фигура, пример, ограничение и следующий шаг.
3. **Визуал и выпуск.** Все SVG прошли XML и safety scan, затем были отрендерены Sharp и просмотрены на 375 px: подписи и stop-ветви читаемы, clipping отсутствует. После подключения overlay `audit:articles` подтвердил по figure/table/code example на каждый slug; registry содержит **226** уникальных ревизий. `npm run build` завершился успешно: **374** статические страницы.
В выпуск войдут только пять файлов июля, обновления registry/production README и результат этой приёмки. Изменения пользователя, черновики других месяцев и исходный `articles.json` в него не включаются.
+2
View File
@@ -73,6 +73,7 @@ import { revisions as march2024Revisions } from '../scripts/upgrade-2024-03.mjs'
import { revisions as april2024Revisions } from '../scripts/upgrade-2024-04.mjs'; import { revisions as april2024Revisions } from '../scripts/upgrade-2024-04.mjs';
import { revisions as may2024Revisions } from '../scripts/upgrade-2024-05.mjs'; import { revisions as may2024Revisions } from '../scripts/upgrade-2024-05.mjs';
import { revisions as june2024Revisions } from '../scripts/upgrade-2024-06.mjs'; import { revisions as june2024Revisions } from '../scripts/upgrade-2024-06.mjs';
import { revisions as july2024Revisions } from '../scripts/upgrade-2024-07.mjs';
// This layer replaces archived source entries without losing their stable slug and date. // This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [ export const editorialRevisions = [
@@ -151,4 +152,5 @@ export const editorialRevisions = [
...april2024Revisions, ...april2024Revisions,
...may2024Revisions, ...may2024Revisions,
...june2024Revisions, ...june2024Revisions,
...july2024Revisions,
]; ];
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="820" viewBox="0 0 1200 820" role="img" aria-labelledby="title desc">
<title id="title">Цепочка доказательств релиза от commit до rollout intent</title>
<desc id="desc">Четыре versioned synthetic objects образуют цепочку: commit, artifact, migration plan и rollout card. Каждая стрелка требует точного совпадения. Нижняя полоса отделяет human review и rollback draft от реального deploy и data undo.</desc>
<rect width="1200" height="820" fill="#0f172a"/>
<rect x="42" y="34" width="1116" height="108" rx="22" fill="#172554" stroke="#60a5fa" stroke-width="3"/>
<text x="76" y="80" fill="#f8fafc" font-family="Arial, sans-serif" font-size="38" font-weight="700">Release evidence: версия должна пройти четыре связи</text>
<text x="76" y="114" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="22">commit → artifact → migration plan → rollout intent; совпадение label не заменяет exact relation</text>
<rect x="54" y="188" width="468" height="180" rx="20" fill="#172554" stroke="#60a5fa" stroke-width="3"/>
<text x="84" y="232" fill="#dbeafe" font-family="Arial, sans-serif" font-size="29" font-weight="700">1. Commit</text>
<text x="84" y="270" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24">version: 2024.07.0</text>
<text x="84" y="304" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24">id: synthetic-commit-7f4a0c1</text>
<text x="84" y="338" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="21">Исходная revision, не branch label</text>
<path d="M522 278 H664" fill="none" stroke="#60a5fa" stroke-width="7" stroke-linecap="round"/>
<path d="M664 278 L632 259 L632 297 Z" fill="#60a5fa"/>
<rect x="678" y="188" width="468" height="180" rx="20" fill="#052e16" stroke="#4ade80" stroke-width="3"/>
<text x="708" y="232" fill="#dcfce7" font-family="Arial, sans-serif" font-size="29" font-weight="700">2. Artifact</text>
<text x="708" y="270" fill="#f0fdf4" font-family="Arial, sans-serif" font-size="24">version: 2024.07.0</text>
<text x="708" y="304" fill="#f0fdf4" font-family="Arial, sans-serif" font-size="24">digest: sha256:…-a1</text>
<text x="708" y="338" fill="#bbf7d0" font-family="Arial, sans-serif" font-size="21">sourceCommitId = commit.id</text>
<path d="M912 368 V416" fill="none" stroke="#4ade80" stroke-width="7" stroke-linecap="round"/>
<path d="M912 416 L893 384 L931 384 Z" fill="#4ade80"/>
<rect x="678" y="438" width="468" height="180" rx="20" fill="#422006" stroke="#fbbf24" stroke-width="3"/>
<text x="708" y="482" fill="#fef3c7" font-family="Arial, sans-serif" font-size="29" font-weight="700">3. Migration plan</text>
<text x="708" y="520" fill="#fef3c7" font-family="Arial, sans-serif" font-size="24">target: 2024.07.0</text>
<text x="708" y="554" fill="#fef3c7" font-family="Arial, sans-serif" font-size="24">compatibility declared</text>
<text x="708" y="588" fill="#fde68a" font-family="Arial, sans-serif" font-size="21">Data effect — отдельный review</text>
<path d="M678 528 H536" fill="none" stroke="#fbbf24" stroke-width="7" stroke-linecap="round"/>
<path d="M536 528 L568 509 L568 547 Z" fill="#fbbf24"/>
<rect x="54" y="438" width="468" height="180" rx="20" fill="#3f1d2e" stroke="#f472b6" stroke-width="3"/>
<text x="84" y="482" fill="#fce7f3" font-family="Arial, sans-serif" font-size="29" font-weight="700">4. Rollout card</text>
<text x="84" y="520" fill="#fce7f3" font-family="Arial, sans-serif" font-size="24">version + digest + migration</text>
<text x="84" y="554" fill="#fce7f3" font-family="Arial, sans-serif" font-size="24">state: proposed, not executed</text>
<text x="84" y="588" fill="#fbcfe8" font-family="Arial, sans-serif" font-size="21">Intent, не deploy result</text>
<rect x="54" y="682" width="1092" height="96" rx="18" fill="#1e293b" stroke="#94a3b8" stroke-width="2"/>
<text x="82" y="722" fill="#e2e8f0" font-family="Arial, sans-serif" font-size="25" font-weight="700">Все relations true → только human deploy review</text>
<text x="82" y="754" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="21">Rollback draft хранит return version + digest; он не запускает deploy и не обещает data undo.</text>
<text x="54" y="806" fill="#94a3b8" font-family="Arial, sans-serif" font-size="17">Static fixed synthetic teaching records only — not CI, artifact registry, cloud, delivery log or production rollout.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="860" viewBox="0 0 1200 860" role="img" aria-labelledby="title desc">
<title id="title">Evidence gate для проверки release records</title>
<desc id="desc">Вертикальный маршрут проверяет release version, связь artifact с commit, target и compatibility migration, затем digest and migration version в rollout card. Любое нет идёт в stop and reconcile. Только все да идут к human review, а return draft отдельно не обещает data undo.</desc>
<rect width="1200" height="860" fill="#111827"/>
<rect x="40" y="30" width="1120" height="108" rx="22" fill="#1e3a5f" stroke="#60a5fa" stroke-width="3"/>
<text x="76" y="78" fill="#f8fafc" font-family="Arial, sans-serif" font-size="38" font-weight="700">Evidence gate: stop на первом broken relation</text>
<text x="76" y="114" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="22">Fixed synthetic records проходят четыре questions; «да» — human review, не реальный deploy.</text>
<rect x="92" y="188" width="610" height="92" rx="18" fill="#172554" stroke="#60a5fa" stroke-width="3"/>
<text x="124" y="226" fill="#dbeafe" font-family="Arial, sans-serif" font-size="27" font-weight="700">1. Release version совпадает?</text>
<text x="124" y="258" fill="#bfdbfe" font-family="Arial, sans-serif" font-size="21">commit, artifact и rollout card называют один release</text>
<path d="M397 280 V308" fill="none" stroke="#60a5fa" stroke-width="6"/>
<path d="M397 308 L381 282 L413 282 Z" fill="#60a5fa"/>
<path d="M702 234 H818" fill="none" stroke="#fb7185" stroke-width="6"/>
<path d="M818 234 L792 218 L792 250 Z" fill="#fb7185"/>
<rect x="838" y="190" width="268" height="86" rx="16" fill="#4c0519" stroke="#fb7185" stroke-width="3"/>
<text x="870" y="228" fill="#ffe4e6" font-family="Arial, sans-serif" font-size="25" font-weight="700">Нет → STOP</text>
<text x="870" y="258" fill="#fecdd3" font-family="Arial, sans-serif" font-size="19">reconcile versions</text>
<rect x="92" y="318" width="610" height="92" rx="18" fill="#052e16" stroke="#4ade80" stroke-width="3"/>
<text x="124" y="356" fill="#dcfce7" font-family="Arial, sans-serif" font-size="27" font-weight="700">2. Artifact binds exact commit?</text>
<text x="124" y="388" fill="#bbf7d0" font-family="Arial, sans-serif" font-size="21">artifact.sourceCommitId = commit.id</text>
<path d="M397 410 V438" fill="none" stroke="#4ade80" stroke-width="6"/>
<path d="M397 438 L381 412 L413 412 Z" fill="#4ade80"/>
<path d="M702 364 H818" fill="none" stroke="#fb7185" stroke-width="6"/>
<path d="M818 364 L792 348 L792 380 Z" fill="#fb7185"/>
<rect x="838" y="320" width="268" height="86" rx="16" fill="#4c0519" stroke="#fb7185" stroke-width="3"/>
<text x="870" y="358" fill="#ffe4e6" font-family="Arial, sans-serif" font-size="25" font-weight="700">Нет → STOP</text>
<text x="870" y="388" fill="#fecdd3" font-family="Arial, sans-serif" font-size="19">reconcile source</text>
<rect x="92" y="448" width="610" height="104" rx="18" fill="#422006" stroke="#fbbf24" stroke-width="3"/>
<text x="124" y="486" fill="#fef3c7" font-family="Arial, sans-serif" font-size="27" font-weight="700">3. Migration target + compatibility?</text>
<text x="124" y="518" fill="#fde68a" font-family="Arial, sans-serif" font-size="21">target release matches; data boundary is named</text>
<path d="M397 552 V580" fill="none" stroke="#fbbf24" stroke-width="6"/>
<path d="M397 580 L381 554 L413 554 Z" fill="#fbbf24"/>
<path d="M702 498 H818" fill="none" stroke="#fb7185" stroke-width="6"/>
<path d="M818 498 L792 482 L792 514 Z" fill="#fb7185"/>
<rect x="838" y="452" width="268" height="96" rx="16" fill="#4c0519" stroke="#fb7185" stroke-width="3"/>
<text x="870" y="490" fill="#ffe4e6" font-family="Arial, sans-serif" font-size="25" font-weight="700">Нет → STOP</text>
<text x="870" y="520" fill="#fecdd3" font-family="Arial, sans-serif" font-size="19">review migration</text>
<rect x="92" y="590" width="610" height="92" rx="18" fill="#3f1d2e" stroke="#f472b6" stroke-width="3"/>
<text x="124" y="628" fill="#fce7f3" font-family="Arial, sans-serif" font-size="27" font-weight="700">4. Rollout card repeats digest?</text>
<text x="124" y="660" fill="#fbcfe8" font-family="Arial, sans-serif" font-size="21">requested digest + migration version are exact</text>
<path d="M702 636 H818" fill="none" stroke="#fb7185" stroke-width="6"/>
<path d="M818 636 L792 620 L792 652 Z" fill="#fb7185"/>
<rect x="838" y="592" width="268" height="86" rx="16" fill="#4c0519" stroke="#fb7185" stroke-width="3"/>
<text x="870" y="630" fill="#ffe4e6" font-family="Arial, sans-serif" font-size="25" font-weight="700">Нет → STOP</text>
<text x="870" y="660" fill="#fecdd3" font-family="Arial, sans-serif" font-size="19">reconcile intent</text>
<path d="M397 682 V710" fill="none" stroke="#4ade80" stroke-width="6"/>
<path d="M397 710 L381 684 L413 684 Z" fill="#4ade80"/>
<rect x="92" y="722" width="610" height="78" rx="18" fill="#14532d" stroke="#86efac" stroke-width="3"/>
<text x="124" y="770" fill="#dcfce7" font-family="Arial, sans-serif" font-size="28" font-weight="700">Да → human deploy review, not execution</text>
<rect x="744" y="722" width="362" height="78" rx="18" fill="#1e293b" stroke="#94a3b8" stroke-width="3"/>
<text x="770" y="756" fill="#e2e8f0" font-family="Arial, sans-serif" font-size="21" font-weight="700">Rollback draft: version + digest</text>
<text x="770" y="782" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="18">not data undo; not real deploy</text>
<text x="42" y="842" fill="#94a3b8" font-family="Arial, sans-serif" font-size="17">Static synthetic decision map only. It does not read CI, registry, cloud, logs or production state.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="820" viewBox="0 0 1200 820" role="img" aria-labelledby="title desc">
<title id="title">Матрица controlled rollback для версии, artifact, migration и rollout</title>
<desc id="desc">Таблица показывает четыре части rollback decision. Code and artifact получают versioned return point; migration требует отдельного review, rollout card остаётся намерением, а evidence record нужно сверить до действия. Внизу указано, что схема не является реальным deploy.</desc>
<rect width="1200" height="820" fill="#f8fafc"/>
<rect x="40" y="32" width="1120" height="110" rx="22" fill="#1e293b"/>
<text x="76" y="80" fill="#f8fafc" font-family="Arial, sans-serif" font-size="38" font-weight="700">Rollback: версия, artifact и migration</text>
<text x="76" y="116" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="22">Return point — versioned decision draft. Он не равен автоматическому откату данных или реальному rollout.</text>
<rect x="48" y="184" width="252" height="82" fill="#0f172a"/>
<rect x="300" y="184" width="294" height="82" fill="#0f172a"/>
<rect x="594" y="184" width="320" height="82" fill="#0f172a"/>
<rect x="914" y="184" width="238" height="82" fill="#0f172a"/>
<text x="72" y="233" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24" font-weight="700">Часть</text>
<text x="324" y="233" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24" font-weight="700">Return record</text>
<text x="618" y="233" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24" font-weight="700">Проверить до действия</text>
<text x="938" y="233" fill="#f8fafc" font-family="Arial, sans-serif" font-size="24" font-weight="700">Граница</text>
<rect x="48" y="266" width="1104" height="106" fill="#e0f2fe" stroke="#7dd3fc" stroke-width="2"/>
<text x="72" y="310" fill="#0c4a6e" font-family="Arial, sans-serif" font-size="25" font-weight="700">Code + artifact</text>
<text x="324" y="306" fill="#0f172a" font-family="Arial, sans-serif" font-size="23">2024.06.3</text>
<text x="324" y="338" fill="#0f172a" font-family="Arial, sans-serif" font-size="21">sha256:…-z9</text>
<text x="618" y="306" fill="#0f172a" font-family="Arial, sans-serif" font-size="21">Exact version and digest</text>
<text x="618" y="338" fill="#0f172a" font-family="Arial, sans-serif" font-size="21">belong to known return point</text>
<text x="938" y="306" fill="#0f172a" font-family="Arial, sans-serif" font-size="21">Draft only</text>
<text x="938" y="338" fill="#0f172a" font-family="Arial, sans-serif" font-size="21">not a deploy</text>
<rect x="48" y="372" width="1104" height="106" fill="#fef3c7" stroke="#fbbf24" stroke-width="2"/>
<text x="72" y="416" fill="#78350f" font-family="Arial, sans-serif" font-size="25" font-weight="700">Migration</text>
<text x="324" y="412" fill="#422006" font-family="Arial, sans-serif" font-size="21">Compatibility window</text>
<text x="324" y="444" fill="#422006" font-family="Arial, sans-serif" font-size="21">named, not assumed</text>
<text x="618" y="412" fill="#422006" font-family="Arial, sans-serif" font-size="21">Data effect and reverse</text>
<text x="618" y="444" fill="#422006" font-family="Arial, sans-serif" font-size="21">procedure have owners</text>
<text x="938" y="412" fill="#422006" font-family="Arial, sans-serif" font-size="21">No automatic</text>
<text x="938" y="444" fill="#422006" font-family="Arial, sans-serif" font-size="21">data undo</text>
<rect x="48" y="478" width="1104" height="106" fill="#fce7f3" stroke="#f9a8d4" stroke-width="2"/>
<text x="72" y="522" fill="#831843" font-family="Arial, sans-serif" font-size="25" font-weight="700">Rollout card</text>
<text x="324" y="518" fill="#500724" font-family="Arial, sans-serif" font-size="21">Previous version +</text>
<text x="324" y="550" fill="#500724" font-family="Arial, sans-serif" font-size="21">previous digest intent</text>
<text x="618" y="518" fill="#500724" font-family="Arial, sans-serif" font-size="21">Card repeats exact</text>
<text x="618" y="550" fill="#500724" font-family="Arial, sans-serif" font-size="21">immutable artifact identity</text>
<text x="938" y="518" fill="#500724" font-family="Arial, sans-serif" font-size="21">Not history,</text>
<text x="938" y="550" fill="#500724" font-family="Arial, sans-serif" font-size="21">not execution</text>
<rect x="48" y="584" width="1104" height="106" fill="#dcfce7" stroke="#86efac" stroke-width="2"/>
<text x="72" y="628" fill="#14532d" font-family="Arial, sans-serif" font-size="25" font-weight="700">Evidence gate</text>
<text x="324" y="624" fill="#14532d" font-family="Arial, sans-serif" font-size="21">Canonical fixed report</text>
<text x="324" y="656" fill="#14532d" font-family="Arial, sans-serif" font-size="21">before decision draft</text>
<text x="618" y="624" fill="#14532d" font-family="Arial, sans-serif" font-size="21">Every link passes;</text>
<text x="618" y="656" fill="#14532d" font-family="Arial, sans-serif" font-size="21">mismatch means stop</text>
<text x="938" y="624" fill="#14532d" font-family="Arial, sans-serif" font-size="21">Human review</text>
<text x="938" y="656" fill="#14532d" font-family="Arial, sans-serif" font-size="21">still required</text>
<rect x="48" y="724" width="1104" height="54" rx="14" fill="#fee2e2" stroke="#f87171" stroke-width="2"/>
<text x="76" y="758" fill="#7f1d1d" font-family="Arial, sans-serif" font-size="22" font-weight="700">Fixture boundary: no real CI, artifact registry, deploy, cloud, logs, production state or migration execution.</text>
<text x="48" y="808" fill="#64748b" font-family="Arial, sans-serif" font-size="17">Static diagram of a teaching contract; it does not report delivery status or authorize rollback.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

+735
View File
@@ -0,0 +1,735 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replaceAll('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
const sources = [
{
title: 'GitHub Changelog: Artifact Attestations is generally available, 25 июня 2024',
url: 'https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/',
note: 'Первичный датированный источник GitHub: в GitHub Actions можно создать attestation для собранного artifact и затем проверить его. Здесь это основание различать artifact и проверяемое утверждение о нём; источник не является policy, журналом или доказательством чужого выпуска.',
},
{
title: 'SLSA: SLSA v1.0 is now final!, 19 апреля 2023',
url: 'https://slsa.dev/blog/2023/04/slsa-v1-final',
note: 'Первичная публикация о первом stable SLSA v1.0. Из неё взята узкая идея versioned provenance и проверки ожиданий, а не claim, что любой набор полей делает конкретную сборку безопасной или готовой к deploy.',
},
{
title: 'Kubernetes documentation: Deployments, snapshot 14.03.2024',
url: 'https://github.com/kubernetes/website/blob/c889d9b2510bba78dbb527a79b8e2099b16f3d67/content/en/docs/concepts/workloads/controllers/deployment.md',
note: 'Первичный неизменяемый снимок ветки release-1.30, доступный до июля 2024: revision Deployment связана с Pod template, а rollback возвращает прежнюю revision шаблона. Это не отменяет data migration и не переносится автоматически на другой deploy-инструмент.',
},
];
function sourceList() {
return '<ul>' + sources.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function revision(meta, parts) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList();
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength);
}
return Object.freeze({ ...meta, contentHtml, proseLength });
}
const FIXTURE_SCOPE = 'synthetic-release-evidence-2024-07';
const FIXTURE_MODE = 'fixed-memory-only';
const FIXTURE_LIMIT = 'fixed-versioned-synthetic-js-objects-in-memory-no-git-no-artifact-registry-no-ci-no-deploy-no-cloud-no-production-no-delivery-logs-no-files-no-network';
function freezeRecord(record) {
return Object.freeze({
...record,
commit: Object.freeze({ ...record.commit }),
artifact: Object.freeze({ ...record.artifact }),
migration: Object.freeze({ ...record.migration }),
rollout: Object.freeze({ ...record.rollout }),
rollback: Object.freeze({ ...record.rollback }),
});
}
const fixedRecords = Object.freeze({
'fixed-aligned-release': freezeRecord({
recordVersion: 'synthetic-release-record/v1',
label: 'all four versioned teaching objects bind to one release and one compatible return point',
releaseVersion: '2024.07.0',
commit: {
kind: 'synthetic-commit/v1',
version: '2024.07.0',
id: 'synthetic-commit-7f4a0c1',
},
artifact: {
kind: 'synthetic-artifact/v1',
version: '2024.07.0',
digest: 'sha256:synthetic-2024-07-0-a1',
sourceCommitId: 'synthetic-commit-7f4a0c1',
evidenceVersion: 'synthetic-evidence/v1',
},
migration: {
kind: 'synthetic-migration-plan/v1',
version: '2024.07.0',
targetReleaseVersion: '2024.07.0',
compatibility: 'expand-compatible-with-2024.06.3',
action: 'not-executed',
},
rollout: {
kind: 'synthetic-rollout-card/v1',
version: '2024.07.0',
requestedReleaseVersion: '2024.07.0',
requestedArtifactDigest: 'sha256:synthetic-2024-07-0-a1',
requestedMigrationVersion: '2024.07.0',
state: 'proposed-not-executed',
},
rollback: {
kind: 'synthetic-rollback-candidate/v1',
version: '2024.07.0',
returnReleaseVersion: '2024.06.3',
returnArtifactDigest: 'sha256:synthetic-2024-06-3-z9',
migrationBoundary: 'does-not-reverse-data-and-requires-separate-human-review',
state: 'draft-not-executed',
},
}),
'fixed-artifact-commit-mismatch': freezeRecord({
recordVersion: 'synthetic-release-record/v1',
label: 'artifact version looks familiar but its declared source commit is different',
releaseVersion: '2024.07.0',
commit: {
kind: 'synthetic-commit/v1',
version: '2024.07.0',
id: 'synthetic-commit-7f4a0c1',
},
artifact: {
kind: 'synthetic-artifact/v1',
version: '2024.07.0',
digest: 'sha256:synthetic-2024-07-0-b2',
sourceCommitId: 'synthetic-commit-other-91',
evidenceVersion: 'synthetic-evidence/v1',
},
migration: {
kind: 'synthetic-migration-plan/v1',
version: '2024.07.0',
targetReleaseVersion: '2024.07.0',
compatibility: 'expand-compatible-with-2024.06.3',
action: 'not-executed',
},
rollout: {
kind: 'synthetic-rollout-card/v1',
version: '2024.07.0',
requestedReleaseVersion: '2024.07.0',
requestedArtifactDigest: 'sha256:synthetic-2024-07-0-b2',
requestedMigrationVersion: '2024.07.0',
state: 'proposed-not-executed',
},
rollback: {
kind: 'synthetic-rollback-candidate/v1',
version: '2024.07.0',
returnReleaseVersion: '2024.06.3',
returnArtifactDigest: 'sha256:synthetic-2024-06-3-z9',
migrationBoundary: 'does-not-reverse-data-and-requires-separate-human-review',
state: 'draft-not-executed',
},
}),
'fixed-migration-version-mismatch': freezeRecord({
recordVersion: 'synthetic-release-record/v1',
label: 'migration points at a later release while the artifact and rollout card name the earlier one',
releaseVersion: '2024.07.0',
commit: {
kind: 'synthetic-commit/v1',
version: '2024.07.0',
id: 'synthetic-commit-7f4a0c1',
},
artifact: {
kind: 'synthetic-artifact/v1',
version: '2024.07.0',
digest: 'sha256:synthetic-2024-07-0-c3',
sourceCommitId: 'synthetic-commit-7f4a0c1',
evidenceVersion: 'synthetic-evidence/v1',
},
migration: {
kind: 'synthetic-migration-plan/v1',
version: '2024.07.1',
targetReleaseVersion: '2024.07.1',
compatibility: 'not-yet-declared-for-2024.07.0',
action: 'not-executed',
},
rollout: {
kind: 'synthetic-rollout-card/v1',
version: '2024.07.0',
requestedReleaseVersion: '2024.07.0',
requestedArtifactDigest: 'sha256:synthetic-2024-07-0-c3',
requestedMigrationVersion: '2024.07.0',
state: 'proposed-not-executed',
},
rollback: {
kind: 'synthetic-rollback-candidate/v1',
version: '2024.07.0',
returnReleaseVersion: '2024.06.3',
returnArtifactDigest: 'sha256:synthetic-2024-06-3-z9',
migrationBoundary: 'does-not-reverse-data-and-requires-separate-human-review',
state: 'draft-not-executed',
},
}),
'fixed-rollout-digest-mismatch': freezeRecord({
recordVersion: 'synthetic-release-record/v1',
label: 'rollout card requests a different digest than the recorded artifact',
releaseVersion: '2024.07.0',
commit: {
kind: 'synthetic-commit/v1',
version: '2024.07.0',
id: 'synthetic-commit-7f4a0c1',
},
artifact: {
kind: 'synthetic-artifact/v1',
version: '2024.07.0',
digest: 'sha256:synthetic-2024-07-0-d4',
sourceCommitId: 'synthetic-commit-7f4a0c1',
evidenceVersion: 'synthetic-evidence/v1',
},
migration: {
kind: 'synthetic-migration-plan/v1',
version: '2024.07.0',
targetReleaseVersion: '2024.07.0',
compatibility: 'expand-compatible-with-2024.06.3',
action: 'not-executed',
},
rollout: {
kind: 'synthetic-rollout-card/v1',
version: '2024.07.0',
requestedReleaseVersion: '2024.07.0',
requestedArtifactDigest: 'sha256:synthetic-other-digest-e5',
requestedMigrationVersion: '2024.07.0',
state: 'proposed-not-executed',
},
rollback: {
kind: 'synthetic-rollback-candidate/v1',
version: '2024.07.0',
returnReleaseVersion: '2024.06.3',
returnArtifactDigest: 'sha256:synthetic-2024-06-3-z9',
migrationBoundary: 'does-not-reverse-data-and-requires-separate-human-review',
state: 'draft-not-executed',
},
}),
});
function hasOnlyKeys(object, allowed) {
return object && typeof object === 'object' && !Array.isArray(object)
&& Object.keys(object).every((key) => allowed.includes(key));
}
function rejectInspection(reason) {
return Object.freeze({
kind: 'synthetic-release-evidence-report/v1',
syntheticOnly: true,
inputAccepted: false,
reason,
modelLimit: FIXTURE_LIMIT,
realGit: 'not-read',
artifactRegistry: 'not-read',
ci: 'not-run',
deployment: 'not-run',
cloud: 'not-read',
production: 'not-read',
deliveryLogs: 'not-read',
filesystem: 'not-read',
network: 'not-used',
});
}
export function createFixedSyntheticReleaseInput(caseId) {
return Object.freeze({
kind: 'synthetic-release-evidence-input/v1',
synthetic: true,
scope: FIXTURE_SCOPE,
mode: FIXTURE_MODE,
caseId,
});
}
function inspectRecord(record) {
const checks = Object.freeze({
commitVersionMatchesRelease: record.commit.version === record.releaseVersion,
artifactVersionMatchesRelease: record.artifact.version === record.releaseVersion,
artifactBindsCommit: record.artifact.sourceCommitId === record.commit.id,
migrationVersionMatchesRelease: record.migration.version === record.releaseVersion,
migrationTargetsRelease: record.migration.targetReleaseVersion === record.releaseVersion,
migrationIsDeclaredCompatible: record.migration.compatibility === 'expand-compatible-with-2024.06.3',
rolloutVersionMatchesRelease: record.rollout.version === record.releaseVersion,
rolloutRequestsRelease: record.rollout.requestedReleaseVersion === record.releaseVersion,
rolloutRequestsArtifact: record.rollout.requestedArtifactDigest === record.artifact.digest,
rolloutRequestsMigration: record.rollout.requestedMigrationVersion === record.migration.version,
rollbackCarriesKnownReturnVersion: record.rollback.returnReleaseVersion === '2024.06.3',
rollbackCarriesKnownReturnArtifact: record.rollback.returnArtifactDigest === 'sha256:synthetic-2024-06-3-z9',
rollbackDoesNotPromiseDataUndo: record.rollback.migrationBoundary === 'does-not-reverse-data-and-requires-separate-human-review',
});
const findings = [];
if (!checks.commitVersionMatchesRelease) findings.push('commit-version-mismatch');
if (!checks.artifactVersionMatchesRelease) findings.push('artifact-version-mismatch');
if (!checks.artifactBindsCommit) findings.push('artifact-does-not-bind-commit');
if (!checks.migrationVersionMatchesRelease || !checks.migrationTargetsRelease) findings.push('migration-does-not-target-release');
if (!checks.migrationIsDeclaredCompatible) findings.push('migration-compatibility-not-declared');
if (!checks.rolloutVersionMatchesRelease || !checks.rolloutRequestsRelease) findings.push('rollout-version-mismatch');
if (!checks.rolloutRequestsArtifact) findings.push('rollout-does-not-request-recorded-artifact');
if (!checks.rolloutRequestsMigration) findings.push('rollout-does-not-request-recorded-migration');
if (!checks.rollbackCarriesKnownReturnVersion || !checks.rollbackCarriesKnownReturnArtifact) findings.push('rollback-return-record-incomplete');
if (!checks.rollbackDoesNotPromiseDataUndo) findings.push('rollback-overstates-migration-reversal');
return Object.freeze({ checks, findings: Object.freeze(findings) });
}
/**
* Inspects one embedded release-evidence record. It never reads a commit,
* artifact registry, CI run, deploy target, cloud account, production state,
* delivery log, file, environment variable, clock or network endpoint.
*/
export function inspectSyntheticReleaseEvidence(input) {
if (!input || input.kind !== 'synthetic-release-evidence-input/v1' || input.synthetic !== true) {
return rejectInspection('synthetic-input-required');
}
const allowed = ['kind', 'synthetic', 'scope', 'mode', 'caseId'];
if (!hasOnlyKeys(input, allowed)) return rejectInspection('unexpected-input-field');
if (input.scope !== FIXTURE_SCOPE) return rejectInspection('unexpected-synthetic-scope');
if (input.mode !== FIXTURE_MODE) return rejectInspection('fixed-memory-mode-required');
if (!Object.hasOwn(fixedRecords, input.caseId)) return rejectInspection('unknown-fixed-synthetic-case');
const record = fixedRecords[input.caseId];
const inspected = inspectRecord(record);
const decision = inspected.findings.length === 0
? 'continue-to-human-deploy-review'
: 'stop-and-reconcile-records';
return Object.freeze({
kind: 'synthetic-release-evidence-report/v1',
syntheticOnly: true,
inputAccepted: true,
reason: 'fixed-versioned-synthetic-record-inspected',
modelLimit: FIXTURE_LIMIT,
scope: FIXTURE_SCOPE,
caseId: input.caseId,
caseLabel: record.label,
record,
checks: inspected.checks,
findings: inspected.findings,
decision,
nextHumanQuestion: decision === 'continue-to-human-deploy-review'
? 'can an authorized reviewer compare the real commit, artifact, migration and deploy intent using the organisation’s approved evidence?'
: 'which versioned record is wrong, and can the discrepancy be corrected before any real deploy review begins?',
evidenceBoundary: Object.freeze({
source: 'embedded-fixed-synthetic-js-objects-only',
realCommit: 'not-read',
artifactRegistry: 'not-read',
ci: 'not-run',
deployment: 'not-run',
cloud: 'not-read',
production: 'not-read',
deliveryLogs: 'not-read',
filesystem: 'not-read',
network: 'not-used',
}),
productionEffect: 'not-attempted',
});
}
function canonicalReportFor(caseId) {
return inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput(caseId));
}
/**
* Produces a teaching decision draft only after comparing the complete report
* with the fixed record again. It does not start CI, publish an artifact, or
* create, approve, change, observe or roll back a deployment.
*/
export function planSyntheticReleaseDecision(report) {
if (!report || report.kind !== 'synthetic-release-evidence-report/v1' || report.syntheticOnly !== true || report.inputAccepted !== true) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'accepted-synthetic-report-required', modelLimit: FIXTURE_LIMIT });
}
const allowed = [
'kind', 'syntheticOnly', 'inputAccepted', 'reason', 'modelLimit', 'scope', 'caseId', 'caseLabel',
'record', 'checks', 'findings', 'decision', 'nextHumanQuestion', 'evidenceBoundary', 'productionEffect',
];
if (!hasOnlyKeys(report, allowed)) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'unexpected-report-field', modelLimit: FIXTURE_LIMIT });
}
const canonical = canonicalReportFor(report.caseId);
if (JSON.stringify(canonical) !== JSON.stringify(report)) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'report-does-not-match-fixed-record', modelLimit: FIXTURE_LIMIT });
}
const actions = canonical.decision === 'continue-to-human-deploy-review'
? Object.freeze([
'request-authorized-human-comparison-of-real-evidence',
'keep-commit-artifact-migration-and-rollout-identifiers-together',
'prepare-only-a-versioned-rollback-draft-with-data-boundary',
])
: Object.freeze([
'stop-before-real-deploy-review',
'reconcile-only-the-named-fixed-record-discrepancy',
'repeat-the-evidence-comparison-after-a-new-authorized-record-exists',
]);
return Object.freeze({
kind: 'synthetic-release-decision-draft/v1',
accepted: true,
syntheticOnly: true,
reason: 'canonical-fixed-synthetic-release-decision-draft',
modelLimit: FIXTURE_LIMIT,
sourceReport: canonical,
decision: canonical.decision,
actions,
rollbackCandidate: canonical.record.rollback,
realGit: 'not-read-or-changed',
artifactRegistry: 'not-read-or-written',
ci: 'not-run',
deployment: 'not-created-or-run',
cloud: 'not-read-or-changed',
production: 'not-read-or-changed',
deliveryLogs: 'not-read-or-written',
filesystem: 'not-read-or-written',
network: 'not-used',
productionEffect: 'not-attempted',
});
}
/**
* Returns a versioned synthetic rollback draft only for the aligned canonical
* decision. The returned record is not a deploy instruction and never changes
* code, artifacts, data, traffic, cloud state or a real rollout.
*/
export function draftSyntheticControlledRollback(plan) {
if (!plan || plan.kind !== 'synthetic-release-decision-draft/v1' || plan.accepted !== true || plan.syntheticOnly !== true || !plan.sourceReport) {
return Object.freeze({ drafted: false, syntheticOnly: true, reason: 'accepted-synthetic-decision-draft-required' });
}
const canonicalPlan = planSyntheticReleaseDecision(plan.sourceReport);
if (canonicalPlan.accepted !== true || JSON.stringify(canonicalPlan) !== JSON.stringify(plan)) {
return Object.freeze({ drafted: false, syntheticOnly: true, reason: 'decision-draft-does-not-match-fixed-record' });
}
if (canonicalPlan.decision !== 'continue-to-human-deploy-review') {
return Object.freeze({ drafted: false, syntheticOnly: true, reason: 'reconcile-records-before-rollback-draft' });
}
const current = canonicalPlan.sourceReport.record;
return Object.freeze({
kind: 'synthetic-controlled-rollback-draft/v1',
drafted: true,
syntheticOnly: true,
reason: 'versioned-return-point-recorded-in-memory',
fromReleaseVersion: current.releaseVersion,
fromArtifactDigest: current.artifact.digest,
toReleaseVersion: current.rollback.returnReleaseVersion,
toArtifactDigest: current.rollback.returnArtifactDigest,
migrationBoundary: current.rollback.migrationBoundary,
realGit: 'not-read-or-changed',
artifactRegistry: 'not-read-or-written',
ci: 'not-run',
deployment: 'not-created-or-run',
cloud: 'not-read-or-changed',
production: 'not-read-or-changed',
deliveryLogs: 'not-read-or-written',
filesystem: 'not-read-or-written',
network: 'not-used',
productionEffect: 'not-attempted',
});
}
export function runReleaseEvidenceFixture() {
const aligned = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-aligned-release'));
const artifactMismatch = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-artifact-commit-mismatch'));
const migrationMismatch = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-migration-version-mismatch'));
const rolloutMismatch = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-rollout-digest-mismatch'));
const nonSynthetic = inspectSyntheticReleaseEvidence({ ...createFixedSyntheticReleaseInput('fixed-aligned-release'), synthetic: false });
const extraCiLikeField = inspectSyntheticReleaseEvidence({ ...createFixedSyntheticReleaseInput('fixed-aligned-release'), ciLog: 'not-read' });
const extraDeployLikeField = inspectSyntheticReleaseEvidence({ ...createFixedSyntheticReleaseInput('fixed-aligned-release'), deployTarget: 'not-read' });
const wrongScope = inspectSyntheticReleaseEvidence({ ...createFixedSyntheticReleaseInput('fixed-aligned-release'), scope: 'another-scope' });
const wrongMode = inspectSyntheticReleaseEvidence({ ...createFixedSyntheticReleaseInput('fixed-aligned-release'), mode: 'read-real-registry' });
const unknownCase = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('invented-release'));
const alignedPlan = planSyntheticReleaseDecision(aligned);
const mismatchPlan = planSyntheticReleaseDecision(artifactMismatch);
const forgedDecision = planSyntheticReleaseDecision({ ...aligned, decision: 'stop-and-reconcile-records' });
const forgedRecord = planSyntheticReleaseDecision({ ...aligned, record: { ...aligned.record, releaseVersion: '2024.99.0' } });
const extraReportField = planSyntheticReleaseDecision({ ...aligned, realDeploymentId: 'not-read' });
const rollbackDraft = draftSyntheticControlledRollback(alignedPlan);
const forgedRollback = draftSyntheticControlledRollback({ ...alignedPlan, actions: ['run-real-deploy'] });
const blockedRollback = draftSyntheticControlledRollback(mismatchPlan);
const reportRollback = draftSyntheticControlledRollback(aligned);
return Object.freeze({
assertions: Object.freeze({
acceptsAlignedFixedRecord: aligned.inputAccepted === true && aligned.decision === 'continue-to-human-deploy-review',
keepsAllTeachingObjectsVersioned: aligned.record.commit.kind === 'synthetic-commit/v1' && aligned.record.artifact.kind === 'synthetic-artifact/v1' && aligned.record.migration.kind === 'synthetic-migration-plan/v1' && aligned.record.rollout.kind === 'synthetic-rollout-card/v1',
bindsCommitToArtifact: aligned.checks.artifactBindsCommit === true && aligned.record.artifact.sourceCommitId === aligned.record.commit.id,
bindsArtifactAndMigrationToRolloutCard: aligned.checks.rolloutRequestsArtifact === true && aligned.checks.rolloutRequestsMigration === true,
requiresMigrationCompatibility: aligned.checks.migrationIsDeclaredCompatible === true && aligned.record.migration.action === 'not-executed',
keepsReturnPointVersioned: aligned.checks.rollbackCarriesKnownReturnVersion === true && aligned.checks.rollbackCarriesKnownReturnArtifact === true,
doesNotClaimMigrationUndo: aligned.checks.rollbackDoesNotPromiseDataUndo === true && aligned.record.rollback.migrationBoundary.includes('does-not-reverse-data'),
stopsArtifactCommitMismatch: artifactMismatch.decision === 'stop-and-reconcile-records' && artifactMismatch.findings.includes('artifact-does-not-bind-commit'),
stopsMigrationMismatch: migrationMismatch.decision === 'stop-and-reconcile-records' && migrationMismatch.findings.includes('migration-does-not-target-release'),
stopsUndeclaredMigrationCompatibility: migrationMismatch.findings.includes('migration-compatibility-not-declared'),
stopsRolloutDigestMismatch: rolloutMismatch.decision === 'stop-and-reconcile-records' && rolloutMismatch.findings.includes('rollout-does-not-request-recorded-artifact'),
rejectsNonSyntheticInput: nonSynthetic.inputAccepted === false && nonSynthetic.reason === 'synthetic-input-required',
rejectsCiLikeInput: extraCiLikeField.inputAccepted === false && extraCiLikeField.reason === 'unexpected-input-field',
rejectsDeployLikeInput: extraDeployLikeField.inputAccepted === false && extraDeployLikeField.reason === 'unexpected-input-field',
rejectsDifferentScope: wrongScope.inputAccepted === false && wrongScope.reason === 'unexpected-synthetic-scope',
rejectsRegistryReadMode: wrongMode.inputAccepted === false && wrongMode.reason === 'fixed-memory-mode-required',
rejectsUnknownFixedCase: unknownCase.inputAccepted === false && unknownCase.reason === 'unknown-fixed-synthetic-case',
createsOnlySyntheticDecisionDraft: alignedPlan.accepted === true && alignedPlan.deployment === 'not-created-or-run' && alignedPlan.ci === 'not-run',
preservesStopDecisionInDraft: mismatchPlan.accepted === true && mismatchPlan.decision === 'stop-and-reconcile-records' && mismatchPlan.actions.includes('stop-before-real-deploy-review'),
rejectsForgedDecision: forgedDecision.accepted === false && forgedDecision.reason === 'report-does-not-match-fixed-record',
rejectsForgedRecord: forgedRecord.accepted === false && forgedRecord.reason === 'report-does-not-match-fixed-record',
rejectsUnexpectedReportField: extraReportField.accepted === false && extraReportField.reason === 'unexpected-report-field',
draftsControlledReturnOnlyForAlignedRecord: rollbackDraft.drafted === true && rollbackDraft.fromReleaseVersion === '2024.07.0' && rollbackDraft.toReleaseVersion === '2024.06.3',
keepsRollbackOutsideRealSystems: rollbackDraft.deployment === 'not-created-or-run' && rollbackDraft.production === 'not-read-or-changed' && rollbackDraft.deliveryLogs === 'not-read-or-written',
rejectsForgedRollbackDraft: forgedRollback.drafted === false && forgedRollback.reason === 'decision-draft-does-not-match-fixed-record',
blocksRollbackDraftForMismatch: blockedRollback.drafted === false && blockedRollback.reason === 'reconcile-records-before-rollback-draft',
refusesReportAsRollbackDraft: reportRollback.drafted === false && reportRollback.reason === 'accepted-synthetic-decision-draft-required',
}),
samples: Object.freeze({
aligned,
artifactMismatch,
migrationMismatch,
rolloutMismatch,
nonSynthetic,
extraCiLikeField,
extraDeployLikeField,
wrongScope,
wrongMode,
unknownCase,
alignedPlan,
mismatchPlan,
forgedDecision,
forgedRecord,
extraReportField,
rollbackDraft,
forgedRollback,
blockedRollback,
reportRollback,
}),
});
}
const practiceExample = [
"import { createFixedSyntheticReleaseInput, inspectSyntheticReleaseEvidence } from './upgrade-2024-07.mjs';",
'',
"const report = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-aligned-release'));",
'',
"if (report.decision !== 'continue-to-human-deploy-review') {",
" throw new Error('fixed synthetic chain is not aligned');",
'}',
'',
'console.log(report.record.commit.id);',
'console.log(report.record.artifact.digest);',
'console.log(report.record.rollout.state); // proposed-not-executed',
'',
'// The fixture reads only embedded objects. It is not CI, a registry, or a rollout.',
'',
'node web/scripts/upgrade-2024-07.mjs --verify-fixture',
].join('\n');
const mechanismExample = [
"import { createFixedSyntheticReleaseInput, inspectSyntheticReleaseEvidence, planSyntheticReleaseDecision, draftSyntheticControlledRollback } from './upgrade-2024-07.mjs';",
'',
"const report = inspectSyntheticReleaseEvidence(createFixedSyntheticReleaseInput('fixed-aligned-release'));",
'const plan = planSyntheticReleaseDecision(report);',
'const rollback = draftSyntheticControlledRollback(plan);',
'',
"if (rollback.drafted !== true || rollback.migrationBoundary !== 'does-not-reverse-data-and-requires-separate-human-review') {",
" throw new Error('fixed synthetic rollback boundary changed');",
'}',
'',
'console.log(rollback.toReleaseVersion); // 2024.06.3',
'console.log(rollback.deployment); // not-created-or-run',
'',
'// This creates no CI run, deploy, registry entry, cloud change, or production effect.',
].join('\n');
const fieldExample = [
"import { runReleaseEvidenceFixture } from './upgrade-2024-07.mjs';",
'',
'const fixture = runReleaseEvidenceFixture();',
'const failed = Object.entries(fixture.assertions)',
' .filter(([, value]) => value !== true)',
' .map(([name]) => name);',
'',
'if (failed.length > 0) throw new Error(failed.join(", "));',
"console.log('fixed synthetic evidence checks passed');",
'',
'// The four cases are versioned JS records held in memory, not delivery evidence.',
'node web/scripts/upgrade-2024-07.mjs --verify-fixture',
].join('\n');
const practice = revision({
slug: 'editorial-2024-07-practice-release-engineering',
title: 'Инженерия релиза: цепочка доказательств между commit и deploy',
excerpt: 'Практический разбор: как связать версию, artifact, migration и намерение rollout так, чтобы расхождение остановило выпуск до действия и оставило проверяемый rollback.',
cover: '/assets/editorial/2024/release-engineering-2024-delivery-chain.svg',
readingMinutes: 10,
}, [
p('В заявке на выпуск написано <code>2024.07.0</code>. Рядом лежат четыре похожих подписи: commit <code>synthetic-commit-7f4a0c1</code>, artifact с digest <code>sha256:synthetic-2024-07-0-a1</code>, migration plan <code>2024.07.0</code> и rollout card с тем же номером. Это конкретная учебная ситуация, а не журнал поставки: все записи заранее вшиты в fixture. Цена понятна и без реальной среды: если хотя бы одна подпись относится к другой версии, команда может обсуждать ошибку не того кода, ожидать не ту схему и готовить возврат к несуществующей точке.') ,
p('Чаще всего симптом выглядит безобидно: на каждой стадии есть знакомый tag, но нельзя показать одну непрерывную связь от commit до запрошенного artifact. Причина — версия используется как украшение, а не как ключ сравнения; migration и rollout появляются отдельными карточками. Проверка короткая: сравнить parent-ссылку artifact на commit, target migration, digest в rollout и return point. Действие ещё короче: при первом несовпадении остановить review и исправить запись до любого реального deploy. Так рассуждают о контракте релиза, не о любимой CI-системе.'),
h2('Что именно должно совпасть'),
p('Минимальная цепочка не требует огромной платформы. Достаточно назвать четыре объекта и запретить им молча расходиться. Commit фиксирует исходный revision. Artifact хранит собственную версию, digest и declared source commit. Migration plan хранит target release и отдельный compatibility statement. Rollout card повторяет версию, digest и migration version, но остаётся намерением: она ещё не доказывает, что кто-либо получил доступ к среде или что операция была выполнена. Отдельно хранится rollback candidate с прошлой версией и digest, но без обещания вернуть данные назад.'),
p('Слово «доказательство» здесь практическое. Оно не означает криптографическое свойство любого поля и не подменяет attestation. Оно означает, что следующий человек может сравнить одну пару значений с другой и получить конкретное «совпало» либо «стоп». GitHub в июне 2024 уже описывал artifact attestations как отдельный объект, который создаётся для build artifact и проверяется позже. Это полезное разделение ролей: label artifact, statement о происхождении и approval на deploy не должны сливаться в одну строку <code>release=green</code>.'),
figure('/assets/editorial/2024/release-engineering-2024-delivery-chain.svg', 'Четыре versioned synthetic объекта: commit, artifact, migration plan и rollout card. Стрелки требуют точного совпадения commit id, версии, digest и migration version; отдельная нижняя карточка показывает rollback draft без реального deploy и без обещания отменить данные.', 'Цепочка показывает только учебный contract fixed records в памяти. Это не pipeline, artifact registry, трасса CI, облачный экран или результат rollout.'),
h2('Рабочая таблица связей'),
table('Какая связь нужна до human review', ['Переход', 'Нужное равенство', 'Что проверить', 'Что делать при расхождении'], [
['commit → artifact', '<code>artifact.sourceCommitId = commit.id</code>', 'Не только tag, но exact declared commit id.', 'Остановить: artifact нельзя связать с исходным revision.'],
['artifact → migration', '<code>migration.targetReleaseVersion = releaseVersion</code>', 'Версия и declared compatibility относятся к одному release.', 'Остановить: не угадывать, для какого кода написана migration.'],
['artifact → rollout card', '<code>requestedArtifactDigest = artifact.digest</code>', 'Card называет именно записанный immutable digest.', 'Остановить: версия может совпасть, а содержимое — нет.'],
['release → rollback draft', 'return version + return digest известны', 'Есть ли точка возврата и граница для migration.', 'Не обещать data undo; вынести его в отдельный review.'],
]),
p('Таблица полезна тем, что не требует доверять одному центральному полю. Например, tag <code>2024.07.0</code> может стоять и у source, и у artifact, но source commit всё равно обязан совпасть. Аналогично, rollout card не становится надёжной от того, что её version выглядит красиво: digest должен быть тем же digest, который зафиксирован рядом с artifact. Каждая строка даёт проверяемый ответ и отдельный stop condition. Такой stop condition дешевле позднего расследования, потому что меняет только запись, а не среду.'),
h2('Минимальная fixture вместо ложной автоматизации'),
p('Ниже — исполнимая fixture sidecar-пакета. В ней четыре versioned synthetic JS objects заранее записаны в модуле. Функция не принимает URL, имя registry, путь к manifest, credentials, журнал CI или target deploy. Она возвращает teaching report: aligned case ведёт лишь к human deploy review, а mismatch case — к stop. Команда <code>--verify-fixture</code> проверяет invariants этих объектов, но не создаёт artifact, не вызывает runner и не читает ни одного внешнего состояния.'),
code(practiceExample),
p('Именно ограничение input делает пример проверяемым. Если передать похожее поле <code>ciLog</code> или <code>deployTarget</code>, fixture отвергнет input как extra field. Это не защита настоящей системы и не проверка CI. Это защита смысла статьи: учебный объект не должен незаметно начать потреблять реальное evidence, а затем выдавать его за подтверждённый rollout. Даже положительный результат fixture означает лишь равенство fixed records, которые автор уже поместил в память.'),
h2('Маршрут перед настоящим выпуском'),
ol([
'<strong>Назовите release version и owner проверки.</strong> Version без ответственного за сравнение остаётся label. Owner не обязан делать deploy, но обязан знать, какая запись считается исходной.',
'<strong>Свяжите artifact с commit exact идентификатором.</strong> Не заменяйте id названием ветки, последним merge или человеческим описанием. Сначала проверьте declared link, затем решайте, достаточно ли он надёжен для вашей среды.',
'<strong>Опишите migration отдельно.</strong> Укажите target version, compatibility с предыдущей версией и то, что data effect требует отдельного review. Не прячьте это в комментарии к artifact.',
'<strong>Соберите rollout intent без действия.</strong> Card должна повторить release, digest и migration version. Её задача — сделать будущую проверку однозначной, не запустить операцию.',
'<strong>Сравните четыре перехода и остановитесь на первом mismatch.</strong> Не лечите расхождение новой попыткой. Исправьте record, повторите сравнение и только затем передавайте вопрос авторизованному reviewer.',
'<strong>Запишите return point до approval.</strong> Версия и digest для возврата нужны до реального deploy; для migration отдельно зафиксируйте, что code rollback не означает data rollback.',
]),
h2('Почему migration нельзя приклеить в самом конце'),
p('Migration обычно ломает простую картину «вернули прошлый artifact — всё вернулось». В Kubernetes Deployment revision связана с Pod template: rollback возвращает прежний template, а не отменяет любые изменения за его пределами. Это не недостаток Kubernetes; это важная граница, которую стоит назвать в любом delivery design. Если migration уже изменила данные или contract, прежний code может потребовать compatibility window, отдельную reverse procedure либо сознательный отказ от автоматического возврата. Поэтому migration version и её declared compatibility — часть evidence chain, а не последний пункт runbook.'),
p('Практика не требует однажды выбрать универсальный способ миграций. Она требует отделить два вопроса. Первый: какая версия artifact допустима для target data contract? Второй: какое действие по данным разрешено и как проверяется его обратимость? Пока второй вопрос не описан, rollback draft должен говорить ровно правду: «return artifact указан; data undo не обещан». Такая формулировка неприятнее зелёной галочки, зато она не заставляет инженера угадывать после первого симптома.'),
h2('Ограничения и следующий проверяемый шаг'),
p('Эта схема не проверяет реальный Git history, build provenance, signature, runtime configuration, secret, доступ к cluster, состояние database, качество monitoring или факт доставки. SLSA v1.0 даёт vocabulary для provenance и verification, но не отменяет local policy и не превращает произвольный JSON в доказательство. GitHub Artifact Attestations и Kubernetes Deployment documentation описывают конкретные возможности своих систем; из них нельзя вывести, что каждая организация должна применять идентичные поля или один и тот же rollback.'),
p('Следующий проверяемый шаг — взять <strong>один</strong> разрешённый к анализу release record и, не выполняя deploy, заполнить ту же таблицу четырьмя значениями: exact commit id, artifact digest, migration target/compatibility и return version/digest. Затем второй reviewer должен найти либо подтвердить одно равенство в каждой строке. Если у строки нет owner-а, источника или stop action, не расширяйте pipeline: сначала исправьте contract. Это маленький тест, который оставляет понятный результат независимо от выбранного CI/CD инструмента.'),
h2('Историческая граница июля 2024'),
p('Материал использует только первичные официальные источники, реально доступные к июлю 2024: SLSA v1.0 был объявлен stable в апреле 2023, GitHub объявил Artifact Attestations generally available 25 июня 2024, а Kubernetes release-1.30 documentation уже содержала versioned Deployment guidance. Технические факты в тексте намеренно узкие: provenance нужно проверять относительно ожиданий, attestation не равна approval, а Deployment rollback не равен откату данных. Все ids, digest, versions и outcomes выше — fixed synthetic teaching records, не наблюдения о чьей-либо доставке.'),
]);
const mechanism = revision({
slug: 'editorial-2024-07-mechanism-release-engineering',
title: 'Инженерия релиза: механизм evidence gate и контролируемого rollback',
excerpt: 'Модель release evidence: почему одинаковая версия не доказывает одинаковое содержимое, как закрыть вход fixture и почему rollback возвращает только явно названную точку.',
cover: '/assets/editorial/2024/release-engineering-2024-rollback-table.svg',
readingMinutes: 11,
}, [
p('Четыре карточки могут честно носить версию <code>2024.07.0</code> и всё равно описывать разные вещи. В учебном case artifact говорит, что он получен из <code>synthetic-commit-other-91</code>, migration уже нацелена на <code>2024.07.1</code>, а rollout card просит знакомый номер. Это не лог CI и не снимок кластера, а специально зафиксированная модель расхождения. Цена такого расхождения в настоящем процессе — потеря точки, где ещё можно безопасно остановиться: команда начинает спорить о rollout, хотя предметом спора должен быть parent artifact или contract migration.'),
p('Симптом — «везде написана одна версия». Причина — system считает tag достаточным идентификатором и допускает свободные поля между стадиями. Проверка — представить каждую связь как exact predicate, а не как описание: commit id у artifact равен commit id source; rollout digest равен artifact digest; migration target равен release version. Действие — закрыть gate: любой false predicate возвращает <code>stop-and-reconcile-records</code>, а не очередную попытку deploy. Так механизм делает несовпадение видимым до действия и не прячет его за успешным названием job.'),
h2('Не четыре текста, а ориентированные связи'),
p('У цепочки есть направление. Commit не подтверждает artifact автоматически; artifact должен явно указать, на какой commit он ссылается. Migration не должна «следовать последнему main»; ей нужен target release и known compatibility statement. Rollout card не является artifact; она лишь говорит, какой immutable digest и какой migration version намерены рассматривать дальше. Rollback candidate не является кнопкой возврата; он описывает предыдущую versioned point и собственную data boundary. Если поменять направление, легко решить, что rollout status доказывает source, хотя он мог смотреть на совсем другой artifact.'),
p('Эта модель близка к тому, как SLSA отделяет artifact от provenance и verification от простого наличия metadata. Но она намеренно меньше спецификации: не пытается моделировать builder, signature, identity, dependency graph или policy engine. Внутри sidecar нужны только считываемые relations. Такая скромность важна: чем больше real-like полей проглатывает fixture, тем выше риск неявно превратить teaching example в плохую imitation pipeline. Поэтому input состоит из marker, scope, fixed-memory-only mode и case id; сами commit, artifact, migration и rollout нельзя передать извне.'),
figure('/assets/editorial/2024/release-engineering-2024-rollback-table.svg', 'Матрица controlled rollback: code and artifact могут иметь versioned return point, migration требует отдельной compatibility and data review, rollout card не становится операцией, а evidence record остаётся тем, что нужно сравнить до действия.', 'Таблица показывает границы решения: вернуть artifact можно планировать, но fixture не выполняет deploy и не объявляет отмену migration или данных.'),
h2('Таблица predicate и stop condition'),
table('Проверки механизма и их смысл', ['Predicate', 'Когда true', 'Когда gate закрыт', 'Разрешённое действие'], [
['<code>artifact.sourceCommitId === commit.id</code>', 'Artifact declared the same source revision.', 'Нельзя понять, чей source представляет artifact.', 'Стоп и сравнение source record; не менять rollout.'],
['<code>migration.targetReleaseVersion === releaseVersion</code>', 'Plan написан именно для этого release contract.', 'Migration относится к другой версии или неизвестной совместимости.', 'Стоп и отдельный migration review.'],
['<code>rollout.requestedArtifactDigest === artifact.digest</code>', 'Intent называет exact recorded content.', 'Card может указать другой immutable artifact.', 'Стоп и правка intent record.'],
['return version + digest известны', 'Есть названная code/artifact point для возврата.', 'Rollback — лишь слово без target.', 'Не давать approval; написать return draft.'],
['data boundary названа', 'Никто не обещает undo того, что не моделировалось.', 'Code rollback маскируется как data recovery.', 'Разделить review code и data.'],
]),
p('В этой таблице нет строки «все jobs зелёные», потому что она недостаточна для исходной проблемы. Зелёный status может быть полезным evidence в реальном разрешённом процессе, но он не отвечает, какой artifact проверяли, для какого release была migration и что именно вернётся назад. Обратное тоже верно: совпадение полей не доказывает успешный deploy. Gate обязан различать semantic consistency records и execution result. Иначе положительный unit test или status check случайно получит смысл release approval, на который он не рассчитан.'),
h2('Почему closed input — техническая, а не декоративная граница'),
p('Функция <code>createFixedSyntheticReleaseInput()</code> принимает case id, а <code>inspectSyntheticReleaseEvidence()</code> сверяет его с object, созданным в source file. Дополнительные <code>ciLog</code>, <code>deployTarget</code>, другой scope, режим чтения registry или unknown case отвергаются. Это звучит жёстко, но именно так видно, что пример не перепутал модель с интеграцией. В нём нельзя случайно положить реальный digest, URL или credentials, дождаться вывода и назвать его evidence. Все возвращаемые поля прямо говорят <code>not-read</code>, <code>not-run</code> или <code>not-attempted</code>.'),
p('После inspection отдельная функция plan повторно строит canonical report из fixed case и сравнивает весь object через serialised representation. Если подменить decision на continue, заменить releaseVersion либо добавить поле, похожее на deployment id, plan откажет. Это не универсальная защита от подделки в production; в fixture нет identity, signature и storage. Это маленькая проверка model invariant: decision не должен зависеть от изменённого object, который caller собрал после inspection. Даже на уровне обучения это полезнее, чем функция, которая верит любому красивому report.'),
h2('Исполнимый пример: plan и return draft'),
p('Этот пример можно запустить как модуль рядом с sidecar. Он сначала получает report только из embedded case, затем делает decision draft и запрашивает return point. Последняя проверка специально требует фразу о migration boundary. Если позже кто-то заменит её на обещание data undo или добавит действие над deploy, fixture должен изменить verdict и тест остановится. Код не является конфигурацией GitHub Actions, Kubernetes manifest или инструкцией registry; он не содержит client, filesystem access или network call.'),
code(mechanismExample),
p('Контролируемый rollback начинается не с глагола «откатить», а с ответа на два разных вопроса. К какому artifact version можно вернуться? И какие effects migration это не покрывает? Для aligned case fixture возвращает <code>2024.06.3</code> и точный synthetic digest, а строка <code>does-not-reverse-data-and-requires-separate-human-review</code> остаётся частью output. Это ограничение избегает одного из самых дорогих ложных обещаний: что возврат template или binary автоматически превращает данные в прежнюю форму.'),
h2('Маршрут проектирования evidence gate'),
ol([
'<strong>Запишите entity types.</strong> Commit, artifact, migration plan, rollout intent и rollback candidate должны быть различимы даже если используют одну release version.',
'<strong>Выберите immutable relation для каждой стрелки.</strong> Для artifact это exact source commit и digest; для migration — target version и compatibility; для intent — повторение exact digest и migration version.',
'<strong>Назовите отрицательные outcomes.</strong> «Неизвестный artifact», «migration другой версии» и «digest в card иной» — разные stop reasons. Не сливайте их в один retry.',
'<strong>Разведите consistency и execution.</strong> Gate может разрешить human review, но не говорит, что deploy состоялся. Result реальной операции должен жить в другой, разрешённой модели evidence.',
'<strong>Сделайте return point versioned.</strong> Нужны previous release и previous digest, а также понятная граница migration. Отсутствие одного поля — повод не выдавать approval.',
'<strong>Проверьте contract отрицательными cases.</strong> Подмените commit, target migration, requested digest и добавьте real-like input. Каждая ветка обязана останавливаться без side effect.',
]),
h2('Ограничения и граница реальных инструментов'),
p('GitHub Changelog от 25 июня 2024 важен здесь не как рекомендация внедрить конкретный action, а как исторически точный пример того, что artifact attestation и её verification — отдельные операции. SLSA v1.0 важна как stable terminology, где output artifact, provenance и проверка ожиданий не смешиваются. Kubernetes v1.30 важен как напоминание, что Deployment revision и её rollback имеют конкретную область: Pod template. Эти источники не диктуют schema полей sidecar и не дают обещаний о CI, registry, cloud или production данной работы.'),
p('Нельзя честно перенести этот fixture в real release path одной строкой. Для этого потребуются авторизация, threat model, источник истины для commit, правила подписей или digest, review migration, target-specific deploy policy, monitoring и заранее разрешённые данные. Ничего из перечисленного fixture не читает и не заменяет. Положительный его output означает только «в этой закрытой коллекции JS objects predicates совпали». Отрицательный означает «в этой коллекции named relation нарушена». Это хороший unit test мысли, но не change record.'),
h2('Следующий проверяемый шаг'),
p('Выберите одну существующую release form и проведите paper review без доступа к deploy: найдите, где в ней должны жить exact commit id, artifact digest, migration target/compatibility, requested digest и return digest. Для каждого поля добавьте owner и question, который проверяющий способен закрыть. Затем специально составьте три ложные карточки: одна с другим commit, одна с migration следующей версии, одна с другим digest. Если process не умеет назвать три разных stop actions, сначала улучшите форму. Если умеет — только после отдельного разрешения исследуйте автоматизацию.'),
h2('Историческая граница июля 2024'),
p('Все внешние ссылки в этой ревизии — первичные официальные материалы, доступные до конца июля 2024. Они подтверждают лишь узкие исторические свойства: SLSA v1.0 существовала как stable release, GitHub Artifact Attestations стали generally available в июне 2024, Kubernetes v1.30 описывала Deployment revisions и rollback Pod template. Внутренние record names, predicates, negative cases и controlled rollback draft — авторская synthetic модель. Она не измеряет delivery, не получает attestation, не вызывает CI и не утверждает состояние production.'),
]);
const field = revision({
slug: 'editorial-2024-07-field-release-engineering',
title: 'Инженерия релиза: полевой разбор расхождений версии, artifact и migration',
excerpt: 'Разбор четырёх synthetic cases: как отличить mismatch source, migration и rollout intent, когда stop сильнее retry и как оставить return point без ложного обещания data rollback.',
cover: '/assets/editorial/2024/release-engineering-2024-evidence-gate.svg',
readingMinutes: 10,
}, [
p('Представим один field-разбор без реальной среды. Карточка A связывает <code>2024.07.0</code>, <code>synthetic-commit-7f4a0c1</code>, digest <code>sha256:synthetic-2024-07-0-a1</code> и compatible migration. Карточка B держит тот же release number, но artifact ссылается на другой commit. Карточка C держит правильный artifact, зато migration уже помечена <code>2024.07.1</code>. Карточка D просит в rollout card другой digest. Все четыре — fixed versioned objects из memory fixture, не delivery records. Их цена как учебного упражнения в том, что похожие названия перестают успокаивать и заставляют назвать точку расхождения.'),
p('Симптом полевого разбора: участники видят «версия одна», а спорят о том, стоит ли повторить deploy или открыть incident. Причина глубже: разные классы несовпадений ведут к одному общему действию — retry. Проверка должна идти по направлению цепочки и останавливаться на первой broken relation. Действие тогда конкретно: mismatch commit требует исправить artifact evidence, mismatch migration — проверить contract и compatibility, mismatch digest — переписать rollout intent. Никакая из этих веток не даёт fixture права вызвать CI, registry или target environment.'),
h2('Четыре fixed cases как тренировочный материал'),
p('Aligned case полезен не потому, что он «зелёный». Он показывает минимальный набор facts, который можно сравнить: artifact declares source commit, migration target совпадает с release, migration объявлена compatible с return version, а rollout card повторяет exact digest и migration version. Это приводит лишь к <code>continue-to-human-deploy-review</code>. Слово <em>human</em> важно: fixture не даёт approval и не запускает операцию, даже когда все собственные predicates true. Она отделяет согласованность description от факта исполнения.'),
p('Artifact-commit mismatch говорит другую вещь. Версия и даже prefix digest могут выглядеть ожидаемо, но <code>artifact.sourceCommitId</code> не равен source commit. Это не повод угадать, какой из двух объектов «вернее». Правильный outcome — <code>stop-and-reconcile-records</code> с причиной <code>artifact-does-not-bind-commit</code>. Полевой reviewer получает точный вопрос: кто исправляет source relation и какое authorised evidence потом сравнит её снова? Сравнение не заменяется новым build и не маскируется повтором deploy, потому что исходное несоответствие ещё не объяснено.'),
figure('/assets/editorial/2024/release-engineering-2024-evidence-gate.svg', 'Decision gate для fixed synthetic records: совпадают ли version, commit-to-artifact link, migration target and compatibility, rollout digest and migration version. Любое нет ведёт в stop and reconcile; да ведёт только к human review. Нижняя полоса отделяет versioned rollback draft от data undo и реального deploy.', 'Схема — маршрут проверки учебных records. Она не показывает работающий pipeline, registry, CI run, облако, production rollout или delivery log.'),
h2('Рабочая таблица разбора'),
table('Как отличить четыре synthetic outcomes', ['Case', 'Первый broken relation', 'Verdict fixture', 'Следующий проверяемый вопрос'], [
['Aligned <code>2024.07.0</code>', 'Нет: все fixed predicates true.', '<code>continue-to-human-deploy-review</code>', 'Может ли authorised reviewer сравнить реальное evidence отдельно от fixture?'],
['Artifact mismatch', '<code>artifact.sourceCommitId ≠ commit.id</code>', '<code>stop-and-reconcile-records</code>', 'Какой commit действительно должен быть parent artifact?'],
['Migration mismatch', '<code>migration.targetReleaseVersion ≠ releaseVersion</code>', '<code>stop-and-reconcile-records</code>', 'Для какого contract написан plan и есть ли declared compatibility?'],
['Rollout digest mismatch', '<code>requestedArtifactDigest ≠ artifact.digest</code>', '<code>stop-and-reconcile-records</code>', 'Почему intent ссылается на другой immutable content?'],
]),
p('Таблица нужна полевому разбору, чтобы не превратить verdict в эмоцию. У всех stop cases одинаковая дисциплина — ничего не исполнять из fixture, — но причина и владелец следующего вопроса различаются. Artifact mismatch чаще всего начинается у source relation; migration mismatch — у compatibility boundary; digest mismatch — у intent. Когда эти случаи складываются в одну категорию «release failed», потеряется информация, которую надо принести в review. А когда у каждого есть короткое название, переписать или уточнить можно до того, как начнётся дорогое действие.'),
h2('Исполнимая проверка негативных веток'),
p('Функция <code>runReleaseEvidenceFixture()</code> возвращает полный набор fixed samples и map assertions. Она проверяет positive case, три вида расхождений, лишние CI-like и deploy-like input fields, подменённый report, подменённый rollback draft и попытку получить rollback из stop decision. Это важнее одной happy path: release discipline ломается именно там, где один объект слегка изменили, а следующий шаг всё равно продолжился. Команда ниже выполняет только Node code из данного файла; у неё нет client library, credentials, endpoint или файлового input.'),
code(fieldExample),
p('Позитивная assertion здесь не является релизным подтверждением. Она доказывает лишь, что fixed record, который автор положил в source code, согласован сам с собой по написанным predicates. Отрицательная assertion также не доказывает ошибку в реальном процессе. Она доказывает, что модель не пропускает известный bad shape. Это правильный масштаб автоматического примера: тестировать не то, что он не может видеть, а границу, на которой он обязан сказать «я не знаю; остановись и проверь разрешённый источник».'),
h2('Маршрут field triage'),
ol([
'<strong>Зафиксируйте точку чтения.</strong> Сначала выберите один record и явно пометьте, что это input для review, а не execution command. Не смешивайте with current environment state.',
'<strong>Сравните commit и artifact.</strong> Проверяйте exact source relation и digest, не только semantic version. При mismatch запишите named reason и owner исходной записи.',
'<strong>Сравните migration и release.</strong> Target version и compatibility должны быть описаны отдельно. Если migration unknown или относится к будущей версии, маршрут заканчивается stop.',
'<strong>Сравните rollout intent с artifact и migration.</strong> Intent обязан повторить immutable digest и exact migration version. Если он иной, не исправляйте его устно.',
'<strong>Проверьте return point.</strong> До approval назовите return version/digest и ограничение: code/artifact return не равен автоматическому data undo.',
'<strong>Передайте только открытый вопрос.</strong> Aligned card передаётся authorised human reviewer; stop card — владельцу discrepancy. Ни одна ветка fixture не передаётся deployment client.',
]),
h2('Rollback без героической легенды'),
p('Контролируемый rollback не обязан быть мгновенным, чтобы быть полезным. Он обязан быть точным. В fixture для aligned case есть return version <code>2024.06.3</code> и return digest <code>sha256:synthetic-2024-06-3-z9</code>. Функция <code>draftSyntheticControlledRollback()</code> создаёт teaching draft только после canonical plan: она снова сравнивает report с fixed record и отказывает при изменённом action. Draft называет, куда <em>мог бы</em> вернуться code/artifact review, но у него поля <code>deployment: not-created-or-run</code> и <code>productionEffect: not-attempted</code>. Это принципиально не rollout.'),
p('Migration boundary остаётся наиболее важной строкой return draft. В ней прямо сказано <code>does-not-reverse-data-and-requires-separate-human-review</code>. Так field reviewer не теряет вопрос о данных в момент, когда все устали и хотят «просто вернуть вчерашний образ». Kubernetes documentation помогает поставить правильную границу: rollback Deployment связан с его revision Pod template. В вашей системе migration может иметь свои механизмы, lock, compatibility period или запрет на reverse. Именно поэтому field triage обязан отправить data question отдельному owner-у, а не присвоить ему успех вслед за возвратом binary.'),
h2('Ограничения разбора'),
p('Ревизия не анализирует настоящие commit, container image, registry metadata, provenance signature, CI configuration, environment protection rules, database schema, cloud account, traffic, telemetry или production response. Она также не выдаёт SLSA level, не проверяет GitHub attestation и не запускает Kubernetes API. Официальные источники в конце помогают удержать terminology и историческую границу, но не являются доказательством того, что конкретная команда использует эти возможности. Ещё меньше они позволяют заключить, что любой record с digest можно безопасно deploy или rollback.'),
p('У field pattern есть сознательное ограничение масштаба: он хорошо ловит disagreement между named records, но не отвечает, почему values были записаны именно такими. Это следующий слой — authorised evidence collection, trust model, signature verification, migration rehearsal и target-specific policy. Если начать тянуть эти задачи в один fixture, он станет непрозрачной копией pipeline и всё равно не получит реального доступа. Лучше оставить маленький deterministic test рядом с формой и вести реальные процедуры в отдельном, разрешённом контуре.'),
h2('Следующий проверяемый шаг'),
p('Проведите tabletop review на бумаге: один author заполняет пять полей для вымышленного release, второй получает только таблицу связей и должен найти один из трёх заранее внесённых mismatch. Никакие реальные name, URL, digest, migration или deploy targets не нужны. Успех измеряется не скоростью и не зелёным status, а тем, что reviewer называет precise broken relation, stop action, owner следующего вопроса и границу rollback. После этого можно сравнить форму с одним разрешённым рабочим процессом, но не заменять tabletop result production claim.'),
h2('Историческая граница июля 2024'),
p('К июлю 2024 уже были доступны три первичных опоры, на которые ссылается текст: stable SLSA v1.0, GitHub Artifact Attestations generally available с 25 июня 2024 и versioned Kubernetes 1.30 documentation о Deployments. Они не используются для прогнозов, рекомендаций конкретного cloud или утверждений о существующей delivery system. Все case ids, commit ids, digest, migration plans, rollout cards, decisions и return drafts в этой ревизии — versioned fixed synthetic JS objects in memory. Их purpose — проверить reasoning, не имитировать pipeline.'),
]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
function verifyFixture() {
const report = runReleaseEvidenceFixture();
const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
if (failed.length > 0) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
return;
}
const count = Object.keys(report.assertions).length;
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
}
if (process.argv.includes('--verify-fixture')) verifyFixture();
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');