Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0b1634a
fix: stabilize Windows CI integration tests
joehan Aug 14, 2026
7fa34d1
fix: address PR review feedback and increase unzip test timeout
joehan Aug 14, 2026
b4442f0
fix: write firebaserc directly in integration tests, simplify taskkil…
joehan Aug 15, 2026
5bdb30d
fix: robust background process cleanup on Windows in hosting and trig…
joehan Aug 15, 2026
acf0ca9
fix: normalize path and case handling in unzip and scale timeouts in …
joehan Aug 15, 2026
6293de4
fix: add non-interactive flag and release working directory before cl…
joehan Aug 15, 2026
27038cd
fix: generate full targets in .firebaserc directly for storage and ho…
joehan Aug 15, 2026
941862c
fix: add --non-interactive and --force to hosting channel deploy by t…
joehan Aug 15, 2026
b24a1f8
fix: place positional channelId argument before option flags in hosti…
joehan Aug 15, 2026
5a0eeea
fix: use robust tarball resolution in clean-install and hoisted linke…
joehan Aug 15, 2026
f15c5a4
fix: exclude npm cache and global npm directories from Defender on Wi…
joehan Aug 15, 2026
57b2b7a
fix: pass --exit to mocha in emulator tests and add --prefer-offline …
joehan Aug 15, 2026
26ac598
ci: validation run 2
joehan Aug 15, 2026
a36d3c2
fix: remove npm@9.5 downgrade in integration-windows to prevent deadl…
joehan Aug 15, 2026
b951a23
ci: validation run 3
joehan Aug 15, 2026
911c033
fix: initialize .firebaserc earlier in storage deploy test temp dir
joehan Aug 15, 2026
7f69fe6
fix: pass --exit to mocha and add --prefer-offline to fixture depende…
joehan Aug 15, 2026
b10a818
fix: kill processes by port in hosting-tests and isolate channel name…
joehan Aug 15, 2026
4766182
fix(test): await process exit with timeout fallback during taskkill o…
joehan Aug 17, 2026
2892c90
fix(test): use firebase target:apply and document windows case-insens…
joehan Aug 17, 2026
a91dfc0
fix(test): add missing EOM delimiter in storage deploy tests
joehan Aug 17, 2026
fa15318
fix(test): add --alias default and explicit --project to target:apply…
joehan Aug 18, 2026
fa6cdcd
fix(test): use firebase target:apply --project directly without fireb…
joehan Aug 18, 2026
1ab79a4
fix: address review feedback for zip slip path check, process cleanup…
joehan Aug 18, 2026
b667bc4
fix(test): use unique runner-scoped channel names and file targets to…
joehan Aug 18, 2026
e4a6842
Merge branch 'main' into fix-windows-ci-flakiness
joehan Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/node-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ 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
Comment thread
joehan marked this conversation as resolved.
run: Set-MpPreference -ExclusionPath @("${{ github.workspace }}", "$env:LocalAppData\npm-cache", "$env:AppData\npm")
shell: powershell

- name: Setup Java JDK
uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # ratchet:actions/setup-java@v3.3.0
with:
Expand All @@ -324,12 +331,11 @@ 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()
run: dir "*.log" /s/b | type
run: Get-ChildItem -Path . -Filter "*debug.log" -Recurse | ForEach-Object { Get-Content $_.FullName }

check-package-lock:
runs-on: ubuntu-latest
Expand Down
6 changes: 4 additions & 2 deletions scripts/clean-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +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"
2 changes: 1 addition & 1 deletion scripts/client-integration-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

source scripts/set-default-credentials.sh

mocha scripts/client-integration-tests/tests.ts
mocha --exit --timeout 30000 scripts/client-integration-tests/tests.ts
30 changes: 18 additions & 12 deletions scripts/client-integration-tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ describe("deployHosting", () => {
});

after(() => {
unlinkSync(firebasercFile);
try {
unlinkSync(firebasercFile);
} catch {
// ignore
}
});

it("should deploy hosting", async () => {
Expand All @@ -53,19 +57,18 @@ describe("deployHosting", () => {
}).timeout(20 * 1e3); // Deploying takes several steps.
});

describe("apps:list", () => {
describe("apps:list", function (this: Mocha.Suite) {
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);
});

Expand All @@ -76,7 +79,10 @@ describe("apps:list", () => {
});
});

describe("apps:sdkconfig", () => {
describe("apps:sdkconfig", function (this: Mocha.Suite) {
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);
Expand Down
4 changes: 2 additions & 2 deletions scripts/emulator-tests/fixtures.ts
Original file line number Diff line number Diff line change
@@ -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 } = {
Expand Down
2 changes: 1 addition & 1 deletion scripts/emulator-tests/functionsEmulator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions scripts/emulator-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
4 changes: 2 additions & 2 deletions scripts/emulator-tests/unzipEmulators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
set -euxo pipefail # bash strict mode
IFS=$'\n\t'

npm i
npm i --prefer-offline --no-audit
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion scripts/functions-discover-tests/fixtures/esm/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion scripts/functions-discover-tests/fixtures/pnpm/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
set -euxo pipefail # bash strict mode
IFS=$'\n\t'

yarn install
yarn install --prefer-offline
6 changes: 3 additions & 3 deletions scripts/functions-discover-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
mocha --exit scripts/functions-discover-tests/tests.ts
43 changes: 34 additions & 9 deletions scripts/hosting-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -50,14 +51,35 @@ touch "public/${TARGET_FILE}"
echo "${DATE}" > "public/${TARGET_FILE}"
echo "Initialized temp directory."

function kill_port() {
Comment thread
joehan marked this conversation as resolved.
local PORT_NUM="$1"
if command -v lsof &> /dev/null; then
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 | 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
fi
done
fi
}

echo "Testing local serve..."
firebase serve --only hosting --project "${FBTOOLS_TARGET_PROJECT}" --port "${PORT}" --debug &
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
kill_port "${PORT}"
echo "Tested local serve."

echo "Testing local hosting emulator..."
Expand All @@ -75,12 +97,16 @@ 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
kill_port "${PORT}"
kill_port "5000"
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 "channel-${RUN_SUFFIX}" | tee channeldeploy.json
URL=$(cat channeldeploy.json | jq -r ".result.\"${FBTOOLS_TARGET_PROJECT}\".url")
sleep 12
VALUE="$(curl $URL/${TARGET_FILE})"
Expand Down Expand Up @@ -123,8 +149,7 @@ mkdir "public"
touch "public/${TARGET_FILE}"
echo "${DATE}" > "public/${TARGET_FILE}"
echo "Setting targets..."
firebase use --add "${FBTOOLS_TARGET_PROJECT}"
firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}"
firebase target:apply hosting customtarget "${FBTOOLS_TARGET_PROJECT}" --project "${FBTOOLS_TARGET_PROJECT}"
echo "Set targets."
echo "Initialized second temp directory."

Expand All @@ -137,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 mychannel --only customtarget --project "${FBTOOLS_TARGET_PROJECT}" --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})"
Expand Down
28 changes: 27 additions & 1 deletion scripts/integration-helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChildProcess } from "child_process";
import { ChildProcess, execSync } from "child_process";
import * as spawn from "cross-spawn";

export class CLIProcess {
Expand Down Expand Up @@ -77,6 +77,32 @@ export class CLIProcess {
return Promise.resolve();
}

if (process.platform === "win32" && p.pid) {
const exitPromise = new Promise<void>((resolve) => {
if (p.exitCode !== null || p.signalCode !== null) {
resolve();
return;
}
p.once("exit", () => resolve());
});

let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<void>((resolve) => {
timeoutId = setTimeout(resolve, 2000);
});

try {
execSync(`taskkill /pid ${p.pid} /T /F`);
} catch {
// ignore if process already exited
}

return Promise.race([exitPromise, timeoutPromise]).then(() => {
clearTimeout(timeoutId);
this.process = undefined;
});
}
Comment thread
joehan marked this conversation as resolved.

const stopped = new Promise<void>((resolve) => {
p.once("exit", (/* exitCode, signal */) => {
this.process = undefined;
Expand Down
14 changes: 7 additions & 7 deletions scripts/storage-deploy-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -48,7 +49,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."
Expand All @@ -64,20 +65,19 @@ cat > "firebase.json" <<- EOM
]
}
EOM
firebase use --add "${FBTOOLS_TARGET_PROJECT}"
firebase target:apply storage storage-target "${FBTOOLS_TARGET_PROJECT}.appspot.com"
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..."
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)
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..."
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."
Loading
Loading