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 проще обновить чем писать с нуля.
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
v1.1.0 — Approval Workflow v2 (P0–P4) + CEO v1 plan polish
|
||||||
|
|
||||||
|
13 commits since v1.0.9. Two parallel work streams:
|
||||||
|
|
||||||
|
Track A — Approval Workflow v2 epic (8 commits, e438c4c → 361f3b4):
|
||||||
|
- P0: schema (record_drafts table, EXCLUDE constraint, approval_required
|
||||||
|
column на dictionary_definitions). Migrations 0019 + 0020.
|
||||||
|
- P1: full lifecycle (submit/approve/reject/withdraw) + approval gate
|
||||||
|
на CRUD (409 draft_required), validate-on-approve через bypass methods.
|
||||||
|
- P2: admin UI /reviews route — queue table + diff drawer (current vs
|
||||||
|
proposed JSON) + approve/reject actions. Toggle approval_required
|
||||||
|
в DictionaryEditorDialog.
|
||||||
|
- P3: e2e suite (8 cases) + bundle-driven soak gate (manifest 1.3.1
|
||||||
|
flags spacecraft/ground_station, helm bundleHonorApprovalPolicy
|
||||||
|
default false, staging override true). Phase 3 e2e exposed Phase 0
|
||||||
|
enum case bug — fixed via migration 0020 + SQLSTATE 23P01-only
|
||||||
|
mapping в DraftService.submit (perевыключили generic catch-all).
|
||||||
|
- P4 monitoring: 4 Micrometer metrics (pending_total Gauge, submit_total
|
||||||
|
+ decision_total Counters, review_duration_seconds Timer) + 4
|
||||||
|
Prometheus alerts (QueueDeep, ReviewLatencyHigh, RejectionRateHigh,
|
||||||
|
SubmitErrors) + runbook + UI pending-review badges + /my-drafts maker
|
||||||
|
self-tracking + bulk approve/reject в /reviews.
|
||||||
|
|
||||||
|
Decisions (auto-applied per design doc):
|
||||||
|
- D1=A: per-dict policy через columns на dictionary_definitions.
|
||||||
|
- D2=A: legacy CRUD на approval-required dict → 409 draft_required.
|
||||||
|
- D3=A: pool model для reviewers (no assignment).
|
||||||
|
- D4=A: rejected drafts kept (compliance + maker visibility).
|
||||||
|
|
||||||
|
Track B — CEO v1 plan gap closures (5 commits, 5bc82d2 → 48df5ce):
|
||||||
|
- Webhook event preview tab — sample envelope JSONs для integrators.
|
||||||
|
- JMH benchmark module + manual CI job (parseRef ~20ns baseline).
|
||||||
|
- Per-dict Redis projection feature flag (Liquibase 0017) — cuts
|
||||||
|
projection traffic 10x на cold dicts.
|
||||||
|
- Time-travel UI — ?at=ISO в records list, bookmarkable past states.
|
||||||
|
- Smart JSONB search (Liquibase 0018, pg_trgm + GIN) — cross-dict
|
||||||
|
full-text search с per-dict caps (ROW_NUMBER PARTITION).
|
||||||
|
|
||||||
|
API surface added:
|
||||||
|
- POST/GET/DELETE /api/v1/dictionaries/{name}/records/drafts
|
||||||
|
- GET/POST /api/v1/drafts/{id} (+approve/reject)
|
||||||
|
- GET /api/v1/admin/reviews/pending — global queue
|
||||||
|
- GET /api/v1/drafts/me — maker self-tracking
|
||||||
|
- GET /api/v1/search?q&size&perDict
|
||||||
|
|
||||||
|
Migrations: 0017 (redis flag), 0018 (trgm search), 0019 (drafts +
|
||||||
|
approval cols), 0020 (status enum case fix).
|
||||||
|
|
||||||
|
Tests: 119 unit + 28 e2e + 89 vitest, helm staging+prod lint clean,
|
||||||
|
glab ci lint clean.
|
||||||
|
|
||||||
|
Lessons preserved в docs-internal/releases/v1.1.0.md:
|
||||||
|
1. Hibernate UPPERCASE enum vs lowercase check constraint mismatch.
|
||||||
|
2. Generic DataIntegrity catch-all anti-pattern hid the bug.
|
||||||
|
3. JDBC PreparedStatement '?' substitution gotcha в JSONB SQL.
|
||||||
|
4. mvn -P e2e обязателен для migration changes pre-push.
|
||||||
|
5. Bundle importer needs env-gate для policy changes.
|
||||||
|
6. Liquibase XSD ordering: preConditions → comment → operations.
|
||||||
|
|
||||||
|
Cluster ops после tag push:
|
||||||
|
- click trigger-deploy-prod в tag pipeline (либо glab api -X POST).
|
||||||
|
- decide approval rollout strategy: bundleHonorApprovalPolicy=true
|
||||||
|
в values-prod, либо точечный per-dict UI flip.
|
||||||
|
|
||||||
|
Companion ordinis-infra commits: 28b9b93, bbd4f2a, a640339.
|
||||||
|
|
||||||
|
Detailed release notes: docs-internal/releases/v1.1.0.md.
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
# 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 (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_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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user