@simplysf/simply-cicd-core 0.4.0 → 0.5.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/README.md +9 -9
- package/lib/build/cleanupScratchOrgs.js +16 -49
- package/lib/build/cleanupScratchOrgs.js.map +1 -1
- package/lib/build/createScratchOrg.js +10 -24
- package/lib/build/createScratchOrg.js.map +1 -1
- package/lib/sfOrg.d.ts +37 -0
- package/lib/sfOrg.js +99 -1
- package/lib/sfOrg.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,15 +57,15 @@ const mr = await gitlab.findMergeRequest({ sourceBranch: 'feature/x' });
|
|
|
57
57
|
|
|
58
58
|
The modules `@simplysf/simply-cicd`'s commands run, also exported from the package root. Each stage function takes a plain options object (the camelCase equivalent of the command's flags) and throws on failure. Full signatures are in the [API reference](https://simplysf.github.io/simply-plugins-core/api/simply-cicd-core/readme/).
|
|
59
59
|
|
|
60
|
-
| Area
|
|
61
|
-
|
|
|
62
|
-
| Build
|
|
63
|
-
| Deploy
|
|
64
|
-
| Notify
|
|
65
|
-
| sfdx-dependabot
|
|
66
|
-
| Schemas (zod)
|
|
67
|
-
| Salesforce org and package
|
|
68
|
-
| `sf`/`git`/env utilities
|
|
60
|
+
| Area | Main exports |
|
|
61
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
62
|
+
| Build | `createScratchOrg`, `deleteScratchOrg`, `cleanupScratchOrgs`, `pushToScratch`, `runScratchApexTests`, `installDependencies`, `createPackageVersion`, `createFallbackTag`, `determinePackageChanges`, `runLwcJest`, `runCheckPermissionSets`, `runFlowDelta`, `runFlexipageDelta`, `publishUtamPageObjects`, `VersionTagFormat` |
|
|
63
|
+
| Deploy | `deployProject`, `deployHappySoup`, `runDeployStage`, `validateDeployFiles`, `toProjectStageOptions`, `toHappySoupStageOptions`, plus the `deployCommon` helpers (`runDeploymentSteps`, `loadProgress`, `updateProgress`, `installDeployPackageDependencies`, `runDeployApexTests`, …) |
|
|
64
|
+
| Notify | `sendBuildNotification`, `projectNotifyBeforeScript`/`projectNotifyAfterScript`, `happySoupNotifyBeforeScript`/`happySoupNotifyAfterScript`, `getCommitStories`, `getRemoteCommitStories`, `sendNotification`, `renderNotifyTemplate`, `NOTIFY_TEMPLATES` |
|
|
65
|
+
| sfdx-dependabot | `discoverEligibleProjects`, `processProject`, `applyProjectUpdate`, `updateSfdxProject` |
|
|
66
|
+
| Schemas (zod) | `deployConfigSchema`, `deployProgressSchema`, `deployRulesSchema`, `deployStageSchema` |
|
|
67
|
+
| Salesforce org and package operations | `getPackageVersionReport`, `listInstalledPackages`, `getOrgConnection`, `getOrgInstanceUrl`, `getRemainingDailyScratchOrgs`, `assignPermissionSets`, `assignPermissionSetLicenses` (`AssignmentError`) (in-process via `@salesforce/core` / `@salesforce/packaging`, no `sf` needed) |
|
|
68
|
+
| `sf`/`git`/env utilities | `runSf`, `runSfJson`, `authenticateOrg`, `installPackageDependencies`, `runApexTests`, `installPlugin`, `cloneRepo`, `appendToEnvFile`, `resolveString`/`resolveBoolean`, `logger` |
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
71
|
import { deployProject } from '@simplysf/simply-cicd-core';
|
|
@@ -13,65 +13,32 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { promises as fs } from 'node:fs';
|
|
18
|
-
import { runSf } from '../exec/sfCli.js';
|
|
19
16
|
import { logger } from '../logger.js';
|
|
17
|
+
import { getOrgConnection } from '../sfOrg.js';
|
|
20
18
|
import { resolveDevHubs } from './devHubs.js';
|
|
21
19
|
async function cleanupScratchOrgsForHub(hubUsername) {
|
|
22
20
|
logger.info(`Querying for scratch orgs in ${hubUsername}...`);
|
|
23
|
-
const csvFile = path.join(process.cwd(), `${hubUsername}_orgs.csv`);
|
|
24
21
|
try {
|
|
22
|
+
const connection = await getOrgConnection(hubUsername);
|
|
25
23
|
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
'data',
|
|
29
|
-
'query',
|
|
30
|
-
'--query',
|
|
31
|
-
query,
|
|
32
|
-
'--result-format',
|
|
33
|
-
'csv',
|
|
34
|
-
'--target-org',
|
|
35
|
-
hubUsername,
|
|
36
|
-
]);
|
|
37
|
-
await fs.writeFile(csvFile, stdout);
|
|
38
|
-
const fileContent = await fs.readFile(csvFile, 'utf-8');
|
|
39
|
-
// Fixed: the original split on '' (individual characters), so this line count was
|
|
40
|
-
// effectively always > 1 for any non-empty CSV and the "no old scratch orgs" branch below
|
|
41
|
-
// was unreachable. Splitting on newlines actually counts CSV rows.
|
|
42
|
-
const lines = fileContent.trim().split('\n');
|
|
43
|
-
if (lines.length > 1) {
|
|
44
|
-
logger.info(`Found ${lines.length - 1} old scratch orgs in ${hubUsername}. Deleting...`);
|
|
45
|
-
await runSf([
|
|
46
|
-
'data',
|
|
47
|
-
'bulk',
|
|
48
|
-
'delete',
|
|
49
|
-
'--sobject',
|
|
50
|
-
'ActiveScratchOrg',
|
|
51
|
-
'--file',
|
|
52
|
-
csvFile,
|
|
53
|
-
'--target-org',
|
|
54
|
-
hubUsername,
|
|
55
|
-
'--wait',
|
|
56
|
-
'30',
|
|
57
|
-
], {
|
|
58
|
-
stdio: 'inherit',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
24
|
+
const { records } = await connection.query(`SELECT Id FROM ActiveScratchOrg WHERE CreatedDate < ${threeHoursAgo}`, { autoFetch: true, maxFetch: 50_000 });
|
|
25
|
+
if (records.length === 0) {
|
|
62
26
|
logger.info(`No old scratch orgs found in ${hubUsername}.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
logger.info(`Found ${records.length} old scratch orgs in ${hubUsername}. Deleting...`);
|
|
30
|
+
// Deleting an ActiveScratchOrg record deletes the scratch org. allowRecursive splits the
|
|
31
|
+
// sObject Collections call into the API's 200-record batches.
|
|
32
|
+
const results = await connection.sobject('ActiveScratchOrg').destroy(records.map((record) => record.Id), { allOrNone: false, allowRecursive: true });
|
|
33
|
+
const failures = results.filter((result) => !result.success);
|
|
34
|
+
for (const failure of failures) {
|
|
35
|
+
const reason = failure.success ? '' : failure.errors.map((e) => e.message).join('; ');
|
|
36
|
+
logger.warn(`Could not delete scratch org ${failure.id ?? 'unknown'} in ${hubUsername}: ${reason}`);
|
|
63
37
|
}
|
|
38
|
+
logger.info(`Deleted ${results.length - failures.length} of ${results.length} old scratch orgs in ${hubUsername}.`);
|
|
64
39
|
}
|
|
65
40
|
catch (error) {
|
|
66
|
-
logger.error(`Failed to cleanup scratch orgs for ${hubUsername}:`, error.
|
|
67
|
-
}
|
|
68
|
-
finally {
|
|
69
|
-
try {
|
|
70
|
-
await fs.unlink(csvFile);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* ignore */
|
|
74
|
-
}
|
|
41
|
+
logger.error(`Failed to cleanup scratch orgs for ${hubUsername}:`, error.message);
|
|
75
42
|
}
|
|
76
43
|
}
|
|
77
44
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanupScratchOrgs.js","sourceRoot":"","sources":["../../src/build/cleanupScratchOrgs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"cleanupScratchOrgs.js","sourceRoot":"","sources":["../../src/build/cleanupScratchOrgs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,KAAK,UAAU,wBAAwB,CAAC,WAAmB;IACzD,MAAM,CAAC,IAAI,CAAC,gCAAgC,WAAW,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,WAAW,CAAC,CAAC;QACvD,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9E,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,UAAU,CAAC,KAAK,CACxC,uDAAuD,aAAa,EAAE,EACtE,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CACtC,CAAC;QACF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,gCAAgC,WAAW,GAAG,CAAC,CAAC;YAC5D,OAAO;QACT,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,wBAAwB,WAAW,eAAe,CAAC,CAAC;QACvF,yFAAyF;QACzF,8DAA8D;QAC9D,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAClE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAClC,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,CAC3C,CAAC;QACF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtF,MAAM,CAAC,IAAI,CAAC,gCAAgC,OAAO,CAAC,EAAE,IAAI,SAAS,OAAO,WAAW,KAAK,MAAM,EAAE,CAAC,CAAC;QACtG,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM,wBAAwB,WAAW,GAAG,CAAC,CAAC;IACtH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,sCAAsC,WAAW,GAAG,EAAG,KAAe,CAAC,OAAO,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,aAAuB;IAC9D,MAAM,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,CAAC;IAEpD,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,wGAAwG;QACxG,MAAM,wBAAwB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -15,21 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { promises as fs } from 'node:fs';
|
|
17
17
|
import { getDefaultPackageDirectory, readSfdxProject } from '@simplysf/simply-core';
|
|
18
|
-
import { runSf
|
|
18
|
+
import { runSf } from '../exec/sfCli.js';
|
|
19
19
|
import { logger } from '../logger.js';
|
|
20
|
-
import { getRemainingDailyScratchOrgs } from '../sfOrg.js';
|
|
20
|
+
import { assignPermissionSetLicenses as assignLicensesToOrgUser, assignPermissionSets as assignPermissionSetsToOrgUser, getOrgConnection, getRemainingDailyScratchOrgs, } from '../sfOrg.js';
|
|
21
21
|
import { resolveDevHubs } from './devHubs.js';
|
|
22
22
|
async function assignPermissionSets(permissionSets) {
|
|
23
23
|
if (permissionSets.length === 0) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
logger.info('Assigning permission sets...');
|
|
27
|
-
const args = ['org', 'assign', 'permset', '--json', ...permissionSets.flatMap((ps) => ['--name', ps])];
|
|
28
27
|
try {
|
|
29
|
-
|
|
30
|
-
if (result.status !== 0) {
|
|
31
|
-
throw new Error(`Permission set assignment failed: ${JSON.stringify(result.failures)}`);
|
|
32
|
-
}
|
|
28
|
+
await assignPermissionSetsToOrgUser(permissionSets);
|
|
33
29
|
logger.success('Permission sets assigned successfully.');
|
|
34
30
|
}
|
|
35
31
|
catch (e) {
|
|
@@ -42,12 +38,8 @@ async function assignPermissionSetLicenses(licenses) {
|
|
|
42
38
|
return;
|
|
43
39
|
}
|
|
44
40
|
logger.info('Assigning permission set licenses...');
|
|
45
|
-
const args = ['org', 'assign', 'permset-license', '--json', ...licenses.flatMap((psl) => ['--name', psl])];
|
|
46
41
|
try {
|
|
47
|
-
|
|
48
|
-
if (result.status !== 0) {
|
|
49
|
-
throw new Error(`Permission set license assignment failed: ${JSON.stringify(result.failures)}`);
|
|
50
|
-
}
|
|
42
|
+
await assignLicensesToOrgUser(licenses);
|
|
51
43
|
logger.success('Permission set licenses assigned successfully.');
|
|
52
44
|
}
|
|
53
45
|
catch (e) {
|
|
@@ -66,19 +58,13 @@ async function assignPermissions(sfdxProjectJson) {
|
|
|
66
58
|
}
|
|
67
59
|
async function trySetDefaultCountry() {
|
|
68
60
|
try {
|
|
69
|
-
|
|
61
|
+
// Runs against the just-created scratch org (the default target org). CountryCode only exists
|
|
62
|
+
// with State and Country/Territory picklists enabled, so this query is the feature probe.
|
|
63
|
+
const connection = await getOrgConnection();
|
|
64
|
+
await connection.query('SELECT CountryCode FROM User LIMIT 1');
|
|
70
65
|
logger.info('State/Country picklists detected. Setting default country for user...');
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
'update',
|
|
74
|
-
'record',
|
|
75
|
-
'--sobject',
|
|
76
|
-
'User',
|
|
77
|
-
'--where',
|
|
78
|
-
"Name='User User'",
|
|
79
|
-
'--values',
|
|
80
|
-
"CountryCode='US'",
|
|
81
|
-
]);
|
|
66
|
+
const { Id } = await connection.singleRecordQuery("SELECT Id FROM User WHERE Name = 'User User'");
|
|
67
|
+
await connection.sobject('User').update({ Id, CountryCode: 'US' });
|
|
82
68
|
logger.success('Default country set.');
|
|
83
69
|
}
|
|
84
70
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createScratchOrg.js","sourceRoot":"","sources":["../../src/build/createScratchOrg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAoB,MAAM,uBAAuB,CAAC;AACtG,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"createScratchOrg.js","sourceRoot":"","sources":["../../src/build/createScratchOrg.ts"],"names":[],"mappings":"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"}
|
package/lib/sfOrg.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Connection } from '@salesforce/core';
|
|
1
2
|
/**
|
|
2
3
|
* The org's instance URL, e.g. `https://my-domain.my.salesforce.com`.
|
|
3
4
|
*
|
|
@@ -11,3 +12,39 @@ export declare function getOrgInstanceUrl(aliasOrUsername?: string): Promise<str
|
|
|
11
12
|
* @param hubAliasOrUsername - The Dev Hub. Must already be authenticated.
|
|
12
13
|
*/
|
|
13
14
|
export declare function getRemainingDailyScratchOrgs(hubAliasOrUsername: string): Promise<number | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* A connection to an authenticated org.
|
|
17
|
+
*
|
|
18
|
+
* @param aliasOrUsername - The org. Omit to use the default target org, as `sf` commands without
|
|
19
|
+
* `--target-org` do.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getOrgConnection(aliasOrUsername?: string): Promise<Connection>;
|
|
22
|
+
/** One failed assignment, shaped like the `failures` entries of `sf org assign … --json`. */
|
|
23
|
+
export type AssignmentFailure = {
|
|
24
|
+
name: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Thrown when any assignment failed; `failures` lists each one. */
|
|
28
|
+
export declare class AssignmentError extends Error {
|
|
29
|
+
readonly failures: AssignmentFailure[];
|
|
30
|
+
constructor(message: string, failures: AssignmentFailure[]);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Assigns permission sets to an org's own user, one at a time: what `sf org assign permset --name …`
|
|
34
|
+
* does. An already-assigned permission set is a failure, as it is for the CLI.
|
|
35
|
+
*
|
|
36
|
+
* @param permissionSetNames - Permission set API names.
|
|
37
|
+
* @param aliasOrUsername - The org. Omit to use the default target org.
|
|
38
|
+
* @throws {AssignmentError} If any assignment failed; the others are still attempted.
|
|
39
|
+
*/
|
|
40
|
+
export declare function assignPermissionSets(permissionSetNames: string[], aliasOrUsername?: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Assigns permission set licenses to an org's own user: what `sf org assign permset-license --name …`
|
|
43
|
+
* does. A license is matched by `DeveloperName` or `MasterLabel`; one that is already assigned is
|
|
44
|
+
* a success (with a warning), as it is for the CLI.
|
|
45
|
+
*
|
|
46
|
+
* @param licenseNames - Permission set license developer names or labels.
|
|
47
|
+
* @param aliasOrUsername - The org. Omit to use the default target org.
|
|
48
|
+
* @throws {AssignmentError} If any assignment failed; the others are still attempted.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assignPermissionSetLicenses(licenseNames: string[], aliasOrUsername?: string): Promise<void>;
|
package/lib/sfOrg.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { Org } from '@salesforce/core';
|
|
16
|
+
import { Org, User } from '@salesforce/core';
|
|
17
|
+
import { escapeSoqlLiteral } from '@simplysf/simply-core';
|
|
18
|
+
import { logger } from './logger.js';
|
|
17
19
|
/**
|
|
18
20
|
* The org's instance URL, e.g. `https://my-domain.my.salesforce.com`.
|
|
19
21
|
*
|
|
@@ -36,4 +38,100 @@ export async function getRemainingDailyScratchOrgs(hubAliasOrUsername) {
|
|
|
36
38
|
const limits = (await org.getConnection().limits());
|
|
37
39
|
return limits.DailyScratchOrgs?.Remaining;
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A connection to an authenticated org.
|
|
43
|
+
*
|
|
44
|
+
* @param aliasOrUsername - The org. Omit to use the default target org, as `sf` commands without
|
|
45
|
+
* `--target-org` do.
|
|
46
|
+
*/
|
|
47
|
+
export async function getOrgConnection(aliasOrUsername) {
|
|
48
|
+
const org = await Org.create({ aliasOrUsername });
|
|
49
|
+
return org.getConnection();
|
|
50
|
+
}
|
|
51
|
+
/** Thrown when any assignment failed; `failures` lists each one. */
|
|
52
|
+
export class AssignmentError extends Error {
|
|
53
|
+
failures;
|
|
54
|
+
constructor(message, failures) {
|
|
55
|
+
super(`${message}: ${JSON.stringify(failures)}`);
|
|
56
|
+
this.failures = failures;
|
|
57
|
+
this.name = 'AssignmentError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function resolveOrgUser(aliasOrUsername) {
|
|
61
|
+
const org = await Org.create({ aliasOrUsername });
|
|
62
|
+
const username = org.getUsername() ?? '';
|
|
63
|
+
const { Id: userId } = await org
|
|
64
|
+
.getConnection()
|
|
65
|
+
.singleRecordQuery(`SELECT Id FROM User WHERE Username = '${escapeSoqlLiteral(username)}'`);
|
|
66
|
+
return { org, username, userId };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Assigns permission sets to an org's own user, one at a time: what `sf org assign permset --name …`
|
|
70
|
+
* does. An already-assigned permission set is a failure, as it is for the CLI.
|
|
71
|
+
*
|
|
72
|
+
* @param permissionSetNames - Permission set API names.
|
|
73
|
+
* @param aliasOrUsername - The org. Omit to use the default target org.
|
|
74
|
+
* @throws {AssignmentError} If any assignment failed; the others are still attempted.
|
|
75
|
+
*/
|
|
76
|
+
export async function assignPermissionSets(permissionSetNames, aliasOrUsername) {
|
|
77
|
+
const { org, username, userId } = await resolveOrgUser(aliasOrUsername);
|
|
78
|
+
const user = await User.create({ org });
|
|
79
|
+
const failures = [];
|
|
80
|
+
for (const permissionSetName of permissionSetNames) {
|
|
81
|
+
try {
|
|
82
|
+
// eslint-disable-next-line no-await-in-loop -- one at a time, matching the CLI
|
|
83
|
+
await user.assignPermissionSets(userId, [permissionSetName]);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
failures.push({ name: username, message: error.message });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (failures.length > 0) {
|
|
90
|
+
throw new AssignmentError('Permission set assignment failed', failures);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Assigns permission set licenses to an org's own user: what `sf org assign permset-license --name …`
|
|
95
|
+
* does. A license is matched by `DeveloperName` or `MasterLabel`; one that is already assigned is
|
|
96
|
+
* a success (with a warning), as it is for the CLI.
|
|
97
|
+
*
|
|
98
|
+
* @param licenseNames - Permission set license developer names or labels.
|
|
99
|
+
* @param aliasOrUsername - The org. Omit to use the default target org.
|
|
100
|
+
* @throws {AssignmentError} If any assignment failed; the others are still attempted.
|
|
101
|
+
*/
|
|
102
|
+
export async function assignPermissionSetLicenses(licenseNames, aliasOrUsername) {
|
|
103
|
+
const { org, username, userId } = await resolveOrgUser(aliasOrUsername);
|
|
104
|
+
const connection = org.getConnection();
|
|
105
|
+
const failures = [];
|
|
106
|
+
for (const licenseName of licenseNames) {
|
|
107
|
+
const literal = escapeSoqlLiteral(licenseName);
|
|
108
|
+
let licenseId;
|
|
109
|
+
try {
|
|
110
|
+
// eslint-disable-next-line no-await-in-loop -- one at a time, keeps failures in input order
|
|
111
|
+
({ Id: licenseId } = await connection.singleRecordQuery(`SELECT Id FROM PermissionSetLicense WHERE DeveloperName = '${literal}' OR MasterLabel = '${literal}'`));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
failures.push({ name: username, message: `PermissionSetLicense not found: ${licenseName}` });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
// eslint-disable-next-line no-await-in-loop -- see above
|
|
119
|
+
await connection
|
|
120
|
+
.sobject('PermissionSetLicenseAssign')
|
|
121
|
+
.create({ AssigneeId: userId, PermissionSetLicenseId: licenseId });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = error.message;
|
|
125
|
+
if (message.startsWith('duplicate value found')) {
|
|
126
|
+
logger.warn(`Permission set license ${licenseName} is already assigned to ${username}.`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
failures.push({ name: username, message });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (failures.length > 0) {
|
|
134
|
+
throw new AssignmentError('Permission set license assignment failed', failures);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
39
137
|
//# sourceMappingURL=sfOrg.js.map
|
package/lib/sfOrg.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sfOrg.js","sourceRoot":"","sources":["../src/sfOrg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"sfOrg.js","sourceRoot":"","sources":["../src/sfOrg.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysf/simply-cicd-core",
|
|
3
3
|
"description": "The library layer behind @simplysf/simply-cicd: build/deploy/notify pipeline stages, sfdx-dependabot, ALM issue linking (Jira, GitLab Issues) and VCS API clients (GitHub, GitLab) — published separately for direct consumption (e.g. a bot or script) without the CLI framework",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"author": "@ClayChipps",
|
|
6
6
|
"homepage": "https://github.com/SimplySF/simply-plugins-core",
|
|
7
7
|
"bugs": "https://github.com/SimplySF/simply-plugins-core/issues",
|
|
@@ -139,5 +139,5 @@
|
|
|
139
139
|
"output": []
|
|
140
140
|
}
|
|
141
141
|
},
|
|
142
|
-
"gitHead": "
|
|
142
|
+
"gitHead": "25a84dc5349f2f3c5a15aba2f71ac79a42e9b1a3"
|
|
143
143
|
}
|