Skip to content

Commit 895cb47

Browse files
Copilotpelikhan
andauthored
Apply remaining changes
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent a529799 commit 895cb47

25 files changed

Lines changed: 1540 additions & 11 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
const { createAzureDevOpsWorkItemHandler } = require("./azure_devops_work_items.cjs");
3+
async function main(config = {}) {
4+
return createAzureDevOpsWorkItemHandler("assign_work_item", config);
5+
}
6+
module.exports = { main };

actions/setup/js/azure_devops_work_items.cjs

Lines changed: 493 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
const { createAzureDevOpsWorkItemHandler } = require("./azure_devops_work_items.cjs");
3+
async function main(config = {}) {
4+
return createAzureDevOpsWorkItemHandler("comment_on_work_item", config);
5+
}
6+
module.exports = { main };
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
const { createAzureDevOpsWorkItemHandler } = require("./azure_devops_work_items.cjs");
3+
async function main(config = {}) {
4+
return createAzureDevOpsWorkItemHandler("create_work_item", config);
5+
}
6+
module.exports = { main };
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @ts-check
2+
const { createAzureDevOpsWorkItemHandler } = require("./azure_devops_work_items.cjs");
3+
async function main(config = {}) {
4+
return createAzureDevOpsWorkItemHandler("link_work_items", config);
5+
}
6+
module.exports = { main };

actions/setup/js/mcp_server_core.cjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,11 +487,15 @@ function loadToolHandlers(server, tools, basePath) {
487487
*/
488488
function registerTool(server, tool) {
489489
const normalizedName = normalizeTool(tool.name);
490+
const existing = server.tools[normalizedName];
491+
if (existing && existing.name !== tool.name) {
492+
throw new Error(`Tool name collision: '${existing.name}' and '${tool.name}' both normalize to '${normalizedName}'`);
493+
}
490494
server.tools[normalizedName] = {
491495
...tool,
492-
name: normalizedName,
496+
name: tool.name,
493497
};
494-
server.debug(`Registered tool: ${normalizedName}`);
498+
server.debug(`Registered tool: ${tool.name}`);
495499
}
496500

497501
/**

actions/setup/js/safe_output_handler_manager.cjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ const HANDLER_MAP = {
8383
report_incomplete: "./report_incomplete_handler.cjs",
8484
create_report_incomplete_issue: "./create_report_incomplete_issue.cjs",
8585
create_project: "./create_project.cjs",
86+
create_work_item: "./create_work_item.cjs",
87+
update_work_item: "./update_work_item.cjs",
88+
comment_on_work_item: "./comment_on_work_item.cjs",
89+
assign_work_item: "./assign_work_item.cjs",
90+
link_work_items: "./link_work_items.cjs",
91+
upload_workitem_attachment: "./upload_workitem_attachment.cjs",
8692
create_project_status_update: "./create_project_status_update.cjs",
8793
update_project: "./update_project.cjs",
8894
upload_artifact: "./upload_artifact.cjs",
@@ -145,6 +151,8 @@ const THREAT_WARNING_REVIEWABLE_TYPES = new Set([
145151
"missing_data",
146152
"create_report_incomplete_issue",
147153
"report_incomplete",
154+
"create_work_item",
155+
"comment_on_work_item",
148156
]);
149157

150158
/**
@@ -193,6 +201,10 @@ const THREAT_WARNING_ABORT_TYPES = new Set([
193201
"call_workflow",
194202
"autofix_code_scanning_alert",
195203
"create_agent_session",
204+
"update_work_item",
205+
"assign_work_item",
206+
"link_work_items",
207+
"upload_workitem_attachment",
196208
]);
197209

198210
/**
@@ -1095,6 +1107,11 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
10951107
});
10961108
core.info(`Registered temporary ID: ${result.temporaryId} -> ${result.repo}#${result.number}`);
10971109
}
1110+
if (result && result.temporaryId && result.temporaryIdEntry) {
1111+
const normalizedTempId = normalizeTemporaryId(result.temporaryId);
1112+
temporaryIdMap.set(normalizedTempId, result.temporaryIdEntry);
1113+
core.info(`Registered Azure DevOps temporary ID: ${result.temporaryId}`);
1114+
}
10981115

10991116
// If this was a successful upload_artifact, register the artifact URL so that
11001117
// subsequent messages can have '#aw_ID' references replaced with the real URL.
@@ -1285,6 +1302,10 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
12851302
originalTempIdMapSize: tempIdMapSizeBefore,
12861303
});
12871304
}
1305+
if (result && result.temporaryId && result.temporaryIdEntry) {
1306+
const normalizedTempId = normalizeTemporaryId(result.temporaryId);
1307+
temporaryIdMap.set(normalizedTempId, result.temporaryIdEntry);
1308+
}
12881309
}
12891310

12901311
// Update the result to success

actions/setup/js/safe_outputs_handlers.cjs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
2020
const { ERR_CONFIG, ERR_PARSE, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs");
2121
const { findRepoCheckout } = require("./find_repo_checkout.cjs");
2222
const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs");
23-
const { getOrGenerateTemporaryId } = require("./temporary_id.cjs");
23+
const { generateTemporaryId, getOrGenerateTemporaryId } = require("./temporary_id.cjs");
2424
const { parseAllowedExtensionsEnv } = require("./allowed_extensions_helpers.cjs");
2525
const { getStagedPatchDiffSizeBytes } = require("./git_patch_utils.cjs");
2626
const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs");
@@ -101,7 +101,7 @@ function readJSONFile(filePath) {
101101

102102
const safeOutputsTools = readJSONFile(path.join(__dirname, "safe_outputs_tools.json"));
103103

104-
const safeOutputsToolMap = new Map(safeOutputsTools.map(tool => [tool.name, tool]));
104+
const safeOutputsToolMap = new Map(safeOutputsTools.map(tool => [tool.name.replace(/-/g, "_"), tool]));
105105

106106
/**
107107
* @param {string} error
@@ -2063,6 +2063,89 @@ function createHandlers(server, appendSafeOutput, config = {}) {
20632063
],
20642064
isError: true,
20652065
};
2066+
2067+
const createWorkItemHandler = args => {
2068+
const temporaryId = `#${generateTemporaryId()}`;
2069+
const entry = { ...(args || {}), type: "create_work_item", temporary_id: temporaryId };
2070+
appendSafeOutputCounted(entry);
2071+
const output = { result: "success", temporary_id: temporaryId };
2072+
return {
2073+
content: [{ type: "text", text: JSON.stringify(output) }],
2074+
structuredContent: output,
2075+
};
2076+
};
2077+
2078+
const createAzureDevOpsWorkItemHandler = type => args => {
2079+
const entry = { ...(args || {}), type };
2080+
appendSafeOutputCounted(entry);
2081+
return {
2082+
content: [{ type: "text", text: JSON.stringify({ result: "success" }) }],
2083+
};
2084+
};
2085+
2086+
const uploadWorkItemAttachmentHandler = args => {
2087+
const entry = { ...(args || {}), type: "upload_workitem_attachment" };
2088+
const rawPath = typeof entry.file_path === "string" ? entry.file_path.trim() : "";
2089+
if (!rawPath || path.isAbsolute(rawPath) || rawPath.includes(":")) {
2090+
return buildIntentErrorResponse("upload-workitem-attachment file_path must be a workspace-relative path without ':'");
2091+
}
2092+
2093+
const segments = rawPath.split(/[\\/]+/);
2094+
if (segments.some(segment => !segment || segment === "." || segment === "..")) {
2095+
return buildIntentErrorResponse("upload-workitem-attachment file_path must not contain empty, '.' or '..' path segments");
2096+
}
2097+
2098+
const workspace = path.resolve(process.env.GITHUB_WORKSPACE || process.cwd());
2099+
const sourcePath = path.resolve(workspace, ...segments);
2100+
if (sourcePath !== workspace && !sourcePath.startsWith(workspace + path.sep)) {
2101+
return buildIntentErrorResponse("upload-workitem-attachment file_path resolves outside the workspace");
2102+
}
2103+
2104+
let current = workspace;
2105+
let sourceStat;
2106+
try {
2107+
for (const segment of segments) {
2108+
current = path.join(current, segment);
2109+
sourceStat = lstatGuard(current);
2110+
if (!sourceStat) {
2111+
return buildIntentErrorResponse("upload-workitem-attachment does not accept symbolic links");
2112+
}
2113+
}
2114+
} catch (error) {
2115+
return buildIntentErrorResponse(`upload-workitem-attachment could not read file_path: ${getErrorMessage(error)}`);
2116+
}
2117+
if (!sourceStat?.isFile()) {
2118+
return buildIntentErrorResponse("upload-workitem-attachment file_path must identify one regular file");
2119+
}
2120+
2121+
const attachmentConfig = getSafeOutputsToolConfig(config, "upload_workitem_attachment");
2122+
const maxFileSize = Number(attachmentConfig.max_file_size || 5 * 1024 * 1024);
2123+
if (!Number.isSafeInteger(maxFileSize) || maxFileSize < 1 || sourceStat.size > maxFileSize) {
2124+
return buildIntentErrorResponse(`upload-workitem-attachment file exceeds the configured max-file-size of ${maxFileSize} bytes`);
2125+
}
2126+
const allowedExtensions = Array.isArray(attachmentConfig.allowed_extensions) ? attachmentConfig.allowed_extensions : [];
2127+
if (allowedExtensions.length > 0 && !allowedExtensions.some(extension => rawPath.toLowerCase().endsWith(String(extension).toLowerCase()))) {
2128+
return buildIntentErrorResponse("upload-workitem-attachment file extension is not allowed by the workflow configuration");
2129+
}
2130+
2131+
try {
2132+
const stagingRoot = path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw", "safeoutputs", "upload-artifacts");
2133+
const stagingDirectory = path.join(stagingRoot, "azure-devops-work-items");
2134+
fs.mkdirSync(stagingDirectory, { recursive: true, mode: 0o700 });
2135+
const stagedName = `${crypto.randomUUID()}-${path.basename(rawPath)}`;
2136+
const stagedPath = path.join(stagingDirectory, stagedName);
2137+
fs.copyFileSync(sourcePath, stagedPath, fs.constants.COPYFILE_EXCL);
2138+
fs.chmodSync(stagedPath, 0o600);
2139+
entry.staged_file = path.posix.join("azure-devops-work-items", stagedName);
2140+
} catch (error) {
2141+
throw new Error(`${ERR_SYSTEM}: Failed to stage Azure DevOps work-item attachment: ${getErrorMessage(error)}`, { cause: error });
2142+
}
2143+
2144+
appendSafeOutputCounted(entry);
2145+
return {
2146+
content: [{ type: "text", text: JSON.stringify({ result: "success", file_path: rawPath }) }],
2147+
};
2148+
};
20662149
}
20672150
const resolvedRepo = repoResult.repo;
20682151

@@ -3117,6 +3200,12 @@ function createHandlers(server, appendSafeOutput, config = {}) {
31173200
pushToPullRequestBranchHandler,
31183201
pushRepoMemoryHandler,
31193202
createIssueHandler,
3203+
createWorkItemHandler,
3204+
updateWorkItemHandler: createAzureDevOpsWorkItemHandler("update_work_item"),
3205+
commentOnWorkItemHandler: createAzureDevOpsWorkItemHandler("comment_on_work_item"),
3206+
assignWorkItemHandler: createAzureDevOpsWorkItemHandler("assign_work_item"),
3207+
linkWorkItemsHandler: createAzureDevOpsWorkItemHandler("link_work_items"),
3208+
uploadWorkItemAttachmentHandler,
31203209
createProjectHandler,
31213210
addCommentHandler,
31223211
createPullRequestReviewCommentHandler,

actions/setup/js/safe_outputs_tools.json

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,5 +2082,146 @@
20822082
"anyOf": ["pull_request_number", "pr_number", "pr", "pull_number"]
20832083
}
20842084
}
2085+
},
2086+
{
2087+
"name": "create-work-item",
2088+
"description": "Create an Azure DevOps work item and return a temporary #aw_ ID for later work-item tools in this run.",
2089+
"inputSchema": {
2090+
"type": "object",
2091+
"required": ["title", "description"],
2092+
"properties": {
2093+
"title": {
2094+
"type": "string",
2095+
"minLength": 6,
2096+
"maxLength": 255,
2097+
"description": "Concise work-item title."
2098+
},
2099+
"description": {
2100+
"type": "string",
2101+
"minLength": 31,
2102+
"maxLength": 65000,
2103+
"description": "Detailed work-item description in Markdown."
2104+
},
2105+
"tags": {
2106+
"type": "array",
2107+
"items": { "type": "string", "minLength": 1, "maxLength": 256 },
2108+
"description": "Optional tags. Tags cannot contain semicolons and may be restricted by allowed-tags."
2109+
}
2110+
},
2111+
"additionalProperties": false
2112+
}
2113+
},
2114+
{
2115+
"name": "update-work-item",
2116+
"description": "Update explicitly enabled fields on an Azure DevOps work item.",
2117+
"inputSchema": {
2118+
"type": "object",
2119+
"required": ["id"],
2120+
"properties": {
2121+
"id": {
2122+
"type": ["number", "string"],
2123+
"description": "Positive work-item ID or a temporary #aw_ ID returned by create-work-item."
2124+
},
2125+
"title": { "type": "string", "minLength": 1, "maxLength": 255 },
2126+
"body": { "type": "string", "maxLength": 65000 },
2127+
"state": { "type": "string", "minLength": 1, "maxLength": 128 },
2128+
"area_path": { "type": "string", "minLength": 1, "maxLength": 512 },
2129+
"iteration_path": { "type": "string", "minLength": 1, "maxLength": 512 },
2130+
"assignee": { "type": "string", "minLength": 1, "maxLength": 256 },
2131+
"tags": {
2132+
"type": "array",
2133+
"items": { "type": "string", "minLength": 1, "maxLength": 256 }
2134+
}
2135+
},
2136+
"additionalProperties": false
2137+
}
2138+
},
2139+
{
2140+
"name": "comment-on-work-item",
2141+
"description": "Add a Markdown comment to an explicitly scoped Azure DevOps work item.",
2142+
"inputSchema": {
2143+
"type": "object",
2144+
"required": ["work_item_id", "body"],
2145+
"properties": {
2146+
"work_item_id": {
2147+
"type": ["number", "string"],
2148+
"description": "Positive work-item ID or a temporary #aw_ ID returned by create-work-item."
2149+
},
2150+
"body": {
2151+
"type": "string",
2152+
"minLength": 10,
2153+
"maxLength": 65000,
2154+
"description": "Comment text in Markdown."
2155+
}
2156+
},
2157+
"additionalProperties": false
2158+
}
2159+
},
2160+
{
2161+
"name": "assign-work-item",
2162+
"description": "Assign an allowed Azure DevOps identity to a work item.",
2163+
"inputSchema": {
2164+
"type": "object",
2165+
"required": ["work_item_id", "assignee"],
2166+
"properties": {
2167+
"work_item_id": {
2168+
"type": ["number", "string"],
2169+
"description": "Positive work-item ID or a temporary #aw_ ID returned by create-work-item."
2170+
},
2171+
"assignee": {
2172+
"type": "string",
2173+
"minLength": 1,
2174+
"maxLength": 256,
2175+
"description": "Azure DevOps identity, such as an email address or display name."
2176+
}
2177+
},
2178+
"additionalProperties": false
2179+
}
2180+
},
2181+
{
2182+
"name": "link-work-items",
2183+
"description": "Create a relationship between two explicitly scoped Azure DevOps work items.",
2184+
"inputSchema": {
2185+
"type": "object",
2186+
"required": ["source_id", "target_id", "link_type"],
2187+
"properties": {
2188+
"source_id": {
2189+
"type": ["number", "string"],
2190+
"description": "Positive source work-item ID or a temporary #aw_ ID."
2191+
},
2192+
"target_id": {
2193+
"type": ["number", "string"],
2194+
"description": "Positive target work-item ID or a temporary #aw_ ID."
2195+
},
2196+
"link_type": {
2197+
"type": "string",
2198+
"enum": ["parent", "child", "related", "predecessor", "successor", "duplicate", "duplicate-of"]
2199+
},
2200+
"comment": { "type": "string", "minLength": 5, "maxLength": 1024 }
2201+
},
2202+
"additionalProperties": false
2203+
}
2204+
},
2205+
{
2206+
"name": "upload-workitem-attachment",
2207+
"description": "Upload one workspace file and attach it to an Azure DevOps work item.",
2208+
"inputSchema": {
2209+
"type": "object",
2210+
"required": ["work_item_id", "file_path"],
2211+
"properties": {
2212+
"work_item_id": {
2213+
"type": ["number", "string"],
2214+
"description": "Positive work-item ID or a temporary #aw_ ID returned by create-work-item."
2215+
},
2216+
"file_path": {
2217+
"type": "string",
2218+
"minLength": 1,
2219+
"maxLength": 1024,
2220+
"description": "Workspace-relative file path. Absolute paths, traversal, colons, and symbolic links are rejected."
2221+
},
2222+
"comment": { "type": "string", "minLength": 3, "maxLength": 1024 }
2223+
},
2224+
"additionalProperties": false
2225+
}
20852226
}
20862227
]

0 commit comments

Comments
 (0)