fix(logs): filter history logs by deployment id (uuid → i32) (#131) #413
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: E2E Tests | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| e2e-test: | |
| name: E2E Deployment Tests | |
| runs-on: ubuntu-latest | |
| needs: [] | |
| timeout-minutes: 60 | |
| services: | |
| timescaledb: | |
| image: timescale/timescaledb-ha:pg18 | |
| env: | |
| POSTGRES_DB: temps | |
| POSTGRES_USER: temps | |
| POSTGRES_PASSWORD: temps | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U temps -d temps" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| DATABASE_URL: postgresql://temps:temps@localhost:5432/temps | |
| TEMPS_DATA_DIR: /tmp/temps-data | |
| ADMIN_EMAIL: admin@localho.st | |
| ADMIN_PASSWORD: E2eTestPass123! | |
| API_BASE: http://localhost:8081/api | |
| steps: | |
| - name: Free up disk space | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL | |
| sudo docker image prune --all --force | |
| sudo apt-get autoremove -y && sudo apt-get clean | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Rust cache (release build) | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: e2e-release | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: web/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('web/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Cache wasm-pack binary | |
| id: wasm-pack-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cargo/bin/wasm-pack | |
| key: ${{ runner.os }}-wasm-pack-0.13 | |
| - name: Install system dependencies | |
| run: sudo apt-get update && sudo apt-get install -y protobuf-compiler | |
| - name: Install wasm-pack and build WASM | |
| run: | | |
| if [ ! -f ~/.cargo/bin/wasm-pack ]; then | |
| cargo install wasm-pack | |
| fi | |
| cd crates/temps-captcha-wasm | |
| bun install | |
| bun run build | |
| - name: Build Web UI | |
| run: | | |
| cd web | |
| bun install | |
| RSBUILD_OUTPUT_PATH=../crates/temps-cli/dist bun run build | |
| - name: Build release binary | |
| run: cargo build --release --bin temps | |
| env: | |
| FORCE_WEB_BUILD: 1 | |
| CARGO_INCREMENTAL: 0 | |
| - name: Generate self-signed localho.st certificate | |
| run: | | |
| openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ | |
| -keyout localho.st.key -out localho.st.crt \ | |
| -days 1 -nodes -subj "/CN=localho.st" \ | |
| -addext "subjectAltName=DNS:localho.st,DNS:*.localho.st" | |
| - name: Install localho.st certificate into trust store | |
| run: | | |
| sudo cp localho.st.crt /usr/local/share/ca-certificates/localho.st.crt | |
| sudo update-ca-certificates | |
| - name: Prepare data directory | |
| run: | | |
| mkdir -p $TEMPS_DATA_DIR | |
| # GeoLite2 database is checked into the repo | |
| cp crates/temps-cli/GeoLite2-City.mmdb $TEMPS_DATA_DIR/GeoLite2-City.mmdb | |
| - name: Verify TimescaleDB is ready | |
| run: | | |
| timeout 60 bash -c 'until nc -z localhost 5432; do sleep 1; done' | |
| echo "TimescaleDB is ready" | |
| - name: Run temps setup | |
| run: | | |
| ./target/release/temps setup --non-interactive \ | |
| --database-url "$DATABASE_URL" \ | |
| --data-dir "$TEMPS_DATA_DIR" \ | |
| --admin-email "$ADMIN_EMAIL" \ | |
| --admin-password "$ADMIN_PASSWORD" \ | |
| --wildcard-domain "*.localho.st" \ | |
| --wildcard-domain-cert localho.st.crt \ | |
| --wildcard-domain-key localho.st.key \ | |
| --skip-dns-records \ | |
| --skip-git \ | |
| --skip-geolite2-download \ | |
| --output-format json | |
| - name: Prepare temps runtime files | |
| run: | | |
| # Sync encryption key to working directory (temps serve reads from cwd) | |
| cp "$TEMPS_DATA_DIR/encryption_key" ./encryption_key 2>/dev/null || true | |
| # Symlink GeoLite2 to working directory | |
| ln -sf "$TEMPS_DATA_DIR/GeoLite2-City.mmdb" ./GeoLite2-City.mmdb 2>/dev/null || true | |
| - name: Start temps serve | |
| run: | | |
| ./target/release/temps serve \ | |
| --database-url "$DATABASE_URL" \ | |
| --data-dir "$TEMPS_DATA_DIR" \ | |
| --address 0.0.0.0:3000 \ | |
| --tls-address 0.0.0.0:3443 \ | |
| --console-address 0.0.0.0:8081 \ | |
| --disable-https-redirect \ | |
| --screenshot-provider noop \ | |
| > /tmp/temps.log 2>&1 & | |
| echo $! > /tmp/temps.pid | |
| echo "Temps server started with PID $(cat /tmp/temps.pid)" | |
| # Give it a moment to either start or crash | |
| sleep 3 | |
| if ! kill -0 $(cat /tmp/temps.pid) 2>/dev/null; then | |
| echo "ERROR: Temps server died immediately. Logs:" | |
| cat /tmp/temps.log | |
| exit 1 | |
| fi | |
| - name: Wait for platform health | |
| run: | | |
| echo "Waiting for Temps to become healthy..." | |
| timeout 120 bash -c 'until curl -sf http://localhost:8081/ > /dev/null 2>&1; do sleep 2; done' | |
| echo "Platform is healthy" | |
| curl -s -o /dev/null -w "Console HTTP %{http_code}" http://localhost:8081/ | |
| echo "" | |
| - name: Create API key via CLI | |
| id: auth | |
| run: | | |
| API_OUTPUT=$(./target/release/temps api-key \ | |
| --database-url "$DATABASE_URL" \ | |
| --name "e2e-test" \ | |
| --role admin \ | |
| --output-format json 2>&1) | |
| echo "CLI output: $API_OUTPUT" | |
| API_KEY=$(echo "$API_OUTPUT" | jq -r '.api_key // empty' 2>/dev/null) | |
| if [ -z "$API_KEY" ]; then | |
| echo "Failed to extract API key from CLI output" | |
| exit 1 | |
| fi | |
| echo "API key created: ${API_KEY:0:12}..." | |
| echo "api_key=$API_KEY" >> $GITHUB_OUTPUT | |
| - name: Deploy example applications | |
| env: | |
| API_KEY: ${{ steps.auth.outputs.api_key }} | |
| run: | | |
| set -euo pipefail | |
| REPO_URL="https://github.com/${{ github.repository }}.git" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) | |
| BRANCH="${{ github.head_ref || github.ref_name }}" | |
| # Define example apps: name|directory|preset | |
| APPS=( | |
| "nextjs-e2e|examples/nextjs/basic|nextjs" | |
| "vite-e2e|examples/vite/react-basic|vite" | |
| "go-e2e|examples/go/gin-basic|go" | |
| ) | |
| RESULTS=() | |
| FAILED=0 | |
| for APP_DEF in "${APPS[@]}"; do | |
| IFS='|' read -r APP_NAME APP_DIR APP_PRESET <<< "$APP_DEF" | |
| echo "" | |
| echo "============================================" | |
| echo "Deploying: $APP_NAME (preset: $APP_PRESET)" | |
| echo " Directory: $APP_DIR" | |
| echo " Branch: $BRANCH" | |
| echo "============================================" | |
| # --- Create project --- | |
| CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" \ | |
| -X POST "$API_BASE/projects" \ | |
| -H "Authorization: Bearer $API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"name\": \"$APP_NAME\", | |
| \"repo_name\": \"$REPO_NAME\", | |
| \"repo_owner\": \"$REPO_OWNER\", | |
| \"directory\": \"$APP_DIR\", | |
| \"main_branch\": \"$BRANCH\", | |
| \"preset\": \"$APP_PRESET\", | |
| \"git_url\": \"$REPO_URL\", | |
| \"is_public_repo\": true, | |
| \"automatic_deploy\": true, | |
| \"storage_service_ids\": [], | |
| \"performance_metrics_enabled\": false | |
| }") | |
| HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -1) | |
| CREATE_BODY=$(echo "$CREATE_RESPONSE" | head -n -1) | |
| if [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "200" ]; then | |
| echo "FAIL: Project creation failed with HTTP $HTTP_CODE" | |
| echo "$CREATE_BODY" | jq . 2>/dev/null || echo "$CREATE_BODY" | |
| RESULTS+=("$APP_NAME: FAIL (project creation)") | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| PROJECT_ID=$(echo "$CREATE_BODY" | jq -r '.id' 2>/dev/null || true) | |
| if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then | |
| echo "FAIL: Could not parse project ID from response" | |
| echo "$CREATE_BODY" | |
| RESULTS+=("$APP_NAME: FAIL (parse project ID)") | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| echo "Project created: id=$PROJECT_ID" | |
| # --- Get production environment ID --- | |
| ENV_RESPONSE=$(curl -s \ | |
| -H "Authorization: Bearer $API_KEY" \ | |
| "$API_BASE/projects/$PROJECT_ID/environments") | |
| ENV_ID=$(echo "$ENV_RESPONSE" | jq -r '.[0].id // .environments[0].id // empty' 2>/dev/null || true) | |
| if [ -z "$ENV_ID" ]; then | |
| echo "FAIL: Could not find environment for project $PROJECT_ID" | |
| RESULTS+=("$APP_NAME: FAIL (no environment)") | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| echo "Environment ID: $ENV_ID" | |
| # Project creation emits a GitPushEvent which is gated by | |
| # automatic_deploy. We pass automatic_deploy=true above so the | |
| # event creates the initial deployment. | |
| echo "Waiting for auto-triggered deployment to be created..." | |
| sleep 5 | |
| # --- Poll deployment status --- | |
| # PipelineStatus enum: pending | running | built | completed | failed | cancelled | |
| # 'running' means the workflow is executing (build, deploy_container, etc). | |
| # 'completed' is the only terminal-success state — the route table is updated | |
| # and the container is serving traffic only after this. | |
| echo "Polling deployment status (timeout: 10 minutes)..." | |
| DEPLOY_STATE="pending" | |
| DEPLOY_ID="" | |
| DEADLINE=$((SECONDS + 600)) | |
| while [ $SECONDS -lt $DEADLINE ]; do | |
| DEPLOY_LIST=$(curl -s \ | |
| -H "Authorization: Bearer $API_KEY" \ | |
| "$API_BASE/projects/$PROJECT_ID/deployments?per_page=1") | |
| DEPLOY_STATE=$(echo "$DEPLOY_LIST" | jq -r '.deployments[0].status // "pending"' 2>/dev/null || echo "pending") | |
| DEPLOY_ID=$(echo "$DEPLOY_LIST" | jq -r '.deployments[0].id // empty' 2>/dev/null || true) | |
| if [ "$DEPLOY_STATE" = "completed" ]; then | |
| echo "Deployment $DEPLOY_ID reached terminal state: $DEPLOY_STATE" | |
| break | |
| fi | |
| if [ "$DEPLOY_STATE" = "failed" ] || [ "$DEPLOY_STATE" = "cancelled" ]; then | |
| echo "Deployment $DEPLOY_ID failed with state: $DEPLOY_STATE" | |
| if [ -n "$DEPLOY_ID" ]; then | |
| echo "--- Deployment jobs ---" | |
| curl -s -H "Authorization: Bearer $API_KEY" \ | |
| "$API_BASE/projects/$PROJECT_ID/deployments/$DEPLOY_ID/jobs" | jq '.[] | {name: .name, status: .status}' 2>/dev/null || true | |
| fi | |
| break | |
| fi | |
| echo " State: $DEPLOY_STATE (waiting for completed...)" | |
| sleep 15 | |
| done | |
| if [ "$DEPLOY_STATE" != "completed" ]; then | |
| echo "FAIL: Deployment did not reach completed state (last state: $DEPLOY_STATE)" | |
| RESULTS+=("$APP_NAME: FAIL (state: $DEPLOY_STATE)") | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| # --- Verify app is reachable --- | |
| # Even after deployment.status=completed the route table propagation and | |
| # container HTTP readiness can lag a few seconds; retry up to ~60s. | |
| APP_URL="https://$APP_NAME.localho.st:3443/" | |
| echo "Verifying app at $APP_URL ..." | |
| VERIFY_DEADLINE=$((SECONDS + 60)) | |
| VERIFY_CODE="000" | |
| while [ $SECONDS -lt $VERIFY_DEADLINE ]; do | |
| VERIFY_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$APP_URL" || echo "000") | |
| if [ "$VERIFY_CODE" = "200" ] || [ "$VERIFY_CODE" = "304" ]; then | |
| break | |
| fi | |
| echo " HTTP $VERIFY_CODE — waiting for app to be reachable..." | |
| sleep 5 | |
| done | |
| if [ "$VERIFY_CODE" = "200" ] || [ "$VERIFY_CODE" = "304" ]; then | |
| echo "PASS: $APP_NAME is reachable (HTTP $VERIFY_CODE)" | |
| RESULTS+=("$APP_NAME: PASS (HTTP $VERIFY_CODE)") | |
| else | |
| echo "FAIL: $APP_NAME not reachable (HTTP $VERIFY_CODE)" | |
| RESULTS+=("$APP_NAME: FAIL (HTTP $VERIFY_CODE)") | |
| FAILED=$((FAILED + 1)) | |
| fi | |
| done | |
| # --- Summary --- | |
| echo "" | |
| echo "============================================" | |
| echo "E2E Test Results" | |
| echo "============================================" | |
| for RESULT in "${RESULTS[@]}"; do | |
| echo " $RESULT" | |
| done | |
| echo "============================================" | |
| if [ $FAILED -gt 0 ]; then | |
| echo "FAILED: $FAILED app(s) failed" | |
| exit 1 | |
| else | |
| echo "ALL PASSED" | |
| fi | |
| - name: Collect logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Temps server logs (last 100 lines) ===" | |
| if [ -f /tmp/temps.log ]; then | |
| tail -100 /tmp/temps.log | |
| else | |
| echo "No temps log file found" | |
| fi | |
| echo "" | |
| echo "=== Temps process status ===" | |
| if [ -f /tmp/temps.pid ]; then | |
| PID=$(cat /tmp/temps.pid) | |
| echo "Temps PID: $PID" | |
| ps aux | grep temps || true | |
| fi | |
| echo "" | |
| echo "=== Docker containers ===" | |
| docker ps -a 2>/dev/null || true | |
| echo "" | |
| echo "=== Docker logs (last running containers) ===" | |
| for CID in $(docker ps -q --last 5 2>/dev/null); do | |
| echo "--- Container $CID ---" | |
| docker logs --tail 50 "$CID" 2>&1 || true | |
| done | |
| echo "" | |
| echo "=== Disk usage ===" | |
| df -h / | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| if [ -f /tmp/temps.pid ]; then | |
| kill $(cat /tmp/temps.pid) 2>/dev/null || true | |
| fi | |
| docker system prune -f 2>/dev/null || true |