Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ USAGE
* [`mw autocomplete`](docs/autocomplete.md) - Display autocomplete installation instructions.
* [`mw backup`](docs/backup.md) - Manage backups of your projects
* [`mw context`](docs/context.md) - Save certain environment parameters for later use
* [`mw contributor`](docs/contributor.md) - Commands for mStudio marketplace contributors
* [`mw conversation`](docs/conversation.md) - Manage your support cases
* [`mw cronjob`](docs/cronjob.md) - Manage cronjobs of your projects
* [`mw database`](docs/database.md) - Manage databases (like MySQL and Redis) in your projects
Expand Down
112 changes: 112 additions & 0 deletions docs/contributor.md
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.
```
131 changes: 131 additions & 0 deletions src/commands/contributor/extension/deploy.tsx
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;
}
}
67 changes: 67 additions & 0 deletions src/commands/contributor/extension/init.tsx
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;
}
}
51 changes: 51 additions & 0 deletions src/commands/contributor/extension/publish.tsx
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";

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

In theory, we should be able to observe the verified/verificationRequested fields, of the extension, shouldn't we? In this case, we could present an appropriate warning after the deploy command when the user needs to take (additional, and/or atm. manual) verification steps. 🤔


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;
}
}
Loading