Merge thing #30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Acceptance Tests | |
| on: | |
| push: | |
| branches: [ dev/main ] | |
| pull_request: | |
| branches: [ dev/main ] | |
| workflow_dispatch: | |
| env: | |
| # CI-only credentials for a throwaway database and Umbraco install. | |
| MSSQL_SA_PASSWORD: 'Acceptance1234!' | |
| UMBRACO_USER_LOGIN: 'acceptance@seotoolkit.local' | |
| UMBRACO_USER_PASSWORD: 'SeoToolkitAcceptance1234!' | |
| SITE_URL: 'https://localhost:5001' | |
| # Kestrel needs a cert to serve HTTPS; Playwright ignores validation errors, so a | |
| # throwaway dev cert is enough. See the "Umbraco backoffice auth requires HTTPS" note | |
| # on the Start step. | |
| DEV_CERT_PASSWORD: 'AcceptanceCert1234!' | |
| jobs: | |
| acceptance: | |
| runs-on: ubuntu-latest | |
| services: | |
| sqlserver: | |
| image: mcr.microsoft.com/mssql/server:2022-latest | |
| env: | |
| ACCEPT_EULA: 'Y' | |
| MSSQL_SA_PASSWORD: 'Acceptance1234!' | |
| ports: | |
| - 1433:1433 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: tests/SeoToolkit.AcceptanceTests/.nvmrc | |
| cache: npm | |
| cache-dependency-path: | | |
| src/package-lock.json | |
| tests/SeoToolkit.AcceptanceTests/package-lock.json | |
| - name: Wait for SQL Server | |
| run: | | |
| # Probe the mapped port from the runner rather than docker exec-ing sqlcmd: | |
| # the sqlcmd path inside the image differs between builds (mssql-tools vs | |
| # mssql-tools18) and `docker ps --filter ancestor` is brittle. Bash /dev/tcp | |
| # needs neither. SQL Server opens the port a moment before it accepts logins, | |
| # hence the grace period. | |
| for i in $(seq 1 60); do | |
| if (exec 3<>/dev/tcp/localhost/1433) 2>/dev/null; then | |
| echo "SQL Server is accepting connections." | |
| sleep 15 | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "::error::SQL Server did not start within 300s." | |
| docker ps -a | |
| exit 1 | |
| # Must run BEFORE any dotnet build: the backoffice bundles are emitted into each | |
| # feature project's wwwroot/entry, and .NET snapshots static web assets at build | |
| # time. Build .NET first and the site boots with no SeoToolkit UI at all. | |
| - name: Install frontend dependencies | |
| working-directory: ./src | |
| run: npm ci | |
| - name: Build backoffice bundles | |
| working-directory: ./src | |
| run: npm run build:all | |
| - name: Publish the test site | |
| run: dotnet publish ./src/SeoToolkit.Umbraco.Site -c Release -o ./publish | |
| - name: Verify backoffice bundles were published | |
| run: | | |
| # Exactly the paths the ManifestLoader.cs files register as backofficeEntryPoint. | |
| missing=0 | |
| for entry in common metaFields notFound redirects robotstxt scriptManager siteAudit sitemap; do | |
| file="./publish/wwwroot/App_Plugins/SeoToolkit/entry/$entry/$entry.js" | |
| if [ ! -s "$file" ]; then | |
| echo "::error::Missing backoffice bundle $file - did 'npm run build:all' run before 'dotnet publish'?" | |
| missing=1 | |
| fi | |
| done | |
| exit $missing | |
| - name: Create a development HTTPS certificate | |
| run: dotnet dev-certs https -ep ${{ github.workspace }}/devcert.pfx -p "$DEV_CERT_PASSWORD" | |
| - name: Start the site | |
| # MUST run from inside the publish folder. ASP.NET Core resolves ContentRoot from | |
| # the current working directory, NOT from the dll location, so starting this from | |
| # the repo root gives a process that boots happily and answers /umbraco, but | |
| # resolves wwwroot/, Views/ and umbraco/ against the repo root - every | |
| # /App_Plugins/SeoToolkit/... request 404s and the readiness check never passes. | |
| working-directory: ./publish | |
| run: | | |
| nohup dotnet ./SeoToolkit.Umbraco.Site.dll > "$GITHUB_WORKSPACE/site.log" 2>&1 & | |
| echo $! > "$GITHUB_WORKSPACE/site.pid" | |
| echo "Started site, pid $(cat "$GITHUB_WORKSPACE/site.pid")" | |
| env: | |
| # Umbraco backoffice auth REQUIRES HTTPS. The login flow relies on the | |
| # __Host-umbPkceCode cookie, and the __Host- prefix plus the Secure flag mean | |
| # the browser never returns it over plain HTTP - the helper package then fails | |
| # in extractPKCECookie with "Cannot read properties of undefined". | |
| ASPNETCORE_URLS: ${{ env.SITE_URL }} | |
| # Unprefixed on purpose: Program.cs uses Host.CreateDefaultBuilder, whose app | |
| # configuration binds plain environment variables. Whether ASPNETCORE_-prefixed | |
| # names also reach the Kestrel section depends on host-config chaining, so do | |
| # not rely on it. | |
| Kestrel__Certificates__Default__Path: ${{ github.workspace }}/devcert.pfx | |
| Kestrel__Certificates__Default__Password: ${{ env.DEV_CERT_PASSWORD }} | |
| ConnectionStrings__umbracoDbDSN: "Server=localhost,1433;Database=SeoToolkitAcceptance;User Id=sa;Password=${{ env.MSSQL_SA_PASSWORD }};TrustServerCertificate=true;" | |
| ConnectionStrings__umbracoDbDSN_ProviderName: 'Microsoft.Data.SqlClient' | |
| Umbraco__CMS__Unattended__InstallUnattended: 'true' | |
| Umbraco__CMS__Unattended__UnattendedUserName: 'Acceptance Tests' | |
| Umbraco__CMS__Unattended__UnattendedUserEmail: ${{ env.UMBRACO_USER_LOGIN }} | |
| Umbraco__CMS__Unattended__UnattendedUserPassword: ${{ env.UMBRACO_USER_PASSWORD }} | |
| Umbraco__CMS__Unattended__PackageMigrationsUnattended: 'true' | |
| # Umbraco only auto-creates the database for SQLite and LocalDB. Against a | |
| # real SQL Server this must be explicit or boot fails on a connection error. | |
| Umbraco__CMS__Global__InstallMissingDatabase: 'true' | |
| - name: Wait for the site | |
| run: | | |
| # Umbraco writes its boot log to its own Serilog FILE sink. site.log (stdout) | |
| # only ever carries unhandled exceptions, so it is empty for a slow or stuck | |
| # boot - always look at the Umbraco logs first when this step fails. | |
| umbraco_logs() { | |
| echo "--- site.log (stdout/stderr) ---" | |
| cat site.log 2>/dev/null || echo "(no site.log)" | |
| echo "--- Umbraco trace logs ---" | |
| if ls ./publish/umbraco/Logs/*.json >/dev/null 2>&1; then | |
| tail -n 40 ./publish/umbraco/Logs/*.json | |
| else | |
| echo "(no Umbraco log files - the host never got far enough to create them)" | |
| fi | |
| echo "--- listening sockets ---" | |
| ss -lntp 2>/dev/null || netstat -lntp 2>/dev/null || true | |
| } | |
| # First boot creates the database, runs every Umbraco migration and then the | |
| # Clean starter kit package migration. That is far slower on a 2-core runner | |
| # against a SQL Server container than it is locally. | |
| deadline=$((SECONDS + 600)) | |
| last_report=0 | |
| while [ $SECONDS -lt $deadline ]; do | |
| # Fail fast if the host died (bad connection string, port clash, migration | |
| # crash) instead of burning the whole budget on a process that is gone. | |
| if ! kill -0 "$(cat site.pid)" 2>/dev/null; then | |
| echo "::error::The site process exited during startup." | |
| umbraco_logs | |
| exit 1 | |
| fi | |
| backoffice_ok=$(curl -skf -o /dev/null "$SITE_URL/umbraco" && echo yes || echo no) | |
| assets_ok=$(curl -skf -o /dev/null "$SITE_URL/App_Plugins/SeoToolkit/entry/common/common.js" && echo yes || echo no) | |
| if [ "$backoffice_ok" = yes ] && [ "$assets_ok" = yes ]; then | |
| echo "Backoffice is up and serving SeoToolkit assets after ${SECONDS}s." | |
| exit 0 | |
| fi | |
| # Report progress so a slow boot is visibly different from a stuck one. | |
| if [ $((SECONDS - last_report)) -ge 30 ]; then | |
| last_report=$SECONDS | |
| echo "[${SECONDS}s] backoffice=$backoffice_ok assets=$assets_ok" | |
| if ls ./publish/umbraco/Logs/*.json >/dev/null 2>&1; then | |
| tail -n 2 ./publish/umbraco/Logs/*.json | tail -n 4 | |
| else | |
| echo " (Umbraco has not written a log file yet)" | |
| fi | |
| fi | |
| sleep 5 | |
| done | |
| echo "::error::Site did not become ready within 600s." | |
| umbraco_logs | |
| exit 1 | |
| - name: Install acceptance test dependencies | |
| working-directory: ./tests/SeoToolkit.AcceptanceTests | |
| run: npm ci | |
| - name: Install Playwright browsers | |
| working-directory: ./tests/SeoToolkit.AcceptanceTests | |
| run: npx playwright install --with-deps chromium | |
| - name: Run acceptance tests | |
| working-directory: ./tests/SeoToolkit.AcceptanceTests | |
| run: npx playwright test | |
| env: | |
| URL: ${{ env.SITE_URL }} | |
| STORAGE_STATE_PATH: 'playwright/.auth/user.json' | |
| CONSOLE_ERRORS_PATH: 'console-errors.json' | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: | | |
| tests/SeoToolkit.AcceptanceTests/playwright-report/ | |
| tests/SeoToolkit.AcceptanceTests/test-results/ | |
| tests/SeoToolkit.AcceptanceTests/console-errors.json | |
| retention-days: 7 | |
| - name: Upload site and Umbraco logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: site-logs | |
| # site.log is stdout only. The Umbraco trace logs are the useful ones for a | |
| # boot failure - see the note in the "Wait for the site" step. | |
| path: | | |
| site.log | |
| publish/umbraco/Logs/ | |
| if-no-files-found: warn | |
| retention-days: 7 |