From f734908f9c0938e5eb0f0569f09b6c2a92630a50 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 12:50:56 +0200 Subject: [PATCH 1/9] fix(ci): generate self-signed certificate for E2E tests The workflow referenced localho.st.crt/key but never created them, causing the "Install certificate" step to fail immediately. --- .github/workflows/e2e-tests.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6fe9a07c..dcf80300 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -97,6 +97,13 @@ jobs: 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 From 8305086870e59b0755a0c44dd816b21cfa008ccb Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 13:26:31 +0200 Subject: [PATCH 2/9] fix(ci): redirect temps serve logs, sync runtime files - Redirect temps serve stdout/stderr to /tmp/temps.log for debugging - Copy encryption_key and symlink GeoLite2 to working directory - Detect immediate crash and show logs before health check timeout - Show actual temps logs in failure collection step --- .github/workflows/e2e-tests.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index dcf80300..9cce96c7 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -135,6 +135,13 @@ jobs: --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 \ @@ -143,9 +150,17 @@ jobs: --address 0.0.0.0:3000 \ --tls-address 0.0.0.0:3443 \ --disable-https-redirect \ - --screenshot-provider noop & + --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: | @@ -378,6 +393,14 @@ jobs: 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" From f8d98b4751f605f428b0dd1143fb58730da462e2 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 13:57:41 +0200 Subject: [PATCH 3/9] fix(ci): use console port and correct API paths for E2E tests - Add --console-address 0.0.0.0:8081 to temps serve - Change API_BASE to http://localhost:8081/api (console serves API) - Health check now polls the console port instead of proxy port - Fixes jq parse error caused by proxy returning HTML instead of JSON --- .github/workflows/e2e-tests.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9cce96c7..b7fc39f4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -15,7 +15,6 @@ jobs: e2e-test: name: E2E Deployment Tests runs-on: ubuntu-latest - # Run after the test build job from rust-tests.yml to reuse its Rust cache needs: [] timeout-minutes: 60 @@ -40,7 +39,7 @@ jobs: TEMPS_DATA_DIR: /tmp/temps-data ADMIN_EMAIL: admin@localho.st ADMIN_PASSWORD: E2eTestPass123! - API_BASE: http://localhost:3000 + API_BASE: http://localhost:8081/api steps: - name: Free up disk space @@ -60,12 +59,11 @@ jobs: with: bun-version: latest - # Reuse the same Rust dependency cache as rust-tests.yml build-tests job - - name: Restore Rust cache + - name: Rust cache (release build) uses: Swatinem/rust-cache@v2 with: - shared-key: test-build - save-if: false + shared-key: e2e-release + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Cache Bun dependencies uses: actions/cache@v4 @@ -75,12 +73,21 @@ jobs: 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun install bun run build @@ -149,6 +156,7 @@ jobs: --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 & @@ -165,9 +173,9 @@ jobs: - name: Wait for platform health run: | echo "Waiting for Temps to become healthy..." - timeout 120 bash -c 'until curl -sf http://localhost:3000/health > /dev/null 2>&1; do sleep 2; done' + timeout 120 bash -c 'until curl -sf http://localhost:8081/ > /dev/null 2>&1; do sleep 2; done' echo "Platform is healthy" - curl -s http://localhost:3000/health | head -c 200 + curl -s -o /dev/null -w "Console HTTP %{http_code}" http://localhost:8081/ echo "" - name: Authenticate and create API key @@ -201,7 +209,7 @@ jobs: exit 1 fi - API_KEY=$(echo "$APIKEY_BODY" | jq -r '.api_key') + API_KEY=$(echo "$APIKEY_BODY" | jq -r '.api_key' 2>/dev/null || true) if [ -z "$API_KEY" ] || [ "$API_KEY" = "null" ]; then echo "Failed to extract API key from response" echo "$APIKEY_BODY" From fe713288b81e861e6c1c3e1f09494f2181a3cfbe Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 14:30:45 +0200 Subject: [PATCH 4/9] fix(ci): use /environments endpoint to get environment ID GET /projects/{id} does not embed environments. Use the dedicated /projects/{id}/environments endpoint instead. --- .github/workflows/e2e-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b7fc39f4..776fb00b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -39,7 +39,7 @@ jobs: TEMPS_DATA_DIR: /tmp/temps-data ADMIN_EMAIL: admin@localho.st ADMIN_PASSWORD: E2eTestPass123! - API_BASE: http://localhost:8081/api + API_BASE: http://localhost:8081 steps: - name: Free up disk space @@ -285,8 +285,8 @@ jobs: # --- Get production environment ID --- ENV_RESPONSE=$(curl -s \ -H "Authorization: Bearer $API_KEY" \ - "$API_BASE/projects/$PROJECT_ID") - ENV_ID=$(echo "$ENV_RESPONSE" | jq -r '.environments[0].id // empty') + "$API_BASE/projects/$PROJECT_ID/environments") + ENV_ID=$(echo "$ENV_RESPONSE" | jq -r '.[0].id // .environments[0].id // empty') if [ -z "$ENV_ID" ]; then echo "FAIL: Could not find environment for project $PROJECT_ID" From 1646e6a54c616ce4b3ab93bbb60d99f620f8d23b Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 14:49:44 +0200 Subject: [PATCH 5/9] ci: cache wasm-pack binary across all workflow jobs Skip `cargo install wasm-pack` (~2 min) when the binary is already cached. Applies to all 5 jobs in rust-tests.yml that install it. --- .github/workflows/rust-tests.yml | 50 ++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index 2aa64b10..cf71a578 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -30,12 +30,20 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 + - name: Cache wasm-pack binary + 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun run build @@ -79,12 +87,20 @@ jobs: - name: Cache dependencies uses: Swatinem/rust-cache@v2 + - name: Cache wasm-pack binary + 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun run build @@ -122,12 +138,20 @@ jobs: shared-key: test-build save-if: true + - name: Cache wasm-pack binary + 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun run build @@ -205,12 +229,20 @@ jobs: shared-key: test-build save-if: false + - name: Cache wasm-pack binary + 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun run build @@ -340,12 +372,20 @@ jobs: shared-key: test-build save-if: false + - name: Cache wasm-pack binary + 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: | - cargo install wasm-pack + if [ ! -f ~/.cargo/bin/wasm-pack ]; then + cargo install wasm-pack + fi cd crates/temps-captcha-wasm bun run build From b84670bd25d637fd8d71dad2b5e0be6b0b592537 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 15:03:26 +0200 Subject: [PATCH 6/9] fix(ci): use CLI to create API key instead of HTTP session auth Session cookie auth was returning HTML instead of JSON for the api-keys endpoint. Use `temps api-key` CLI command directly which accesses the database and avoids HTTP auth complexity. --- .github/workflows/e2e-tests.yml | 46 +++++++++------------------------ 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 776fb00b..59dd0912 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -178,45 +178,23 @@ jobs: curl -s -o /dev/null -w "Console HTTP %{http_code}" http://localhost:8081/ echo "" - - name: Authenticate and create API key + - name: Create API key via CLI id: auth run: | - # Login to get session cookie - LOGIN_RESPONSE=$(curl -s -w "\n%{http_code}" -c /tmp/cookies.txt \ - -X POST "$API_BASE/auth/login" \ - -H "Content-Type: application/json" \ - -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}") - - HTTP_CODE=$(echo "$LOGIN_RESPONSE" | tail -1) - if [ "$HTTP_CODE" != "200" ]; then - echo "Login failed with HTTP $HTTP_CODE" - echo "$LOGIN_RESPONSE" | head -n -1 - exit 1 - fi - echo "Login successful" - - # Create API key using session cookie - APIKEY_RESPONSE=$(curl -s -w "\n%{http_code}" -b /tmp/cookies.txt \ - -X POST "$API_BASE/api-keys" \ - -H "Content-Type: application/json" \ - -d '{"name":"e2e-test","role_type":"admin"}') - - HTTP_CODE=$(echo "$APIKEY_RESPONSE" | tail -1) - APIKEY_BODY=$(echo "$APIKEY_RESPONSE" | head -n -1) - if [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "200" ]; then - echo "API key creation failed with HTTP $HTTP_CODE" - echo "$APIKEY_BODY" - exit 1 - fi - - API_KEY=$(echo "$APIKEY_BODY" | jq -r '.api_key' 2>/dev/null || true) - if [ -z "$API_KEY" ] || [ "$API_KEY" = "null" ]; then - echo "Failed to extract API key from response" - echo "$APIKEY_BODY" + 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 successfully" + echo "API key created: ${API_KEY:0:12}..." echo "api_key=$API_KEY" >> $GITHUB_OUTPUT - name: Deploy example applications From 9aebbdffcc8971c8445408893ffb3fb20813ad12 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 15:44:18 +0200 Subject: [PATCH 7/9] fix(ci): restore /api prefix and harden jq calls in deploy step API_BASE must include /api since plugin routes are nested under /api by build_application(). Without it, requests hit the SPA fallback (200 HTML) and jq fails parsing. Also added error handling to all jq calls in the deploy step to surface response bodies on failure. --- .github/workflows/e2e-tests.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 59dd0912..f31fef3a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -39,7 +39,7 @@ jobs: TEMPS_DATA_DIR: /tmp/temps-data ADMIN_EMAIL: admin@localho.st ADMIN_PASSWORD: E2eTestPass123! - API_BASE: http://localhost:8081 + API_BASE: http://localhost:8081/api steps: - name: Free up disk space @@ -257,14 +257,21 @@ jobs: continue fi - PROJECT_ID=$(echo "$CREATE_BODY" | jq -r '.id') + 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') + 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" @@ -304,8 +311,8 @@ jobs: -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"') - DEPLOY_ID=$(echo "$DEPLOY_LIST" | jq -r '.deployments[0].id // empty') + 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" = "running" ] || [ "$DEPLOY_STATE" = "deployed" ] || [ "$DEPLOY_STATE" = "completed" ]; then echo "Deployment $DEPLOY_ID reached state: $DEPLOY_STATE" From 2c8c6879b5d218bd0357219cd4f30a64a1f46ea6 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 16:21:28 +0200 Subject: [PATCH 8/9] fix(deployments): include deployment ID in temp directory path The clone directory used only a unix timestamp (second granularity), so concurrent deployments starting in the same second would collide on the same path. Include the deployment ID to guarantee uniqueness. --- crates/temps-deployments/src/jobs/download_repo.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/temps-deployments/src/jobs/download_repo.rs b/crates/temps-deployments/src/jobs/download_repo.rs index c7c10eb2..3fe1f5f2 100644 --- a/crates/temps-deployments/src/jobs/download_repo.rs +++ b/crates/temps-deployments/src/jobs/download_repo.rs @@ -193,8 +193,9 @@ impl DownloadRepoJob { } /// Create temporary directory for repository - /// Uses unix epoch timestamp to avoid conflicts when reinstalling temps with reused deployment IDs - fn create_temp_dir(&self, _context: &WorkflowContext) -> Result { + /// Uses deployment ID + timestamp to guarantee uniqueness across concurrent deployments + /// and across reinstalls with reused deployment IDs + fn create_temp_dir(&self, context: &WorkflowContext) -> Result { use std::time::SystemTime; let unix_epoch = SystemTime::now() @@ -202,8 +203,10 @@ impl DownloadRepoJob { .map_err(|e| WorkflowError::Other(format!("Failed to get unix timestamp: {}", e)))? .as_secs(); - let temp_dir = std::path::PathBuf::from("/tmp/temps-deployments") - .join(format!("deployment-{}", unix_epoch)); + let temp_dir = std::path::PathBuf::from("/tmp/temps-deployments").join(format!( + "deployment-{}-{}", + context.deployment_id, unix_epoch + )); std::fs::create_dir_all(&temp_dir).map_err(WorkflowError::IoError)?; Ok(temp_dir) } From 3f1e3c098db7a08453626355c6327966387ad31f Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 29 Mar 2026 16:23:39 +0200 Subject: [PATCH 9/9] test(deployments): add regression test for temp dir uniqueness Verifies that concurrent deployments with different IDs produce distinct clone directory paths, preventing the directory collision bug from recurring. --- .../src/jobs/download_repo.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/temps-deployments/src/jobs/download_repo.rs b/crates/temps-deployments/src/jobs/download_repo.rs index 3fe1f5f2..76b7d6cc 100644 --- a/crates/temps-deployments/src/jobs/download_repo.rs +++ b/crates/temps-deployments/src/jobs/download_repo.rs @@ -890,4 +890,48 @@ mod tests { let context = crate::test_utils::create_test_context("test".to_string(), 1, 1, 1); assert_eq!(job.get_checkout_ref(&context), "v2.0.0"); } + + #[test] + fn test_create_temp_dir_unique_per_deployment() { + let git_manager: Arc = Arc::new(MockGitProviderManager); + + let job = DownloadRepoJob::new( + "test".to_string(), + "owner".to_string(), + "repo".to_string(), + 1, + git_manager, + ); + + // Two contexts with different deployment IDs + let ctx_a = crate::test_utils::create_test_context("wf-a".to_string(), 100, 1, 1); + let ctx_b = crate::test_utils::create_test_context("wf-b".to_string(), 200, 1, 1); + + let dir_a = job.create_temp_dir(&ctx_a).unwrap(); + let dir_b = job.create_temp_dir(&ctx_b).unwrap(); + + // Directories must be different even when created in the same second + assert_ne!( + dir_a, dir_b, + "Different deployment IDs must produce different paths" + ); + + // Both should contain their deployment ID + let dir_a_str = dir_a.to_string_lossy(); + let dir_b_str = dir_b.to_string_lossy(); + assert!( + dir_a_str.contains("deployment-100-"), + "Path should contain deployment ID: {}", + dir_a_str + ); + assert!( + dir_b_str.contains("deployment-200-"), + "Path should contain deployment ID: {}", + dir_b_str + ); + + // Cleanup + let _ = std::fs::remove_dir_all(&dir_a); + let _ = std::fs::remove_dir_all(&dir_b); + } }