Merge pull request #40 from Refraggerator/fix/visual-tests-fixes #65
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: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| env: | |
| APP_PATH: './app' | |
| AS_PATH: './server' # Concord Application Service (Go) | |
| FLUTTER_VERSION: 'stable' | |
| DOCKER_IMAGE: 'concord-as' # Go Application Service image name on Docker Hub | |
| jobs: | |
| # ==================== QA STAGE ==================== | |
| flutter_tests: | |
| name: Flutter Unit Tests & Analysis | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Install dependencies | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter pub get | |
| - name: Analyze code | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter analyze | tee ../analyze-output.txt | |
| continue-on-error: true | |
| - name: Run unit and widget tests | |
| working-directory: ${{ env.APP_PATH }} | |
| # Explicitly list test directories to exclude test/integration/ which | |
| # requires a live Docker stack and is run in the integration_tests job. | |
| run: flutter test test/features test/core test/shared --coverage --reporter expanded | |
| - name: Install lcov | |
| if: always() | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y lcov | |
| - name: Generate coverage summary | |
| if: always() | |
| working-directory: ${{ env.APP_PATH }} | |
| run: lcov --summary coverage/lcov.info 2>&1 | tee ../coverage-summary.txt | |
| continue-on-error: true | |
| - name: Upload analysis reports | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: analysis-reports | |
| path: | | |
| analyze-output.txt | |
| coverage-summary.txt | |
| retention-days: 30 | |
| continue-on-error: true | |
| - name: Upload coverage report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: flutter-coverage-report | |
| path: ${{ env.APP_PATH }}/coverage/lcov.info | |
| retention-days: 30 | |
| continue-on-error: true | |
| go_tests: | |
| name: Go Application Service Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24' | |
| cache-dependency-path: ${{ env.AS_PATH }}/go.sum | |
| - name: Run Go tests | |
| working-directory: ${{ env.AS_PATH }} | |
| run: | | |
| go test -v -coverprofile=coverage.out ./... | |
| go tool cover -func=coverage.out | grep total: | awk '{print $3}' > coverage-summary.txt | |
| - name: Upload coverage report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: go-coverage-report | |
| path: | | |
| ${{ env.AS_PATH }}/coverage.out | |
| ${{ env.AS_PATH }}/coverage-summary.txt | |
| retention-days: 30 | |
| continue-on-error: true | |
| integration_tests: | |
| name: Integration Tests | |
| needs: [flutter_tests, go_tests] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Install Linux dependencies | |
| run: | | |
| sudo apt-get update -y | |
| sudo apt-get install -y \ | |
| clang cmake ninja-build pkg-config \ | |
| libgtk-3-dev liblzma-dev libstdc++-12-dev \ | |
| xvfb x11-utils \ | |
| libegl1-mesa-dev libgles2-mesa-dev libgl1-mesa-dri libgl1 mesa-utils \ | |
| pulseaudio dbus-x11 \ | |
| libolm3 libolm-dev | |
| - name: Install Flutter dependencies | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter pub get | |
| - name: Copy .env for integration stack | |
| run: cp .env.example .env | |
| - name: Build Application Service Docker image | |
| run: docker build -t ${{ env.DOCKER_IMAGE }}:latest -f ${{ env.AS_PATH }}/Dockerfile ${{ env.AS_PATH }} | |
| - name: Start full stack | |
| env: | |
| DOCKER_IMAGE: ${{ env.DOCKER_IMAGE }} | |
| DOCKER_TAG: latest | |
| run: | | |
| docker compose --env-file .env up -d | |
| echo "Waiting for initial startup..." | |
| sleep 10 | |
| - name: Wait for all services to be healthy | |
| run: | | |
| wait_for() { | |
| local name=$1 cmd=$2 | |
| for i in $(seq 1 40); do | |
| if eval "$cmd" > /dev/null 2>&1; then echo "$name is ready!"; return 0; fi | |
| [ $i -eq 40 ] && { echo "ERROR: $name failed to become ready"; return 1; } | |
| echo "Waiting for $name... ($i/40)"; sleep 3 | |
| done | |
| } | |
| wait_for "PostgreSQL" "docker exec concord-db pg_isready -U concord_user" | |
| wait_for "Redis" "docker exec concord-redis redis-cli ping" | |
| wait_for "MinIO" "curl -sf http://localhost:9000/minio/health/live" | |
| wait_for "LiveKit" "nc -z localhost 7880" | |
| wait_for "Synapse" "curl -sf http://localhost:8008/health" | |
| wait_for "Concord AS" "curl -sf http://localhost:3000/health" | |
| echo "All services are ready!" | |
| - name: Setup virtual display and audio | |
| run: | | |
| pulseaudio --start --exit-idle-time=-1 || true | |
| sudo Xvfb -ac :99 -screen 0 1920x1080x24 > /dev/null 2>&1 & | |
| sleep 3 | |
| echo "DISPLAY=:99" >> $GITHUB_ENV | |
| xdpyinfo -display :99 > /dev/null 2>&1 && echo "Display :99 ready" || echo "WARNING: display not ready" | |
| - name: Run integration tests | |
| working-directory: ${{ env.APP_PATH }} | |
| timeout-minutes: 30 | |
| env: | |
| DISPLAY: ':99' | |
| LIBGL_ALWAYS_SOFTWARE: '1' | |
| MESA_GL_VERSION_OVERRIDE: '4.5' | |
| MESA_GLSL_VERSION_OVERRIDE: '450' | |
| run: | | |
| set -o pipefail | |
| flutter test integration_test/all_tests.dart --device-id linux --reporter expanded 2>&1 | tee test_output.txt | |
| - name: Collect service logs on failure | |
| if: failure() | |
| run: | | |
| docker logs concord-synapse > synapse.log 2>&1 || true | |
| docker logs concord-as > as.log 2>&1 || true | |
| docker logs concord-db > db.log 2>&1 || true | |
| - name: Upload logs | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: integration-test-logs | |
| path: | | |
| app/test_output.txt | |
| synapse.log | |
| as.log | |
| db.log | |
| retention-days: 7 | |
| continue-on-error: true | |
| - name: Cleanup | |
| if: always() | |
| env: | |
| DOCKER_IMAGE: ${{ env.DOCKER_IMAGE }} | |
| DOCKER_TAG: latest | |
| run: docker compose --env-file .env down -v | |
| # ==================== RELEASE STAGE ==================== | |
| get_version: | |
| name: Get Version & Coverage | |
| needs: [flutter_tests, go_tests, integration_tests] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.extract.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Extract version from git history | |
| id: extract | |
| run: | | |
| if LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null); then | |
| LATEST_VERSION=${LATEST_TAG#v} | |
| MAJOR=$(echo $LATEST_VERSION | cut -d. -f1) | |
| MINOR=$(echo $LATEST_VERSION | cut -d. -f2) | |
| PATCH=$(echo $LATEST_VERSION | cut -d. -f3) | |
| COMMITS_SINCE=$(git rev-list ${LATEST_TAG}..HEAD --count) | |
| NEW_PATCH=$((PATCH + COMMITS_SINCE)) | |
| VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" | |
| else | |
| COMMITS=$(git rev-list HEAD --count) | |
| VERSION="0.0.${COMMITS}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Resolved version: $VERSION" | |
| - name: Download Flutter coverage report | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: flutter-coverage-report | |
| path: coverage-data/app/coverage | |
| - name: Download Go coverage report | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: go-coverage-report | |
| path: coverage-data/server | |
| - name: Install lcov | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y lcov | |
| - name: Generate Release Notes | |
| run: | | |
| echo "## Test Coverage" > release-notes.md | |
| echo "" >> release-notes.md | |
| if [ -f "coverage-data/app/coverage/lcov.info" ]; then | |
| FLUTTER_COV=$(lcov --summary coverage-data/app/coverage/lcov.info 2>&1 | grep lines | cut -d ':' -f 2 | xargs) | |
| echo "- **Flutter App:** $FLUTTER_COV" >> release-notes.md | |
| else | |
| echo "- **Flutter App:** Coverage not found" >> release-notes.md | |
| fi | |
| if [ -f "coverage-data/server/coverage-summary.txt" ]; then | |
| GO_COV=$(cat coverage-data/server/coverage-summary.txt) | |
| echo "- **Go Application Service:** $GO_COV" >> release-notes.md | |
| else | |
| echo "- **Go Application Service:** Coverage not found" >> release-notes.md | |
| fi | |
| - name: Upload Release Notes | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-notes | |
| path: release-notes.md | |
| retention-days: 1 | |
| build-windows-exe-native: | |
| name: Build Windows Executable (Native) | |
| needs: [get_version] | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Install dependencies | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter pub get | |
| - name: Build Windows app | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter build windows --release | |
| - name: Package Windows release | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path "${{ env.APP_PATH }}\\build\\windows\\x64\\runner\\Release\\*" ` | |
| -DestinationPath "concord-windows-v${{ needs.get_version.outputs.version }}.zip" | |
| - name: Upload Windows artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-release | |
| path: concord-windows-v${{ needs.get_version.outputs.version }}.zip | |
| retention-days: 1 | |
| build-android-apk: | |
| name: Build Android APK | |
| needs: [get_version] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_VERSION }} | |
| cache: true | |
| - name: Install dependencies | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter pub get | |
| - name: Build Android APK | |
| working-directory: ${{ env.APP_PATH }} | |
| run: flutter build apk --release | |
| - name: Rename APK | |
| run: | | |
| mv ${{ env.APP_PATH }}/build/app/outputs/flutter-apk/app-release.apk \ | |
| concord-android-v${{ needs.get_version.outputs.version }}.apk | |
| - name: Upload Android artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-release | |
| path: concord-android-v${{ needs.get_version.outputs.version }}.apk | |
| retention-days: 1 | |
| create_release: | |
| name: Create GitHub Release | |
| needs: [get_version, build-windows-exe-native, build-android-apk] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download Windows artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: windows-release | |
| path: release-files | |
| - name: Download Android artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-release | |
| path: release-files | |
| - name: Download Release Notes | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-notes | |
| path: . | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.get_version.outputs.version }} | |
| name: Release v${{ needs.get_version.outputs.version }} | |
| files: release-files/* | |
| body_path: release-notes.md | |
| generate_release_notes: true | |
| build-and-push-docker: | |
| name: Build and Push Docker Image (Application Service) | |
| needs: [get_version] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE }} | |
| tags: | | |
| type=raw,value=${{ needs.get_version.outputs.version }} | |
| type=raw,value=latest | |
| - name: Build and push Application Service image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| # Build context is the server/ directory (contains go.mod + Dockerfile) | |
| context: ./server | |
| file: ./server/Dockerfile | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| platforms: linux/amd64 | |
| - name: Docker image summary | |
| run: | | |
| echo "### Docker Image Published" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Image:** \`${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo '${{ steps.meta.outputs.tags }}' | while IFS= read -r tag; do | |
| echo "- \`docker pull $tag\`" >> $GITHUB_STEP_SUMMARY | |
| done |