-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add commands for publishing extensions #1140
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
base: master
Are you sure you want to change the base?
Changes from all commits
527e727
7850884
c27e6d0
1b4f71f
5826ccd
f06a460
b710517
40a851b
a381096
8dacee1
ec1c296
a2bcc75
4f67334
c0f50ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| `mw contributor` | ||
| ================ | ||
|
|
||
| Commands for mStudio marketplace contributors | ||
|
|
||
| * [`mw contributor extension deploy EXTENSION-MANIFEST`](#mw-contributor-extension-deploy-extension-manifest) | ||
| * [`mw contributor extension init EXTENSION-MANIFEST`](#mw-contributor-extension-init-extension-manifest) | ||
| * [`mw contributor extension publish EXTENSION-MANIFEST`](#mw-contributor-extension-publish-extension-manifest) | ||
| * [`mw contributor extension withdraw EXTENSION-MANIFEST`](#mw-contributor-extension-withdraw-extension-manifest) | ||
|
|
||
| ## `mw contributor extension deploy EXTENSION-MANIFEST` | ||
|
|
||
| Deploy an extension manifest to the marketplace | ||
|
|
||
| ``` | ||
| USAGE | ||
| $ mw contributor extension deploy EXTENSION-MANIFEST [-q] [--create] | ||
|
|
||
| ARGUMENTS | ||
| EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON) | ||
|
|
||
| FLAGS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
| --[no-]create create the extension if it does not exist | ||
|
|
||
| DESCRIPTION | ||
| Deploy an extension manifest to the marketplace | ||
|
|
||
| FLAG DESCRIPTIONS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
|
|
||
| This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in | ||
| scripts), you can use this flag to easily get the IDs of created resources for further processing. | ||
| ``` | ||
|
|
||
| ## `mw contributor extension init EXTENSION-MANIFEST` | ||
|
|
||
| Init a new extension manifest file | ||
|
|
||
| ``` | ||
| USAGE | ||
| $ mw contributor extension init EXTENSION-MANIFEST [-q] [--overwrite] | ||
|
|
||
| ARGUMENTS | ||
| EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON) | ||
|
|
||
| FLAGS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
| --overwrite overwrite an existing extension manifest if found | ||
|
|
||
| DESCRIPTION | ||
| Init a new extension manifest file | ||
|
|
||
| This command will create a new extension manifest file. It only operates on your local file system; afterwards, use | ||
| the 'deploy' command to upload the manifest to the marketplace. | ||
|
|
||
| FLAG DESCRIPTIONS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
|
|
||
| This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in | ||
| scripts), you can use this flag to easily get the IDs of created resources for further processing. | ||
| ``` | ||
|
|
||
| ## `mw contributor extension publish EXTENSION-MANIFEST` | ||
|
|
||
| Publish an extension on the marketplace | ||
|
|
||
| ``` | ||
| USAGE | ||
| $ mw contributor extension publish EXTENSION-MANIFEST [-q] | ||
|
|
||
| ARGUMENTS | ||
| EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON) | ||
|
|
||
| FLAGS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
|
|
||
| DESCRIPTION | ||
| Publish an extension on the marketplace | ||
|
|
||
| FLAG DESCRIPTIONS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
|
|
||
| This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in | ||
| scripts), you can use this flag to easily get the IDs of created resources for further processing. | ||
| ``` | ||
|
|
||
| ## `mw contributor extension withdraw EXTENSION-MANIFEST` | ||
|
|
||
| Withdraw an extension from the marketplace | ||
|
|
||
| ``` | ||
| USAGE | ||
| $ mw contributor extension withdraw EXTENSION-MANIFEST --reason <value> [-q] [-f] | ||
|
|
||
| ARGUMENTS | ||
| EXTENSION-MANIFEST [default: ./mstudio-extension.yaml] file path to the extension manifest (as YAML or JSON) | ||
|
|
||
| FLAGS | ||
| -f, --force do not ask for confirmation | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
| --reason=<value> (required) reason for withdrawal | ||
|
|
||
| DESCRIPTION | ||
| Withdraw an extension from the marketplace | ||
|
|
||
| FLAG DESCRIPTIONS | ||
| -q, --quiet suppress process output and only display a machine-readable summary. | ||
|
|
||
| This flag controls if you want to see the process output or only a summary. When using mw non-interactively (e.g. in | ||
| scripts), you can use this flag to easily get the IDs of created resources for further processing. | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import React from "react"; | ||
| import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js"; | ||
| import { | ||
| makeProcessRenderer, | ||
| processFlags, | ||
| } from "../../../rendering/process/process_flags.js"; | ||
| import { Flags } from "@oclif/core"; | ||
| import { assertStatus } from "@mittwald/api-client"; | ||
| import { Success } from "../../../rendering/react/components/Success.js"; | ||
| import { | ||
| extensionManifestArg, | ||
| parseExtensionManifest, | ||
| } from "../../../lib/resources/extension/args_contributor.js"; | ||
|
|
||
| const createFlagName = "create"; | ||
|
|
||
| export default class Deploy extends ExecRenderBaseCommand<typeof Deploy, void> { | ||
| static description = "Deploy an extension manifest to the marketplace"; | ||
|
|
||
| static flags = { | ||
| ...processFlags, | ||
| [createFlagName]: Flags.boolean({ | ||
| description: "create the extension if it does not exist", | ||
| default: true, | ||
| allowNo: true, | ||
| }), | ||
| }; | ||
|
|
||
| static args = { | ||
| "extension-manifest": extensionManifestArg({ | ||
| required: true, | ||
| }), | ||
| }; | ||
|
|
||
| protected async exec(): Promise<void> { | ||
| const p = makeProcessRenderer(this.flags, "Updating extension manifest"); | ||
|
|
||
| const manifest = await parseExtensionManifest( | ||
| this.args["extension-manifest"], | ||
| ); | ||
| const { contributorId, id } = manifest; | ||
|
|
||
| const existing = await p.runStep( | ||
| "Retrieving current extension state", | ||
| async () => { | ||
| const response = | ||
| await this.apiClient.marketplace.extensionGetOwnExtension({ | ||
| contributorId, | ||
| extensionId: id, | ||
| }); | ||
|
|
||
| if (response.status === 404) { | ||
| return null; | ||
| } | ||
|
|
||
| assertStatus(response, 200); | ||
|
|
||
| return response.data; | ||
| }, | ||
| ); | ||
|
|
||
| if (existing === null) { | ||
| if (!this.flags[createFlagName]) { | ||
| await p.error( | ||
| `Extension does not exist, use --${createFlagName} to create it`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| await p.runStep("Registering extension", async () => { | ||
| if (manifest.deprecation) { | ||
| throw new Error( | ||
| '"deprecation" is not supported when creating a new extension', | ||
| ); | ||
| } | ||
|
|
||
| await this.apiClient.marketplace.extensionRegisterExtension({ | ||
| contributorId, | ||
|
|
||
| // Note: This mapping step is necessary because the API apparently | ||
| // does not like additional attributes which may be present in the | ||
| // manifest file. Also, the input formats differ slightly for the | ||
| // POST and PATCH endpoints. | ||
| data: { | ||
| description: manifest.description, | ||
| detailedDescriptions: manifest.detailedDescriptions, | ||
| externalFrontends: manifest.externalFrontends, | ||
| frontendFragments: manifest.frontendFragments, | ||
| name: manifest.name, | ||
| scopes: manifest.scopes, | ||
| subTitle: manifest.subTitle, | ||
| support: manifest.support, | ||
| tags: manifest.tags, | ||
| webhookURLs: manifest.webhookUrls, | ||
| }, | ||
| }); | ||
| }); | ||
| } else { | ||
| await p.runStep("Updating extension", async () => { | ||
| await this.apiClient.marketplace.extensionPatchExtension({ | ||
| extensionId: manifest.id, | ||
| contributorId: manifest.contributorId, | ||
|
|
||
| // Note: This mapping step is necessary because the API apparently | ||
| // does not like additional attributes which may be present in the | ||
| // manifest file. Also, the input formats differ slightly for the | ||
| // POST and PATCH endpoints. | ||
| data: { | ||
| deprecation: manifest.deprecation, | ||
| description: manifest.description, | ||
| detailedDescriptions: manifest.detailedDescriptions, | ||
| externalFrontends: manifest.externalFrontends, | ||
| frontendFragments: manifest.frontendFragments, | ||
| name: manifest.name, | ||
| scopes: manifest.scopes, | ||
| subTitle: manifest.subTitle, | ||
| support: manifest.support, | ||
| tags: manifest.tags, | ||
| webhookUrls: manifest.webhookUrls, | ||
| }, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| await p.complete(<Success>Extension deployed successfully</Success>); | ||
| } | ||
|
|
||
| protected render(): React.ReactNode { | ||
| return undefined; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import React from "react"; | ||
| import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js"; | ||
| import { | ||
| makeProcessRenderer, | ||
| processFlags, | ||
| } from "../../../rendering/process/process_flags.js"; | ||
| import { Flags } from "@oclif/core"; | ||
| import { Success } from "../../../rendering/react/components/Success.js"; | ||
| import { extensionManifestArg } from "../../../lib/resources/extension/args_contributor.js"; | ||
| import { writeFile } from "fs/promises"; | ||
| import { Value } from "../../../rendering/react/components/Value.js"; | ||
| import { pathExists } from "../../../lib/util/fs/pathExists.js"; | ||
| import { generateInitialExtensionManifest } from "../../../lib/resources/extension/init.js"; | ||
| import { ManifestAlreadyExistsError } from "../../../lib/resources/extension/init_error.js"; | ||
|
|
||
| const overwriteFlagName = "overwrite"; | ||
|
|
||
| export default class Init extends ExecRenderBaseCommand<typeof Init, void> { | ||
| static summary = "Init a new extension manifest file"; | ||
| static description = | ||
| "This command will create a new extension manifest file. It only operates on your local file system; afterwards, use the 'deploy' command to upload the manifest to the marketplace."; | ||
|
|
||
| static flags = { | ||
| ...processFlags, | ||
| [overwriteFlagName]: Flags.boolean({ | ||
| description: "overwrite an existing extension manifest if found", | ||
| default: false, | ||
| }), | ||
| }; | ||
|
|
||
| static args = { | ||
| "extension-manifest": extensionManifestArg({ | ||
| required: true, | ||
| }), | ||
| }; | ||
|
|
||
| protected async exec(): Promise<void> { | ||
| const p = makeProcessRenderer( | ||
| this.flags, | ||
| "Initializing extension manifest", | ||
| ); | ||
|
|
||
| const { overwrite } = this.flags; | ||
| const target = this.args["extension-manifest"]; | ||
|
|
||
| await p.runStep("generating extension manifest file", async () => { | ||
| const renderedConfiguration = generateInitialExtensionManifest(); | ||
| const manifestAlreadyExists = await pathExists(target); | ||
|
|
||
| if (manifestAlreadyExists && !overwrite) { | ||
| throw new ManifestAlreadyExistsError(target, overwriteFlagName); | ||
| } | ||
|
|
||
| await writeFile(target, renderedConfiguration); | ||
| }); | ||
|
|
||
| await p.complete( | ||
| <Success> | ||
| Extension manifest file created at <Value>{target}</Value> | ||
| </Success>, | ||
| ); | ||
| } | ||
|
|
||
| protected render(): React.ReactNode { | ||
| return undefined; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import React from "react"; | ||
| import { ExecRenderBaseCommand } from "../../../lib/basecommands/ExecRenderBaseCommand.js"; | ||
| import { | ||
| makeProcessRenderer, | ||
| processFlags, | ||
| } from "../../../rendering/process/process_flags.js"; | ||
| import { Success } from "../../../rendering/react/components/Success.js"; | ||
| import { | ||
| extensionManifestArg, | ||
| parseExtensionManifest, | ||
| } from "../../../lib/resources/extension/args_contributor.js"; | ||
|
|
||
| export default class Publish extends ExecRenderBaseCommand< | ||
| typeof Publish, | ||
| void | ||
| > { | ||
| static description = "Publish an extension on the marketplace"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The publishing of extensions will only work if verification has been requested and successfully completed beforehand. I believe the corresponding route to request verification has not yet been published
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rely on the API to respond with an appropriate and sensible error message in this case (in which case we could leave this as-is and just present the API error message), or should we catch that condition (successful verification) beforehand client-side? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API will likely respond with a reasonable error message. I just wanted to point out, the current flow will be interrupted by writing some mittwald guys to verify the extension (including starting the verification process) until the corresponding API route is published.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory, we should be able to observe the |
||
|
|
||
| static flags = { | ||
| ...processFlags, | ||
| }; | ||
|
|
||
| static args = { | ||
| "extension-manifest": extensionManifestArg({ required: true }), | ||
| }; | ||
|
|
||
| protected async exec(): Promise<void> { | ||
| const p = makeProcessRenderer(this.flags, "Publishing extension"); | ||
|
|
||
| const manifest = await parseExtensionManifest( | ||
| this.args["extension-manifest"], | ||
| ); | ||
| const { contributorId, id } = manifest; | ||
|
|
||
| await p.runStep("Publishing extension", async () => { | ||
| await this.apiClient.marketplace.extensionSetExtensionPublishedState({ | ||
| contributorId, | ||
| extensionId: id, | ||
| data: { | ||
| published: true, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| await p.complete(<Success>Extension published successfully</Success>); | ||
| } | ||
|
|
||
| protected render(): React.ReactNode { | ||
| return undefined; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.