Tests (Refactored) Docker #13
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: Tests (Refactored) Docker | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| platformDockerTag: | |
| description: "Platform Docker image tag" | |
| required: false | |
| type: string | |
| default: "linux-latest" | |
| frontendZipUrl: | |
| description: "Frontend zip URL ('latest' for latest vc-frontend release)" | |
| required: false | |
| type: string | |
| default: "latest" | |
| jobs: | |
| tests: | |
| timeout-minutes: 60 | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| checks: write | |
| pull-requests: write | |
| issues: read | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} | |
| FORCE_COLOR: "1" | |
| steps: | |
| # ────────────────────────────────────────────── | |
| # 1. Checkout & install modules | |
| # ────────────────────────────────────────────── | |
| - name: Checkout testing repo | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: VirtoCommerce/vc-testing-module | |
| ref: ${{ github.ref_name }} | |
| path: vc-testing-module | |
| token: ${{ secrets.REPO_TOKEN }} | |
| - name: Install VirtoCommerce.GlobalTool | |
| uses: VirtoCommerce/vc-github-actions/setup-vcbuild@master | |
| - name: Install modules and pull platform image | |
| run: | | |
| mkdir modules && cd modules | |
| vc-build install --package-manifest-path $GITHUB_WORKSPACE/vc-testing-module/backend-packages.json --skip-dependency-solving | |
| docker pull ghcr.io/virtocommerce/platform:${{ inputs.platformDockerTag }} | |
| docker tag ghcr.io/virtocommerce/platform:${{ inputs.platformDockerTag }} platform:local-latest | |
| # ────────────────────────────────────────────── | |
| # 2. Build frontend Docker image | |
| # ────────────────────────────────────────────── | |
| - name: Build frontend Docker image | |
| shell: pwsh | |
| run: | | |
| if ("${{ inputs.frontendZipUrl }}" -eq "latest") { | |
| $releaseInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/VirtoCommerce/vc-frontend/releases/latest" | |
| $zipUrl = $releaseInfo.assets.browser_download_url | |
| Write-Host "Using latest frontend release: $zipUrl" | |
| } else { | |
| $zipUrl = "${{ inputs.frontendZipUrl }}" | |
| Write-Host "Using provided frontend zip: $zipUrl" | |
| } | |
| $fileName = Split-Path $zipUrl -Leaf | |
| Invoke-WebRequest -Uri $zipUrl -OutFile $fileName -UseBasicParsing -OperationTimeoutSeconds 15 -RetryIntervalSec 1 -MaximumRetryCount 3 | |
| mkdir -p frontend | |
| unzip -o $fileName -d ./frontend | |
| @' | |
| server { | |
| listen 80; | |
| server_name localhost; | |
| root /usr/share/nginx/html; | |
| index index.html; | |
| location / { | |
| try_files $uri $uri/ /index.html; | |
| add_header 'Access-Control-Allow-Origin' '*'; | |
| } | |
| location ~* \.(json|png|ico|gif|jpg|jpeg|css|js|xml|txt)$ { | |
| try_files $uri /assets/stores/B2B-store$uri /Themes/B2B-store/default$uri =404; | |
| root /usr/share/nginx/html; | |
| add_header Cache-Control "no-cache, must-revalidate, proxy-revalidate"; | |
| error_page 404 = @static_404; | |
| } | |
| location @static_404 { | |
| add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; | |
| return 404; | |
| } | |
| error_page 500 502 503 504 /50x.html; | |
| location = /50x.html { root /usr/share/nginx/html; } | |
| proxy_buffer_size 128k; | |
| proxy_buffers 4 256k; | |
| proxy_busy_buffers_size 256k; | |
| proxy_connect_timeout 600; | |
| proxy_send_timeout 600; | |
| proxy_read_timeout 600; | |
| location /files { proxy_pass http://vc-platform-web; } | |
| location /connect/token { proxy_pass http://vc-platform-web; } | |
| location /graphql { proxy_pass http://vc-platform-web; } | |
| location /revoke/token { proxy_pass http://vc-platform-web; } | |
| location /api/files { proxy_pass http://vc-platform-web; } | |
| location /cms-content { proxy_pass http://vc-platform-web; } | |
| location /hub/ { | |
| proxy_pass http://vc-platform-web; | |
| proxy_http_version 1.1; | |
| proxy_set_header Upgrade $http_upgrade; | |
| proxy_set_header Connection "upgrade"; | |
| } | |
| } | |
| '@ | Set-Content -Path nginx.conf | |
| @' | |
| FROM nginx:alpine | |
| COPY nginx.conf /etc/nginx/conf.d/default.conf | |
| COPY ./frontend/default/ /usr/share/nginx/html | |
| EXPOSE 80 | |
| '@ | Set-Content -Path Dockerfile.frontend | |
| docker build -f Dockerfile.frontend -t nginx_frontend:local-latest . | |
| # ────────────────────────────────────────────── | |
| # 3. Start Docker infrastructure | |
| # ────────────────────────────────────────────── | |
| - name: Create docker-compose.yml | |
| run: | | |
| cat > docker-compose.yml << 'COMPOSE' | |
| services: | |
| vc-db: | |
| image: mcr.microsoft.com/mssql/server:2017-latest | |
| ports: | |
| - "1433:1433" | |
| environment: | |
| - ACCEPT_EULA=Y | |
| - MSSQL_PID=Express | |
| - SA_PASSWORD=v!rto_Labs! | |
| networks: | |
| - virto | |
| es: | |
| image: docker.elastic.co/elasticsearch/elasticsearch:8.18.0 | |
| volumes: | |
| - esdata01:/usr/share/elasticsearch/data | |
| ports: | |
| - "9200:9200" | |
| networks: | |
| - virto | |
| environment: | |
| - node.name=es | |
| - cluster.name=elasticsearch | |
| - cluster.initial_master_nodes=es | |
| - ELASTIC_PASSWORD=v!rto_Labs! | |
| - bootstrap.memory_lock=true | |
| - xpack.security.enabled=false | |
| - xpack.security.http.ssl.enabled=false | |
| - xpack.security.transport.ssl.enabled=false | |
| - xpack.license.self_generated.type=basic | |
| - xpack.ml.use_auto_machine_memory_percent=true | |
| mem_limit: 1g | |
| ulimits: | |
| memlock: | |
| soft: -1 | |
| hard: -1 | |
| healthcheck: | |
| test: ["CMD-SHELL", "curl -s http://localhost:9200"] | |
| interval: 10s | |
| timeout: 10s | |
| retries: 120 | |
| nginx_frontend: | |
| depends_on: | |
| - vc-platform-web | |
| image: nginx_frontend:local-latest | |
| ports: | |
| - "80:80" | |
| networks: | |
| - virto | |
| vc-platform-web: | |
| image: platform:local-latest | |
| ports: | |
| - "8090:80" | |
| environment: | |
| - ASPNETCORE_URLS=http://+ | |
| - VirtoCommerce__AllowInsecureHttp=true | |
| - VirtoCommerce__Hangfire__JobStorageType=Memory | |
| - ConnectionStrings__VirtoCommerce=Data Source=vc-db;Initial Catalog=VirtoCommerce3docker;Persist Security Info=True;User ID=sa;Password=v!rto_Labs!;MultipleActiveResultSets=False;Connect Timeout=360;TrustServerCertificate=True; | |
| - Assets__FileSystem__PublicUrl=http://localhost:8090/assets/ | |
| - Content__FileSystem__PublicUrl=http://localhost:8090/cms-content/ | |
| - Search__Provider=ElasticSearch8 | |
| - Search__ElasticSearch8__Server=http://es:9200 | |
| - Search__ElasticSearch8__User=elastic | |
| - Search__ElasticSearch8__Key=v!rto_Labs! | |
| - Search__ElasticSearch8__EnableCompatibilityMode=true | |
| - Search__PickupLocationFullTextSearchEnabled=true | |
| - IdentityOptions__User__MaxPasswordAge=0 | |
| - Notifications__DefaultSender=virto.start@virtoway.com | |
| - Notifications__Gateway=SendGrid | |
| - Notifications__SendGrid__ApiKey=${SENDGRID_API_KEY} | |
| depends_on: | |
| - vc-db | |
| - es | |
| entrypoint: ["/wait-for-it.sh", "vc-db:1433", "-t", "120", "--", "dotnet", "VirtoCommerce.Platform.Web.dll"] | |
| volumes: | |
| - ./modules/modules:/opt/virtocommerce/platform/modules | |
| - ./modules/app_data:/opt/virtocommerce/platform/app_data | |
| networks: | |
| - virto | |
| restart: unless-stopped | |
| volumes: | |
| esdata01: | |
| driver: local | |
| networks: | |
| virto: | |
| COMPOSE | |
| - name: Start database and Elasticsearch | |
| env: | |
| SENDGRID_API_KEY: ${{ secrets.SENDGRID_APIKEY_4E2E_AUTOTESTS }} | |
| run: | | |
| docker compose --project-name virtocommerce up -d vc-db es | |
| sleep 15 | |
| - name: Start platform and frontend | |
| env: | |
| SENDGRID_API_KEY: ${{ secrets.SENDGRID_APIKEY_4E2E_AUTOTESTS }} | |
| run: | | |
| docker compose --project-name virtocommerce up -d | |
| echo "Waiting for platform to start..." | |
| for i in $(seq 1 20); do | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8090/connect/token 2>/dev/null) || true | |
| echo "Attempt $i/20: HTTP $HTTP_CODE" | |
| if [ "$HTTP_CODE" -ge 200 ] 2>/dev/null && [ "$HTTP_CODE" -lt 500 ] 2>/dev/null; then | |
| echo "Platform is ready!" | |
| break | |
| fi | |
| sleep 15 | |
| done | |
| echo "Checking frontend..." | |
| for i in $(seq 1 10); do | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost 2>/dev/null) || true | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "Frontend is ready!" | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| # ────────────────────────────────────────────── | |
| # 4. Prepare platform | |
| # ────────────────────────────────────────────── | |
| - name: Reset admin password | |
| shell: pwsh | |
| env: | |
| SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }} | |
| run: | | |
| if ($env:SECRET_ENV_FILE -match 'ADMIN_PASSWORD=(\S+)') { | |
| $newAdminPassword = $matches[1] | |
| } else { | |
| $newAdminPassword = 'store' | |
| } | |
| $body = "grant_type=password&username=admin&password=store" | |
| $headers = @{ "Content-Type" = "application/x-www-form-urlencoded" } | |
| $token = ((Invoke-WebRequest -Uri "http://localhost:8090/connect/token" -Body $body -Headers $headers -Method POST).Content | ConvertFrom-Json).access_token | |
| $authHeaders = @{ "Content-Type" = "application/json-patch+json"; "Authorization" = "Bearer $token" } | |
| $resetBody = @{ "newPassword" = $newAdminPassword; "forcePasswordChangeOnNextSignIn" = $false } | ConvertTo-Json | |
| Invoke-WebRequest -Uri "http://localhost:8090/api/platform/security/users/admin/resetpassword" -Body $resetBody -Headers $authHeaders -Method POST | |
| Write-Host "Admin password reset successfully" | |
| # ────────────────────────────────────────────── | |
| # 5. Set up Python & dependencies | |
| # ────────────────────────────────────────────── | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Create .env and install dependencies | |
| working-directory: vc-testing-module/_refactored | |
| env: | |
| SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }} | |
| run: | | |
| echo "$SECRET_ENV_FILE" > .env | |
| chmod 600 .env | |
| sed -i 's|^BACKEND_BASE_URL=.*|BACKEND_BASE_URL=http://localhost:8090|g' .env | |
| sed -i 's|^FRONTEND_BASE_URL=.*|FRONTEND_BASE_URL=http://localhost|g' .env | |
| python -m venv .venv | |
| source .venv/bin/activate | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| playwright install --with-deps chromium | |
| # ────────────────────────────────────────────── | |
| # 6. Seed data & trigger indexing | |
| # ────────────────────────────────────────────── | |
| - name: Seed test data | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| python -m dataset.dataset_manager --seed --mode ci | |
| - name: Trigger search indexing | |
| shell: pwsh | |
| env: | |
| SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }} | |
| run: | | |
| $platformUrl = "http://localhost:8090" | |
| if ($env:SECRET_ENV_FILE -match 'ADMIN_PASSWORD=(\S+)') { $adminPassword = $matches[1] } else { $adminPassword = 'store' } | |
| $headers = @{ "Content-Type" = "application/x-www-form-urlencoded" } | |
| $token = ((Invoke-WebRequest -Uri "$platformUrl/connect/token" -Body "grant_type=password&username=admin&password=$adminPassword" -Headers $headers -Method POST).Content | ConvertFrom-Json).access_token | |
| $authHeaders = @{ "Content-Type" = "application/json-patch+json"; "Authorization" = "Bearer $token" } | |
| $indexBody = @( | |
| @{ "DocumentType" = "Member"; "DeleteExistingIndex" = $true }, | |
| @{ "DocumentType" = "Product"; "DeleteExistingIndex" = $true }, | |
| @{ "DocumentType" = "Category"; "DeleteExistingIndex" = $true }, | |
| @{ "DocumentType" = "CustomerOrder"; "DeleteExistingIndex" = $true }, | |
| @{ "DocumentType" = "PickupLocation"; "DeleteExistingIndex" = $true } | |
| ) | |
| $result = Invoke-WebRequest -Uri "$platformUrl/api/search/indexes/index" -Body ($indexBody | ConvertTo-Json) -Headers $authHeaders -Method POST | ConvertFrom-Json | |
| do { | |
| $job = Invoke-WebRequest -Uri "$platformUrl/api/platform/jobs/$($result.JobId)" -Headers $authHeaders -Method GET | ConvertFrom-Json | |
| Write-Host "Indexing completed: $($job.completed)" | |
| if (-not $job.completed) { Start-Sleep -Seconds 5 } | |
| } while (-not $job.completed) | |
| Write-Host "Indexing finished." | |
| # ────────────────────────────────────────────── | |
| # 7. Run tests | |
| # ────────────────────────────────────────────── | |
| - name: Clean allure-results | |
| working-directory: vc-testing-module/_refactored | |
| run: rm -rf allure-results | |
| - name: Run GraphQL tests | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/graphql -m "not ignore" -v -s --color=yes \ | |
| --junitxml=graphql-junit.xml | |
| - name: Run REST API tests (parallel-safe) | |
| if: success() || failure() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/restapi -m "restapi and not ignore and not serial" -v -s --color=yes \ | |
| --junitxml=restapi-junit.xml | |
| - name: Run E2E tests | |
| if: success() || failure() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/e2e -m "not ignore" --browser chromium -v -s --color=yes \ | |
| --junitxml=e2e-junit.xml | |
| - name: Run REST API tests (serial — global state mutators) | |
| if: success() || failure() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/restapi -m "restapi and serial and not ignore" -v -s --color=yes \ | |
| --junitxml=restapi-serial-junit.xml \ | |
| --deselect tests/restapi/platform/test_misc.py::test_restart_platform | |
| - name: Run REST API test — restart platform (last) | |
| if: success() || failure() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| source .venv/bin/activate | |
| pytest tests/restapi/platform/test_misc.py::test_restart_platform -v -s --color=yes \ | |
| --junitxml=restapi-restart-junit.xml | |
| # ────────────────────────────────────────────── | |
| # 8. Collect results | |
| # ────────────────────────────────────────────── | |
| - name: Install Allure CLI | |
| if: always() | |
| run: | | |
| ALLURE_VERSION=2.29.0 | |
| wget -q -O allure.tgz \ | |
| "https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.tgz" | |
| sudo tar -xzf allure.tgz -C /opt | |
| sudo ln -sf "/opt/allure-${ALLURE_VERSION}/bin/allure" /usr/local/bin/allure | |
| allure --version | |
| - name: Generate Allure HTML report | |
| if: always() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| if [ -d allure-results ] && [ -n "$(ls -A allure-results 2>/dev/null)" ]; then | |
| allure generate allure-results --clean -o allure-report | |
| else | |
| echo "::warning::allure-results is empty — skipping report render" | |
| fi | |
| - name: Bundle Allure into single HTML | |
| if: always() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| if [ -d allure-report ]; then | |
| pip install allure-combine | |
| allure-combine allure-report | |
| ls -la allure-report/complete.html | |
| else | |
| echo "::warning::allure-report missing — skipping allure-combine" | |
| fi | |
| - name: Stage consolidated report | |
| if: always() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| mkdir -p report | |
| [ -d allure-report ] && mv allure-report report/ || echo "no allure-report" | |
| [ -d har-output ] && mv har-output report/ || echo "no har-output" | |
| [ -d test-results ] && mv test-results report/ || echo "no test-results" | |
| [ -d screenshots ] && mv screenshots report/ || echo "no screenshots" | |
| [ -f graphql-junit.xml ] && mv graphql-junit.xml report/ || echo "no graphql-junit.xml" | |
| [ -f restapi-junit.xml ] && mv restapi-junit.xml report/ || echo "no restapi-junit.xml" | |
| [ -f restapi-serial-junit.xml ] && mv restapi-serial-junit.xml report/ || echo "no restapi-serial-junit.xml" | |
| [ -f restapi-restart-junit.xml ] && mv restapi-restart-junit.xml report/ || echo "no restapi-restart-junit.xml" | |
| [ -f e2e-junit.xml ] && mv e2e-junit.xml report/ || echo "no e2e-junit.xml" | |
| cat > report/index.html << 'HTML' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="refresh" content="0; url=allure-report/complete.html"> | |
| <title>Refactored Test Results</title> | |
| </head> | |
| <body> | |
| <p>Redirecting to the <a href="allure-report/complete.html">Allure report</a>. | |
| If nothing happens, open <code>allure-report/complete.html</code> manually.</p> | |
| </body> | |
| </html> | |
| HTML | |
| cat > report/README.md << 'EOF' | |
| # Refactored Test Results | |
| Run: ${{ github.run_number }} (attempt ${{ github.run_attempt }}) | |
| Commit: ${{ github.sha }} | |
| Branch: ${{ github.ref_name }} | |
| ## What's in this bundle | |
| | Path | Use it for | | |
| |---|---| | |
| | `index.html` | Landing page — double-click to open the standalone Allure report. | | |
| | `allure-report/complete.html` | Standalone single-file Allure report (GraphQL + REST API + E2E combined). Works from `file://` — no server needed. | | |
| | `allure-report/` (the rest) | Original multi-file Allure report. Needs `python -m http.server` or `allure open allure-report/`. | | |
| | `har-output/<module>/<test>.har` | Per-test HTTP traces (HAR 1.2) from REST API tests, grouped by module. Import into Chrome DevTools Network tab. Auth headers redacted. | | |
| | `test-results/` | Playwright trace zips per failed E2E test. Open with `playwright show-trace <file>.zip` or at [trace.playwright.dev](https://trace.playwright.dev). | | |
| | `screenshots/` | Full-page screenshots captured on E2E test failure. Attached to Allure report automatically. | | |
| | `graphql-junit.xml` | JUnit XML for GraphQL tests. | | |
| | `restapi-junit.xml` | JUnit XML for REST API tests. | | |
| | `e2e-junit.xml` | JUnit XML for E2E tests. | | |
| The same JUnit results drive the per-test PR comment and the per-file breakdown on the run's Summary tab. | |
| EOF | |
| - name: Upload consolidated test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ github.run_number }}-${{ github.run_attempt }} | |
| path: vc-testing-module/_refactored/report/ | |
| if-no-files-found: warn | |
| retention-days: 30 | |
| - name: Publish test results summary | |
| if: always() | |
| uses: EnricoMi/publish-unit-test-result-action@v2 | |
| with: | |
| files: | | |
| vc-testing-module/_refactored/report/graphql-junit.xml | |
| vc-testing-module/_refactored/report/restapi-junit.xml | |
| vc-testing-module/_refactored/report/restapi-serial-junit.xml | |
| vc-testing-module/_refactored/report/restapi-restart-junit.xml | |
| vc-testing-module/_refactored/report/e2e-junit.xml | |
| check_name: Test Results (Refactored) | |
| comment_mode: always | |
| report_individual_runs: true | |
| - name: Per-file test summary | |
| if: always() | |
| working-directory: vc-testing-module/_refactored | |
| run: | | |
| python <<'PYEOF' | |
| import os | |
| import xml.etree.ElementTree as ET | |
| from collections import defaultdict | |
| from pathlib import Path | |
| stats = defaultdict( | |
| lambda: {"total": 0, "passed": 0, "failed": 0, "errored": 0, "skipped": 0, "time": 0.0} | |
| ) | |
| for xml_name in ["report/graphql-junit.xml", "report/restapi-junit.xml", "report/restapi-serial-junit.xml", "report/restapi-restart-junit.xml", "report/e2e-junit.xml"]: | |
| xml_path = Path(xml_name) | |
| if not xml_path.exists(): | |
| continue | |
| tree = ET.parse(xml_path) | |
| suite_label = "graphql" if "graphql" in xml_name else ("restapi" if "restapi" in xml_name else "e2e") | |
| for tc in tree.iter("testcase"): | |
| classname = tc.attrib.get("classname", "") | |
| bucket = classname.rsplit(".", 1)[-1] if classname else "(unknown)" | |
| key = f"{suite_label}/{bucket}" | |
| s = stats[key] | |
| s["total"] += 1 | |
| try: | |
| s["time"] += float(tc.attrib.get("time") or 0) | |
| except ValueError: | |
| pass | |
| if tc.find("failure") is not None: | |
| s["failed"] += 1 | |
| elif tc.find("error") is not None: | |
| s["errored"] += 1 | |
| elif tc.find("skipped") is not None: | |
| s["skipped"] += 1 | |
| else: | |
| s["passed"] += 1 | |
| if not stats: | |
| print("No JUnit XML found — skipping summary") | |
| raise SystemExit(0) | |
| lines = [ | |
| "## Refactored tests — per-file breakdown", | |
| "", | |
| "| Suite / Test file | Total | Passed | Failed | Errored | Skipped | Time |", | |
| "|---|---:|---:|---:|---:|---:|---:|", | |
| ] | |
| totals = {"total": 0, "passed": 0, "failed": 0, "errored": 0, "skipped": 0, "time": 0.0} | |
| for key in sorted(stats): | |
| s = stats[key] | |
| for k in totals: | |
| totals[k] += s[k] | |
| lines.append( | |
| f"| `{key}` | {s['total']} | {s['passed']} | {s['failed']} | " | |
| f"{s['errored']} | {s['skipped']} | {s['time']:.1f}s |" | |
| ) | |
| lines.append( | |
| f"| **Total** | **{totals['total']}** | **{totals['passed']}** | " | |
| f"**{totals['failed']}** | **{totals['errored']}** | " | |
| f"**{totals['skipped']}** | **{totals['time']:.1f}s** |" | |
| ) | |
| lines.append("") | |
| output = "\n".join(lines) | |
| summary_path = os.environ.get("GITHUB_STEP_SUMMARY") | |
| if summary_path: | |
| with open(summary_path, "a", encoding="utf-8") as f: | |
| f.write(output + "\n") | |
| print(output) | |
| PYEOF | |
| # ────────────────────────────────────────────── | |
| # 9. Diagnostics on failure | |
| # ────────────────────────────────────────────── | |
| - name: Print container logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Platform logs ===" | |
| docker logs virtocommerce-vc-platform-web-1 --tail 100 | |
| echo "=== Elasticsearch logs ===" | |
| docker logs virtocommerce-es-1 --tail 50 |