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

Migrate extensions commands to use registry APIs #3273

Merged
merged 24 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d38ce3f
Add publisher to registry file fields (#686)
huangjeff5 Mar 3, 2021
f9da303
Merge branch 'master' into launch.extensions-registry-migration
huangjeff5 Mar 3, 2021
0c6ef31
Migrate ext:update ref-based flow to include update warnings + min ex…
huangjeff5 Mar 23, 2021
1bd4416
Migrate ext:install flow to install via extension reference and remov…
huangjeff5 Mar 24, 2021
f2166f0
Remove EAP-specific copy in ext:dev:register and ext:dev:publish comm…
huangjeff5 Mar 24, 2021
2f91305
Warn user that unpublishing is final in ext:dev:unpublish command (#693)
huangjeff5 Mar 24, 2021
8c6b383
Add extMinVersion flag to ext:dev:unpublish command (#698)
huangjeff5 Mar 24, 2021
e328238
Migrate ext:info flow to retrieve spec from Registry API (#683)
huangjeff5 Mar 24, 2021
3b1d58b
Merge branch 'master' into launch.extensions-registry-migration
huangjeff5 Mar 25, 2021
1d14085
Merge branch 'master' into launch.extensions-registry-migration
huangjeff5 Mar 26, 2021
5afc3cf
Migrate "author" terminology to use "publisher" in Extensions CLI com…
huangjeff5 Mar 29, 2021
c223b50
Fix bug in confirmInstallByReference and refactor ext:install error m…
huangjeff5 Mar 31, 2021
3640c7e
Fix local path detection logic and refactor warnings logic in ext:upd…
huangjeff5 Apr 5, 2021
68ddb09
Migrate extensions warnings relating to audiences to use (backend) la…
huangjeff5 Apr 6, 2021
35769b0
Only infer firebase if publisher not provided as part of user input i…
huangjeff5 Apr 6, 2021
53520eb
Adds new warning prompt for non-trusted publishers during ext:install…
joehan Apr 6, 2021
a0047af
no please
joehan Apr 7, 2021
34b9c8a
Update copy to link user to documentation on ext:install flow if inpu…
huangjeff5 Apr 8, 2021
f0e90a4
Adds console install link to ext:dev:publish (#709)
joehan Apr 9, 2021
7d6e6db
Add firebase ext:dev:delete command to CLI (#712)
elvisun Apr 13, 2021
eb82b08
Merge branch 'master' into launch.extensions-registry-migration
joehan Apr 13, 2021
df82d16
adds changelog
joehan Apr 13, 2021
0e9ec53
formats
joehan Apr 13, 2021
4db8ed7
Merge branch 'master' into launch.extensions-registry-migration
joehan Apr 13, 2021
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Migrates Firebase Extensions commands to use registry API.
58 changes: 58 additions & 0 deletions src/commands/ext-dev-extension-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as utils from "../utils";
import * as clc from "cli-color";

import { Command } from "../command";
import { logPrefix } from "../extensions/extensionsHelper";
import { parseRef, getExtension, deleteExtension } from "../extensions/extensionsApi";
import { promptOnce } from "../prompt";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";

module.exports = new Command("ext:dev:delete <extensionRef>")
.description("delete an extension")
.help(
"use this command to delete an extension, and make it unavailable for developers to install or reconfigure. " +
"Specify the extension you want to delete using the format '<publisherId>/<extensionId>."
)
.before(requireAuth)
.before(checkMinRequiredVersion, "extDevMinVersion")
.action(async (extensionRef: string) => {
const { publisherId, extensionId, version } = parseRef(extensionRef);
if (version) {
throw new FirebaseError(
`Deleting a single version is not currently supported. You can only delete ${clc.bold(
"ALL versions"
)} of an extension. To delete all versions, please remove the version from the reference.`
);
}
utils.logLabeledWarning(
logPrefix,
"If you delete this extension, developers won't be able to install it. " +
"For developers who currently have this extension installed, " +
"it will continue to run and will appear as unpublished when " +
"listed in the Firebase console or Firebase CLI."
);
utils.logLabeledWarning(
"This is a permanent action",
`Once deleted, you may never use the extension name '${clc.bold(extensionId)}' again.`
);
await getExtension(extensionRef);
const consent = await confirmDelete(publisherId, extensionId);
if (!consent) {
throw new FirebaseError("deletion cancelled.");
}
await deleteExtension(extensionRef);
utils.logLabeledSuccess(logPrefix, "successfully deleted all versions of this extension.");
});

async function confirmDelete(publisherId: string, extensionId: string): Promise<string> {
const message = `You are about to delete ALL versions of ${clc.green(
`${publisherId}/${extensionId}`
)}.\nDo you wish to continue? `;
return await promptOnce({
type: "confirm",
message,
default: false, // Force users to explicitly type 'yes'
});
}
17 changes: 14 additions & 3 deletions src/commands/ext-dev-publish.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import * as clc from "cli-color";
import * as marked from "marked";
import TerminalRenderer = require("marked-terminal");

import { Command } from "../command";
import { publishExtensionVersionFromLocalSource } from "../extensions/extensionsHelper";
import { publishExtensionVersionFromLocalSource, logPrefix } from "../extensions/extensionsHelper";
import { parseRef } from "../extensions/extensionsApi";

import { findExtensionYaml } from "../extensions/localHelper";
import { consoleInstallLink } from "../extensions/publishHelpers";
import { requireAuth } from "../requireAuth";
import * as clc from "cli-color";
import { FirebaseError } from "../error";
import * as utils from "../utils";

marked.setOptions({
renderer: new TerminalRenderer(),
});

/**
* Command for publishing an extension version.
Expand Down Expand Up @@ -40,5 +48,8 @@ export default new Command("ext:dev:publish <extensionRef>")
extensionId,
extensionYamlDirectory
);
if (res) {
utils.logLabeledBullet(logPrefix, marked(`[Install Link](${consoleInstallLink(res.ref)})`));
}
return res;
});
2 changes: 1 addition & 1 deletion src/commands/ext-dev-register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default new Command("ext:dev:register")
const msg =
"What would you like to register as your publisher ID? " +
"This value identifies you in Firebase's registry of extensions as the author of your extensions. " +
"Examples: my-company-name, MyGitHubUsername. If you are a member of the Extensions EAP group, your published extensions will only be accessible to other members of the EAP group. \n\n" +
"Examples: my-company-name, MyGitHubUsername.\n\n" +
"You can only do this once for each project.";
const publisherId = await promptOnce({
name: "publisherId",
Expand Down
13 changes: 10 additions & 3 deletions src/commands/ext-dev-unpublish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { promptOnce } from "../prompt";
import * as clc from "cli-color";
import { requireAuth } from "../requireAuth";
import { FirebaseError } from "../error";
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";

module.exports = new Command("ext:dev:unpublish <extensionRef>")
.description("unpublish an extension")
Expand All @@ -14,11 +15,17 @@ module.exports = new Command("ext:dev:unpublish <extensionRef>")
"Specify the extension you want to unpublish using the format '<publisherId>/<extensionId>."
)
.before(requireAuth)
.before(checkMinRequiredVersion, "extDevMinVersion")
.action(async (extensionRef: string) => {
const { publisherId, extensionId, version } = parseRef(extensionRef);
const message =
"If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI.";
utils.logLabeledWarning(logPrefix, message);
utils.logLabeledWarning(
logPrefix,
"If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI."
);
utils.logLabeledWarning(
"This is a permanent action",
`Once unpublished, you may never use the extension name '${clc.bold(extensionId)}' again.`
);
if (version) {
throw new FirebaseError(
`Unpublishing a single version is not currently supported. You can only unpublish ${clc.bold(
Expand Down
20 changes: 13 additions & 7 deletions src/commands/ext-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as _ from "lodash";

import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
import { Command } from "../command";
import { resolveRegistryEntry, resolveSourceUrl } from "../extensions/resolveSource";
import { resolveRegistryEntry } from "../extensions/resolveSource";
import * as extensionsApi from "../extensions/extensionsApi";
import { ensureExtensionsApiEnabled, logPrefix } from "../extensions/extensionsHelper";
import { isLocalExtension, getLocalExtensionSpec } from "../extensions/localHelper";
Expand Down Expand Up @@ -31,12 +31,18 @@ export default new Command("ext:info <extensionName>")
} else {
await requirePermissions(options, ["firebaseextensions.sources.get"]);
await ensureExtensionsApiEnabled(options);

const [name, version] = extensionName.split("@");
const registryEntry = await resolveRegistryEntry(name);
const sourceUrl = resolveSourceUrl(registryEntry, name, version);
const source = await extensionsApi.getSource(sourceUrl);
spec = source.spec;
const hasPublisherId = extensionName.split("/").length >= 2;
if (hasPublisherId) {
const nameAndVersion = extensionName.split("/")[1];
if (nameAndVersion.split("@").length < 2) {
extensionName = extensionName + "@latest";
}
} else {
const [name, version] = extensionName.split("@");
extensionName = `firebase/${name}@${version || "latest"}`;
}
const version = await extensionsApi.getExtensionVersion(extensionName);
spec = version.spec;
}

if (!options.markdown) {
Expand Down
177 changes: 87 additions & 90 deletions src/commands/ext-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,23 @@ import { Command } from "../command";
import { FirebaseError } from "../error";
import * as getProjectId from "../getProjectId";
import * as extensionsApi from "../extensions/extensionsApi";
import {
promptForAudienceConsent,
resolveRegistryEntry,
resolveSourceUrl,
} from "../extensions/resolveSource";
import { promptForLaunchStageConsent } from "../extensions/resolveSource";
import * as paramHelper from "../extensions/paramHelper";
import {
confirmInstallInstance,
createSourceFromLocation,
ensureExtensionsApiEnabled,
getSourceOrigin,
instanceIdExists,
logPrefix,
promptForOfficialExtension,
promptForRepeatInstance,
promptForValidInstanceId,
SourceOrigin,
isLocalOrURLPath,
} from "../extensions/extensionsHelper";
import { getRandomString } from "../extensions/utils";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { previews } from "../previews";

marked.setOptions({
Expand Down Expand Up @@ -144,6 +138,64 @@ async function installExtension(options: InstallExtensionOptions): Promise<void>
}
}

async function confirmInstallBySource(
projectId: string,
extensionName: string
): Promise<extensionsApi.ExtensionSource> {
// Create a one off source to use for the install flow.
let source;
try {
source = await createSourceFromLocation(projectId, extensionName);
} catch (err) {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}', ` +
`and encountered the following error when trying to create an instance of extension '${clc.bold(
extensionName
)}':\n ${err.message}`
);
}
displayExtInfo(extensionName, "", source.spec);
const confirm = await confirmInstallInstance();
if (!confirm) {
throw new FirebaseError("Install cancelled.");
}
return source;
}

async function confirmInstallByReference(
extensionName: string
): Promise<extensionsApi.ExtensionVersion> {
// Infer firebase if publisher ID not provided.
if (extensionName.split("/").length < 2) {
const [extensionID, version] = extensionName.split("@");
extensionName = `firebase/${extensionID}@${version || "latest"}`;
}
// Get the correct version for a given extension reference from the Registry API.
const ref = extensionsApi.parseRef(extensionName);
const extension = await extensionsApi.getExtension(`${ref.publisherId}/${ref.extensionId}`);
if (!ref.version) {
extensionName = `${extensionName}@latest`;
}
const extVersion = await extensionsApi.getExtensionVersion(extensionName);
displayExtInfo(extensionName, ref.publisherId, extVersion.spec, true);
const confirm = await confirmInstallInstance();
if (!confirm) {
throw new FirebaseError("Install cancelled.");
}
const audienceConsent = await promptForLaunchStageConsent(extension.registryLaunchStage);
if (!audienceConsent) {
throw new FirebaseError("Install cancelled.");
}
const eapPublisherConsent = await askUserForConsent.checkAndPromptForEapPublisher(
ref.publisherId,
extVersion.spec?.sourceUrl
);
if (!eapPublisherConsent) {
throw new FirebaseError("Install cancelled.");
}
return extVersion;
}

/**
* Command for installing an extension
*/
Expand All @@ -170,80 +222,29 @@ export default new Command("ext:install [extensionName]")
"Which official extension do you wish to install?\n" +
" Select an extension, then press Enter to learn more."
);
} else {
throw new FirebaseError(
`Please provide an extension name, or run ${clc.bold(
"firebase ext:install -i"
)} to select from the list of all available official extensions.`
);
}
}

const [name, version] = extensionName.split("@");
let source;
let extVersion;
try {
const registryEntry = await resolveRegistryEntry(name);
const sourceUrl = resolveSourceUrl(registryEntry, name, version);
source = await extensionsApi.getSource(sourceUrl);
displayExtInfo(extensionName, source.spec, true);
await confirmInstallInstance();
const audienceConsent = await promptForAudienceConsent(registryEntry);
if (!audienceConsent) {
logger.info("Install cancelled.");
return;
}
} catch (err) {
if (previews.extdev) {
const sourceOrigin = await getSourceOrigin(extensionName);
switch (sourceOrigin) {
case SourceOrigin.LOCAL || SourceOrigin.URL: {
try {
source = await createSourceFromLocation(projectId, extensionName);
} catch (err) {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}', ` +
`and encountered the following error when trying to create an instance of extension '${clc.bold(
extensionName
)}':\n ${err.message}`
);
}
displayExtInfo(extensionName, source.spec);
await confirmInstallInstance();
break;
}
case SourceOrigin.PUBLISHED_EXTENSION: {
await extensionsApi.getExtension(extensionName);
extVersion = await extensionsApi.getExtensionVersion(`${extensionName}@latest`);
displayExtInfo(extensionName, extVersion.spec, true);
await confirmInstallInstance();
break;
}
case SourceOrigin.PUBLISHED_EXTENSION_VERSION: {
extVersion = await extensionsApi.getExtensionVersion(`${extensionName}`);
displayExtInfo(extensionName, extVersion.spec, true);
await confirmInstallInstance();
break;
}
default: {
throw new FirebaseError(
`Could not determine source origin for extension '${extensionName}'. If this is a published extension, ` +
"please make sure the publisher and extension exist before trying again. If trying to create an extension, " +
"please ensure the path or URL given is valid."
);
}
}
} else {
throw new FirebaseError(
`Unable to find published extension '${clc.bold(extensionName)}'. ` +
`Run ${clc.bold(
"firebase ext:install -i"
)} to select from the list of all available published extensions.`,
{ original: err }
)} to select from the list of all available published extensions.`
);
}
}

let source;
let extVersion;
// If the user types in URL, or a local path (prefixed with ~/, ../, or ./), install from local/URL source.
// Otherwise, treat the input as an extension reference and proceed with reference-based installation.
if (isLocalOrURLPath(extensionName)) {
source = await confirmInstallBySource(projectId, extensionName);
} else {
extVersion = await confirmInstallByReference(extensionName);
}
if (!source && !extVersion) {
throw new FirebaseError(
"Could not find a source. Please specify a valid source to continue."
);
}
const spec = source?.spec || extVersion?.spec;
if (!spec) {
throw new FirebaseError(
Expand All @@ -252,23 +253,19 @@ export default new Command("ext:install [extensionName]")
)}'. Please make sure this is a valid extension and try again.`
);
}
try {
if (learnMore) {
utils.logLabeledBullet(
logPrefix,
`You selected: ${clc.bold(spec.displayName)}.\n` +
`${spec.description}\n` +
`View details: https://firebase.google.com/products/extensions/${name}\n`
);
const confirm = await promptOnce({
type: "confirm",
default: true,
message: "Do you wish to install this extension?",
});
if (!confirm) {
return;
}
if (learnMore) {
utils.logLabeledBullet(
logPrefix,
`You selected: ${clc.bold(spec.displayName)}.\n` +
`${spec.description}\n` +
`View details: https://firebase.google.com/products/extensions/${spec.name}\n`
);
const confirm = await confirmInstallInstance();
if (!confirm) {
return;
}
}
try {
return installExtension({
paramFilePath,
projectId,
Expand Down
Loading