Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# GitHub Actions — cost & deploy playbook

Copy these patterns into product monorepos. Goal: keep private-repo
**billable minutes under the free ~2,000/month** (Windows ×2, macOS ×10).

## Defaults every deploy workflow should have

| Control | Why |
|---------|-----|
| `paths:` on `push` | Unrelated monorepo edits must not rebuild docker |
| Never `packages/**` | Prefer explicit `packages/<name>/**` per consumer |
| No `pull_request` on deploy | PR CI lives in `review.yml`; deploy = main only |
| `concurrency` + `cancel-in-progress: true` | Rapid main pushes: only latest SHA finishes |
| `docker/build-push-action` + `cache-from/to: type=gha,scope=<img>` | Warm builds cut 40–60% of docker time |
| `permissions.actions: write` | Required to write GHA docker cache |
| `timeout-minutes` on light jobs | Bound runaway runners (e.g. release-please) |

## Path fan-out rules

```yaml
# BAD — one package edit rebuilds api + web + worker
paths:
- "packages/**"

# GOOD — each service lists only packages it COPYs / imports
# api/worker (python workspace dep):
paths:
- "apps/api/**"
- "packages/core/**"
# web (bun workspace tokens/i18n only):
paths:
- "apps/web/**"
- "packages/design-tokens/**"
- "packages/i18n/**"
```

If the Dockerfile context is `apps/<svc>` only and does **not** COPY monorepo
`packages/`, do not list packages at all.

## Docker build snippet (canonical)

```yaml
permissions:
contents: read
id-token: write
actions: write

concurrency:
group: deploy-<svc>
cancel-in-progress: true

steps:
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v6
with:
context: apps/api # or monorepo root if Dockerfile needs packages/*
file: apps/api/Dockerfile
push: true
platforms: linux/amd64
tags: ${{ env.IMAGE }}:${{ github.sha }}
cache-from: type=gha,scope=<unique-image-name>
cache-to: type=gha,mode=max,scope=<unique-image-name>
```

Use a **unique `scope` per image** so api/web/worker caches do not clobber each other.

## release-please

```yaml
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
concurrency:
group: release-please
cancel-in-progress: true
```

## Measure usage

```bash
# Org monthly Actions usage (enhanced billing)
gh api "orgs/<org>/settings/billing/usage?year=YYYY&month=M" \
| jq '[.usageItems[]
| select(.product=="actions" and .unitType=="Minutes")
| {repo: .repositoryName, sku, qty: .quantity}]'
```

Public repos usually do not count against the free private minutes quota;
private Linux minutes count 1×.

## What not to do

- Auto-deploy on every PR (test in `review.yml`, deploy on main/tag only)
- macOS/Windows matrix on every PR (10× / 2× billable)
- `cancel-in-progress: false` on deploy when agents push many main commits
- Bare `docker build && docker push` without GHA cache in monorepos that ship often
40 changes: 30 additions & 10 deletions .github/workflows/deploy-api.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
name: Deploy API

# Template cost controls (Actions minutes) — copy into product repos:
# - path filters so unrelated monorepo edits do not rebuild
# - concurrency cancel-in-progress: only the latest SHA deploys on main
# - docker/build-push-action GHA layer cache (scope per image)
# - PR CI lives in review.yml; this workflow is main/deploy only

on:
push:
branches: [main]
Expand All @@ -8,12 +14,22 @@ on:
- ".github/workflows/deploy-api.yml"
workflow_dispatch:

concurrency:
group: deploy-api
cancel-in-progress: true

permissions:
contents: read
id-token: write
actions: write # docker GHA cache (cache-to: type=gha)

env:
PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
REGION: ${{ vars.GCP_REGION }}
SERVICE_NAME: fullstack-starter-api
WORKLOAD_IDENTITY_PROVIDER: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }}
SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
IMAGE: ${{ vars.GCP_REGION }}-docker.pkg.dev/${{ vars.GCP_PROJECT_ID }}/fullstack-starter-images/api

jobs:
test:
Expand All @@ -29,6 +45,8 @@ jobs:
uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "apps/api/uv.lock"

- name: Set up Python
run: uv python install 3.12
Expand All @@ -48,9 +66,6 @@ jobs:
build-and-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v6
Expand All @@ -65,21 +80,26 @@ jobs:
uses: google-github-actions/setup-gcloud@v3

- name: Configure Docker
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build and push image
run: |
IMAGE="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/fullstack-starter-images/api:${{ github.sha }}"
docker build -t $IMAGE apps/api
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
uses: docker/build-push-action@v6
with:
context: apps/api
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
cache-from: type=gha,scope=fullstack-starter-api
cache-to: type=gha,mode=max,scope=fullstack-starter-api

- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.REGION }}
image: ${{ env.IMAGE }}
image: ${{ env.IMAGE }}:${{ github.sha }}
flags: |
--min-instances=0
--max-instances=10
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/deploy-mobile.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
name: Mobile CI

# Cost controls (Actions minutes):
# - path filters so non-mobile monorepo edits skip this workflow
# - concurrency cancel-in-progress (macos is 10x billable — cancel stale)
# - PR runs analyze/test only; build-android/ios/deploy stay main-push only
# - prefer ubuntu for analyze; never matrix macos on every PR

on:
push:
branches: [main]
paths:
- "apps/mobile/**"
- ".github/workflows/mobile-ci.yml"
- ".github/workflows/deploy-mobile.yml"
pull_request:
branches: [main]
paths:
- "apps/mobile/**"
workflow_dispatch:

concurrency:
group: mobile-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
FLUTTER_VERSION: "3.38.5"
RUBY_VERSION: "3.3"
Expand Down
43 changes: 32 additions & 11 deletions .github/workflows/deploy-web.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
name: Deploy Web

# Template cost controls (Actions minutes) — copy into product repos:
# - path filters so unrelated monorepo edits do not rebuild
# - concurrency cancel-in-progress: only the latest SHA deploys on main
# - docker/build-push-action GHA layer cache (scope per image)
# - PR CI lives in review.yml; this workflow is main/deploy only
# - if web copies monorepo packages in its Dockerfile, add those package
# paths explicitly (e.g. packages/design-tokens/**) — never packages/**

on:
push:
branches: [main]
Expand All @@ -8,12 +16,22 @@ on:
- ".github/workflows/deploy-web.yml"
workflow_dispatch:

concurrency:
group: deploy-web
cancel-in-progress: true

permissions:
contents: read
id-token: write
actions: write # docker GHA cache (cache-to: type=gha)

env:
PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
REGION: ${{ vars.GCP_REGION }}
SERVICE_NAME: fullstack-starter-web
WORKLOAD_IDENTITY_PROVIDER: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }}
SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
IMAGE: ${{ vars.GCP_REGION }}-docker.pkg.dev/${{ vars.GCP_PROJECT_ID }}/fullstack-starter-images/web

jobs:
test:
Expand Down Expand Up @@ -45,9 +63,6 @@ jobs:
build-and-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v6
Expand All @@ -62,22 +77,28 @@ jobs:
uses: google-github-actions/setup-gcloud@v3

- name: Configure Docker
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build and push image
run: |
IMAGE="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/fullstack-starter-images/web:${{ github.sha }}"
docker build -t $IMAGE apps/web \
--build-arg NEXT_PUBLIC_API_URL=${{ vars.API_URL }}
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
uses: docker/build-push-action@v6
with:
context: apps/web
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
build-args: |
NEXT_PUBLIC_API_URL=${{ vars.API_URL }}
cache-from: type=gha,scope=fullstack-starter-web
cache-to: type=gha,mode=max,scope=fullstack-starter-web

- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.REGION }}
image: ${{ env.IMAGE }}
image: ${{ env.IMAGE }}:${{ github.sha }}
flags: |
--min-instances=0
--max-instances=10
Expand Down
40 changes: 30 additions & 10 deletions .github/workflows/deploy-worker.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
name: Deploy Worker

# Template cost controls (Actions minutes) — copy into product repos:
# - path filters so unrelated monorepo edits do not rebuild
# - concurrency cancel-in-progress: only the latest SHA deploys on main
# - docker/build-push-action GHA layer cache (scope per image)
# - PR CI lives in review.yml; this workflow is main/deploy only

on:
push:
branches: [main]
Expand All @@ -8,12 +14,22 @@ on:
- ".github/workflows/deploy-worker.yml"
workflow_dispatch:

concurrency:
group: deploy-worker
cancel-in-progress: true

permissions:
contents: read
id-token: write
actions: write # docker GHA cache (cache-to: type=gha)

env:
PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
REGION: ${{ vars.GCP_REGION }}
SERVICE_NAME: fullstack-starter-worker
WORKLOAD_IDENTITY_PROVIDER: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }}
SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
IMAGE: ${{ vars.GCP_REGION }}-docker.pkg.dev/${{ vars.GCP_PROJECT_ID }}/fullstack-starter-images/worker

jobs:
test:
Expand All @@ -29,6 +45,8 @@ jobs:
uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "apps/worker/uv.lock"

- name: Set up Python
run: uv python install 3.12
Expand All @@ -48,9 +66,6 @@ jobs:
build-and-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v6
Expand All @@ -65,21 +80,26 @@ jobs:
uses: google-github-actions/setup-gcloud@v3

- name: Configure Docker
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build and push image
run: |
IMAGE="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/fullstack-starter-images/worker:${{ github.sha }}"
docker build -t $IMAGE apps/worker
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
uses: docker/build-push-action@v6
with:
context: apps/worker
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
cache-from: type=gha,scope=fullstack-starter-worker
cache-to: type=gha,mode=max,scope=fullstack-starter-worker

- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.REGION }}
image: ${{ env.IMAGE }}
image: ${{ env.IMAGE }}:${{ github.sha }}
flags: |
--min-instances=0
--max-instances=5
Expand Down
Loading
Loading