sfdx-hardis 6.0.7-beta202508172156.0 → 6.0.7-beta202508202323.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -2
- package/defaults/refresh-sandbox/package-certificates-to-save.xml +8 -0
- package/defaults/refresh-sandbox/package-metadatas-to-save.xml +44 -0
- package/lib/commands/hardis/org/refresh/after-refresh.d.ts +13 -0
- package/lib/commands/hardis/org/refresh/after-refresh.js +435 -32
- package/lib/commands/hardis/org/refresh/after-refresh.js.map +1 -1
- package/lib/commands/hardis/org/refresh/before-refresh.d.ts +13 -1
- package/lib/commands/hardis/org/refresh/before-refresh.js +317 -74
- package/lib/commands/hardis/org/refresh/before-refresh.js.map +1 -1
- package/lib/common/utils/index.js +3 -0
- package/lib/common/utils/index.js.map +1 -1
- package/lib/common/utils/orgUtils.js +1 -1
- package/lib/common/utils/orgUtils.js.map +1 -1
- package/lib/common/websocketClient.js +5 -0
- package/lib/common/websocketClient.js.map +1 -1
- package/lib/hooks/init/check-upgrade.js +2 -3
- package/lib/hooks/init/check-upgrade.js.map +1 -1
- package/lib/hooks/init/log.js +1 -1
- package/lib/hooks/init/log.js.map +1 -1
- package/oclif.manifest.json +502 -503
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta`
|
|
6
6
|
|
|
7
|
+
- [hardis:org:refresh:before-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/before-refresh/)
|
|
8
|
+
- Retrieve Certificates and other metadatas that could need to be restored
|
|
9
|
+
- Retrieve Custom Settings values
|
|
10
|
+
- [hardis:org:refresh:after-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/after-refresh/)
|
|
11
|
+
- Restore Certificates and other metadatas that could need to be restored
|
|
12
|
+
- Restore Custom Settings values
|
|
13
|
+
- Smart restore of SAML SSO Config by prompting the user to select a valid certificate
|
|
14
|
+
- Send path to command log file to WebSocketServer
|
|
15
|
+
- Improve startup performances by checking for sfdx-hardis upgrades every 6h and not every 15 mn!
|
|
16
|
+
|
|
7
17
|
## [6.0.6 (beta)] 2025-08-17
|
|
8
18
|
|
|
9
|
-
- New command [hardis:org:refresh:
|
|
10
|
-
- New command [hardis:org:refresh:
|
|
19
|
+
- New command [hardis:org:refresh:before-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/before-refresh/) : Save Connected Apps before refreshing a sandbox.
|
|
20
|
+
- New command [hardis:org:refresh:after-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/after-refresh/) : Restore Connected Apps after refreshing a sandbox.
|
|
11
21
|
- Update JSON Schema documentation
|
|
12
22
|
- When authenticating to an expired org token, delete the SF Cli file that can mess with us when we refreshed a sandbox.
|
|
13
23
|
- Improve logs display
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
3
|
+
<types>
|
|
4
|
+
<members>*</members>
|
|
5
|
+
<name>AuthProvider</name>
|
|
6
|
+
</types>
|
|
7
|
+
<types>
|
|
8
|
+
<members>*</members>
|
|
9
|
+
<name>CorsWhitelistOrigin</name>
|
|
10
|
+
</types>
|
|
11
|
+
<types>
|
|
12
|
+
<members>*</members>
|
|
13
|
+
<name>CustomMetadata</name>
|
|
14
|
+
</types>
|
|
15
|
+
<types>
|
|
16
|
+
<members>*</members>
|
|
17
|
+
<name>EmailServicesFunction</name>
|
|
18
|
+
</types>
|
|
19
|
+
<types>
|
|
20
|
+
<members>*</members>
|
|
21
|
+
<name>ExternalCredential</name>
|
|
22
|
+
</types>
|
|
23
|
+
<types>
|
|
24
|
+
<members>*</members>
|
|
25
|
+
<name>ExternalDataSource</name>
|
|
26
|
+
</types>
|
|
27
|
+
<types>
|
|
28
|
+
<members>*</members>
|
|
29
|
+
<name>NamedCredential</name>
|
|
30
|
+
</types>
|
|
31
|
+
<types>
|
|
32
|
+
<members>*</members>
|
|
33
|
+
<name>RemoteSiteSetting</name>
|
|
34
|
+
</types>
|
|
35
|
+
<types>
|
|
36
|
+
<members>*</members>
|
|
37
|
+
<name>SamlSsoConfig</name>
|
|
38
|
+
</types>
|
|
39
|
+
<types>
|
|
40
|
+
<members>*</members>
|
|
41
|
+
<name>WorkflowOutboundMessage</name>
|
|
42
|
+
</types>
|
|
43
|
+
<version>64.0</version>
|
|
44
|
+
</Package>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SfCommand } from '@salesforce/sf-plugins-core';
|
|
2
|
+
import { Connection } from '@salesforce/core';
|
|
2
3
|
import { AnyJson } from '@salesforce/ts-types';
|
|
3
4
|
export default class OrgRefreshAfterRefresh extends SfCommand<AnyJson> {
|
|
4
5
|
static title: string;
|
|
@@ -14,7 +15,19 @@ export default class OrgRefreshAfterRefresh extends SfCommand<AnyJson> {
|
|
|
14
15
|
static requiresProject: boolean;
|
|
15
16
|
protected refreshSandboxConfig: any;
|
|
16
17
|
protected saveProjectPath: string;
|
|
18
|
+
protected result: any;
|
|
19
|
+
protected orgUsername: string;
|
|
20
|
+
protected nameFilter: string | undefined;
|
|
21
|
+
protected processAll: boolean;
|
|
22
|
+
protected conn: Connection;
|
|
23
|
+
protected instanceUrl: any;
|
|
24
|
+
protected orgId: string;
|
|
17
25
|
run(): Promise<AnyJson>;
|
|
26
|
+
private restoreCertificates;
|
|
27
|
+
private restoreOtherMetadata;
|
|
28
|
+
private restoreSamlSsoConfig;
|
|
29
|
+
private restoreCustomSettings;
|
|
30
|
+
private restoreConnectedApps;
|
|
18
31
|
private findConnectedAppsInProject;
|
|
19
32
|
private selectConnectedApps;
|
|
20
33
|
private deleteExistingConnectedApps;
|
|
@@ -2,14 +2,16 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
|
|
|
2
2
|
import { Messages } from '@salesforce/core';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import c from 'chalk';
|
|
5
|
-
import
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
|
-
import { uxLog } from '../../../../common/utils/index.js';
|
|
8
|
-
import { parseXmlFile } from '../../../../common/utils/xmlUtils.js';
|
|
7
|
+
import { execSfdxJson, uxLog } from '../../../../common/utils/index.js';
|
|
8
|
+
import { parsePackageXmlFile, parseXmlFile, writePackageXmlFile } from '../../../../common/utils/xmlUtils.js';
|
|
9
9
|
import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js';
|
|
10
10
|
import { deleteConnectedApps, deployConnectedApps, toConnectedAppFormat, validateConnectedApps, selectConnectedAppsForProcessing, createConnectedAppSuccessResponse, handleConnectedAppError } from '../../../../common/utils/refresh/connectedAppUtils.js';
|
|
11
11
|
import { getConfig } from '../../../../config/index.js';
|
|
12
12
|
import { prompts } from '../../../../common/utils/prompts.js';
|
|
13
|
+
import { WebSocketClient } from '../../../../common/websocketClient.js';
|
|
14
|
+
import { soqlQuery, soqlQueryTooling } from '../../../../common/utils/apiUtils.js';
|
|
13
15
|
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
|
|
14
16
|
const messages = Messages.loadMessages('sfdx-hardis', 'org');
|
|
15
17
|
export default class OrgRefreshAfterRefresh extends SfCommand {
|
|
@@ -79,18 +81,32 @@ This command is part of [sfdx-hardis Sandbox Refresh](https://sfdx-hardis.cloudi
|
|
|
79
81
|
static requiresProject = true;
|
|
80
82
|
refreshSandboxConfig = {};
|
|
81
83
|
saveProjectPath;
|
|
84
|
+
result;
|
|
85
|
+
orgUsername;
|
|
86
|
+
nameFilter;
|
|
87
|
+
processAll;
|
|
88
|
+
conn;
|
|
89
|
+
instanceUrl;
|
|
90
|
+
orgId;
|
|
82
91
|
async run() {
|
|
83
92
|
const { flags } = await this.parse(OrgRefreshAfterRefresh);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
this.orgUsername = flags["target-org"].getUsername();
|
|
94
|
+
this.conn = flags["target-org"].getConnection();
|
|
95
|
+
this.orgId = flags["target-org"].getOrgId();
|
|
96
|
+
this.instanceUrl = this.conn.instanceUrl;
|
|
87
97
|
/* jscpd:ignore-start */
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
this.processAll = flags.all || false;
|
|
99
|
+
this.nameFilter = this.processAll ? undefined : flags.name; // If --all is set, ignore --name
|
|
90
100
|
const config = await getConfig("user");
|
|
91
101
|
this.refreshSandboxConfig = config?.refreshSandboxConfig || {};
|
|
102
|
+
this.result = {};
|
|
92
103
|
/* jscpd:ignore-end */
|
|
93
|
-
uxLog("action", this, c.cyan(`This command
|
|
104
|
+
uxLog("action", this, c.cyan(`This command will restore information after the refresh of org ${this.instanceUrl}
|
|
105
|
+
- Certificates
|
|
106
|
+
- Other Metadatas
|
|
107
|
+
- SAML SSO Config
|
|
108
|
+
- Custom Settings
|
|
109
|
+
- Connected Apps`));
|
|
94
110
|
// Prompt user to select a save project path
|
|
95
111
|
const saveProjectPathRoot = path.join(process.cwd(), 'scripts', 'sandbox-refresh');
|
|
96
112
|
// Only get immediate subfolders of saveProjectPathRoot (not recursive)
|
|
@@ -108,33 +124,420 @@ This command is part of [sfdx-hardis Sandbox Refresh](https://sfdx-hardis.cloudi
|
|
|
108
124
|
})),
|
|
109
125
|
});
|
|
110
126
|
this.saveProjectPath = saveProjectPath.path;
|
|
127
|
+
// 1. Restore Certificates
|
|
128
|
+
await this.restoreCertificates();
|
|
129
|
+
// 2. Restore Other Metadata
|
|
130
|
+
await this.restoreOtherMetadata();
|
|
131
|
+
// 3. Restore SamlSsoConfig
|
|
132
|
+
await this.restoreSamlSsoConfig();
|
|
133
|
+
// 4. Restore Custom Settings
|
|
134
|
+
await this.restoreCustomSettings();
|
|
135
|
+
// 5. Restore Connected Apps
|
|
136
|
+
await this.restoreConnectedApps();
|
|
137
|
+
return this.result;
|
|
138
|
+
}
|
|
139
|
+
async restoreCertificates() {
|
|
140
|
+
const certsDir = path.join(this.saveProjectPath, 'force-app', 'main', 'default', 'certs');
|
|
141
|
+
const manifestDir = path.join(this.saveProjectPath, 'manifest');
|
|
142
|
+
const certsPackageXml = path.join(manifestDir, 'package-certificates-to-save.xml');
|
|
143
|
+
if (!fs.existsSync(certsDir) || !fs.existsSync(certsPackageXml)) {
|
|
144
|
+
uxLog("log", this, c.yellow('No certificates backup found, skipping certificate restore.'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Copy certs to a temporary folder for deployment
|
|
148
|
+
const mdApiCertsRestoreFolder = path.join(this.saveProjectPath, 'mdapi_certs_restore');
|
|
149
|
+
await fs.ensureDir(mdApiCertsRestoreFolder);
|
|
150
|
+
await fs.emptyDir(mdApiCertsRestoreFolder);
|
|
151
|
+
await fs.copy(certsDir, path.join(mdApiCertsRestoreFolder, "certs"), { overwrite: true });
|
|
152
|
+
// List certificates in the restore folder
|
|
153
|
+
const certsFiles = fs.readdirSync(certsDir);
|
|
154
|
+
if (certsFiles.length === 0) {
|
|
155
|
+
uxLog("log", this, c.yellow('No certificates found in the backup folder, skipping certificate restore.'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// List .crt files and get their name, then check that each cert must have a .crt and a .crt-meta.xml file
|
|
159
|
+
const certsToRestoreNames = certsFiles.filter(file => file.endsWith('.crt')).map(file => path.basename(file, '.crt'));
|
|
160
|
+
const validCertsToRestoreNames = certsToRestoreNames.filter(name => {
|
|
161
|
+
return fs.existsSync(path.join(certsDir, `${name}.crt-meta.xml`));
|
|
162
|
+
});
|
|
163
|
+
if (validCertsToRestoreNames.length === 0) {
|
|
164
|
+
uxLog("log", this, c.yellow('No valid certificates found in the backup folder (with .crt + .crt-meta.xml), skipping certificate restore.'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Prompt certificates to restore (all by default)
|
|
168
|
+
const promptCerts = await prompts({
|
|
169
|
+
type: 'multiselect',
|
|
170
|
+
name: 'certs',
|
|
171
|
+
message: `Select certificates to restore`,
|
|
172
|
+
description: 'Select the certificates you want to restore from the backup. You can select multiple certificates.',
|
|
173
|
+
choices: validCertsToRestoreNames.map(name => ({
|
|
174
|
+
title: name,
|
|
175
|
+
value: name
|
|
176
|
+
})),
|
|
177
|
+
initial: validCertsToRestoreNames, // Select all by default
|
|
178
|
+
});
|
|
179
|
+
const selectedCerts = promptCerts.certs;
|
|
180
|
+
if (selectedCerts.length === 0) {
|
|
181
|
+
uxLog("log", this, c.yellow('No certificates selected for restore, skipping certificate restore.'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Ask user confirmation before restoring certificates
|
|
185
|
+
const prompt = await prompts({
|
|
186
|
+
type: 'confirm',
|
|
187
|
+
name: 'restore',
|
|
188
|
+
message: `Do you confirm you want to restore ${selectedCerts.length} certificate(s) ?`,
|
|
189
|
+
description: 'This will deploy all certificate files and definitions saved before the refresh.',
|
|
190
|
+
initial: true
|
|
191
|
+
});
|
|
192
|
+
if (!prompt.restore) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Create manifest/package.xml within mdApiCertsRestoreFolder
|
|
196
|
+
const packageXmlCerts = {
|
|
197
|
+
"Certificate": selectedCerts
|
|
198
|
+
};
|
|
199
|
+
await writePackageXmlFile(path.join(mdApiCertsRestoreFolder, 'package.xml'), packageXmlCerts);
|
|
200
|
+
// Deploy using metadata API
|
|
201
|
+
uxLog("log", this, c.grey(`Deploying certificates in org ${this.instanceUrl} using Metadata API (Source Api does not support it)...`));
|
|
202
|
+
await execSfdxJson(`sf project deploy start --metadata-dir ${mdApiCertsRestoreFolder} --target-org ${this.orgUsername}`, this, { output: true, fail: true, cwd: this.saveProjectPath });
|
|
203
|
+
uxLog("success", this, c.green(`Certificates restored successfully in org ${this.instanceUrl}`));
|
|
204
|
+
}
|
|
205
|
+
async restoreOtherMetadata() {
|
|
206
|
+
const manifestDir = path.join(this.saveProjectPath, 'manifest');
|
|
207
|
+
const restorePackageXml = path.join(manifestDir, 'package-metadata-to-restore.xml');
|
|
208
|
+
// Check if the restore package.xml exists
|
|
209
|
+
if (!fs.existsSync(restorePackageXml)) {
|
|
210
|
+
uxLog("log", this, c.yellow('No package-metadata-to-restore.xml found, skipping metadata restore.'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Warn user about the restore package.xml that needs to be manually checked
|
|
214
|
+
WebSocketClient.sendReportFileMessage(restorePackageXml, "Restore Metadatas package.xml", "report");
|
|
215
|
+
uxLog("action", this, c.cyan(`Now handling the restore of other metadata from ${restorePackageXml}...`));
|
|
216
|
+
const metadataRestore = await parsePackageXmlFile(restorePackageXml);
|
|
217
|
+
const metadataSummary = Object.keys(metadataRestore).map(key => {
|
|
218
|
+
return `${key}(${Array.isArray(metadataRestore[key]) ? metadataRestore[key].length : 0})`;
|
|
219
|
+
}).join(', ');
|
|
220
|
+
uxLog("warning", this, c.yellow(`Look at the package-metadata-to-restore.xml file in ${c.bold(this.saveProjectPath)} to see what will be restored.`));
|
|
221
|
+
uxLog("warning", this, c.yellow(`Confirm it's content, or remove/comment part of it if you don't want some metadata to be restored\n${metadataSummary}`));
|
|
222
|
+
const prompt = await prompts({
|
|
223
|
+
type: 'confirm',
|
|
224
|
+
name: 'restore',
|
|
225
|
+
message: `Please double check package-metadata-to-restore.xml. Do you confirm you want to restore all these metadatas ?\n${metadataSummary}`,
|
|
226
|
+
description: `WARNING: Check and validate/update file ${restorePackageXml} BEFORE it is deployed !`,
|
|
227
|
+
initial: true
|
|
228
|
+
});
|
|
229
|
+
if (!prompt.restore) {
|
|
230
|
+
uxLog("warning", this, c.yellow('Metadata restore cancelled by user.'));
|
|
231
|
+
this.result = Object.assign(this.result, { success: false, message: 'Metadata restore cancelled by user' });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Deploy the metadata using the package.xml
|
|
235
|
+
uxLog("action", this, c.cyan('Deploying other metadatas to org...'));
|
|
236
|
+
const deployCmd = `sf project deploy start --manifest ${restorePackageXml} --target-org ${this.orgUsername} --json`;
|
|
237
|
+
const deployResult = await execSfdxJson(deployCmd, this, { output: true, fail: true, cwd: this.saveProjectPath });
|
|
238
|
+
if (deployResult.status === 0) {
|
|
239
|
+
uxLog("success", this, c.green(`Other metadata restored successfully in org ${this.instanceUrl}`));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
uxLog("error", this, c.red(`Failed to restore other metadata in org ${this.instanceUrl}: ${deployResult.error}`));
|
|
243
|
+
this.result = Object.assign(this.result, { success: false, message: `Failed to restore other metadata: ${deployResult.error}` });
|
|
244
|
+
throw new Error(`Failed to restore other metadata:\n${JSON.stringify(deployResult, null, 2)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async restoreSamlSsoConfig() {
|
|
248
|
+
// 0. List all samlssoconfigs in the project, prompt user to select which to restore
|
|
249
|
+
const samlDir = path.join(this.saveProjectPath, 'force-app', 'main', 'default', 'samlssoconfigs');
|
|
250
|
+
if (!fs.existsSync(samlDir)) {
|
|
251
|
+
uxLog("action", this, c.cyan('No SAML SSO Configs found, skipping SAML SSO config restore.'));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const allSamlFiles = fs.readdirSync(samlDir).filter(f => f.endsWith('.samlssoconfig-meta.xml'));
|
|
255
|
+
if (allSamlFiles.length === 0) {
|
|
256
|
+
uxLog("action", this, c.yellow('No SAML SSO Config XML files found., skipping SAML SSO config restore.'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Prompt user to select which SAML SSO configs to restore
|
|
260
|
+
const promptSaml = await prompts({
|
|
261
|
+
type: 'multiselect',
|
|
262
|
+
name: 'samlFiles',
|
|
263
|
+
message: 'Select SAML SSO Configs to restore',
|
|
264
|
+
description: 'Select the SAML SSO Configs you want to restore from the backup. You can select multiple configs.',
|
|
265
|
+
choices: allSamlFiles.map(f => ({ title: f.replace('.samlssoconfig-meta.xml', ''), value: f })),
|
|
266
|
+
initial: allSamlFiles // select all by default
|
|
267
|
+
});
|
|
268
|
+
const selectedSamlFiles = promptSaml.samlFiles;
|
|
269
|
+
if (!selectedSamlFiles || selectedSamlFiles.length === 0) {
|
|
270
|
+
uxLog("log", this, c.yellow('No SAML SSO Configs selected for restore, skipping.'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// 1. Clean up XML and prompt for cert
|
|
274
|
+
// Query active certificates
|
|
275
|
+
const soql = "SELECT Id, MasterLabel FROM Certificate WHERE ExpirationDate > TODAY LIMIT 200";
|
|
276
|
+
let certs = [];
|
|
111
277
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
278
|
+
const res = await soqlQueryTooling(soql, this.conn);
|
|
279
|
+
certs = res.records;
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
uxLog("error", this, c.red(`Failed to query active certificates: ${e}`));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!certs.length) {
|
|
286
|
+
uxLog("error", this, c.yellow('No active certificates found in org. You\'ll need to update manually field requestSigningCertId with the id of a valid certificate.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const updated = [];
|
|
290
|
+
const errors = [];
|
|
291
|
+
for (const samlFile of selectedSamlFiles) {
|
|
292
|
+
const samlName = samlFile.replace('.samlssoconfig-meta.xml', '');
|
|
293
|
+
// Prompt user to select a certificate
|
|
294
|
+
const certPrompt = await prompts({
|
|
295
|
+
type: 'select',
|
|
296
|
+
name: 'certId',
|
|
297
|
+
message: `Select the certificate to use for SAML SSO config ${samlName}`,
|
|
298
|
+
description: `This will update <requestSigningCertId> in ${samlFile}.`,
|
|
299
|
+
choices: certs.map(cert => ({
|
|
300
|
+
title: cert.MasterLabel,
|
|
301
|
+
value: cert.Id.substring(0, 15)
|
|
302
|
+
})),
|
|
303
|
+
});
|
|
304
|
+
const selectedCertId = certPrompt.certId;
|
|
305
|
+
if (!selectedCertId) {
|
|
306
|
+
uxLog("warning", this, c.yellow('No certificate selected. Skipping SAML SSO config update.'));
|
|
307
|
+
errors.push(`No certificate selected for ${samlName}`);
|
|
308
|
+
continue;
|
|
117
309
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
310
|
+
const filePath = path.join(samlDir, samlFile);
|
|
311
|
+
let xml = await fs.readFile(filePath, 'utf8');
|
|
312
|
+
// Remove <oauthTokenEndpoint>...</oauthTokenEndpoint>
|
|
313
|
+
xml = xml.replace(/<oauthTokenEndpoint>.*?<\/oauthTokenEndpoint>\s*/gs, '');
|
|
314
|
+
// Remove <salesforceLoginUrl>...</salesforceLoginUrl>
|
|
315
|
+
xml = xml.replace(/<salesforceLoginUrl>.*?<\/salesforceLoginUrl>\s*/gs, '');
|
|
316
|
+
// Replace <requestSigningCertId>...</requestSigningCertId>
|
|
317
|
+
if (/<requestSigningCertId>.*?<\/requestSigningCertId>/s.test(xml)) {
|
|
318
|
+
xml = xml.replace(/<requestSigningCertId>.*?<\/requestSigningCertId>/s, `<requestSigningCertId>${selectedCertId}</requestSigningCertId>`);
|
|
319
|
+
}
|
|
320
|
+
await fs.writeFile(filePath, xml, 'utf8');
|
|
321
|
+
uxLog("log", this, c.grey(`Updated SAML SSO config ${samlFile} with certificate ${selectedCertId} and removed readonly tags oauthTokenEndpoint & salesforceLoginUrl`));
|
|
322
|
+
// 2. Prompt user to confirm deployment
|
|
323
|
+
const promptDeploy = await prompts({
|
|
324
|
+
type: 'confirm',
|
|
325
|
+
name: 'deploy',
|
|
326
|
+
message: `Do you confirm you want to deploy ${samlFile} SAML SSO Config to the org?`,
|
|
327
|
+
description: 'This will deploy the selected SAML SSO Configs to the org using SFDX',
|
|
328
|
+
initial: true
|
|
329
|
+
});
|
|
330
|
+
if (!promptDeploy.deploy) {
|
|
331
|
+
uxLog("warning", this, c.yellow(`SAML SSO Config ${samlFile} deployment cancelled by user.`));
|
|
332
|
+
errors.push(`Deployment cancelled for ${samlFile}`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const deployCommand = `sf project deploy start -m SamlSsoConfig:${samlName} --target-org ${this.orgUsername}`;
|
|
336
|
+
try {
|
|
337
|
+
uxLog("action", this, c.cyan(`Deploying SAML SSO Config ${samlName} to org ${this.instanceUrl}...`));
|
|
338
|
+
const deployResult = await execSfdxJson(deployCommand, this, { output: true, fail: true, cwd: this.saveProjectPath });
|
|
339
|
+
if (deployResult.status === 0) {
|
|
340
|
+
uxLog("success", this, c.green(`SAML SSO Config ${samlName} deployed successfully in org ${this.instanceUrl}`));
|
|
341
|
+
updated.push(samlName);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
uxLog("error", this, c.red(`Failed to deploy SAML SSO Config ${samlName}: ${deployResult.error}`));
|
|
345
|
+
errors.push(`Failed to deploy ${samlName}: ${deployResult.error}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
uxLog("error", this, c.red(`Error deploying SAML SSO Config ${samlName}: ${e.message}`));
|
|
350
|
+
errors.push(`Error deploying ${samlName}: ${e.message}`);
|
|
124
351
|
}
|
|
125
|
-
/* jscpd:ignore-end */
|
|
126
|
-
// Step 3: Delete existing Connected Apps from the org for clean deployment
|
|
127
|
-
await this.deleteExistingConnectedApps(orgUsername, selectedApps);
|
|
128
|
-
// Step 4: Deploy the Connected Apps to the org
|
|
129
|
-
await this.deployConnectedApps(orgUsername, selectedApps);
|
|
130
|
-
// Return the result
|
|
131
|
-
uxLog("action", this, c.cyan(`Summary`));
|
|
132
|
-
const appNames = selectedApps.map(app => `- ${app.fullName}`).join('\n');
|
|
133
|
-
uxLog("success", this, c.green(`Successfully restored ${selectedApps.length} Connected App(s) to ${conn.instanceUrl}\n${appNames}`));
|
|
134
|
-
return createConnectedAppSuccessResponse(`Successfully restored ${selectedApps.length} Connected App(s) to the org`, selectedApps.map(app => app.fullName));
|
|
135
352
|
}
|
|
136
|
-
|
|
137
|
-
|
|
353
|
+
// 3. Summary of results
|
|
354
|
+
uxLog("action", this, c.cyan(`SAML SSO Config processing completed.`));
|
|
355
|
+
if (updated.length > 0) {
|
|
356
|
+
uxLog("success", this, c.green(`Successfully updated and deployed SAML SSO Configs: ${updated.join(', ')}`));
|
|
357
|
+
}
|
|
358
|
+
if (errors.length > 0) {
|
|
359
|
+
uxLog("error", this, c.red(`Errors occurred during SAML SSO Config processing:\n${errors.join('\n')}`));
|
|
360
|
+
this.result = Object.assign(this.result, { success: false, message: `SAML SSO Config processing errors:\n${errors.join('\n')}` });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async restoreCustomSettings() {
|
|
364
|
+
// Check there are custom settings to restore
|
|
365
|
+
const csDir = path.join(this.saveProjectPath, 'savedCustomSettings');
|
|
366
|
+
if (!fs.existsSync(csDir)) {
|
|
367
|
+
uxLog("log", this, c.yellow('No savedCustomSettings folder found, skipping custom settings restore.'));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const csFolders = fs.readdirSync(csDir).filter(f => fs.statSync(path.join(csDir, f)).isDirectory());
|
|
371
|
+
if (csFolders.length === 0) {
|
|
372
|
+
uxLog("log", this, c.yellow('No custom settings data found, skipping custom settings restore.'));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// List custom settings to restore so users can select them. Keep only folders that have a .json file
|
|
376
|
+
const csToRestore = csFolders.filter(folder => {
|
|
377
|
+
const jsonFile = path.join(csDir, folder, `${folder}.json`);
|
|
378
|
+
return fs.existsSync(jsonFile);
|
|
379
|
+
});
|
|
380
|
+
if (csToRestore.length === 0) {
|
|
381
|
+
uxLog("log", this, c.yellow('No custom settings data found to restore, skipping custom settings restore.'));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Prompt custom settings to restore: All by default
|
|
385
|
+
const promptRestore = await prompts({
|
|
386
|
+
type: 'multiselect',
|
|
387
|
+
name: 'settings',
|
|
388
|
+
message: `Select custom settings to restore`,
|
|
389
|
+
description: 'Select the custom settings you want to restore from the backup. You can select multiple settings.',
|
|
390
|
+
choices: csToRestore.map(folder => ({
|
|
391
|
+
title: folder,
|
|
392
|
+
value: folder
|
|
393
|
+
})),
|
|
394
|
+
initial: csToRestore // Select all by default
|
|
395
|
+
});
|
|
396
|
+
const selectedSettings = promptRestore.settings;
|
|
397
|
+
if (selectedSettings.length === 0) {
|
|
398
|
+
uxLog("log", this, c.yellow('No custom settings selected for restore, skipping custom settings restore.'));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// Ask last confirmation to user
|
|
402
|
+
const prompt = await prompts({
|
|
403
|
+
type: 'confirm',
|
|
404
|
+
name: 'restore',
|
|
405
|
+
message: `Do you confirm you want to restore ${selectedSettings.length} Custom Settings values from backup?`,
|
|
406
|
+
description: 'This will import all custom settings data saved before the refresh.',
|
|
407
|
+
initial: true
|
|
408
|
+
});
|
|
409
|
+
if (!prompt.restore) {
|
|
410
|
+
uxLog("warning", this, c.yellow('Custom settings restore cancelled by user.'));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
uxLog("action", this, c.cyan(`Restoring ${selectedSettings.length} Custom Settings...`));
|
|
414
|
+
const successSettings = [];
|
|
415
|
+
const failedSettings = [];
|
|
416
|
+
for (const folder of selectedSettings) {
|
|
417
|
+
const jsonFile = path.join(csDir, folder, `${folder}.json`);
|
|
418
|
+
if (!fs.existsSync(jsonFile)) {
|
|
419
|
+
uxLog("warning", this, c.yellow(`No data file for custom setting ${folder}`));
|
|
420
|
+
failedSettings.push(folder);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Remove standard fields from the JSON file and create a new file without them, and replace Org Id with the current org one
|
|
424
|
+
const jsonFileForImport = path.join(csDir, folder, `${folder}-without-standard-fields.json`);
|
|
425
|
+
const jsonData = await fs.readJson(jsonFile);
|
|
426
|
+
const standardFields = ['LastModifiedDate', 'IsDeleted', 'CreatedById', 'CreatedDate', 'LastModifiedById', 'SystemModstamp'];
|
|
427
|
+
let deleteExistingCsBefore = false;
|
|
428
|
+
jsonData.records = (jsonData?.records || []).map((record) => {
|
|
429
|
+
const newRecord = {};
|
|
430
|
+
for (const key in record) {
|
|
431
|
+
// Remove standard fields
|
|
432
|
+
if (!standardFields.includes(key)) {
|
|
433
|
+
newRecord[key] = record[key];
|
|
434
|
+
}
|
|
435
|
+
// Replace Org Id with the current org one
|
|
436
|
+
if (key === 'SetupOwnerId') {
|
|
437
|
+
newRecord[key] = this.orgId; // Replace with current org Id
|
|
438
|
+
deleteExistingCsBefore = true; // Use upsert if SetupOwnerId is present
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return newRecord;
|
|
442
|
+
});
|
|
443
|
+
// Write the new JSON file without standard fields
|
|
444
|
+
await fs.writeJson(jsonFileForImport, jsonData, { spaces: 2 });
|
|
445
|
+
// Delete existing custom settings before import if needed
|
|
446
|
+
if (deleteExistingCsBefore) {
|
|
447
|
+
uxLog("log", this, c.grey(`Deleting existing custom settings for ${folder} in org ${this.orgUsername} before import...`));
|
|
448
|
+
// Query existing custom settings to delete
|
|
449
|
+
const query = `SELECT Id FROM ${folder} WHERE SetupOwnerId = '${this.orgId}'`;
|
|
450
|
+
const queryRes = await soqlQuery(query, this.conn);
|
|
451
|
+
if (queryRes.records.length > 0) {
|
|
452
|
+
const idsToDelete = (queryRes?.records.map(record => record.Id) || []).filter((id) => typeof id === 'string');
|
|
453
|
+
uxLog("log", this, c.grey(`Found ${idsToDelete.length} existing custom settings to delete for ${folder} in org ${this.orgUsername}`));
|
|
454
|
+
const deleteResults = await this.conn.sobject(folder).destroy(idsToDelete, { allOrNone: true });
|
|
455
|
+
const deletedSuccessFullyIds = deleteResults.filter(result => result.success).map(result => "- " + result.id).join('\n');
|
|
456
|
+
uxLog("log", this, c.grey(`Deleted ${deletedSuccessFullyIds.length} existing custom settings for ${folder} in org ${this.orgUsername}\n${deletedSuccessFullyIds}`));
|
|
457
|
+
const deletedErrorIds = deleteResults.filter(result => !result.success).map(result => "- " + result.id).join('\n');
|
|
458
|
+
if (deletedErrorIds.length > 0) {
|
|
459
|
+
uxLog("warning", this, c.yellow(`Failed to delete existing custom settings for ${folder} in org ${this.orgUsername}\n${deletedErrorIds}`));
|
|
460
|
+
continue; // Skip to next setting if deletion failed
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
uxLog("log", this, c.grey(`No existing custom settings found for ${folder} in org ${this.orgUsername}.`));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Import the custom setting using sf data tree import
|
|
468
|
+
const importCmd = `sf data tree import --files ${jsonFileForImport} --target-org ${this.orgUsername} --json`;
|
|
469
|
+
try {
|
|
470
|
+
const importRes = await execSfdxJson(importCmd, this, { output: true, fail: true, cwd: this.saveProjectPath });
|
|
471
|
+
if (importRes.status === 0) {
|
|
472
|
+
uxLog("success", this, c.green(`Custom setting ${folder} restored.`));
|
|
473
|
+
successSettings.push(folder);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
uxLog("error", this, c.red(`Failed to restore custom setting ${folder}:\n${JSON.stringify(importRes, null, 2)}`));
|
|
477
|
+
failedSettings.push(folder);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
uxLog("error", this, c.red(`Custom setting ${folder} restore failed:\n${JSON.stringify(e)}`));
|
|
482
|
+
failedSettings.push(folder);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
uxLog("action", this, c.cyan(`Custom settings restore complete (${successSettings.length} successful, ${failedSettings.length} failed)`));
|
|
487
|
+
if (successSettings.length > 0) {
|
|
488
|
+
const successSettingsNames = successSettings.map(name => "- " + name).join('\n');
|
|
489
|
+
uxLog("success", this, c.green(`Successfully restored ${successSettings.length} Custom Setting(s):\n ${successSettingsNames}`));
|
|
490
|
+
}
|
|
491
|
+
if (failedSettings.length > 0) {
|
|
492
|
+
const failedSettingsNames = failedSettings.map(name => "- " + name).join('\n');
|
|
493
|
+
uxLog("error", this, c.red(`Failed to restore ${failedSettings.length} Custom Setting(s): ${failedSettingsNames}`));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async restoreConnectedApps() {
|
|
497
|
+
let restoreConnectedApps = false;
|
|
498
|
+
const promptRestoreConnectedApps = await prompts({
|
|
499
|
+
type: 'confirm',
|
|
500
|
+
name: 'confirmRestore',
|
|
501
|
+
message: `Do you want to restore Connected Apps from the backup in ${c.bold(this.saveProjectPath)}?`,
|
|
502
|
+
initial: true,
|
|
503
|
+
description: 'This will restore all Connected Apps (including Consumer Secrets) from the backup created before the org refresh.'
|
|
504
|
+
});
|
|
505
|
+
if (promptRestoreConnectedApps.confirmRestore) {
|
|
506
|
+
restoreConnectedApps = true;
|
|
507
|
+
}
|
|
508
|
+
if (restoreConnectedApps) {
|
|
509
|
+
try {
|
|
510
|
+
// Step 1: Find Connected Apps in the project
|
|
511
|
+
const connectedApps = await this.findConnectedAppsInProject(this.nameFilter, this.processAll);
|
|
512
|
+
if (connectedApps.length === 0) {
|
|
513
|
+
uxLog("warning", this, c.yellow('No Connected Apps found in the project'));
|
|
514
|
+
this.result = Object.assign(this.result, { success: false, message: 'No Connected Apps found in the project' });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
/* jscpd:ignore-start */
|
|
518
|
+
// Step 2: Select which Connected Apps to process
|
|
519
|
+
const selectedApps = await this.selectConnectedApps(connectedApps, this.processAll, this.nameFilter);
|
|
520
|
+
if (selectedApps.length === 0) {
|
|
521
|
+
uxLog("warning", this, c.yellow('No Connected Apps selected'));
|
|
522
|
+
this.result = Object.assign(this.result, { success: false, message: 'No Connected Apps selected' });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
/* jscpd:ignore-end */
|
|
526
|
+
// Step 3: Delete existing Connected Apps from the org for clean deployment
|
|
527
|
+
await this.deleteExistingConnectedApps(this.orgUsername, selectedApps);
|
|
528
|
+
// Step 4: Deploy the Connected Apps to the org
|
|
529
|
+
await this.deployConnectedApps(this.orgUsername, selectedApps);
|
|
530
|
+
// Return the result
|
|
531
|
+
uxLog("action", this, c.cyan(`Summary`));
|
|
532
|
+
const appNames = selectedApps.map(app => `- ${app.fullName}`).join('\n');
|
|
533
|
+
uxLog("success", this, c.green(`Successfully restored ${selectedApps.length} Connected App(s) to ${this.conn.instanceUrl}\n${appNames}`));
|
|
534
|
+
const restoreResult = createConnectedAppSuccessResponse(`Successfully restored ${selectedApps.length} Connected App(s) to the org`, selectedApps.map(app => app.fullName));
|
|
535
|
+
this.result = Object.assign(this.result, restoreResult);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const restoreResult = handleConnectedAppError(error, this);
|
|
539
|
+
this.result = Object.assign(this.result, restoreResult);
|
|
540
|
+
}
|
|
138
541
|
}
|
|
139
542
|
}
|
|
140
543
|
async findConnectedAppsInProject(nameFilter, processAll) {
|