From 7f5157b1e31d45c27e5e8a8c8bf30f0e8d3c02f2 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: Fri, 29 May 2026 18:59:10 +0300 Subject: [PATCH] ci(rebuild): parameterized SQL rebuild workflow (audit chain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Принимает partition + from_id + table_kind (activity_log | balance_transactions). Используется для cleanup'а Stage 5 findings 1+2 без перезаписи Laravel AuditRebuildChain (тот не работает на проде из-за permissions crm_supplier_worker — не может SET session_replication_role). Renamed from sql-rebuild-chain-599.yml. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sql-rebuild-audit-chain.yml | 185 ++++++++++++++++++ .github/workflows/sql-rebuild-chain-599.yml | 145 -------------- 2 files changed, 185 insertions(+), 145 deletions(-) create mode 100644 .github/workflows/sql-rebuild-audit-chain.yml delete mode 100644 .github/workflows/sql-rebuild-chain-599.yml diff --git a/.github/workflows/sql-rebuild-audit-chain.yml b/.github/workflows/sql-rebuild-audit-chain.yml new file mode 100644 index 00000000..d10d5e59 --- /dev/null +++ b/.github/workflows/sql-rebuild-audit-chain.yml @@ -0,0 +1,185 @@ +name: SQL rebuild audit hash-chain (per-tenant via postgres) + +# Запускает per-tenant rebuild hash-chain для аудит-партиции через +# sudo -u postgres psql (обход limitation crm_supplier_worker роли — +# она не может SET session_replication_role). +# +# Поддерживает 2 таблицы (Stage 5 finding 1+2): +# - activity_log → ROW(id,tenant_id,user_id,deal_id,event,old_value, +# new_value,context,ip_address,user_agent,NULL::bytea,created_at) +# - balance_transactions → ROW(id,tenant_id,type,amount_rub,amount_leads, +# balance_rub_after,balance_leads_after,description,related_type, +# related_id,user_id,admin_user_id,NULL::bytea,created_at) + +on: + workflow_dispatch: + inputs: + partition: + description: 'Имя партиции, например activity_log_y2026_m05' + required: true + type: string + from_id: + description: 'ID с которого начать пересчёт (включительно)' + required: true + type: string + table_kind: + description: 'activity_log или balance_transactions' + required: true + type: choice + options: + - activity_log + - balance_transactions + confirm_apply: + description: 'Подтверждаю выполнение mutating cleanup' + required: true + default: false + type: boolean + +jobs: + rebuild: + runs-on: ubuntu-latest + timeout-minutes: 10 + + env: + LIDERRA_HOST: 111.88.246.137 + LIDERRA_USER: ubuntu + PARTITION: ${{ github.event.inputs.partition }} + FROM_ID: ${{ github.event.inputs.from_id }} + TABLE_KIND: ${{ github.event.inputs.table_kind }} + + steps: + - name: Confirm check + run: | + if [[ "${{ github.event.inputs.confirm_apply }}" != "true" ]]; then + echo "::error::confirm_apply=true обязателен" + exit 1 + fi + # Sanity: partition must match table_kind + case "$TABLE_KIND" in + activity_log) + if [[ ! "$PARTITION" =~ ^activity_log_y[0-9]{4}_m[0-9]{2}$ ]]; then + echo "::error::partition '$PARTITION' не соответствует table_kind=activity_log" + exit 1 + fi + ;; + balance_transactions) + if [[ ! "$PARTITION" =~ ^balance_transactions_y[0-9]{4}_m[0-9]{2}$ ]]; then + echo "::error::partition '$PARTITION' не соответствует table_kind=balance_transactions" + exit 1 + fi + ;; + *) + echo "::error::table_kind unknown" + exit 1 + ;; + esac + if ! [[ "$FROM_ID" =~ ^[0-9]+$ ]]; then + echo "::error::from_id must be numeric" + exit 1 + fi + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.LIDERRA_SSH_KEY }}" > ~/.ssh/liderra_deploy + chmod 600 ~/.ssh/liderra_deploy + ssh-keyscan -H ${{ env.LIDERRA_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: Execute SQL rebuild on prod + run: | + # Build ROW expression per table_kind + if [[ "$TABLE_KIND" = "activity_log" ]]; then + ROW_EXPR="ROW(t.id, t.tenant_id, t.user_id, t.deal_id, t.event, t.old_value, t.new_value, t.context, t.ip_address, t.user_agent, NULL::bytea, t.created_at)" + else + ROW_EXPR="ROW(t.id, t.tenant_id, t.type, t.amount_rub, t.amount_leads, t.balance_rub_after, t.balance_leads_after, t.description, t.related_type, t.related_id, t.user_id, t.admin_user_id, NULL::bytea, t.created_at)" + fi + + # Build SQL with substituted PARTITION + FROM_ID + ROW_EXPR + cat > /tmp/rebuild.sql <= ${FROM_ID} ORDER BY tenant_id + LOOP + tenant_count := tenant_count + 1; + + SELECT log_hash INTO prev_hash + FROM ${PARTITION} + WHERE tenant_id = tenant_rec.tenant_id AND id < ${FROM_ID} + ORDER BY id DESC LIMIT 1; + + FOR row_rec IN + SELECT id FROM ${PARTITION} + WHERE tenant_id = tenant_rec.tenant_id AND id >= ${FROM_ID} + ORDER BY id + LOOP + UPDATE ${PARTITION} p + SET log_hash = digest( + COALESCE(prev_hash, ''::bytea) + || (SELECT ${ROW_EXPR}::text::bytea FROM ${PARTITION} t WHERE t.id = row_rec.id), + 'sha256' + ) + WHERE p.id = row_rec.id + RETURNING log_hash INTO new_hash; + + prev_hash := new_hash; + updated_count := updated_count + 1; + END LOOP; + END LOOP; + + SET session_replication_role = 'origin'; + RAISE NOTICE 'Rebuild complete: % tenants, % rows updated', tenant_count, updated_count; + END\$\$; + + SELECT 'AFTER: mismatches in partition' AS phase, COUNT(*) AS cnt + FROM ( + WITH ordered AS ( + SELECT id, tenant_id, log_hash AS stored_hash, + LAG(log_hash) OVER (PARTITION BY tenant_id ORDER BY id) AS prev_hash + FROM ${PARTITION} + ) + SELECT o.id + FROM ordered o + WHERE o.stored_hash IS DISTINCT FROM + digest( + COALESCE(o.prev_hash, ''::bytea) + || (SELECT ${ROW_EXPR}::text::bytea FROM ${PARTITION} t WHERE t.id = o.id), + 'sha256' + ) + ) sub; + SQL + + scp -i ~/.ssh/liderra_deploy /tmp/rebuild.sql ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }}:/tmp/rebuild.sql + ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'sudo -u postgres psql -d liderra -f /tmp/rebuild.sql && rm /tmp/rebuild.sql' + + - name: Cleanup SSH key + if: always() + run: rm -f ~/.ssh/liderra_deploy diff --git a/.github/workflows/sql-rebuild-chain-599.yml b/.github/workflows/sql-rebuild-chain-599.yml deleted file mode 100644 index 52f33fd5..00000000 --- a/.github/workflows/sql-rebuild-chain-599.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: SQL rebuild activity_log_y2026_m05 from id=599 (one-off) - -# Один-раз: пересчитать hash-chain в activity_log_y2026_m05 начиная с id=599, -# воспроизводя per-tenant логику AuditRebuildChain.php через PL/pgSQL под -# postgres superuser'ом (обходит limitation crm_supplier_worker роли — -# она не может SET session_replication_role). -# -# Подключается через sudo -u postgres psql, обход Laravel pgsql_supplier -# connection limit. После выполнения этот workflow удалить. - -on: - workflow_dispatch: - inputs: - confirm_apply: - description: 'Подтверждаю выполнение mutating cleanup (обязательно true)' - required: true - default: false - type: boolean - -jobs: - rebuild: - runs-on: ubuntu-latest - timeout-minutes: 10 - - env: - LIDERRA_HOST: 111.88.246.137 - LIDERRA_USER: ubuntu - - steps: - - name: Confirm check - run: | - if [[ "${{ github.event.inputs.confirm_apply }}" != "true" ]]; then - echo "::error::confirm_apply=true обязателен" - exit 1 - fi - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.LIDERRA_SSH_KEY }}" > ~/.ssh/liderra_deploy - chmod 600 ~/.ssh/liderra_deploy - ssh-keyscan -H ${{ env.LIDERRA_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - - - name: Execute SQL rebuild on prod - run: | - ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'sudo -u postgres psql -d liderra -v ON_ERROR_STOP=1' <<'SQL' - -- Pre-verify: count mismatches before - SELECT 'BEFORE: mismatches in partition' AS phase, COUNT(*) AS cnt - FROM ( - WITH ordered AS ( - SELECT id, tenant_id, log_hash AS stored_hash, - LAG(log_hash) OVER (PARTITION BY tenant_id ORDER BY id) AS prev_hash - FROM activity_log_y2026_m05 - ) - SELECT o.id - FROM ordered o - WHERE o.stored_hash IS DISTINCT FROM - digest( - COALESCE(o.prev_hash, ''::bytea) - || (SELECT ROW(t.id, t.tenant_id, t.user_id, t.deal_id, t.event, t.old_value, t.new_value, t.context, t.ip_address, t.user_agent, NULL::bytea, t.created_at)::text::bytea - FROM activity_log_y2026_m05 t WHERE t.id = o.id), - 'sha256' - ) - ) sub; - - -- Per-tenant rebuild от id=599. Mirrors AuditRebuildChain::rebuildScope() PHP логику. - DO $$ - DECLARE - tenant_rec RECORD; - row_rec RECORD; - prev_hash BYTEA; - new_hash BYTEA; - updated_count INT := 0; - tenant_count INT := 0; - BEGIN - -- Disable BEFORE triggers (audit_block_mutation блокирует UPDATE на audit-таблицах) - SET session_replication_role = 'replica'; - - -- Для каждого tenant, у которого есть строки с id >= 599 - FOR tenant_rec IN - SELECT DISTINCT tenant_id - FROM activity_log_y2026_m05 - WHERE id >= 599 - ORDER BY tenant_id - LOOP - tenant_count := tenant_count + 1; - - -- Найти prev_hash = log_hash последней строки этого tenant'а с id<599 - SELECT log_hash INTO prev_hash - FROM activity_log_y2026_m05 - WHERE tenant_id = tenant_rec.tenant_id AND id < 599 - ORDER BY id DESC - LIMIT 1; - -- prev_hash может быть NULL если у tenant'а нет строк до 599 (новый tenant) - - -- Iterate rows of this tenant ordered by id, propagate prev_hash forward - FOR row_rec IN - SELECT id - FROM activity_log_y2026_m05 - WHERE tenant_id = tenant_rec.tenant_id AND id >= 599 - ORDER BY id - LOOP - UPDATE activity_log_y2026_m05 p - SET log_hash = digest( - COALESCE(prev_hash, ''::bytea) - || (SELECT ROW(t.id, t.tenant_id, t.user_id, t.deal_id, t.event, t.old_value, t.new_value, t.context, t.ip_address, t.user_agent, NULL::bytea, t.created_at)::text::bytea - FROM activity_log_y2026_m05 t WHERE t.id = row_rec.id), - 'sha256' - ) - WHERE p.id = row_rec.id - RETURNING log_hash INTO new_hash; - - prev_hash := new_hash; - updated_count := updated_count + 1; - END LOOP; - END LOOP; - - SET session_replication_role = 'origin'; - - RAISE NOTICE 'Rebuild complete: % tenants, % rows updated', tenant_count, updated_count; - END$$; - - -- Post-verify: count mismatches after - SELECT 'AFTER: mismatches in partition' AS phase, COUNT(*) AS cnt - FROM ( - WITH ordered AS ( - SELECT id, tenant_id, log_hash AS stored_hash, - LAG(log_hash) OVER (PARTITION BY tenant_id ORDER BY id) AS prev_hash - FROM activity_log_y2026_m05 - ) - SELECT o.id - FROM ordered o - WHERE o.stored_hash IS DISTINCT FROM - digest( - COALESCE(o.prev_hash, ''::bytea) - || (SELECT ROW(t.id, t.tenant_id, t.user_id, t.deal_id, t.event, t.old_value, t.new_value, t.context, t.ip_address, t.user_agent, NULL::bytea, t.created_at)::text::bytea - FROM activity_log_y2026_m05 t WHERE t.id = o.id), - 'sha256' - ) - ) sub; - SQL - - - name: Cleanup SSH key - if: always() - run: rm -f ~/.ssh/liderra_deploy