From 31c3f4698b71da34273704507dca466e7eeff04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemek=20Wi=C4=99ch?= Date: Fri, 3 Jul 2026 23:55:08 +0200 Subject: [PATCH] Add docker container prober --- .github/workflows/README.md | 2 + .github/workflows/deploy-everywhere.yml | 12 ++++ .github/workflows/prober-docker.yml | 87 +++++++++++++++++++++++++ playwright.prober.config.ts | 5 ++ tests/probers/docker.spec.ts | 36 ++++++++++ 5 files changed, 142 insertions(+) create mode 100644 .github/workflows/prober-docker.yml create mode 100644 tests/probers/docker.spec.ts diff --git a/.github/workflows/README.md b/.github/workflows/README.md index eec9d91..a7f2622 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -12,3 +12,5 @@ This directory contains GitHub Actions workflow files that automate various task - [node.js.yml](node.js.yml): The main Continuous Integration (CI) workflow. It installs dependencies, checks formatting, lints, builds, and runs tests across multiple Node.js versions. - [prober-gh-pages.yml](prober-gh-pages.yml): Reusable prober that smoke-tests the GitHub Pages deployment with GEDCOM-from-URL through the CORS proxy. Runs daily and after deploy. - [prober-wikitree.yml](prober-wikitree.yml): Reusable prober that smoke-tests the WikiTree direct API path on the live WikiTree deployment. Runs daily and after deploy. +- [prober-wikitree-cors.yml](prober-wikitree-cors.yml): Reusable prober that smoke-tests the CORS proxy from the WikiTree deployment with GEDCOM-from-URL. Runs daily and after deploy. +- [prober-docker.yml](prober-docker.yml): Reusable prober that smoke-tests the published Docker image from GHCR (Dockerfile, Caddy config, app startup) by pulling and running it locally. Runs daily and after deploy. diff --git a/.github/workflows/deploy-everywhere.yml b/.github/workflows/deploy-everywhere.yml index 9da1da9..f89a093 100644 --- a/.github/workflows/deploy-everywhere.yml +++ b/.github/workflows/deploy-everywhere.yml @@ -28,3 +28,15 @@ jobs: secrets: inherit with: wait_for_propagation: true + + prober-wikitree-cors: + needs: deploy-wikitree-apps + uses: ./.github/workflows/prober-wikitree-cors.yml + secrets: inherit + with: + wait_for_propagation: true + + prober-docker: + needs: deploy-docker + uses: ./.github/workflows/prober-docker.yml + secrets: inherit diff --git a/.github/workflows/prober-docker.yml b/.github/workflows/prober-docker.yml new file mode 100644 index 0000000..559a33b --- /dev/null +++ b/.github/workflows/prober-docker.yml @@ -0,0 +1,87 @@ +name: Prober - Docker + +on: + workflow_call: + workflow_dispatch: + schedule: + - cron: '0 5 * * *' + +permissions: + contents: read + actions: write + +concurrency: + group: prober-${{ github.workflow }} + cancel-in-progress: false + +jobs: + prober: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Docker image + run: docker pull ghcr.io/pewu/topola-viewer:latest + + - name: Run container + run: | + docker run -d -p 8080:8080 -e STATIC_URL=test.ged \ + -v $(pwd)/src/datasource/testdata/test.ged:/app/public/test.ged \ + --name topola-prober-${{ github.run_id }} \ + ghcr.io/pewu/topola-viewer:latest + + - name: Wait for container to be ready + run: | + for i in $(seq 1 30); do + if curl -sf -o /dev/null http://localhost:8080/; then break; fi + sleep 1 + done + curl -sf -o /dev/null http://localhost:8080/ + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'npm' + + - run: npm ci + + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + + - name: Cache Playwright Browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright System Dependencies + run: npx playwright install-deps chromium + + - name: Install Playwright Browsers (If Not Cached) + if: steps.cache-playwright.outputs.cache-hit != 'true' + run: npx playwright install chromium + + - name: Run prober + env: + PLAYWRIGHT_HTML_REPORT: playwright-report/prober + run: npx playwright test --config=playwright.prober.config.ts docker.spec.ts + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: prober-report-docker + path: playwright-report/ + retention-days: 30 + + - name: Stop and remove container + if: always() + run: | + docker stop topola-prober-${{ github.run_id }} 2>/dev/null || true + docker rm topola-prober-${{ github.run_id }} 2>/dev/null || true diff --git a/playwright.prober.config.ts b/playwright.prober.config.ts index 213ad64..7b3bcfc 100644 --- a/playwright.prober.config.ts +++ b/playwright.prober.config.ts @@ -25,6 +25,11 @@ export default defineConfig({ projects: [ { name: 'prober', + // docker.spec.ts is excluded from local runs (npm run test:probers) + // because it requires a running Docker container. The CI workflow + // explicitly passes docker.spec.ts as an argument, which overrides + // testIgnore. + testIgnore: 'docker.spec.ts', use: { ...devices['Desktop Chrome'], }, diff --git a/tests/probers/docker.spec.ts b/tests/probers/docker.spec.ts new file mode 100644 index 0000000..45b9133 --- /dev/null +++ b/tests/probers/docker.spec.ts @@ -0,0 +1,36 @@ +import {expect, test} from '@playwright/test'; + +/** + * Prober: Docker container + * + * Pulls the Docker image published to GHCR, runs it locally with the test + * GEDCOM file mounted via STATIC_URL, and verifies that the containerized + * application starts and renders data. This exercises the Docker build path + * (multi-stage Dockerfile, Caddy server configuration, static URL template + * injection) and confirms the published container image is functional. + * + * The workflow starts the container externally (via `docker run`) before + * this test runs, so Playwright connects to localhost:8080 directly. + */ +test('Docker container prober', async ({page}) => { + await page.goto('http://localhost:8080/'); + + // Wait for the app to reach SHOWING_CHART state. + await expect(page.locator('#content')).toBeVisible(); + + // Assert the expected person's name appears in the chart SVG. + await expect(page.locator('#chart')).toContainText('Bonifacy'); + + // Assert the expected person's name appears in the side panel. + // Scoped to div.details to avoid matching SVG + // elements in the chart that also carry the "details" class. + await expect(page.locator('div.details')).toContainText('Bonifacy'); + + // Assert no fatal error is displayed (replaces chart when state is ERROR). + await expect(page.locator('.ui.error.message')).not.toBeVisible(); + + // Assert no popup error is displayed. + // ErrorPopup uses Semantic UI React's , which renders at + // document.body level, not inside #content. + await expect(page.locator('.ui.errorPopup.message')).not.toBeVisible(); +});