Skip to content

Migrate API URLs to data.cambeerfestival.app custom domain with relative URL support #775

Migrate API URLs to data.cambeerfestival.app custom domain with relative URL support

Migrate API URLs to data.cambeerfestival.app custom domain with relative URL support #775

Workflow file for this run

name: Flutter App CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write
pull-requests: write
deployments: write
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
# Detect which files have changed
changes:
runs-on: ubuntu-latest
outputs:
app: ${{ steps.filter.outputs.app }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for file changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
app:
- 'lib/**'
- 'web/**'
- 'pubspec.yaml'
- 'test/**'
- 'test-e2e/**' # Includes test-e2e/screenshots.ts
- 'package.json'
- 'playwright.config.ts'
- 'android/**'
- '.github/workflows/build-deploy.yml'
- 'mise.toml'
# Run tests once before building
test:
needs: changes
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.38.3'
channel: 'stable'
cache: true
- name: Create Firebase google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
- name: Get dependencies
run: flutter pub get
- name: Generate mocks
run: dart run build_runner build --delete-conflicting-outputs
- name: Analyze code
run: flutter analyze --no-fatal-infos
- name: Run tests with coverage
run: flutter test --coverage
- name: Install lcov
run: sudo apt-get update && sudo apt-get install -y lcov
- name: Report code coverage
uses: zgosalvez/github-actions-report-lcov@v4
with:
coverage-files: coverage/lcov.info
# TODO: Increase coverage target back to 70% as test coverage improves
minimum-coverage: 25
artifact-name: code-coverage-report
github-token: ${{ secrets.GITHUB_TOKEN }}
update-comment: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
# Build web after tests pass
build-web:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.38.3'
channel: 'stable'
cache: true
- name: Create Firebase google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
- name: Get dependencies
run: flutter pub get
- name: Get git version info
id: git_version
run: |
chmod +x scripts/get_version_info.sh
scripts/get_version_info.sh github >> $GITHUB_OUTPUT
- name: Build web
run: |
flutter build web --release --base-href "/" \
--dart-define=GIT_TAG=${{ steps.git_version.outputs.git_tag }} \
--dart-define=GIT_COMMIT=${{ steps.git_version.outputs.git_commit }} \
--dart-define=GIT_BRANCH=${{ steps.git_version.outputs.git_branch }} \
--dart-define=BUILD_VERSION=${{ steps.git_version.outputs.version }} \
--dart-define=BUILD_TIME=${{ steps.git_version.outputs.build_time }}
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: web-build
path: build/web
# Run E2E tests on web build
test-e2e-web:
needs: build-web
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '21'
- name: Download web build artifact
uses: actions/download-artifact@v4
with:
name: web-build
path: build/web
- name: Install npm dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start http-server in background
run: |
npx http-server build/web -p 8080 -c-1 -a 127.0.0.1 > /dev/null 2>&1 &
echo $! > .http-server.pid
- name: Wait for server to be ready
run: |
timeout 30 bash -c 'until curl -s http://127.0.0.1:8080 > /dev/null; do sleep 1; done' || exit 1
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Stop http-server
if: always()
run: |
if [ -f .http-server.pid ]; then
kill $(cat .http-server.pid) || true
rm .http-server.pid
fi
# Capture screenshots for PR visual review
capture-screenshots:
needs: [build-web, changes]
runs-on: ubuntu-latest
# Only run on PRs to main when app files change
if: |
github.event_name == 'pull_request' &&
github.base_ref == 'main' &&
needs.changes.outputs.app == 'true'
permissions:
contents: write # For pushing to pr-screenshots branch
pull-requests: write # For posting PR comments
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '21'
- name: Download web build artifact
uses: actions/download-artifact@v4
with:
name: web-build
path: build/web
- name: Install npm dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start http-server in background
run: |
npx http-server build/web -p 8080 -c-1 -a 127.0.0.1 > /dev/null 2>&1 &
echo $! > .http-server.pid
- name: Wait for server to be ready
run: |
timeout 30 bash -c 'until curl -s http://127.0.0.1:8080 > /dev/null; do sleep 1; done' || exit 1
- name: Capture screenshots
run: |
mkdir -p screenshots
npm run screenshots screenshots
- name: Stop http-server
if: always()
run: |
if [ -f .http-server.pid ]; then
kill $(cat .http-server.pid) || true
rm .http-server.pid
fi
- name: Upload screenshots to pr-screenshots branch
run: |
# Verify screenshots exist before proceeding
if [ ! -d screenshots ] || [ -z "$(ls -A screenshots/*.png 2>/dev/null)" ]; then
echo "Error: No screenshots found to upload"
exit 1
fi
# Move screenshots to temporary location to avoid git working tree conflicts
mkdir -p /tmp/pr-screenshots-upload
cp screenshots/*.png /tmp/pr-screenshots-upload/
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Try to fetch the pr-screenshots branch if it exists
if git fetch origin pr-screenshots:pr-screenshots 2>/dev/null; then
# Branch exists remotely, check it out
git checkout pr-screenshots
else
# Branch doesn't exist, create orphan branch
git checkout --orphan pr-screenshots
# Remove all files from the working tree (orphan branch is empty)
git rm -rf . 2>/dev/null || true
fi
# Create directory for this PR's screenshots
PR_DIR="pr-${{ github.event.pull_request.number }}"
mkdir -p "$PR_DIR"
# Copy screenshots from temp location
cp /tmp/pr-screenshots-upload/*.png "$PR_DIR/"
# Add and commit
git add "$PR_DIR"
# Only commit if there are changes
if ! git diff --staged --quiet; then
git commit -m "Add screenshots for PR #${{ github.event.pull_request.number }}"
# Push with retry logic for concurrent PR handling
MAX_RETRIES=3
RETRY_COUNT=0
until git push origin pr-screenshots; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "Error: Failed to push screenshots after $MAX_RETRIES attempts"
exit 1
fi
echo "Push failed, retrying ($RETRY_COUNT/$MAX_RETRIES)..."
sleep 2
# Pull with rebase to handle concurrent updates
git pull --rebase origin pr-screenshots
done
echo "Successfully pushed screenshots"
else
echo "No changes to commit (screenshots unchanged)"
fi
# Clean up temp directory
rm -rf /tmp/pr-screenshots-upload
- name: Post PR comment with screenshots
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prNumber = context.issue.number;
const screenshotDir = 'screenshots';
// Check if screenshot directory exists
if (!fs.existsSync(screenshotDir)) {
console.log('Screenshot directory does not exist, skipping comment');
return;
}
// Get list of screenshot files
const screenshots = fs.readdirSync(screenshotDir)
.filter(f => f.endsWith('.png'))
.sort();
if (screenshots.length === 0) {
console.log('No screenshots found, skipping comment');
return;
}
// Build markdown table with screenshots
const baseUrl = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/pr-screenshots/pr-${prNumber}`;
let tableRows = '';
for (const screenshot of screenshots) {
const name = screenshot.replace(/^\d+-/, '').replace(/-/g, ' ').replace('.png', '');
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
const imageUrl = `${baseUrl}/${screenshot}`;
tableRows += `| ${capitalizedName} | ![${capitalizedName}](${imageUrl}) |\n`;
}
const comment = [
'## 📸 App Screenshots',
'',
'Visual preview of the app after your changes:',
'',
'| Screen | Preview (Mobile 390x844) |',
'|--------|--------------------------|',
tableRows.trimEnd(),
'',
'---',
'<sub>Screenshots are automatically generated for PRs targeting main branch when app files change.</sub>'
].join('\n');
// Find existing screenshot comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('📸 App Screenshots')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment,
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: comment,
});
}
# Build Android after tests pass
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.38.3'
channel: 'stable'
cache: true
- name: Create Firebase google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
- name: Get dependencies
run: flutter pub get
- name: Get git version info
id: git_version
run: |
chmod +x scripts/get_version_info.sh
scripts/get_version_info.sh github >> $GITHUB_OUTPUT
- name: Build debug APK
run: |
flutter build apk --debug \
--dart-define=GIT_TAG=${{ steps.git_version.outputs.git_tag }} \
--dart-define=GIT_COMMIT=${{ steps.git_version.outputs.git_commit }} \
--dart-define=GIT_BRANCH=${{ steps.git_version.outputs.git_branch }} \
--dart-define=BUILD_VERSION=${{ steps.git_version.outputs.version }} \
--dart-define=BUILD_TIME=${{ steps.git_version.outputs.build_time }}
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug-apk
path: build/app/outputs/flutter-apk/app-debug.apk
if-no-files-found: error
deploy-web-preview:
needs: [changes, build-web, test-e2e-web]
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true'
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: web-build
path: build/web
- name: Deploy to Cloudflare Pages (Staging/PR Previews)
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy build/web --project-name=staging-cambeerfestival --branch=${{ github.head_ref || github.ref_name }}
- name: Comment PR with Preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const aliasUrl = '${{ steps.deploy.outputs.pages-deployment-alias-url }}';
const deployUrl = '${{ steps.deploy.outputs.deployment-url }}';
const previewUrl = aliasUrl || deployUrl;
const comment = `## 🚀 Cloudflare Pages Preview
Your preview deployment is ready!
**Preview URL**: ${previewUrl}
This preview will be automatically updated when you push new commits to this PR.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});