Skip to content
21 changes: 8 additions & 13 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,16 @@ inputs:
default: "7d"
required: false
projectId:
description:
"The project to deploy to. If you leave this blank, make sure you check in
a .firebaserc file"
description: "The project to deploy to. If you leave this blank, make sure you check in a .firebaserc file"
required: false
channelId:
description: "The ID of the channel to deploy to. If you leave this blank,
a preview channel and its ID will be auto-generated per branch or PR."
description: "The ID of the channel to deploy to. If you leave this blank, a preview channel and its ID will be auto-generated per branch or PR."
required: false
target:
description:
"The target name of the Hosting site to deploy to. If you leave this blank,
the default target or all targets defined in the .firebaserc will be deployed to.
Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites)
for more information about deploy targets."
description: "The target name of the Hosting site to deploy to. If you leave this blank, the default target or all targets defined in the .firebaserc will be deployed to. Refer to the Hosting docs about [multiple sites](https://firebase.google.com/docs/hosting/multisites) for more information about deploy targets."
required: false
entryPoint:
description:
"The location of your firebase.json file, relative to the root of your
directory"
description: "The location of your firebase.json file, relative to the root of your directory"
default: "."
required: false
firebaseToolsVersion:
Expand All @@ -64,6 +55,10 @@ inputs:
Disable auto-commenting with the preview channel URL to the pull request
default: "false"
required: false
force:
description: "Pass 'true' to use the --force flag with firebase deploy. This will automatically accept any warning or prompts during deploy. Use with caution."
default: "false"
required: false
outputs:
urls:
description: The url(s) deployed to
Expand Down
64 changes: 55 additions & 9 deletions bin/action.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -92946,8 +92946,9 @@ async function execWithCredentials(args, projectId, gacFilename, opts) {
let deployOutputBuf = [];
const debug = opts.debug || false;
const firebaseToolsVersion = opts.firebaseToolsVersion || "latest";
const force = opts.force;
try {
await exec_1.exec(`npx firebase-tools@${firebaseToolsVersion}`, [...args, ...(projectId ? ["--project", projectId] : []), debug ? "--debug" // gives a more thorough error message
await exec_1.exec(`npx firebase-tools@${firebaseToolsVersion}`, [...args, ...(projectId ? ["--project", projectId] : []), ...(force ? ["--force"] : []), debug ? "--debug" // gives a more thorough error message
: "--json" // allows us to easily parse the output
], {
listeners: {
Expand All @@ -92968,25 +92969,27 @@ async function execWithCredentials(args, projectId, gacFilename, opts) {
console.log("Retrying deploy with the --debug flag for better error output");
await execWithCredentials(args, projectId, gacFilename, {
debug: true,
firebaseToolsVersion
firebaseToolsVersion,
force
});
} else {
throw e;
}
}
return deployOutputBuf.length ? deployOutputBuf[deployOutputBuf.length - 1].toString("utf-8") : ""; // output from the CLI
}

async function deployPreview(gacFilename, deployConfig) {
const {
projectId,
channelId,
target,
expires,
firebaseToolsVersion
firebaseToolsVersion,
force
} = deployConfig;
const deploymentText = await execWithCredentials(["hosting:channel:deploy", channelId, ...(target ? ["--only", target] : []), ...(expires ? ["--expires", expires] : [])], projectId, gacFilename, {
firebaseToolsVersion
firebaseToolsVersion,
force
});
const deploymentResult = JSON.parse(deploymentText.trim());
return deploymentResult;
Expand All @@ -92995,10 +92998,26 @@ async function deployProductionSite(gacFilename, productionDeployConfig) {
const {
projectId,
target,
firebaseToolsVersion
firebaseToolsVersion,
force
} = productionDeployConfig;
const deploymentText = await execWithCredentials(["deploy", "--only", `hosting${target ? ":" + target : ""}`], projectId, gacFilename, {
firebaseToolsVersion
firebaseToolsVersion,
force
});
const deploymentResult = JSON.parse(deploymentText);
return deploymentResult;
}
async function deployWithForce(gacFilename, deployConfig) {
const {
projectId,
target,
firebaseToolsVersion,
force
} = deployConfig;
const deploymentText = await execWithCredentials(["deploy", "--only", `hosting${target ? ":" + target : ""}`, "--force"], projectId, gacFilename, {
firebaseToolsVersion,
force
});
const deploymentResult = JSON.parse(deploymentText);
return deploymentResult;

Choose a reason for hiding this comment

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

critical

This compiled file appears to be out of sync with the TypeScript source files (src/index.ts and src/deploy.ts). It contains a deployWithForce function and a special if (force) block in the main run function, which are not present in the source and represent an incorrect implementation approach. The logic in src/index.ts correctly passes the force flag to the existing deploy functions, which is the right way to do it.

Please run the build process to update this file from the TypeScript source. The current state of this file will cause incorrect behavior.

Expand Down Expand Up @@ -93182,6 +93201,7 @@ const entryPoint = core.getInput("entryPoint");
const target = core.getInput("target");
const firebaseToolsVersion = core.getInput("firebaseToolsVersion");
const disableComment = core.getInput("disableComment");
const force = core.getInput("force") === "true";
async function run() {
const isPullRequest = !!github.context.payload.pull_request;
let finish = details => console.log(details);
Expand All @@ -93208,12 +93228,37 @@ async function run() {
const gacFilename = await createGacFile(googleApplicationCredentials);
console.log("Created a temporary file with Application Default Credentials.");
core.endGroup();
if (force) {
core.startGroup("Deploying with force flag");
const deployment = await deployWithForce(gacFilename, {
projectId,
target,
firebaseToolsVersion,
force
});
if (deployment.status === "error") {
throw Error(deployment.error);
}
core.endGroup();
const hostname = target ? `${target}.web.app` : `${projectId}.web.app`;
const url = `https://${hostname}/`;
await finish({
details_url: url,
conclusion: "success",
output: {
title: `Production deploy succeeded`,
summary: `[${hostname}](${url})`
}
});
return;
}
if (isProductionDeploy) {
core.startGroup("Deploying to production site");
const deployment = await deployProductionSite(gacFilename, {
projectId,
target,
firebaseToolsVersion
firebaseToolsVersion,
force
});
if (deployment.status === "error") {
throw Error(deployment.error);
Expand All @@ -93238,7 +93283,8 @@ async function run() {
expires,
channelId,
target,
firebaseToolsVersion
firebaseToolsVersion,
force
});
if (deployment.status === "error") {
throw Error(deployment.error);
Expand Down
33 changes: 27 additions & 6 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,36 @@ export type ProductionSuccessResult = {
};
};

export type ForceProductionSuccessResult = {
status: "success";
result: {
hosting: string | string[];
};
};

export type ForceChannelSuccessResult = {
status: "success";
result: { [key: string]: SiteDeploy };
};

Choose a reason for hiding this comment

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

high

The new types ForceProductionSuccessResult and ForceChannelSuccessResult are identical to the existing ProductionSuccessResult and ChannelSuccessResult types, respectively. This introduces redundancy. Please remove these new types and use the existing ones.


type DeployConfig = {
projectId: string;
target?: string;
// Optional version specification for firebase-tools. Defaults to `latest`.
firebaseToolsVersion?: string;
force?: boolean;
};

export type ChannelDeployConfig = DeployConfig & {
expires: string;
channelId: string;
force?: boolean;
};

export type ProductionDeployConfig = DeployConfig & {};

export type ProductionDeployConfig = DeployConfig & {
force?: boolean;
};

Choose a reason for hiding this comment

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

medium

The force?: boolean property is already defined in the base DeployConfig type on line 60. It's redundant to declare it again in ChannelDeployConfig and ProductionDeployConfig since they extend DeployConfig.

Suggested change
export type ChannelDeployConfig = DeployConfig & {
expires: string;
channelId: string;
force?: boolean;
};
export type ProductionDeployConfig = DeployConfig & {};
export type ProductionDeployConfig = DeployConfig & {
force?: boolean;
};
export type ChannelDeployConfig = DeployConfig & {
expires: string;
channelId: string;
};
export type ProductionDeployConfig = DeployConfig & {};


export function interpretChannelDeployResult(
deployResult: ChannelSuccessResult
Expand All @@ -74,18 +91,20 @@ async function execWithCredentials(
args: string[],
projectId,
gacFilename,
opts: { debug?: boolean; firebaseToolsVersion?: string }
opts: { debug?: boolean; firebaseToolsVersion?: string; force?: boolean }
) {
let deployOutputBuf: Buffer[] = [];
const debug = opts.debug || false;
const firebaseToolsVersion = opts.firebaseToolsVersion || "latest";
const force = opts.force;

try {
await exec(
`npx firebase-tools@${firebaseToolsVersion}`,
[
...args,
...(projectId ? ["--project", projectId] : []),
...(force ? ["--force"] : []),
debug
? "--debug" // gives a more thorough error message
: "--json", // allows us to easily parse the output
Expand Down Expand Up @@ -114,6 +133,7 @@ async function execWithCredentials(
await execWithCredentials(args, projectId, gacFilename, {
debug: true,
firebaseToolsVersion,
force,
});
} else {
throw e;
Expand All @@ -129,7 +149,7 @@ export async function deployPreview(
gacFilename: string,
deployConfig: ChannelDeployConfig
) {
const { projectId, channelId, target, expires, firebaseToolsVersion } =
const { projectId, channelId, target, expires, firebaseToolsVersion, force } =
deployConfig;

const deploymentText = await execWithCredentials(
Expand All @@ -141,7 +161,7 @@ export async function deployPreview(
],
projectId,
gacFilename,
{ firebaseToolsVersion }
{ firebaseToolsVersion, force }
);

const deploymentResult = JSON.parse(deploymentText.trim()) as
Expand All @@ -155,13 +175,14 @@ export async function deployProductionSite(
gacFilename,
productionDeployConfig: ProductionDeployConfig
) {
const { projectId, target, firebaseToolsVersion } = productionDeployConfig;
const { projectId, target, firebaseToolsVersion, force } =
productionDeployConfig;

const deploymentText = await execWithCredentials(
["deploy", "--only", `hosting${target ? ":" + target : ""}`],
projectId,
gacFilename,
{ firebaseToolsVersion }
{ firebaseToolsVersion, force }
);

const deploymentResult = JSON.parse(deploymentText) as
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const entryPoint = getInput("entryPoint");
const target = getInput("target");
const firebaseToolsVersion = getInput("firebaseToolsVersion");
const disableComment = getInput("disableComment");
const force = getInput("force") === "true";

async function run() {
const isPullRequest = !!context.payload.pull_request;
Expand Down Expand Up @@ -94,6 +95,7 @@ async function run() {
projectId,
target,
firebaseToolsVersion,
force,
});
if (deployment.status === "error") {
throw Error((deployment as ErrorResult).error);
Expand Down Expand Up @@ -122,6 +124,7 @@ async function run() {
channelId,
target,
firebaseToolsVersion,
force,
});

if (deployment.status === "error") {
Expand Down
78 changes: 78 additions & 0 deletions test/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ const baseLiveDeployConfig: ProductionDeployConfig = {
projectId: "my-project",
};

const forceProductionDeployConfig: ProductionDeployConfig = {
projectId: "my-project",
force: true,
};

const forcePreviewDeployConfig: ChannelDeployConfig = {
projectId: "my-project",
channelId: "my-channel",
expires: undefined,
force: true,
};

async function fakeExecFail(
mainCommand: string,
args: string[],
Expand Down Expand Up @@ -128,6 +140,48 @@ describe("deploy", () => {
});
});

describe("deploy to preview channel with force flag", () => {
it("calls exec and interprets the output, including the --force flag when force is true", async () => {
// @ts-ignore read-only property
exec.exec = jest.fn(fakeExec);

const deployOutput: ChannelSuccessResult = (await deployPreview(
"my-file",
forcePreviewDeployConfig
)) as ChannelSuccessResult;

expect(exec.exec).toBeCalled();
expect(deployOutput).toEqual(channelSingleSiteSuccess);

// Check the arguments that exec was called with
// @ts-ignore Jest adds a magic "mock" property
const args = exec.exec.mock.calls;
const deployFlags = args[0][1];
expect(deployFlags).toContain("hosting:channel:deploy");
expect(deployFlags).toContain("--force");
});

it("specifies a target when one is provided", async () => {
// @ts-ignore read-only property
exec.exec = jest.fn(fakeExec);

const config: ChannelDeployConfig = {
...baseChannelDeployConfig,
target: "my-second-site",
};

Choose a reason for hiding this comment

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

high

This test case is intended to check that --force is used when a target is provided. However, the config object is created from baseChannelDeployConfig, which does not set force: true. As a result, the expectation on line 181 that deployFlags contains "--force" will fail. You should add force: true to the configuration for this test.

Suggested change
const config: ChannelDeployConfig = {
...baseChannelDeployConfig,
target: "my-second-site",
};
const config: ChannelDeployConfig = {
...baseChannelDeployConfig,
target: "my-second-site",
force: true,
};


await deployPreview("my-file", config);

// Check the arguments that exec was called with
// @ts-ignore Jest adds a magic "mock" property
const args = exec.exec.mock.calls;
const deployFlags = args[0][1];
expect(deployFlags).toContain("--only");
expect(deployFlags).toContain("my-second-site");
expect(deployFlags).toContain("--force");
});
});

describe("deploy to live channel", () => {
it("calls exec and interprets the output", async () => {
// @ts-ignore read-only property
Expand All @@ -150,4 +204,28 @@ describe("deploy", () => {
expect(deployFlags).toContain("hosting");
});
});

describe("deploy to live channel with force flag", () => {
it("includes --force flag when force is true for deploy", async () => {
// @ts-ignore read-only property
exec.exec = jest.fn(fakeExec);

const forceDeployOutput: ProductionSuccessResult = (await deployProductionSite(
"my-file",
forceProductionDeployConfig
)) as ProductionSuccessResult;

expect(exec.exec).toBeCalled();
expect(forceDeployOutput).toEqual(liveDeploySingleSiteSuccess);

// Check the arguments that exec was called with
// @ts-ignore Jest adds a magic "mock" property
const args = exec.exec.mock.calls;
const deployFlags = args[0][1];
expect(deployFlags).toContain("deploy");
expect(deployFlags).toContain("--only");
expect(deployFlags).toContain("hosting");
expect(deployFlags).toContain("--force");
});
});
});
Loading
Loading