Skip to content

Monorepo Cloud Run Fix: Upload the whole project for Cloud Run builds on Cloud Build, like App Hosting - #11282

Open
falahat wants to merge 1 commit into
mainfrom
bapi_run_monorepo
Open

falahat wants to merge 1 commit into
mainfrom
bapi_run_monorepo

Conversation

@falahat

@falahat falahat commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Description

Follow-up to Wei's comment on #11205. For builds on Cloud Build, firebase deploy --only run zipped only the service's rootDir. So an app in a monorepo was built without the monorepo's root lockfile and shared packages.

This makes builds on Cloud Build work like App Hosting's:

  • Zip the whole project (the folder with firebase.json), the same folder App Hosting's deploy zips. .gitignore and the service's ignore list still apply.
  • Set GOOGLE_BUILDABLE to rootDir when it's a subfolder, so the buildpacks know which folder is the app.

The GOOGLE_BUILDABLE rule comes from App Hosting's local builds. It's now one exported helper, googleBuildable in apphosting/localbuilds.ts, used by App Hosting's local builds, Cloud Run's local builds, and Cloud Run's builds on Cloud Build. App Hosting's behavior doesn't change.

Cloud Run's local builds already built from the whole project with this rule, because they reuse App Hosting's local build code.

Scenarios Tested

  • Unit tests: the Cloud Run deploy tests, plus App Hosting's existing GOOGLE_BUILDABLE tests in localbuilds.spec.ts, which now run through the helper.
  • Not yet tested against Cloud Build.

Sample Commands

# With "rootDir": "apps/web" on the service in firebase.json:
firebase deploy --only run:web

@falahat
falahat added this pull request to stack #11267 October 14, 2026 16:19

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for deploying applications located within Nx or Turborepo monorepos by building them from the monorepo's root directory. It introduces a buildDirectory utility to locate the monorepo root and configures Cloud Build with the GOOGLE_BUILDABLE environment variable. Feedback on the changes suggests marking the static MONOREPO_CONFIG_FILES array as as const to align with the repository's style guide. Additionally, the reviewer recommends normalizing path separators in the new unit tests to prevent failures on Windows environments.

Comment thread src/deploy/run/deploy.ts Outdated
const NO_IMAGE = "scratch";

/** Files that mark the root of an Nx or Turborepo monorepo, the monorepos App Hosting supports. */
const MONOREPO_CONFIG_FILES = ["nx.json", "turbo.json"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

According to the repository style guide, static configuration arrays and lookup tables should be marked as readonly or as const to prevent accidental modification and improve type safety.

Suggested change
const MONOREPO_CONFIG_FILES = ["nx.json", "turbo.json"];
const MONOREPO_CONFIG_FILES = ["nx.json", "turbo.json"] as const;
References
  1. Mark static configuration arrays and lookup tables as readonly or as const. (link)

Comment on lines +76 to +85
it("builds an app in an Nx or Turborepo monorepo from the monorepo's root", async () => {
sinon.stub(fs, "existsSync").callsFake((file) => file === "/project/turbo.json");

await deployOne({ config: web });

// The upload has the monorepo's root files, like its lockfile and shared packages.
expect(createArchiveStub).to.have.been.calledWithMatch({ backendId: "web" }, "/project");
const { environmentVariables } = submitBuildStub.firstCall.args[2].buildpackBuild;
expect(environmentVariables).to.include({ GOOGLE_BUILDABLE: "apps/web" });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

On Windows, path separators are backslashes (\\), which will cause the fs.existsSync stub comparison and the createArchiveStub assertion to fail because they use hardcoded forward-slash paths. Normalizing the paths by replacing backslashes with forward slashes makes the test cross-platform and robust.

  it("builds an app in an Nx or Turborepo monorepo from the monorepo's root", async () => {
    sinon.stub(fs, "existsSync").callsFake((file) => String(file).replace(/\\/g, "/") === "/project/turbo.json");

    await deployOne({ config: web });

    // The upload has the monorepo's root files, like its lockfile and shared packages.
    expect(createArchiveStub).to.have.been.calledWithMatch(
      { backendId: "web" },
      sinon.match((val: string) => val.replace(/\\/g, "/") === "/project")
    );
    const { environmentVariables } = submitBuildStub.firstCall.args[2].buildpackBuild;
    expect(environmentVariables).to.include({ GOOGLE_BUILDABLE: "apps/web" });
  });

Comment on lines +284 to +331
describe("buildDirectory", () => {
/** Pretends that only these files exist. */
function withFiles(...files: string[]): void {
sinon.stub(fs, "existsSync").callsFake((file) => files.includes(file as string));
}

it("builds an app from its own folder if it isn't in a monorepo", () => {
withFiles("/project/apps/web/package.json");
expect(buildDirectory("/project", "apps/web")).to.deep.equal({
dir: "/project/apps/web",
app: "",
});
expect(buildDirectory("/project")).to.deep.equal({ dir: "/project", app: "" });
});

it("builds an app in a monorepo from the monorepo's root", () => {
withFiles("/project/turbo.json");
expect(buildDirectory("/project", "apps/web")).to.deep.equal({
dir: "/project",
app: "apps/web",
});
});

it("uses the closest folder above the app with nx.json or turbo.json", () => {
withFiles("/project/turbo.json", "/project/frontend/nx.json");
expect(buildDirectory("/project", "frontend/apps/web")).to.deep.equal({
dir: "/project/frontend",
app: "apps/web",
});
});

it("builds from the app's folder if the app is the monorepo's root", () => {
withFiles("/project/apps/web/nx.json");
expect(buildDirectory("/project", "apps/web")).to.deep.equal({
dir: "/project/apps/web",
app: "",
});
});

it("doesn't look for a monorepo outside the project", () => {
withFiles("/turbo.json");
expect(buildDirectory("/project", "apps/web")).to.deep.equal({
dir: "/project/apps/web",
app: "",
});
expect(buildDirectory("/project", "../other")).to.deep.equal({ dir: "/other", app: "" });
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To ensure the buildDirectory tests run successfully on Windows, we should normalize the path separators in both the withFiles stub helper and the test assertions. This avoids failures caused by the mismatch between Windows backslashes (\\) and hardcoded forward-slash paths.

  describe("buildDirectory", () => {
    /** Pretends that only these files exist. */
    function withFiles(...files: string[]): void {
      sinon.stub(fs, "existsSync").callsFake((file) => files.includes(String(file).replace(/\\/g, "/")));
    }

    function normalizeDir(res: { dir: string; app: string }) {
      return { ...res, dir: res.dir.replace(/\\/g, "/") };
    }

    it("builds an app from its own folder if it isn't in a monorepo", () => {
      withFiles("/project/apps/web/package.json");
      expect(normalizeDir(buildDirectory("/project", "apps/web"))).to.deep.equal({
        dir: "/project/apps/web",
        app: "",
      });
      expect(normalizeDir(buildDirectory("/project"))).to.deep.equal({ dir: "/project", app: "" });
    });

    it("builds an app in a monorepo from the monorepo's root", () => {
      withFiles("/project/turbo.json");
      expect(normalizeDir(buildDirectory("/project", "apps/web"))).to.deep.equal({
        dir: "/project",
        app: "apps/web",
      });
    });

    it("uses the closest folder above the app with nx.json or turbo.json", () => {
      withFiles("/project/turbo.json", "/project/frontend/nx.json");
      expect(normalizeDir(buildDirectory("/project", "frontend/apps/web"))).to.deep.equal({
        dir: "/project/frontend",
        app: "apps/web",
      });
    });

    it("builds from the app's folder if the app is the monorepo's root", () => {
      withFiles("/project/apps/web/nx.json");
      expect(normalizeDir(buildDirectory("/project", "apps/web"))).to.deep.equal({
        dir: "/project/apps/web",
        app: "",
      });
    });

    it("doesn't look for a monorepo outside the project", () => {
      withFiles("/turbo.json");
      expect(normalizeDir(buildDirectory("/project", "apps/web"))).to.deep.equal({
        dir: "/project/apps/web",
        app: "",
      });
      expect(normalizeDir(buildDirectory("/project", "../other"))).to.deep.equal({ dir: "/other", app: "" });
    });
  });

@falahat falahat changed the title Build Nx and Turborepo apps from the monorepo root on Cloud Build Monorepo Cloud Run Fix: Upload the whole project for Cloud Run builds on Cloud Build, like App Hosting Oct 14, 2026
Base automatically changed from bapi_sdk_autoinit to main October 16, 2026 18:17

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants