Skip to content

Merge pull request #38 from Refraggerator/fix/flutter-integration-tests #61

Merge pull request #38 from Refraggerator/fix/flutter-integration-tests

Merge pull request #38 from Refraggerator/fix/flutter-integration-tests #61

Workflow file for this run

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