create_or_update_release
Create or modify releases in DevOps Plan systems, adding them to projects automatically when specified.
Instructions
Creates a new release or updates an existing release in Plan. If releaseDbid is provided, updates the release; otherwise creates a new one. If projectID is provided, automatically adds the release to the project (atomic operation).
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| application | Yes | Name of the application | |
| projectID | Yes | The ID of the project to automatically add this release to | |
| releaseDbid | No | The dbid of the release to update (optional - omit to create new release) | |
| fields | Yes | Array of fields to set/update. For creation, 'Name' is required. When setting Sprints, always use type='REFERENCE_LIST' and provide sprint names, not dbids. |
Implementation Reference
- src/lib/server.js:798-1022 (handler)The 'create_or_update_release' tool handler definition and implementation. It handles creating a new release or updating an existing one, and optionally adds it to a project.
// Tool to create or update a release server.tool( "create_or_update_release", "Creates a new release or updates an existing release in Plan. If releaseDbid is provided, updates the release; otherwise creates a new one. If projectID is provided, automatically adds the release to the project (atomic operation).", { application: z.string().describe("Name of the application"), projectID: z.string().describe("The ID of the project to automatically add this release to"), releaseDbid: z.string().optional().describe("The dbid of the release to update (optional - omit to create new release)"), fields: z.array(z.object({ name: z.string().describe("Field name (e.g., 'Name', 'ReleaseType', 'Description', 'Frozen', 'Sprints', etc.)"), value: z.string().describe("The new value for the field. IMPORTANT: For REFERENCE_LIST fields like 'Sprints', use NAMES not IDs - provide comma-separated sprint names (e.g., 'Sprint 1,Sprint 2,Sprint 3')."), type: z.string().optional().describe("Field type (e.g., 'SHORT_STRING', 'MULTILINE_STRING', 'REFERENCE_LIST', 'DATE_TIME'). Use 'REFERENCE_LIST' for fields like 'Sprints' that reference other entities. Defaults to 'SHORT_STRING'.") })).describe("Array of fields to set/update. For creation, 'Name' is required. When setting Sprints, always use type='REFERENCE_LIST' and provide sprint names, not dbids.") }, async ({ application, releaseDbid, projectID, fields }) => { try { if (!globalCookies) { globalCookies = await getCookiesFromServer(serverURL); if (!globalCookies) { console.error("Failed to retrieve cookies from server."); return { error: "Failed to retrieve cookies." }; } console.log("Received Cookies:", globalCookies); } else { console.log("Reusing Stored Cookies:", globalCookies); } let targetDbid = releaseDbid; const isCreating = !releaseDbid; // CREATE MODE: Step 1 - POST to create empty Release if (isCreating) { const hasName = fields.some(f => f.name === "Name"); if (!hasName) { throw new Error("Name field is required when creating a new release"); } const createResponse = await fetch(`${serverURL}/ccmweb/rest/repos/${teamspaceID}/databases/${application}/records/Release?operation=Edit&useDbid=true`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${personal_access_token_string}`, 'Cookie': globalCookies }, body: JSON.stringify({ fields: [] }) , ...getAgentOptions(serverURL) }); if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error(`Create operation failed: ${createResponse.status} ${errorText}`); } const createData = await createResponse.json(); targetDbid = createData.dbId; console.log("Created Release with dbId:", targetDbid); } else { if (fields.length === 0) { throw new Error("At least one field must be provided for update"); } } // Step 2: PATCH Edit to set fields (may need multiple calls) for (const field of fields) { // For REFERENCE_LIST fields, we need to send valueAsList instead of value const fieldPayload = { name: field.name }; if (field.type === "REFERENCE_LIST") { fieldPayload.valueAsList = field.value.split(',').map(v => v.trim()); } else { fieldPayload.value = field.value; } const editPayload = { fields: [fieldPayload] }; const editResponse = await fetch(`${serverURL}/ccmweb/rest/repos/${teamspaceID}/databases/${application}/records/Release/${targetDbid}?operation=Edit&useDbid=true`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${personal_access_token_string}`, 'Cookie': globalCookies }, body: JSON.stringify(editPayload) , ...getAgentOptions(serverURL) }); if (!editResponse.ok) { const errorText = await editResponse.text(); throw new Error(`Edit operation failed for ${field.name}: ${editResponse.status} ${errorText}`); } const editData = await editResponse.json(); //console.log(`Edit response for ${field.name}:`, JSON.stringify(editData)); } // Step 3: PATCH Commit with full field metadata const commitFields = fields.map(field => { const baseField = { name: field.name, valueStatus: "HAS_VALUE", validationStatus: "_KNOWN_VALID", requiredness: field.name === "Name" ? "MANDATORY" : "OPTIONAL", requirednessForUser: field.name === "Name" ? "MANDATORY" : "OPTIONAL", type: field.type || "SHORT_STRING", messageText: "", maxLength: (field.type === "MULTILINE_STRING" || field.type === "REFERENCE_LIST") ? 0 : 254 }; // Handle REFERENCE_LIST type (like Sprints) if (field.type === "REFERENCE_LIST") { baseField.valueAsList = field.value.split(',').map(v => v.trim()); // For REFERENCE_LIST, join values with newline for the value field baseField.value = baseField.valueAsList.join('\n'); } else { baseField.value = field.value; baseField.valueAsList = [field.value]; } return baseField; }); const commitPayload = { dbId: targetDbid, fields: commitFields }; const commitResponse = await fetch(`${serverURL}/ccmweb/rest/repos/${teamspaceID}/databases/${application}/records/Release/${targetDbid}?operation=Commit&useDbid=true`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${personal_access_token_string}`, 'Cookie': globalCookies }, body: JSON.stringify(commitPayload) , ...getAgentOptions(serverURL) }); if (!commitResponse.ok) { const errorText = await commitResponse.text(); throw new Error(`Commit operation failed: ${commitResponse.status} ${errorText}`); } const commitData = await commitResponse.json(); // Get the release name for adding to project const releaseName = fields.find(f => f.name === "Name")?.value; // Step 4: If projectID is provided, add this release to the project if (projectID && releaseName) { try { // First, get the current project to retrieve existing releases const getProjectResponse = await fetch(`${serverURL}/ccmweb/rest/repos/${teamspaceID}/databases/${application}/records/Project/${projectID}?useDbid=true`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${personal_access_token_string}`, 'Cookie': globalCookies } , ...getAgentOptions(serverURL) }); if (getProjectResponse.ok) { const projectData = await getProjectResponse.json(); const releasesField = projectData.fields.find(f => f.name === "Releases"); // Get existing releases and add the new one if not already present let existingReleases = releasesField?.valueAsList || []; if (!existingReleases.includes(releaseName)) { existingReleases.push(releaseName); // Update the project with the new releases list const projectCommitPayload = { dbId: projectID, fields: [{ name: "Releases", value: existingReleases.join('\n'), valueStatus: "HAS_VALUE", validationStatus: "_KNOWN_VALID", requiredness: "OPTIONAL", requirednessForUser: "OPTIONAL", type: "REFERENCE_LIST", valueAsList: existingReleases, messageText: "", maxLength: 0 }] }; const updateProjectResponse = await fetch(`${serverURL}/ccmweb/rest/repos/${teamspaceID}/databases/${application}/records/Project/${projectID}?operation=Commit&useDbid=true`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${personal_access_token_string}`, 'Cookie': globalCookies }, body: JSON.stringify(projectCommitPayload) , ...getAgentOptions(serverURL) }); if (updateProjectResponse.ok) { console.log(`Added release ${releaseName} to project ${projectID}`); } else { const errorText = await updateProjectResponse.text(); console.warn(`Failed to add release to project: ${errorText}`); } } else { console.log(`Release ${releaseName} already in project ${projectID}`); } } } catch (projectError) { console.warn(`Failed to update project with release: ${projectError.message}`); // Don't fail the whole operation if project update fails } } const action = isCreating ? "created" : "updated"; const projectMessage = projectID ? ` and added to project ${projectID}` : ""; return { content: [{ type: 'text', text: `Release ${action} successfully${projectMessage}: ${JSON.stringify(commitData)}` }] }; } catch (e) { return { content: [{ type: 'text', text: `Error ${releaseDbid ? 'updating' : 'creating'} release: ${e.message}` }] }; } } )