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
65 changes: 21 additions & 44 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: |
if [ -f "package-lock.json" ]; then
npm ci
npm ci --no-audit --no-fund
else
npm install
npm install --no-audit --no-fund
fi

- name: Run Linter
Expand All @@ -74,48 +74,12 @@ jobs:
working-directory: ${{ matrix.package }}
run: npm test --if-present

- name: Build Application
working-directory: ${{ matrix.package }}
run: npm run build --if-present

build-and-smoke:
name: Build all apps + gateway smoke test
runs-on: ubuntu-latest
timeout-minutes: 15
# No secrets here: this job builds and smoke-tests the container's
# contents with dummy/no keys (see scripts/build-local.mjs and
# scripts/smoke.mjs), so it's safe to run on pull_request from forks.
steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
cache-dependency-path: '**/package-lock.json'

- name: Build every app (stages apps/<name>/ like the Dockerfile does)
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
run: node scripts/build-local.mjs --allow-missing-artifacts

- name: Validate lab manifest and cross-wiring
run: npm run check:labs && npm run test:labs

# Use `npm test` (not raw `node --test`): the npm script sets
# NODE_ENV=test. The server also guards on NODE_TEST_CONTEXT now, but
# belt and suspenders — a listening socket here hung CI for hours.
- name: Run gateway unit tests
working-directory: gateway
run: npm test

- name: Smoke test the gateway against the staged apps
run: node scripts/smoke.mjs

docker-build:
name: Docker Build Verification
# Pull requests verify the exact deployable container. A push to main is
# immediately built and smoke-tested by deploy.yml, so repeating this
# expensive build on the same commit only adds runner time and queueing.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
Expand All @@ -125,8 +89,9 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

# Layer-cached via the GitHub Actions cache: unchanged apps rebuild
# from cache instead of re-running npm ci + vite build in every stage.
# This is the single full application build in PR CI. Package jobs keep
# lint and unit-test isolation; the container build proves compilation,
# packaging, and the gateway boundary once.
- name: Build Docker Image
uses: docker/build-push-action@v7
with:
Expand Down Expand Up @@ -157,6 +122,18 @@ jobs:
done
BASE_URL=http://127.0.0.1:8080 node scripts/smoke.mjs

build-and-smoke:
# Branch protection requires this historical check name. Keep it as a
# lightweight compatibility gate over the production-shaped Docker job;
# do not rebuild every app a second time here.
name: Build all apps + gateway smoke test
if: github.event_name == 'pull_request'
needs: docker-build
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- run: echo "Docker build and gateway smoke test passed."

secret-scan:
name: Secret Scanner
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,13 @@ jobs:
run: |
set -euo pipefail
IMAGE="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/fieldwork/app:${GITHUB_SHA}"
CACHE_IMAGE="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/fieldwork/app:build-cache"
echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"
# `gcloud builds submit --tag` can't forward Docker --build-arg values,
# so the VITE_* build args are threaded through cloudbuild.yaml via
# substitutions instead (see that file's header comment).
gcloud builds submit --project "$GCP_PROJECT_ID" --config cloudbuild.yaml \
--substitutions=_IMAGE="$IMAGE",_VITE_GMP_API_KEY="${{ secrets.VITE_GMP_API_KEY }}",_VITE_ISOCHRONES_GMP_API_KEY="${{ secrets.VITE_ISOCHRONES_GMP_API_KEY }}",_VITE_STRAVA_CLIENT_ID="${{ secrets.VITE_STRAVA_CLIENT_ID }}",_ANALYTICS_MEASUREMENT_ID="${{ vars.ANALYTICS_MEASUREMENT_ID }}" .
--substitutions=_IMAGE="$IMAGE",_CACHE_IMAGE="$CACHE_IMAGE",_VITE_GMP_API_KEY="${{ secrets.VITE_GMP_API_KEY }}",_VITE_ISOCHRONES_GMP_API_KEY="${{ secrets.VITE_ISOCHRONES_GMP_API_KEY }}",_VITE_STRAVA_CLIENT_ID="${{ secrets.VITE_STRAVA_CLIENT_ID }}",_ANALYTICS_MEASUREMENT_ID="${{ vars.ANALYTICS_MEASUREMENT_ID }}" .

- name: Deploy to Cloud Run
if: github.event_name != 'schedule' || steps.scheduled_gate.outputs.due == 'true'
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ All notable changes to this project will be documented in this file.
- Added gateway test coverage verifying that all external image, font, and avatar hosts loaded by demo apps are permitted by their respective CSP policies.

### Changed
- Consolidated the pending Dependabot updates across Atlas, Hairstyle AI
Studio, Strava Explorer, Isochrones, and Air Quality Map, including the
required Tailwind 4 PostCSS compatibility update for Hairstyle AI Studio.
- Reduced CI/CD duplication so pull requests compile the complete application
once in the deployable Docker image, while package jobs retain isolated lint
and unit tests. Main-branch pushes now rely on the production Cloud Build
instead of rebuilding the same container in GitHub Actions, and Cloud Build
reuses the last successful image as an inline BuildKit cache. The historical
required smoke-check name remains as a lightweight gate over the Docker job
so branch protection does not wait for a removed status.
- Reworked Hairstyle AI Studio's Gemini access to match the proven hosted-plus-BYOK pattern: each client IP receives five successful image generations per UTC day, recommendation analysis stays outside that spend cap, and a validated memory-only personal key bypasses the shared allowance while retaining a separate abuse guard. The UI now opens directly into the studio, shows remaining free generations, and distinguishes shared exhaustion from personal-key provider quota.
- Updated Hairstyle AI Studio to current compatible dependencies with a clean
audit, routed optional recommendations to `gemini-3.5-flash-lite`, retained
Expand Down
20 changes: 20 additions & 0 deletions LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,26 @@ Evidence: Social cards were regenerated with `ryanbaumann.dev`; Lab metadata now
Use next time: Treat a domain move as a dependency inventory, not a string replacement. Check generated text in images, absolute asset URLs, deployment health targets, OAuth origins, email senders, analytics, API referrers, and search ownership before changing DNS.


## 2026-07-27 - Compile the deployable graph once per CI event

Context: Pull-request CI built each app in its package job, rebuilt every app
for a staged gateway smoke test, then rebuilt them again inside Docker. A
main-branch push repeated the Docker verification immediately before Cloud
Build produced the production image.
Learning: Keep fast package lint and unit tests isolated, but use the deployable
container as the single full compilation and smoke boundary on pull requests.
On main, let the production build own that boundary. Since Cloud Build workers
are ephemeral, explicitly pull the last successful image and export inline
BuildKit cache metadata. Before removing or renaming a CI job, inspect branch
protection's required contexts; preserve any required name as a cheap dependent
gate until the protection rule is deliberately migrated.
Evidence: `.github/workflows/ci.yml` removes the duplicate staged build and
per-package build step, gates Docker verification to pull requests, and keeps
the legacy required smoke context without a second compilation.
`cloudbuild.yaml` pulls, consumes, and refreshes the `build-cache` image.
Use next time: Before adding a build job, map which existing job already proves
compilation, packaging, or runtime liveness and add only missing evidence.

## 2026-07-17 - Distribution and privacy copy must match the deployed data path

Context: The live site loaded privacy-preserving GA4 analytics by default while the Privacy page described an opt-in control, and the email implementation still used Resend's retired Audience API.
Expand Down
18 changes: 18 additions & 0 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@
# True runtime secrets (STRAVA_CLIENT_SECRET, GMP_SERVER_API_KEY) are never
# built into the image; they live on the Cloud Run service (see deploy.yml).
steps:
# Cloud Build workers are ephemeral. Pull the last successful image so
# BuildKit can reuse its inline layer metadata; a cold cache is harmless.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: 'bash'
args:
- '-c'
- 'docker pull "${_CACHE_IMAGE}" || true'

- name: 'gcr.io/cloud-builders/docker'
env:
- 'DOCKER_BUILDKIT=1'
args:
- 'build'
- '--cache-from'
- '${_CACHE_IMAGE}'
- '--build-arg'
- 'BUILDKIT_INLINE_CACHE=1'
- '--build-arg'
- 'VITE_GMP_API_KEY=${_VITE_GMP_API_KEY}'
- '--build-arg'
Expand All @@ -23,6 +37,8 @@ steps:
- 'ANALYTICS_MEASUREMENT_ID=${_ANALYTICS_MEASUREMENT_ID}'
- '--tag'
- '${_IMAGE}'
- '--tag'
- '${_CACHE_IMAGE}'
- '.'

- name: 'gcr.io/cloud-builders/docker'
Expand All @@ -45,9 +61,11 @@ steps:

images:
- '${_IMAGE}'
- '${_CACHE_IMAGE}'

substitutions:
_IMAGE: ''
_CACHE_IMAGE: ''
_VITE_GMP_API_KEY: ''
_VITE_ISOCHRONES_GMP_API_KEY: ''
_VITE_STRAVA_CLIENT_ID: ''
Expand Down
12 changes: 6 additions & 6 deletions demos/aqi-map/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/aqi-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@googlemaps/js-api-loader": "^2.1.1",
"vite": "^8.0.16"
"vite": "^8.1.5"
},
"engines": {
"node": ">=20"
Expand Down
5 changes: 2 additions & 3 deletions demos/hairstyle-ai-studio/app/index.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@config "../tailwind.config.js";

/* Main Stylesheet */

Expand Down
Loading
Loading