Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d555ac5
Add new zip deploy and local build fields to configs and Build interf…
annajowang Sep 16, 2025
50d33c9
Add new EnvironmentVariable for Builds and set them during rollouts (…
annajowang Sep 19, 2025
b0d381f
[FDC init] Handle error of template create (#9119)
fredzqm Sep 17, 2025
ecfc289
14.17.0
bkendall Sep 17, 2025
7c7e0ad
clear changelog for v14.17.0 release
bkendall Sep 17, 2025
2c4dfde
(feat) Crashlytics tools improvements (#9138)
maxl0rd Sep 17, 2025
594d19b
Tweak the connect prompt to look at gitignored files and to be less e…
schnecle Sep 19, 2025
f0e450c
[VS Code] Remove unused codes associated with gca cmds (#9117)
fredzqm Sep 19, 2025
cf9a048
[MCP] `firebase_update_environment` tool can be used to accept Gemini…
fredzqm Sep 19, 2025
8dae829
Fix never resolved grouped promise in apphosting release.spec.ts test…
annajowang Sep 23, 2025
f0e4b9b
Add localbuild.ts for apphosting (#9173)
annajowang Sep 25, 2025
6ab08e8
Create archive subdir (#9176)
annajowang Sep 25, 2025
c34bbef
Set local build configs throughout the deploy steps (#9184)
annajowang Sep 25, 2025
5fd659f
Update mcp readme (#9144)
fredzqm Sep 20, 2025
ffb8bd4
feat(mcp): Adds MCP resources and a `read_resources` tool. (#9149)
mbleigh Sep 22, 2025
76c60f0
[MCP] Update GEMINI_TOS_ERROR (#9156)
fredzqm Sep 23, 2025
b6cf828
[FDC init] Fix React template creation when launched from VS Code (#9…
fredzqm Sep 24, 2025
7330998
Expose init prompt without mcpalpha experiment (#9178)
samedson Sep 24, 2025
2a0991d
Add MCP init prompt for Firestore and AI Logic (#9177)
samedson Sep 24, 2025
90613ce
refactor(mcp): change consult from a tool to a prompt (#9172)
mbleigh Sep 24, 2025
e528c94
Combine auth_get_user and auth_list_users MCP tools (#9165)
google-labs-jules[bot] Sep 24, 2025
8cd67b5
Combine auth_disable_user and auth_set_claims into auth_update_user (…
google-labs-jules[bot] Sep 24, 2025
412b449
Support preview releases in publish script (#9170)
google-labs-jules[bot] Sep 24, 2025
d61d876
feat: Rename experimental:mcp to mcp (#9168)
google-labs-jules[bot] Sep 24, 2025
e8fff1b
Prevent the init prompt from building a mobile app backend (#9180)
samedson Sep 24, 2025
e110fe7
feat: Add GA4 tracking for Gemini CLI extension (#9124)
google-labs-jules[bot] Sep 25, 2025
5c30b3e
Fixing bug
joehan Sep 25, 2025
6513784
Update auth, firestore, and hosting instructions (#9186)
kmandrika Sep 25, 2025
57d574a
BYO bucket for v2 functions uploads with runfunctions enabled (#8980)
inlined Sep 25, 2025
7799d8d
Add short-circuiting functionality to the init prompt (#9179)
samedson Sep 25, 2025
5979e90
Integrate latest changes from AI Logic prompts (#9188)
samedson Sep 25, 2025
4c1fa5b
Merge branch 'master' into jojwang-localbuild
annajowang Sep 26, 2025
09fcfff
Fix lint and test errors from master merge (#9192)
annajowang Sep 26, 2025
a2995c3
Merge branch 'master' into jojwang-localbuild
annajowang Sep 26, 2025
b6302ec
Remove debugging code.
annajowang Sep 26, 2025
f932a96
Disable localBuild for rollouts.
annajowang Sep 26, 2025
6323d1d
lint
annajowang Sep 26, 2025
97caaf8
Remove tests for rolling out localBuilds since we are skipping those …
annajowang Sep 26, 2025
a097f00
Merge branch 'master' into jojwang-localbuild
annajowang Sep 26, 2025
d0c29e1
Merge branch 'master' into jojwang-localbuild
annajowang Sep 26, 2025
959d224
Make local builds synchronous
annajowang Oct 4, 2025
6d20871
Merge branch 'master' into jojwang-localbuild
annajowang Oct 4, 2025
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
329 changes: 259 additions & 70 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
]
},
"dependencies": {
"@apphosting/build": "^0.1.6",
"@apphosting/common": "^0.0.8",
"@electric-sql/pglite": "^0.3.3",
"@electric-sql/pglite-tools": "^0.2.8",
"@google-cloud/cloud-sql-connector": "^1.3.3",
Expand Down
6 changes: 6 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,9 @@
},
"type": "array"
},
"localBuild": {
"type": "boolean"
},
"rootDir": {
"type": "string"
}
Expand Down Expand Up @@ -1134,6 +1137,9 @@
},
"type": "array"
},
"localBuild": {
"type": "boolean"
},
"rootDir": {
"type": "string"
}
Expand Down
47 changes: 47 additions & 0 deletions src/apphosting/localbuilds.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as sinon from "sinon";
import { expect } from "chai";
import * as localBuildModule from "@apphosting/build";
import { localBuild } from "./localbuilds";

describe("localBuild", () => {
afterEach(() => {
sinon.restore();
});

it("returns the expected output", async () => {
const bundleConfig = {
version: "v1" as const,
runConfig: {
runCommand: "npm run build:prod",
},
metadata: {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
},
outputFiles: {
serverApp: {
include: ["./next/standalone"],
},
},
};
const expectedAnnotations = {
adapterPackageName: "@apphosting/angular-adapter",
adapterVersion: "14.1",
framework: "nextjs",
};
const expectedOutputFiles = ["./next/standalone"];
const expectedBuildConfig = {
runCommand: "npm run build:prod",
env: [],
};
const localApphostingBuildStub: sinon.SinonStub = sinon
.stub(localBuildModule, "localBuild")
.resolves(bundleConfig);
const { outputFiles, annotations, buildConfig } = await localBuild("./", "nextjs");
expect(annotations).to.deep.equal(expectedAnnotations);
expect(buildConfig).to.deep.equal(expectedBuildConfig);
expect(outputFiles).to.deep.equal(expectedOutputFiles);
sinon.assert.calledWith(localApphostingBuildStub, "./", "nextjs");
});
});
37 changes: 37 additions & 0 deletions src/apphosting/localbuilds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BuildConfig, Env } from "../gcp/apphosting";
import { localBuild as localAppHostingBuild } from "@apphosting/build";

/**
* Triggers a local apphosting build.
*/
export async function localBuild(
projectRoot: string,
framework: string,
): Promise<{
outputFiles: string[];
annotations: Record<string, string>;
buildConfig: BuildConfig;
}> {
const apphostingBuildOutput = await localAppHostingBuild(projectRoot, framework);

const annotations: Record<string, string> = Object.fromEntries(
Object.entries(apphostingBuildOutput.metadata).map(([key, value]) => [key, String(value)]),
);

const env: Env[] | undefined = apphostingBuildOutput.runConfig.environmentVariables?.map(
({ variable, value, availability }) => ({
variable,
value,
availability,
}),
);

return {
outputFiles: apphostingBuildOutput.outputFiles?.serverApp.include ?? [],
annotations,
buildConfig: {
runCommand: apphostingBuildOutput.runConfig.runCommand,
env: env ?? [],
},
};
}
8 changes: 8 additions & 0 deletions src/deploy/apphosting/args.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { AppHostingSingle } from "../../firebaseConfig";
import { BuildConfig } from "../../gcp/apphosting";

export interface LocalBuild {
buildConfig: BuildConfig;
buildDir: string;
annotations: Record<string, string>;
}

export interface Context {
backendConfigs: Record<string, AppHostingSingle>;
backendLocations: Record<string, string>;
backendStorageUris: Record<string, string>;
backendLocalBuilds: Record<string, LocalBuild>;
}
92 changes: 80 additions & 12 deletions src/deploy/apphosting/deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,22 @@ function initializeContext(): Context {
rootDir: "/",
ignore: [],
},
fooLocalBuild: {
backendId: "fooLocalBuild",
rootDir: "/",
ignore: [],
localBuild: true,
},
},
backendLocations: { foo: "us-central1" },
backendLocations: { foo: "us-central1", fooLocalBuild: "us-central1" },
backendStorageUris: {},
backendLocalBuilds: {
fooLocalBuild: {
buildDir: "./nextjs/standalone",
buildConfig: {},
annotations: {},
},
},
};
}

Expand All @@ -59,33 +72,49 @@ describe("apphosting", () => {
sinon.verifyAndRestore();
});

describe("deploy", () => {
describe("deploy local source", () => {
const opts = {
...BASE_OPTS,
projectId: "my-project",
only: "apphosting",
config: new Config({
apphosting: {
backendId: "foo",
rootDir: "/",
ignore: [],
},
apphosting: [
{
backendId: "foo",
rootDir: "/",
ignore: [],
},
{
backendId: "fooLocalBuild",
rootDir: "/",
ignore: [],
localBuild: true,
},
],
}),
};

it("upserts regional GCS bucket", async () => {
const context = initializeContext();
getProjectNumberStub.resolves("000000000000");
upsertBucketStub.resolves();
createArchiveStub.resolves("path/to/foo-1234.zip");
uploadObjectStub.resolves({
createArchiveStub.onFirstCall().resolves("path/to/foo-1234.zip");
createArchiveStub.onSecondCall().resolves("path/to/foo-local-build-1234.zip");
uploadObjectStub.onFirstCall().resolves({
bucket: "firebaseapphosting-sources-12345678-us-central1",
object: "foo-1234",
});
uploadObjectStub.onSecondCall().resolves({
bucket: "firebaseapphosting-build-12345678-us-central1",
object: "foo-local-build-1234",
});

createReadStreamStub.resolves();

await deploy(context, opts);

// assert backend foo calls

expect(upsertBucketStub).to.be.calledWith({
product: "apphosting",
createMessage:
Expand All @@ -104,24 +133,63 @@ describe("apphosting", () => {
},
},
});

// assert backend fooLocalBuild calls
expect(upsertBucketStub).to.be.calledWith({
product: "apphosting",
createMessage:
"Creating Cloud Storage bucket in us-central1 to store App Hosting source code uploads at firebaseapphosting-build-000000000000-us-central1...",
projectId: "my-project",
req: {
name: "firebaseapphosting-build-000000000000-us-central1",
location: "us-central1",
lifecycle: {
rule: [
{
action: { type: "Delete" },
condition: { age: 30 },
},
],
},
},
});
expect(createArchiveStub).to.be.calledWithExactly(
context.backendConfigs["fooLocalBuild"],
process.cwd(),
"./nextjs/standalone",
);
expect(uploadObjectStub).to.be.calledWithMatch(
sinon.match.any,
"firebaseapphosting-build-000000000000-us-central1",
);
});

it("correctly creates and sets storage URIs", async () => {
const context = initializeContext();
getProjectNumberStub.resolves("000000000000");
upsertBucketStub.resolves();
createArchiveStub.resolves("path/to/foo-1234.zip");
uploadObjectStub.resolves({
bucket: "firebaseapphosting-sources-12345678-us-central1",
createArchiveStub.onFirstCall().resolves("path/to/foo-1234.zip");
createArchiveStub.onSecondCall().resolves("path/to/foo-local-build-1234.zip");

uploadObjectStub.onFirstCall().resolves({
bucket: "firebaseapphosting-sources-000000000000-us-central1",
object: "foo-1234",
});

uploadObjectStub.onSecondCall().resolves({
bucket: "firebaseapphosting-build-000000000000-us-central1",
object: "foo-local-build-1234",
});
createReadStreamStub.resolves();

await deploy(context, opts);

expect(context.backendStorageUris["foo"]).to.equal(
"gs://firebaseapphosting-sources-000000000000-us-central1/foo-1234.zip",
);
expect(context.backendStorageUris["fooLocalBuild"]).to.equal(
"gs://firebaseapphosting-build-000000000000-us-central1/foo-local-build-1234.zip",
);
});
});
});
35 changes: 28 additions & 7 deletions src/deploy/apphosting/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@

// Ensure that a bucket exists in each region that a backend is or will be deployed to
await Promise.all(
Object.values(context.backendLocations).map(async (loc) => {
const bucketName = `firebaseapphosting-sources-${options.projectNumber}-${loc.toLowerCase()}`;
Object.entries(context.backendLocations).map(async ([backendId, loc]) => {
const cfg = context.backendConfigs[backendId];
if (!cfg) {
throw new FirebaseError(
`Failed to find config for backend ${backendId}. Please contact support with the contents of your firebase-debug.log to report your issue.`,
);
}

const bucketName = `firebaseapphosting-${cfg.localBuild ? "build" : "sources"}-${options.projectNumber}-${loc.toLowerCase()}`;

Check warning on line 36 in src/deploy/apphosting/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "string | undefined" of template literal expression
await gcs.upsertBucket({
product: "apphosting",
createMessage: `Creating Cloud Storage bucket in ${loc} to store App Hosting source code uploads at ${bucketName}...`,
Expand All @@ -51,10 +58,23 @@
}),
);

// Zip and upload code to GCS bucket.
await Promise.all(
Object.values(context.backendConfigs).map(async (cfg) => {
const projectSourcePath = options.projectRoot ? options.projectRoot : process.cwd();
const zippedSourcePath = await createArchive(cfg, projectSourcePath);
const rootDir = options.projectRoot ?? process.cwd();
let builtAppDir;
if (cfg.localBuild) {
builtAppDir = context.backendLocalBuilds[cfg.backendId].buildDir;
if (!builtAppDir) {
throw new FirebaseError(`No local build dir found for ${cfg.backendId}`);
}
}
const zippedSourcePath = await createArchive(cfg, rootDir, builtAppDir);
logLabeledBullet(
"apphosting",
`Zipped ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}`,
);

const backendLocation = context.backendLocations[cfg.backendId];
if (!backendLocation) {
throw new FirebaseError(
Expand All @@ -63,17 +83,18 @@
}
logLabeledBullet(
"apphosting",
`Uploading source code at ${projectSourcePath} for backend ${cfg.backendId}...`,
`Uploading ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}...`,
);
const bucketName = `firebaseapphosting-sources-${options.projectNumber}-${backendLocation.toLowerCase()}`;
const bucketName = `firebaseapphosting-${cfg.localBuild ? "build" : "sources"}-${options.projectNumber}-${backendLocation.toLowerCase()}`;

Check warning on line 88 in src/deploy/apphosting/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "string | undefined" of template literal expression

const { bucket, object } = await gcs.uploadObject(
{
file: zippedSourcePath,
stream: fs.createReadStream(zippedSourcePath),
},
bucketName,
);
logLabeledBullet("apphosting", `Source code uploaded at gs://${bucket}/${object}`);
logLabeledBullet("apphosting", `Uploaded at gs://${bucket}/${object}`);
context.backendStorageUris[cfg.backendId] =
`gs://${bucketName}/${path.basename(zippedSourcePath)}`;
}),
Expand Down
Loading
Loading