Skip to content
Draft
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
24 changes: 23 additions & 1 deletion src/firebase_studio/migrate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@

describe("extractMetadata", () => {
it("should use overrideProjectId if provided", async () => {
sandbox.stub(fs, "readFile").callsFake(async (p: any) => {

Check warning on line 26 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
const pStr = p.toString();

Check warning on line 27 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 27 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 27 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (pStr.endsWith("metadata.json")) {

Check warning on line 28 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 28 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .endsWith on an `any` value
return JSON.stringify({ projectId: "original-project" });
}
if (pStr.endsWith("blueprint.md")) {

Check warning on line 31 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 31 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .endsWith on an `any` value
return "# **App Name**: Test App";
}
return "";
Expand All @@ -39,8 +39,8 @@
});

it("should fallback to metadata.json if no override is provided", async () => {
sandbox.stub(fs, "readFile").callsFake(async (p: any) => {

Check warning on line 42 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
const pStr = p.toString();

Check warning on line 43 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (pStr.endsWith("metadata.json")) {
return JSON.stringify({ projectId: "original-project" });
}
Expand Down Expand Up @@ -201,10 +201,14 @@
sandbox.restore();
});

it("should call apphostingSecretsSetAction if .env exists and has a non-blank GEMINI_API_KEY", async () => {
it("should call apphostingSecretsSetAction if .env exists, has a non-blank GEMINI_API_KEY, and backends exist", async () => {
const secretsStub = sandbox.stub(secrets, "apphostingSecretsSetAction").resolves();
sandbox.stub(fs, "access").resolves();
sandbox.stub(fs, "readFile").resolves("GEMINI_API_KEY=test-key");
sandbox.stub(apphosting, "listBackends").resolves({
backends: [{ name: "backend" }] as any[],
unreachable: [],
});

await uploadSecrets(testRoot, "test-project");

Expand All @@ -220,10 +224,28 @@
).to.be.true;
});

it("should not call apphostingSecretsSetAction if no backends exist", async () => {
const secretsStub = sandbox.stub(secrets, "apphostingSecretsSetAction").resolves();
sandbox.stub(fs, "access").resolves();
sandbox.stub(fs, "readFile").resolves("GEMINI_API_KEY=test-key");
sandbox.stub(apphosting, "listBackends").resolves({
backends: [],
unreachable: [],
});

await uploadSecrets(testRoot, "test-project");

expect(secretsStub.called).to.be.false;
});

it("should not call apphostingSecretsSetAction if GEMINI_API_KEY is blank in .env", async () => {
const secretsStub = sandbox.stub(secrets, "apphostingSecretsSetAction").resolves();
sandbox.stub(fs, "access").resolves();
sandbox.stub(fs, "readFile").resolves("GEMINI_API_KEY= ");
sandbox.stub(apphosting, "listBackends").resolves({
backends: [{ name: "backend" }] as any[],
unreachable: [],
});

await uploadSecrets(testRoot, "test-project");

Expand Down
108 changes: 70 additions & 38 deletions src/firebase_studio/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,50 +290,69 @@ async function createFirebaseConfigs(

// firebase.json (App Hosting)
const firebaseJsonPath = path.join(rootPath, "firebase.json");
let firebaseJson: any;
try {
await fs.access(firebaseJsonPath);
logger.info("ℹ️ firebase.json already exists, skipping creation.");
const content = await fs.readFile(firebaseJsonPath, "utf8");
firebaseJson = JSON.parse(content);
} catch {
let backendId = "studio"; // Default
try {
logger.info(`⏳ Fetching App Hosting backends for project ${projectId}...`);
const backendsData = await apphosting.listBackends(projectId, "-");
const backends = backendsData.backends || [];

if (backends.length > 0) {
const studioBackend = backends.find(
(b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"),
);
if (studioBackend) {
backendId = studioBackend.name.split("/").pop()!;
} else {
backendId = backends[0].name.split("/").pop()!;
}
logger.info(`✅ Selected App Hosting backend: ${backendId}`);
// Does not exist or not JSON
}

if (firebaseJson?.apphosting) {
logger.info("ℹ️ firebase.json already contains apphosting configuration, skipping.");
return;
}

let backendId: string | undefined;
try {
logger.info(`⏳ Fetching App Hosting backends for project ${projectId}...`);
const backendsData = await apphosting.listBackends(projectId, "-");
const backends = backendsData.backends || [];

if (backends.length > 0) {
const studioBackend = backends.find(
(b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"),
);
if (studioBackend) {
backendId = studioBackend.name.split("/").pop()!;
} else {
utils.logWarning('No App Hosting backends found, using default "studio"');
backendId = backends[0].name.split("/").pop()!;
}
} catch (err: unknown) {
utils.logWarning(
`Could not fetch backends from Firebase CLI, using default "studio". ${err}`,
);
logger.info(`✅ Selected App Hosting backend: ${backendId}`);
}
} catch (err: unknown) {
logger.debug(`Could not fetch backends from Firebase CLI: ${err}`);
}

const firebaseJson = {
apphosting: {
backendId: backendId,
ignore: [
"node_modules",
".git",
".agent",
".idx",
"firebase-debug.log",
"firebase-debug.*.log",
"functions",
],
},
};
await fs.writeFile(firebaseJsonPath, JSON.stringify(firebaseJson, null, 2));
if (!backendId) {
if (firebaseJson) {
logger.info("ℹ️ No App Hosting backends found, skipping update to existing firebase.json.");
} else {
logger.info("ℹ️ No App Hosting backends found, skipping creation of firebase.json.");
}
return;
}

const updatedFirebaseJson = {
...firebaseJson,
apphosting: {
backendId: backendId,
ignore: [
"node_modules",
".git",
".agent",
".idx",
"firebase-debug.log",
"firebase-debug.*.log",
"functions",
],
},
};

await fs.writeFile(firebaseJsonPath, JSON.stringify(updatedFirebaseJson, null, 2));
if (firebaseJson) {
logger.info(`✅ Updated firebase.json with backendId: ${backendId}`);
} else {
logger.info(`✅ Created firebase.json with backendId: ${backendId}`);
}
}
Expand Down Expand Up @@ -454,6 +473,19 @@ export async function uploadSecrets(
return;
}

try {
const backendsData = await apphosting.listBackends(projectId, "-");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is duplicated now between here and createFirebaseConfig. Can we dedupe?

if (!backendsData.backends || backendsData.backends.length === 0) {
logger.debug(
`No App Hosting backends found for project ${projectId}. Skipping secret upload.`,
);
return;
}
} catch (err: unknown) {
utils.logWarning(`Could not fetch App Hosting backends for project ${projectId}: ${err}`);
return;
}

try {
const envContent = await fs.readFile(envPath, "utf8");
const parsedEnv = env.parse(envContent);
Expand Down
Loading