Skip to content

Commit 8dc021e

Browse files
committed
chore: update workitem status
1 parent 8bd75f3 commit 8dc021e

5 files changed

Lines changed: 453 additions & 1 deletion

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# summary
2+
3+
Update the status of a work item in DevOps Center.
4+
5+
# description
6+
7+
Allowed statuses are "In Progress" and "Ready to Promote".
8+
9+
# flags.work-item-name.summary
10+
11+
Name of the work item, such as WI-000001.
12+
13+
# flags.work-item-id.summary
14+
15+
ID of the work item.
16+
17+
# flags.status.summary
18+
19+
Status to set for the work item. Allowed values: "In Progress", "Ready to Promote".
20+
21+
# examples
22+
23+
- Update a work item status by its name to indicate the work is underway:
24+
25+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --work-item-name WI-000001 --status "In Progress"
26+
27+
- Update a work item status by its ID to indicate the changes are ready for promotion:
28+
29+
<%= config.bin %> <%= command.id %> --target-org my-devops-org --work-item-id 0Wx000000000001 --status "Ready to Promote"

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@
8585
"description": "Commands for managing DevOps Center projects."
8686
},
8787
"work-item": {
88-
"description": "Commands for managing DevOps Center work items."
88+
"description": "Commands for managing DevOps Center work items.",
89+
"subtopics": {
90+
"status": {
91+
"description": "Commands for managing DevOps Center work item statuses."
92+
}
93+
}
8994
}
9095
}
9196
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Messages, Org } from '@salesforce/core';
18+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
19+
import {
20+
resolveWorkItemByName,
21+
resolveProjectIdForWorkItem,
22+
updateWorkItemStatus,
23+
UpdateWorkItemStatusResult,
24+
ALLOWED_STATUSES,
25+
} from '../../../../utils/updateWorkItemStatus.js';
26+
27+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
28+
const messages = Messages.loadMessages('@salesforce/plugin-devops-center', 'devops.work-item.status.update');
29+
const commonErrorMessages = Messages.loadMessages('@salesforce/plugin-devops-center', 'commonErrors');
30+
31+
export default class DevopsWorkItemStatusUpdate extends SfCommand<UpdateWorkItemStatusResult> {
32+
public static readonly summary = messages.getMessage('summary');
33+
public static readonly description = messages.getMessage('description');
34+
public static readonly examples = messages.getMessages('examples');
35+
36+
public static readonly flags = {
37+
'target-org': Flags.requiredOrg(),
38+
'api-version': Flags.orgApiVersion(),
39+
'work-item-name': Flags.string({
40+
summary: messages.getMessage('flags.work-item-name.summary'),
41+
char: 'n',
42+
exactlyOne: ['work-item-name', 'work-item-id'],
43+
}),
44+
'work-item-id': Flags.salesforceId({
45+
summary: messages.getMessage('flags.work-item-id.summary'),
46+
char: 'w',
47+
exactlyOne: ['work-item-name', 'work-item-id'],
48+
}),
49+
status: Flags.string({
50+
summary: messages.getMessage('flags.status.summary'),
51+
required: true,
52+
options: [...ALLOWED_STATUSES],
53+
}),
54+
};
55+
56+
public async run(): Promise<UpdateWorkItemStatusResult> {
57+
const { flags } = await this.parse(DevopsWorkItemStatusUpdate);
58+
const org: Org = flags['target-org'];
59+
const connection = org.getConnection(flags['api-version']);
60+
61+
let workItemId: string;
62+
let projectId: string;
63+
try {
64+
if (flags['work-item-name']) {
65+
const ctx = await resolveWorkItemByName(connection, flags['work-item-name']);
66+
workItemId = ctx.workItemId;
67+
projectId = ctx.projectId;
68+
} else {
69+
workItemId = flags['work-item-id']!;
70+
projectId = await resolveProjectIdForWorkItem(connection, workItemId);
71+
}
72+
} catch (error: unknown) {
73+
const errMsg = error instanceof Error ? error.message : String(error);
74+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
75+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
76+
}
77+
throw error;
78+
}
79+
80+
let result: UpdateWorkItemStatusResult;
81+
try {
82+
result = await updateWorkItemStatus({ connection, workItemId, projectId, status: flags['status'] });
83+
} catch (error: unknown) {
84+
const errMsg = error instanceof Error ? error.message : String(error);
85+
if (errMsg.includes('sObject type') && errMsg.includes('is not supported')) {
86+
this.error(commonErrorMessages.getMessage('error.DevopsCenterNotEnabled'));
87+
}
88+
throw error;
89+
}
90+
91+
if (result.success) {
92+
const identifier = flags['work-item-name'] ?? result.workItemId;
93+
this.log(`Successfully updated status for work item ${identifier} to "${result.status ?? flags['status']}".`);
94+
} else {
95+
this.error(`Failed to update work item status: ${result.error ?? ''}`);
96+
}
97+
98+
return result;
99+
}
100+
}

src/utils/updateWorkItemStatus.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Connection } from '@salesforce/core';
18+
19+
export type WorkItemContext = {
20+
workItemId: string;
21+
projectId: string;
22+
};
23+
24+
export type UpdateWorkItemStatusParams = {
25+
connection: Connection;
26+
workItemId: string;
27+
projectId: string;
28+
status: string;
29+
};
30+
31+
export type UpdateWorkItemStatusResult = {
32+
success: boolean;
33+
workItemId: string;
34+
workItemName?: string;
35+
status?: string;
36+
error?: string;
37+
};
38+
39+
/**
40+
* Resolves a work item's Salesforce ID and project ID from its Name (e.g. WI-000001) via SOQL.
41+
*/
42+
export async function resolveWorkItemByName(connection: Connection, workItemName: string): Promise<WorkItemContext> {
43+
const result = await connection.query<{ Id: string; DevopsProjectId: string }>(
44+
`SELECT Id, DevopsProjectId FROM WorkItem WHERE Name = '${workItemName}' LIMIT 1`
45+
);
46+
const record = (result.records ?? [])[0];
47+
if (!record) {
48+
throw new Error(`Work item with name '${workItemName}' not found.`);
49+
}
50+
return { workItemId: record.Id, projectId: record.DevopsProjectId };
51+
}
52+
53+
/**
54+
* Fetches the project ID for a given work item ID via SOQL.
55+
*/
56+
export async function resolveProjectIdForWorkItem(connection: Connection, workItemId: string): Promise<string> {
57+
const result = await connection.query<{ DevopsProjectId: string }>(
58+
`SELECT DevopsProjectId FROM WorkItem WHERE Id = '${workItemId}' LIMIT 1`
59+
);
60+
const record = (result.records ?? [])[0];
61+
if (!record) {
62+
throw new Error(`Work item with ID '${workItemId}' not found.`);
63+
}
64+
return record.DevopsProjectId;
65+
}
66+
67+
export const ALLOWED_STATUSES = ['In Progress', 'Ready to Promote'] as const;
68+
export type AllowedStatus = (typeof ALLOWED_STATUSES)[number];
69+
70+
const STATUS_LABEL_TO_API: Record<string, string> = {
71+
'in progress': 'IN_PROGRESS',
72+
'ready to promote': 'READY_TO_PROMOTE',
73+
};
74+
75+
export function toApiStatus(status: string): string {
76+
const apiStatus = STATUS_LABEL_TO_API[status.toLowerCase()];
77+
if (!apiStatus) {
78+
throw new Error(`Invalid status "${status}". Allowed values: ${ALLOWED_STATUSES.join(', ')}`);
79+
}
80+
return apiStatus;
81+
}
82+
83+
/**
84+
* Updates the status of a DevOps Center work item via the Connect API.
85+
* API: PATCH /services/data/v{version}/connect/devops/projects/{projectId}/workitem/{workItemId}
86+
*/
87+
export async function updateWorkItemStatus(params: UpdateWorkItemStatusParams): Promise<UpdateWorkItemStatusResult> {
88+
const { connection, workItemId, projectId, status } = params;
89+
90+
const path = `/services/data/v${connection.getApiVersion()}/connect/devops/projects/${projectId}/workitem/${workItemId}`;
91+
const body = JSON.stringify({ status: toApiStatus(status) });
92+
93+
const response = await connection.request({
94+
method: 'PATCH',
95+
url: path,
96+
body,
97+
headers: { 'Content-Type': 'application/json' },
98+
});
99+
100+
const data = (response as Record<string, unknown>) ?? {};
101+
return {
102+
success: true,
103+
workItemId,
104+
status: (data.status ?? data.Status ?? status) as string,
105+
};
106+
}

0 commit comments

Comments
 (0)