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:

FileIn git?How CI gets it
.firebasercNoWrite 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.jsonNonpm run arc:configure with flags (below) writes it.
firebase.<projectId>.jsonNoWritten by the same arc:configure, for a project with its own database, bucket, site or functions region.
functions/.env.<projectId>NoWritten 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>.tsYesCommitted, 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.tsYesCommitted. 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.

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

--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.