Skip to content

Commit 6c320d6

Browse files
committed
Merge github.com:firebase/firebase-tools into zip_deploy_barebones
2 parents 28ae3b2 + a64faef commit 6c320d6

17 files changed

Lines changed: 566 additions & 218 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
- Updated Pub/Sub emulator to version 0.8.29.

npm-shrinkwrap.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-tools",
3-
"version": "15.10.0",
3+
"version": "15.10.1",
44
"description": "Command-Line Interface for Firebase",
55
"main": "./lib/index.js",
66
"mcpName": "io.github.firebase/firebase-mcp",

scripts/publish/cloudbuild.yaml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,26 @@ steps:
111111
- "REPOSITORY_ORG=${_REPOSITORY_ORG}"
112112
- "REPOSITORY_NAME=${_REPOSITORY_NAME}"
113113

114-
# Wait a bit of time for npm to catch up.
114+
# Wait for package to be available on npm.
115115
- name: "gcr.io/$PROJECT_ID/package-builder"
116-
args: ["sleep", "240"]
116+
entrypoint: "bash"
117+
args:
118+
- "-c"
119+
- |
120+
echo "Waiting for firebase-tools@$(cat /workspace/version_number.txt) to be available on npm..."
121+
timeout=300
122+
elapsed=0
123+
interval=15
124+
until npm view "firebase-tools@$(cat /workspace/version_number.txt)" > /dev/null 2>&1; do
125+
if [ $$elapsed -ge $$timeout ]; then
126+
echo "Timeout waiting for package."
127+
exit 1
128+
fi
129+
echo "Still waiting..."
130+
sleep $$interval
131+
elapsed=$$((elapsed + interval))
132+
done
133+
echo "Package firebase-tools@$(cat /workspace/version_number.txt) is now available on npm."
117134
118135
# Set up the hub credentials for firepit-builder.
119136
- name: "gcr.io/$PROJECT_ID/firepit-builder"
@@ -134,7 +151,7 @@ steps:
134151
- "-c"
135152
- |
136153
if [ "${_VERSION}" != "preview" ]; then
137-
GITHUB_TOKEN=$(cat ~/.config/hub) node /usr/src/app/pipeline.js --package=firebase-tools@latest --publish
154+
GITHUB_TOKEN=$(cat ~/.config/hub) node /usr/src/app/pipeline.js --package=firebase-tools@$(cat /workspace/version_number.txt) --publish
138155
echo "Please review the draft release notes at https://github.com/${_REPOSITORY_ORG}/${_REPOSITORY_NAME}/releases. If it looks good, publish it"
139156
else
140157
echo "Skipping firepit build for preview version."

scripts/publish/firebase-docker-image/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ COPY package-lock.json /usr/local/node_packages/
2020

2121
WORKDIR /usr/local/node_packages/
2222
RUN npm install && npm update
23+
RUN chmod -R 755 /usr/local/node_packages /usr/local/lib/node_modules
2324
ENV PATH="/usr/local/node_packages/node_modules/.bin:${PATH}"
2425

2526
WORKDIR /

src/apptesting/parseTestFiles.spec.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ describe("parseTestFiles", () => {
6868
]);
6969
});
7070

71+
it("ignores non-yaml files", async () => {
72+
writeFile(
73+
"my_test.yaml",
74+
stringify({
75+
tests: [{ displayName: "my test", steps: [{ goal: "click a button" }] }],
76+
}),
77+
);
78+
writeFile(
79+
"my_test.txt",
80+
stringify({
81+
tests: [{ displayName: "should not be parsed", steps: [{ goal: "do nothing" }] }],
82+
}),
83+
);
84+
const tests = await parseTestFiles(tempdir.name, "http://www.foo.com");
85+
expect(tests.length).to.equal(1);
86+
expect(tests[0].testCase.displayName).to.equal("my test");
87+
});
88+
7189
it("parses the sample test case file", async () => {
7290
writeFile("smoke_test.yaml", readTemplateSync("init/apptesting/smoke_test.yaml"));
7391
const tests = await parseTestFiles(tempdir.name, "http://www.foo.com");
@@ -181,27 +199,43 @@ describe("parseTestFiles", () => {
181199
}
182200

183201
it("returns an empty list if no match", async () => {
184-
writeFile("aaa", createBasicTest(["axx", "ayy", "azz"]));
185-
writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"]));
202+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
203+
writeFile("bbb.yaml", createBasicTest(["bxx", "byy", "bzz"]));
186204
expect(await getTestCaseNames("yyy")).to.eql([]);
187205
});
188206

189207
it("filters on filename", async () => {
190-
writeFile("aaa", createBasicTest(["axx", "ayy", "azz"]));
191-
writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"]));
208+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
209+
writeFile("bbb.yaml", createBasicTest(["bxx", "byy", "bzz"]));
192210
expect(await getTestCaseNames("aaa")).to.eql(["axx", "ayy", "azz"]);
193211
});
194212

195213
it("filters on test case name", async () => {
196-
writeFile("aaa", createBasicTest(["axx", "ayy", "azz"]));
197-
writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"]));
214+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
215+
writeFile("bbb.yaml", createBasicTest(["bxx", "byy", "bzz"]));
198216
expect(await getTestCaseNames("", ".xx")).to.eql(["axx", "bxx"]);
199217
});
200218

201219
it("filters on filename and test case name", async () => {
202-
writeFile("aaa", createBasicTest(["axx", "ayy", "azz"]));
203-
writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"]));
204-
expect(await getTestCaseNames("a$", "xx")).to.eql(["axx"]);
220+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
221+
writeFile("bbb.yaml", createBasicTest(["bxx", "byy", "bzz"]));
222+
expect(await getTestCaseNames("aaa\\.yaml$", "xx")).to.eql(["axx"]);
223+
});
224+
225+
it("throws an error for invalid filename regex", async () => {
226+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
227+
await expect(getTestCaseNames("*.txt")).to.be.rejectedWith(
228+
FirebaseError,
229+
"Invalid file pattern regex: *.txt",
230+
);
231+
});
232+
233+
it("throws an error for invalid test case name regex", async () => {
234+
writeFile("aaa.yaml", createBasicTest(["axx", "ayy", "azz"]));
235+
await expect(getTestCaseNames("", "*.txt")).to.be.rejectedWith(
236+
FirebaseError,
237+
"Invalid test name pattern regex: *.txt",
238+
);
205239
});
206240
});
207241

src/apptesting/parseTestFiles.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export async function parseTestFiles(
3535
{} as Record<string, TestCaseInvocation>,
3636
);
3737

38-
const fileFilterFn = createFilter(filePattern);
39-
const nameFilterFn = createFilter(namePattern);
38+
const fileFilterFn = createFilter(filePattern, "file pattern");
39+
const nameFilterFn = createFilter(namePattern, "test name pattern");
4040
const filteredInvocations = files
4141
.filter((file) => fileFilterFn(file.path))
4242
.flatMap((file) => file.invocations)
@@ -76,9 +76,17 @@ export async function parseTestFiles(
7676
});
7777
}
7878

79-
function createFilter(pattern?: string) {
80-
const regex = pattern ? new RegExp(pattern) : undefined;
81-
return (s: string) => !regex || regex.test(s);
79+
function createFilter(pattern?: string, context?: string) {
80+
try {
81+
const regex = pattern ? new RegExp(pattern) : undefined;
82+
return (s: string) => !regex || regex.test(s);
83+
} catch (ex) {
84+
if (ex instanceof SyntaxError) {
85+
const errMsg = context ? `Invalid ${context} regex: ${pattern}` : `Invalid regex: ${pattern}`;
86+
throw new FirebaseError(errMsg, { original: getError(ex) });
87+
}
88+
throw ex;
89+
}
8290
}
8391

8492
interface TestCaseFile {
@@ -92,25 +100,24 @@ async function parseTestFilesRecursive(params: {
92100
}): Promise<TestCaseFile[]> {
93101
const testDir = params.testDir;
94102
const targetUri = params.targetUri;
95-
const items = listFiles(testDir);
103+
const filenames = listFiles(testDir);
96104
const results = [];
97-
for (const item of items) {
98-
const path = join(testDir, item);
105+
for (const filename of filenames) {
106+
const path = join(testDir, filename);
99107
if (dirExistsSync(path)) {
100108
results.push(...(await parseTestFilesRecursive({ testDir: path, targetUri })));
101-
} else if (fileExistsSync(path)) {
109+
} else if (fileExistsSync(path) && (path.endsWith(".yaml") || path.endsWith(".yml"))) {
102110
try {
103-
const file = await readFileFromDirectory(testDir, item);
104-
logger.debug(`Read the file ${file.source}.`);
111+
logger.debug(`Reading ${path}.`);
112+
const file = await readFileFromDirectory(testDir, filename);
105113
const parsedFile = wrappedSafeLoad(file.source);
106-
logger.debug(`Parsed the file.`);
107114
const tests = parsedFile.tests;
108-
logger.debug(`There are ${tests.length} tests.`);
109115
const defaultConfig = parsedFile.defaultConfig;
110116
if (!tests || !tests.length) {
111117
logger.debug(`No tests found in ${path}. Ignoring.`);
112118
continue;
113119
}
120+
logger.debug(`File contains ${pluralizeTests(tests.length)}.`);
114121
const invocations = [];
115122
for (const rawTestDef of tests) {
116123
const invocation = toTestCaseInvocation(rawTestDef, targetUri, defaultConfig);
@@ -129,6 +136,10 @@ async function parseTestFilesRecursive(params: {
129136
return results;
130137
}
131138

139+
export function pluralizeTests(numTests: number) {
140+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
141+
}
142+
132143
function toTestCaseInvocation(
133144
testDef: any,
134145
targetUri: any,

src/commands/apptesting.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { requireAuth } from "../requireAuth";
22
import { Command } from "../command";
3-
import { parseTestFiles } from "../apptesting/parseTestFiles";
3+
import { parseTestFiles, pluralizeTests } from "../apptesting/parseTestFiles";
44
import * as ora from "ora";
55
import { TestCaseInvocation } from "../apptesting/types";
66
import { FirebaseError, getError } from "../error";
@@ -9,6 +9,8 @@ import { awaitTestResults, Distribution, upload } from "../appdistribution/distr
99
import { AiInstructions, ReleaseTest, TestDevice, Release } from "../appdistribution/types";
1010
import { getAppName, parseTestDevices } from "../appdistribution/options-parser-util";
1111
import * as utils from "../utils";
12+
import { dirExistsSync } from "../fsutils";
13+
import * as path from "path";
1214

1315
const defaultDevices = [
1416
{
@@ -33,24 +35,29 @@ export const command = new Command("apptesting:execute <release-binary-file>")
3335
"--test-name-pattern <pattern>",
3436
"Test name pattern. Only tests with names that match this pattern will be executed.",
3537
)
36-
.option("--test-dir <test_dir>", "Directory where tests can be found.")
38+
.option("--test-dir <test_dir>", "Directory where tests can be found. Defaults to './tests'.")
3739
.option(
3840
"--test-devices <string>",
39-
"semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.",
41+
"Semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.",
4042
)
4143
.option(
4244
"--test-devices-file <string>",
43-
"path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.",
45+
"Path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.",
4446
)
4547
.option(
4648
"--test-non-blocking",
47-
"run automated tests without waiting for them to complete. Visit the Firebase console for the test results.",
49+
"Run automated tests without waiting for them to complete. Visit the Firebase console for the test results.",
4850
)
4951
.before(requireAuth)
5052
.action(async (target: string, options: any) => {
5153
const appName = getAppName(options);
5254

53-
const testDir = options.testDir || "tests";
55+
const testDir = path.resolve(options.testDir || "tests");
56+
if (!dirExistsSync(testDir)) {
57+
throw new FirebaseError(
58+
`Tests directory not found: ${testDir}. Use the --test-dir flag to choose a different directory.`,
59+
);
60+
}
5461
const tests = await parseTestFiles(
5562
testDir,
5663
undefined,
@@ -60,8 +67,9 @@ export const command = new Command("apptesting:execute <release-binary-file>")
6067
const testDevices = parseTestDevices(options.testDevices, options.testDevicesFile);
6168

6269
if (!tests.length) {
63-
throw new FirebaseError("No tests found");
70+
throw new FirebaseError(`No tests found under test directory ${testDir}`);
6471
}
72+
utils.logBullet(`Found ${pluralizeTests(tests.length)} to run under test directory ${testDir}`);
6573

6674
const invokeSpinner = ora("Requesting test execution");
6775
const client = new AppDistributionClient();
@@ -97,10 +105,6 @@ export const command = new Command("apptesting:execute <release-binary-file>")
97105
}
98106
});
99107

100-
function pluralizeTests(numTests: number) {
101-
return `${numTests} test${numTests === 1 ? "" : "s"}`;
102-
}
103-
104108
async function invokeTests(
105109
client: AppDistributionClient,
106110
releaseName: string,

src/commands/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export function load(client: CLIClient): CLIClient {
4343
client.appdistribution.testCases = {};
4444
client.appdistribution.testCases.export = loadCommand("appdistribution-testcases-export");
4545
client.appdistribution.testCases.import = loadCommand("appdistribution-testcases-import");
46+
client.apptesting = {};
47+
client.apptesting.execute = loadCommand("apptesting");
48+
if (experiments.isEnabled("apptesting")) {
49+
client.apptesting.wata = loadCommand("apptesting-wata");
50+
}
4651
client.apps = {};
4752
client.apps.create = loadCommand("apps-create");
4853
client.apps.list = loadCommand("apps-list");
@@ -264,11 +269,6 @@ export function load(client: CLIClient): CLIClient {
264269
client.target.clear = loadCommand("target-clear");
265270
client.target.remove = loadCommand("target-remove");
266271
client.use = loadCommand("use");
267-
client.apptesting = {};
268-
client.apptesting.execute = loadCommand("apptesting");
269-
if (experiments.isEnabled("apptesting")) {
270-
client.apptesting.wata = loadCommand("apptesting-wata");
271-
}
272272

273273
const t1 = process.hrtime.bigint();
274274
const diffMS = (t1 - t0) / BigInt(1e6);

src/commands/studio-export.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import { FirebaseError } from "../error";
77
import { unzip } from "../unzip";
88
import * as fs from "fs";
99

10-
export const command = new Command("studio:export <path>")
10+
export const command = new Command("studio:export [path]")
1111
.description(
1212
"Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download, or directly on the downloaded zip file.",
1313
)
1414
.option("--no-start-antigravity", "skip starting the Antigravity IDE after migration")
1515
.action(async (exportPath: string, options: Options) => {
1616
if (!exportPath) {
17-
throw new FirebaseError("Must specify a path for migration.", { exit: 1 });
17+
throw new FirebaseError(
18+
"Must specify the path to the Firebase Studio downloaded zip file or the unzipped folder path.",
19+
{ exit: 1 },
20+
);
1821
}
1922

2023
let rootPath = path.resolve(exportPath);

0 commit comments

Comments
 (0)