Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/pr-multisig-v1-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ on:
default: ""
type: string

# Serialize against the Playwright e2e workflow: both broadcast from the same
# preprod CI wallets, so they must never run at the same time. A constant group
# (not per-PR) ensures mutual exclusion across PRs too. cancel-in-progress:false
# means runs queue rather than cancel each other.
concurrency:
group: ci-preprod-wallets
cancel-in-progress: false

jobs:
multisig-v1-smoke:
if: github.repository == 'MeshJS/multisig'
Expand Down
146 changes: 146 additions & 0 deletions .github/workflows/pr-playwright-browser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: PR Playwright Browser Tests

on:
pull_request:
branches:
- main
- preprod
workflow_dispatch:
inputs:
transfer_lovelace:
description: "Lovelace amount for ring transfer legs"
required: false
default: "2000000"
type: string

# Serialize against the Multisig v1 smoke workflow: both broadcast from the same
# preprod CI wallets, so they must never run at the same time. A constant group
# (not per-PR) ensures mutual exclusion across PRs too. cancel-in-progress:false
# means runs queue rather than cancel each other.
concurrency:
group: ci-preprod-wallets
cancel-in-progress: false

jobs:
playwright-browser:
if: github.repository == 'MeshJS/multisig'
runs-on: ubuntu-latest
timeout-minutes: 60
env:
CI_JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
CI_MNEMONIC_1: ${{ secrets.CI_MNEMONIC_1 }}
CI_MNEMONIC_2: ${{ secrets.CI_MNEMONIC_2 }}
CI_MNEMONIC_3: ${{ secrets.CI_MNEMONIC_3 }}
CI_BLOCKFROST_PREPROD_API_KEY: ${{ secrets.CI_BLOCKFROST_PREPROD_API_KEY }}
CI_NETWORK_ID: "0"
CI_WALLET_TYPES: "legacy,hierarchical,sdk"
CI_NUM_REQUIRED_SIGNERS: "2"
CI_TRANSFER_LOVELACE: ${{ github.event_name == 'workflow_dispatch' && inputs.transfer_lovelace || '2000000' }}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Validate required secrets
shell: bash
run: |
missing=()
[[ -n "$CI_JWT_SECRET" ]] || missing+=("CI_JWT_SECRET")
[[ -n "$CI_MNEMONIC_1" ]] || missing+=("CI_MNEMONIC_1")
[[ -n "$CI_MNEMONIC_2" ]] || missing+=("CI_MNEMONIC_2")
[[ -n "$CI_MNEMONIC_3" ]] || missing+=("CI_MNEMONIC_3")
[[ -n "$CI_BLOCKFROST_PREPROD_API_KEY" ]] || missing+=("CI_BLOCKFROST_PREPROD_API_KEY")
if [[ "${#missing[@]}" -gt 0 ]]; then
echo "Missing required secrets: ${missing[*]}"
echo "Set these in repo settings before running the Playwright browser workflow."
exit 1
fi

- name: Pull base images (with retry)
shell: bash
run: |
for i in 1 2 3; do
docker pull node:20-alpine && break
echo "Pull attempt $i failed, retrying in 30s..."
sleep 30
done

- name: Build CI containers
shell: bash
run: docker compose -f docker-compose.playwright.yml build app bootstrap-runner playwright-runner

- name: Start Postgres + App containers
shell: bash
run: docker compose -f docker-compose.playwright.yml up -d postgres app

- name: Wait for app healthcheck
shell: bash
run: |
for i in {1..60}; do
status=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$(docker compose -f docker-compose.playwright.yml ps -q app)")
if [[ "$status" == "healthy" ]]; then
echo "App is healthy."
exit 0
fi
sleep 2
done
echo "App failed to become healthy in time."
docker compose -f docker-compose.playwright.yml ps
exit 1

- name: Run CI wallet bootstrap
shell: bash
run: |
docker compose -f docker-compose.playwright.yml --profile playwright \
run --rm bootstrap-runner

- name: Run Playwright ring transfer tests
shell: bash
run: |
docker compose -f docker-compose.playwright.yml --profile playwright \
run --rm --no-deps playwright-runner

- name: Dump container logs on failure
if: failure()
shell: bash
run: |
docker compose -f docker-compose.playwright.yml logs --no-color \
| sed -E 's/(Bearer )[A-Za-z0-9._-]+/\1[REDACTED]/g' \
| sed -E 's/("token"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\2/g' \
| sed -E 's/("secret"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\2/g' \
| sed -E 's/("mnemonic([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
| sed -E 's/("private([[:alnum:]_-]*)?key([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
| sed -E 's/("signing([[:alnum:]_-]*)?key([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
| sed -E 's/("seed([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
| sed -E 's/("xprv([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
| sed -E 's/(ed25519e?_sk[[:alnum:]_]+)/[REDACTED]/gI' \
| sed -E 's/(xprv[[:alnum:]]+)/[REDACTED]/gI' \
> docker-compose-playwright.log

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: docker-compose-playwright-logs
path: docker-compose-playwright.log

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: ci-artifacts/playwright-report/
if-no-files-found: warn

- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: ci-artifacts/playwright-traces/
if-no-files-found: ignore

- name: Tear down containers
if: always()
shell: bash
run: docker compose -f docker-compose.playwright.yml down -v --remove-orphans
9 changes: 9 additions & 0 deletions Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ EXPOSE 3000
# --- Application image: production build served with `next start` ---------------
FROM base AS app

# NEXT_PUBLIC_* vars are inlined into the client bundle by `next build`, so the
# browser-driven Playwright flow passes them as build args.
ARG NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD=
ARG NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET=
ARG NEXT_PUBLIC_NETWORK_ID=0
ENV NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD=$NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD
ENV NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET=$NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET
ENV NEXT_PUBLIC_NETWORK_ID=$NEXT_PUBLIC_NETWORK_ID

# Build the production app so the smoke runs `next start` (not `next dev`) and
# exercises the same output Vercel deploys. `next dev` mis-resolves the
# @meshsdk/core-csl / whisky-evaluator WASM path at runtime, 500-ing tx routes.
Expand Down
16 changes: 16 additions & 0 deletions Dockerfile.playwright
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/playwright:v1.60.0-jammy

WORKDIR /app

# Install app dependencies (needed for @meshsdk/* imports in Phase 2 helpers).
COPY package.json package-lock.json* ./
COPY prisma ./prisma
RUN npm ci

# Install Chromium and its system dependencies.
RUN npx playwright install chromium --with-deps

# Copy e2e suite and the CI framework types it imports.
COPY e2e/ ./e2e/
COPY scripts/ci/framework/ ./scripts/ci/framework/
COPY tsconfig*.json ./
152 changes: 152 additions & 0 deletions docker-compose.playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
services:
postgres:
image: postgres:14-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: multisig
volumes:
- postgres-playwright-data:/var/lib/postgresql/data
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 20
networks:
- multisig-playwright-network

app:
build:
context: .
dockerfile: Dockerfile.ci
target: app
# `next build` inlines NEXT_PUBLIC_* into the client bundle, so the
# browser flow needs them at build time, not just at runtime. If any of
# these change in .env.playwright, rebuild the app image.
args:
NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
# Preprod currently requires this public env var even when the test
# only uses network 0, so reuse the preprod key to satisfy validation.
NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
NEXT_PUBLIC_NETWORK_ID: ${CI_NETWORK_ID:-0}
environment:
NODE_ENV: test
NEXT_TELEMETRY_DISABLED: "1"
SKIP_ENV_VALIDATION: "true"
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/multisig
DIRECT_URL: postgresql://postgres:postgres@postgres:5432/multisig
JWT_SECRET: ${CI_JWT_SECRET}
CORS_ORIGINS: http://webapp:3000,http://localhost:3000
NEXT_PUBLIC_NETWORK_ID: ${CI_NETWORK_ID:-0}
NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
depends_on:
postgres:
condition: service_healthy
networks:
multisig-playwright-network:
# "app" matches the .app HSTS-preloaded gTLD — Chromium hard-redirects
# all http://app:* to https:// before the request leaves the browser.
# "webapp" is not on the preload list, so Playwright can reach it over HTTP.
aliases:
- webapp
# Serve the production build, like docker-compose.ci.yml. `next dev` cannot
# serve this app: Turbopack dev bundles @sidan-lab/whisky-js-nodejs despite
# serverExternalPackages (breaking its WASM path, 500-ing every SSR page),
# and webpack dev chokes on react-refresh's import.meta injection into the
# CJS @harmoniclabs/cbor dist under the transpiled @meshsdk/react.
command: >
sh -c "
echo 'Waiting for PostgreSQL to be ready...' &&
until pg_isready -h postgres -p 5432 -U postgres; do sleep 1; done &&
echo 'Running Prisma migrations...' &&
npx prisma migrate deploy || npx prisma db push &&
echo 'Starting application (production build)...' &&
node_modules/.bin/next start --hostname 0.0.0.0 --port 3000
"
healthcheck:
test:
- CMD-SHELL
- node -e "fetch('http://localhost:3000/api/swagger').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 5s
timeout: 5s
retries: 30

bootstrap-runner:
build:
context: .
dockerfile: Dockerfile.ci
target: base
environment:
NODE_ENV: test
NEXT_TELEMETRY_DISABLED: "1"
SKIP_ENV_VALIDATION: "true"
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/multisig
DIRECT_URL: postgresql://postgres:postgres@postgres:5432/multisig
API_BASE_URL: http://webapp:3000
CI_NETWORK_ID: ${CI_NETWORK_ID:-0}
CI_NUM_REQUIRED_SIGNERS: ${CI_NUM_REQUIRED_SIGNERS:-2}
CI_JWT_SECRET: ${CI_JWT_SECRET}
CI_MNEMONIC_1: ${CI_MNEMONIC_1:-}
CI_MNEMONIC_2: ${CI_MNEMONIC_2:-}
CI_MNEMONIC_3: ${CI_MNEMONIC_3:-}
CI_BLOCKFROST_PREPROD_API_KEY: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
CI_WALLET_TYPES: ${CI_WALLET_TYPES:-legacy,hierarchical,sdk}
CI_CONTEXT_PATH: /artifacts/ci-wallet-context.json
depends_on:
app:
condition: service_healthy
networks:
- multisig-playwright-network
volumes:
- ./ci-artifacts:/artifacts
profiles:
- playwright
command: node .ci-dist/bootstrap.mjs

playwright-runner:
build:
context: .
dockerfile: Dockerfile.playwright
# Three Chromium instances run in parallel (one per ring-transfer leg);
# Docker's default 64MB /dev/shm makes Chromium crash under that load.
shm_size: 1gb
environment:
APP_URL: http://webapp:3000
PLAYWRIGHT_WORKERS: ${PLAYWRIGHT_WORKERS:-3}
CI_CONTEXT_PATH: /artifacts/ci-wallet-context.json
CI_JWT_SECRET: ${CI_JWT_SECRET}
CI_MNEMONIC_1: ${CI_MNEMONIC_1:-}
CI_MNEMONIC_2: ${CI_MNEMONIC_2:-}
CI_MNEMONIC_3: ${CI_MNEMONIC_3:-}
CI_BLOCKFROST_PREPROD_API_KEY: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
CI_TRANSFER_LOVELACE: ${CI_TRANSFER_LOVELACE:-2000000}
PLAYWRIGHT_HTML_REPORT: /artifacts/playwright-report
PLAYWRIGHT_OUTPUT_DIR: /artifacts/playwright-traces
depends_on:
bootstrap-runner:
condition: service_completed_successfully
app:
condition: service_healthy
networks:
- multisig-playwright-network
volumes:
- ./ci-artifacts:/artifacts
# Mount e2e sources and CI framework at runtime so local edits are picked
# up immediately without rebuilding the image. Docker's BuildKit layer
# cache on Windows often fails to detect file changes inside COPY layers,
# so this is the reliable alternative.
- ./e2e:/app/e2e:ro
- ./scripts/ci/framework:/app/scripts/ci/framework:ro
profiles:
- playwright
command: npx playwright test --config=e2e/playwright.config.ts

volumes:
postgres-playwright-data:

networks:
multisig-playwright-network:
driver: bridge
Loading
Loading