Skip to content

Commit

Permalink
feat: v2 change case API
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed May 24, 2024
1 parent b6b5e7f commit 26de260
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 159 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
module.exports = {
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/migration'],
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/recommended'],
rules: {
camelcase: 'off',
'sf-plugin/get-connection-with-version': 'off',
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
40 changes: 8 additions & 32 deletions messages/changecase.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,19 @@
# check.description
# flags.changecaseid.summary

check the status of a change case record
change case id

# create.description
# flags.dry-run.summary

create a change case record based on a template ID with one implementation step
run the command without making any API calls - all calls will be 'successful'

# create.flags.templateid.description
# NoOrgError

change case template id
The command needs either a target org specified via flag or an environment variable: %s

# create.flags.release.description
# flags.release.summary

schedule build of the new release

# create.flags.location.description
# flags.location.summary

url of the source control location

# create.flags.configurationitem.description

Full path from the configuration item, ex: Salesforce.SF_Off_Core.DeveloperTools.NPM

# close.description

stops the implementation steps, and closes the change case record

# command.flags.changecaseid.description

change case id

# command.flags.dryrun.description

run the command without making any API calls - all calls will be 'successful'

# close.flags.status.summary

What the status of the implementation steps should be set to

# NoOrgError

The command needs either a target org specified via flag or an environment variable: %s
3 changes: 3 additions & 0 deletions messages/check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# summary

Checks if the change case (for the release and location) is currently blocked by a moratorium (that is, it's pre-approved and scheduled)
7 changes: 7 additions & 0 deletions messages/close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# summary

Close a change case and stop its implementation steps.

# flags.status.summary

What the status of the implementation steps should be set to.
11 changes: 11 additions & 0 deletions messages/create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# summary

create a change case record based on a template ID with one implementation step

# flags.template-id.summary

change case template id

# flags.configuration-item.summary

Full path from the configuration item, ex: Salesforce.SF_Off_Core.DeveloperTools.NPM
13 changes: 1 addition & 12 deletions src/changeCaseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { Connection, SfError } from '@salesforce/core';
import { Ux } from '@salesforce/sf-plugins-core';
import { Case, ChangeCaseApiResponse, CreateCaseResponse, Implementation } from './types.js';
import { Case, Implementation } from './types.js';

const retrieveOrCreateBuildId = async (conn: Connection, ux: Ux, release: string): Promise<string> => {
const buildResults = await conn.query<{ Id: string }>(`SELECT Id FROM ADM_Build__c WHERE Name = '${release}'`);
Expand Down Expand Up @@ -109,14 +109,3 @@ export const retrieveCaseFromIdOrRelease = async ({
return cases[0];
}
};

export const parseErrors = (body: ChangeCaseApiResponse | CreateCaseResponse): string => {
if (body.errors) {
return body.errors.map((error) => error.message).join(',');
}
if (body.results?.length && typeof body.results[0].message === 'string') {
return body.results[0].message;
} else {
throw new SfError('Unexpected error response from change case API');
}
};
6 changes: 4 additions & 2 deletions src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { changeCaseIdFlag, environmentAwareOrgFlag, locationFlag, releaseFlag }
import { getEnvVarFullName } from '../functions.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/change-case-management', 'changecase');
const messages = Messages.loadMessages('@salesforce/change-case-management', 'check');

// ID for Standard Pre Approved
const CHANGE_TYPE_ID = env.getString(getEnvVarFullName('CHANGE_TYPE_ID'), 'a8hB00000004DIzIAM');
Expand All @@ -24,7 +24,9 @@ export type CheckResult = {
type: string;
};
export default class Check extends SfCommand<CheckResult> {
public static readonly summary = messages.getMessage('check.description');
public static readonly deprecated = true;
public static readonly hidden = true;
public static readonly summary = messages.getMessage('summary');
public static readonly examples = [];
public static readonly flags = {
'target-org': environmentAwareOrgFlag({ required: true }),
Expand Down
80 changes: 25 additions & 55 deletions src/commands/close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core';
import { Connection, Messages, SfError } from '@salesforce/core';
import { AnyJson } from '@salesforce/ts-types';
import { parseErrors, retrieveCaseFromIdOrRelease, retrieveImplementationFromCase } from '../changeCaseApi.js';
import { Implementation, ChangeCaseApiResponse, StartApiResponse } from '../types.js';
import { retrieveCaseFromIdOrRelease } from '../changeCaseApi.js';
import { ChangeCaseCloseApiResponse } from '../types.js';
import { getEnvVarFullName } from '../functions.js';
import { changeCaseIdFlag, dryrunFlag, environmentAwareOrgFlag, locationFlag, releaseFlag } from '../flags.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/change-case-management', 'changecase');
const messages = Messages.loadMessages('@salesforce/change-case-management', 'close');

export type CloseResult = {
case: {
Expand All @@ -23,8 +23,7 @@ export type CloseResult = {
};
};
export default class Close extends SfCommand<AnyJson> {
public static readonly summary = messages.getMessage('close.description');
public static readonly description = messages.getMessage('close.description');
public static readonly summary = messages.getMessage('summary');

public static readonly examples = [];

Expand All @@ -34,7 +33,8 @@ export default class Close extends SfCommand<AnyJson> {
release: releaseFlag,
location: locationFlag,
status: Flags.string({
summary: messages.getMessage('close.flags.status.summary'),
// eslint-disable-next-line sf-plugin/no-hardcoded-messages-flags
summary: messages.getMessage('flags.status.summary'),
char: 's',
default: 'Implemented - per plan',
options: ['Implemented - per plan', 'Not Implemented', 'Rolled back - with no impact'],
Expand All @@ -58,62 +58,32 @@ export default class Close extends SfCommand<AnyJson> {
})
).Id;

const implementationSteps = await retrieveImplementationFromCase(conn, changeCaseId);

if (!flags['dry-run']) {
await this.stopImplementation(flags.status, implementationSteps, conn);
await this.closeCase(flags.status, changeCaseId, conn);
(await closeCase(flags.status, changeCaseId, conn)).map((msg) => this.log(msg));
} else {
this.log('Case will not be closed because of the dryrun flag.');
}

// delete the config file, until the next release

return { case: { Id: changeCaseId, Status: flags.status } };
}
}

private async closeCase(status: string, changecaseid: string, conn: Connection): Promise<void> {
// close the case
const closeBody = {
cases: [{ Id: changecaseid }],
};

const closeResult = await conn.request<ChangeCaseApiResponse>(
{
method: 'PATCH',
url: '/services/apexrest/change-management/v1/change-cases/close',
body: JSON.stringify(closeBody),
},
{ responseType: 'application/json' }
);

if (closeResult.results && closeResult.results[0].success === false) {
throw new SfError(`Stoping the implementation steps failed with ${parseErrors(closeResult)}`);
}

this.log(`Release ${closeResult.results[0].id} set to ${status}.`);
const closeCase = async (status: string, changecaseid: string, conn: Connection): Promise<string[]> => {
// close the case
const closeBody = { cases: [{ Id: changecaseid }] };

const closeResult = await conn.request<ChangeCaseCloseApiResponse>(
{
method: 'PATCH',
url: '/services/apexrest/change-management/v2/change-cases/close',
body: JSON.stringify(closeBody),
},
{ responseType: 'application/json' }
);

if (closeResult.hasErrors) {
throw new SfError(`Stoping the implementation steps failed with ${JSON.stringify(closeResult)}`);
}

private async stopImplementation(status: string, steps: Implementation[], conn: Connection): Promise<void> {
// stop the implementation steps
// add the status to the implementation steps
const implementationsToStop = {
implementationSteps: steps.map((step) => ({ Id: step.Id, Status__c: status })),
};

const stopResult = await conn.request<StartApiResponse>(
{
method: 'PATCH',
url: '/services/apexrest/change-management/v1/implementation-steps/stop',
body: JSON.stringify(implementationsToStop),
},
{ responseType: 'application/json' }
);

if (stopResult.results && stopResult.results[0].success === false) {
throw new SfError(`Stoping the implementation steps failed with ${parseErrors(stopResult)}`);
}

this.log(`Successfully stopped implementation steps ${steps[0].Id}`);
}
}
return closeResult.results.map((r) => `Release ${r.id} set to ${status}.`);
};
38 changes: 16 additions & 22 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { Interfaces } from '@oclif/core';
import { Step, StartApiResponse, CreateCaseResponse, Implementation, CaseWithImpl, Case } from '../types.js';
import { getEnvVarFullName } from '../functions.js';
import { dryrunFlag, environmentAwareOrgFlag, locationFlag, releaseFlag } from '../flags.js';
import { parseErrors, retrieveOrCreateReleaseId } from '../changeCaseApi.js';
import { retrieveOrCreateReleaseId } from '../changeCaseApi.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/change-case-management', 'changecase');
const messages = Messages.loadMessages('@salesforce/change-case-management', 'create');

const CHANGE_RECORD_TYPE_ID = env.getString(getEnvVarFullName('CHANGE_RECORD_TYPE_ID'), '012B000000009fBIAQ');
const CHANGE_TEMPLATE_RECORD_TYPE_ID = env.getString(
Expand Down Expand Up @@ -58,16 +58,15 @@ export type CreateResponse = {
record: CaseWithImpl;
};
export default class Create extends SfCommand<CreateResponse> {
public static readonly summary = messages.getMessage('create.description');
public static readonly description = messages.getMessage('create.description');
public static readonly summary = messages.getMessage('summary');

public static readonly examples = [];

public static readonly flags = {
'target-org': environmentAwareOrgFlag({ required: true }),
'template-id': Flags.salesforceId({
length: 'both',
summary: messages.getMessage('create.flags.templateid.description'),
summary: messages.getMessage('flags.template-id.summary'),
char: 'i',
required: true,
startsWith: '500',
Expand All @@ -78,7 +77,7 @@ export default class Create extends SfCommand<CreateResponse> {
release: releaseFlag,
location: locationFlag,
'configuration-item': Flags.string({
summary: messages.getMessage('create.flags.configurationitem.description'),
summary: messages.getMessage('flags.configuration-item.summary'),
required: true,
char: 'c',
env: getEnvVarFullName('CONFIGURATION_ITEM'),
Expand Down Expand Up @@ -109,13 +108,16 @@ export default class Create extends SfCommand<CreateResponse> {
}

private async startImplementations(createRes: CreateCaseResponse, conn: Connection): Promise<Step[]> {
if (createRes.success === false) {
throw new SfError(`Creating release failed with ${JSON.stringify(createRes)}`);
}
const implementationsToStart = generateImplementations(createRes.implementationSteps);

// start the implementation steps
const startResult = await conn.request<StartApiResponse>(
{
method: 'PATCH',
url: '/services/apexrest/change-management/v1/implementation-steps/start',
url: '/services/apexrest/change-management/v2/implementation-steps/start',
body: JSON.stringify(implementationsToStart),
},
{ responseType: 'application/json' }
Expand All @@ -129,11 +131,7 @@ export default class Create extends SfCommand<CreateResponse> {
}
this.styledJSON(startResult);

throw new SfError(
`Starting release failed with ${startResult.results
.map((result) => result.errors?.map((error) => error.message?.message).join(','))
.join(',')}`
);
throw new SfError(`Starting release failed with ${JSON.stringify(startResult.results)}}`);
}

private async createCase(record: CaseWithImpl, conn: Connection): Promise<CreateCaseResponse> {
Expand All @@ -142,7 +140,7 @@ export default class Create extends SfCommand<CreateResponse> {
const createResult = await conn.request<CreateCaseResponse>(
{
method: 'POST',
url: conn.instanceUrl + '/services/apexrest/change-management/v1/change-cases',
url: conn.instanceUrl + '/services/apexrest/change-management/v2/change-cases',
body: JSON.stringify(record),
},
{ responseType: 'application/json' }
Expand All @@ -155,11 +153,11 @@ export default class Create extends SfCommand<CreateResponse> {
return createResult;
}

throw new SfError(`Creating release failed with ${parseErrors(createResult)}`);
throw new SfError(`Creating release failed with ${JSON.stringify(createResult.errors)}`);
} catch (e) {
const err = e as Error;
const error = JSON.parse(err.message) as CreateCaseResponse;
throw new SfError(`Creating release failed with ${parseErrors(error)}`);
throw new SfError(`Creating release failed with ${JSON.stringify(error)}`);
}
}

Expand Down Expand Up @@ -195,10 +193,6 @@ export default class Create extends SfCommand<CreateResponse> {
record.Status = 'Approved, Scheduled';
record.SM_Risk_Level__c = 'Low';

const startTime = new Date();
// set the estimated end time 10 minutes in the future
const endTime = new Date(startTime.getTime() + 10 * 60000);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const identity = await conn.identity();
return {
change: record as Case,
Expand All @@ -207,12 +201,12 @@ export default class Create extends SfCommand<CreateResponse> {
Description__c: 'releasing the salesforce CLI',
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
OwnerId: identity.user_id,
SM_Estimated_Start_Time__c: startTime.toISOString(),
SM_Estimated_End_Time__c: endTime.toISOString(),
Planned_Start_Time__c: new Date().toISOString(),
Planned_Duration_In_Hours__c: 0.25,
SM_Implementation_Steps__c: 'N/A',
Configuration_Item_Path_List__c: this.flags['configuration-item'],
SM_Infrastructure_Type__c: 'Off Core',
} as Implementation,
} satisfies Implementation,
],
} as CaseWithImpl;
}
Expand Down
Loading

0 comments on commit 26de260

Please sign in to comment.