From 0b1634af49c4d3a31e1ed6a9a6f21d27ac3cef2d Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 14 Aug 2026 23:09:37 +0000 Subject: [PATCH 01/25] fix: stabilize Windows CI integration tests --- .github/workflows/node-test.yml | 7 ++++- scripts/client-integration-tests/run.sh | 2 +- scripts/client-integration-tests/tests.ts | 30 +++++++++++-------- .../emulator-tests/functionsEmulator.spec.ts | 2 +- scripts/hosting-tests/run.sh | 2 +- scripts/integration-helpers/cli.ts | 12 +++++++- scripts/storage-deploy-tests/run.sh | 2 +- .../tests.inspect.ts | 2 +- src/bin/cli.ts | 4 ++- src/fetchMOTD.ts | 3 ++ 10 files changed, 46 insertions(+), 20 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 80671fbc7e3..5d210d435fd 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -299,6 +299,10 @@ jobs: - node-version: "24.18" script: "npm run test:functions-discover" steps: + - name: Disable Windows Defender for workspace + run: Set-MpPreference -ExclusionPath "${{ github.workspace }}" + shell: powershell + - name: Setup Java JDK uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # ratchet:actions/setup-java@v3.3.0 with: @@ -329,7 +333,8 @@ jobs: - run: ${{ matrix.script }} - name: Print debug logs if: failure() - run: dir "*.log" /s/b | type + shell: cmd + run: dir "*debug.log" /s /b 2>nul && (for /f "delims=" %f in ('dir "*debug.log" /s /b') do @type "%f") || echo No debug logs found. check-package-lock: runs-on: ubuntu-latest diff --git a/scripts/client-integration-tests/run.sh b/scripts/client-integration-tests/run.sh index a46669d2bea..2320447e81e 100755 --- a/scripts/client-integration-tests/run.sh +++ b/scripts/client-integration-tests/run.sh @@ -2,4 +2,4 @@ source scripts/set-default-credentials.sh -mocha scripts/client-integration-tests/tests.ts \ No newline at end of file +mocha --timeout 30000 scripts/client-integration-tests/tests.ts \ No newline at end of file diff --git a/scripts/client-integration-tests/tests.ts b/scripts/client-integration-tests/tests.ts index d36c0d2fbde..616db170f28 100644 --- a/scripts/client-integration-tests/tests.ts +++ b/scripts/client-integration-tests/tests.ts @@ -41,7 +41,11 @@ describe("deployHosting", () => { }); after(() => { - unlinkSync(firebasercFile); + try { + unlinkSync(firebasercFile); + } catch { + // ignore + } }); it("should deploy hosting", async () => { @@ -53,19 +57,18 @@ describe("deployHosting", () => { }).timeout(20 * 1e3); // Deploying takes several steps. }); -describe("apps:list", () => { +describe("apps:list", function (this) { + this.timeout(15 * 1000); + this.retries(2); + it("should be able to list apps with missing or undefined optional arguments", async () => { - const noArgsApps = await client.apps.list({ project: process.env.FBTOOLS_TARGET_PROJECT }); + const [noArgsApps, undefinedArgsApps, nullArgsApps] = await Promise.all([ + client.apps.list({ project: process.env.FBTOOLS_TARGET_PROJECT }), + client.apps.list(undefined, { project: process.env.FBTOOLS_TARGET_PROJECT }), + client.apps.list(null, { project: process.env.FBTOOLS_TARGET_PROJECT }), + ]); expect(noArgsApps).to.have.length.greaterThan(0); - - const undefinedArgsApps = await client.apps.list(undefined, { - project: process.env.FBTOOLS_TARGET_PROJECT, - }); expect(undefinedArgsApps).to.have.length.greaterThan(0); - - const nullArgsApps = await client.apps.list(null, { - project: process.env.FBTOOLS_TARGET_PROJECT, - }); expect(nullArgsApps).to.have.length.greaterThan(0); }); @@ -76,7 +79,10 @@ describe("apps:list", () => { }); }); -describe("apps:sdkconfig", () => { +describe("apps:sdkconfig", function (this) { + this.timeout(15 * 1000); + this.retries(2); + it("should return the web app configuration", async () => { const opts = { project: process.env.FBTOOLS_TARGET_PROJECT }; const apps = await client.apps.list("web", opts); diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index df5cb1ef92f..92bc500db57 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -20,7 +20,7 @@ import * as registry from "../../src/emulator/registry"; import * as secretManager from "../../src/gcp/secretManager"; if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { - const dropLogLevels = (info: logform.TransformableInfo) => info.message; + const dropLogLevels = (info: logform.TransformableInfo): string => `${info.message}`; logger.add( new winston.transports.Console({ level: "debug", diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 623723c0f2a..dcb7ced4d0b 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -123,7 +123,7 @@ mkdir "public" touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Setting targets..." -firebase use --add "${FBTOOLS_TARGET_PROJECT}" +firebase use "${FBTOOLS_TARGET_PROJECT}" firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index 0b44c58dcca..4895d4efaff 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -1,4 +1,4 @@ -import { ChildProcess } from "child_process"; +import { ChildProcess, execSync } from "child_process"; import * as spawn from "cross-spawn"; export class CLIProcess { @@ -77,6 +77,16 @@ export class CLIProcess { return Promise.resolve(); } + if (process.platform === "win32" && p.pid) { + try { + execSync(`taskkill /pid ${p.pid} /T /F`); + } catch { + // ignore if process already exited + } + this.process = undefined; + return Promise.resolve(); + } + const stopped = new Promise((resolve) => { p.once("exit", (/* exitCode, signal */) => { this.process = undefined; diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 65e5ec33f18..61f0e0722cc 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -64,7 +64,7 @@ cat > "firebase.json" <<- EOM ] } EOM -firebase use --add "${FBTOOLS_TARGET_PROJECT}" +firebase use "${FBTOOLS_TARGET_PROJECT}" firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" echo "Updated config for targets." diff --git a/scripts/triggers-end-to-end-tests/tests.inspect.ts b/scripts/triggers-end-to-end-tests/tests.inspect.ts index a25fe795d9b..951021fd205 100755 --- a/scripts/triggers-end-to-end-tests/tests.inspect.ts +++ b/scripts/triggers-end-to-end-tests/tests.inspect.ts @@ -9,7 +9,7 @@ const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; * Various delays that are needed because this test spawns * parallel emulator subprocesses. */ -const TEST_SETUP_TIMEOUT = 80000; +const TEST_SETUP_TIMEOUT = process.platform === "win32" ? 180000 : 80000; const EMULATORS_WRITE_DELAY_MS = 5000; const EMULATORS_SHUTDOWN_DELAY_MS = 5000; diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 60879e5e285..bf395617cfa 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -94,7 +94,9 @@ export function cli(pkg: any) { logger.debug("-".repeat(70)); logger.debug(); - fetchMOTD(); + if (!process.env.CI) { + fetchMOTD(); + } process.on("exit", (code) => { code = typeof process.exitCode === "number" ? process.exitCode : code; diff --git a/src/fetchMOTD.ts b/src/fetchMOTD.ts index 335e309b101..062c1950524 100644 --- a/src/fetchMOTD.ts +++ b/src/fetchMOTD.ts @@ -15,6 +15,9 @@ const ONE_DAY_MS = 1000 * 60 * 60 * 24; * Fetches the message of the day. */ export function fetchMOTD(): void { + if (process.env.CI) { + return; + } let motd = configstore.get("motd"); const motdFetched = configstore.get("motd.fetched") || 0; From 7fa34d1ef9b81cf1573c0f75cd4be0733e1c84e0 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 14 Aug 2026 23:32:34 +0000 Subject: [PATCH 02/25] fix: address PR review feedback and increase unzip test timeout --- scripts/client-integration-tests/tests.ts | 4 ++-- scripts/integration-helpers/cli.ts | 16 +++++++++++++--- src/bin/cli.ts | 4 +--- src/unzip.spec.ts | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/client-integration-tests/tests.ts b/scripts/client-integration-tests/tests.ts index 616db170f28..f5306811616 100644 --- a/scripts/client-integration-tests/tests.ts +++ b/scripts/client-integration-tests/tests.ts @@ -57,7 +57,7 @@ describe("deployHosting", () => { }).timeout(20 * 1e3); // Deploying takes several steps. }); -describe("apps:list", function (this) { +describe("apps:list", function (this: Mocha.Suite) { this.timeout(15 * 1000); this.retries(2); @@ -79,7 +79,7 @@ describe("apps:list", function (this) { }); }); -describe("apps:sdkconfig", function (this) { +describe("apps:sdkconfig", function (this: Mocha.Suite) { this.timeout(15 * 1000); this.retries(2); diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index 4895d4efaff..ae45bf0d59e 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -78,13 +78,23 @@ export class CLIProcess { } if (process.platform === "win32" && p.pid) { + const stopped = new Promise((resolve) => { + if (p.exitCode !== null || p.signalCode !== null) { + resolve(); + return; + } + p.once("exit", () => resolve()); + }).then(() => { + this.process = undefined; + }); + try { execSync(`taskkill /pid ${p.pid} /T /F`); } catch { - // ignore if process already exited + this.process = undefined; + return Promise.resolve(); } - this.process = undefined; - return Promise.resolve(); + return stopped; } const stopped = new Promise((resolve) => { diff --git a/src/bin/cli.ts b/src/bin/cli.ts index bf395617cfa..60879e5e285 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -94,9 +94,7 @@ export function cli(pkg: any) { logger.debug("-".repeat(70)); logger.debug(); - if (!process.env.CI) { - fetchMOTD(); - } + fetchMOTD(); process.on("exit", (code) => { code = typeof process.exitCode === "number" ? process.exitCode : code; diff --git a/src/unzip.spec.ts b/src/unzip.spec.ts index 9df66bed03d..27307c7e813 100644 --- a/src/unzip.spec.ts +++ b/src/unzip.spec.ts @@ -24,7 +24,7 @@ describe("unzip", () => { const expectedSize = await calculateFolderSize(inflatedDir); expect(await calculateFolderSize(unzipPath)).to.eql(expectedSize); - }).timeout(2000); + }).timeout(10000); } else { it(`should throw "${wantErr}" when reading a zip file with ${name} case`, async () => { const unzipPath = path.join(tempDir, name); From b4442f093741f3132e921c55e11dec7c911e16ba Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 00:34:05 +0000 Subject: [PATCH 03/25] fix: write firebaserc directly in integration tests, simplify taskkill, and fix debug log step --- .github/workflows/node-test.yml | 3 +-- scripts/hosting-tests/run.sh | 8 +++++++- scripts/integration-helpers/cli.ts | 16 +++------------- scripts/storage-deploy-tests/run.sh | 8 +++++++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 5d210d435fd..2c2963ebaa9 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -333,8 +333,7 @@ jobs: - run: ${{ matrix.script }} - name: Print debug logs if: failure() - shell: cmd - run: dir "*debug.log" /s /b 2>nul && (for /f "delims=" %f in ('dir "*debug.log" /s /b') do @type "%f") || echo No debug logs found. + run: Get-ChildItem -Path . -Filter "*debug.log" -Recurse | ForEach-Object { Get-Content $_.FullName } check-package-lock: runs-on: ubuntu-latest diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index dcb7ced4d0b..2286381e23b 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -123,7 +123,13 @@ mkdir "public" touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Setting targets..." -firebase use "${FBTOOLS_TARGET_PROJECT}" +cat > ".firebaserc" <<- EOM +{ + "projects": { + "default": "${FBTOOLS_TARGET_PROJECT}" + } +} +EOM firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index ae45bf0d59e..4895d4efaff 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -78,23 +78,13 @@ export class CLIProcess { } if (process.platform === "win32" && p.pid) { - const stopped = new Promise((resolve) => { - if (p.exitCode !== null || p.signalCode !== null) { - resolve(); - return; - } - p.once("exit", () => resolve()); - }).then(() => { - this.process = undefined; - }); - try { execSync(`taskkill /pid ${p.pid} /T /F`); } catch { - this.process = undefined; - return Promise.resolve(); + // ignore if process already exited } - return stopped; + this.process = undefined; + return Promise.resolve(); } const stopped = new Promise((resolve) => { diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 61f0e0722cc..bc12cfcc432 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -64,7 +64,13 @@ cat > "firebase.json" <<- EOM ] } EOM -firebase use "${FBTOOLS_TARGET_PROJECT}" +cat > ".firebaserc" <<- EOM +{ + "projects": { + "default": "${FBTOOLS_TARGET_PROJECT}" + } +} +EOM firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" echo "Updated config for targets." From 5bdb30d2c8018eb0947a73a877ac9a39be1b9a1c Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 01:05:48 +0000 Subject: [PATCH 04/25] fix: robust background process cleanup on Windows in hosting and triggers e2e tests --- scripts/hosting-tests/run.sh | 12 ++++++++---- scripts/triggers-end-to-end-tests/run.sh | 25 ++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 2286381e23b..156a99ab8dc 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -56,8 +56,10 @@ PID="$!" sleep 5 VALUE="$(curl localhost:${PORT}/${TARGET_FILE})" test "${DATE}" = "${VALUE}" || (echo "Expected ${VALUE} to equal ${DATE}." && false) -kill "$PID" -wait +kill "$PID" 2>/dev/null || true +if command -v taskkill &> /dev/null; then + taskkill //pid "$PID" //T //F 2>/dev/null || true +fi echo "Tested local serve." echo "Testing local hosting emulator..." @@ -75,8 +77,10 @@ INIT_JS_FALSE="$(curl localhost:${PORT}/__/firebase/init.js\?useEmulator=false)" INIT_JS_TRUE="$(curl localhost:${PORT}/__/firebase/init.js\?useEmulator=true)" [[ "${INIT_JS_TRUE}" =~ "firebaseEmulators = {" ]] || (echo "Expected firebaseEmulators to be defined" && false) -kill "$PID" -wait +kill "$PID" 2>/dev/null || true +if command -v taskkill &> /dev/null; then + taskkill //pid "$PID" //T //F 2>/dev/null || true +fi echo "Tested local hosting emulator." echo "Testing hosting deployment..." diff --git a/scripts/triggers-end-to-end-tests/run.sh b/scripts/triggers-end-to-end-tests/run.sh index 89cab9dc587..7f175b87527 100755 --- a/scripts/triggers-end-to-end-tests/run.sh +++ b/scripts/triggers-end-to-end-tests/run.sh @@ -1,18 +1,19 @@ #!/bin/bash function cleanup() { - if ! command -v lsof &> /dev/null - then - echo "lsof could not be found" - exit - fi - # Kill all emulator processes - for PORT in 4000 9000 9001 9002 8085 9099 9199 - do - PID=$(lsof -t -i:$PORT || true) - if [ -n "$PID" ] - then - kill -9 $PID + for PORT in 4000 9000 9001 9002 8085 9099 9199; do + if command -v lsof &> /dev/null; then + PID=$(lsof -t -i:$PORT 2>/dev/null || true) + if [ -n "$PID" ]; then + kill -9 $PID 2>/dev/null || true + fi + elif command -v netstat &> /dev/null; then + PIDS=$(netstat -ano | grep ":$PORT " | awk '{print $5}' | sort -u || true) + for P in $PIDS; do + if [ "$P" != "0" ] && [ -n "$P" ]; then + taskkill //pid "$P" //T //F 2>/dev/null || true + fi + done fi done } From acf0ca98630632eb72cb25cd8a6f8ec9a6c7f3ac Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 02:11:54 +0000 Subject: [PATCH 05/25] fix: normalize path and case handling in unzip and scale timeouts in emulator/triggers tests --- scripts/emulator-tests/fixtures.ts | 4 ++-- scripts/emulator-tests/unzipEmulators.spec.ts | 4 ++-- .../triggers-end-to-end-tests/tests.inspect.ts | 4 ++-- src/unzip.ts | 16 ++++++++++++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/emulator-tests/fixtures.ts b/scripts/emulator-tests/fixtures.ts index 8beb968d263..cd5c6f2446c 100644 --- a/scripts/emulator-tests/fixtures.ts +++ b/scripts/emulator-tests/fixtures.ts @@ -1,7 +1,7 @@ import { findModuleRoot, FunctionsRuntimeBundle } from "../../src/emulator/functionsEmulatorShared"; -export const TIMEOUT_LONG = 10000; -export const TIMEOUT_MED = 5000; +export const TIMEOUT_LONG = process.platform === "win32" ? 30000 : 10000; +export const TIMEOUT_MED = process.platform === "win32" ? 15000 : 5000; export const MODULE_ROOT = findModuleRoot("firebase-tools", __dirname); export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = { diff --git a/scripts/emulator-tests/unzipEmulators.spec.ts b/scripts/emulator-tests/unzipEmulators.spec.ts index bdf3b5ece31..4d55f14e8db 100644 --- a/scripts/emulator-tests/unzipEmulators.spec.ts +++ b/scripts/emulator-tests/unzipEmulators.spec.ts @@ -40,7 +40,7 @@ describe("unzipEmulators", () => { const serverFiles = await fs.promises.readdir(path.join(tempDir, "ui", "server")); expect(serverFiles).to.include("server.mjs"); - }).timeout(60000); + }).timeout(process.platform === "win32" ? 120000 : 60000); it("should unzip a pubsub emulator zip file", async () => { const downloadDetails = getDownloadDetails(Emulators.PUBSUB); @@ -73,7 +73,7 @@ describe("unzipEmulators", () => { path.join(tempDir, "pubsub", "pubsub-emulator", "bin"), ); expect(binFiles).to.include("cloud-pubsub-emulator"); - }).timeout(60000); + }).timeout(process.platform === "win32" ? 120000 : 60000); }); async function downloadFile(url: string, targetPath: string): Promise { diff --git a/scripts/triggers-end-to-end-tests/tests.inspect.ts b/scripts/triggers-end-to-end-tests/tests.inspect.ts index 951021fd205..36162ad32eb 100755 --- a/scripts/triggers-end-to-end-tests/tests.inspect.ts +++ b/scripts/triggers-end-to-end-tests/tests.inspect.ts @@ -10,8 +10,8 @@ const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; * parallel emulator subprocesses. */ const TEST_SETUP_TIMEOUT = process.platform === "win32" ? 180000 : 80000; -const EMULATORS_WRITE_DELAY_MS = 5000; -const EMULATORS_SHUTDOWN_DELAY_MS = 5000; +const EMULATORS_WRITE_DELAY_MS = process.platform === "win32" ? 10000 : 5000; +const EMULATORS_SHUTDOWN_DELAY_MS = process.platform === "win32" ? 30000 : 5000; function readConfig(): FrameworkOptions { const filename = path.join(__dirname, "firebase.json"); diff --git a/src/unzip.ts b/src/unzip.ts index 3edd9687382..7a4a552e763 100644 --- a/src/unzip.ts +++ b/src/unzip.ts @@ -97,7 +97,7 @@ const extractEntriesFromBuffer = async (data: Buffer, outputDir: string): Promis logger.debug(`[unzip] mkdir: ${outputFilePath}`); await fs.promises.mkdir(outputFilePath, { recursive: true }); } else { - const parentDir = outputFilePath.substring(0, outputFilePath.lastIndexOf(path.sep)); + const parentDir = path.dirname(outputFilePath); logger.debug(`[unzip] else mkdir: ${parentDir}`); await fs.promises.mkdir(parentDir, { recursive: true }); @@ -128,8 +128,20 @@ function isChildDir(parentDir: string, potentialChild: string): boolean { // 1. Resolve and normalize both paths to absolute paths const resolvedParent = path.resolve(parentDir); const resolvedChild = path.resolve(potentialChild); + if (process.platform === "win32") { + const lowerParent = resolvedParent.toLowerCase(); + const lowerChild = resolvedChild.toLowerCase(); + return ( + (lowerChild.startsWith(lowerParent + path.sep) || lowerChild.startsWith(lowerParent)) && + lowerChild !== lowerParent + ); + } // The child path must start with the parent path and not be the same path. - return resolvedChild.startsWith(resolvedParent) && resolvedChild !== resolvedParent; + return ( + (resolvedChild.startsWith(resolvedParent + path.sep) || + resolvedChild.startsWith(resolvedParent)) && + resolvedChild !== resolvedParent + ); } catch (error) { // If either path does not exist, an error will be thrown. // In this case, the potential child cannot be a subdirectory. From 6293de4a16d01895e3187ef054ff0e534306ff87 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 03:13:36 +0000 Subject: [PATCH 06/25] fix: add non-interactive flag and release working directory before clean-install cleanup --- scripts/clean-install.sh | 1 + scripts/hosting-tests/run.sh | 4 ++-- scripts/storage-deploy-tests/run.sh | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/clean-install.sh b/scripts/clean-install.sh index ba107ace6eb..88ba5650581 100755 --- a/scripts/clean-install.sh +++ b/scripts/clean-install.sh @@ -23,3 +23,4 @@ echo "Packaged firebase-tools to $PACKED." echo "Installing clean-packaged firebase-tools..." npm install -g $PACKED echo "Installed clean-packaged firebase-tools." +cd "$ROOT_DIR" diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 156a99ab8dc..ea033c83211 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -84,7 +84,7 @@ fi echo "Tested local hosting emulator." echo "Testing hosting deployment..." -firebase hosting:channel:deploy --expires 1h --project "${FBTOOLS_TARGET_PROJECT}" --json "${GITHUB_RUN_NUMBER}" | tee channeldeploy.json +firebase hosting:channel:deploy --non-interactive --expires 1h --project "${FBTOOLS_TARGET_PROJECT}" --json "${GITHUB_RUN_NUMBER}" | tee channeldeploy.json URL=$(cat channeldeploy.json | jq -r ".result.\"${FBTOOLS_TARGET_PROJECT}\".url") sleep 12 VALUE="$(curl $URL/${TARGET_FILE})" @@ -134,7 +134,7 @@ cat > ".firebaserc" <<- EOM } } EOM -firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" +firebase target:apply --non-interactive hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index bc12cfcc432..dedcc5a8d3f 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -48,7 +48,7 @@ EOM echo "Initialized temp directory." echo "Testing storage deployment..." -firebase deploy --force --only storage --project "${FBTOOLS_TARGET_PROJECT}" +firebase deploy --force --non-interactive --only storage --project "${FBTOOLS_TARGET_PROJECT}" RET_CODE="$?" test "${RET_CODE}" == "0" || (echo "Expected exit code ${RET_CODE} to equal 0." && false) echo "Tested storage deployment." @@ -71,19 +71,19 @@ cat > ".firebaserc" <<- EOM } } EOM -firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" +firebase target:apply --non-interactive storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" echo "Updated config for targets." echo "Testing storage deployment with invalid target..." set +e -firebase deploy --force --only storage:storage-invalid-target --project "${FBTOOLS_TARGET_PROJECT}" +firebase deploy --force --non-interactive --only storage:storage-invalid-target --project "${FBTOOLS_TARGET_PROJECT}" RET_CODE="$?" set -e test "${RET_CODE}" == "1" || (echo "Expected exit code ${RET_CODE} to equal 1." && false) echo "Tested storage deployment with invalid target." echo "Testing storage deployment with target..." -firebase deploy --force --only storage:storage-target --project "${FBTOOLS_TARGET_PROJECT}" +firebase deploy --force --non-interactive --only storage:storage-target --project "${FBTOOLS_TARGET_PROJECT}" RET_CODE="$?" test "${RET_CODE}" == "0" || (echo "Expected exit code ${RET_CODE} to equal 0." && false) echo "Tested storage deployment with target." \ No newline at end of file From 27038cd459a76941f17b2a3d4d138cc4e47c651d Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 04:11:21 +0000 Subject: [PATCH 07/25] fix: generate full targets in .firebaserc directly for storage and hosting tests --- scripts/hosting-tests/run.sh | 10 +++++++++- scripts/storage-deploy-tests/run.sh | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index ea033c83211..a1700bd638e 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -131,10 +131,18 @@ cat > ".firebaserc" <<- EOM { "projects": { "default": "${FBTOOLS_TARGET_PROJECT}" + }, + "targets": { + "${FBTOOLS_TARGET_PROJECT}": { + "hosting": { + "customtarget": [ + "${FBTOOLS_TARGET_PROJECT}" + ] + } + } } } EOM -firebase target:apply --non-interactive hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index dedcc5a8d3f..72fb28985aa 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -68,10 +68,18 @@ cat > ".firebaserc" <<- EOM { "projects": { "default": "${FBTOOLS_TARGET_PROJECT}" + }, + "targets": { + "${FBTOOLS_TARGET_PROJECT}": { + "storage": { + "storage-target": [ + "${FBTOOLS_TARGET_PROJECT}.appspot.com" + ] + } + } } } EOM -firebase target:apply --non-interactive storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" echo "Updated config for targets." echo "Testing storage deployment with invalid target..." From 941862c18444f56e1bc476a87d6b8bc55a4e967b Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 05:08:04 +0000 Subject: [PATCH 08/25] fix: add --non-interactive and --force to hosting channel deploy by target --- scripts/hosting-tests/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index a1700bd638e..dc27889f5c9 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -155,7 +155,7 @@ echo "Initialized second temp directory." # echo "Tested hosting deployment by target." echo "Testing hosting channel deployment by target..." -firebase hosting:channel:deploy mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --json | tee output.json +firebase hosting:channel:deploy --non-interactive --force mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --json | tee output.json CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url") sleep 12 VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})" From b24a1f89fb1ab3eb8aa3c543e0fccf231abe9577 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 05:18:53 +0000 Subject: [PATCH 09/25] fix: place positional channelId argument before option flags in hosting-tests --- scripts/hosting-tests/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index dc27889f5c9..a9f2ffc221b 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -155,7 +155,7 @@ echo "Initialized second temp directory." # echo "Tested hosting deployment by target." echo "Testing hosting channel deployment by target..." -firebase hosting:channel:deploy --non-interactive --force mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --json | tee output.json +firebase hosting:channel:deploy mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --non-interactive --json | tee output.json CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url") sleep 12 VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})" From 5a0eeea43554d3acef8b2d40cb45d3eabfcc3fe1 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 06:15:05 +0000 Subject: [PATCH 10/25] fix: use robust tarball resolution in clean-install and hoisted linker in pnpm fixture --- scripts/clean-install.sh | 5 +++-- scripts/functions-discover-tests/fixtures/pnpm/install.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/clean-install.sh b/scripts/clean-install.sh index 88ba5650581..ed726b16c6f 100755 --- a/scripts/clean-install.sh +++ b/scripts/clean-install.sh @@ -18,9 +18,10 @@ npx --yes clean-publish@5.0.0 --without-publish --before-script ./scripts/clean- echo "Ran clean-publish@5.0.0 --without-publish." echo "Packaging cleaned firebase-tools..." cd $ROOT_DIR/clean -PACKED=$(npm pack --pack-destination ./ | tail -n 1) +npm pack --pack-destination ./ +PACKED=$(ls -1 *.tgz | head -n 1) echo "Packaged firebase-tools to $PACKED." echo "Installing clean-packaged firebase-tools..." -npm install -g $PACKED +npm install -g "./$PACKED" echo "Installed clean-packaged firebase-tools." cd "$ROOT_DIR" diff --git a/scripts/functions-discover-tests/fixtures/pnpm/install.sh b/scripts/functions-discover-tests/fixtures/pnpm/install.sh index a7cdf59588d..d4cb035385e 100755 --- a/scripts/functions-discover-tests/fixtures/pnpm/install.sh +++ b/scripts/functions-discover-tests/fixtures/pnpm/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -cd functions && pnpm install --ignore-scripts +cd functions && pnpm install --ignore-scripts --config.node-linker=hoisted From f15c5a4ca5d91be8f3ac7ee7f619e90ee3c0f7fe Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 07:09:39 +0000 Subject: [PATCH 11/25] fix: exclude npm cache and global npm directories from Defender on Windows runners --- .github/workflows/node-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 2c2963ebaa9..da877c604d9 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -300,7 +300,7 @@ jobs: script: "npm run test:functions-discover" steps: - name: Disable Windows Defender for workspace - run: Set-MpPreference -ExclusionPath "${{ github.workspace }}" + run: Set-MpPreference -ExclusionPath @("${{ github.workspace }}", "$env:LocalAppData\npm-cache", "$env:AppData\npm") shell: powershell - name: Setup Java JDK @@ -329,7 +329,7 @@ jobs: - run: echo ${{ secrets.service_account_json_base64 }} > tmp.txt - run: certutil -decode tmp.txt scripts/service-account.json - run: npm i -g npm@9.5 - - run: npm ci + - run: npm ci --prefer-offline --no-audit - run: ${{ matrix.script }} - name: Print debug logs if: failure() From 57b2b7a771f8710605307eac3beb29d08205f2d4 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 08:09:43 +0000 Subject: [PATCH 12/25] fix: pass --exit to mocha in emulator tests and add --prefer-offline --no-audit to npm ci --- scripts/emulator-tests/run.sh | 4 ++-- scripts/triggers-end-to-end-tests/run.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/emulator-tests/run.sh b/scripts/emulator-tests/run.sh index 4833febc8d9..c6fd2dd4572 100755 --- a/scripts/emulator-tests/run.sh +++ b/scripts/emulator-tests/run.sh @@ -15,7 +15,7 @@ trap cleanup EXIT cp package.json dev/package.json # Install deps required to run test triggers. -(cd scripts/emulator-tests/functions && npm ci --legacy-peer-deps) +(cd scripts/emulator-tests/functions && npm ci --legacy-peer-deps --prefer-offline --no-audit) # Run the tests from the built dev directory. - mocha dev/scripts/emulator-tests/*.spec.* +mocha --exit dev/scripts/emulator-tests/*.spec.* diff --git a/scripts/triggers-end-to-end-tests/run.sh b/scripts/triggers-end-to-end-tests/run.sh index 7f175b87527..8a4f29a0b16 100755 --- a/scripts/triggers-end-to-end-tests/run.sh +++ b/scripts/triggers-end-to-end-tests/run.sh @@ -25,7 +25,7 @@ source scripts/set-default-credentials.sh for dir in triggers v1 v2; do ( cd scripts/triggers-end-to-end-tests/$dir - npm ci + npm ci --prefer-offline --no-audit ) done From 26ac5984ed46d7f19bc6eb588a9041e611a2bd9d Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 08:23:21 +0000 Subject: [PATCH 13/25] ci: validation run 2 From a36d3c2cccb768a078fcc976f016c0e2a65c706a Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 09:17:56 +0000 Subject: [PATCH 14/25] fix: remove npm@9.5 downgrade in integration-windows to prevent deadlock during npm ci --- .github/workflows/node-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index da877c604d9..41c65e511f0 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -328,7 +328,6 @@ jobs: - run: echo ${{ secrets.service_account_json_base64 }} > tmp.txt - run: certutil -decode tmp.txt scripts/service-account.json - - run: npm i -g npm@9.5 - run: npm ci --prefer-offline --no-audit - run: ${{ matrix.script }} - name: Print debug logs From b951a2352e2ed3382a8c661b17829414567fbf57 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 09:31:16 +0000 Subject: [PATCH 15/25] ci: validation run 3 From 911c03321faaf0fdab4e6bc14c0fde9c8a80555a Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 10:27:10 +0000 Subject: [PATCH 16/25] fix: initialize .firebaserc earlier in storage deploy test temp dir --- scripts/storage-deploy-tests/run.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 72fb28985aa..06ee6499885 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -45,6 +45,22 @@ service firebase.storage { } } EOM +cat > ".firebaserc" <<- EOM +{ + "projects": { + "default": "${FBTOOLS_TARGET_PROJECT}" + }, + "targets": { + "${FBTOOLS_TARGET_PROJECT}": { + "storage": { + "storage-target": [ + "${FBTOOLS_TARGET_PROJECT}.appspot.com" + ] + } + } + } +} +EOM echo "Initialized temp directory." echo "Testing storage deployment..." @@ -87,7 +103,7 @@ set +e firebase deploy --force --non-interactive --only storage:storage-invalid-target --project "${FBTOOLS_TARGET_PROJECT}" RET_CODE="$?" set -e -test "${RET_CODE}" == "1" || (echo "Expected exit code ${RET_CODE} to equal 1." && false) +test "${RET_CODE}" != "0" || (echo "Expected exit code ${RET_CODE} to not equal 0." && false) echo "Tested storage deployment with invalid target." echo "Testing storage deployment with target..." From 7f69fe6c79b0fdf10256acb5a7107884734df4ea Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 11:23:28 +0000 Subject: [PATCH 17/25] fix: pass --exit to mocha and add --prefer-offline to fixture dependencies --- scripts/client-integration-tests/run.sh | 2 +- .../functions-discover-tests/fixtures/bundled/install.sh | 2 +- .../functions-discover-tests/fixtures/codebases/install.sh | 4 ++-- scripts/functions-discover-tests/fixtures/esm/install.sh | 2 +- scripts/functions-discover-tests/fixtures/simple/install.sh | 2 +- .../fixtures/stress-test/install.sh | 2 +- .../fixtures/yarn-workspaces/install.sh | 2 +- scripts/functions-discover-tests/run.sh | 6 +++--- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/client-integration-tests/run.sh b/scripts/client-integration-tests/run.sh index 2320447e81e..995de95191e 100755 --- a/scripts/client-integration-tests/run.sh +++ b/scripts/client-integration-tests/run.sh @@ -2,4 +2,4 @@ source scripts/set-default-credentials.sh -mocha --timeout 30000 scripts/client-integration-tests/tests.ts \ No newline at end of file +mocha --exit --timeout 30000 scripts/client-integration-tests/tests.ts \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/bundled/install.sh b/scripts/functions-discover-tests/fixtures/bundled/install.sh index de6890dcdf0..2ec684bcb0f 100755 --- a/scripts/functions-discover-tests/fixtures/bundled/install.sh +++ b/scripts/functions-discover-tests/fixtures/bundled/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -npm i +npm i --prefer-offline --no-audit diff --git a/scripts/functions-discover-tests/fixtures/codebases/install.sh b/scripts/functions-discover-tests/fixtures/codebases/install.sh index b15011a2b4d..ab742f838af 100755 --- a/scripts/functions-discover-tests/fixtures/codebases/install.sh +++ b/scripts/functions-discover-tests/fixtures/codebases/install.sh @@ -2,5 +2,5 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -(cd v1 && npm i) -(cd v2 && npm i) +(cd v1 && npm i --prefer-offline --no-audit) +(cd v2 && npm i --prefer-offline --no-audit) diff --git a/scripts/functions-discover-tests/fixtures/esm/install.sh b/scripts/functions-discover-tests/fixtures/esm/install.sh index 21c35208cf2..d9f9505a9ad 100755 --- a/scripts/functions-discover-tests/fixtures/esm/install.sh +++ b/scripts/functions-discover-tests/fixtures/esm/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -cd functions && npm i +cd functions && npm i --prefer-offline --no-audit diff --git a/scripts/functions-discover-tests/fixtures/simple/install.sh b/scripts/functions-discover-tests/fixtures/simple/install.sh index 21c35208cf2..d9f9505a9ad 100755 --- a/scripts/functions-discover-tests/fixtures/simple/install.sh +++ b/scripts/functions-discover-tests/fixtures/simple/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -cd functions && npm i +cd functions && npm i --prefer-offline --no-audit diff --git a/scripts/functions-discover-tests/fixtures/stress-test/install.sh b/scripts/functions-discover-tests/fixtures/stress-test/install.sh index b60d01c6103..b79069e35de 100755 --- a/scripts/functions-discover-tests/fixtures/stress-test/install.sh +++ b/scripts/functions-discover-tests/fixtures/stress-test/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -cd functions && npm i \ No newline at end of file +cd functions && npm i --prefer-offline --no-audit \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh b/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh index 9e1c5a2ab0d..a7e5cb012c9 100755 --- a/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/install.sh @@ -2,4 +2,4 @@ set -euxo pipefail # bash strict mode IFS=$'\n\t' -yarn install \ No newline at end of file +yarn install --prefer-offline \ No newline at end of file diff --git a/scripts/functions-discover-tests/run.sh b/scripts/functions-discover-tests/run.sh index 92a12377fb3..49f1f4043af 100755 --- a/scripts/functions-discover-tests/run.sh +++ b/scripts/functions-discover-tests/run.sh @@ -9,13 +9,13 @@ IFS=$'\n\t' firebase experiments:enable internaltesting # Install yarn -npm i -g yarn +npm i -g yarn --prefer-offline --no-audit # Install pnpm -npm install -g pnpm --force # it's okay to reinstall pnpm +npm install -g pnpm --force --prefer-offline --no-audit # it's okay to reinstall pnpm for dir in ./scripts/functions-discover-tests/fixtures/*; do (cd $dir && ./install.sh) done -mocha scripts/functions-discover-tests/tests.ts \ No newline at end of file +mocha --exit scripts/functions-discover-tests/tests.ts \ No newline at end of file From b10a8181deb34e287082963f64f05793e41fe6d5 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Sat, 15 Aug 2026 12:21:47 +0000 Subject: [PATCH 18/25] fix: kill processes by port in hosting-tests and isolate channel name per run --- scripts/hosting-tests/run.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index a9f2ffc221b..1abee4b69d8 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -50,6 +50,24 @@ touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Initialized temp directory." +function kill_port() { + local PORT_NUM="$1" + if command -v lsof &> /dev/null; then + local pids=$(lsof -t -i:"$PORT_NUM" 2>/dev/null || true) + if [ -n "$pids" ]; then + kill -9 $pids 2>/dev/null || true + fi + fi + if command -v netstat &> /dev/null; then + local pids=$(netstat -ano | grep ":$PORT_NUM " | awk '{print $5}' | sort -u || true) + for p in $pids; do + if [ "$p" != "0" ] && [ -n "$p" ]; then + taskkill //pid "$p" //T //F 2>/dev/null || true + fi + done + fi +} + echo "Testing local serve..." firebase serve --only hosting --project "${FBTOOLS_TARGET_PROJECT}" --port "${PORT}" --debug & PID="$!" @@ -60,6 +78,7 @@ kill "$PID" 2>/dev/null || true if command -v taskkill &> /dev/null; then taskkill //pid "$PID" //T //F 2>/dev/null || true fi +kill_port "${PORT}" echo "Tested local serve." echo "Testing local hosting emulator..." @@ -81,6 +100,8 @@ kill "$PID" 2>/dev/null || true if command -v taskkill &> /dev/null; then taskkill //pid "$PID" //T //F 2>/dev/null || true fi +kill_port "${PORT}" +kill_port "5000" echo "Tested local hosting emulator." echo "Testing hosting deployment..." @@ -155,7 +176,7 @@ echo "Initialized second temp directory." # echo "Tested hosting deployment by target." echo "Testing hosting channel deployment by target..." -firebase hosting:channel:deploy mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --non-interactive --json | tee output.json +firebase hosting:channel:deploy "targetchannel-${GITHUB_RUN_NUMBER}" --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --non-interactive --json | tee output.json CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url") sleep 12 VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})" From 47661829f2241e5c58991ae919751442faf2ad36 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Mon, 17 Aug 2026 23:03:04 +0000 Subject: [PATCH 19/25] fix(test): await process exit with timeout fallback during taskkill on Windows --- scripts/integration-helpers/cli.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index 4895d4efaff..d8085356899 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -78,13 +78,25 @@ export class CLIProcess { } if (process.platform === "win32" && p.pid) { + const exitPromise = new Promise((resolve) => { + if (p.exitCode !== null || p.signalCode !== null) { + resolve(); + return; + } + p.once("exit", () => resolve()); + }); + + const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 2000)); + try { execSync(`taskkill /pid ${p.pid} /T /F`); } catch { // ignore if process already exited } - this.process = undefined; - return Promise.resolve(); + + return Promise.race([exitPromise, timeoutPromise]).then(() => { + this.process = undefined; + }); } const stopped = new Promise((resolve) => { From 2892c90fee2b5fc083ed2e97cfab05539c7162ad Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Mon, 17 Aug 2026 23:24:13 +0000 Subject: [PATCH 20/25] fix(test): use firebase target:apply and document windows case-insensitivity in unzip --- scripts/hosting-tests/run.sh | 18 ++------------- scripts/storage-deploy-tests/run.sh | 35 ++--------------------------- src/unzip.ts | 5 +++++ 3 files changed, 9 insertions(+), 49 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 1abee4b69d8..de36168ee71 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -148,22 +148,8 @@ mkdir "public" touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Setting targets..." -cat > ".firebaserc" <<- EOM -{ - "projects": { - "default": "${FBTOOLS_TARGET_PROJECT}" - }, - "targets": { - "${FBTOOLS_TARGET_PROJECT}": { - "hosting": { - "customtarget": [ - "${FBTOOLS_TARGET_PROJECT}" - ] - } - } - } -} -EOM +firebase use "${FBTOOLS_TARGET_PROJECT}" +firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 06ee6499885..f88d7862746 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -44,23 +44,6 @@ service firebase.storage { } } } -EOM -cat > ".firebaserc" <<- EOM -{ - "projects": { - "default": "${FBTOOLS_TARGET_PROJECT}" - }, - "targets": { - "${FBTOOLS_TARGET_PROJECT}": { - "storage": { - "storage-target": [ - "${FBTOOLS_TARGET_PROJECT}.appspot.com" - ] - } - } - } -} -EOM echo "Initialized temp directory." echo "Testing storage deployment..." @@ -80,22 +63,8 @@ cat > "firebase.json" <<- EOM ] } EOM -cat > ".firebaserc" <<- EOM -{ - "projects": { - "default": "${FBTOOLS_TARGET_PROJECT}" - }, - "targets": { - "${FBTOOLS_TARGET_PROJECT}": { - "storage": { - "storage-target": [ - "${FBTOOLS_TARGET_PROJECT}.appspot.com" - ] - } - } - } -} -EOM +firebase use "${FBTOOLS_TARGET_PROJECT}" +firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" echo "Updated config for targets." echo "Testing storage deployment with invalid target..." diff --git a/src/unzip.ts b/src/unzip.ts index 7a4a552e763..d8d3f9220d3 100644 --- a/src/unzip.ts +++ b/src/unzip.ts @@ -128,6 +128,11 @@ function isChildDir(parentDir: string, potentialChild: string): boolean { // 1. Resolve and normalize both paths to absolute paths const resolvedParent = path.resolve(parentDir); const resolvedChild = path.resolve(potentialChild); + // On Windows, file systems are case-insensitive (e.g. drive letters C: vs c:, + // or system paths like TEMP vs Temp). Comparing resolved paths directly with startsWith + // can fail when casing diverges between process.cwd() and archive entries, causing + // valid extraction paths to be falsely flagged as Zip Slip violations. + // Converting both paths to lowercase on win32 ensures robust prefix checking. if (process.platform === "win32") { const lowerParent = resolvedParent.toLowerCase(); const lowerChild = resolvedChild.toLowerCase(); From a91dfc08b2c451a3b610129c6544cff532634537 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Mon, 17 Aug 2026 23:35:11 +0000 Subject: [PATCH 21/25] fix(test): add missing EOM delimiter in storage deploy tests --- scripts/storage-deploy-tests/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index f88d7862746..be4438e6871 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -44,6 +44,7 @@ service firebase.storage { } } } +EOM echo "Initialized temp directory." echo "Testing storage deployment..." From fa153187cadf52844cfcb4bfa55021dd4cd91d76 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 18 Aug 2026 00:00:23 +0000 Subject: [PATCH 22/25] fix(test): add --alias default and explicit --project to target:apply commands --- scripts/hosting-tests/run.sh | 4 ++-- scripts/storage-deploy-tests/run.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index de36168ee71..86b6e699f17 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -148,8 +148,8 @@ mkdir "public" touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Setting targets..." -firebase use "${FBTOOLS_TARGET_PROJECT}" -firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" +firebase use "${FBTOOLS_TARGET_PROJECT}" --alias default +firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" --project "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index be4438e6871..55876067c8a 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -64,8 +64,8 @@ cat > "firebase.json" <<- EOM ] } EOM -firebase use "${FBTOOLS_TARGET_PROJECT}" -firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" +firebase use "${FBTOOLS_TARGET_PROJECT}" --alias default +firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" --project "${FBTOOLS_TARGET_PROJECT}" echo "Updated config for targets." echo "Testing storage deployment with invalid target..." From fa6cdcd787b0e0b5e5b6ef3f340ca0b9e5f7a6de Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 18 Aug 2026 00:21:53 +0000 Subject: [PATCH 23/25] fix(test): use firebase target:apply --project directly without firebase use --- scripts/hosting-tests/run.sh | 1 - scripts/storage-deploy-tests/run.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index 86b6e699f17..b0ed971b8fd 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -148,7 +148,6 @@ mkdir "public" touch "public/${TARGET_FILE}" echo "${DATE}" > "public/${TARGET_FILE}" echo "Setting targets..." -firebase use "${FBTOOLS_TARGET_PROJECT}" --alias default firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" --project "${FBTOOLS_TARGET_PROJECT}" echo "Set targets." echo "Initialized second temp directory." diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 55876067c8a..9df7f959f61 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -64,7 +64,6 @@ cat > "firebase.json" <<- EOM ] } EOM -firebase use "${FBTOOLS_TARGET_PROJECT}" --alias default firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com" --project "${FBTOOLS_TARGET_PROJECT}" echo "Updated config for targets." From 1ab79a46fb2dcda49b3858e32b198e529caf0518 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 18 Aug 2026 23:04:07 +0000 Subject: [PATCH 24/25] fix: address review feedback for zip slip path check, process cleanup, and port filtering --- .github/workflows/node-test.yml | 3 +++ scripts/hosting-tests/run.sh | 4 ++-- scripts/integration-helpers/cli.ts | 6 +++++- scripts/triggers-end-to-end-tests/run.sh | 4 ++-- src/unzip.spec.ts | 26 +++++++++++++++++++++++- src/unzip.ts | 23 +++++++++++---------- 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 41c65e511f0..279fa7e8bbb 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -299,6 +299,9 @@ jobs: - node-version: "24.18" script: "npm run test:functions-discover" steps: + # Exclude workspace and npm caches from Windows Defender real-time scanning. + # Windows Defender locks files during rapid disk operations (npm ci, binary/emulator extraction), + # which causes intermittent EPERM errors and test runner deadlocks on Windows runners. - name: Disable Windows Defender for workspace run: Set-MpPreference -ExclusionPath @("${{ github.workspace }}", "$env:LocalAppData\npm-cache", "$env:AppData\npm") shell: powershell diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index b0ed971b8fd..c46f197cb5c 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -53,13 +53,13 @@ echo "Initialized temp directory." function kill_port() { local PORT_NUM="$1" if command -v lsof &> /dev/null; then - local pids=$(lsof -t -i:"$PORT_NUM" 2>/dev/null || true) + local pids=$(lsof -t -sTCP:LISTEN -i:"$PORT_NUM" 2>/dev/null || true) if [ -n "$pids" ]; then kill -9 $pids 2>/dev/null || true fi fi if command -v netstat &> /dev/null; then - local pids=$(netstat -ano | grep ":$PORT_NUM " | awk '{print $5}' | sort -u || true) + local pids=$(netstat -ano | awk -v port=":$PORT_NUM" '$2 ~ port"$" && $4 == "LISTENING" {print $5}' | sort -u || true) for p in $pids; do if [ "$p" != "0" ] && [ -n "$p" ]; then taskkill //pid "$p" //T //F 2>/dev/null || true diff --git a/scripts/integration-helpers/cli.ts b/scripts/integration-helpers/cli.ts index d8085356899..cc64ad189da 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -86,7 +86,10 @@ export class CLIProcess { p.once("exit", () => resolve()); }); - const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 2000)); + let timeoutId: NodeJS.Timeout; + const timeoutPromise = new Promise((resolve) => { + timeoutId = setTimeout(resolve, 2000); + }); try { execSync(`taskkill /pid ${p.pid} /T /F`); @@ -95,6 +98,7 @@ export class CLIProcess { } return Promise.race([exitPromise, timeoutPromise]).then(() => { + clearTimeout(timeoutId); this.process = undefined; }); } diff --git a/scripts/triggers-end-to-end-tests/run.sh b/scripts/triggers-end-to-end-tests/run.sh index 8a4f29a0b16..a27c5180ebb 100755 --- a/scripts/triggers-end-to-end-tests/run.sh +++ b/scripts/triggers-end-to-end-tests/run.sh @@ -3,12 +3,12 @@ function cleanup() { for PORT in 4000 9000 9001 9002 8085 9099 9199; do if command -v lsof &> /dev/null; then - PID=$(lsof -t -i:$PORT 2>/dev/null || true) + PID=$(lsof -t -sTCP:LISTEN -i:$PORT 2>/dev/null || true) if [ -n "$PID" ]; then kill -9 $PID 2>/dev/null || true fi elif command -v netstat &> /dev/null; then - PIDS=$(netstat -ano | grep ":$PORT " | awk '{print $5}' | sort -u || true) + PIDS=$(netstat -ano | awk -v port=":$PORT" '$2 ~ port"$" && $4 == "LISTENING" {print $5}' | sort -u || true) for P in $PIDS; do if [ "$P" != "0" ] && [ -n "$P" ]; then taskkill //pid "$P" //T //F 2>/dev/null || true diff --git a/src/unzip.spec.ts b/src/unzip.spec.ts index 27307c7e813..a23ed5edbc6 100644 --- a/src/unzip.spec.ts +++ b/src/unzip.spec.ts @@ -2,9 +2,33 @@ import { expect } from "chai"; import * as fs from "fs"; import { tmpdir } from "os"; import * as path from "path"; -import { unzip } from "./unzip"; +import { unzip, isChildDir } from "./unzip"; import { ZIP_CASES } from "./test/fixtures/zip-files"; +describe("isChildDir", () => { + it("should return true for legitimate subdirectories and files", () => { + expect(isChildDir("/parent", "/parent/child")).to.be.true; + expect(isChildDir("/parent", "/parent/child/grandchild.txt")).to.be.true; + expect(isChildDir("/parent/", "/parent/child")).to.be.true; + }); + + it("should return false for the exact same path", () => { + expect(isChildDir("/parent", "/parent")).to.be.false; + expect(isChildDir("/parent/", "/parent/")).to.be.false; + }); + + it("should return false for sibling directories sharing a prefix (Zip Slip protection)", () => { + expect(isChildDir("/parent", "/parent-sibling")).to.be.false; + expect(isChildDir("/parent", "/parent_sibling/file.txt")).to.be.false; + expect(isChildDir("/tmp/app", "/tmp/app-secret/config.json")).to.be.false; + }); + + it("should return false for parent or ancestor traversal", () => { + expect(isChildDir("/parent/sub", "/parent")).to.be.false; + expect(isChildDir("/parent/sub", "/parent/other")).to.be.false; + }); +}); + describe("unzip", () => { let tempDir: string; diff --git a/src/unzip.ts b/src/unzip.ts index d8d3f9220d3..76c8869c90a 100644 --- a/src/unzip.ts +++ b/src/unzip.ts @@ -123,7 +123,11 @@ const extractEntriesFromBuffer = async (data: Buffer, outputDir: string): Promis } }; -function isChildDir(parentDir: string, potentialChild: string): boolean { +/** + * Validates whether potentialChild is a strict subdirectory or descendant file of parentDir. + * Protects against Zip Slip directory traversal vulnerabilities. + */ +export function isChildDir(parentDir: string, potentialChild: string): boolean { try { // 1. Resolve and normalize both paths to absolute paths const resolvedParent = path.resolve(parentDir); @@ -136,17 +140,14 @@ function isChildDir(parentDir: string, potentialChild: string): boolean { if (process.platform === "win32") { const lowerParent = resolvedParent.toLowerCase(); const lowerChild = resolvedChild.toLowerCase(); - return ( - (lowerChild.startsWith(lowerParent + path.sep) || lowerChild.startsWith(lowerParent)) && - lowerChild !== lowerParent - ); + const parentWithSep = lowerParent.endsWith(path.sep) ? lowerParent : lowerParent + path.sep; + return lowerChild.startsWith(parentWithSep) && lowerChild !== lowerParent; } - // The child path must start with the parent path and not be the same path. - return ( - (resolvedChild.startsWith(resolvedParent + path.sep) || - resolvedChild.startsWith(resolvedParent)) && - resolvedChild !== resolvedParent - ); + // The child path must start with the parent path with separator and not be the same path. + const parentWithSep = resolvedParent.endsWith(path.sep) + ? resolvedParent + : resolvedParent + path.sep; + return resolvedChild.startsWith(parentWithSep) && resolvedChild !== resolvedParent; } catch (error) { // If either path does not exist, an error will be thrown. // In this case, the potential child cannot be a subdirectory. From b667bc42d51c031c90433c75b28105ddd01c72d1 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 18 Aug 2026 23:46:02 +0000 Subject: [PATCH 25/25] fix(test): use unique runner-scoped channel names and file targets to prevent cross-OS test collision --- scripts/hosting-tests/run.sh | 7 ++++--- scripts/storage-deploy-tests/run.sh | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/hosting-tests/run.sh b/scripts/hosting-tests/run.sh index c46f197cb5c..8acba54895a 100755 --- a/scripts/hosting-tests/run.sh +++ b/scripts/hosting-tests/run.sh @@ -4,7 +4,8 @@ CWD="$(pwd)" source scripts/set-default-credentials.sh -TARGET_FILE="${COMMIT_SHA}-${CI_JOB_ID}.txt" +RUN_SUFFIX="${GITHUB_RUN_NUMBER:-$RANDOM}-${RUNNER_OS:-linux}-${RANDOM}" +TARGET_FILE="${COMMIT_SHA}-${RUN_SUFFIX}.txt" echo "Running in ${CWD}" echo "Running with node: $(which node)" @@ -105,7 +106,7 @@ kill_port "5000" echo "Tested local hosting emulator." echo "Testing hosting deployment..." -firebase hosting:channel:deploy --non-interactive --expires 1h --project "${FBTOOLS_TARGET_PROJECT}" --json "${GITHUB_RUN_NUMBER}" | tee channeldeploy.json +firebase hosting:channel:deploy --non-interactive --expires 1h --project "${FBTOOLS_TARGET_PROJECT}" --json "channel-${RUN_SUFFIX}" | tee channeldeploy.json URL=$(cat channeldeploy.json | jq -r ".result.\"${FBTOOLS_TARGET_PROJECT}\".url") sleep 12 VALUE="$(curl $URL/${TARGET_FILE})" @@ -161,7 +162,7 @@ echo "Initialized second temp directory." # echo "Tested hosting deployment by target." echo "Testing hosting channel deployment by target..." -firebase hosting:channel:deploy "targetchannel-${GITHUB_RUN_NUMBER}" --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --non-interactive --json | tee output.json +firebase hosting:channel:deploy "targetchannel-${RUN_SUFFIX}" --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --non-interactive --json | tee output.json CHANNEL_URL=$(cat output.json | jq -r ".result.customtarget.url") sleep 12 VALUE="$(curl ${CHANNEL_URL}/${TARGET_FILE})" diff --git a/scripts/storage-deploy-tests/run.sh b/scripts/storage-deploy-tests/run.sh index 9df7f959f61..ae6e530b809 100755 --- a/scripts/storage-deploy-tests/run.sh +++ b/scripts/storage-deploy-tests/run.sh @@ -4,7 +4,8 @@ CWD="$(pwd)" source scripts/set-default-credentials.sh -TARGET_FILE="${COMMIT_SHA}-${CI_JOB_ID}.txt" +RUN_SUFFIX="${GITHUB_RUN_NUMBER:-$RANDOM}-${RUNNER_OS:-linux}-${RANDOM}" +TARGET_FILE="${COMMIT_SHA}-${RUN_SUFFIX}.txt" echo "Running in ${CWD}" echo "Running with node: $(which node)"