CI/CD Pipeline¶
ScamShield AI uses GitHub Actions for continuous integration and deployment. Pushing to main triggers an automated test-then-deploy pipeline. Pull requests and feature branches run tests only.
Pipeline Overview¶
Push to feature branch / PR to main
└── test.yml: Run pytest suite
Push to main
└── deploy.yml:
├── test job: Run pytest suite
├── deploy job: Deploy Cloud Functions (Firebase)
└── deploy-dashboard job: Deploy Dashboard (Cloud Run)
Workflows¶
test.yml -- Test on PRs and Feature Branches¶
Trigger: Push to any non-main branch, or pull request targeting main.
name: Run Tests
on:
push:
branches-ignore: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: |
pip install -r functions/requirements.txt
pip install pytest
- run: pytest tests/ -v
deploy.yml -- Deploy on Push to Main¶
Trigger: Push to main, or manual dispatch from the GitHub Actions UI.
The deploy workflow has three jobs:
- test -- Runs the full pytest suite
- deploy -- Deploys Cloud Functions via Firebase CLI (depends on test passing)
- deploy-dashboard -- Deploys the Streamlit dashboard to Cloud Run (depends on test passing)
Both deploy jobs use Workload Identity Federation for authentication (no stored secrets).
Workload Identity Federation (WIF)¶
Why WIF Over Stored Secrets¶
Traditional GitHub Actions deployments store a GCP service account key as a GitHub Secret. This approach has problems:
- Key rotation burden -- JSON keys don't expire automatically
- Blast radius -- A leaked key grants full service account access until revoked
- Audit gaps -- Hard to trace which workflow used a key
Workload Identity Federation eliminates stored keys entirely. Instead, GitHub Actions exchanges a short-lived OIDC token for GCP credentials at runtime.
How OIDC Token Exchange Works¶
1. GitHub Actions generates a signed OIDC JWT
(claims include: repository, ref, workflow, actor)
2. JWT is sent to GCP's Security Token Service (STS)
via the google-github-actions/auth action
3. STS validates the JWT against the WIF pool/provider config
(checks issuer, repository_owner attribute condition)
4. STS returns a federated access token
5. The federated token is exchanged for a short-lived
service account access token (1-hour TTL)
6. All subsequent gcloud/firebase commands use this token
Running setup-wif.sh¶
The repository includes a one-time setup script at the project root:
Before running, edit the variables at the top of the script:
PROJECT_ID="your-gcp-project-id"
GITHUB_REPO="your-github-org/scamshield-ai"
GITHUB_OWNER="your-github-org"
The script performs these steps:
- Enables APIs --
iamcredentials,sts,cloudresourcemanager - Creates service account --
firebase-github-deploy@your-gcp-project-id.iam.gserviceaccount.com - Grants IAM roles -- See required roles below
- Creates WIF pool --
github-actions-pool - Creates OIDC provider --
github-actions-provider(issuer:token.actions.githubusercontent.com) - Binds WIF to service account -- Restricted to your GitHub organization's repositories
After running, the script outputs the WIF_PROVIDER and SERVICE_ACCOUNT values to use in deploy.yml.
Service Account IAM Roles¶
The deploy service account needs these roles:
| Role | Purpose |
|---|---|
roles/cloudfunctions.developer |
Deploy Cloud Functions |
roles/run.developer |
Deploy Cloud Run services |
roles/iam.serviceAccountUser |
Act as the service account |
roles/firebaserules.admin |
Deploy Firestore security rules |
roles/firebasehosting.admin |
Deploy Firebase Hosting (if used) |
roles/secretmanager.viewer |
Read secret metadata for deployment |
roles/storage.objectAdmin |
Upload function source to Cloud Storage |
roles/firebase.admin |
General Firebase admin operations |
roles/artifactregistry.writer |
Push Docker images for Cloud Run |
roles/datastore.indexAdmin |
Manage Firestore indexes |
roles/serviceusage.serviceUsageConsumer |
Enable/check API usage |
Workflow Authentication Config¶
In deploy.yml, the auth step references your WIF provider and service account:
permissions:
contents: read
id-token: write # Required for OIDC token generation
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: "projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider"
service_account: "firebase-deploy@your-gcp-project-id.iam.gserviceaccount.com"
The id-token: write permission is required
Without id-token: write in the workflow's permissions block, the GitHub Actions runner cannot generate the OIDC token, and authentication will fail silently.
Manual Deploy Trigger¶
You can trigger a deployment manually from the GitHub Actions UI:
- Go to Actions > Deploy to Firebase
- Click Run workflow
- Select the
mainbranch - Click Run workflow
This is configured via workflow_dispatch in deploy.yml:
Troubleshooting¶
"Unable to detect a Firebase project"¶
The Firebase CLI needs a project reference. Ensure deploy.yml includes --project your-gcp-project-id:
"Error: PERMISSION_DENIED"¶
Check that:
- The service account has all required IAM roles
- The WIF binding is correct (check
attribute.repositorymatches your repo) - The
id-token: writepermission is set in the workflow
Verify IAM roles:
gcloud projects get-iam-policy your-gcp-project-id \
--flatten="bindings[].members" \
--filter="bindings.members:firebase-github-deploy@" \
--format="table(bindings.role)"
"venv not found" During Deploy¶
The deploy job must create functions/venv before running firebase deploy:
- name: Create venv and install dependencies
run: |
python -m venv functions/venv
functions/venv/bin/pip install -r functions/requirements.txt
This step is already in deploy.yml but can fail if the Python version does not match (must be 3.11).
Tests Pass Locally but Fail in CI¶
Common causes:
- Missing test dependencies -- Ensure
pytestis installed in the CI job - Python version mismatch -- CI uses the version specified in
setup-python. Ensure it matches3.11 - Import path issues -- Tests run from the repository root. Ensure
PYTHONPATHincludesfunctions/or that test imports are correctly configured viaconftest.py
WIF Provider Not Found¶
If the auth step fails with "provider not found":
# Verify the provider exists
gcloud iam workload-identity-pools providers describe github-actions-provider \
--workload-identity-pool=github-actions-pool \
--location=global \
--project=your-gcp-project-id
If it does not exist, re-run setup-wif.sh.