Skip to content
93 changes: 9 additions & 84 deletions .github/workflows/deploy-functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false

env:
PROJECT_ID: ${{ github.event.inputs.environment == 'prod' && 'hello-wisdom-prod' || 'hello-wisdom-staging' }}
CREDENTIALS_JSON: ${{ github.event.inputs.environment == 'prod' && secrets.FIREBASE_PROD_SERVICE_ACCOUNT || secrets.FIREBASE_STAGING_SERVICE_ACCOUNT }}

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

Expand All @@ -41,87 +44,9 @@ jobs:
exit 1
fi

- name: Set up Node.js for Firebase CLI
uses: actions/setup-node@v3
- name: Deploy Firebase Functions
uses: ./
with:
node-version: '20'

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

- name: Install uv package manager
uses: astral-sh/setup-uv@v5
with:
version: "0.6.4"

- name: Install Python 3.11 with uv
run: uv python install 3.11

- name: Use uv to install dependencies in /functions/venv
working-directory: functions
run: |
uv venv venv --python 3.11
source venv/bin/activate
uv pip install --upgrade pip
uv sync --active
uv pip freeze > requirements.txt
deactivate

# Store key in the runner's environment variable to persist across steps
- name: Authenticate with GCP service account key
env:
SERVICE_ACCOUNT_JSON: ${{ github.event.inputs.environment == 'prod' && secrets.FIREBASE_PROD_SERVICE_ACCOUNT || secrets.FIREBASE_STAGING_SERVICE_ACCOUNT }}
run: |
echo "$SERVICE_ACCOUNT_JSON" > service-account.json
echo "GOOGLE_APPLICATION_CREDENTIALS=service-account.json" >> $GITHUB_ENV

- name: Authenticate GitHub Actions service account
run: |
gcloud auth activate-service-account --key-file=service-account.json

- name: Verify service account ADC (Debug)
run: |
gcloud config get-value account
gcloud auth list

- name: Verify Enabled APIs (Debug)
run: |
gcloud services list --enabled --project=${{ github.event.inputs.environment == 'prod' && 'hello-wisdom-prod' || 'hello-wisdom-staging' }}

- name: List Firebase Projects (Debug Mode)
run: firebase projects:list --debug

- name: Verify Service Account IAM Policy (Debug)
run: |
PROJECT_ID=${{ github.event.inputs.environment == 'prod' && 'hello-wisdom-prod' || 'hello-wisdom-staging' }}
echo "Verifying IAM policy for project: $PROJECT_ID"
gcloud projects get-iam-policy $PROJECT_ID --format=json

- name: Select Firebase Project
id: select_project
run: |
if [ "${{ github.event.inputs.environment }}" = "prod" ]; then
firebase use prod --non-interactive
else
firebase use staging --non-interactive
fi

- name: Deploy Firebase Functions (Debug Mode)
run: firebase deploy --only functions --debug

- name: Post-deployment summary
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
TRIGGER: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "### Deployment Summary 🚀" >> $GITHUB_STEP_SUMMARY
echo "* **Environment**: $ENVIRONMENT" >> $GITHUB_STEP_SUMMARY
echo "* **Trigger**: $TRIGGER" >> $GITHUB_STEP_SUMMARY
echo "* **Branch/Ref**: $REF_NAME" >> $GITHUB_STEP_SUMMARY

- name: Cleanup credentials
if: always()
run: rm -f service-account.json
project_id: ${{ env.PROJECT_ID }}
service_account_json: ${{ env.CREDENTIALS_JSON }}
deploy_command: "firebase deploy --only functions"
22 changes: 11 additions & 11 deletions .github/workflows/lint-pr-title.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
name: Lint PR Title
name: "Lint PR"

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened

jobs:
lint-pr-title:
name: Validate PR Title
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
Expand All @@ -33,6 +36,3 @@ jobs:
✅ feat: Add new deployment option
✅ fix(functions): Resolve authentication issue
❌ feat: add new deployment option
wip: true
validateSingleCommit: false
validateSingleCommitMatchesPrTitle: false
82 changes: 82 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Repository Overview

This is a GitHub Action for deploying Python-based Firebase Cloud Functions. The repository serves dual purposes:
1. GitHub Action implementation (`action.yml`)
2. Live example Firebase function (`functions/`)

## Key Commands

### Development Commands
```bash
# Install Python dependencies (in functions/ directory)
cd functions && uv sync

# Generate requirements.txt for Firebase deployment
cd functions && uv pip freeze > requirements.txt

# Start Firebase emulators for local development
firebase emulators:start

# Deploy to Firebase (default)
firebase deploy --only functions

# Deploy specific functions
firebase deploy --only functions:api,functions:webhook

# Deploy using make
make deploy-staging

# Select Firebase project
firebase use <project_id>
```

### Testing and Validation
```bash
# Test the action locally (requires service account)
firebase deploy --only functions --debug

# List available Firebase projects
firebase projects:list

# Check Firebase emulator status
firebase emulators:start --only functions,auth
```

## Architecture

### GitHub Action Structure
- `action.yml`: Composite action definition with steps for deployment
- Uses `uv` for Python dependency management
- Integrates with `google-github-actions/auth@v2` for authentication
- Supports configurable deployment commands via `deploy_command` input
- Can execute Firebase CLI commands or make targets

### Firebase Function Structure
- `functions/main.py`: Simple Flask-based HTTP function
- `functions/pyproject.toml`: Python dependencies (firebase-admin, firebase-functions, flask)
- `firebase.json`: Firebase configuration with Python 3.11 runtime
- Emulator ports: Functions (5001), Auth (9099)

### Key Dependencies
- Python 3.10-3.11 (Firebase Functions constraint)
- `uv` package manager for dependency management
- Firebase CLI for deployment
- Node.js 20 for Firebase CLI

## Development Workflow

1. Modify functions in `functions/main.py`
2. Update dependencies in `functions/pyproject.toml` if needed
3. Run `uv sync` to update lockfile
4. Test locally with Firebase emulators
5. Deploy with `firebase deploy --only functions`

## Security Notes

- Service account credentials are handled securely via GitHub Actions
- Credentials are automatically cleaned up after deployment
- Uses Application Default Credentials (ADC) pattern
126 changes: 43 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,111 +4,71 @@ A GitHub Action for deploying Python-based Firebase Cloud Functions. This reposi

## Features

- 🐍 Automatic Python version detection from pyproject.toml
- 📦 Dependency management using UV package manager
- 🔄 Environment-based deployments (staging/prod)
- 🐍 Python dependency management from pyproject.toml
- 🔐 Secure handling of service account credentials
- 📝 Detailed deployment summaries
- 🧪 Self-testing repository structure
- Detailed deployment summaries

## Repository Structure

This repository is structured to serve two purposes:
## Usage

1. Provide the composite action implementation
2. Serve as a live example and test environment
Basic usage:

```
.
├── action.yml # The composite action definition
├── .github/workflows/ # Contains workflow using the action
│ └── deploy-functions.yml
├── functions/ # Example Firebase Functions
│ ├── main.py
│ └── pyproject.toml
└── README.md # Documentation
```yaml
- uses: digital-wisdom/deploy-firebase-python@v1
with:
project_id: my-firebase-project
service_account_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
deploy_command: "firebase deploy --only functions"
```

## Usage
## Examples

The workflow in this repository (.github/workflows/deploy-functions.yml) demonstrates the recommended usage:
### Basic Firebase deployment:
```yaml
- uses: digital-wisdom/deploy-firebase-python@v1
with:
project_id: my-firebase-project
service_account_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
```

### Deploy specific functions:
```yaml
name: Test & Deploy Python Functions

on:
push:
branches: [staging, prod]
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, prod]
description: 'Environment to deploy to'
required: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: ./ # Uses local action for testing
with:
environment: ${{ github.event_name == 'push' && github.ref_name || inputs.environment }}
service_account_json: ${{ secrets[format('FIREBASE_{0}_SERVICE_ACCOUNT', github.event_name == 'push' && github.ref_name || inputs.environment)] }}
project_id: ${{ vars[format('FIREBASE_{0}_PROJECT_ID', github.event_name == 'push' && github.ref_name || inputs.environment)] }}
- uses: digital-wisdom/deploy-firebase-python@v1
with:
project_id: my-firebase-project
service_account_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
deploy_command: "firebase deploy --only functions:api,functions:webhook"
```

When using in your own repository, reference a specific version:
### Using make for deployment:
```yaml
- uses: digital-wisdom/deploy-firebase-python@v1
with:
service_account_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
deploy_command: "make deploy-staging"
```

### Custom functions directory:
```yaml
- uses: digital-wisdom/deploy-firebase-python@v1
with:
functions_dir: 'src/functions' # Default is 'functions'
environment: staging
service_account_json: ${{ secrets.FIREBASE_STAGING_SERVICE_ACCOUNT }}
project_id: my-project-staging
service_account_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
functions_dir: "backend/functions"
project_id: my-firebase-project
```

## Inputs

| Input | Description | Required | Default |
| ---------------------- | ------------------------------------------------- | -------- | ----------- |
| `functions_dir` | Directory containing functions and pyproject.toml | No | `functions` |
| `environment` | Environment to deploy to (staging/prod) | Yes | N/A |
| `service_account_json` | Firebase service account JSON | Yes | N/A |
| `project_id` | Firebase project ID | Yes | N/A |
| Input | Description | Required | Default |
| --------------------- | ------------------------------------------------- | -------- | ------------------------------ |
| `service_account_json`| Firebase service account JSON | Yes | N/A |
| `deploy_command` | Deployment command to execute | No | `firebase deploy --only functions` |
| `project_id` | Firebase project ID (runs `firebase use` if specified) | No | N/A |
| `functions_dir` | Directory with pyproject.toml (creates requirements.txt if missing) | No | `functions` |

## Prerequisites

1. **Firebase Project Setup**

- Create Firebase projects for your environments
- Generate service account keys
- Store service account JSON in GitHub Secrets
- Store project IDs in GitHub Variables

2. **Python Project Structure**
- Valid pyproject.toml in your functions directory
- Python version specified in requires-python
- Dependencies listed in project dependencies

## Project Structure

Your project should look something like this:

```
.
├── .github
│ └── workflows
│ └── deploy.yml
├── functions
│ ├── main.py
│ ├── pyproject.toml
│ └── other_files.py
└── firebase.json
```
- Firebase service account key
- requirements.txt or pyproject.toml in functions_dir

## Environment Variables

Expand Down
Loading