This document describes all GitHub Actions workflows used for continuous integration and deployment of the Cambridge Beer Festival app.
The project uses 3 separate workflows to handle different aspects of the CI/CD pipeline:
| Workflow | File | Purpose | Triggers |
|---|---|---|---|
| CI | ci.yml |
Build, test, and deploy Flutter app | Push to main, PRs to main |
| Cloudflare Worker | deploy-worker.yml |
Deploy API proxy worker and festivals data | Push to main, PRs (when worker/festivals.json changes) |
| Release Web | release-web.yml |
Production web releases to Cloudflare Pages | Version tags (v*) |
File: .github/workflows/ci.yml
Name: CI
Handles all Flutter app building, testing, and deployment workflows for staging, development, and PR previews.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:- Push to
main: Full build, test, deploy to Cloudflare Pages staging - Pull Requests to
main: Build, test, deploy preview to Cloudflare Pages (runs once per push) - Manual: Via workflow_dispatch in GitHub Actions UI
Note: The workflow triggers only on pull_request events for PR branches, not on push events, which prevents duplicate workflow runs when pushing commits to a PR branch.
Detects which files have changed to optimize workflow execution.
Outputs:
app: Changed if Flutter app files modified
Filters:
lib/**,web/**,pubspec.yaml,test/**,android/**.github/workflows/ci.yml,mise.toml
Runs Flutter tests with coverage reporting.
Runs when: needs.changes.outputs.app == 'true'
Steps:
- Setup Flutter 3.38.3
- Create Firebase configuration from secrets
- Install dependencies (
flutter pub get) - Generate mocks (
build_runner) - Analyze code (
flutter analyze --no-fatal-infos) - Run tests with coverage (
flutter test --coverage) - Report coverage to GitHub PR comments and Codecov
Coverage Requirements:
- Minimum: 70%
- Reports posted as PR comments
- Uploaded to Codecov
Builds Flutter web application.
Runs when: test job succeeds
Steps:
- Setup Flutter
- Create Firebase configuration
- Install dependencies
- Build web with
--base-href "/"(for Cloudflare Pages) - Upload build artifact
Artifact: web-build (used by deployment jobs)
Builds Android APK and App Bundle.
Runs when: test job succeeds
Steps:
- Setup JDK 17
- Cache Gradle dependencies (caches
~/.gradle/cachesand~/.gradle/wrapper) - Setup Flutter
- Create Firebase configuration
- Install dependencies
- Build debug APK
Artifacts:
app-debug-apk
Performance Optimizations:
- Gradle dependency caching reduces build time by 2-5 minutes on cache hits
- Gradle build cache enabled (see gradle.properties)
Deploys to Cloudflare Pages (staging and PR previews).
Runs when: needs.changes.outputs.app == 'true'
Environments:
- Staging:
main.cambeerfestival.pages.dev(push tomain) - PR Previews: Unique URL per PR (pull requests)
Steps:
- Download
web-buildartifact - Deploy to Cloudflare Pages using
cloudflare/pages-action@v1 - Comment PR with preview URL (if PR)
Preview URL Format:
- PR:
https://<pr-branch>.cambeerfestival.pages.dev - Staging:
https://main.cambeerfestival.pages.dev
File: .github/workflows/deploy-worker.yml
Name: Cloudflare Worker
Handles deployment of the Cloudflare Worker (API proxy) and festivals.json data.
on:
push:
branches: [main]
paths:
- 'cloudflare-worker/**'
- 'data/festivals.json'
- '.github/workflows/deploy-worker.yml'
pull_request:
paths:
- 'cloudflare-worker/**'
- 'data/festivals.json'
- '.github/workflows/deploy-worker.yml'
workflow_dispatch:- Push to
main: Deploy worker if worker or festivals.json changed - Pull Requests: Validate worker (dry-run) if worker or festivals.json changed (runs once per push)
- Manual: Via workflow_dispatch in GitHub Actions UI
Note: The pull_request trigger doesn't specify branches, allowing PRs from any branch while still running only once per push.
Detects which files have changed.
Outputs:
worker: Changed ifcloudflare-worker/**modifiedfestivals: Changed ifdata/festivals.jsonmodified
Validates festivals.json against JSON schema.
Runs when: needs.changes.outputs.festivals == 'true'
Steps:
- Setup Node.js 20
- Install validation dependencies (
scripts/package.json) - Run
node scripts/validate-festivals.js
Validation checks:
- JSON syntax validity
- Schema compliance (
docs/api/festival-registry-schema.json) - Required fields present
- Data types correct
Validates Cloudflare Worker deployment (dry-run).
Runs when: PR and (worker == 'true' || festivals == 'true')
Steps:
- Setup Node.js 20
- Install worker dependencies
- Copy
festivals.jsonto worker directory - Run
wrangler deploy --dry-run
Purpose: Catch deployment errors before merging to main
Deploys Cloudflare Worker to production.
Runs when: Push to main and (worker == 'true' || festivals == 'true')
Steps:
- Setup Node.js 20
- Validate festivals.json (if changed)
- Install worker dependencies
- Copy
festivals.jsonto worker directory - Deploy using
wranglerviacloudflare/wrangler-action@v3
Worker URL: https://data.cambeerfestival.app
Key Features:
- CORS proxy for Cambridge Beer Festival API
- Serves festivals.json registry
- Injects CORS headers for allowed origins
File: .github/workflows/release-web.yml
Name: Release Web to Cloudflare Pages
Production releases of the web app to the custom domain cambeerfestival.app.
on:
push:
tags:
- 'v*' # Matches v2025.12.1, v2025.12.0, etc.
workflow_dispatch:
inputs:
version:
description: 'Version tag using CalVer (e.g., v2025.12.1)'
required: true- Tag Push: Automatic deployment when version tag pushed
- Manual: Via workflow_dispatch with version input
Builds and deploys production web app.
Environment: production
URL: https://cambeerfestival.app
Steps:
- Checkout code
- Extract version from tag
- Setup Flutter 3.38.3
- Create Firebase configuration
- Install dependencies
- Generate mocks
- Analyze code
- Run tests (must pass!)
- Build web with
--base-href "/" - Deploy to Cloudflare Pages production
Deployment Strategy:
- Tests must pass before deployment
- Code analysis must pass
- Deploys to production branch in Cloudflare Pages
- Custom domain
cambeerfestival.apppoints to this deployment
┌─────────────────────────────────────────────────────────────┐
│ Code Change / PR │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────┐
│ Flutter App CI/CD │ │ Cloudflare Worker │
│ (ci.yml) │ │ (cloudflare-worker │
│ │ │ .yml) │
│ • Test & Build │ │ │
│ • Deploy to GH Pages │ │ • Validate JSON │
│ • Deploy to CF Pages │ │ • Validate Worker │
│ (PR preview/staging) │ │ • Deploy Worker (main) │
└───────────────────────────┘ └─────────────────────────┘
┌─────────────┐
│ Tag Push │
│ (v*.*.*) │
└──────┬──────┘
│
▼
┌────────────────────────┐
│ Release Web │
│ (release-web.yml) │
│ │
│ • Test & Build │
│ • Deploy to Prod │
│ (cambeerfestival │
│ .app) │
└────────────────────────┘
All workflows require the following secrets (set in repository Settings → Secrets):
| Secret | Used By | Description |
|---|---|---|
CLOUDFLARE_API_TOKEN |
Worker, Release Web, App CI/CD | API token with Workers + Pages permissions |
CLOUDFLARE_ACCOUNT_ID |
Worker, Release Web, App CI/CD | Cloudflare account ID |
GOOGLE_SERVICES_JSON |
App CI/CD, Release Web | Firebase Android configuration |
CODECOV_TOKEN |
App CI/CD | Codecov upload token (optional) |
See GITHUB_SECRETS.md for setup instructions.
| Environment | Workflow | Trigger | URL | Purpose |
|---|---|---|---|---|
| Production | Release Web | Version tag | cambeerfestival.app |
Live production site |
| Staging | App CI/CD | Push to main |
main.cambeerfestival.pages.dev |
Stable staging environment |
| PR Preview | App CI/CD | Pull request | <branch>.cambeerfestival.pages.dev |
Test PRs before merge |
| Development | App CI/CD | Push to main |
richardthe3rd.github.io/... |
Alternative dev environment |
| Worker | Worker | Push to main |
data.cambeerfestival.app |
API proxy |
-
Create feature branch
git checkout -b feature/my-feature
-
Push branch and open PR
- GitHub Actions automatically:
- Runs tests (
testjob) - Builds app (
build-web,build-android) - Deploys preview to Cloudflare Pages
- Comments PR with preview URL
- Runs tests (
- GitHub Actions automatically:
-
Review preview
- Visit preview URL in PR comment
- Test changes in production-like environment
-
Merge to
main- Triggers deployment to:
- Cloudflare Pages staging (
staging.cambeerfestival.app)
- Cloudflare Pages staging (
- If worker/festivals.json changed:
- Deploys updated worker
- Triggers deployment to:
-
Create and push version tag
git tag v2025.12.1 git push origin v2025.12.1
-
Automatic production deployment
- GitHub Actions automatically:
- Runs tests (must pass)
- Builds web app
- Deploys to
cambeerfestival.app
- GitHub Actions automatically:
-
Verify production
- Visit
https://cambeerfestival.app - Check functionality
- Visit
-
Update worker code or
data/festivals.json# Edit files git commit -m "Update festivals.json" git push origin main
-
Automatic worker deployment
- GitHub Actions automatically:
- Validates festivals.json (if changed)
- Deploys worker to production
- GitHub Actions automatically:
All workflows support manual triggering via GitHub Actions UI:
- Go to Actions tab in GitHub
- Select workflow from left sidebar:
- Flutter App CI/CD
- Cloudflare Worker
- Release Web to Cloudflare Pages
- Click Run workflow button
- Select branch (or enter version for Release Web)
- Click Run workflow
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}- Each branch gets its own concurrency group
- Non-main branches: new pushes cancel in-progress runs
- Main branch: runs always complete (no cancellation)
concurrency:
group: cloudflare-worker-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}- Separate concurrency group for worker deployments
- Same cancellation logic as App CI/CD
concurrency:
group: cloudflare-pages-release-${{ github.ref }}
cancel-in-progress: false- Never cancels production releases
- Each release runs to completion
Check:
- Run tests locally:
flutter test - Check coverage meets minimum (70%)
- Review test failure logs in GitHub Actions
Fix:
- Fix failing tests
- Increase coverage if below threshold
- Push new commit to trigger re-run
Check:
- Build locally:
flutter build web - Check for analyzer errors:
flutter analyze --no-fatal-infos - Verify dependencies:
flutter pub get
Fix:
- Fix analyzer errors
- Update dependencies if needed
- Check Firebase configuration
Check:
- Verify GitHub Secrets are set correctly
- Check Cloudflare account/token permissions
- Review deployment logs in GitHub Actions
Fix:
- Update secrets if expired
- Verify Cloudflare token has correct permissions
- Check Cloudflare Pages project exists
Check:
- Validate festivals.json:
node scripts/validate-festivals.js - Test worker locally:
cd cloudflare-worker && wrangler dev - Check Cloudflare Worker quota/limits
Fix:
- Fix festivals.json schema errors
- Update worker code if needed
- Check Cloudflare account limits
- Go to Actions tab
- View all workflow runs
- Click on run to see details
- Expand job to see step logs
- Coverage reports posted automatically
- Preview URLs for Cloudflare Pages
- Test results summary
Configure in Settings → Notifications:
- Email on workflow failure
- Slack/Discord webhooks (optional)
All workflows use caching to speed up builds:
Flutter builds:
- uses: subosito/flutter-action@v2
with:
cache: true # Caches Flutter SDKAndroid builds (Gradle):
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-Impact:
- First build: Normal duration (populates cache)
- Subsequent builds: 2-5 min faster from cached Gradle dependencies
Node.js builds:
- uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: scripts/package-lock.jsonThe android/gradle.properties file includes performance optimizations:
# Gradle build optimizations
org.gradle.caching=true # Enable build cache for incremental builds
org.gradle.parallel=true # Run tasks in parallel when possibleImpact:
- Build cache: 1-3 min savings from incremental builds
- Parallel execution: Better CPU utilization during builds
Note on deprecated flags:
org.gradle.configureondemandis intentionally NOT used- This flag is deprecated in modern Gradle versions (8.9.1+)
- It can cause configuration issues with Flutter's multi-project builds
- The combination of
cachingandparallelprovides sufficient optimization
The build-web job creates an artifact that is reused by:
deploy-webdeploy-web-preview
This avoids rebuilding the app multiple times.
Jobs only run when relevant files change:
testonly runs whenappfiles change- Worker jobs only run when
workerorfestivalschange
- Never commit secrets to repository
- Rotate tokens periodically
- Use minimal permissions for tokens
- Separate tokens for different services (optional)
- Secrets are NOT available to fork PRs
- Fork contributors must run tests locally
- Maintainers can merge to branch to trigger CI
- Require PR reviews before merge
- Enable branch protection on
main - Require status checks to pass
- GitHub Secrets Setup - How to configure secrets
- Cloudflare Pages Setup - Cloudflare configuration
- Firebase Setup - Firebase project setup
- Development Guide - Local development workflow
git tag v2025.12.1
git push origin v2025.12.1# Edit cloudflare-worker/* or data/festivals.json
git commit -m "Update worker"
git push origin maingh run list --workflow="Flutter App CI/CD"
gh run list --workflow="Cloudflare Worker"
gh run list --workflow="Release Web to Cloudflare Pages"gh run cancel <run-id>gh run rerun <run-id>When a workflow is configured with both push and pull_request triggers for the same branches, it can run twice for the same commit:
# ❌ BAD: Causes duplicate runs on PR pushes
on:
push:
branches: [main, feature/**]
pull_request:
branches: [main]Result: Push to a PR branch → workflow runs on push event AND on pull_request event = 2 runs 💰💸
Our workflows are configured to run only once per commit:
# ✅ GOOD: Runs only once per PR push
on:
push:
branches: [main] # Only run on direct pushes to main
pull_request:
branches: [main] # Run on all PRs targeting mainResult:
- Push to a PR branch → workflow runs only on
pull_requestevent = 1 run ✅ - Push directly to main → workflow runs only on
pushevent = 1 run ✅
- Cost savings - Reduces GitHub Actions minutes usage by 50%
- Faster feedback - No waiting for duplicate runs to complete
- Cleaner UI - Fewer runs to monitor in the Actions tab
- Resource efficiency - Less CI queue contention
- The
pull_requesttrigger in some workflows (e.g.,deploy-worker.yml) doesn't specifybranches, which allows PRs from any branch while still maintaining single-run behavior - The
workflow_dispatchtrigger allows manual runs when needed - Concurrency groups ensure that new pushes to the same branch cancel in-progress runs (except on
main)
The Cambridge Beer Festival app uses 3 specialized workflows:
- Flutter App CI/CD - Comprehensive app testing, building, and deployment
- Cloudflare Worker - API proxy and festivals data deployment
- Release Web - Production releases to custom domain
This separation provides:
- Clear responsibilities - Each workflow has a specific purpose
- Independent triggers - Worker can deploy without rebuilding app
- Optimized execution - Only relevant jobs run for each change
- Better monitoring - Easier to track specific deployment types
- Single run per commit - Avoids duplicate CI runs on PR pushes