Готовит 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 проще обновить чем писать с нуля.
12 KiB
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 (P0–P4 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_draftstable with EXCLUDE constraint (one pending draft per business_key) + check constraints + FK todictionary_definitions.approval_required(BOOLEAN, default false) +approval_min_role(VARCHAR) ondictionary_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-checksapproval_required=trueon create/update/close → 409draft_requiredwith hintPOST /api/v1/dictionaries/{name}/records/drafts.applyApprovedCreate/Update/Closebypass methods called only byDraftService.approve(validate-on-approve, not on submit).
Bundle integration:
BundleManifest.DictionaryEntryextended with optionalapprovalRequired+approvalMinRole.CuodBundleProperties.honorApprovalPolicy(default false) gates whether manifest values are applied — bundle 1.3.1 shipsapprovalRequired=trueforspacecraft+ground_stationbut prod ignores until explicitly enabled.- Helm:
writer.bundleHonorApprovalPolicyenv-gated; staging overridetrue, prod defaultfalse.
Admin UI:
/reviewsroute — global pool queue (D3 model). Multi-select + bulk approve/reject (parallel mutations, per-draft error reporting)./my-draftsroute — 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.OrdinisApprovalSubmitErrors—outcome=error> 0 for 5m → critical (catches schema bugs that previously masked asdraft_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_requireddefaults tofalse. Existing CRUD on existing dicts is identical. - Bundle policy off in prod: even after deploy,
honorApprovalPolicy=falseinvalues-prodmeans bundle'sapprovalRequired:trueis ignored. Explicit opt-in required. - Records seeding bypasses gate:
CuodBundleImporterwrites viarecordRepository.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 submitGET /api/v1/dictionaries/{name}/records/drafts— pending list per dictGET /api/v1/drafts/{id}— single draftDELETE /api/v1/drafts/{id}— withdraw (maker-only)POST /api/v1/drafts/{id}/approve?comment=— reviewer approvePOST /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)
-
Hibernate
@Enumerated(EnumType.STRING)writes UPPERCASE but Liquibase 0019 declared lowercase check constraint. Every draft insert failed silently — masked by overly broadDataIntegrityViolationExceptioncatch inDraftService.submitmapping everything todraft_already_pending. Phase 3 e2e exposed it; migration 0020 + SQLSTATE 23P01-only mapping fixed it. -
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. -
JDBC
PreparedStatementinterprets every?as a parameter placeholder — JSONB?operator must be rewritten as-> 'key' IS NOT NULLin Liquibase SQL. Same for regex$anchor → useLIKE '%.%'. -
mvn verify -pl '!ordinis-app'skips e2e (testcontainers profile). Migration changes now require localmvn -P e2e -pl ordinis-app -am testpre-push. -
Bundle importer needs env-gate for policy changes — shipping
approvalRequired=truein manifest withouthonorApprovalPolicyflag would auto-flip prod on next deploy. Default-off pattern keeps prod safe; staging opts in. -
Liquibase XSD ordering is strict:
preConditions→comment→ operations.
Cluster ops checklist (operator, not code)
After tag is pushed:
- Click
trigger-deploy-prodin tag pipeline (or now viaglab 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: trueinvalues-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.
- Option A: flip
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