Skip to content

Commit 4b09997

Browse files
authored
feat: run deploy hooks (#54)
* feat: run deploy hooks * chore: update command snapshot * fix: work with Deployables * chore: bump oclif/core * chore: bump plugin-project-utils * fix: use latest plugin-project-utils * chore: add docs * fix: set max listeners * feat: handle non-interactive scenario * chore: code review * chore: bump plugin-command-snapshot * chore: bump plugin-command-snapshot
1 parent 11b43d5 commit 4b09997

File tree

10 files changed

+2971
-149
lines changed

10 files changed

+2971
-149
lines changed

CONTRIBUTING.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Add a new project command
2+
3+
The project plugin does not have any commands for deploying or or retrieving specific pieces of a salesforce project (e.g. metadata to a scratch or functions to a compute environment). Instead, we ask developers to create their own plugin with those commands. In order for the `project deploy` or `project retrieve` command to know about the individual plugins, each plugin must implement an [oclif hook](https://oclif.io/docs/hooks) which returns [`Deployers`](https://github.com/salesforcecli/plugin-project-utils/blob/main/src/deployer.ts) and `Retirevers`.
4+
5+
This method allows developers to own their own plugins while also allowing a simple way for the overarching `project` topic to interact with those plugins.
6+
7+
## Deploy
8+
9+
To implement the oclif hook for the deploy commands, you must implement a `project:findDeployers` hook in your project. To do this you need to specify the location of the hook in your package.json and implement the hook. For example, in your package.json
10+
11+
```json
12+
"oclif": {
13+
"hooks": {
14+
"project:findDeployers": "./lib/hooks/findDeployers"
15+
}
16+
}
17+
```
18+
19+
And then in your code, you'll have a `findDeployers.ts` file under the `hooks` directory. Here's an example of a very basic implementation:
20+
21+
```typescript
22+
// hooks/findDeployers.ts
23+
import { Deployer, Options } from '@salesforce/plugin-project-utils';
24+
25+
const hook = async function (options: Options): Promise<Deployer[]> {
26+
return []; // return your Deployers here
27+
};
28+
29+
export default hook;
30+
```
31+
32+
### Deployers
33+
34+
The [Deployer class](https://github.com/salesforcecli/plugin-project-utils/blob/main/src/deployer.ts) is a simple interface for setting up and executing deploys. It has two primary methods, `setup` and `deploy`.
35+
36+
The `setup` method allows developers to do any setup that's required before a deploy can be executed. This could be anything - in some cases you might need to prompt the user for additional information or in other cases you might need to do some environment setup.
37+
38+
The `deploy` method is where you will write the code that executes the deploy. Again, developers have full autonomy over how this is implemented.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ sf plugins:install [email protected]
2222
8. Sign CLA (see [CLA](#cla) below).
2323
9. Send us a pull request when you are done. We'll review your code, suggest any needed changes, and merge it in.
2424

25+
To add a new project command see the [contributing guide](CONTRIBUTING.md)
26+
2527
### CLA
2628

2729
External contributors will be required to sign a Contributor's License
@@ -75,10 +77,10 @@ OPTIONS
7577
--target-env=target-env TBD
7678
7779
DESCRIPTION
78-
Deploy a project, including org metadata and functions. Be default, the deploy analyze your project and assume
79-
sensible defaults when possible, otherwise it will prompt. To always prompt and not assume defaults, use
80+
Deploy a project, including org metadata and functions. Be default, the deploy analyze your project and assume
81+
sensible defaults when possible, otherwise it will prompt. To always prompt and not assume defaults, use
8082
"--interctive".
81-
To run specialized deploys, especially when interactivity isn't an option like continuous deployment, used the scoped
83+
To run specialized deploys, especially when interactivity isn't an option like continuous deployment, used the scoped
8284
deploy commands like "sf project deploy org" or "sf project deploy functions"
8385
8486
EXAMPLES

command-snapshot.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
[
2-
{
3-
"command": "project:deploy",
4-
"plugin": "@salesforce/plugin-project",
5-
"flags": ["directory", "interactive", "target-env"]
6-
}
7-
]
2+
{
3+
"command": "project:deploy",
4+
"plugin": "@salesforce/plugin-project",
5+
"flags": [
6+
"interactive"
7+
]
8+
}
9+
]

messages/messages.json

Lines changed: 0 additions & 22 deletions
This file was deleted.

messages/project.deploy.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# summary
2+
3+
Deploy a project interactively to any Salesforce environment.
4+
5+
# description
6+
7+
The command first analyzes your project, active or logged-into environments, and local defaults to determine what to deploy and where. The command then prompts you for information about this particular deployment and provides intelligent choices based on its analysis.
8+
9+
For example, if your local project contains a package directory with metadata source files, the command asks if you want to deploy that Salesforce app to an org. The command lists your connected orgs and asks which one you want to deploy to. If the command finds Apex tests, it asks if you want to run them and at which level.
10+
11+
Similarly, if the command finds a local functions directory, the command prompts if you want to deploy it and to which compute environment. The command prompts and connects you to a compute environment of your choice if you’re not currently connected to any.
12+
13+
This command must be run from within a project.
14+
15+
The command stores your responses in a local file and uses them as defaults when you rerun the command. Specify --interactive to force the command to reprompt.
16+
17+
Use this command for quick and simple deploys. For more complicated deployments, use the environment-specific commands, such as "sf project deploy org", that provide additional flags.
18+
19+
# examples
20+
21+
- <%= config.bin %> <%= command.id %>
22+
23+
# flags.interactive.summary
24+
25+
Force the CLI to prompt for all deployment inputs.

messages/project.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

package.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
"author": "Salesforce",
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
8-
"@oclif/core": "^0.5.0",
9-
"@salesforce/core": "^2.23.1",
8+
"@oclif/core": "^0.5.19",
9+
"@salesforce/core": "^3.1.1-v3.1",
10+
"@salesforce/plugin-project-utils": "^0.0.5",
1011
"tslib": "^2"
1112
},
1213
"devDependencies": {
13-
"@oclif/dev-cli": "^1",
14-
"@oclif/plugin-command-snapshot": "^2.1.1",
14+
"@oclif/dev-cli": "^1.26.0",
15+
"@oclif/plugin-command-snapshot": "^2.2.2",
1516
"@salesforce/cli-plugins-testkit": "^1.1.5",
1617
"@salesforce/dev-config": "^2.1.2",
1718
"@salesforce/dev-scripts": "^0.9.14",
1819
"@salesforce/plugin-command-reference": "^1.3.0",
20+
"@salesforce/plugin-functions": "^0.2.13",
21+
"@salesforce/plugin-project-org": "^0.0.1",
1922
"@salesforce/prettier-config": "^0.0.2",
2023
"@salesforce/ts-sinon": "1.3.18",
2124
"@typescript-eslint/eslint-plugin": "^4.2.0",
@@ -73,7 +76,9 @@
7376
"devPlugins": [
7477
"@oclif/plugin-help",
7578
"@oclif/plugin-command-snapshot",
76-
"@salesforce/plugin-command-reference"
79+
"@salesforce/plugin-command-reference",
80+
"@salesforce/plugin-project-org",
81+
"@salesforce/plugin-functions"
7782
],
7883
"topics": {
7984
"project": {

schemas/project-deploy.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/commands/project/deploy.ts

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,103 @@
11
/*
2-
* Copyright (c) 2020, salesforce.com, inc.
2+
* Copyright (c) 2021, salesforce.com, inc.
33
* All rights reserved.
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import * as os from 'os';
98
import { Command, Flags } from '@oclif/core';
10-
import { Messages } from '@salesforce/core';
9+
import { fs, Messages } from '@salesforce/core';
10+
import { Env } from '@salesforce/kit';
11+
import { Deployable, Deployer, generateTableChoices, Prompter } from '@salesforce/plugin-project-utils';
1112

1213
Messages.importMessagesDirectory(__dirname);
1314

14-
const messages = Messages.loadMessages('@salesforce/plugin-project', 'project');
15-
16-
export type DeployResult = Record<string, unknown>;
15+
const messages = Messages.loadMessages('@salesforce/plugin-project', 'project.deploy');
1716

1817
export default class ProjectDeploy extends Command {
19-
public static description = messages.getMessage('deploy.commandDescription');
20-
21-
public static examples = messages.getMessage('deploy.examples').split(os.EOL);
18+
public static summary = messages.getMessage('summary');
19+
public static description = messages.getMessage('description');
20+
public static examples = messages.getMessages('examples');
21+
public static disableJsonFlag = true;
2222

2323
public static flags = {
24-
directory: Flags.string({
25-
description: 'directory to deploy',
26-
}),
27-
'target-env': Flags.string({
28-
description: 'TBD',
29-
multiple: true,
30-
}),
3124
interactive: Flags.boolean({
32-
description: 'TBD',
25+
summary: messages.getMessage('flags.interactive.summary'),
3326
}),
3427
};
3528

36-
public async run(): Promise<DeployResult> {
29+
public async run(): Promise<void> {
30+
process.setMaxListeners(new Env().getNumber('SF_MAX_EVENT_LISTENERS') || 1000);
3731
const { flags } = await this.parse(ProjectDeploy);
38-
this.log(JSON.stringify(flags));
39-
return {};
32+
33+
flags.interactive = await this.isInteractive(flags.interactive);
34+
35+
this.log('Analyzing project');
36+
37+
let deployers = (await this.config.runHook('project:findDeployers', {})) as Deployer[];
38+
deployers = deployers.reduce((x, y) => x.concat(y), [] as Deployer[]);
39+
40+
if (deployers.length === 0) {
41+
this.log('Found nothing in the project to deploy');
42+
} else {
43+
deployers = await this.selectDeployers(deployers);
44+
45+
if (deployers.length === 0) {
46+
this.log('Nothing was selected to deploy.');
47+
}
48+
49+
for (const deployer of deployers) {
50+
await deployer.setup(flags);
51+
await deployer.deploy();
52+
}
53+
}
54+
}
55+
56+
/**
57+
* If the deploy file exists, we do not want the command to be interactive. But if the file
58+
* does not exist then we want to force the command to be interactive.
59+
*/
60+
public async isInteractive(interactive: boolean): Promise<boolean> {
61+
if (interactive) return true;
62+
const deployFileExists = await fs.fileExists('project-deploy-options.json');
63+
return deployFileExists ? false : true;
64+
}
65+
66+
public async selectDeployers(deployers: Deployer[]): Promise<Deployer[]> {
67+
const deployables: Deployable[] = deployers.reduce((x, y) => x.concat(y.deployables), [] as Deployable[]);
68+
const columns = { name: 'APP OR PACKAGE', type: 'TYPE', path: 'PATH' };
69+
const options = deployables.map((deployable) => ({
70+
name: deployable.getAppName(),
71+
type: deployable.getAppType(),
72+
path: deployable.getAppPath(),
73+
value: deployable,
74+
}));
75+
const prompter = new Prompter();
76+
const responses = await prompter.prompt<{ deployables: Deployable[] }>([
77+
{
78+
name: 'deployables',
79+
message: 'Select apps and packages to deploy:',
80+
type: 'checkbox',
81+
choices: generateTableChoices<Deployable>(columns, options),
82+
},
83+
]);
84+
85+
const chosenDeployers: Map<Deployer, Deployable[]> = new Map();
86+
for (const deployable of responses.deployables) {
87+
const parent = deployable.getParent();
88+
if (chosenDeployers.has(parent)) {
89+
const existing = chosenDeployers.get(parent) || [];
90+
chosenDeployers.set(parent, [...existing, deployable]);
91+
} else {
92+
chosenDeployers.set(parent, [deployable]);
93+
}
94+
}
95+
96+
const final: Deployer[] = [];
97+
for (const [parent, children] of chosenDeployers.entries()) {
98+
parent.selectDeployables(children);
99+
final.push(parent);
100+
}
101+
return final;
40102
}
41103
}

0 commit comments

Comments
 (0)