Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
80ca5a3
feat(ext:migrate): export configs into functions env and invoke kit init
inlined Aug 31, 2026
6e040eb
fix(ext:migrate): address code review feedback
inlined Aug 31, 2026
4c09b3f
Merge branch 'main' into export_migrate_config_init
inlined Aug 31, 2026
cf9170e
refactor(functions): use string arrays instead of Sets in kit install…
inlined Aug 31, 2026
1f2bf6e
feat(functions): support npm package versions and tags in kit install
inlined Aug 31, 2026
67a295c
fix(extensions): support optional source on extension instances and a…
inlined Aug 31, 2026
379b6b1
feat(functions): support seedEnv and skipReport in kit installation
inlined Aug 31, 2026
2bf2770
Merge branch 'pr-11008' into export_migrate_config_init
inlined Aug 31, 2026
643abaf
Merge branch 'pr-11008' into feat_kit_version_tag_parsing
inlined Aug 31, 2026
7b100a5
Merge branch 'pr-11008' into feat_kit_seed_env_skip_report
inlined Aug 31, 2026
86d0378
Merge branch 'pr-11008' into feat_kit_seed_env_skip_report
inlined Aug 31, 2026
1bca47e
fix(extensions): address review feedback on optional chaining and fal…
inlined Aug 31, 2026
142d623
Merge branch 'fix_ext_ensure_instance_spec' into export_migrate_confi…
inlined Aug 31, 2026
9113626
fix(functions): validate raw package specifier before parsing in reso…
inlined Aug 31, 2026
b39c1d9
fix(extensions): refactor ensureInstanceSpec with early returns and e…
inlined Aug 31, 2026
8914a68
Merge branch 'feat_kit_version_tag_parsing' into export_migrate_confi…
inlined Aug 31, 2026
4237729
Merge branch 'fix_ext_ensure_instance_spec' into export_migrate_confi…
inlined Aug 31, 2026
56cc303
Merge branch 'main' into fix_ext_ensure_instance_spec
inlined Aug 31, 2026
914df41
Merge branch 'main' into feat_kit_seed_env_skip_report
inlined Aug 31, 2026
589439d
Merge remote-tracking branch 'origin/main' into feat_kit_seed_env_ski…
inlined Aug 31, 2026
ab06b0b
Merge remote-tracking branch 'origin/feat_kit_seed_env_skip_report' i…
inlined Aug 31, 2026
2654346
Merge branch 'feat_kit_seed_env_skip_report' into export_migrate_conf…
inlined Sep 1, 2026
79e49d3
Merge branch 'main' into fix_ext_ensure_instance_spec
inlined Sep 1, 2026
649d001
feat(ext:migrate): default suggested kit instance ID to extension ins…
inlined Sep 1, 2026
d26cd67
feat(functions): remove skipReport option from kit install helpers
inlined Sep 1, 2026
51526a3
Merge branch 'feat_kit_seed_env_skip_report' into export_migrate_conf…
inlined Sep 1, 2026
4ec0a7d
feat(ext:migrate): remove skipReport option from installKitOrInstance…
inlined Sep 1, 2026
20e66d7
Merge remote-tracking branch 'origin/main' into fix_ext_ensure_instan…
inlined Sep 1, 2026
d2c1fe8
fix(ext): address PR review comments
inlined Sep 1, 2026
4744d03
Merge remote-tracking branch 'origin/fix_ext_ensure_instance_spec' in…
inlined Sep 1, 2026
7ad6c1f
Merge branch 'fix_ext_ensure_instance_spec' into export_migrate_confi…
inlined Sep 1, 2026
9b671ee
Merge branch 'main' into export_migrate_config_init
inlined Sep 1, 2026
d96d854
style: restore empty comment lines in JSDocs
inlined Sep 1, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- [Added] Support exporting Extension parameters and initializing Function Kits in `firebase ext:migrate`.
- [Added] Add -f, --force option to `firebase ext:migrate`.
- [Fixed] Fix parameter type preservation and optional system parameter handling during extension updates in `firebase ext:migrate`.
53 changes: 49 additions & 4 deletions src/commands/ext-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import * as clc from "colorette";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
import { Command } from "../command";
import { needProjectId } from "../projectUtils";
import { ensureExtensionsApiEnabled, logPrefix } from "../extensions/extensionsHelper";
import {
ensureExtensionsApiEnabled,
ensureInstanceSpec,
logPrefix,
} from "../extensions/extensionsHelper";
import { requirePermissions } from "../requirePermissions";
import { createMigrationPlan, ensureInstanceUpToDate } from "../extensions/migrate";
import { createMigrationPlan, ensureInstanceUpToDate, migrateSecrets } from "../extensions/migrate";
import { functionsEnvFromInstance } from "../extensions/export";
import { installKitOrInstance } from "../functions/kits/install";
import { validateNpmPackageName } from "../functions/kits";
import { logger } from "../logger";
import { Options } from "../options";
import { logLabeledBullet } from "../utils";
import { logLabeledBullet, logLabeledWarning } from "../utils";
import { FirebaseError } from "../error";
import { logger } from "../logger";

export interface ExtMigrateOptions extends Options {
package?: string;
Expand All @@ -26,6 +33,9 @@ export const command = new Command("ext:migrate")
.before(ensureExtensionsApiEnabled)
.before(checkMinRequiredVersion, "extMinVersion")
.action(async (options: ExtMigrateOptions) => {
if (!options.config) {
throw new FirebaseError("Not in a Firebase project directory (firebase.json not found).");
}
const projectId = needProjectId(options);
if (options.package) {
validateNpmPackageName(options.package);
Expand All @@ -45,6 +55,41 @@ export const command = new Command("ext:migrate")

plan.instance = await ensureInstanceUpToDate(projectId, plan.instance, options);

if (plan.instance.state !== "ACTIVE") {
logLabeledWarning(
logPrefix,
`Extension instance ${clc.bold(plan.instanceId)} is in state ${plan.instance.state}. Migration may not function as expected.`,
);
}

plan.instance = await ensureInstanceSpec(plan.instance);
Comment thread
inlined marked this conversation as resolved.
if (!plan.instance.config?.source?.spec) {
throw new FirebaseError(
`Could not load extension specification for ${clc.bold(plan.instanceId)}. Unable to export configuration.`,
);
}

const exportedEnvs = functionsEnvFromInstance(plan.instance);

await migrateSecrets(plan.instance, { force: options.force });

logLabeledBullet(
logPrefix,
`Installing kit ${clc.bold(plan.kitPackage)} for instance ${clc.bold(plan.instanceId)}...`,
);

await installKitOrInstance({
...options,
config: options.config,
package: plan.kitPackage,
template: "migration",
defaultInstanceId: plan.instanceId,
seedEnv: {
projectId,
envs: exportedEnvs,
},
});

logger.info("TODO: Draw the rest of the owl");
return plan;
});
107 changes: 106 additions & 1 deletion src/extensions/export.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { expect } from "chai";
import * as sinon from "sinon";

import { functionsEnvFromInstance, parameterizeProject, setSecretParamsToLatest } from "./export";
import {
functionsEnvFromInstance,
parameterizeProject,
setSecretParamsToLatest,
ejectSecretsFromInstance,
} from "./export";
import { DeploymentInstanceSpec } from "../deploy/extensions/planner";
import { ExtensionInstance, ParamType } from "./types";
import * as secretsModule from "../deploy/extensions/secrets";
import { FirebaseError } from "../error";

describe("ext:export helpers", () => {
describe("parameterizeProject", () => {
Expand Down Expand Up @@ -371,3 +379,100 @@ describe("functionsEnvFromInstance", () => {
});
});
});

describe("ejectSecretsFromInstance", () => {
let transferSecretToKitsStub: sinon.SinonStub;

beforeEach(() => {
transferSecretToKitsStub = sinon.stub(secretsModule, "transferSecretToKits");
});

afterEach(() => {
sinon.restore();
});

it("should eject secrets successfully", async () => {
const instance: ExtensionInstance = {
name: "projects/my-proj/instances/my-inst",
createTime: "",
updateTime: "",
state: "ACTIVE",
serviceAccountEmail: "",
config: {
name: "projects/my-proj/instances/my-inst/configurations/1",
createTime: "",
params: {
API_KEY: "projects/my-proj/secrets/API_KEY/versions/1",
},
systemParams: {},
source: {
name: "sources/1",
state: "ACTIVE",
packageUri: "",
hash: "",
spec: {
name: "my-ext",
version: "1.0.0",
resources: [],
params: [
{
param: "API_KEY",
label: "API Key",
type: ParamType.SECRET,
},
],
systemParams: [],
},
},
},
};

transferSecretToKitsStub.resolves();
const changed = await ejectSecretsFromInstance(instance);
expect(changed).to.deep.equal({ success: ["my-proj/API_KEY"], fail: [] });
expect(transferSecretToKitsStub).to.have.been.calledWith("my-proj", "API_KEY");
});

it("should record failed secret ejections without throwing", async () => {
const instance: ExtensionInstance = {
name: "projects/my-proj/instances/my-inst",
createTime: "",
updateTime: "",
state: "ACTIVE",
serviceAccountEmail: "",
config: {
name: "projects/my-proj/instances/my-inst/configurations/1",
createTime: "",
params: {
API_KEY: "projects/my-proj/secrets/API_KEY/versions/1",
},
systemParams: {},
source: {
name: "sources/1",
state: "ACTIVE",
packageUri: "",
hash: "",
spec: {
name: "my-ext",
version: "1.0.0",
resources: [],
params: [
{
param: "API_KEY",
label: "API Key",
type: ParamType.SECRET,
},
],
systemParams: [],
},
},
},
};

const permError = new FirebaseError("Forbidden", { status: 403 });
transferSecretToKitsStub.rejects(permError);

const changed = await ejectSecretsFromInstance(instance);
expect(changed).to.deep.equal({ success: [], fail: ["my-proj/API_KEY"] });
});
});
2 changes: 1 addition & 1 deletion src/extensions/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
logger.info(`\t${p[0]}=${p[1]}`);
}
if (spec.allowedEventTypes?.length) {
logger.info(`\tALLOWED_EVENTS=${spec.allowedEventTypes}`);

Check warning on line 91 in src/extensions/export.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Invalid type "string[]" of template literal expression
}
if (spec.eventarcChannel) {
logger.info(`\tEVENTARC_CHANNEL=${spec.eventarcChannel}`);
Expand Down Expand Up @@ -144,7 +144,7 @@
if (renamed === "EXT_MIGRATED_SYSTEM_LOCATION") {
renamed = "FUNCTION_DEFAULT_REGION";
}
envs[renamed] = specSystemParam.default ?? "";
envs[renamed] = String(specSystemParam.default ?? "");
}
}

Expand Down Expand Up @@ -198,7 +198,7 @@
/**
* Removes the Extensions label from all secrets in an ExtensionInstance and replaces them
* them with the Functions label.
* @return {success: string[], fail: string[]}, both in projectId/secretId format

Check warning on line 201 in src/extensions/export.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Syntax error in type: success: string[], fail: string[]
*/
export async function ejectSecretsFromInstance(
instance: ExtensionInstance,
Expand Down
Loading
Loading