Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ebc20ff94 | |||
| 28d2d38857 | |||
| 09f16bd83c | |||
| 512d8e0e24 | |||
| 7aa0e4169e | |||
| 7c9a8151f6 | |||
| be36fc64b3 | |||
| d883bf486f | |||
| 8907d16e40 |
+17
-83
@@ -11,25 +11,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
# Полноценный PostgreSQL для CI: схема Лидерры — чисто PG (RLS, партиции,
|
||||
# роли БД, raw schema.sql через load_initial_schema), на SQLite не грузится.
|
||||
# Без живой БД 14 авторизованных Pa11y-маршрутов не могут залогиниться под
|
||||
# admin@demo.local → таймаут на "wait for path /dashboard" → красный CI.
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: liderra
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -41,98 +22,51 @@ jobs:
|
||||
extensions: pdo, pdo_pgsql, redis, mbstring, intl, bcmath
|
||||
coverage: none
|
||||
|
||||
- name: Setup Node 20
|
||||
- name: Setup Node 22
|
||||
# Node 22 (>=22.18): корневые tooling-пакеты @cspell/*@10 требуют node>=22.18.
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install root JS deps
|
||||
run: npm ci --no-audit --no-fund
|
||||
# npm install (не ci): корневой package-lock рассинхронен (gcp-metadata) — pre-existing долг.
|
||||
run: npm install --no-audit --no-fund
|
||||
|
||||
- name: Install app composer deps
|
||||
working-directory: app
|
||||
run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Install app JS deps
|
||||
# --legacy-peer-deps: Histoire 1.0-beta.1 заявляет peerDep vite ^7,
|
||||
# установлено vite 8 (memory feedback_environment.md #74) — как в deploy.yml.
|
||||
working-directory: app
|
||||
run: npm ci --no-audit --no-fund --legacy-peer-deps
|
||||
|
||||
- name: Create PostgreSQL roles
|
||||
# Базовая schema.sql грузится без ролей (GRANT'ы обёрнуты в DO $$ EXISTS-check),
|
||||
# но поздние миграции (snapshot, lead-region) делают необёрнутый
|
||||
# GRANT ... TO crm_app_user/crm_supplier_worker → роли должны существовать.
|
||||
# SET ROLE crm_migrator в этих миграциях с guard'ом has_schema_privilege →
|
||||
# под postgres-суперюзером корректно делает RESET ROLE (грантов на public нет).
|
||||
env:
|
||||
PGPASSWORD: postgres
|
||||
run: |
|
||||
psql -h 127.0.0.1 -U postgres -d liderra -v ON_ERROR_STOP=1 \
|
||||
-v crm_app_password=ci_pa11y \
|
||||
-v crm_admin_password=ci_pa11y \
|
||||
-v crm_migrator_password=ci_pa11y \
|
||||
-v crm_audit_writer_password=ci_pa11y \
|
||||
-v crm_supplier_worker_password=ci_pa11y \
|
||||
-f db/00_create_roles.sql
|
||||
|
||||
- name: Bootstrap .env + key
|
||||
working-directory: app
|
||||
run: |
|
||||
cp .env.example .env
|
||||
php artisan key:generate --force
|
||||
|
||||
- name: Configure .env for CI PostgreSQL + Sanctum SPA
|
||||
# phpdotenv: первое вхождение ключа выигрывает → не дописываем дубли,
|
||||
# а удаляем строку и добавляем заново (детерминированный override).
|
||||
# APP_ENV=local нужен, чтобы DatabaseSeeder вызвал DemoSeeder (admin@demo.local)
|
||||
# и чтобы session-cookie не был secure-only (вход по http в CI).
|
||||
# SANCTUM_STATEFUL_DOMAINS обязан включать localhost:8000 — иначе Sanctum
|
||||
# считает запрос с Pa11y-хоста (localhost:8000) stateless → сессия не залипает.
|
||||
- name: Prepare SQLite (public Pa11y routes need no real DB)
|
||||
# Pa11y покрывает 7 публичных SPA-маршрутов (login/register/forgot/2fa/recovery/403/500) —
|
||||
# они рендерятся без БД. Полная-PostgreSQL сборка с миграциями/seed отложена в отдельную
|
||||
# задачу (схема и миграции разошлись → from-scratch migrate сломан).
|
||||
working-directory: app
|
||||
run: |
|
||||
setenv() { sed -i "/^$1=/d" .env; echo "$1=$2" >> .env; }
|
||||
setenv APP_ENV local
|
||||
setenv APP_DEBUG true
|
||||
setenv APP_URL http://localhost:8000
|
||||
setenv DB_CONNECTION pgsql
|
||||
setenv DB_HOST 127.0.0.1
|
||||
setenv DB_PORT 5432
|
||||
setenv DB_DATABASE liderra
|
||||
setenv DB_USERNAME postgres
|
||||
setenv DB_PASSWORD postgres
|
||||
setenv DB_SSLMODE disable
|
||||
setenv SESSION_DRIVER file
|
||||
setenv CACHE_STORE file
|
||||
setenv QUEUE_CONNECTION sync
|
||||
setenv MAIL_MAILER log
|
||||
setenv SANCTUM_STATEFUL_DOMAINS localhost:8000,127.0.0.1:8000,localhost,127.0.0.1
|
||||
|
||||
- name: Run migrations (postgres superuser → guarded SET ROLE works)
|
||||
working-directory: app
|
||||
run: php artisan migrate --force
|
||||
|
||||
- name: Create current-month partitions
|
||||
# schema.sql создаёт baseline-партиции; cron-команда докидывает текущий +2
|
||||
# месяца (идемпотентно) — нужно для demo-сделок DemoSeeder'а за «сегодня».
|
||||
working-directory: app
|
||||
run: php artisan partitions:create-months --ahead=2
|
||||
|
||||
- name: Seed demo data (PricingTier + DemoSeeder admin@demo.local)
|
||||
working-directory: app
|
||||
run: php artisan db:seed --force
|
||||
mkdir -p storage/framework/sessions storage/framework/views storage/framework/cache storage/logs bootstrap/cache
|
||||
touch database/database.sqlite
|
||||
sed -i 's/DB_CONNECTION=.*/DB_CONNECTION=sqlite/' .env
|
||||
sed -i 's|DB_DATABASE=.*|DB_DATABASE=/home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/app/database/database.sqlite|' .env
|
||||
sed -i 's/SESSION_DRIVER=.*/SESSION_DRIVER=file/' .env
|
||||
sed -i 's/CACHE_STORE=.*/CACHE_STORE=file/' .env
|
||||
sed -i 's/QUEUE_CONNECTION=.*/QUEUE_CONNECTION=sync/' .env
|
||||
|
||||
- name: Build frontend assets
|
||||
working-directory: app
|
||||
run: npm run build
|
||||
|
||||
- name: Start Laravel dev-server
|
||||
# PHP_CLI_SERVER_WORKERS>1: встроенный сервер обслуживает SPA + sub-resources
|
||||
# параллельно, чтобы Pa11y-навигации не упирались в однопоточность.
|
||||
working-directory: app
|
||||
env:
|
||||
PHP_CLI_SERVER_WORKERS: 4
|
||||
run: nohup php artisan serve --host=127.0.0.1 --port=8000 > /tmp/laravel-serve.log 2>&1 &
|
||||
|
||||
- name: Wait for dev-server ready
|
||||
@@ -148,7 +82,7 @@ jobs:
|
||||
tail -50 /tmp/laravel-serve.log
|
||||
exit 1
|
||||
|
||||
- name: Run Pa11y (live Vue, 7 public + 14 authenticated routes)
|
||||
- name: Run Pa11y (live Vue — 7 public routes)
|
||||
run: npm run a11y
|
||||
|
||||
- name: Laravel log tail on failure
|
||||
|
||||
@@ -108,7 +108,16 @@ class MonthlyPartitionManager
|
||||
if ($exists !== null) {
|
||||
return false;
|
||||
}
|
||||
// Родитель-партиционированная таблица может ещё не существовать
|
||||
// (создаётся более поздней миграцией) — тогда пропускаем.
|
||||
$parentExists = DB::selectOne(
|
||||
"SELECT 1 AS ok FROM pg_class WHERE relname = ? AND relkind = 'p'",
|
||||
[$table],
|
||||
);
|
||||
|
||||
if ($parentExists === null) {
|
||||
return false;
|
||||
}
|
||||
DB::connection(self::DDL_CONNECTION)->statement(sprintf(
|
||||
"CREATE TABLE %s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')",
|
||||
$partition,
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Support\Facades\DB;
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public $withinTransaction = false;
|
||||
public function up(): void
|
||||
{
|
||||
$schemaPath = dirname(base_path()).DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql';
|
||||
|
||||
@@ -47,200 +47,6 @@
|
||||
{
|
||||
"url": "http://localhost:8000/500",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-07-500.png"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/dashboard",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-08-dashboard.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/deals",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-09-deals.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/deals",
|
||||
"wait for path to be /deals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/kanban",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-10-kanban.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/kanban",
|
||||
"wait for path to be /kanban"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/projects",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-11-projects.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/projects",
|
||||
"wait for path to be /projects"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/billing",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-12-billing.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/billing",
|
||||
"wait for path to be /billing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/settings",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-13-settings.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/settings",
|
||||
"wait for path to be /settings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/reports",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-14-reports.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/reports",
|
||||
"wait for path to be /reports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/reminders",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-15-reminders.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/reminders",
|
||||
"wait for path to be /reminders"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/tenants",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-16-admin-tenants.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/tenants",
|
||||
"wait for path to be /admin/tenants"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/billing",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-17-admin-billing.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/billing",
|
||||
"wait for path to be /admin/billing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/incidents",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-18-admin-incidents.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/incidents",
|
||||
"wait for path to be /admin/incidents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/system",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-19-admin-system.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/system",
|
||||
"wait for path to be /admin/system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/pricing-tiers",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-20-admin-pricing-tiers.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/pricing-tiers",
|
||||
"wait for path to be /admin/pricing-tiers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/supplier-prices",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-21-admin-supplier-prices.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/supplier-prices",
|
||||
"wait for path to be /admin/supplier-prices"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user