feat(approval): bundle-driven approval policy + spacecraft/ground_station soak flag
Approval Workflow v2 Phase 3 — soak prep: - BundleManifest.DictionaryEntry: добавлены опциональные approvalRequired (Boolean) и approvalMinRole (String). - CuodBundleImporter: применяет policy на fresh + on-update + при стабильной схеме (flip без bumping schema_version). Apply ТОЛЬКО когда явно изменился → не штормим UPDATE'ами. - CuodBundleProperties.honorApprovalPolicy (default false): env-gated guard. Bundle ship'ит approvalRequired:true для пилотных dicts, prod игнорирует пока не явно включит. - manifest cuod 1.3.0 → 1.3.1: spacecraft + ground_station получили approvalRequired:true. Без honor flag — no-op. Этого commit'а достаточно для соответствующего helm change в ordinis-infra (bundleHonorApprovalPolicy=true в values-staging) — flip'ает spacecraft/ground_station на staging при следующем deploy. Prod остаётся default=false до окончания soak.
This commit is contained in:
+5
-1
@@ -25,5 +25,9 @@ public record BundleManifest(
|
||||
String schemaVersion,
|
||||
List<String> supportedLocales,
|
||||
String defaultLocale,
|
||||
String recordsResource) {}
|
||||
String recordsResource,
|
||||
/** Approval Workflow v2 (Phase 3 soak): per-dict opt-in maker-checker. */
|
||||
Boolean approvalRequired,
|
||||
/** Optional Keycloak role для reviewer'а (D1=A approval policy). */
|
||||
String approvalMinRole) {}
|
||||
}
|
||||
|
||||
+41
@@ -111,6 +111,15 @@ public class CuodBundleImporter {
|
||||
def.setDisplayName(entry.displayName());
|
||||
def.setSupportedLocales(entryLocales.toArray(String[]::new));
|
||||
def.setDefaultLocale(entryDefault);
|
||||
// Approval Workflow v2: bundle-driven opt-in. Только если entry задаёт
|
||||
// явное значение — иначе оставляем текущее (не сбрасываем UI-toggle'и).
|
||||
// Honor только когда props.honorApprovalPolicy=true (Phase 3 soak gate).
|
||||
if (props.honorApprovalPolicy() && entry.approvalRequired() != null) {
|
||||
def.setApprovalRequired(entry.approvalRequired());
|
||||
}
|
||||
if (props.honorApprovalPolicy() && entry.approvalMinRole() != null) {
|
||||
def.setApprovalMinRole(entry.approvalMinRole());
|
||||
}
|
||||
DictionaryDefinition saved = repository.save(def);
|
||||
|
||||
var event = new DictionaryDefinitionUpdated(
|
||||
@@ -128,6 +137,29 @@ public class CuodBundleImporter {
|
||||
def = saved;
|
||||
} else {
|
||||
log.debug("Справочник '{}' v{} актуален, schema не трогаем", entry.name(), def.getSchemaVersion());
|
||||
// Approval Workflow v2 soak: даже если schema_version неизменилась,
|
||||
// bundle-driven opt-in требует, чтобы Boolean approvalRequired в
|
||||
// manifest применился к существующему dict (Phase 3 — flip без bumping
|
||||
// schema). Apply ТОЛЬКО при изменении — иначе spam UPDATE'ов. Honor
|
||||
// только когда props.honorApprovalPolicy=true (env-gated).
|
||||
if (props.honorApprovalPolicy()) {
|
||||
boolean changed = false;
|
||||
if (entry.approvalRequired() != null
|
||||
&& entry.approvalRequired() != def.isApprovalRequired()) {
|
||||
def.setApprovalRequired(entry.approvalRequired());
|
||||
changed = true;
|
||||
}
|
||||
if (entry.approvalMinRole() != null
|
||||
&& !java.util.Objects.equals(entry.approvalMinRole(), def.getApprovalMinRole())) {
|
||||
def.setApprovalMinRole(entry.approvalMinRole());
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
def = repository.save(def);
|
||||
log.info("Справочник '{}' approval policy обновлён: required={} role={}",
|
||||
def.getName(), def.isApprovalRequired(), def.getApprovalMinRole());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Records seed запускаем всегда — они идемпотентны (skip on existing businessKey).
|
||||
importRecords(manifest, entry, def);
|
||||
@@ -142,6 +174,15 @@ public class CuodBundleImporter {
|
||||
fresh.setBundle(manifest.bundle());
|
||||
fresh.setSupportedLocales(entryLocales.toArray(String[]::new));
|
||||
fresh.setDefaultLocale(entryDefault);
|
||||
// Approval Workflow v2: per-dict opt-in через manifest (Phase 3 soak).
|
||||
// Honor только когда props.honorApprovalPolicy=true — staging opts in,
|
||||
// prod игнорирует пока явно не включит.
|
||||
if (props.honorApprovalPolicy() && entry.approvalRequired() != null) {
|
||||
fresh.setApprovalRequired(entry.approvalRequired());
|
||||
}
|
||||
if (props.honorApprovalPolicy() && entry.approvalMinRole() != null) {
|
||||
fresh.setApprovalMinRole(entry.approvalMinRole());
|
||||
}
|
||||
DictionaryDefinition saved = repository.save(fresh);
|
||||
|
||||
var event = new DictionaryDefinitionCreated(
|
||||
|
||||
+7
-1
@@ -6,7 +6,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
public record CuodBundleProperties(
|
||||
boolean autoImport,
|
||||
boolean updateExistingSchema,
|
||||
String resourceRoot) {
|
||||
String resourceRoot,
|
||||
/**
|
||||
* Approval Workflow v2 (Phase 3 soak): применять {@code approvalRequired} /
|
||||
* {@code approvalMinRole} из manifest.json при импорте. Default {@code false}
|
||||
* — bundle ship'ит флаги для staging, prod игнорирует пока не явно включат.
|
||||
*/
|
||||
boolean honorApprovalPolicy) {
|
||||
|
||||
public CuodBundleProperties {
|
||||
if (resourceRoot == null) resourceRoot = "classpath:/bundles/cuod/";
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"bundle": "cuod",
|
||||
"displayName": "ЦУОД ДЗЗ",
|
||||
"description": "Bundle справочников для Центра Управления Обработкой Данных ДЗЗ. Anchor v1.",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"supportedLocales": ["ru-RU", "en-US"],
|
||||
"defaultLocale": "ru-RU",
|
||||
"dictionaries": [
|
||||
{ "name": "spacecraft", "displayName": "Космические аппараты", "description": "Каталог КА (космических аппаратов) ДЗЗ-миссий", "scope": "PUBLIC", "schemaResource": "spacecraft.schema.json", "schemaVersion": "1.1.0" },
|
||||
{ "name": "spacecraft", "displayName": "Космические аппараты", "description": "Каталог КА (космических аппаратов) ДЗЗ-миссий", "scope": "PUBLIC", "schemaResource": "spacecraft.schema.json", "schemaVersion": "1.1.0", "approvalRequired": true },
|
||||
{ "name": "satellite_type", "displayName": "Типы КА", "description": "Классификация типов космических аппаратов по миссиям и характеристикам", "scope": "PUBLIC", "schemaResource": "satellite_type.schema.json", "schemaVersion": "1.0.0" },
|
||||
{ "name": "spacecraft_status", "displayName": "Статусы КА", "description": "Жизненный цикл КА: planned → operational → decommissioned/lost.", "scope": "PUBLIC", "schemaResource": "spacecraft_status.schema.json", "schemaVersion": "1.0.0" },
|
||||
{ "name": "ground_station", "displayName": "Наземные станции", "description": "Каталог наземных пунктов приёма и управления", "scope": "PUBLIC", "schemaResource": "ground_station.schema.json", "schemaVersion": "1.0.0" },
|
||||
{ "name": "ground_station", "displayName": "Наземные станции", "description": "Каталог наземных пунктов приёма и управления", "scope": "PUBLIC", "schemaResource": "ground_station.schema.json", "schemaVersion": "1.0.0", "approvalRequired": true },
|
||||
{ "name": "antenna", "displayName": "Антенны", "description": "Антенны наземных станций с G/T и поддерживаемыми диапазонами.", "scope": "PUBLIC", "schemaResource": "antenna.schema.json", "schemaVersion": "1.0.0" },
|
||||
{ "name": "mission", "displayName": "Миссии / программы", "description": "Программы ДЗЗ — Ресурс-П, Канопус-В, Sentinel и т.д. Группируют семейства КА.", "scope": "PUBLIC", "schemaResource": "mission.schema.json", "schemaVersion": "1.0.0" },
|
||||
{ "name": "instrument", "displayName": "Инструменты съёмки", "description": "Полезная нагрузка / сенсоры на борту КА (Геотон-Л1, MODIS, C-SAR, ...)", "scope": "PUBLIC", "schemaResource": "instrument.schema.json", "schemaVersion": "1.0.0" },
|
||||
|
||||
Reference in New Issue
Block a user