Stabilize Github CI failurs #45
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: Compatibility Smoke Tests | |
| on: | |
| push: | |
| branches: [ master, develop ] | |
| pull_request: | |
| branches: [ master, develop ] | |
| workflow_dispatch: | |
| inputs: | |
| timeout: | |
| description: Seconds to wait for server ready | |
| required: false | |
| default: '180' | |
| permissions: | |
| contents: read | |
| env: | |
| DOTNET_VERSION: '10.0.x' | |
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| DOTNET_NOLOGO: true | |
| SMOKE_HTTPS_PORT: 8443 | |
| SMOKE_PG_PORT: 5433 | |
| SMOKE_USERNAME: smokeadmin | |
| SMOKE_PASSWORD: admin123 | |
| SMOKE_DATABASE: smokedb | |
| SMOKE_CERT_PASS: smoketest | |
| SERVER_READY_TIMEOUT: ${{ github.event.inputs.timeout || '180' }} | |
| jobs: | |
| compatibility-smoke: | |
| name: Server Compatibility Smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET ${{ env.DOTNET_VERSION }} | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install Python dependencies | |
| run: pip install requests --quiet | |
| - name: Restore server dependencies | |
| run: dotnet restore src/SharpCoreDB.Server/SharpCoreDB.Server.csproj | |
| - name: Build server | |
| run: dotnet build src/SharpCoreDB.Server/SharpCoreDB.Server.csproj --configuration Release --no-restore | |
| - name: Generate TLS development certificate | |
| run: | | |
| mkdir -p tests/CompatibilitySmoke/smoke-certs | |
| mkdir -p tests/CompatibilitySmoke/smoke-data | |
| mkdir -p tests/CompatibilitySmoke/smoke-logs | |
| dotnet dev-certs https \ | |
| -ep tests/CompatibilitySmoke/smoke-certs/smoke.pfx \ | |
| -p "${{ env.SMOKE_CERT_PASS }}" | |
| - name: Patch smoke server configuration | |
| shell: python3 {0} | |
| run: | | |
| import json, os, pathlib | |
| smoke_dir = pathlib.Path("tests/CompatibilitySmoke") | |
| with open(smoke_dir / "appsettings.smoke.json") as f: | |
| cfg = json.load(f) | |
| cert_path = str((smoke_dir / "smoke-certs/smoke.pfx").resolve()) | |
| data_path = str((smoke_dir / "smoke-data/smokedb.scdb").resolve()) | |
| log_path = str((smoke_dir / "smoke-logs/smoke.log").resolve()) | |
| cfg["Server"]["Security"]["TlsCertificatePath"] = cert_path | |
| cfg["Server"]["Databases"][0]["DatabasePath"] = data_path | |
| cfg["Server"]["Logging"]["FilePath"] = log_path | |
| cfg["Server"]["GrpcPort"] = 5001 | |
| cfg["Server"]["HttpsApiPort"] = int(os.environ["SMOKE_HTTPS_PORT"]) | |
| cfg["Server"]["BinaryProtocolPort"] = int(os.environ["SMOKE_PG_PORT"]) | |
| out = smoke_dir / "appsettings.smoke.ci.json" | |
| with open(out, "w") as f: | |
| json.dump(cfg, f, indent=2) | |
| print(f"Patched config written to {out}") | |
| - name: Start SharpCoreDB Server (background) | |
| run: | | |
| dotnet run \ | |
| --project src/SharpCoreDB.Server/SharpCoreDB.Server.csproj \ | |
| --configuration Release \ | |
| --no-build \ | |
| -- --appsettings "$(pwd)/tests/CompatibilitySmoke/appsettings.smoke.ci.json" \ | |
| > tests/CompatibilitySmoke/smoke-logs/server-stdout.log 2>&1 & | |
| echo "SERVER_PID=$!" >> "$GITHUB_ENV" | |
| echo "Server started in background (PID: $!)" | |
| - name: Wait for server to be healthy | |
| shell: python3 {0} | |
| run: | | |
| import ssl, time, sys, urllib.request, urllib.error, os | |
| port = int(os.environ["SMOKE_HTTPS_PORT"]) | |
| timeout = int(os.environ["SERVER_READY_TIMEOUT"]) | |
| server_pid_raw = os.environ.get("SERVER_PID", "").strip() | |
| server_pid = int(server_pid_raw) if server_pid_raw.isdigit() else None | |
| log_path = "tests/CompatibilitySmoke/smoke-logs/server-stdout.log" | |
| urls = [ | |
| f"https://127.0.0.1:{port}/api/v1/health", | |
| f"https://127.0.0.1:{port}/health" | |
| ] | |
| ctx = ssl.create_default_context() | |
| ctx.check_hostname = False | |
| ctx.verify_mode = ssl.CERT_NONE | |
| def is_process_alive(pid: int | None) -> bool: | |
| if pid is None: | |
| return True | |
| try: | |
| os.kill(pid, 0) | |
| return True | |
| except OSError: | |
| return False | |
| def print_log_tail(path: str, max_lines: int = 120) -> None: | |
| print("=== Server stdout/stderr tail ===", file=sys.stderr) | |
| try: | |
| with open(path, "r", encoding="utf-8", errors="replace") as f: | |
| lines = f.readlines() | |
| for line in lines[-max_lines:]: | |
| print(line.rstrip("\n"), file=sys.stderr) | |
| except Exception as ex: | |
| print(f"Unable to read server log tail from {path}: {ex}", file=sys.stderr) | |
| print(f"Polling {urls[0]} / fallback {urls[1]} for up to {timeout}s …", flush=True) | |
| deadline = time.monotonic() + timeout | |
| last_error = None | |
| while time.monotonic() < deadline: | |
| if not is_process_alive(server_pid): | |
| print(f"Server process exited early (PID: {server_pid_raw or 'unknown'}) before readiness.", file=sys.stderr) | |
| print_log_tail(log_path) | |
| sys.exit(1) | |
| for url in urls: | |
| try: | |
| with urllib.request.urlopen(url, context=ctx, timeout=3) as r: | |
| if r.status == 200: | |
| print(f"Server is healthy via {url}", flush=True) | |
| sys.exit(0) | |
| except Exception as e: | |
| last_error = e | |
| time.sleep(2) | |
| print(f"Server not ready after {timeout}s — failing.", file=sys.stderr) | |
| if last_error is not None: | |
| print(f"Last readiness error: {last_error}", file=sys.stderr) | |
| print_log_tail(log_path) | |
| sys.exit(1) | |
| - name: Dump server logs on readiness failure | |
| if: failure() | |
| run: | | |
| echo "=== Server stdout/stderr (tail -200) ===" | |
| tail -n 200 tests/CompatibilitySmoke/smoke-logs/server-stdout.log || true | |
| echo "=== Smoke log file (tail -200) ===" | |
| tail -n 200 tests/CompatibilitySmoke/smoke-logs/smoke.log || true | |
| - name: Run compatibility smoke tests | |
| run: | | |
| python3 tests/CompatibilitySmoke/smoke_tests.py \ | |
| --host 127.0.0.1 \ | |
| --https-port "$SMOKE_HTTPS_PORT" \ | |
| --pg-port "$SMOKE_PG_PORT" \ | |
| --username "$SMOKE_USERNAME" \ | |
| --password "$SMOKE_PASSWORD" \ | |
| --database "$SMOKE_DATABASE" \ | |
| --no-verify-tls \ | |
| --output tests/CompatibilitySmoke/smoke-results.json \ | |
| --timeout 30 | |
| - name: Print smoke results summary | |
| if: always() | |
| run: | | |
| if [ -f tests/CompatibilitySmoke/smoke-results.json ]; then | |
| echo "=== Smoke Test Results ===" | |
| python3 -c " | |
| import json, sys | |
| with open('tests/CompatibilitySmoke/smoke-results.json') as f: | |
| r = json.load(f) | |
| print(f\"Passed: {r['passed']} Failed: {r['failed']}\") | |
| for t in r['results']: | |
| icon = '✓' if t['passed'] else '✗' | |
| ms = f\" {t['elapsed_ms']:.0f}ms\" if t['elapsed_ms'] else '' | |
| print(f\" {icon} {t['name']}{ms}\") | |
| if not t['passed'] and t['detail']: | |
| print(f\" → {t['detail']}\") | |
| " | |
| fi | |
| - name: Stop server | |
| if: always() | |
| run: | | |
| if [ -n "${SERVER_PID:-}" ]; then | |
| kill "$SERVER_PID" 2>/dev/null || true | |
| echo "Server stopped." | |
| fi | |
| - name: Upload smoke test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: smoke-test-results-${{ github.run_number }} | |
| retention-days: 14 | |
| path: | | |
| tests/CompatibilitySmoke/smoke-results.json | |
| tests/CompatibilitySmoke/smoke-logs/ |