Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions skills/firebase-v1-v2-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
name: firebase-v1-v2-migration
description: Use this skill when a user wants to upgrade their legacy Firebase Functions from V1 (GCF 1st Gen) to V2 (GCF 2nd Gen) safely without rewriting their internal business logic. This skill relies on the Destructuring Compatibility Shim.
---

# 🚀 Supported Migration Strategies

This skill currently supports the **In-Place Migration** strategy.

### 🚜 In-Place Migration (Standard)

The agent modifies the existing V1 code file directly to use V2 syntax and
overwrites the deployment slot.

- **Pros**: Fast, simple, clean repository history.
- **Cons**: No safety net during deployment. If the V2 deployment fails, you
must rollback using Git.

*Note: For complex or zero-downtime migrations (e.g. Side-by-Side deployment),
refer to external orchestration skills.*

# Prerequisites

Please ensure the workspace is ready for V2 before attempting a code migration:

1. **Configuration Check**: Ensure the workspace has transitioned away from
functions.config() to Parameterized Configuration or standard environment
variables.
1. **Dependencies**: The project must be using firebase-functions version that
supports V2 (>= 4.0.0).

# 🔍 Pre-Migration Checklist

Before modifying any code, the agent should run a quick scan:

1. **Scan for legacy configs**: Run a `grep` or text search for usages of
`functions.config()`.
- **Action**: If found, **stop and warn the user** that these configs will
evaluate to `undefined` in V2 unless they migrate to Parameterized
Configuration or standard `.env` variables first.

# Principles of Safe Migration

Always follow these principles to ensure zero-touch logic migration:

1. **Use Context-Aware Editing over Global Regex**: Never use naive
find-and-replace. Rely on syntax-aware editing (such as an AI agent reading
the file context and making precise edits, or tools like ts-morph/ast
parsers) to ensure context isolation.
1. **Signature Modernization with Destructuring**: Do NOT rewrite the internal
variable usages of context or params inside the function body. Instead, use
JavaScript's native object destructuring in the new V2 signature parameters.
(Note: For `https.onCall`, the context shim is not available; you should
destructure `auth` and `data` directly from the request object instead of
expecting a `.context` property).

### 🛡️ Example Transformation

#### Before (V1 Legacy)

```typescript
import * as functions from "firebase-functions";
export const processOrder = functions.pubsub.topic("orders").onPublish((message, context) => {
const orderId = message.json.id;
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```

#### After (V2 Target - Safe Migration)

```typescript
import { onMessagePublished } from "firebase-functions/v2/pubsub";
// Using direct object destructuring in the signature!
export const processOrder = onMessagePublished("orders", ({ message, context }) => {
const orderId = message.json.id; // Legacy logic remains untouched!
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```

# Verification

After making any migration edits, immediately run the following verification
steps:

1. Run `npm run build` to ensure the TypeScript compiler is happy with the types
and parameters.
1. Run `npm test` to verify no regressions occurred in existing unit tests.

> [!WARNING] **Test Signature Mismatch**: The destructuring shim changes the
> function signature from two arguments `(data, context)` to a single
> destructured object `({ message, context })`.
>
> Existing V1 unit tests that invoke the function with two parameters separately
> (e.g., `myFn(mockData, mockContext)`) **will fail** because the function
> treats `mockData` as the entire event object. You will need to update test
> calls to pass a single object.
>
> **Crucial**: The key name in the test mock must match the specific **Shimmed
> Key** for that trigger (e.g., `change` for `onDocumentWritten`, `snapshot` for
> `onDocumentCreated`, `message` for PubSub, or `object` for Storage).
>
> Example: `myFn({ change: mockChange, context: mockContext })` or
> `myFn({ message: mockMessage, context: mockContext })`. See
> [signature-mapping.md](references/signature-mapping.md) for the exact keys.

# 💸 Performance & Cost Considerations

In Firebase Functions V2, you can handle multiple requests concurrently per
instance (up to 1,000 requests, default 80 if CPU >= 1). However, enabling
concurrency requires assigning at least 1 full CPU.

- **V1 Cost Parity**: If you want to keep V1 fractional CPU pricing (and disable
concurrency), you must explicitly set `cpu: "gcf_gen1"`.
- **Modernization**: If you want to take advantage of Concurrency, you must
assign at least 1 CPU.

See [configuration-migration.md](references/configuration-migration.md) for how
to set these options.

# 🛠️ Additional V2 SDK Features

When upgrading to V2, take advantage of modern V2 SDK features where applicable:

- **Declarative Security (IAM Roles & APIs)**: Use `requiresRole("roles/...")`
and `requiresAPI("service.googleapis.com", "reason")` from
`firebase-functions/v2` so IAM role and Google Cloud API dependencies are
declared directly in code instead of instructing users to run manual `gcloud`
commands.
- **Lifecycle Hooks**: Use post-deployment lifecycle hooks (`afterFirstDeploy`,
`afterRedeploy`) from `firebase-functions/v2` for automated post-deployment
initialization tasks.

# References

- **Deep Dive into Shims**: See the architectural choices for the shim in
[destructuring-shim.md](references/destructuring-shim.md).
- **Function Name Mapping**: See the V1 vs V2 function signature mapping table
in [signature-mapping.md](references/signature-mapping.md).
151 changes: 151 additions & 0 deletions skills/firebase-v1-v2-migration/references/configuration-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Migrating Runtime Configurations (runWith)

In Firebase Functions V1, you configured runtime settings like memory, timeout,
and service accounts using `.runWith()`. In V2, `.runWith()` is removed and
replaced by a more flexible options system.

You can configure V2 functions in two ways: **Globally** (for all functions in a
file) or **Per-Function**.

______________________________________________________________________

## 🌍 1. Global Configuration (`setGlobalOptions`)

Use `setGlobalOptions` at the top of your file to set defaults for all functions
defined after it.

### V1 Legacy

```typescript
import * as functions from "firebase-functions";

export const myFn = functions
.runWith({
memory: "1GB",
timeoutSeconds: 120,
serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com",
})
.https.onRequest((req, res) => { ... });
```

### V2 Modern Equivalent

```typescript
import { setGlobalOptions } from "firebase-functions/v2";
import { onRequest } from "firebase-functions/v2/https";

// Set global defaults for this file
setGlobalOptions({
memory: "1GiB", // Note: GiB instead of GB is preferred in V2 types
timeoutSeconds: 120,
serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com",
});

export const myFn = onRequest((req, res) => { ... });
```

______________________________________________________________________

## 🎯 2. Per-Function Configuration

Pass the configuration object as the **first argument** to the V2 trigger
function.

### V1 Legacy

```typescript
export const processOrder = functions
.runWith({ memory: "2GB" })
.pubsub.topic("orders")
.onPublish((message, context) => { ... });
```

### V2 Modern Equivalent

```typescript
import { onMessagePublished } from "firebase-functions/v2/pubsub";

export const processOrder = onMessagePublished(
{
topic: "orders",
memory: "2GiB", // Options passed as the first argument!
},
({ message, context }) => { ... } // Destructuring shim pattern
);
```

> [!TIP] **Memory Unit Recommendation**: While V1 accepted non-IEC units like
> `"1GB"` or `"2GB"`, V2 options strongly prefer IEC units like `"1GiB"`,
> `"2GiB"`, etc., to align with Cloud Run types and prevent TypeScript type
> errors.

______________________________________________________________________

## ⚠️ Common Property Translations

| V1 Property | V2 Property | Notes |
| :--------------------------- | :--------------------------- | :--------------------------------------------------------------------- |
| `memory` | `memory` | Prefer IEC units (`"1GiB"`) instead of (`"1GB"`). |
| `timeoutSeconds` | `timeoutSeconds` | Same. |
| `ingressSettings` | `ingressSettings` | Same. |
| `vpcConnector` | `vpcConnector` | Same. |
| `vpcConnectorEgressSettings` | `vpcConnectorEgressSettings` | Same. |
| `serviceAccount` | `serviceAccount` | Same. |
| `secrets` | `secrets` | Pass array of secret parameter variables, e.g. `{ secrets: [myKey] }`. |
| `failurePolicy` | `retry` | Renamed to boolean `retry: true/false` in V2 Eventarc triggers. |

______________________________________________________________________

## 🔐 3. Migrating Environment Configurations (`functions.config()`)

In V1, you used `functions.config()` to access environment configuration. In V2,
this is replaced by **Parameterized Configuration**.

### Deterministic Rules for Migration

Follow these rules to ensure a deterministic and safe migration:

#### Typing

- **Numbers**: If the value is used as a number, use `defineNumber`.
- **Secrets**: If the key contains "KEY", "SECRET", "TOKEN", or "PASSWORD", use
`defineSecret()`.
- *Note*: Secrets MUST be explicitly bound to the function that uses them in
the options object using the secret parameter variable (e.g.,
`{ secrets: [myKey] }`, where `const myKey = defineSecret("MY_KEY")`).
Passing an unreferenced uppercase identifier will cause a TypeScript
reference error.
- **Lists**: Use `defineList` for comma-separated lists.
- **JSON**: Use `defineJSON` for JSON strings.
- **Buckets**: If the param is a storage bucket, set `input: 'BUCKET_PICKER'`.

#### Initialization & Scope

- **Global Initialization**: If a variable was initialized globally in V1 (e.g.,
`const client = new Client(functions.config().key)`), you must split it to
have declaration at global scope and initialization inside `onInit`:
```typescript
import { onInit } from "firebase-functions/v2";

const myKey = defineSecret("MY_KEY");
let client: Client;

onInit(() => {
client = new Client(myKey.value());
});
```

#### Advanced Interpolation & Logic

- **String Interpolation**: Use the `expr` tagged template literal from
`firebase-functions/params` (e.g., `expr`every ${period} days\`\`) instead of
standard template literals when constructing dynamic strings with parameters.
Do NOT call `.value()` inside `expr`.
- **Logic Operators**: Use expressions like
`projectID.equals('prod').thenElse(1, 0)` for logical operations instead of
ternary operators on `.value()`.

#### Built-ins

- Prefer built-in variables like `databaseURL`, `projectID`, `gcloudProject`,
`storageBucket` rather than defining new params for these values.
Loading
Loading