refactor(ci): standardize workflows and add retry mechanisms and centralized config #255
Workflow file for this run
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
| # | |
| # .github/workflows/java-spring-ci.yml | |
| # Workflow for building and testing java-spring with BrowserStack integration | |
| # | |
| --- | |
| name: Java Spring CI | |
| on: | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| name: Lint (ubuntu-latest) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Create .env file (root) | |
| run: | | |
| echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env | |
| echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env | |
| echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env | |
| - name: Run linter (PMD + SpotBugs) | |
| working-directory: java-spring | |
| run: ./gradlew pmdMain pmdTest spotbugsMain | |
| build: | |
| name: Build (ubuntu-latest) | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Create .env file (root) | |
| run: | | |
| echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env | |
| echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env | |
| echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env | |
| - name: Build Spring Boot JAR | |
| working-directory: java-spring | |
| env: | |
| DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} | |
| DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} | |
| DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} | |
| DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} | |
| DITTO_ENABLE_CLOUD_SYNC: true | |
| run: ./gradlew bootJar -x test | |
| - name: Upload JAR artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: java-spring-jar-${{ github.run_number }} | |
| path: java-spring/build/libs/*.jar | |
| retention-days: 1 | |
| browserstack-test: | |
| name: BrowserStack Device Testing (macos-latest) | |
| runs-on: macos-latest | |
| needs: [build] | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| timeout-minutes: 150 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download JAR artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: java-spring-jar-${{ github.run_number }} | |
| path: java-spring/build/libs/ | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Create .env file (root) | |
| run: | | |
| echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env | |
| echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env | |
| echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env | |
| echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env | |
| - name: Install BrowserStack Local binary | |
| run: | | |
| curl -O "https://www.browserstack.com/browserstack-local/BrowserStackLocal-darwin-x64.zip" | |
| unzip BrowserStackLocal-darwin-x64.zip | |
| chmod +x BrowserStackLocal | |
| - name: Start BrowserStack Local tunnel | |
| run: | | |
| echo "Starting BrowserStack Local tunnel..." | |
| nohup ./BrowserStackLocal --key "${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| --daemon-mode > browserstack-local.log 2>&1 & | |
| # Wait for tunnel to establish | |
| TIMEOUT=60 | |
| ELAPSED=0 | |
| while [ $ELAPSED -lt $TIMEOUT ]; do | |
| if [ -f browserstack-local.log ] && grep -q "You can now access your local server(s) in our remote browser" browserstack-local.log; then | |
| echo "BrowserStack Local tunnel established" | |
| exit 0 | |
| fi | |
| if [ -f browserstack-local.log ] && grep -q -i "error\|failed\|unable" browserstack-local.log; then | |
| echo "BrowserStack Local tunnel failed:" | |
| cat browserstack-local.log | |
| exit 1 | |
| fi | |
| sleep 5 | |
| ELAPSED=$((ELAPSED + 5)) | |
| done | |
| echo "BrowserStack Local tunnel timeout after ${TIMEOUT}s" | |
| [ -f browserstack-local.log ] && cat browserstack-local.log | |
| exit 1 | |
| - name: Create BrowserStack config | |
| working-directory: java-spring | |
| run: | | |
| # Load platforms from centralized config and convert to YAML format | |
| PLATFORMS=$(jq -r '.["java-spring"].platforms[] | " - os: \(.os)\n osVersion: \"\(.osVersion)\"\n browserName: \(.browserName)\n browserVersion: \(.browserVersion)"' ../.github/browserstack-devices.json) | |
| cat > browserstack.yml << EOF | |
| userName: ${{ secrets.BROWSERSTACK_USERNAME }} | |
| accessKey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} | |
| projectName: QuickStart Java Spring | |
| buildName: CI Build #${{ github.run_number }} | |
| platforms: | |
| $PLATFORMS | |
| browserstackLocal: true | |
| debug: true | |
| video: true | |
| parallelsPerPlatform: 1 | |
| EOF | |
| - name: Start Spring Boot app in background | |
| working-directory: java-spring | |
| env: | |
| DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} | |
| DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} | |
| DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} | |
| DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} | |
| DITTO_ENABLE_CLOUD_SYNC: true | |
| run: | | |
| nohup java -jar build/libs/*.jar \ | |
| --server.port=8080 \ | |
| --spring.profiles.active=test > app.log 2>&1 & | |
| echo $! > app.pid | |
| # Wait for application to be ready | |
| echo "Waiting for Spring Boot app to start..." | |
| for i in {1..30}; do | |
| if curl -f http://localhost:8080 >/dev/null 2>&1; then | |
| echo "Spring Boot app is ready" | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| - name: Seed and execute Selenium tests on BrowserStack cloud browsers | |
| uses: nick-fields/retry@v3 | |
| env: | |
| BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} | |
| BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} | |
| BROWSERSTACK_LOCAL: true | |
| with: | |
| max_attempts: 5 | |
| timeout_minutes: 20 | |
| retry_wait_seconds: 900 | |
| command: | | |
| # Seed test task to Ditto Cloud | |
| echo "Seeding test task to Ditto Cloud..." | |
| TIMESTAMP=$(date +%s) | |
| INVERTED_TIMESTAMP=$((9999999999 - TIMESTAMP)) | |
| SEED_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H 'Content-type: application/json' \ | |
| -H "Authorization: Bearer ${{ secrets.DITTO_API_KEY }}" \ | |
| -d "{ | |
| \"statement\": \"INSERT INTO tasks DOCUMENTS (:newTask) ON ID CONFLICT DO UPDATE\", | |
| \"args\": { | |
| \"newTask\": { | |
| \"_id\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"title\": \"${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}\", | |
| \"done\": false, | |
| \"deleted\": false | |
| } | |
| } | |
| }" \ | |
| "https://${{ secrets.DITTO_API_URL }}/api/v4/store/execute") | |
| HTTP_CODE=$(echo "$SEED_RESPONSE" | tail -n1) | |
| BODY=$(echo "$SEED_RESPONSE" | sed '$d') | |
| if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ]; then | |
| TASK_TITLE="${INVERTED_TIMESTAMP}_java-spring_ci_test_${{ github.run_id }}_${{ github.run_number }}" | |
| echo "Seeded task: $TASK_TITLE" | |
| else | |
| echo "Error: Failed to seed task. HTTP Status: $HTTP_CODE" | |
| echo "Response: $BODY" | |
| exit 1 | |
| fi | |
| # Export as environment variable (for System.getenv()) | |
| export DITTO_CLOUD_TASK_TITLE="$TASK_TITLE" | |
| # Run BrowserStack Selenium tests | |
| cd java-spring | |
| ./gradlew test --tests "*TaskVisibilityIntegrationTest.shouldPassWithExistingTask" \ | |
| -DBROWSERSTACK_USERNAME="${{ secrets.BROWSERSTACK_USERNAME }}" \ | |
| -DBROWSERSTACK_ACCESS_KEY="${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ | |
| -DBROWSERSTACK_BUILD_NAME="CI Build #${{ github.run_number }}" \ | |
| -DBROWSERSTACK_LOCAL=true \ | |
| -DDITTO_CLOUD_TASK_TITLE="$TASK_TITLE" \ | |
| --info | |
| - name: Stop Spring Boot app | |
| if: always() | |
| working-directory: java-spring | |
| run: | | |
| if [ -f app.pid ]; then | |
| kill $(cat app.pid) || true | |
| rm app.pid | |
| fi | |
| - name: Stop BrowserStack Local tunnel | |
| if: always() | |
| run: pkill -f "BrowserStackLocal" || true | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: browserstack-test-reports-${{ github.run_number }} | |
| path: java-spring/build/reports/tests/ | |
| retention-days: 1 | |
| - name: Upload app logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: spring-boot-logs-${{ github.run_number }} | |
| path: | | |
| java-spring/app.log | |
| browserstack-local.log | |
| retention-days: 1 | |
| summary: | |
| name: CI Report | |
| runs-on: ubuntu-latest | |
| needs: [browserstack-test] | |
| if: always() | |
| steps: | |
| - name: Report Results | |
| run: | | |
| echo "## ☕ Java Spring CI" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Overall status | |
| if [[ "${{ needs.lint.result }}" == "success" && \ | |
| "${{ needs.build.result }}" == "success" && \ | |
| "${{ needs.browserstack-test.result }}" == "success" ]]; then | |
| echo "**Overall Status:** ✅ All checks passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "**Overall Status:** ❌ Failed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint | ${{ needs.lint.result == 'success' && '✅ Passed' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Build | ${{ needs.build.result == 'success' && '✅ Passed' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| BrowserStack Tests | ${{ needs.browserstack-test.result == 'success' && '✅ Passed' || (needs.browserstack-test.result == 'skipped' && '⏭️ Skipped') || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # BrowserStack link | |
| if [[ "${{ needs.browserstack-test.result }}" != "skipped" ]]; then | |
| echo "### BrowserStack Session" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "🔗 [View Test Results](https://automate.browserstack.com/builds?project=QuickStart+Java+Spring&build=CI+Build+%23${{ github.run_number }})" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Tested Browser:**" >> $GITHUB_STEP_SUMMARY | |
| echo "- Chrome Latest (Windows 11)" >> $GITHUB_STEP_SUMMARY | |
| fi |