From ceb47eff107c83a2a8fb3086fb230026d4bdb2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 28 May 2026 12:33:47 +0300 Subject: [PATCH] fix(ci/deploy): pre-apply partitioned migrations via postgres superuser + e2e CWD fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workflow run 26564909645 failed: migration 2026_05_27_120000_create_project_routing_snapshots_table hit 'SET ROLE crm_migrator' failure (pgsql conn = crm_app_user, not member of crm_migrator). Failed SET ROLE poisoned transaction → subsequent CREATE TABLE failed SQLSTATE[25P02]. Fix in deploy.yml: New step 'Pre-apply partitioned migrations via postgres superuser' runs CREATE TABLE + indexes + RLS + GRANTs + partitions + system_settings insert via sudo -u postgres psql, then marks migration as ran in migrations table. Idempotent (checks both migrations table AND information_schema). Established prod pattern (memory: paused_at migration 26.05). Side fix in tools/enforce-override-limit.test.mjs: CLI e2e tests used 'node tools/enforce-override-limit.mjs' without cwd, failed when vitest ran from app/. Added cwd: projectRoot via fileURLToPath(import.meta.url). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/deploy.yml | 79 +++++++++++++++++++++++++++ docs/observer/STATUS.md | 18 +++--- tools/enforce-override-limit.test.mjs | 7 ++- 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 35c48cd0..5c2d0117 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -91,6 +91,85 @@ jobs: scp -i ~/.ssh/liderra_deploy -o StrictHostKeyChecking=accept-new \ /tmp/deploy.tgz ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }}:/tmp/deploy.tgz + - name: Pre-apply partitioned migrations via postgres superuser + # Workaround for partitioned-table migrations: + # 2026_05_27_120000_create_project_routing_snapshots_table.php has SET ROLE crm_migrator + # which fails when pgsql connection = crm_app_user (not a member of crm_migrator), + # poisoning the transaction. Established prod pattern (memory: paused_at migration 26.05): + # apply schema via sudo -u postgres psql + insert into migrations table. + # Idempotent — skips if already applied. + run: | + ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'bash -s' <<'REMOTE' + set -euo pipefail + MIG_NAME='2026_05_27_120000_create_project_routing_snapshots_table' + + ALREADY=$(sudo -u postgres psql -d liderra -tAc \ + "SELECT 1 FROM migrations WHERE migration = '${MIG_NAME}' LIMIT 1") + if [ "${ALREADY}" = "1" ]; then + echo "Migration ${MIG_NAME} already in migrations table — skipping." + exit 0 + fi + + TABLE_EXISTS=$(sudo -u postgres psql -d liderra -tAc \ + "SELECT 1 FROM information_schema.tables WHERE table_name='project_routing_snapshots' LIMIT 1") + + if [ "${TABLE_EXISTS}" != "1" ]; then + echo "Applying CREATE TABLE project_routing_snapshots via postgres superuser..." + sudo -u postgres psql -d liderra -v ON_ERROR_STOP=1 <<'PSQL' + BEGIN; + CREATE TABLE project_routing_snapshots ( + snapshot_date DATE NOT NULL, + project_id BIGINT NOT NULL, + tenant_id BIGINT NOT NULL, + daily_limit INT NOT NULL CHECK (daily_limit >= 0), + delivery_days_mask INT NOT NULL CHECK (delivery_days_mask BETWEEN 0 AND 127), + regions INT[] NOT NULL DEFAULT '{}', + signal_type TEXT NOT NULL CHECK (signal_type IN ('call','site','sms')), + signal_identifier TEXT, + sms_senders JSONB, + sms_keyword TEXT, + expected_volume INT NOT NULL CHECK (expected_volume >= 0), + delivered_count INT NOT NULL DEFAULT 0 CHECK (delivered_count >= 0), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY (snapshot_date, project_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE + ) PARTITION BY RANGE (snapshot_date); + ALTER TABLE project_routing_snapshots OWNER TO crm_migrator; + CREATE INDEX project_routing_snapshots_tenant_date_idx + ON project_routing_snapshots (tenant_id, snapshot_date); + CREATE INDEX project_routing_snapshots_signal_idx + ON project_routing_snapshots (snapshot_date, signal_type, lower(signal_identifier)); + ALTER TABLE project_routing_snapshots ENABLE ROW LEVEL SECURITY; + CREATE POLICY project_routing_snapshots_tenant_isolation + ON project_routing_snapshots + USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint); + GRANT SELECT, INSERT, UPDATE ON project_routing_snapshots TO crm_app_user; + GRANT SELECT, INSERT, UPDATE, DELETE ON project_routing_snapshots TO crm_supplier_worker; + CREATE TABLE project_routing_snapshots_y2026_m05 + PARTITION OF project_routing_snapshots + FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); + CREATE TABLE project_routing_snapshots_y2026_m06 + PARTITION OF project_routing_snapshots + FOR VALUES FROM ('2026-06-01') TO ('2026-07-01'); + ALTER TABLE project_routing_snapshots_y2026_m05 OWNER TO crm_migrator; + ALTER TABLE project_routing_snapshots_y2026_m06 OWNER TO crm_migrator; + INSERT INTO system_settings (key, value, type, description, updated_at) + VALUES ('partition_retention_months_project_routing_snapshots', '3', 'int', + 'Retention в месяцах для project_routing_snapshots (90 дней)', NOW()) + ON CONFLICT (key) DO NOTHING; + COMMIT; + PSQL + else + echo "Table project_routing_snapshots already exists but migration not marked — marking only." + fi + + # Mark migration as applied so Laravel migrate skips it + NEXT_BATCH=$(sudo -u postgres psql -d liderra -tAc "SELECT COALESCE(MAX(batch),0)+1 FROM migrations") + sudo -u postgres psql -d liderra -c \ + "INSERT INTO migrations (migration, batch) VALUES ('${MIG_NAME}', ${NEXT_BATCH}) ON CONFLICT (migration) DO NOTHING;" + echo "Marked ${MIG_NAME} as applied (batch ${NEXT_BATCH})" + REMOTE + - name: Extract + run redeploy.sh on prod run: | ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'bash -s' <<'REMOTE' diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 67a2b6fa..905de12c 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-28T09:14:02.222Z +Last updated: 2026-05-28T09:26:13.354Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,13 +8,13 @@ Last updated: 2026-05-28T09:14:02.222Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ⚠️ | 604 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro | +| C5 Observer-coverage | ⚠️ | 607 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro | | C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync | ## Метрики (информационные, не алерты) -- Observer evidence: 604 episodes this month, 0 observer_error markers, 117 PII matches before filter -- Legacy v1 episodes (not in factor analysis): 465 +- Observer evidence: 607 episodes this month, 0 observer_error markers, 117 PII matches before filter +- Legacy v1 episodes (not in factor analysis): 468 - Last /brain-retro: 1 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). @@ -31,9 +31,9 @@ Baseline дисциплины роутера (этап 2 router discipline overh | cleanup | 6 | 0.0% | 0.0% | | refactor | 1 | 0.0% | 0.0% | -Router step distribution: 1: 257, 2: 224, 3: 59, 5: 57 +Router step distribution: 1: 259, 2: 224, 3: 59, 5: 58 -Boundaries applied (ADR / границы): 71 of 597 эпизодов (11.9%). +Boundaries applied (ADR / границы): 71 of 600 эпизодов (11.8%). ## Активные многоэтапные проекты @@ -67,7 +67,7 @@ Episodes since last run: 542 / threshold: 10 ## Reviewer: субагент vs fallback -0 эпизодов проверено из 604. +0 эпизодов проверено из 607. ## Reviewer findings @@ -109,9 +109,9 @@ Episodes since last run: 542 / threshold: 10 | Фраза | За всё время | За сегодня | |---|---|---| -| `recovery` | 775 | 502 ⚠️ | +| `recovery` | 787 | 514 ⚠️ | | `ремонт инфраструктуры` | 185 | 26 ⚠️ | -| `без скилов` | 123 | 65 ⚠️ | +| `без скилов` | 138 | 80 ⚠️ | | `срочно` | 93 | 11 ⚠️ | | `memory dump` | 17 | 9 ⚠️ | | `direct ok` | 6 | 0 | diff --git a/tools/enforce-override-limit.test.mjs b/tools/enforce-override-limit.test.mjs index f559802e..b697858e 100644 --- a/tools/enforce-override-limit.test.mjs +++ b/tools/enforce-override-limit.test.mjs @@ -2,7 +2,10 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { execFileSync } from 'child_process'; import { writeFileSync, mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; -import { join } from 'path'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); import { countTodayUsage, findPhrasesInPrompt, @@ -130,6 +133,7 @@ describe('CLI e2e', () => { const input = JSON.stringify({ prompt: 'обычный prompt без override' }); const out = execFileSync('node', ['tools/enforce-override-limit.mjs'], { input, + cwd: projectRoot, encoding: 'utf-8', timeout: 5000, }); @@ -139,6 +143,7 @@ describe('CLI e2e', () => { it('silent pass when CLI given empty stdin', () => { const out = execFileSync('node', ['tools/enforce-override-limit.mjs'], { input: '', + cwd: projectRoot, encoding: 'utf-8', timeout: 5000, });