How Solomon is built, tested, and shipped to production
Solomon uses two GitHub Actions workflows to optimize deploy times based on what changed. Each workflow has its own concurrency lock so they never interfere with each other.
deploy-and-release.yml
main (excludes functions/**)deploy-app-${{ github.ref }}deploy-functions.yml
main with changes only in functions/**deploy-functions-${{ github.ref }}functions/, this workflow detects that the Full Deploy will handle it and skips all subsequent jobs automaticallydeploy-app-* and deploy-functions-*) so they queue independently.
version.ts, runs npm install then unit tests, security rules tests (Firestore + Storage emulator), and finally npm run build:prod. Uploads artifact with 1-day retention.
functions/.env, authenticates via WIF, deploys hosting + Firestore + functions.
claude-sonnet-4-6) with the commit log to generate categorized release notes. Falls back to grep/sed categorization on failure.
When only functions/** files change, deploy-functions.yml runs a leaner pipeline. The version job now also checks whether the push touches any files outside functions/ β if so, all subsequent jobs are skipped because the Full Deploy workflow will handle everything (including functions).
The scope step computes the list of changed files between github.event.before and github.sha and filters out anything under functions/. If any outside files are found, it sets app_changed=true and all downstream jobs (test-functions, deploy, release) are skipped via an if condition.
The build job runs Firestore and Storage security rules tests against the Firebase Emulator before the production build.
npm run test:rules β covers tests/rules/firestore.test.mjs and tests/rules/storage.test.mjsnpm run build:prod. A rules failure blocks the build artifact from being created, preventing a broken deploy.
Version bumps are determined automatically from conventional commit messages in the commit log.
v{major}.{minor}.{patch} β each release creates an annotated git tag and a GitHub Release with categorized notes.
Solomon uses Workload Identity Federation (WIF) for GitHub Actions CI/CD β no stored service account keys.
github-actions-poolgithub-providergoogle-github-actions/auth@v2 with token_format: 'access_token'FIREBASE_TOKEN: ${{ steps.auth.outputs.access_token }}--allowed-audiences when creating the OIDC provider. The google-github-actions/auth@v2 action sends the provider resource name as the audience by default β setting a custom audience causes an "audience does not match" error.
On each release, the pipeline sends the conventional commit log to Claude (claude-sonnet-4-6) which generates categorized markdown release notes:
If the Claude API call fails (non-200 response), a shell-based fallback using grep and sed categorizes commits into the same sections.
| Issue | Solution |
|---|---|
| firebase-tools@13.15.3+ failed with 403 on the extensions API check (deploy) | Fixed in later versions. Every job now uses firebase-tools@15.6.0 |
| Firebase Emulator requires Java 21 with newer CLI versions | Add actions/setup-java@v4 (Temurin 21) before running rules tests in CI |
| npm ci fails with 403/404 on older transitive deps | Use npm install (resilient to registry errors) |
| SSL errors in Firebase deploy | Not needed in CI: GitHub's runners trust Google's certificates. Never set NODE_TLS_REJECT_UNAUTHORIZED |
| WIF audience mismatch | Omit --allowed-audiences on OIDC provider |
| Secret Manager permissions in deploy | Write secrets to functions/.env in CI before deploy, not firebase.json secrets[] or gcloud run services update |
| Shared concurrency group between workflows lets functions-only run cancel waiting full deploy | Give each workflow its own concurrency group: deploy-app-* and deploy-functions-* |
| Functions-only push also changes app files β both workflows trigger | The scope step in deploy-functions.yml detects outside-functions changes and skips all downstream jobs, letting the Full Deploy handle it |