-
-
Notifications
You must be signed in to change notification settings - Fork 0
CI CD
CI is split per component with path filters, so a web change never triggers an app build and vice versa. Each component is linted, tested, built, versioned with semantic-release, and shipped automatically — the web to Cloudflare, the app to Google Play. Workflows live in .github/workflows/.
| Workflow | Triggers on | Does |
|---|---|---|
ci-web.yml |
web/** changes |
lint, test, build, typecheck, deploy, release |
ci-app.yml |
app/** changes |
Flutter format check, analyze, test (+coverage), build |
release-app.yml |
manual (workflow_dispatch) |
semantic-release + automatic Google Play delivery |
_deploy-web.yml |
reusable | shared web deploy steps |
cleanup-web-development.yml |
PR close | deletes the per-PR preview worker |
dependabot-auto-merge.yml, renovate-auto-approve.yml
|
dependency PRs | automated dependency updates |
zizmor.yml |
— | GitHub Actions security linting |
---
config:
look: handDrawn
theme: neutral
---
flowchart LR
check["web-check (lint + test + coverage)"] --> build["web-build (build + typecheck)"]
build --> prod["deploy-production"]
build --> dev["deploy-development"]
build --> rel["release (semantic-release)"]
dev --> comment["comment preview URL"]
- web-check — Biome lint, Vitest tests, upload coverage to Codecov.
-
web-build — production build +
tsctypecheck. -
deploy-production — on push to
main: build withCLOUDFLARE_ENV=production,wrangler deploy→ workercontribkitoncontribkit.app. -
deploy-development — on PRs: build with
CLOUDFLARE_ENV=development, deploy an ephemeral workerpr-<n>-contribkit-developmenton*.workers.dev; a bot comment posts the preview URL; the worker is removed on PR close bycleanup-web-development.yml. - release — semantic-release versions the web component (decoupled from deploy).
Concurrency cancels in-progress runs for pull requests only.
Runs on every app/** change:
---
config:
look: handDrawn
theme: neutral
---
flowchart LR
analyze["flutter-analyze (format + analyze --fatal-infos)"] --> build["flutter-build (debug APK)"]
test["flutter-test (+ coverage → Codecov)"] --> build
-
flutter-analyze —
dart formatverification +flutter analyze --fatal-infos. - flutter-test — unit/widget tests with coverage uploaded to Codecov.
- flutter-build — builds a debug APK to catch build breakages early.
The fancy part. Triggered manually with a track choice (internal / alpha / beta / production), it versions and ships the Android app end-to-end:
---
config:
look: handDrawn
theme: neutral
---
flowchart TD
dispatch(["workflow_dispatch (track)"]) --> release["release: semantic-release (app)"]
release -->|published?| gate{"new version?"}
gate -->|no| stop(["nothing to ship"])
gate -->|yes| deliver["deliver: Deliver to Google Play"]
deliver --> sign["decode keystore + signing config"]
deliver --> notes["generate Play notes from CHANGELOG"]
deliver --> aab["flutter build appbundle --release"]
sign & notes & aab --> upload["fastlane deploy → Google Play"]
-
Version — semantic-release computes the next version from Conventional Commits, updates the changelog, tags
app-vX.Y.Z, and force-updates the major tag (app-vX). Adetectstep decides whether anything was actually published. - Sign — the upload keystore and Play service-account JSON are decoded from GitHub secrets at runtime; nothing sensitive is committed.
-
Release notes — the latest
CHANGELOG.mdsection is transformed into a Google Playchangelogs/<versionCode>.txt: drop the version header, flatten subheadings, unwrap Markdown links, strip bold/commit-hashes, bulletize, and clamp to 500 chars (Play's limit). The notes are also echoed to the job summary. -
Build —
flutter build appbundle --release, with the RevenueCat key injected via--dart-define-from-fileand shared assets synced first. -
Upload —
fastlane deploy track:<track>pushes the AAB to the chosen Play track.
The job binds to the app-production or app-development GitHub Environment depending on the selected track, so production secrets stay scoped.
Environments are repo-global, so they're namespaced by component (<component>-<stage>) and hold component-specific secrets:
| Environment | Component | Stage | Deployed by |
|---|---|---|---|
web-production |
Astro web | production |
ci-web.yml (push to main) |
web-development |
Astro web | development |
ci-web.yml (per-PR preview) |
app-production |
Flutter app | production |
release-app.yml (track = production) |
app-development |
Flutter app | development |
release-app.yml (track ≠ production) |
App development is the internal Play track + RevenueCat sandbox; web development is a per-PR preview Worker. Component-scoped configs don't repeat the prefix: wrangler uses [env.production] / [env.development]; Flutter uses production / development flavors.
semantic-release runs per component and tags web-vX.Y.Z / app-vX.Y.Z, driven by Conventional Commits (enforced by commitlint, see Git Hooks). To keep per-package changelogs clean, the auto-scope hook blocks any commit that touches both web/ and app/. Web deploys are decoupled from versioning (production deploys on every qualifying push to main); the app's Play delivery is gated on a real semantic-release publish.
-
Pinned actions — every
uses:is pinned to a full commit SHA, not a floating tag. -
Least privilege — workflows declare minimal
permissions;release-app.ymlstarts frompermissions: {}and grants per-job. - zizmor — static security analysis of the workflows themselves.
-
Secrets never touch disk in the repo — keystore and service-account JSON are base64/secret-decoded into
$RUNNER_TEMPat runtime. -
Dependency autopilot — Dependabot and Renovate PRs are auto-approved/merged once green; pnpm enforces a
minimumReleaseAgecooldown before pulling new versions. - Path-filtered, cancel-in-progress concurrency keeps runs fast and cheap.
- Git Hooks — the same checks enforced locally before push
-
Web Application — the
@astrojs/cloudflaredeploy gotcha - Mobile App — flavors, signing, in-app purchases
- Project Structure — monorepo tooling