React Deploy to Firebase Hosting

react-deploy-firebase@1.0.1 Template

Build a React web app and publish it to Firebase Hosting, live or to a preview channel.

Start from it

flightstack template use react-deploy-firebase

What it does

Install, build and deploy a React web app to Firebase Hosting in one job, asserting the built bundle before it can ship.

Inputs

projectId string required
Firebase project id, e.g. postwire-ea1e8.
site string
Hosting site name. Naming it is load-bearing: an unnamed deploy targets the project's DEFAULT site, and a project can host more than one app, so leaving this empty lets a deploy overwrite a sibling site by omission.
channel string
Preview channel name. Empty deploys to the live site.
expires string default 7d
How long a preview channel lives. Ignored for live deploys.
workingDir string default .
Directory holding package.json and firebase.json.
outputDir string default dist
Directory the build writes to. Only used for the bundle assertions - what actually gets uploaded is whatever firebase.json declares.
build boolean default true
Install dependencies and build before deploying.
buildScript string default build
The npm script that produces the production bundle.
packageManager auto | npm | pnpm | yarn | bun default auto
Which package manager to use.
env object
Build-time environment variables, compiled INTO the bundle and therefore public. Never put a secret here.
mustContain array
Strings that must appear in the built JavaScript.
mustNotContain array
Strings that must NOT appear, e.g. "localhost:". Checked before the upload, so a bad bundle never reaches the site.
include string default *.js
Filename pattern the assertions are checked against.
firebaseToolsVersion string default latest
Version of firebase-tools to run via npx.

Definition

name: React Deploy to Firebase Hosting
description: >-
  Build a React web app and publish it to Firebase Hosting, to the live site or
  to a preview channel.

triggers:
  - type: push
    branches: [main]

jobs:
  deploy:
    name: Deploy console
    type: plugin/react-deploy-firebase
    with:
      projectId: postwire-ea1e8
      site: postwire-console-app
      outputDir: apps/console/dist
      env:
        VITE_API_URL: https://api.postwire.ai
      mustContain:
        - api.postwire.ai

plugin:
  name: react-deploy-firebase
  displayName: React Deploy to Firebase Hosting
  description: >-
    Install, build and deploy a React web app to Firebase Hosting. Does the
    whole thing in one job because a job gets a fresh workspace - a deploy job
    that expected an earlier job's dist/ would find nothing.
  category: deploy

  requiredTools:
    - name: nodejs
      version: ">=20.0.0"

  supportedPlatforms:
    - linux
    - macos
    - windows

  parameters:
    type: object
    properties:
      projectId:
        type: string
        description: Firebase project id, e.g. postwire-ea1e8.
      site:
        type: string
        default: ""
        description: >-
          Hosting site name. Naming it is load-bearing: an unnamed deploy
          targets the project's DEFAULT site, and a project can host more than
          one app, so leaving this empty lets a deploy overwrite a sibling site
          by omission.
      channel:
        type: string
        default: ""
        description: >-
          Preview channel name. Empty deploys to the live site.
      expires:
        type: string
        default: 7d
        description: How long a preview channel lives. Ignored for live deploys.
      workingDir:
        type: string
        default: "."
        description: Directory holding package.json and firebase.json.
      outputDir:
        type: string
        default: dist
        description: >-
          Directory the build writes to. Only used for the bundle assertions -
          what actually gets uploaded is whatever firebase.json declares.
      build:
        type: boolean
        default: true
        description: Install dependencies and build before deploying.
      buildScript:
        type: string
        default: build
        description: The npm script that produces the production bundle.
      packageManager:
        type: string
        enum: [auto, npm, pnpm, yarn, bun]
        default: auto
        description: Which package manager to use.
      env:
        type: object
        description: >-
          Build-time environment variables, compiled INTO the bundle and
          therefore public. Never put a secret here.
      mustContain:
        type: array
        description: Strings that must appear in the built JavaScript.
      mustNotContain:
        type: array
        description: >-
          Strings that must NOT appear, e.g. "localhost:". Checked before the
          upload, so a bad bundle never reaches the site.
      include:
        type: string
        default: "*.js"
        description: Filename pattern the assertions are checked against.
      firebaseToolsVersion:
        type: string
        default: latest
        description: Version of firebase-tools to run via npx.
    required: [projectId]

  script: |
    set -euo pipefail
    cd "{{workingDir}}"

    # Said up front rather than discovered after a full build - but NOT fatal.
    # A self-hosted agent may already hold credentials from `firebase login` or
    # from application-default credentials on the machine, and refusing to try
    # would break a setup that works. If there is genuinely no auth the CLI says
    # so, clearly, at the point it matters.
    if [ -z "${FIREBASE_TOKEN:-}" ] && [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]; then
      echo "note: neither FIREBASE_TOKEN nor GOOGLE_APPLICATION_CREDENTIALS is set."
      echo "note: relying on credentials the agent already holds."
      echo "note: if the deploy cannot authenticate, set one as a pipeline secret."
    fi

    pm="{{packageManager}}"
    if [ "$pm" = "auto" ]; then
      if [ -f pnpm-lock.yaml ]; then pm=pnpm
      elif [ -f yarn.lock ]; then pm=yarn
      elif [ -f bun.lockb ] || [ -f bun.lock ]; then pm=bun
      else pm=npm
      fi
    fi

    case "$pm" in
      pnpm|yarn) corepack enable >/dev/null 2>&1 || true ;;
    esac

    {{#build}}
    echo "--- installing dependencies"
    case "$pm" in
      npm)  if [ -f package-lock.json ]; then npm ci; else npm install; fi ;;
      pnpm) pnpm install --frozen-lockfile ;;
      yarn) yarn install --immutable || yarn install --frozen-lockfile ;;
      bun)  bun install --frozen-lockfile ;;
    esac

    echo "--- building"
    {{#each env}}
    export {{@key}}="{{this}}"
    {{/each}}
    "$pm" run "{{buildScript}}"
    {{/build}}

    out="{{outputDir}}"
    failed=0
    {{#each mustContain}}
    if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
      echo "ok: bundle contains '{{this}}'"
    else
      echo "FAIL: '{{this}}' is missing from the bundle - refusing to deploy" >&2
      failed=1
    fi
    {{/each}}
    {{#each mustNotContain}}
    if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
      echo "FAIL: '{{this}}' appears in the bundle - refusing to deploy" >&2
      failed=1
    else
      echo "ok: bundle is free of '{{this}}'"
    fi
    {{/each}}
    [ "$failed" = "0" ] || exit 1

    FIREBASE="npx --yes firebase-tools@{{firebaseToolsVersion}}"

    site="{{site}}"
    channel="{{channel}}"

    if [ -n "$channel" ]; then
      echo "--- deploying to preview channel '$channel'"
      if [ -n "$site" ]; then
        $FIREBASE hosting:channel:deploy "$channel" \
          --only "$site" \
          --project "{{projectId}}" \
          --expires "{{expires}}" \
          --non-interactive
      else
        $FIREBASE hosting:channel:deploy "$channel" \
          --project "{{projectId}}" \
          --expires "{{expires}}" \
          --non-interactive
      fi
    else
      # An `if` rather than `[ -n "$site" ] && target=...`: under `set -e` that
      # one-liner exits the script when site is empty, which is the default.
      target="hosting"
      if [ -n "$site" ]; then
        target="hosting:$site"
      fi
      echo "--- deploying to $target"
      $FIREBASE deploy --only "$target" \
        --project "{{projectId}}" \
        --non-interactive
    fi