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

210 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/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.
- `OrdinisApprovalSubmitErrors``outcome=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: `preConditions``comment` → 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
```