diff --git a/.github/workflows/deploy-functions.yml b/.github/workflows/deploy-functions.yml index 89d0e6d..0b8cd37 100644 --- a/.github/workflows/deploy-functions.yml +++ b/.github/workflows/deploy-functions.yml @@ -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 @@ -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 \ No newline at end of file + project_id: ${{ env.PROJECT_ID }} + service_account_json: ${{ env.CREDENTIALS_JSON }} + deploy_command: "firebase deploy --only functions" diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 0d1bf14..272930e 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -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: @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c2777c2 --- /dev/null +++ b/CLAUDE.md @@ -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 +``` + +### 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 \ No newline at end of file diff --git a/README.md b/README.md index f95eabc..8d5ab8a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.yml b/action.yml index d50ed09..ca2f592 100644 --- a/action.yml +++ b/action.yml @@ -7,31 +7,30 @@ branding: color: 'orange' inputs: - functions_dir: - description: 'Directory containing functions and pyproject.toml' - required: false - default: 'functions' - to_deploy: - description: 'Firebase resource to deploy (e.g. functions, hosting)' - required: false - default: 'functions' - environment: - description: 'Environment to deploy to' - required: true - type: choice - options: - - staging - - prod service_account_json: description: 'Firebase service account JSON' required: true + deploy_command: + description: 'Deploy command (e.g. "firebase deploy --only functions", "make deploy")' + required: false + default: 'firebase deploy --only functions' project_id: - description: 'Firebase project ID' - required: true + description: 'Firebase project ID (will firebase use if specified)' + required: false + functions_dir: + description: 'Directory containing functions and pyproject.toml (if requirements.txt is not present, will create with uv)' + required: false + default: 'functions' runs: using: 'composite' steps: + - name: Install build tools for make support + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y build-essential + - name: Set up Node.js for Firebase CLI uses: actions/setup-node@v3 with: @@ -47,6 +46,9 @@ runs: uses: astral-sh/setup-uv@v5 - name: Setup Python from pyproject.toml + env: + REQUIREMENTS_PATH: ${{ inputs.functions_dir }}/requirements.txt + if: ${{ !hashFiles(env.REQUIREMENTS_PATH) }} shell: bash run: | cd ${{ inputs.functions_dir }} @@ -55,38 +57,13 @@ runs: ln -s .venv venv - name: Authenticate with GCP service account key - shell: bash - run: | - echo "${{ inputs.service_account_json }}" > service-account.json - echo "GOOGLE_APPLICATION_CREDENTIALS=service-account.json" >> $GITHUB_ENV - - - name: Authenticate GitHub Actions service account - shell: bash - run: | - gcloud auth activate-service-account --key-file=service-account.json - - - name: Verify service account ADC (Debug) - shell: bash - run: | - gcloud config get-value account - gcloud auth list - - - name: Verify Enabled APIs (Debug) - shell: bash - run: | - gcloud services list --enabled --project=${{ inputs.project_id }} - - - name: List Firebase Projects (Debug Mode) - shell: bash - run: firebase projects:list --debug + uses: 'google-github-actions/auth@v2' + with: + credentials_json: '${{ inputs.service_account_json }}' - - name: Verify Service Account IAM Policy (Debug) - shell: bash - run: | - echo "Verifying IAM policy for project: ${{ inputs.project_id }}" - gcloud projects get-iam-policy ${{ inputs.project_id }} --format=json - name: Select Firebase Project + if: ${{ inputs.project_id }} shell: bash run: | firebase use ${{ inputs.project_id }} --non-interactive @@ -94,18 +71,12 @@ runs: - name: Deploy Firebase Resources shell: bash run: | - firebase deploy --only ${{ inputs.to_deploy }} --debug + ${{ inputs.deploy_command }} - name: Post-deployment summary shell: bash run: | echo "### Deployment Summary ๐Ÿš€" >> $GITHUB_STEP_SUMMARY - echo "* **Environment**: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY echo "* **Project**: ${{ inputs.project_id }}" >> $GITHUB_STEP_SUMMARY - echo "* **Resources**: ${{ inputs.to_deploy }}" >> $GITHUB_STEP_SUMMARY + echo "* **Deploy Command**: ${{ inputs.deploy_command }}" >> $GITHUB_STEP_SUMMARY echo "* **Functions Directory**: ${{ inputs.functions_dir }}" >> $GITHUB_STEP_SUMMARY - - - name: Cleanup credentials - if: always() - shell: bash - run: rm -f service-account.json