diff --git a/.gitea/workflows/deploy-stage.yml b/.gitea/workflows/deploy-stage.yml index e85ccad..a68f2b9 100644 --- a/.gitea/workflows/deploy-stage.yml +++ b/.gitea/workflows/deploy-stage.yml @@ -1,10 +1,10 @@ name: Deploy stage (admin) -# Auto после успешного CI на master: тот же sha-*, плюс floating :stage. +# Auto после успешного E2E IFT на master: promote sha-* → :stage и SSH deploy. # Product version (VERSION) не бампается — только sha в рантайме/registry. on: workflow_run: - workflows: [CI] + workflows: ["E2E IFT (admin)"] types: [completed] branches: [master, main] workflow_dispatch: diff --git a/.gitea/workflows/e2e-ift.yml b/.gitea/workflows/e2e-ift.yml index 118d400..6e9b4b3 100644 --- a/.gitea/workflows/e2e-ift.yml +++ b/.gitea/workflows/e2e-ift.yml @@ -1,9 +1,7 @@ -name: E2E IFT smoke (admin) +name: E2E IFT (admin) -# После успешного Deploy IFT — smoke против живого IFT (без API-моков). -# Учётки берутся с хоста IFT из /opt/eventhub-ift/.env -# (SMOKE_ADMIN_* → fallback ADMIN_SUPER_*; см. EventHubDevOps/ift/.env.example). -# Опц. secret E2E_IFT_BASE_URL (default https://admin-ui.ift.eventhub.local). +# После успешного Deploy IFT — полный UI-сьют (mock-тесты на живом SPA + smoke API). +# Учётки smoke — из /opt/eventhub-ift/.env по SSH (SMOKE_ADMIN_* / ADMIN_SUPER_*). on: workflow_run: workflows: ["Deploy IFT (admin)"] @@ -15,12 +13,12 @@ concurrency: cancel-in-progress: false jobs: - ift-smoke: + ift-e2e: if: | github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 with: @@ -40,33 +38,9 @@ jobs: SSH_HOST: ${{ secrets.IFT_SSH_HOST }} SSH_USER: ${{ secrets.IFT_SSH_USER }} SSH_KEY: ${{ secrets.IFT_SSH_PRIVATE_KEY }} - run: | - set -euo pipefail - KEY=$(mktemp) - printf '%s\n' "$SSH_KEY" > "$KEY" - chmod 600 "$KEY" - # Источник истины — env на стенде (не отдельные Gitea E2E secrets). - REMOTE=$(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ - "${SSH_USER}@${SSH_HOST}" \ - "grep -E '^(SMOKE_ADMIN_EMAIL|SMOKE_ADMIN_PASSWORD|ADMIN_SUPER_EMAIL|ADMIN_SUPER_PASSWORD)=' /opt/eventhub-ift/.env || true") - rm -f "$KEY" - EMAIL=$(printf '%s\n' "$REMOTE" | sed -n 's/^SMOKE_ADMIN_EMAIL=//p' | head -1) - PASS=$(printf '%s\n' "$REMOTE" | sed -n 's/^SMOKE_ADMIN_PASSWORD=//p' | head -1) - if [ -z "$EMAIL" ]; then - EMAIL=$(printf '%s\n' "$REMOTE" | sed -n 's/^ADMIN_SUPER_EMAIL=//p' | head -1) - fi - if [ -z "$PASS" ]; then - PASS=$(printf '%s\n' "$REMOTE" | sed -n 's/^ADMIN_SUPER_PASSWORD=//p' | head -1) - fi - if [ -z "$EMAIL" ] || [ -z "$PASS" ]; then - echo "::error::В /opt/eventhub-ift/.env нет SMOKE_ADMIN_* / ADMIN_SUPER_*" - exit 1 - fi - echo "SMOKE_ADMIN_EMAIL=${EMAIL}" >> "$GITHUB_ENV" - echo "SMOKE_ADMIN_PASSWORD=${PASS}" >> "$GITHUB_ENV" - echo "Loaded smoke admin email: ${EMAIL}" + run: bash scripts/load-stand-smoke-env.sh ift - - name: IFT smoke + - name: E2E IFT (full UI suite) env: E2E_TARGET: ift E2E_IFT_BASE_URL: ${{ secrets.E2E_IFT_BASE_URL }} diff --git a/.gitea/workflows/e2e-stage.yml b/.gitea/workflows/e2e-stage.yml new file mode 100644 index 0000000..f279764 --- /dev/null +++ b/.gitea/workflows/e2e-stage.yml @@ -0,0 +1,55 @@ +name: E2E stage (admin) + +# После успешного Deploy stage — полный UI-сьют (mock-тесты на живом SPA + smoke API). +# Учётки smoke — из /opt/eventhub-stage/.env по SSH (SMOKE_ADMIN_* / ADMIN_SUPER_*). +on: + workflow_run: + workflows: ["Deploy stage (admin)"] + types: [completed] + workflow_dispatch: + +concurrency: + group: eventhub-frontadmin-e2e-stage + cancel-in-progress: false + +jobs: + stage-e2e: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - run: npm ci + + - name: Install Playwright browser + run: bash scripts/install-playwright-chromium.sh + + - name: Load smoke credentials from stage .env + env: + SSH_HOST: ${{ secrets.STAGE_SSH_HOST }} + SSH_USER: ${{ secrets.STAGE_SSH_USER }} + SSH_KEY: ${{ secrets.STAGE_SSH_PRIVATE_KEY }} + run: bash scripts/load-stand-smoke-env.sh stage + + - name: E2E stage (full UI suite) + env: + E2E_TARGET: stage + E2E_STAGE_BASE_URL: ${{ secrets.E2E_STAGE_BASE_URL }} + run: npm run test:e2e:stage + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v3 + with: + name: playwright-stage-report + path: playwright-report/ + retention-days: 7 diff --git a/e2e/TESTIDS.md b/e2e/TESTIDS.md index ea91c2f..7d28733 100644 --- a/e2e/TESTIDS.md +++ b/e2e/TESTIDS.md @@ -29,7 +29,8 @@ Helper: `src/lib/testid.ts`. | Команда | Назначение | |---------|------------| | `npm run test:e2e` | mock-suite (CI / PR), API через Playwright route | -| `npm run test:e2e:ift` | IFT smoke (живой API), см. ниже | +| `npm run test:e2e:ift` | IFT: полный UI-сьют на живом SPA + smoke API | +| `npm run test:e2e:stage` | stage: полный UI-сьют на живом SPA + smoke API | ### IFT / stage учётки @@ -42,4 +43,4 @@ Helper: `src/lib/testid.ts`. Локально: `set -a && source /path/to/ift/.env && set +a && npm run test:e2e:ift`. -Workflow `e2e-ift.yml` после Deploy IFT читает те же ключи по SSH с `/opt/eventhub-ift/.env` (нужны уже имеющиеся `IFT_SSH_*`). Опц. secret `E2E_IFT_BASE_URL`. +Workflow `e2e-ift.yml` / `e2e-stage.yml` читают те же ключи по SSH с `/opt/eventhub-{ift,stage}/.env`. Опц. secrets `E2E_IFT_BASE_URL`, `E2E_STAGE_BASE_URL`. diff --git a/package.json b/package.json index 5be9b4e..8181b28 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test --project=mock", - "test:e2e:ift": "E2E_TARGET=ift playwright test --project=ift-smoke" + "test:e2e:ift": "E2E_TARGET=ift playwright test --project=ift-ui --project=ift-smoke", + "test:e2e:stage": "E2E_TARGET=stage playwright test --project=stage-ui --project=stage-smoke" }, "dependencies": { "@hookform/resolvers": "^5.2.2", diff --git a/playwright.config.ts b/playwright.config.ts index 19eeb74..29e0902 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,13 @@ import { defineConfig, devices } from '@playwright/test'; const port = Number(process.env.E2E_PORT || 4173); const baseURL = process.env.E2E_BASE_URL || `http://127.0.0.1:${port}`; +const iftBaseURL = process.env.E2E_IFT_BASE_URL || 'https://admin-ui.ift.eventhub.local'; +const stageBaseURL = process.env.E2E_STAGE_BASE_URL || 'https://admin-ui.stage.eventhub.local'; +const liveTarget = process.env.E2E_TARGET; +const liveChrome = { + ...devices['Desktop Chrome'], + ignoreHTTPSErrors: true, +}; export default defineConfig({ testDir: './e2e/tests', @@ -23,23 +30,33 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, testIgnore: /ift\//, }, + { + name: 'ift-ui', + use: { ...liveChrome, baseURL: iftBaseURL }, + testIgnore: /ift\//, + }, { name: 'ift-smoke', - use: { - ...devices['Desktop Chrome'], - baseURL: process.env.E2E_IFT_BASE_URL || 'https://admin-ui.ift.eventhub.local', - ignoreHTTPSErrors: true, - }, + use: { ...liveChrome, baseURL: iftBaseURL }, + testMatch: /ift\//, + }, + { + name: 'stage-ui', + use: { ...liveChrome, baseURL: stageBaseURL }, + testIgnore: /ift\//, + }, + { + name: 'stage-smoke', + use: { ...liveChrome, baseURL: stageBaseURL }, testMatch: /ift\//, }, ], - webServer: - process.env.E2E_TARGET === 'ift' - ? undefined - : { - command: `npm run preview -- --host 127.0.0.1 --port ${port}`, - url: baseURL, - reuseExistingServer: !process.env.CI, - timeout: 120_000, - }, + webServer: liveTarget + ? undefined + : { + command: `npm run preview -- --host 127.0.0.1 --port ${port}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, }); diff --git a/scripts/load-stand-smoke-env.sh b/scripts/load-stand-smoke-env.sh new file mode 100644 index 0000000..4bc030d --- /dev/null +++ b/scripts/load-stand-smoke-env.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Load SMOKE_ADMIN_* (fallback ADMIN_SUPER_*) from stand .env via SSH into GITHUB_ENV. +# Usage: load-stand-smoke-env.sh +set -euo pipefail + +STAND="${1:?stand: ift|stage}" +case "${STAND}" in + ift) + SSH_HOST="${SSH_HOST:?IFT_SSH_HOST required}" + SSH_USER="${SSH_USER:?IFT_SSH_USER required}" + SSH_KEY="${SSH_KEY:?IFT_SSH_PRIVATE_KEY required}" + ENV_FILE="/opt/eventhub-ift/.env" + ;; + stage) + SSH_HOST="${SSH_HOST:?STAGE_SSH_HOST required}" + SSH_USER="${SSH_USER:?STAGE_SSH_USER required}" + SSH_KEY="${SSH_KEY:?STAGE_SSH_PRIVATE_KEY required}" + ENV_FILE="/opt/eventhub-stage/.env" + ;; + *) + echo "Unknown stand: ${STAND}" + exit 1 + ;; +esac + +KEY="$(mktemp)" +printf '%s\n' "${SSH_KEY}" > "${KEY}" +chmod 600 "${KEY}" + +REMOTE="$(ssh -i "${KEY}" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ + "${SSH_USER}@${SSH_HOST}" \ + "grep -E '^(SMOKE_ADMIN_EMAIL|SMOKE_ADMIN_PASSWORD|ADMIN_SUPER_EMAIL|ADMIN_SUPER_PASSWORD)=' ${ENV_FILE} || true")" +rm -f "${KEY}" + +EMAIL="$(printf '%s\n' "${REMOTE}" | sed -n 's/^SMOKE_ADMIN_EMAIL=//p' | head -1)" +PASS="$(printf '%s\n' "${REMOTE}" | sed -n 's/^SMOKE_ADMIN_PASSWORD=//p' | head -1)" +if [[ -z "${EMAIL}" ]]; then + EMAIL="$(printf '%s\n' "${REMOTE}" | sed -n 's/^ADMIN_SUPER_EMAIL=//p' | head -1)" +fi +if [[ -z "${PASS}" ]]; then + PASS="$(printf '%s\n' "${REMOTE}" | sed -n 's/^ADMIN_SUPER_PASSWORD=//p' | head -1)" +fi +if [[ -z "${EMAIL}" || -z "${PASS}" ]]; then + echo "::error::В ${ENV_FILE} нет SMOKE_ADMIN_* / ADMIN_SUPER_*" + exit 1 +fi + +if [[ -n "${GITHUB_ENV:-}" ]]; then + echo "SMOKE_ADMIN_EMAIL=${EMAIL}" >> "${GITHUB_ENV}" + echo "SMOKE_ADMIN_PASSWORD=${PASS}" >> "${GITHUB_ENV}" +fi +echo "Loaded smoke admin email: ${EMAIL}"