Keyless Deployment of a Static Website to Firebase Hosting

GitHub Actions with Google Cloud Workload Identity Federation

Jesse Carpenter, Carpenter Software
September 25, 2026

Contents

  1. Purpose and Result
  2. Values to Fill In
  3. Terminology
  4. Procedure
  5. Troubleshooting
  6. Routine Update Procedure
  7. Supplementary Notes
  8. Appendix A. Alternatives Considered
  9. Appendix B. Superseded Key Based Workflow
  10. Bibliography

1. Purpose and Result

This procedure replaces manual deployment with the Firebase command line interface (CLI) on a development computer. After setup, a git push to the default branch of a GitHub repository deploys the contents of the repository's public folder to Firebase Hosting automatically. No credential file is stored on the computer or in the repository.

The procedure was carried out and verified on September 25, 2026; the first successful deployment completed in 51 seconds.

2. Values to Fill In

Throughout this page, values in angle brackets are placeholders. Replace each one with your own value. The examples are fictional and shown only for clarity.

Placeholder Meaning Example
<PROJECT_ID> Firebase / Google Cloud project ID (Project settings, General tab) my-project-12345
<PROJECT_NUMBER> Google Cloud project number (Project settings, General tab) 123456789012
<GITHUB_OWNER> GitHub user or organization that owns the repository octocat
<REPOSITORY> Repository name, exactly as on GitHub (case sensitive) my-static-site
<BRANCH> Default branch of the repository main (older repositories: master)
<SERVICE_ACCOUNT_NAME> Name chosen for the deployment service account github-actions-deployer
<POOL_ID> Workload identity pool ID github-pool
<PROVIDER_ID> OIDC provider ID inside the pool github-provider

Two values are built from the placeholders:

  • Service account email: <SERVICE_ACCOUNT_NAME>@<PROJECT_ID>.iam.gserviceaccount.com
    Example: github-actions-deployer@my-project-12345.iam.gserviceaccount.com
  • Provider resource path: projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>
    Example: projects/123456789012/locations/global/workloadIdentityPools/github-pool/providers/github-provider

To confirm the owner, repository, and branch, open the repository on GitHub. The page header shows owner / repository, and the branch selector above the file list shows the default branch.

3. Terminology

GitHub Actions
The automation service built into GitHub. It executes jobs on GitHub hosted virtual machines (runners) in response to repository events such as a push.
Workflow
A YAML file in .github/workflows/ that defines the trigger event and the sequence of steps a runner executes.
Service account
A Google Cloud identity that represents a program rather than a person. Roles assigned to it define what it may do.
OpenID Connect (OIDC)
An identity protocol. GitHub issues each workflow run a signed OIDC token that states which repository and owner produced the run.
Workload Identity Federation (WIF)
A Google Cloud mechanism that accepts an external OIDC token and exchanges it for short lived Google credentials. It replaces the long lived JSON service account key, which does not expire and can be misused if leaked [1].
Workload identity pool and provider
The pool is a container for trusted external identities; the provider inside it defines the trusted issuer (GitHub) and how token claims map to Google attributes.
Attribute mapping and attribute condition
A mapping copies an OIDC claim (for example assertion.repository) into a Google attribute. A condition is a Common Expression Language (CEL) rule that must be true for a token to be accepted.
CI/CD (continuous integration and continuous deployment)
The practice of building and deploying automatically on each change to the repository.

4. Procedure

Part A. Create the Service Account

IAM & Admin is part of the Google Cloud Console, not the Firebase Console. Every Firebase project is also a Google Cloud project, so the Firebase Console links to it directly.

  1. In the Firebase Console, open the project and click the gear icon beside Project Overview, then Project settings.
  2. Open the Service accounts tab and click Manage service account permissions. The Google Cloud Console opens in a new tab at IAM & Admin for the project.
  3. In the left menu, click Service Accounts, then Create Service Account.
  4. Service account name: <SERVICE_ACCOUNT_NAME>. Click Create and Continue.
    Example: github-actions-deployer
  5. In Select a role, type Firebase Hosting Admin in the filter box and select it. It does not appear under "Currently used", because that list shows only roles already assigned in the project. Alternatively, select the Firebase category on the left and then Firebase Hosting Admin on the right.
  6. Click Continue, then Done. Assign the role before clicking Done; a service account without a role cannot deploy.

Part B. Create the Workload Identity Pool and OIDC Provider

  1. In the Google Cloud Console, go to IAM & Admin > Workload Identity Federation. On first use a Get started screen appears; click it.
  2. Pool name: <POOL_ID>. Click Next.
    Example: github-pool
  3. Provider source: OpenID Connect (OIDC). Provider name: <PROVIDER_ID>.
    Example: github-provider
  4. Issuer (URL): https://token.actions.githubusercontent.com
  5. Audience: Default audience. Click Next.
  6. Under Configure provider attributes, enter three mappings (use Add mapping for rows 2 and 3):
    Google attribute (left) OIDC value (right)
    google.subject assertion.sub
    attribute.repository assertion.repository
    attribute.repository_owner assertion.repository_owner

    The hints "Supported keys: google.subject, google.groups, attribute.custom_attribute" and "Value must be a CEL expression" are informational; any name after the attribute. prefix is valid.

  7. In Attribute conditions, enter the following. Use straight double quotes, not curly quotes:
    assertion.repository_owner == "<GITHUB_OWNER>"

    Example:

    assertion.repository_owner == "octocat"
  8. Click Save. The pool details page confirms success.

The attribute condition is mandatory. GitHub uses a single issuer URL for all accounts, so Google Cloud requires a condition that restricts tokens to your GitHub owner [1][2]. Every claim the condition references must also be mapped, which is why the third mapping is needed.

Part C. Grant the Repository Access to the Service Account

  1. On the pool details page, click Grant access.
  2. Choose Grant access using service account impersonation. This grants the pool the Workload Identity User role (roles/iam.workloadIdentityUser) on the service account, which the workflow requires because it names a service account [1][2]. Do not download the Application Default Credential (ADC) file.
  3. Service account: <SERVICE_ACCOUNT_NAME>.
  4. Select principals: attribute.repository. Value: <GITHUB_OWNER>/<REPOSITORY>.
    Example: octocat/my-static-site
  5. Click Save.
  6. A Configure your application dialog appears, showing the service account, the provider, and fields for "OIDC ID token path" and "Format type". Close it; nothing needs to be filled in or downloaded. The google-github-actions/auth action performs the token exchange in memory during each run [2].

Google Cloud configuration is now complete. Only runs originating from <GITHUB_OWNER>/<REPOSITORY> can impersonate the service account.

The auth action documentation lists direct federation without a service account as its preferred option; impersonation is fully supported and is the method verified here [2].

Part D. Repository Layout and Files

Repository layout:

<REPOSITORY>/
├── .github/
│   └── workflows/
│       └── deploy.yml        (deployment workflow)
├── public/                   (website files)
│   └── index.html            (home page; required)
├── .gitignore
└── firebase.json

Example (package.json and package-lock.json are optional and appear only if the project uses npm):

my-static-site/
├── .git/
├── .github/
│   └── workflows/
│       └── deploy.yml
├── public/
│   ├── index.html
│   ├── styles.css
│   └── script.js
├── .gitignore
├── firebase.json
├── package.json
└── package-lock.json

The home page must be named index.html in lowercase and placed directly inside public. Otherwise the deployment succeeds but the site shows a blank page.

Folders whose names begin with a period are hidden in the macOS Finder by default (press Cmd+Shift+. to show them). Create .github/workflows/ in a code editor, or on the GitHub website by typing .github/workflows/deploy.yml as the new file name.

firebase.json (repository root)

Identifies public as the folder to deploy and excludes hidden files and dependencies from upload. This file contains no project specific values.

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

.gitignore (repository root)

Excludes operating system metadata and local caches from Git. .DS_Store is a macOS folder settings file; Thumbs.db is the Windows equivalent.

# Local OS metadata
.DS_Store
Thumbs.db

# Dependencies and Firebase caches
node_modules/
.firebase/

.github/workflows/deploy.yml

The deployment workflow. YAML uses indentation to define structure, so spaces must be exact (two spaces per level, no tab characters). The workload_identity_provider and service_account values must each stay on one line.

name: Deploy Static Site to Firebase Hosting

on:
  push:
    branches:
      - <BRANCH>

permissions:
  contents: read
  id-token: write # Required for Workload Identity Federation

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v7

      # Optional: Uncomment if your site has a build step (e.g., Vite, Webpack)
      # - name: Install & Build
      #   run: |
      #     npm ci
      #     npm run build

      - name: Authenticate to Google Cloud (Keyless)
        uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: 'projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>'
          service_account: '<SERVICE_ACCOUNT_NAME>@<PROJECT_ID>.iam.gserviceaccount.com'

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Deploy to Firebase Hosting
        run: firebase deploy --only hosting --project <PROJECT_ID>

Example, with the fictional values filled in:

name: Deploy Static Site to Firebase Hosting

on:
  push:
    branches:
      - main

permissions:
  contents: read
  id-token: write # Required for Workload Identity Federation

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v7

      # Optional: Uncomment if your site has a build step (e.g., Vite, Webpack)
      # - name: Install & Build
      #   run: |
      #     npm ci
      #     npm run build

      - name: Authenticate to Google Cloud (Keyless)
        uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: 'projects/123456789012/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'github-actions-deployer@my-project-12345.iam.gserviceaccount.com'

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Deploy to Firebase Hosting
        run: firebase deploy --only hosting --project my-project-12345

id-token: write permits the runner to request the OIDC token. The Firebase CLI runs only on the GitHub runner; it is not installed or used on the development computer. No GitHub Secrets are required.

Part E. Verify GitHub Settings

  1. On GitHub, open the repository and click Settings.
  2. In the left sidebar, click Actions > General.
  3. Under Actions permissions, confirm Allow all actions and reusable workflows is selected. The workflow uses actions published by GitHub (actions/checkout) and Google (google-github-actions/auth).
  4. No entries are needed under Secrets and variables.

Part F. Commit, Push, and Verify

  1. Commit and push from the repository root:
    git add .
    git commit -m "Configure keyless deployment via WIF"
    git push origin <BRANCH>

    Example: git push origin main

  2. On GitHub, open the repository Actions tab. Before the first workflow file is pushed, GitHub shows "Get started with GitHub Actions"; afterward it shows All workflows with Deploy Static Site to Firebase Hosting.
  3. Open the newest run and click the deploy job to view each step: Checkout Code, Authenticate to Google Cloud (Keyless), Install Firebase CLI, Deploy to Firebase Hosting. A yellow circle means running, a green check means success, and a red X means failure. The log ends with the hosting URLs, <PROJECT_ID>.web.app and <PROJECT_ID>.firebaseapp.com.
    Example: my-project-12345.web.app
  4. Confirm the change on the live site. If an old version appears, force reload (Cmd+Shift+R on Mac, Ctrl+F5 on Windows).

Editing a file on the GitHub website and clicking Commit changes is also a push, and triggers a run.

5. Troubleshooting

Symptom Cause and fix
Creating the provider fails: "The attribute condition must reference one of the provider's claims." The attribute condition is empty, or it references a claim that is not mapped. Add the attribute.repository_owner mapping and the condition from Part B [1].
"This workflow graph cannot be shown" with the annotation "No event triggers defined in on" Incorrect indentation in the on: block. Required: on: at column 0, push: 2 spaces, branches: 4 spaces, - <BRANCH> 6 spaces. The annotation is on the run details page under Annotations; the commit diff page does not show it.
Run fails at "Authenticate to Google Cloud" Typo in the provider path, the service account email, or the repository value in Part C.
Run fails at "Deploy to Firebase Hosting" firebase.json is missing from the repository root, or the public folder was not found.
Deploy step fails with "Failed to authenticate, have you run firebase login?" npm install -g firebase-tools always installs the newest release. A regression in firebase-tools 15.22.2 (June 2026, since fixed) broke keyless authentication [5]. Pin the last version that worked, shown in the "Install Firebase CLI" log of a successful run.
Example: npm install -g firebase-tools@15.22.1
Deploy step fails with a permission (403) error The service account lacks a role. The Firebase action documentation also lists API Keys Viewer (roles/serviceusage.apiKeysViewer) for CLI deploys; add it to the service account in IAM if required [6].
Run page is blank The runner is starting. Wait 10 to 15 seconds and refresh.
deploy.yml editor is empty The file was cleared or a new file was opened. Paste the workflow from Part D and commit.
Run succeeds but the website is blank index.html is missing, misnamed, or not directly inside public.
Old version still shown Browser cache. Force reload (Cmd+Shift+R).

6. Routine Update Procedure

  1. Edit files in the local public folder.
  2. Run the following from the repository root:
    git add .
    git commit -m "Update homepage"
    git push origin <BRANCH>
  3. GitHub Actions deploys automatically, typically in under one minute, and Firebase Hosting clears its content delivery network (CDN) cache on each deploy [7]. No login to GitHub, Firebase, or Google Cloud is needed, and the computer need not stay on after the push completes. Open the Actions tab only to inspect logs.

7. Supplementary Notes

Verifying Git Authentication on the Development Computer

A successful git push already demonstrates that the computer is authenticated; GitHub rejects unauthorized pushes with "Permission denied". Authentication is configured once per computer (GitHub Desktop, an SSH key, or a personal access token). To see which method is in use, run in the repository folder:

git remote -v
# git@github.com:...     = SSH key
# https://github.com/... = credential manager or personal access token

ssh -T git@github.com
# SSH only. Expected reply:
# Hi <GITHUB_OWNER>! You've successfully authenticated, but GitHub does not provide shell access.

Repository Visibility

The setup works with a public or private repository. The workflow file contains no secrets, and the attribute condition plus the repository principal restrict deployment to <GITHUB_OWNER>/<REPOSITORY>. A private repository keeps the website source from public view.

GitHub Actions Cost

No paid GitHub plan is required. Actions usage is free for public repositories using standard GitHub hosted runners. Private repositories receive a monthly allowance: GitHub Free, 2,000 minutes and 500 MB artifact storage; GitHub Pro, 3,000 minutes and 1 GB; GitHub Team, 3,000 minutes and 2 GB [8]. Each deployment uses about one minute.

Without a payment method on file, usage is blocked when the allowance is used up. With a payment method on file (for example, from a GitHub Copilot subscription, which is billed separately and adds no Actions minutes), usage beyond the allowance is billed unless a budget stops it [8][9]. To prevent charges, open Settings > Billing and licensing > Budgets and alerts, create a $0 budget for Actions, and enable the option to stop usage when the budget is reached. A new budget applies only to usage from its creation date onward [9].

Custom Domain

A custom domain connected to the Firebase Hosting site is updated by every deployment, together with the web.app and firebaseapp.com addresses. To add one without the CLI [10]:

  1. In the Firebase Console, open the project and select Hosting.
  2. Click Add custom domain and enter the domain.
    Example: www.example.com
  3. Add the DNS records Firebase supplies at the domain registrar: a TXT record for ownership verification, and A and AAAA records that direct traffic to Firebase Hosting. Remove conflicting CNAME records.
  4. Firebase provisions an SSL certificate automatically, usually within a few hours and up to 24 hours after DNS points to Firebase Hosting.

Appendix A. Alternatives Considered

These options were considered but not used:

  • Firebase Console: web interface for managing Firebase products and settings.
  • Google Cloud Console: manages databases, Cloud Functions, storage buckets, and IAM for the same project.
  • Terraform: infrastructure as code, using the Google Cloud and Firebase providers.
  • Firebase Management REST API: manages projects and apps through HTTP requests.
  • Firebase App Hosting: push to deploy for full stack frameworks such as Next.js or Angular, set up in the Firebase Console (App Hosting > Get Started, connect GitHub, choose branch). Not suited to a static site.

Appendix B. Superseded Key Based Workflow (Reference Only)

An earlier approach used a downloaded JSON service account key stored as a GitHub secret. It was abandoned in favor of Workload Identity Federation and should not be used alongside the keyless workflow. It is recorded here for reference.

  1. Create the service account as in Part A, then open it, select Keys > Add Key > Create new key > JSON, and download the file.
  2. On GitHub: Settings > Secrets and variables > Actions > New repository secret. Name: FIREBASE_SERVICE_ACCOUNT. Value: the entire JSON file contents.
  3. Delete the downloaded JSON file and never commit it to the repository.
  4. Use this workflow in place of the keyless one [11]:
    name: Deploy Static Site to Firebase Hosting
    
    on:
      push:
        branches:
          - <BRANCH>
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v7
    
          - name: Deploy to Firebase Hosting
            uses: FirebaseExtended/action-hosting-deploy@v0
            with:
              repoToken: ${{ secrets.GITHUB_TOKEN }}
              firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
              projectId: <PROJECT_ID>
              channelId: live

    Example: projectId: my-project-12345

For this method, the Firebase action documentation lists additional service account roles, including API Keys Viewer, and Firebase Authentication Admin for preview channels [6].

Bibliography

All sources accessed September 25, 2026.

  1. Google Cloud. "Configure Workload Identity Federation with Deployment Pipelines." Google Cloud Documentation. https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines.
  2. Google GitHub Actions. "auth: A GitHub Action for Authenticating to Google Cloud." GitHub. https://github.com/google-github-actions/auth.
  3. GitHub. "Deprecation of Node 20 on GitHub Actions Runners." GitHub Changelog, September 19, 2025. https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/.
  4. GitHub. "actions/checkout: Releases." GitHub. https://github.com/actions/checkout/releases.
  5. Firebase. "Auth Regression in 15.22.2: ADC / Workload Identity Federation Fails." firebase-tools Issue #10716, GitHub, June 25, 2026. https://github.com/firebase/firebase-tools/issues/10716.
  6. FirebaseExtended. "Service Account." action-hosting-deploy Documentation, GitHub. https://github.com/FirebaseExtended/action-hosting-deploy/blob/main/docs/service-account.md.
  7. Firebase. "Manage Cache Behavior." Firebase Documentation. https://firebase.google.com/docs/hosting/manage-cache.
  8. GitHub. "GitHub Actions Billing." GitHub Docs. https://docs.github.com/en/billing/concepts/product-billing/github-actions.
  9. GitHub. "Budgets and Alerts." GitHub Docs. https://docs.github.com/en/billing/concepts/budgets-and-alerts.
  10. Firebase. "Connect a Custom Domain." Firebase Documentation. https://firebase.google.com/docs/hosting/custom-domain.
  11. FirebaseExtended. "action-hosting-deploy: Deploy to Firebase Hosting." GitHub. https://github.com/FirebaseExtended/action-hosting-deploy.

Disclaimer: This procedure is provided as is, without warranty of any kind. Cloud consoles, action versions, and pricing change over time; verify against the sources above before use. You are responsible for the security and cost of your own accounts.

© 2026 Jesse Carpenter, Carpenter Software. All rights reserved.