Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #8168 - enforce webframeworks only when needed #8169

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Added code generation of React hooks for Data Connect
- Genkit init improvements around gcloud login and flow input values.
- Enforce webframeworks enablement only on webframeworks sites (#8168)
104 changes: 104 additions & 0 deletions src/deploy/hosting/deploy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect } from "chai";
import * as sinon from "sinon";

import { setEnabled } from "../../experiments";
import { FirebaseConfig } from "../../firebaseConfig";
import * as frameworks from "../../frameworks";
import * as config from "../../hosting/config";
import { HostingOptions } from "../../hosting/options";
import { Options } from "../../options";
import { prepareFrameworksIfNeeded } from "../index";

describe("hosting prepare", () => {
let frameworksStub: sinon.SinonStubbedInstance<typeof frameworks>;
let classicSiteConfig: config.HostingResolved;
let webFrameworkSiteConfig: config.HostingResolved;
let firebaseJson: FirebaseConfig;
let options: HostingOptions & Options;

beforeEach(() => {
frameworksStub = sinon.stub(frameworks);

// We're intentionally using pointer references so that editing site
// edits the results of hostingConfig() and changes firebase.json
classicSiteConfig = {
site: "classic",
target: "classic",
public: ".",
};
webFrameworkSiteConfig = {
site: "webframework",
target: "webframework",
source: "src",
};
firebaseJson = {
hosting: [classicSiteConfig, webFrameworkSiteConfig],
};
options = {
cwd: ".",
configPath: ".",
only: "hosting",
except: "",
filteredTargets: ["HOSTING"],
force: false,
json: false,
nonInteractive: false,
interactive: true,
debug: false,
projectId: "project",
config: {
src: firebaseJson,
get: (key: string) => {
if (key === "hosting") {
return firebaseJson.hosting;
}
return null;
},
} as any,
rc: null as any,

// Forces caching behavior of hostingConfig call
normalizedHostingConfig: [classicSiteConfig, webFrameworkSiteConfig],
};
});

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

it("deploys classic site without webframeworks disabled", async () => {
setEnabled("webframeworks", false);
options.only = "hosting:classic";
await expect(prepareFrameworksIfNeeded(["hosting"], options, {})).to.not.be.rejected;
});

it("fails webframework deploy with webframeworks disabled", async () => {
setEnabled("webframeworks", false);
options.only = "hosting:webframework";
await expect(prepareFrameworksIfNeeded(["hosting"], options, {})).to.be.rejectedWith(
/Cannot deploy a web framework from source because the experiment.+webframeworks.+is not enabled/,
);
});

it("deploys webframework site with webframeworks enabled", async () => {
setEnabled("webframeworks", true);
options.only = "hosting:webframework";
await expect(prepareFrameworksIfNeeded(["hosting"], options, {})).to.not.be.rejected;
expect(frameworksStub.prepareFrameworks).to.have.been.calledOnceWith("deploy", ["hosting"]);
});

it("deploys classic site with webframeworks enabled", async () => {
setEnabled("webframeworks", true);
options.only = "hosting:classic";
await expect(prepareFrameworksIfNeeded(["hosting"], options, {})).to.not.be.rejected;
expect(frameworksStub.prepareFrameworks).to.not.have.been.called;
});

it("fails when at least one site has webframeworks enabled and the experiment is disabled", async () => {
setEnabled("webframeworks", false);
options.only = "hosting";
await expect(prepareFrameworksIfNeeded(["hosting"], options, {})).to.be.rejectedWith(
/Cannot deploy a web framework from source because the experiment.+webframeworks.+is not enabled/,
);
});
});
39 changes: 29 additions & 10 deletions src/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import * as StorageTarget from "./storage";
import * as RemoteConfigTarget from "./remoteconfig";
import * as ExtensionsTarget from "./extensions";
import * as DataConnectTarget from "./dataconnect";
import { HostingConfig } from "../firebaseConfig";
import { prepareFrameworks } from "../frameworks";
import { HostingDeploy } from "./hosting/context";
import { FrameworkContext } from "../frameworks/interfaces";
import { addPinnedFunctionsToOnlyString, hasPinnedFunctions } from "./hosting/prepare";
import { isRunningInGithubAction } from "../init/features/hosting/github";
import { TARGET_PERMISSIONS } from "../commands/deploy";
import { requirePermissions } from "../requirePermissions";
import { Options } from "../options";
import { Context } from "./hosting/context";

const TARGETS = {
hosting: HostingTarget,
Expand All @@ -46,6 +48,27 @@ const chain = async function (fns: Chain, context: any, options: any, payload: a
}
};

export const prepareFrameworksIfNeeded = async function (
targetNames: (keyof typeof TARGETS)[],
options: DeployOptions,
context: FrameworkContext,
): Promise<void> {
const config = options.config.get("hosting") as HostingConfig;
if (
Array.isArray(config)
? config.some(
(it) =>
it.source &&
(!options.only.includes("hosting:") ||
new RegExp(`\\bhosting:${it.target ?? ""}\\b`).exec(options.only)),
)
: config.source
) {
experiments.assertEnabled("webframeworks", "deploy a web framework from source");
await prepareFrameworks("deploy", targetNames, context, options);
}
};

/**
* The `deploy()` function runs through a three step deploy process for a listed
* number of deploy targets. This allows deploys to be done all together or
Expand All @@ -59,7 +82,7 @@ export const deploy = async function (
const projectId = needProjectId(options);
const payload = {};
// a shared context object for deploy targets to decorate as needed
const context: any = Object.assign({ projectId }, customContext);
const context: Context = Object.assign({ projectId }, customContext);
const predeploys: Chain = [];
const prepares: Chain = [];
const deploys: Chain = [];
Expand All @@ -68,11 +91,7 @@ export const deploy = async function (
const startTime = Date.now();

if (targetNames.includes("hosting")) {
const config = options.config.get("hosting");
if (Array.isArray(config) ? config.some((it) => it.source) : config.source) {
experiments.assertEnabled("webframeworks", "deploy a web framework from source");
await prepareFrameworks("deploy", targetNames, context, options);
}
await prepareFrameworksIfNeeded(targetNames, options, context);
}

if (targetNames.includes("hosting") && hasPinnedFunctions(options)) {
Expand Down Expand Up @@ -148,11 +167,11 @@ export const deploy = async function (
const deployedHosting = includes(targetNames, "hosting");
logger.info(bold("Project Console:"), consoleUrl(options.project ?? "_", "/overview"));
if (deployedHosting) {
each(context.hosting.deploys as HostingDeploy[], (deploy) => {
each(context.hosting?.deploys, (deploy) => {
logger.info(bold("Hosting URL:"), addSubdomain(hostingOrigin(), deploy.config.site));
});
const versionNames = context.hosting.deploys.map((deploy: any) => deploy.version);
return { hosting: versionNames.length === 1 ? versionNames[0] : versionNames };
const versionNames = context.hosting?.deploys.map((deploy: any) => deploy.version);
return { hosting: versionNames?.length === 1 ? versionNames[0] : versionNames };
} else {
return { hosting: undefined };
}
Expand Down