From bb25dd726c1915d3a0e5e3459652255f53bea450 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Mon, 21 Jul 2025 10:07:10 -0400 Subject: [PATCH 01/11] added in a flag for '--force' when deploying changes that may increase your bill, such as minimum instances required. --- action.yml | 20 +++++++------------- src/deploy.ts | 14 +++++++++----- src/index.ts | 16 +++++++++++----- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/action.yml b/action.yml index 52bff61b..c291116d 100644 --- a/action.yml +++ b/action.yml @@ -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: @@ -64,6 +55,9 @@ inputs: Disable auto-commenting with the preview channel URL to the pull request default: "false" required: false + force: + description: "Pass the --force option to firebase deploy, which is useful for deploying functions" + required: false outputs: urls: description: The url(s) deployed to diff --git a/src/deploy.ts b/src/deploy.ts index 76c61858..656b2deb 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -45,6 +45,7 @@ type DeployConfig = { target?: string; // Optional version specification for firebase-tools. Defaults to `latest`. firebaseToolsVersion?: string; + force?: string; }; export type ChannelDeployConfig = DeployConfig & { @@ -74,11 +75,12 @@ async function execWithCredentials( args: string[], projectId, gacFilename, - opts: { debug?: boolean; firebaseToolsVersion?: string } + opts: { debug?: boolean; firebaseToolsVersion?: string; force?: string } ) { let deployOutputBuf: Buffer[] = []; const debug = opts.debug || false; const firebaseToolsVersion = opts.firebaseToolsVersion || "latest"; + const force = opts.force; try { await exec( @@ -86,6 +88,7 @@ async function execWithCredentials( [ ...args, ...(projectId ? ["--project", projectId] : []), + ...(force ? ["--force"] : []), debug ? "--debug" // gives a more thorough error message : "--json", // allows us to easily parse the output @@ -114,6 +117,7 @@ async function execWithCredentials( await execWithCredentials(args, projectId, gacFilename, { debug: true, firebaseToolsVersion, + force, }); } else { throw e; @@ -129,7 +133,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( @@ -141,7 +145,7 @@ export async function deployPreview( ], projectId, gacFilename, - { firebaseToolsVersion } + { firebaseToolsVersion, force } ); const deploymentResult = JSON.parse(deploymentText.trim()) as @@ -155,13 +159,13 @@ 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 diff --git a/src/index.ts b/src/index.ts index fe34502b..730dc1ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,7 @@ const entryPoint = getInput("entryPoint"); const target = getInput("target"); const firebaseToolsVersion = getInput("firebaseToolsVersion"); const disableComment = getInput("disableComment"); +const force = getInput("force"); async function run() { const isPullRequest = !!context.payload.pull_request; @@ -90,11 +91,15 @@ async function run() { if (isProductionDeploy) { startGroup("Deploying to production site"); - const deployment = await deployProductionSite(gacFilename, { - projectId, - target, - firebaseToolsVersion, - }); + const deployment = await deployProductionSite( + gacFilename, + { + projectId, + target, + firebaseToolsVersion, + force, + } + ); if (deployment.status === "error") { throw Error((deployment as ErrorResult).error); } @@ -122,6 +127,7 @@ async function run() { channelId, target, firebaseToolsVersion, + force, }); if (deployment.status === "error") { From 93bfe4792a633f2b9fb2cbd84aa67c3aaca37253 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Mon, 21 Jul 2025 10:23:04 -0400 Subject: [PATCH 02/11] updated force flag to accept a boolean value. : --- action.yml | 3 ++- src/deploy.ts | 8 ++++---- src/index.ts | 17 +++++++---------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/action.yml b/action.yml index c291116d..b19312a8 100644 --- a/action.yml +++ b/action.yml @@ -56,7 +56,8 @@ inputs: default: "false" required: false force: - description: "Pass the --force option to firebase deploy, which is useful for deploying functions" + description: "Pass 'true' to use the --force flag with firebase deploy, which is useful if you need to make changes that may increase your minimum bill like deploying more than 0 minimum instances of a function." + default: "false" required: false outputs: urls: diff --git a/src/deploy.ts b/src/deploy.ts index 656b2deb..40c29037 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -45,7 +45,7 @@ type DeployConfig = { target?: string; // Optional version specification for firebase-tools. Defaults to `latest`. firebaseToolsVersion?: string; - force?: string; + force?: boolean; }; export type ChannelDeployConfig = DeployConfig & { @@ -75,7 +75,7 @@ async function execWithCredentials( args: string[], projectId, gacFilename, - opts: { debug?: boolean; firebaseToolsVersion?: string; force?: string } + opts: { debug?: boolean; firebaseToolsVersion?: string; force?: boolean } ) { let deployOutputBuf: Buffer[] = []; const debug = opts.debug || false; @@ -159,13 +159,13 @@ export async function deployProductionSite( gacFilename, productionDeployConfig: ProductionDeployConfig ) { - const { projectId, target, firebaseToolsVersion, force } = productionDeployConfig; + const { projectId, target, firebaseToolsVersion } = productionDeployConfig; const deploymentText = await execWithCredentials( ["deploy", "--only", `hosting${target ? ":" + target : ""}`], projectId, gacFilename, - { firebaseToolsVersion, force } + { firebaseToolsVersion } ); const deploymentResult = JSON.parse(deploymentText) as diff --git a/src/index.ts b/src/index.ts index 730dc1ba..1b03d77d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,7 +51,7 @@ const entryPoint = getInput("entryPoint"); const target = getInput("target"); const firebaseToolsVersion = getInput("firebaseToolsVersion"); const disableComment = getInput("disableComment"); -const force = getInput("force"); +const force = getInput("force") === "true"; async function run() { const isPullRequest = !!context.payload.pull_request; @@ -91,15 +91,12 @@ async function run() { if (isProductionDeploy) { startGroup("Deploying to production site"); - const deployment = await deployProductionSite( - gacFilename, - { - projectId, - target, - firebaseToolsVersion, - force, - } - ); + const deployment = await deployProductionSite(gacFilename, { + projectId, + target, + firebaseToolsVersion, + force, + }); if (deployment.status === "error") { throw Error((deployment as ErrorResult).error); } From 0c94d234e11df8b4e590a5a3b2badcf5d81f995b Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Mon, 21 Jul 2025 11:09:34 -0400 Subject: [PATCH 03/11] updated tests for using force flag --- bin/action.min.js | 20 +++++++++++++------- src/deploy.ts | 35 +++++++++++++++++++++++++++++++++-- src/index.ts | 27 +++++++++++++++++++++++++++ test/deploy.test.ts | 35 ++++++++++++++++++++++++++++++++++- test/samples/cliOutputs.ts | 8 ++++++++ 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/bin/action.min.js b/bin/action.min.js index 8324beb8..d9d758a4 100644 --- a/bin/action.min.js +++ b/bin/action.min.js @@ -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: { @@ -92968,7 +92969,8 @@ 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; @@ -92976,17 +92978,18 @@ async function execWithCredentials(args, projectId, gacFilename, opts) { } 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; @@ -93182,6 +93185,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); @@ -93213,7 +93217,8 @@ async function run() { const deployment = await deployProductionSite(gacFilename, { projectId, target, - firebaseToolsVersion + firebaseToolsVersion, + force }); if (deployment.status === "error") { throw Error(deployment.error); @@ -93238,7 +93243,8 @@ async function run() { expires, channelId, target, - firebaseToolsVersion + firebaseToolsVersion, + force }); if (deployment.status === "error") { throw Error(deployment.error); diff --git a/src/deploy.ts b/src/deploy.ts index 40c29037..69c8a591 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -40,6 +40,13 @@ export type ProductionSuccessResult = { }; }; +export type ForceSuccessResult = { + status: "success"; + result: { + hosting: string | string[]; + }; +}; + type DeployConfig = { projectId: string; target?: string; @@ -53,6 +60,10 @@ export type ChannelDeployConfig = DeployConfig & { channelId: string; }; +export type ForceDeployConfig = DeployConfig & { + force: boolean; +}; + export type ProductionDeployConfig = DeployConfig & {}; export function interpretChannelDeployResult( @@ -159,13 +170,13 @@ 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 @@ -174,3 +185,23 @@ export async function deployProductionSite( return deploymentResult; } + +export async function deployWithForce( + gacFilename, + deployConfig: ForceDeployConfig +) { + 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) as + | ForceSuccessResult + | ErrorResult; + + return deploymentResult; +} diff --git a/src/index.ts b/src/index.ts index 1b03d77d..d533ca94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { createGacFile } from "./createGACFile"; import { deployPreview, deployProductionSite, + deployWithForce, ErrorResult, interpretChannelDeployResult, } from "./deploy"; @@ -89,6 +90,32 @@ async function run() { ); endGroup(); + if (force) { + startGroup("Deploying with force flag"); + const deployment = await deployWithForce(gacFilename, { + projectId, + target, + firebaseToolsVersion, + force, + }); + if (deployment.status === "error") { + throw Error((deployment as ErrorResult).error); + } + 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) { startGroup("Deploying to production site"); const deployment = await deployProductionSite(gacFilename, { diff --git a/test/deploy.test.ts b/test/deploy.test.ts index c72dd153..2e7c4308 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -5,12 +5,16 @@ import { deployProductionSite, ProductionDeployConfig, ProductionSuccessResult, + ForceDeployConfig, + deployWithForce, + ForceSuccessResult, } from "../src/deploy"; import * as exec from "@actions/exec"; import { channelError, channelMultiSiteSuccess, channelSingleSiteSuccess, + forceDeploySingleSiteSuccess, liveDeployMultiSiteSuccess, liveDeploySingleSiteSuccess, } from "./samples/cliOutputs"; @@ -25,6 +29,11 @@ const baseLiveDeployConfig: ProductionDeployConfig = { projectId: "my-project", }; +const forceDeployConfig: ForceDeployConfig = { + projectId: "my-project", + force: true, +}; + async function fakeExecFail( mainCommand: string, args: string[], @@ -150,4 +159,28 @@ describe("deploy", () => { expect(deployFlags).toContain("hosting"); }); }); -}); + + describe("deploy 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: ForceSuccessResult = (await deployWithForce( + "my-file", + forceDeployConfig + )) as ForceSuccessResult; + + expect(exec.exec).toBeCalled(); + expect(forceDeployOutput).toEqual(forceDeploySingleSiteSuccess); + + // 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"); + }); + }); +}); \ No newline at end of file diff --git a/test/samples/cliOutputs.ts b/test/samples/cliOutputs.ts index 1fda8f7d..1eb95d76 100644 --- a/test/samples/cliOutputs.ts +++ b/test/samples/cliOutputs.ts @@ -1,6 +1,7 @@ import { ChannelSuccessResult, ErrorResult, + ForceSuccessResult, ProductionSuccessResult, } from "../../src/deploy"; @@ -57,3 +58,10 @@ export const liveDeployMultiSiteSuccess: ProductionSuccessResult = { ], }, }; + +export const forceDeploySingleSiteSuccess: ForceSuccessResult = { + status: "success", + result: { + hosting: "sites/jeff-test-699d3/versions/7aebddc461b66922", + }, +}; \ No newline at end of file From 6f6d7eb358b7b51be7a411e8e2d142c2414ec3b8 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Mon, 21 Jul 2025 11:20:32 -0400 Subject: [PATCH 04/11] updated formatting to match contributor guidelines --- bin/action.min.js | 44 ++++++++++++++++++++++++++++++++++++-- src/deploy.ts | 3 ++- test/deploy.test.ts | 2 +- test/samples/cliOutputs.ts | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/bin/action.min.js b/bin/action.min.js index d9d758a4..df31ea9f 100644 --- a/bin/action.min.js +++ b/bin/action.min.js @@ -92998,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; @@ -93212,6 +93228,30 @@ 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, { diff --git a/src/deploy.ts b/src/deploy.ts index 69c8a591..dde4140c 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -170,7 +170,8 @@ export async function deployProductionSite( gacFilename, productionDeployConfig: ProductionDeployConfig ) { - const { projectId, target, firebaseToolsVersion, force } = productionDeployConfig; + const { projectId, target, firebaseToolsVersion, force } = + productionDeployConfig; const deploymentText = await execWithCredentials( ["deploy", "--only", `hosting${target ? ":" + target : ""}`], diff --git a/test/deploy.test.ts b/test/deploy.test.ts index 2e7c4308..6be4f59b 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -183,4 +183,4 @@ describe("deploy", () => { expect(deployFlags).toContain("--force"); }); }); -}); \ No newline at end of file +}); diff --git a/test/samples/cliOutputs.ts b/test/samples/cliOutputs.ts index 1eb95d76..4f0d1851 100644 --- a/test/samples/cliOutputs.ts +++ b/test/samples/cliOutputs.ts @@ -64,4 +64,4 @@ export const forceDeploySingleSiteSuccess: ForceSuccessResult = { result: { hosting: "sites/jeff-test-699d3/versions/7aebddc461b66922", }, -}; \ No newline at end of file +}; From 0aeca8258baa1eaf0edc2dbfbde4eeb91cf2d2a7 Mon Sep 17 00:00:00 2001 From: Kayce Elgin <14020716+ThadCastl3@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:09:51 -0400 Subject: [PATCH 05/11] Update action.yml Co-authored-by: Bryan Kendall --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index b19312a8..9ed35186 100644 --- a/action.yml +++ b/action.yml @@ -56,7 +56,7 @@ inputs: default: "false" required: false force: - description: "Pass 'true' to use the --force flag with firebase deploy, which is useful if you need to make changes that may increase your minimum bill like deploying more than 0 minimum instances of a function." + 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: From 43f1c769edbf3331daabf35f216522a9ace3b164 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Tue, 12 Aug 2025 15:41:24 -0400 Subject: [PATCH 06/11] accepted update to action.yml for 'force' flag description, added support for channel site deployment with force flag. Updated tests to and types to account for force flags. Force flag is now optional on ProductionDeployConfig and ChannelDeployConfig. Simplified deploy calls using force flag to automatically append force based on the boolean value nad removed the force-specific branch in index.ts --- src/deploy.ts | 35 ++++++-------------- src/index.ts | 27 ---------------- test/deploy.test.ts | 65 ++++++++++++++++++++++++++++++++------ test/samples/cliOutputs.ts | 10 ++---- 4 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/deploy.ts b/src/deploy.ts index dde4140c..c62c4c40 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -40,13 +40,18 @@ export type ProductionSuccessResult = { }; }; -export type ForceSuccessResult = { +export type ForceProductionSuccessResult = { status: "success"; result: { hosting: string | string[]; }; }; +export type ForceChannelSuccessResult = { + status: "success"; + result: { [key: string]: SiteDeploy }; +}; + type DeployConfig = { projectId: string; target?: string; @@ -58,13 +63,13 @@ type DeployConfig = { export type ChannelDeployConfig = DeployConfig & { expires: string; channelId: string; + force?: boolean; }; -export type ForceDeployConfig = DeployConfig & { - force: boolean; -}; -export type ProductionDeployConfig = DeployConfig & {}; +export type ProductionDeployConfig = DeployConfig & { + force?: boolean; +}; export function interpretChannelDeployResult( deployResult: ChannelSuccessResult @@ -186,23 +191,3 @@ export async function deployProductionSite( return deploymentResult; } - -export async function deployWithForce( - gacFilename, - deployConfig: ForceDeployConfig -) { - 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) as - | ForceSuccessResult - | ErrorResult; - - return deploymentResult; -} diff --git a/src/index.ts b/src/index.ts index d533ca94..1b03d77d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,6 @@ import { createGacFile } from "./createGACFile"; import { deployPreview, deployProductionSite, - deployWithForce, ErrorResult, interpretChannelDeployResult, } from "./deploy"; @@ -90,32 +89,6 @@ async function run() { ); endGroup(); - if (force) { - startGroup("Deploying with force flag"); - const deployment = await deployWithForce(gacFilename, { - projectId, - target, - firebaseToolsVersion, - force, - }); - if (deployment.status === "error") { - throw Error((deployment as ErrorResult).error); - } - 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) { startGroup("Deploying to production site"); const deployment = await deployProductionSite(gacFilename, { diff --git a/test/deploy.test.ts b/test/deploy.test.ts index 6be4f59b..dacb4e9f 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -5,16 +5,12 @@ import { deployProductionSite, ProductionDeployConfig, ProductionSuccessResult, - ForceDeployConfig, - deployWithForce, - ForceSuccessResult, } from "../src/deploy"; import * as exec from "@actions/exec"; import { channelError, channelMultiSiteSuccess, channelSingleSiteSuccess, - forceDeploySingleSiteSuccess, liveDeployMultiSiteSuccess, liveDeploySingleSiteSuccess, } from "./samples/cliOutputs"; @@ -29,11 +25,18 @@ const baseLiveDeployConfig: ProductionDeployConfig = { projectId: "my-project", }; -const forceDeployConfig: ForceDeployConfig = { +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[], @@ -137,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", + }; + + 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 @@ -160,18 +205,18 @@ describe("deploy", () => { }); }); - describe("deploy with force flag", () => { + 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: ForceSuccessResult = (await deployWithForce( + const forceDeployOutput: ProductionSuccessResult = (await deployProductionSite( "my-file", - forceDeployConfig - )) as ForceSuccessResult; + forceProductionDeployConfig + )) as ProductionSuccessResult; expect(exec.exec).toBeCalled(); - expect(forceDeployOutput).toEqual(forceDeploySingleSiteSuccess); + expect(forceDeployOutput).toEqual(liveDeploySingleSiteSuccess); // Check the arguments that exec was called with // @ts-ignore Jest adds a magic "mock" property diff --git a/test/samples/cliOutputs.ts b/test/samples/cliOutputs.ts index 4f0d1851..fca9eeda 100644 --- a/test/samples/cliOutputs.ts +++ b/test/samples/cliOutputs.ts @@ -1,7 +1,8 @@ import { ChannelSuccessResult, ErrorResult, - ForceSuccessResult, + ForceProductionSuccessResult, + ForceChannelSuccessResult, ProductionSuccessResult, } from "../../src/deploy"; @@ -58,10 +59,3 @@ export const liveDeployMultiSiteSuccess: ProductionSuccessResult = { ], }, }; - -export const forceDeploySingleSiteSuccess: ForceSuccessResult = { - status: "success", - result: { - hosting: "sites/jeff-test-699d3/versions/7aebddc461b66922", - }, -}; From 4c9959f540608b3f6208660dc66a6c8b683dd5a0 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Wed, 13 Aug 2025 12:01:03 -0400 Subject: [PATCH 07/11] updated test to use the correct ChannelDeployConfig --- test/deploy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/deploy.test.ts b/test/deploy.test.ts index dacb4e9f..ce7a9ae8 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -166,7 +166,7 @@ describe("deploy", () => { exec.exec = jest.fn(fakeExec); const config: ChannelDeployConfig = { - ...baseChannelDeployConfig, + ...forcePreviewDeployConfig, target: "my-second-site", }; From 561f0e10fab5d85dbed86488f8136b2ba4e21fbe Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Wed, 13 Aug 2025 12:21:49 -0400 Subject: [PATCH 08/11] merged remote main branch updates and formatted, tests are passing now --- bin/action.min.js | 38 -------------------------------------- src/deploy.ts | 1 - test/deploy.test.ts | 9 +++++---- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/bin/action.min.js b/bin/action.min.js index df31ea9f..7291ada8 100644 --- a/bin/action.min.js +++ b/bin/action.min.js @@ -93008,20 +93008,6 @@ async function deployProductionSite(gacFilename, productionDeployConfig) { 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; -} /** * Copyright 2020 Google LLC @@ -93228,30 +93214,6 @@ 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, { diff --git a/src/deploy.ts b/src/deploy.ts index c62c4c40..cf609a33 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -66,7 +66,6 @@ export type ChannelDeployConfig = DeployConfig & { force?: boolean; }; - export type ProductionDeployConfig = DeployConfig & { force?: boolean; }; diff --git a/test/deploy.test.ts b/test/deploy.test.ts index ce7a9ae8..43f09d50 100644 --- a/test/deploy.test.ts +++ b/test/deploy.test.ts @@ -210,10 +210,11 @@ describe("deploy", () => { // @ts-ignore read-only property exec.exec = jest.fn(fakeExec); - const forceDeployOutput: ProductionSuccessResult = (await deployProductionSite( - "my-file", - forceProductionDeployConfig - )) as ProductionSuccessResult; + const forceDeployOutput: ProductionSuccessResult = + (await deployProductionSite( + "my-file", + forceProductionDeployConfig + )) as ProductionSuccessResult; expect(exec.exec).toBeCalled(); expect(forceDeployOutput).toEqual(liveDeploySingleSiteSuccess); From 8ec7c1ddbacf00766a5f60d48e65890c78dcd121 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Thu, 14 Aug 2025 17:07:00 -0400 Subject: [PATCH 09/11] removed unused types for tests --- src/deploy.ts | 12 ------------ test/samples/cliOutputs.ts | 2 -- 2 files changed, 14 deletions(-) diff --git a/src/deploy.ts b/src/deploy.ts index cf609a33..f7a419b7 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -40,18 +40,6 @@ export type ProductionSuccessResult = { }; }; -export type ForceProductionSuccessResult = { - status: "success"; - result: { - hosting: string | string[]; - }; -}; - -export type ForceChannelSuccessResult = { - status: "success"; - result: { [key: string]: SiteDeploy }; -}; - type DeployConfig = { projectId: string; target?: string; diff --git a/test/samples/cliOutputs.ts b/test/samples/cliOutputs.ts index fca9eeda..1fda8f7d 100644 --- a/test/samples/cliOutputs.ts +++ b/test/samples/cliOutputs.ts @@ -1,8 +1,6 @@ import { ChannelSuccessResult, ErrorResult, - ForceProductionSuccessResult, - ForceChannelSuccessResult, ProductionSuccessResult, } from "../../src/deploy"; From 2123c9bf9eaf72bd62d18966a1252af63273d526 Mon Sep 17 00:00:00 2001 From: ThadCastl3 Date: Fri, 15 Aug 2025 13:23:16 -0400 Subject: [PATCH 10/11] updated readme for force flag --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index c755c437..f014b6be 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ jobs: firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}" expires: 30d projectId: your-Firebase-project-ID + # Optionally add Force flag + # force: true ``` ### Deploy to your live channel on merge @@ -81,6 +83,8 @@ jobs: firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT }}" projectId: your-Firebase-project-ID channelId: live + # Optionally add Force flag + # force: true ``` ## Options @@ -151,6 +155,10 @@ The version of `firebase-tools` to use. If not specified, defaults to `latest`. Disable commenting in a PR with the preview URL. +### `force` _{boolean}_ + +Bypass the confirmation prompt. + ## Outputs Values emitted by this action that can be consumed by other actions later in your workflow From 91aca772247ec2fce037bc4b0f2fad918899b9da Mon Sep 17 00:00:00 2001 From: Bryan Kendall Date: Mon, 18 Aug 2025 10:05:26 -0700 Subject: [PATCH 11/11] Update README.md Co-authored-by: Jeff <3759507+jhuleatt@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f014b6be..d4decb35 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Disable commenting in a PR with the preview URL. ### `force` _{boolean}_ -Bypass the confirmation prompt. +Uses the Firebase CLI's `--force` flag to automatically accept all interactive prompts. ## Outputs