Skip to content

Commit

Permalink
Implement Experiments feature (#4994)
Browse files Browse the repository at this point in the history
Add experiments to the Firebase CLI.  New commands:

```
firebase experiments:enable <experiment>
firebase experiments:disable <experiment>
firebase experiments:describe <experiment>
firebase experiments:list
```
  • Loading branch information
inlined committed Sep 29, 2022
1 parent 20d3d7c commit c3ec450
Show file tree
Hide file tree
Showing 27 changed files with 424 additions and 124 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/node-test.yml
Expand Up @@ -67,7 +67,6 @@ jobs:
runs-on: ubuntu-latest

env:
FIREBASE_CLI_PREVIEWS: none
FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache
COMMIT_SHA: ${{ github.sha }}
CI_JOB_ID: ${{ github.action }}
Expand Down Expand Up @@ -122,7 +121,6 @@ jobs:
runs-on: windows-latest

env:
FIREBASE_CLI_PREVIEWS: none
FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache
COMMIT_SHA: ${{ github.sha }}
CI_JOB_ID: ${{ github.action }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -0,0 +1 @@
- Add the "experiments" family of commands (#4994)
6 changes: 3 additions & 3 deletions src/commands/database-instances-list.ts
Expand Up @@ -9,7 +9,7 @@ import { needProjectNumber } from "../projectUtils";
import firedata = require("../gcp/firedata");
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { previews } from "../previews";
import * as experiments from "../experiments";
import { needProjectId } from "../projectUtils";
import {
listDatabaseInstances,
Expand Down Expand Up @@ -52,7 +52,7 @@ export let command = new Command("database:instances:list")
).start();
let instances;

if (previews.rtdbmanagement) {
if (experiments.isEnabled("rtdbmanagement")) {
const projectId = needProjectId(options);
try {
instances = await listDatabaseInstances(projectId, location);
Expand Down Expand Up @@ -80,7 +80,7 @@ export let command = new Command("database:instances:list")
return instances;
});

if (previews.rtdbmanagement) {
if (experiments.isEnabled("rtdbmanagement")) {
command = command.option(
"-l, --location <location>",
"(optional) location for the database instance, defaults to us-central1"
Expand Down
32 changes: 32 additions & 0 deletions src/commands/experiments-describe.ts
@@ -0,0 +1,32 @@
import { bold } from "colorette";

import { Command } from "../command";
import { FirebaseError } from "../error";
import * as experiments from "../experiments";
import { logger } from "../logger";
import { last } from "../utils";

export const command = new Command("experiments:describe <experiment>")
.description("enable an experiment on this machine")
.action((experiment: string) => {
if (!experiments.isValidExperiment(experiment)) {
let message = `Cannot find experiment ${bold(experiment)}`;
const potentials = experiments.experimentNameAutocorrect(experiment);
if (potentials.length === 1) {
message = `${message}\nDid you mean ${potentials[0]}?`;
} else if (potentials.length) {
message = `${message}\nDid you mean ${potentials.slice(0, -1).join(",")} or ${last(
potentials
)}?`;
}
throw new FirebaseError(message);
}

const spec = experiments.ALL_EXPERIMENTS[experiment];
logger.info(`${bold("Name")}: ${experiment}`);
logger.info(`${bold("Enabled")}: ${experiments.isEnabled(experiment) ? "yes" : "no"}`);
if (spec.docsUri) {
logger.info(`${bold("Documentation")}: ${spec.docsUri}`);
}
logger.info(`${bold("Description")}: ${spec.fullDescription || spec.shortDescription}`);
});
29 changes: 29 additions & 0 deletions src/commands/experiments-disable.ts
@@ -0,0 +1,29 @@
import { bold } from "colorette";

import { Command } from "../command";
import { FirebaseError } from "../error";
import * as experiments from "../experiments";
import { logger } from "../logger";
import { last } from "../utils";

export const command = new Command("experiments:disable <experiment>")
.description("disable an experiment on this machine")
.action((experiment: string) => {
if (experiments.isValidExperiment(experiment)) {
experiments.setEnabled(experiment, false);
experiments.flushToDisk();
logger.info(`Disabled experiment ${bold(experiment)}`);
return;
}

let message = `Cannot find experiment ${bold(experiment)}`;
const potentials = experiments.experimentNameAutocorrect(experiment);
if (potentials.length === 1) {
message = `${message}\nDid you mean ${potentials[0]}?`;
} else if (potentials.length) {
message = `${message}\nDid you mean ${potentials.slice(0, -1).join(",")} or ${last(
potentials
)}?`;
}
throw new FirebaseError(message);
});
29 changes: 29 additions & 0 deletions src/commands/experiments-enable.ts
@@ -0,0 +1,29 @@
import { bold } from "colorette";

import { Command } from "../command";
import { FirebaseError } from "../error";
import * as experiments from "../experiments";
import { logger } from "../logger";
import { last } from "../utils";

export const command = new Command("experiments:enable <experiment>")
.description("enable an experiment on this machine")
.action((experiment: string) => {
if (experiments.isValidExperiment(experiment)) {
experiments.setEnabled(experiment, true);
experiments.flushToDisk();
logger.info(`Enabled experiment ${bold(experiment)}`);
return;
}

let message = `Cannot find experiment ${bold(experiment)}`;
const potentials = experiments.experimentNameAutocorrect(experiment);
if (potentials.length === 1) {
message = `${message}\nDid you mean ${potentials[0]}?`;
} else if (potentials.length) {
message = `${message}\nDid you mean ${potentials.slice(0, -1).join(",")} or ${last(
potentials
)}?`;
}
throw new FirebaseError(message);
});
25 changes: 25 additions & 0 deletions src/commands/experiments-list.ts
@@ -0,0 +1,25 @@
import { Command } from "../command";
import Table = require("cli-table");
import * as experiments from "../experiments";
import { partition } from "../functional";
import { logger } from "../logger";

export const command = new Command("experiments:list").action(() => {
const table = new Table({
head: ["Enabled", "Name", "Description"],
style: { head: ["yellow"] },
});
const [enabled, disabled] = partition(Object.entries(experiments.ALL_EXPERIMENTS), ([name]) => {
return experiments.isEnabled(name as experiments.ExperimentName);
});
for (const [name, exp] of enabled) {
table.push(["y", name, exp.shortDescription]);
}
for (const [name, exp] of disabled) {
if (!exp.public) {
continue;
}
table.push(["n", name, exp.shortDescription]);
}
logger.info(table.toString());
});
4 changes: 2 additions & 2 deletions src/commands/ext-install.ts
Expand Up @@ -30,7 +30,7 @@ import { getRandomString } from "../extensions/utils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { track } from "../track";
import { previews } from "../previews";
import * as experiments from "../experiments";
import { Options } from "../options";
import * as manifest from "../extensions/manifest";

Expand All @@ -44,7 +44,7 @@ marked.setOptions({
export const command = new Command("ext:install [extensionName]")
.description(
"install an official extension if [extensionName] or [extensionName@version] is provided; " +
(previews.extdev
(experiments.isEnabled("extdev")
? "install a local extension if [localPathOrUrl] or [url#root] is provided; install a published extension (not authored by Firebase) if [publisherId/extensionId] is provided "
: "") +
"or run with `-i` to see all available extensions."
Expand Down
4 changes: 2 additions & 2 deletions src/commands/ext-update.ts
Expand Up @@ -22,7 +22,7 @@ import * as refs from "../extensions/refs";
import { getProjectId } from "../projectUtils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { previews } from "../previews";
import * as experiments from "../experiments";
import * as manifest from "../extensions/manifest";
import { Options } from "../options";
import * as askUserForEventsConfig from "../extensions/askUserForEventsConfig";
Expand All @@ -36,7 +36,7 @@ marked.setOptions({
*/
export const command = new Command("ext:update <extensionInstanceId> [updateSource]")
.description(
previews.extdev
experiments.isEnabled("extdev")
? "update an existing extension instance to the latest version or from a local or URL source"
: "update an existing extension instance to the latest version"
)
Expand Down
18 changes: 13 additions & 5 deletions src/commands/index.ts
@@ -1,5 +1,8 @@
import { previews } from "../previews";
import * as experiments from "../experiments";

/**
* Loads all commands for our parser.
*/
export function load(client: any): any {
function loadCommand(name: string) {
const t0 = process.hrtime.bigint();
Expand Down Expand Up @@ -48,7 +51,7 @@ export function load(client: any): any {
client.database.profile = loadCommand("database-profile");
client.database.push = loadCommand("database-push");
client.database.remove = loadCommand("database-remove");
if (previews.rtdbrules) {
if (experiments.isEnabled("rtdbrules")) {
client.database.rules = {};
client.database.rules.get = loadCommand("database-rules-get");
client.database.rules.list = loadCommand("database-rules-list");
Expand All @@ -69,6 +72,11 @@ export function load(client: any): any {
client.experimental = {};
client.experimental.functions = {};
client.experimental.functions.shell = loadCommand("experimental-functions-shell");
client.experiments = {};
client.experiments.list = loadCommand("experiments-list");
client.experiments.describe = loadCommand("experiments-describe");
client.experiments.enable = loadCommand("experiments-enable");
client.experiments.disable = loadCommand("experiments-disable");
client.ext = loadCommand("ext");
client.ext.configure = loadCommand("ext-configure");
client.ext.info = loadCommand("ext-info");
Expand All @@ -77,11 +85,11 @@ export function load(client: any): any {
client.ext.list = loadCommand("ext-list");
client.ext.uninstall = loadCommand("ext-uninstall");
client.ext.update = loadCommand("ext-update");
if (previews.ext) {
if (experiments.isEnabled("ext")) {
client.ext.sources = {};
client.ext.sources.create = loadCommand("ext-sources-create");
}
if (previews.extdev) {
if (experiments.isEnabled("extdev")) {
client.ext.dev = {};
client.ext.dev.init = loadCommand("ext-dev-init");
client.ext.dev.list = loadCommand("ext-dev-list");
Expand Down Expand Up @@ -110,7 +118,7 @@ export function load(client: any): any {
client.functions.log = loadCommand("functions-log");
client.functions.shell = loadCommand("functions-shell");
client.functions.list = loadCommand("functions-list");
if (previews.deletegcfartifacts) {
if (experiments.isEnabled("deletegcfartifacts")) {
client.functions.deletegcfartifacts = loadCommand("functions-deletegcfartifacts");
}
client.functions.secrets = {};
Expand Down
4 changes: 2 additions & 2 deletions src/deploy/functions/build.ts
Expand Up @@ -2,7 +2,7 @@ import * as backend from "./backend";
import * as proto from "../../gcp/proto";
import * as api from "../../.../../api";
import * as params from "./params";
import { previews } from "../../previews";
import * as experiments from "../../experiments";
import { FirebaseError } from "../../error";
import { assertExhaustive, mapObject, nullsafeVisitor } from "../../functional";
import { UserEnvsOpts, writeUserEnvs } from "../../functions/env";
Expand Down Expand Up @@ -281,7 +281,7 @@ export async function resolveBackend(
nonInteractive?: boolean
): Promise<{ backend: backend.Backend; envs: Record<string, params.ParamValue> }> {
let paramValues: Record<string, params.ParamValue> = {};
if (previews.functionsparams) {
if (experiments.isEnabled("functionsparams")) {
paramValues = await params.resolveParams(
build.params,
firebaseConfig,
Expand Down
7 changes: 5 additions & 2 deletions src/deploy/functions/prepare.ts
Expand Up @@ -29,7 +29,7 @@ import { FirebaseError } from "../../error";
import { configForCodebase, normalizeAndValidate } from "../../functions/projectConfig";
import { AUTH_BLOCKING_EVENTS } from "../../functions/events/v1";
import { generateServiceIdentity } from "../../gcp/serviceusage";
import { previews } from "../../previews";
import * as experiments from "../../experiments";
import { applyBackendHashToBackends } from "./cache/applyHash";
import { allEndpoints, Backend } from "./backend";

Expand Down Expand Up @@ -272,7 +272,7 @@ export async function prepare(
* This must be called after `await validate.secretsAreValid`.
*/
updateEndpointTargetedStatus(wantBackends, context.filters || []);
if (previews.skipdeployingnoopfunctions) {
if (experiments.isEnabled("skipdeployingnoopfunctions")) {
applyBackendHashToBackends(wantBackends, context);
}
}
Expand Down Expand Up @@ -346,6 +346,9 @@ function maybeCopyTriggerRegion(wantE: backend.Endpoint, haveE: backend.Endpoint
wantE.eventTrigger.region = haveE.eventTrigger.region;
}

/**
* Determines whether endpoints are targeted by an --only flag.
*/
export function updateEndpointTargetedStatus(
wantBackends: Record<string, Backend>,
endpointFilters: EndpointFilter[]
Expand Down
14 changes: 12 additions & 2 deletions src/deploy/functions/release/planner.ts
@@ -1,3 +1,5 @@
import * as clc from "colorette";

import {
EndpointFilter,
endpointMatchesAnyFilter,
Expand All @@ -8,7 +10,7 @@ import { FirebaseError } from "../../../error";
import * as utils from "../../../utils";
import * as backend from "../backend";
import * as v2events from "../../../functions/events/v2";
import { previews } from "../../../previews";
import * as experiments from "../../../experiments";

export interface EndpointUpdate {
endpoint: backend.Endpoint;
Expand Down Expand Up @@ -54,7 +56,7 @@ export function calculateChangesets(
keyFn
);

const { skipdeployingnoopfunctions } = previews;
const skipdeployingnoopfunctions = experiments.isEnabled("skipdeployingnoopfunctions");

// If the hashes are matching, that means the local function is the same as the server copy.
const toSkipPredicate = (id: string): boolean =>
Expand All @@ -75,6 +77,14 @@ export function calculateChangesets(
}, {});

const toSkip = utils.groupBy(Object.values(toSkipEndpointsMap), keyFn);
if (Object.keys(toSkip).length) {
utils.logLabeledBullet(
"functions",
`Skipping the deploy of unchanged functions with ${clc.bold(
"experimental"
)} support for skipdeployingnoopfunctions`
);
}

const toUpdate = utils.groupBy(
Object.keys(want)
Expand Down
5 changes: 3 additions & 2 deletions src/deploy/functions/runtimes/index.ts
@@ -1,6 +1,5 @@
import * as backend from "../backend";
import * as build from "../build";
import * as golang from "./golang";
import * as node from "./node";
import * as validate from "../validate";
import { FirebaseError } from "../../../error";
Expand Down Expand Up @@ -109,7 +108,9 @@ export interface DelegateContext {
}

type Factory = (context: DelegateContext) => Promise<RuntimeDelegate | undefined>;
const factories: Factory[] = [node.tryCreateDelegate, golang.tryCreateDelegate];
// Note: golang has been removed from delegates because it does not work and it
// is not worth having an experiment for yet.
const factories: Factory[] = [node.tryCreateDelegate];

/**
*
Expand Down
5 changes: 3 additions & 2 deletions src/deploy/index.ts
Expand Up @@ -7,7 +7,7 @@ import { logBullet, logSuccess, consoleUrl, addSubdomain } from "../utils";
import { FirebaseError } from "../error";
import { track } from "../track";
import { lifecycleHooks } from "./lifecycleHooks";
import { previews } from "../previews";
import * as experiments from "../experiments";
import * as HostingTarget from "./hosting";
import * as DatabaseTarget from "./database";
import * as FirestoreTarget from "./firestore";
Expand Down Expand Up @@ -56,9 +56,10 @@ export const deploy = async function (
const postdeploys: Chain = [];
const startTime = Date.now();

if (previews.frameworkawareness && targetNames.includes("hosting")) {
if (targetNames.includes("hosting")) {
const config = options.config.get("hosting");
if (Array.isArray(config) ? config.some((it) => it.source) : config.source) {
experiments.assertEnabled("frameworkawareness", "deploy a web framework to hosting");
await prepareFrameworks(targetNames, context, options);
}
}
Expand Down

0 comments on commit c3ec450

Please sign in to comment.