Skip to content

Merge pull request #501 from webstackdev/maintenance/refactor-html-im… #49

Merge pull request #501 from webstackdev/maintenance/refactor-html-im…

Merge pull request #501 from webstackdev/maintenance/refactor-html-im… #49

Workflow file for this run

# Runs lint, unit tests, and E2E suites to gate deployments
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }}
CONVERTKIT_FORM_ID: ${{ secrets.CONVERTKIT_FORM_ID }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
KV_URL: ${{ secrets.KV_URL }}
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
KV_REST_API_READ_ONLY_TOKEN: ${{ secrets.KV_REST_API_READ_ONLY_TOKEN }}
REDIS_URL: ${{ secrets.REDIS_URL }}
WEBMENTION_IO_TOKEN: ${{ secrets.WEBMENTION_IO_TOKEN }}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
if: ${{ !startsWith(github.head_ref || github.ref_name, 'hotfix/') }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Sync Astro types
run: npm run sync
- name: Run Astro check
run: npm run check
- name: Run lint
run: npm run lint:base
- name: Run Actions lint
run: npm run lint:actions
unit-test:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint
if: ${{ !startsWith(github.head_ref || github.ref_name, 'hotfix/') }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
# Astro build runs sync, check, and integrations including verify links
- name: Verify green build
run: npm run build
- name: Run unit tests (Vitest — coverage with GitHub Actions reporter)
id: vitest
run: npm run test:coverage
- name: Report Coverage
if: steps.vitest.outcome == 'success' && hashFiles('coverage/coverage-summary.json') != ''
uses: davelosert/vitest-coverage-report-action@v2.9.0
with:
json-summary-path: './coverage/coverage-summary.json'
json-final-path: './coverage/coverage-final.json'
- name: Upload test coverage
if: always() && hashFiles('coverage/coverage-summary.json') != ''
uses: actions/upload-artifact@v5
with:
name: test-coverage
path: coverage/
retention-days: 30
e2e-test:
name: E2E Tests
runs-on: ubuntu-latest
needs: unit-test
# Temporarily disabled while Supabase migrations are fixed; set this expression back to the previous condition when ready
if: ${{ false && !startsWith(github.head_ref || github.ref_name, 'hotfix/') }}
env:
CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }}
CONVERTKIT_FORM_ID: ${{ secrets.CONVERTKIT_FORM_ID }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
KV_URL: ${{ secrets.KV_URL }}
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
KV_REST_API_READ_ONLY_TOKEN: ${{ secrets.KV_REST_API_READ_ONLY_TOKEN }}
REDIS_URL: ${{ secrets.REDIS_URL }}
WEBMENTION_IO_TOKEN: ${{ secrets.WEBMENTION_IO_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22.x'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
- name: Build Upstash mock image
run: docker buildx build --load -t wb/upstash-redis-local:test test/containers/upstash/local-proxy
- name: Generate container env file
shell: bash
run: |
set -euo pipefail
mkdir -p test/containers
: "${SUPABASE_SERVICE_ROLE_KEY:?SUPABASE_SERVICE_ROLE_KEY secret is required}"
cat <<EOF_ENV > test/containers/.env
COMPOSE_PROJECT_NAME=wb-e2e
CONVERTKIT_HTTP_PORT=9010
RESEND_HTTP_PORT=9011
UPSTASH_HTTP_PORT=${UPSTASH_HTTP_PORT:-8079}
UPSTASH_REDIS_PORT=${UPSTASH_REDIS_PORT:-6380}
UPSTASH_TOKEN=${UPSTASH_TOKEN:-local-dev-token}
SUPABASE_HEALTH_TIMEOUT=${SUPABASE_HEALTH_TIMEOUT:-240}
SUPABASE_URL=${SUPABASE_URL:-http://127.0.0.1:54321}
SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
EOF_ENV
- name: Start mock containers
run: npm run containers:up
- name: Wait for mock services
run: npm run containers:wait
- name: Pre-pull Supabase storage image
run: |
set -euo pipefail
image="public.ecr.aws/supabase/storage-api:v1.32.1"
for attempt in 1 2 3 4 5 6; do
echo "Attempt ${attempt}: pulling ${image}"
if docker pull "${image}"; then
echo "Pulled ${image} successfully"
exit 0
fi
sleep_seconds=$((2 ** attempt))
echo "Pull failed; sleeping ${sleep_seconds}s before retry"
sleep "${sleep_seconds}"
done
echo "Failed to pull ${image} after multiple attempts" >&2
exit 1
- name: Start Supabase stack
run: npm run containers:supabase:start
- name: Apply Supabase migrations (local container)
run: |
set -euo pipefail
echo "::group::Supabase directory layout"
ls -al suprabase || true
find suprabase -maxdepth 2 -type f -print | sort || true
echo "::endgroup::"
echo "::group::Supabase migrations catalog check"
bash <<'BASH'
set -euo pipefail
find_db_container() {
docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$3 ~ /^supabase\/postgres(:|$)/ {print $1" "$2; exit}'
}
db_container_line=$(find_db_container)
if [ -z "${db_container_line}" ]; then
db_container_line=$(docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$2 ~ /_db_/ {print $1" "$2; exit}')
fi
if [ -z "${db_container_line}" ]; then
echo "Supabase database container not found; cannot verify catalog" >&2
exit 1
fi
container_id=$(echo "${db_container_line}" | awk '{print $1}')
catalog_present=$(docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -At -c "select to_regclass('supabase_migrations.schema_migrations');" || true)
if [ -z "${catalog_present}" ] || [ "${catalog_present}" = "" ] || [ "${catalog_present}" = "NULL" ]; then
echo "supabase_migrations.schema_migrations missing; creating manually"
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "CREATE SCHEMA IF NOT EXISTS supabase_migrations;"
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (version text PRIMARY KEY, inserted_at timestamptz DEFAULT now());"
echo "Catalog created; verifying contents"
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -c "\\d supabase_migrations.schema_migrations"
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -c "table supabase_migrations.schema_migrations"
else
echo "Catalog present: ${catalog_present}"
fi
BASH
echo "::endgroup::"
echo "::group::Supabase migrations"
npm run containers:supabase:db-push
echo "::endgroup::"
echo "::group::Supabase migration status"
FORCE_COLOR=1 npx supabase migration list --local --workdir suprabase
echo "::endgroup::"
echo "::group::Supabase migration status (JSON)"
FORCE_COLOR=1 npx supabase migration list --local --workdir suprabase --output json || true
echo "::endgroup::"
echo "::group::Supabase schema snapshot"
schema_dir="artifacts/supabase"
schema_path="${schema_dir}/schema.sql"
mkdir -p "${schema_dir}"
FORCE_COLOR=1 npx dotenv-cli -e test/containers/.env -- bash -c 'npx supabase db dump --local --schema "public,graphql_public,storage" --workdir suprabase' > "${schema_path}"
head -n 200 "${schema_path}" || true
echo "(full schema saved to ${schema_path})"
echo "::endgroup::"
echo "::group::Supabase table inventory"
bash <<'BASH'
set -euo pipefail
find_db_container() {
docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$3 ~ /^supabase\/postgres(:|$)/ {print $1" "$2; exit}'
}
db_container_line=$(find_db_container)
if [ -z "${db_container_line}" ]; then
db_container_line=$(docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$2 ~ /_db_/ {print $1" "$2; exit}')
fi
if [ -z "${db_container_line}" ]; then
echo "Supabase database container not found; skipping inventory" >&2
exit 0
fi
container_id=$(echo "${db_container_line}" | awk '{print $1}')
echo "Listing public schema tables via container ${container_id}"
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -c '\dt public.*' || true
docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -At -c "select table_schema || '.' || table_name from information_schema.tables where table_schema='public' order by 1;" || true
BASH
echo "::endgroup::"
echo "::group::Supabase schema guard"
bash <<'BASH'
set -euo pipefail
find_db_container() {
docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$3 ~ /^supabase\/postgres(:|$)/ {print $1" "$2; exit}'
}
db_container_line=$(find_db_container)
if [ -z "${db_container_line}" ]; then
db_container_line=$(docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$2 ~ /_db_/ {print $1" "$2; exit}')
fi
if [ -z "${db_container_line}" ]; then
echo "Supabase database container not found; cannot verify migrations" >&2
exit 1
fi
container_id=$(echo "${db_container_line}" | awk '{print $1}')
container_name=$(echo "${db_container_line}" | awk '{print $2}')
echo "Inspecting tables inside ${container_name} (${container_id})"
required_tables=(newsletter_confirmations consent_records dsar_requests)
missing=0
for table in "${required_tables[@]}"; do
if ! docker exec -e PGPASSWORD=postgres "${container_id}" psql -U postgres -d postgres -At -c "select to_regclass('public.${table}');" | grep -q "public.${table}"; then
echo "❌ Missing table public.${table}"
missing=1
else
echo "✅ Found table public.${table}"
fi
done
if [ "${missing}" -ne 0 ]; then
echo "Required tables missing after Supabase migrations" >&2
exit 1
fi
BASH
echo "::endgroup::"
echo "::group::Restart Supabase REST container"
bash <<'BASH'
set -euo pipefail
rest_container_line=$(docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$3 ~ /postgrest/ {print $1" "$2; exit}')
if [ -z "${rest_container_line}" ]; then
echo "Supabase REST container not found; skipping restart" >&2
exit 0
fi
container_id=$(echo "${rest_container_line}" | awk '{print $1}')
container_name=$(echo "${rest_container_line}" | awk '{print $2}')
echo "Restarting ${container_name} (${container_id}) to refresh schema cache"
docker restart "${container_id}"
BASH
echo "::endgroup::"
echo "::group::Supabase container logs (last 200 lines)"
bash <<'BASH'
set -euo pipefail
containers=$(docker ps --format '{{.ID}} {{.Names}} {{.Image}}' | awk '$3 ~ /supabase\// {print $1" "$2}')
if [ -z "${containers}" ]; then
echo "No Supabase containers found" >&2
exit 0
fi
while read -r container_id container_name; do
[ -z "${container_id}" ] && continue
echo "--- ${container_name} (tail -n 200) ---"
docker logs --tail 200 "${container_id}" || true
done <<< "${containers}"
BASH
echo "::endgroup::"
- name: Upload Supabase schema snapshot
if: ${{ always() && hashFiles('artifacts/supabase/schema.sql') != '' }}
uses: actions/upload-artifact@v5
with:
name: supabase-schema
path: artifacts/supabase/schema.sql
retention-days: 14
- name: Start Astro dev server
run: |
npm run dev -- --host 0.0.0.0 > /tmp/astro-dev.log 2>&1 &
echo $! > /tmp/astro-dev.pid
- name: Wait for dev server
run: |
for attempt in $(seq 1 60); do
if curl -fsS http://127.0.0.1:4321 >/dev/null; then
echo "✅ Dev server is responding"
exit 0
fi
sleep 2
done
echo "❌ Dev server failed to start" >&2
if [ -f /tmp/astro-dev.log ]; then
echo '--- Astro dev server log ---'
cat /tmp/astro-dev.log
fi
exit 1
- name: Run Playwright E2E tests
run: npx playwright test
env:
CI: '1'
FORCE_COLOR: '1'
E2E_MOCKS: '1'
- name: Stop Astro dev server
if: always()
run: |
if [ -f /tmp/astro-dev.pid ]; then
kill $(cat /tmp/astro-dev.pid) || true
rm /tmp/astro-dev.pid
fi
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v5
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Stop Supabase stack
if: always()
run: npm run containers:supabase:stop || true
- name: Stop mock containers
if: always()
run: npm run containers:down || true
hotfix-bypass:
name: Hotfix Bypass Notice
runs-on: ubuntu-latest
if: ${{ startsWith(github.head_ref || github.ref_name, 'hotfix/') }}
steps:
- name: Skip testing for hotfix branch
run: echo "hotfix/* branch detected; skipping lint, unit, and e2e workflows but allowing deployments."