Deploy from CI
A CI job starts from a clean checkout, with no terminal and nobody to answer a question. Recreate the few files git ignores, then one npm run deploy builds and deploys the project in one pass, with no prompt.
What CI must recreate
Some files hold one checkout's settings, so git ignores them. A clean checkout does not have them:
| File | In git? | How CI gets it |
|---|---|---|
.firebaserc | No | Write it in one line, with the aliases the job uses. Optional: every script also takes a project id where it takes an alias. |
arccms.config.json | No | npm run arc:configure with flags (below) writes it. |
firebase.<projectId>.json | No | Written by the same arc:configure, for a project with its own database, bucket, site or functions region. |
functions/.env.<projectId> | No | Written by the same arc:configure, which keeps every line it does not own. If your own functions read other values from it, write those lines from CI secrets first. |
src/environments/firebase-web.<projectId>.ts | Yes | Committed, so CI has it. arc:configure rewrites it from arccms.config.json and removes it when the config has no web settings for the project, so pass it back with --web-config. |
src/environments/arc-install.ts | Yes | Committed. arc:configure rewrites it with the projects the job configured, which is all the build needs. Never commit it from CI. |
.arc-deploy-state.json is ignored too, and CI does not need it: it only remembers the guided deploy's choices.
Recreate them with flags
Run arc:configure once per project the job deploys, with the same settings you gave it on your machine. Copy them from your own arccms.config.json: each key has a flag (see Config keys).
echo '{"projects":{"staging":"your-staging-project-id"}}' > .firebaserc
npm run arc:configure -- --project=staging \
--web-config=src/environments/firebase-web.your-staging-project-id.ts \
--functions-region=asia-south1
--web-config reads the web settings back from the committed file (an environment file, JSON or the Firebase console's snippet all work), so nothing has to be fetched. Add --database, --site, --bucket, --prefix or --build-mode when the project uses them.
Always pass --functions-region. On a first deploy, when the database is outside us-central1, the deploy sets the functions next to the database and saves that in arccms.config.json, which CI then throws away. Without the flag, the next run would put the functions in us-central1 again. Use the region the first deploy printed, or the one in your own config (see Regions).
Another way is to keep your whole arccms.config.json in a CI secret or variable, write it to the file, and run npm run arc:configure -- --project=staging with no other flags to write the rest. Its values are not secret: the web settings are public, and the rest names databases and buckets.
The service account
CI signs in as a Google Cloud service account of the project it deploys. The Firebase CLI, the website release and the static pages all use it through GOOGLE_APPLICATION_CREDENTIALS, which the auth step below sets.
- Simplest: Firebase Admin (
roles/firebase.admin) and Service Account User (roles/iam.serviceAccountUser) on the project. Firebase Admin covers Hosting, the rules, the Firestore indexes, the functions and the data the static pages are published from. Service Account User lets the deploy run the functions as the project's default compute account. - The region check runs
firebase firestore:databases:get, which needs thedatastore.databases.getpermission. Firebase Admin has it; with narrower roles, add Cloud Datastore Viewer (roles/datastore.viewer). Without it the deploy cannot see where the database is, and it deploys to the region inarccms.config.jsonwithout a check. - Narrower roles, if your organisation asks for them: Cloud Functions Admin, Firebase Hosting Admin, Firebase Rules Admin, Cloud Datastore Index Admin, Cloud Datastore User (the static pages), Service Account User and Cloud Datastore Viewer. A missing permission makes the Firebase CLI stop and name it; add that one.
- A first deploy to a new project turns on the Google Cloud services the functions need (listed in A first deploy to a new project), which needs the
serviceusage.services.enablepermission. Add Service Usage Admin (roles/serviceusage.serviceUsageAdmin) when the CLI stops and names it, or run that first deploy once from your own machine. - With the
smsfeature, the deploy also checks phone sign-in's Google Cloud setup withgcloud. Off a terminal it never asks, and withoutgcloudit says so and goes on.
Keyless sign-in (workload identity federation) is safer than a key file: GitHub proves who the job is, and there is no key to leak. The workflow below shows both.
Java for the rules tests
npm run test:rules runs the security rules tests in the Firebase emulators, which need Java 21. Install it with actions/setup-java, as below. The emulators run against a demo project and need no sign-in. npm run test needs no Java.
A GitHub Actions workflow
Save this as .github/workflows/deploy-staging.yml. Workflows are your app's own: npm run check:core counts .github/ as custom, and Arc CMS ships none. It tests, then deploys the staging project on every push to main, with no prompt. Replace the project id and the region with your own.
name: Deploy staging
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-staging
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
PROJECT_ID: your-staging-project-id
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
package-lock.json
functions/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
# Also installs the Firebase CLI pinned in package.json, which the npm scripts use for the deploy and the emulators.
- run: npm ci
- run: npm ci --prefix functions
- run: npm run test
- run: npm run test:rules
- uses: google-github-actions/auth@v2
with:
# A key: the service account's JSON key, saved as a repository secret.
credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
# Or keyless, instead of credentials_json:
# workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/your-repo
# service_account: deployer@your-staging-project-id.iam.gserviceaccount.com
- name: Recreate the files git ignores
run: |
echo "{\"projects\":{\"staging\":\"$PROJECT_ID\"}}" > .firebaserc
npm run arc:configure -- --project=staging \
--web-config=src/environments/firebase-web.$PROJECT_ID.ts \
--functions-region=asia-south1
- run: npm run deploy -- --project staging --non-interactive --force
- run: npm run seed -- staging
npm run testruns once and exits in CI (Vitest sees theCIvariable GitHub sets). It type-checks the app and the functions too, so a type error stops the job before the deploy.- The deploy builds the website with staging's settings and the functions, then deploys everything the config has.
--non-interactivetells the Firebase CLI to ask nothing, and--forcelets it delete what the code no longer has, such as deployed functions, instead of asking (see Functions that are deleted); use--onlyto deploy less. npm run seed -- stagingpublishes the static pages, as after every website deploy.- The website build needs a few GB of memory (see npm scripts); GitHub's standard runners have enough.
--force deletes functions without asking, on every run. Any Arc CMS or app function deployed in the project that this commit does not have is deleted: one you removed from functions/src/custom/, those of a feature you turned off, one Arc CMS renamed or dropped in an update you merged, or one deployed by hand from another branch. On your machine the deploy asks first; in CI nothing asks. Deploy from your machine once after such a change to see what would go, or deploy less with --only. See Functions that are deleted.
The first run on a new project takes longer. Google sets up Eventarc for the project's first database triggers in the minutes after the deploy turns it on, so they fail at first. The deploy recognises the error, waits 5 minutes and retries them, twice at most, so allow about 15 extra minutes for that run. If they still fail, the job fails with "Wait a few minutes, then run the same deploy again": re-run the job. The website is not deployed in a run whose functions failed. See A first deploy to a new project.
Production from CI
A project marked production (arc:configure --production=yes) is guarded on the command line too. A deploy with options deploys it only with its id given back:
npm run deploy -- --project production --confirm-production=your-live-project-id --non-interactive --force
Without the flag, a terminal asks you to type the id, and anywhere else the deploy stops before it does anything. An id that is not the target's stops any deploy, marked or not, so a command copied from another job can never deploy the wrong project. Projects that are not marked, like staging above, need nothing. On the job, recreate the marker with --production=yes like any other setting, or the guard is not there.