Skip to content

Release 2026.8.0

Release 2026.8.0 #847

Workflow file for this run

name: CI
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 }}
functions: ${{ steps.filter.outputs.functions }}
proto: ${{ steps.filter.outputs.proto }}
formattable: ${{ steps.filter.outputs.formattable }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Check for file changes
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
app:
- 'lib/**'
- 'web/**'
- 'pubspec.yaml'
- 'test/**'
- 'test-e2e/**'
- 'package.json'
- 'playwright.config.ts'
- 'android/**'
- '.github/workflows/ci.yml'
- 'mise.toml'
functions:
- 'functions/**'
proto:
- 'proto/**'
# Any file the fmt job checks (dart format, prettier, shfmt),
# anywhere in the repo — so a formattable file added under
# docs/** or .claude/** is format-checked on its own PR instead
# of failing a later, unrelated PR when the repo-wide fmt globs
# first run. Keep in sync with the fmt job's checks.
formattable:
- '**/*.dart'
- '**/*.js'
- '**/*.ts'
- '**/*.mjs'
- '**/*.sh'
# Check formatting before expensive jobs
fmt:
needs: changes
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true' ||
needs.changes.outputs.functions == 'true' ||
needs.changes.outputs.formattable == 'true'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
channel: 'stable'
cache: true
- name: Cache pub dependencies
uses: actions/cache@v6
with:
path: ~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-pub-
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- name: Get dependencies
run: flutter pub get
- name: Install shfmt
run: |
curl -sSL "https://github.com/mvdan/sh/releases/download/v3.8.0/shfmt_v3.8.0_linux_amd64" -o /usr/local/bin/shfmt
chmod +x /usr/local/bin/shfmt
- name: Check formatting
run: |
dart format --output=none --set-exit-if-changed .
npm ci
npx prettier --check "**/*.{js,ts,mjs}"
mapfile -t SH_FILES < <(find . -name '*.sh' -not -path './.git/*' -not -path './.mise/*' -not -path '*/node_modules/*' | sort)
[[ ${#SH_FILES[@]} -gt 0 ]] && shfmt -d -i 0 -ci "${SH_FILES[@]}"
# Lint proto files and check for breaking changes against main.
# buf breaking runs on PRs only (bufbuild/buf-action skips it on push to main
# where the change is already merged). FILE stability level (configured in
# proto/buf.yaml) is appropriate for v1alpha; switch to WIRE_JSON_COMPATIBLE
# when the API graduates to v1.
proto:
needs: changes
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.proto == 'true'
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0 # buf breaking needs full history to compare against main
- uses: bufbuild/buf-action@v1
with:
input: proto
push: false
pr_comment: false
breaking_against: "https://github.com/${{ github.repository }}.git#branch=main,subdir=proto"
# Analyze code in parallel with tests so builds can start sooner
analyze:
needs: changes
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true' ||
needs.changes.outputs.functions == 'true'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
generate-mocks: 'true'
- name: Analyze code
run: flutter analyze --no-fatal-infos
# Run tests once before building
test:
needs: changes
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true' ||
needs.changes.outputs.functions == 'true'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
generate-mocks: 'true'
- name: Run tests with coverage
run: flutter test --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
files: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: Test Pages Functions
run: |
cd functions
npm ci
npm test
# Build web after analysis passes (tests run in parallel; deploy gates on test)
build-web:
needs: [changes, analyze, fmt]
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
- name: Get git version info
id: git_version
run: scripts/get_version_info.sh github >> $GITHUB_OUTPUT
- name: Build web
run: |
flutter build web --release --base-href "/" --source-maps \
--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 source maps artifact
uses: actions/upload-artifact@v7
with:
name: source-maps
path: build/web/main.dart.js.map
retention-days: 7
- name: Strip source maps from deployable artifact
run: rm -f build/web/main.dart.js.map
- name: Upload build artifact
uses: actions/upload-artifact@v7
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- name: Download web build artifact
uses: actions/download-artifact@v8
with:
name: web-build
path: build/web
- name: Install npm dependencies
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ hashFiles('package-lock.json') }}
restore-keys: playwright-chromium-
- 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 --proxy http://127.0.0.1:8080? > /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
# Additional wait to ensure Flutter app fully initializes
sleep 3
# Verify server responds to festival route (tests SPA routing)
curl -f -s http://127.0.0.1:8080/cbf2025 > /dev/null || (echo "SPA routing not working" && exit 1)
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v7
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
# Build Android after analysis passes (tests run in parallel)
build-android:
needs: [changes, analyze, fmt]
runs-on: ubuntu-latest
if: |
github.actor != 'dependabot[bot]' && (
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true'
)
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Flutter App
uses: ./.github/actions/setup-flutter-app
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON }}
- name: Get git version info
id: git_version
run: scripts/get_version_info.sh github >> $GITHUB_OUTPUT
- name: Build release APK (debug-signed)
# Build with release config (enabling R8 minification) so ProGuard/R8
# issues are caught on every PR. key.properties is absent here, so
# build.gradle automatically falls back to the debug signing config.
run: |
flutter build apk --release \
--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 release APK
uses: actions/upload-artifact@v7
with:
name: app-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
if-no-files-found: error
deploy-web-preview:
needs: [changes, build-web, test-e2e-web, test]
runs-on: ubuntu-latest
outputs:
preview-url: ${{ steps.set-url.outputs.url }}
if: |
github.actor != 'dependabot[bot]' && (
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.app == 'true' ||
needs.changes.outputs.functions == 'true'
)
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: web-build
path: build/web
- name: Deploy to Cloudflare Pages (Staging/PR Previews)
id: deploy
uses: cloudflare/wrangler-action@v4
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: Set preview URL output
id: set-url
run: |
URL="${{ steps.deploy.outputs.pages-deployment-alias-url }}"
[ -z "$URL" ] && URL="${{ steps.deploy.outputs.deployment-url }}"
echo "url=$URL" >> "$GITHUB_OUTPUT"
- name: Comment PR with Preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v9
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
});
# Run CSP smoke test against the live Cloudflare Pages preview.
# The normal test-e2e-web job runs against a plain local server where
# web/_headers is never applied — CSP violations are invisible there.
# This job hits the real deployment so any missing CSP allowances fail fast.
smoke-test-preview:
needs: [deploy-web-preview]
runs-on: ubuntu-latest
if: needs.deploy-web-preview.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
- name: Install npm dependencies
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ hashFiles('package-lock.json') }}
restore-keys: playwright-chromium-
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run CSP smoke test against deployed preview
run: npx playwright test test-e2e/csp-smoke.spec.ts
env:
BASE_URL: ${{ needs.deploy-web-preview.outputs.preview-url }}
- name: Upload smoke test report
if: always()
uses: actions/upload-artifact@v7
with:
name: csp-smoke-report
path: playwright-report/
retention-days: 7