diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6fe9a07c..f31fef3a 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 @@ -97,6 +104,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 @@ -128,6 +142,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 \ @@ -135,58 +156,45 @@ 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 & + --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: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 + - 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') - 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 @@ -249,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") - 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' 2>/dev/null || true) if [ -z "$ENV_ID" ]; then echo "FAIL: Could not find environment for project $PROJECT_ID" @@ -296,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" @@ -371,6 +386,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" 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 diff --git a/crates/temps-deployments/src/jobs/download_repo.rs b/crates/temps-deployments/src/jobs/download_repo.rs index c7c10eb2..76b7d6cc 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) } @@ -887,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); + } }