14c98c37c2
Run 26566803068 created project_routing_snapshots successfully on prod (CREATE TABLE + partitions + RLS + GRANTs all committed). Marker INSERT into migrations table failed: "there is no unique or exclusion constraint matching the ON CONFLICT specification" because Laravel's migrations table has no UNIQUE on `migration` column. Replaced with INSERT...SELECT WHERE NOT EXISTS for idempotency. Table is now LIVE on prod — next workflow run will skip the CREATE block (TABLE_EXISTS check passes) and go straight to the now-fixed marker INSERT. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
230 lines
11 KiB
YAML
230 lines
11 KiB
YAML
name: Deploy to liderra.ru
|
||
|
||
# Запускается вручную через web-интерфейс GitHub или через `gh workflow run`.
|
||
# Решает проблему «дев-машина не достучится по SSH до прод-сервера через YC backbone»:
|
||
# GitHub Actions runner — внешний по отношению к YC, его IP не блокируется тем
|
||
# фильтром что блокирует мой dev-IP `89.144.17.119`.
|
||
#
|
||
# Требуемые secrets (Settings → Secrets and variables → Actions):
|
||
# LIDERRA_SSH_KEY — содержимое приватного ключа `~/.ssh/liderra_deploy`
|
||
# (начинается с `-----BEGIN OPENSSH PRIVATE KEY-----`).
|
||
# Host/user захардкожены — публичная информация, нет смысла в secrets.
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
ref:
|
||
description: 'Branch/tag/SHA для деплоя (по умолчанию main)'
|
||
required: true
|
||
default: 'main'
|
||
type: string
|
||
backfill_snapshot:
|
||
description: 'Запустить snapshot:backfill за сегодня (default yes)'
|
||
required: false
|
||
default: true
|
||
type: boolean
|
||
|
||
jobs:
|
||
deploy:
|
||
name: Deploy code + run redeploy.sh
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
concurrency:
|
||
group: liderra-prod-deploy
|
||
cancel-in-progress: false
|
||
|
||
env:
|
||
LIDERRA_HOST: 111.88.246.137
|
||
LIDERRA_USER: ubuntu
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.inputs.ref }}
|
||
|
||
- name: Setup Node 22
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '22'
|
||
cache: 'npm'
|
||
cache-dependency-path: app/package-lock.json
|
||
|
||
- name: Install frontend deps
|
||
# --legacy-peer-deps: Histoire 1.0-beta.1 заявляет peerDep vite ^7,
|
||
# установлено vite 8 — известный квирк проекта (memory feedback_environment.md #74).
|
||
working-directory: app
|
||
run: npm ci --legacy-peer-deps
|
||
|
||
- name: Build frontend
|
||
working-directory: app
|
||
run: npm run build
|
||
|
||
- name: Verify build artifacts present
|
||
run: |
|
||
test -f app/public/build/manifest.json
|
||
ls app/public/build/assets/ | head -5
|
||
du -sh app/public/build/
|
||
|
||
- name: Create deploy tarball
|
||
run: |
|
||
tar czf /tmp/deploy.tgz \
|
||
--exclude='app/.env' \
|
||
--exclude='app/.env.example' \
|
||
--exclude='app/.env.production' \
|
||
--exclude='app/storage' \
|
||
--exclude='app/vendor' \
|
||
--exclude='app/node_modules' \
|
||
--exclude='app/bootstrap/cache' \
|
||
app db
|
||
ls -lh /tmp/deploy.tgz
|
||
|
||
- 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: Upload tarball to prod
|
||
run: |
|
||
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.
|
||
# Laravel's migrations table has no UNIQUE on `migration` column, so
|
||
# ON CONFLICT doesn't work — use INSERT...SELECT WHERE NOT EXISTS for idempotency.
|
||
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) SELECT '${MIG_NAME}', ${NEXT_BATCH} WHERE NOT EXISTS (SELECT 1 FROM migrations WHERE migration='${MIG_NAME}');"
|
||
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'
|
||
set -euo pipefail
|
||
TS=$(date -u +%Y%m%d-%H%M%S)
|
||
echo "=== Backup current app ==="
|
||
sudo tar czf /home/ubuntu/deploy-backups/app-pre-deploy-${TS}.tgz \
|
||
--exclude='storage' --exclude='vendor' --exclude='node_modules' --exclude='public/build' \
|
||
-C /var/www/liderra app
|
||
ls -lh /home/ubuntu/deploy-backups/app-pre-deploy-${TS}.tgz
|
||
|
||
echo "=== Extract overlay ==="
|
||
cd /var/www/liderra
|
||
sudo tar xzf /tmp/deploy.tgz
|
||
sudo chown -R www-data:www-data /var/www/liderra/app /var/www/liderra/db
|
||
|
||
echo "=== redeploy.sh (composer + migrate + optimize + restart) ==="
|
||
sudo bash /var/www/liderra/redeploy.sh
|
||
|
||
rm -f /tmp/deploy.tgz
|
||
REMOTE
|
||
|
||
- name: Backfill today's snapshot
|
||
if: ${{ github.event.inputs.backfill_snapshot != 'false' }}
|
||
run: |
|
||
ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'bash -s' <<'REMOTE'
|
||
set -e
|
||
cd /var/www/liderra/app
|
||
sudo -u www-data php artisan snapshot:backfill --date=$(date +%Y-%m-%d) || \
|
||
echo "WARN: backfill returned non-zero — проверь вручную"
|
||
REMOTE
|
||
|
||
- name: Smoke tests
|
||
run: |
|
||
ssh -i ~/.ssh/liderra_deploy ${{ env.LIDERRA_USER }}@${{ env.LIDERRA_HOST }} 'bash -s' <<'REMOTE'
|
||
set -e
|
||
cd /var/www/liderra/app
|
||
echo '=== Migrations status (last 5) ==='
|
||
sudo -u www-data php artisan migrate:status 2>&1 | tail -5
|
||
echo '=== Snapshots count (last 3 dates) ==='
|
||
sudo -u postgres psql -d liderra -c "SELECT snapshot_date, COUNT(*) AS rows FROM project_routing_snapshots GROUP BY 1 ORDER BY 1 DESC LIMIT 3;" || true
|
||
echo '=== Service status ==='
|
||
systemctl is-active nginx php8.3-fpm postgresql liderra-queue
|
||
echo '=== Internal portal health ==='
|
||
curl -sf -o /dev/null -w 'https=%{http_code} time=%{time_total}s\n' --max-time 8 https://127.0.0.1/ -k || true
|
||
REMOTE
|
||
|
||
- name: External portal health (from runner)
|
||
run: |
|
||
curl -sf -o /dev/null -w 'external https=%{http_code} time=%{time_total}s\n' \
|
||
--max-time 15 https://liderra.ru/ || echo "external health returned non-zero"
|
||
|
||
- name: Cleanup SSH key
|
||
if: always()
|
||
run: rm -f ~/.ssh/liderra_deploy
|