-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathsf-deploy-metadata.ts
More file actions
212 lines (187 loc) · 7.31 KB
/
sf-deploy-metadata.ts
File metadata and controls
212 lines (187 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { Connection, Org, SfError, SfProject } from '@salesforce/core';
import { SourceTracking } from '@salesforce/source-tracking';
import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve';
import { ensureString } from '@salesforce/ts-types';
import { Duration } from '@salesforce/kit';
import { directoryParam, usernameOrAliasParam } from '../../shared/params.js';
import { textResponse } from '../../shared/utils.js';
import { getConnection } from '../../shared/auth.js';
import { SfMcpServer } from '../../sf-mcp-server.js';
const deployMetadataParams = z.object({
filePaths: z
.array(z.string())
.describe(
`Path to the local source files to deploy. Leave this unset if the user is vague about what to deploy.
All file paths should be relative to the current directory.
`
)
.optional(),
manifest: z.string().describe('Full file path for manifest (XML file) of components to deploy.').optional(),
// `RunSpecifiedTests` is excluded on purpose because the tool sets this level when Apex tests to run are passed in.
//
// Can be left unset to let the org decide which test level to use:
// https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_running_tests.htm
apexTestLevel: z
.enum(['NoTestRun', 'RunLocalTests', 'RunAllTestsInOrg'])
.optional()
.describe(
`Apex test level to use during deployment.
AGENT INSTRUCTIONS
Set this only if the user specifically ask to run apex tests in some of these ways:
NoTestRun="No tests are run"
RunLocalTests="Run all tests in the org, except the ones that originate from installed managed and unlocked packages."
RunAllTestsInOrg="Run all tests in the org, including tests of managed packages"
Don't set this param if "apexTests" is also set.
`
),
apexTests: z
.array(z.string())
.describe(
`Apex tests classes to run.
Set this param if the user ask an Apex test to be run during deployment.
`
)
.optional(),
usernameOrAlias: usernameOrAliasParam,
directory: directoryParam,
});
export type DeployMetadata = z.infer<typeof deployMetadataParams>;
/*
* Deploy metadata to a Salesforce org.
*
* Parameters:
* - sourceDir: Path to the local source files to deploy.
* - manifest: Full file path for manifest (XML file) of components to deploy.
* - apexTestLevel: Apex test level to use during deployment.
* - apexTests: Apex tests classes to run.
* - usernameOrAlias: Username or alias of the Salesforce org to deploy to.
* - directory: Directory of the local project.
*
* Returns:
* - textResponse: Deploy result.
*/
export const registerToolDeployMetadata = (server: SfMcpServer): void => {
server.tool(
'sf-deploy-metadata',
`Deploy metadata to an org from your local project.
AGENT INSTRUCTIONS:
If the user doesn't specify what to deploy exactly ("deploy my changes"), leave the "sourceDir" and "manifest" params empty so the tool calculates which files to deploy.
EXAMPLE USAGE:
Deploy changes to my org
Deploy this file to my org
Deploy the manifest
Deploy X metadata to my org
Deploy X to my org and run A,B and C apex tests.
`,
deployMetadataParams.shape,
{
title: 'Deploy Metadata',
destructiveHint: true,
openWorldHint: false,
},
async ({ filePaths, usernameOrAlias, apexTests, apexTestLevel, directory, manifest }) => {
if (apexTests && apexTestLevel) {
return textResponse("You can't specify both `apexTests` and `apexTestLevel` parameters.", true);
}
if (filePaths && manifest) {
return textResponse("You can't specify both `sourceDir` and `manifest` parameters.", true);
}
if (!usernameOrAlias)
return textResponse(
'The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool',
true
);
// needed for org allowlist to work
process.chdir(directory);
const connection = await getConnection(usernameOrAlias);
const project = await SfProject.resolve(directory);
const org = await Org.create({ connection });
if (!filePaths && !manifest && !(await org.tracksSource())) {
return textResponse(
'This org does not have source-tracking enabled or does not support source-tracking. You should specify the files or a manifest to deploy.',
true
);
}
let jobId: string = '';
try {
const stl = await SourceTracking.create({
org,
project,
subscribeSDREvents: true,
});
const componentSet = await buildDeployComponentSet(connection, project, stl, filePaths, manifest);
if (componentSet.size === 0) {
// STL found no changes
return textResponse('No local changes to deploy were found.');
}
const deploy = await componentSet.deploy({
usernameOrConnection: connection,
apiOptions: {
...(apexTests ? { runTests: apexTests, testLevel: 'RunSpecifiedTests' } : {}),
...(apexTestLevel ? { testLevel: apexTestLevel } : {}),
},
});
jobId = deploy.id ?? '';
// polling freq. is set dynamically by SDR based on the component set size.
const result = await deploy.pollStatus({
timeout: Duration.minutes(10),
});
return textResponse(`Deploy result: ${JSON.stringify(result.response)}`, !result.response.success);
} catch (error) {
const err = SfError.wrap(error);
if (err.message.includes('timed out')) {
return textResponse(
`
YOU MUST inform the user that the deploy timed out and if they want to resume the deploy, they can use the #sf-resume tool
and ${jobId} for the jobId parameter.`,
true
);
}
return textResponse(`Failed to deploy metadata: ${err.message}`, true);
}
}
);
};
async function buildDeployComponentSet(
connection: Connection,
project: SfProject,
stl: SourceTracking,
sourceDir?: string[],
manifestPath?: string
): Promise<ComponentSet> {
if (sourceDir || manifestPath) {
return ComponentSetBuilder.build({
apiversion: connection.getApiVersion(),
sourceapiversion: ensureString((await project.resolveProjectConfig()).sourceApiVersion),
sourcepath: sourceDir,
...(manifestPath
? {
manifest: {
manifestPath,
directoryPaths: project.getUniquePackageDirectories().map((pDir) => pDir.fullPath),
},
}
: {}),
projectDir: stl?.projectPath,
});
}
// No specific metadata requested to deploy, build component set from STL.
const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry);
return cs;
}