Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Experiments feature #4994

Merged
merged 19 commits into from Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One detail that can be punted on until later: for --json (machine readable) output, we could export the table as a JSON-ish object (just return an object in this method). Would that be easy enough to do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't get this to work. You'll have to teach me how --json works with the commands library

});
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")) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Berlioz Does this still need to be hidden behind an experiment or do we think we are stable enough to roll forward? (If not, can we leave a comment on when this should be rolled forward?)

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")) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheIronDev When does this experiment go away?

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")) {
inlined marked this conversation as resolved.
Show resolved Hide resolved
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