Files
mdm-ordinis/docs-internal/releases/v1.1.0.md
T
Zimin A.N. 07c5ca04c9 docs(release): v1.1.0 release notes draft + tag message
Готовит ground для тага v1.1.0 после окончания Approval Workflow v2 soak'а
на staging (≥7 дней с 2026-05-08 → target ≥2026-05-15).

- docs-internal/releases/v1.1.0.md — полные release notes:
  Track A (Approval Workflow v2 P0–P4, 8 commits) + Track B (5 CEO v1
  gap closures), migrations 0017-0020, API surface additions, decisions
  D1-D4 rationale, 6 lessons learned, cluster ops checklist, out-of-scope
  list (deferred к v3 design doc).
- docs-internal/releases/v1.1.0-tag-message.txt — готовый текст для
  `git tag -a v1.1.0 -F docs-internal/releases/v1.1.0-tag-message.txt`.

NOT тагаем сейчас — soak ahead. Documentация заранее = меньше work
после soak conclusion. Если в soak найдутся issues, release notes
проще обновить чем писать с нуля.
2026-05-10 12:31:57 +03:00

12 KiB
Raw Blame History

v1.1.0 — Approval Workflow v2 + CEO v1 plan polish

Status: DRAFT (waiting on staging soak ≥7 days for Approval Workflow v2). Soak window: started 2026-05-08, target tag date ≥2026-05-15. Previous release: v1.0.9 — dict-relationships-v2 epic (4 phases). Range: 13 commits, v1.0.9..main.

Two parallel work streams ship in this release. Both have been fully validated locally + on staging; prod cutover requires soak observation of the Approval workflow under real user load.


Track A — Approval Workflow v2 epic (P0P4 complete)

The maker-checker pattern for sensitive dictionaries. Per-dict opt-in via dictionary_definitions.approval_required. Reviewers approve/reject drafts; live data only changes after approval.

Design doc: ~/.gstack/projects/claude/zimin-main-design-approval-workflow-v2-20260507-121632.md

What's new

Schema (Liquibase 0019, 0020):

  • record_drafts table with EXCLUDE constraint (one pending draft per business_key) + check constraints + FK to dictionary_definitions.
  • approval_required (BOOLEAN, default false) + approval_min_role (VARCHAR) on dictionary_definitions.
  • 0020 fixes Hibernate UPPERCASE enum vs lowercase check constraint mismatch (caught only by Phase 3 e2e — masked в P0/P1 because unit tests used in-memory stub repo).

Lifecycle (DraftService):

  • submit (POST /api/v1/dictionaries/{name}/records/drafts) — maker proposes CREATE/UPDATE/CLOSE.
  • approve (POST /api/v1/drafts/{id}/approve) — SERIALIZABLE tx, COPY → live + outbox event + audit. Self-approve guard (DB constraint + Java).
  • reject (POST /api/v1/drafts/{id}/reject) — reason required (compliance). Status=REJECTED kept (D4 soft-archive).
  • withdraw (DELETE /api/v1/drafts/{id}) — maker-only; status=WITHDRAWN.
  • 6 error codes: draft_required, draft_already_pending, draft_not_pending, self_approve_forbidden, self_reject_forbidden, not_draft_maker, reject_reason_required.

Approval gate (DictionaryRecordService):

  • requireApprovalGate() pre-checks approval_required=true on create/update/close → 409 draft_required with hint POST /api/v1/dictionaries/{name}/records/drafts.
  • applyApprovedCreate/Update/Close bypass methods called only by DraftService.approve (validate-on-approve, not on submit).

Bundle integration:

  • BundleManifest.DictionaryEntry extended with optional approvalRequired + approvalMinRole.
  • CuodBundleProperties.honorApprovalPolicy (default false) gates whether manifest values are applied — bundle 1.3.1 ships approvalRequired=true for spacecraft + ground_station but prod ignores until explicitly enabled.
  • Helm: writer.bundleHonorApprovalPolicy env-gated; staging override true, prod default false.

Admin UI:

  • /reviews route — global pool queue (D3 model). Multi-select + bulk approve/reject (parallel mutations, per-draft error reporting).
  • /my-drafts route — maker self-tracking (all statuses, withdraw button on PENDING).
  • DictionaryEditorDialog — toggle "Require approval" + optional min reviewer role.
  • Records list — "На review" / "Pending review" badge per row when business_key has pending draft.

Phase 4 monitoring (Micrometer → Prometheus):

  • nsi_draft_pending_total — Gauge, global queue depth.
  • nsi_draft_submit_total{dictionary,operation,outcome=ok|already_pending|error} — Counter.
  • nsi_draft_decision_total{dictionary,operation,outcome=approved|rejected|withdrawn} — Counter.
  • nsi_draft_review_duration_seconds — Timer (publishPercentileHistogram for p95/p99).

Prometheus alerts (ordinis.approval rule group):

  • OrdinisApprovalQueueDeep — pending > 50 for 30m → warn.
  • OrdinisApprovalReviewLatencyHigh — p95 review_duration > 8h for 30m → warn.
  • OrdinisApprovalRejectionRateHigh — reject share > 50% for 30m → warn.
  • OrdinisApprovalSubmitErrorsoutcome=error > 0 for 5m → critical (catches schema bugs that previously masked as draft_already_pending).

Operational docs:

  • ordinis-infra/docs/runbooks/approval-workflow-v2.md — debug procedures per alert + emergency disable + soak readiness checklist + SQL queries.

Decisions (auto-applied per design doc)

ID Choice Rationale
D1 A — per-dict policy via dictionary_definitions columns Flexibility without separate config table. UI toggle obvious.
D2 A — legacy CRUD on approval-required dict → 409 draft_required Clean cut. Forces clients to use drafts API.
D3 A — pool model for reviewers No assignment overhead. First approver handles.
D4 A — keep rejected drafts (soft archive) Compliance + maker sees rejection reason.

Backward compatibility

  • Default-off: approval_required defaults to false. Existing CRUD on existing dicts is identical.
  • Bundle policy off in prod: even after deploy, honorApprovalPolicy=false in values-prod means bundle's approvalRequired:true is ignored. Explicit opt-in required.
  • Records seeding bypasses gate: CuodBundleImporter writes via recordRepository.save() directly, not via service — so bundle imports work even on approval-required dicts.

Track B — CEO v1 plan gap closures (5 features)

Closes the last 5 gaps from ~/.gstack/projects/claude/ceo-plans/2026-05-02-nsi-product-foundation.md.

B1 — Webhook event preview tab (5bc82d2)

DictionaryEditorDialog got a new "Event preview" tab generating 5 sample envelope JSONs (RecordCreated/Updated/Deleted, DefinitionCreated/Updated). Integrators see exactly what their HMAC-signed payload will look like before subscribing.

B2 — JMH benchmark module (c11044c)

New ordinis-bench/ Maven module with LineageBenchmark.java measuring hot paths (parseRef, schema traversal). Build via mvn -P bench package -pl ordinis-bench produces uber-jar; manual GitLab CI job maven-bench runs on-demand. Baseline numbers in ordinis-bench/README.md (parseRef ~20ns, schemaTraversal ~417ns).

B3 — Per-dict Redis projection feature flag (CEO plan E2 tiered perf, 0a8275e)

Liquibase 0017 added redis_projection_enabled BOOLEAN on dictionary_definitions (default false). ProjectionWriter early-returns when flag is off — only flipped dicts get materialized to Redis. UI toggle in DictionaryEditorDialog metadata tab. Cuts projection traffic by 10x on cold dicts.

B4 — Time-travel UI (08405fe)

Records list route accepts ?at=<ISO timestamp> searchParam. Datepicker control sends the parameter through useRecords → backend reads via existing at= query string. View any dict as-of any past moment. Bookmarkable URL.

B5 — Smart JSONB search across dictionaries (48df5ce)

Liquibase 0018 added pg_trgm extension + GIN trigram index on dictionary_records.data::text. New endpoint GET /api/v1/search?q=&size&perDict runs native SQL with ROW_NUMBER() OVER (PARTITION BY dictionary_id) per-dict cap. New /search admin UI route shows grouped results by dict with display names. Min 3 chars (trigram limitation).


Migrations applied (Liquibase)

ID Description
0017 redis_projection_enabled column on dictionary_definitions
0018 pg_trgm extension + GIN trigram index on dictionary_records.data::text
0019 record_drafts table + approval_required/approval_min_role columns
0020 Fix record_drafts_status_check + exclusion constraint UPPERCASE values (matches Hibernate enum serialization)

All apply cleanly via testcontainers e2e (28/28 PASS).


API surface (added)

Approval Workflow v2:

  • POST /api/v1/dictionaries/{name}/records/drafts — maker submit
  • GET /api/v1/dictionaries/{name}/records/drafts — pending list per dict
  • GET /api/v1/drafts/{id} — single draft
  • DELETE /api/v1/drafts/{id} — withdraw (maker-only)
  • POST /api/v1/drafts/{id}/approve?comment= — reviewer approve
  • POST /api/v1/drafts/{id}/reject?comment= — reviewer reject (reason required)
  • GET /api/v1/admin/reviews/pending?page&size — global queue (D3 pool)
  • GET /api/v1/drafts/me?page&size — maker self-tracking

CEO v1 gaps:

  • GET /api/v1/search?q=&size&perDict — smart JSONB search

Test counts

Suite Pass Notes
ordinis-rest-api unit 119/119 +30 since v1.0.9 (DraftService, MV, Cascade, Lineage)
ordinis-app e2e (testcontainers) 28/28 +8 ApprovalWorkflowE2ETest
ordinis-admin-ui vitest 89/89 unchanged
helm lint (staging+prod) clean new approval rule group renders
glab ci lint clean maven-bench manual job added

Lessons learned (preserved)

  1. Hibernate @Enumerated(EnumType.STRING) writes UPPERCASE but Liquibase 0019 declared lowercase check constraint. Every draft insert failed silently — masked by overly broad DataIntegrityViolationException catch in DraftService.submit mapping everything to draft_already_pending. Phase 3 e2e exposed it; migration 0020 + SQLSTATE 23P01-only mapping fixed it.

  2. Generic exception catch-all is an anti-pattern when error codes are user-facing. We now alert specifically on nsi_draft_submit_total{outcome=error} to catch future schema mismatches early.

  3. JDBC PreparedStatement interprets every ? as a parameter placeholder — JSONB ? operator must be rewritten as -> 'key' IS NOT NULL in Liquibase SQL. Same for regex $ anchor → use LIKE '%.%'.

  4. mvn verify -pl '!ordinis-app' skips e2e (testcontainers profile). Migration changes now require local mvn -P e2e -pl ordinis-app -am test pre-push.

  5. Bundle importer needs env-gate for policy changes — shipping approvalRequired=true in manifest without honorApprovalPolicy flag would auto-flip prod on next deploy. Default-off pattern keeps prod safe; staging opts in.

  6. Liquibase XSD ordering is strict: preConditionscomment → operations.


Cluster ops checklist (operator, not code)

After tag is pushed:

  • Click trigger-deploy-prod in tag pipeline (or now via glab api -X POST projects/:id/jobs/<id>/play).
  • Verify prod health: curl https://ordinis.k8s.264.nstart.cloud/actuator/health.
  • Verify metrics flowing: kubectl -n ordinis-prod exec deploy/ordinis-app -- curl -s localhost:8080/actuator/prometheus | grep nsi_draft_.
  • Decide rollout strategy:
    • Option A: flip bundleHonorApprovalPolicy: true in values-prod.yaml → bundle 1.3.1 auto-applies approval to spacecraft + ground_station on next deploy.
    • Option B: point per-dict UI flip via DictionaryEditorDialog → operator chooses scope.

Out of scope (deferred to v3 design doc)

  • Multi-step approval chains (3+ levels)
  • SLA auto-escalate timers
  • Email/Slack notifications for reviewers
  • ML anomaly detection for high-risk submissions
  • Federation between MDM clusters

Commit list

361f3b4 feat(approval): bulk approve/reject в /reviews queue (Phase 4 polish)
aedeafc feat(approval): /my-drafts maker self-tracking view + /api/v1/drafts/me
b2e7fd8 feat(approval): Approval Workflow v2 Phase 4 monitoring + pending-review badges
d229c54 feat(approval): bundle-driven approval policy + spacecraft/ground_station soak flag
9ef9b81 feat(approval): Approval Workflow v2 Phase 3 — e2e suite + 0020 case fix
84a4de4 feat(approval): Approval Workflow v2 Phase 2 — admin UI reviewer queue + toggle
2020af9 feat(approval): Approval Workflow v2 Phase 1 — full lifecycle + approval gate
e438c4c feat(approval): Approval Workflow v2 Phase 0 — schema + skeleton API
48df5ce feat(search): smart JSONB search across all dictionaries
08405fe feat(admin-ui): time-travel UI — view dictionary as-of any past date
0a8275e feat(redis-projection): per-dict feature flag (CEO plan E2 tiered perf)
c11044c feat(bench): JMH microbenchmarks module + manual CI job
5bc82d2 feat(admin-ui): webhook event preview tab в DictionaryEditorDialog

Companion ordinis-infra commits since 6f8b92c:

a640339 docs(runbook): Approval Workflow v2 debug runbook + alert runbook_url wiring
bbd4f2a feat(monitoring): Approval Workflow v2 Phase 4 — 4 Prometheus alerts
28b9b93 feat(helm): wire bundleHonorApprovalPolicy + opt staging in soak