-
Notifications
You must be signed in to change notification settings - Fork 15
feat: add diagnose_deployment_failure and add_release_notes prompts #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { z } from "zod"; | ||
| import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
|
|
||
| export function registerAddReleaseNotesPrompt(server: McpServer) { | ||
| server.registerPrompt( | ||
| "add_release_notes", | ||
| { | ||
| title: "Add release notes to an existing Octopus release", | ||
| description: | ||
| "Summarize a set of changes (typically PRs) into a Markdown changelog and write it to an Octopus release. Primary path updates ReleaseNotes on an existing release; falls back to creating a new release if none matches.", | ||
| argsSchema: { | ||
| spaceName: z.string().describe("The Octopus space name (required)"), | ||
| projectName: z.string().describe("The Octopus project name (required)"), | ||
| releaseVersion: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| "Release version to update (optional — omit to be guided through picking from the latest releases, or to fall back to creating a new release)", | ||
| ), | ||
| }, | ||
| }, | ||
| ({ spaceName, projectName, releaseVersion }) => { | ||
| const versionClause = releaseVersion | ||
| ? ` version **${releaseVersion}**` | ||
| : ""; | ||
|
|
||
| const text = `Add release notes to${versionClause ? ` release${versionClause} of` : ""} project **${projectName}** in space **${spaceName}**. | ||
|
|
||
| Work through these steps in order. Think step-by-step and check in with the user at each marked decision point. | ||
|
|
||
| 1. **Gather the change set.** If the user has not already supplied the changes in the surrounding conversation, ask them now. Accept any of: | ||
| - a list of PR titles and/or numbers, | ||
| - commit messages, | ||
| - Jira/Linear ticket summaries, | ||
| - a free-form bullet list of changes the user types out. | ||
|
|
||
| Do not invent changes the user did not provide. | ||
|
|
||
| 2. **Draft the changelog.** Summarize the change set into Markdown: | ||
| - One \`## {version}\` heading (use \`${releaseVersion ?? "the resolved release version"}\` once it's known). | ||
| - If the input clearly groups into categories (e.g. PR labels like \`feat\`/\`fix\`/\`chore\`), use \`### Features\`, \`### Fixes\`, \`### Chores\` sub-sections. Otherwise emit a flat bullet list. | ||
| - One bullet per change. Sentence-case, present tense, no trailing period. Plain English — no marketing language, no emoji unless the user asked. | ||
| - Bold project, environment, and entity names. | ||
| - Quote any log lines, code, or commands in fenced code blocks. | ||
|
|
||
| Show the draft to the user and **wait for explicit approval** before writing anything back to Octopus. | ||
|
|
||
| 3. **Find the release.** | ||
| - Resolve \`projectName\` to a project ID with \`list_projects\`. | ||
| - Call \`find_releases\` with the resolved \`projectId\`${releaseVersion ? ` and \`searchByVersion: "${releaseVersion}"\`` : " (and `searchByVersion` if the user supplied a version)"}. | ||
| - If multiple matches come back, or no version was supplied, show the most recent few and ask the user to pick one. If the user says "latest", pick the highest-version assembled release. | ||
| - Confirm the resolved release ID and version back to the user before proceeding. | ||
|
|
||
| 4. **Fetch the full release body.** The \`find_releases\` summary deliberately omits heavy fields. Call \`read_resource\` with the \`resourceUri\` from step 3 (form: \`octopus://spaces/${spaceName}/releases/{releaseId}\`) to get the full release object — including \`SpaceId\`, \`ProjectId\`, \`ChannelId\`, \`Version\`, \`SelectedPackages\`, \`SelectedGitResources\`, \`CustomFields\`, and the existing \`ReleaseNotes\`. | ||
|
|
||
| 5. **Update via PUT round-trip.** Octopus's \`PUT /releases/{id}\` endpoint requires the **full** release body back — any field you omit gets wiped. So: | ||
| - Take the object from step 4 exactly as-is. | ||
| - Replace **only** the \`ReleaseNotes\` field with the approved changelog from step 2. | ||
| - Call the \`execute\` tool with \`method: "PUT"\`, \`path: "/api/{SpaceId}/releases/{Id}"\` (use the \`SpaceId\` and \`Id\` from the fetched body), and \`body\` set to the modified object. | ||
|
|
||
| The PUT lands in the write tier, so the MCP client will show a confirmation prompt — this is expected. After confirmation, verify the update by re-reading the release and showing the user that \`ReleaseNotes\` changed while \`SelectedPackages\`, \`ChannelId\`, and other fields stayed the same. | ||
|
|
||
| 6. **Fallback: release does not exist.** If \`find_releases\` returns no matches, this is the secondary path: | ||
| - Tell the user the release was not found and ask whether to create a new one with the drafted notes. | ||
| - On approval, call \`create_release\` with \`spaceName\`, \`projectName\`, \`releaseVersion\` (ask if not supplied), and \`releaseNotes\` set to the approved changelog. Pass through \`channelName\`, \`gitRef\`, etc. if the user provides them; otherwise let Octopus apply defaults. | ||
| - Be explicit that this is a new release, not an edit to an existing one. | ||
|
|
||
| **Gotchas to flag to the user:** | ||
| - **Octostache expressions don't re-evaluate.** Variable expressions like \`#{Octopus.Release.Number}\` in release notes are resolved against the variable snapshot at the moment the release is first created. Notes written via PUT are stored literally and not re-evaluated. If the user's draft contains \`#{...}\`, warn them and offer to substitute resolved literal values or strip the expressions. | ||
| - **Config-as-Code projects.** \`ReleaseNotes\` is stored on the release server-side, not in the Git repo, so the round-trip works the same way as for database-backed projects. | ||
| - **Permissions.** The user needs \`ReleaseEdit\` on the project. A 401/403 from \`execute\` means the API key/account is missing that permission — surface the error directly rather than retrying. | ||
|
|
||
| Follow the Octopus Deploy writing guide throughout: concise, direct, plain English. Bold project/release/environment names. Avoid speculation.`; | ||
|
|
||
| return { | ||
| messages: [ | ||
| { | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| }, | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { z } from "zod"; | ||
| import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
|
|
||
| export function registerDiagnoseDeploymentFailurePrompt(server: McpServer) { | ||
| server.registerPrompt( | ||
| "diagnose_deployment_failure", | ||
| { | ||
| title: "Diagnose a failed deployment or runbook run", | ||
| description: | ||
| "Analyze and summarize a failed Octopus Deploy deployment or runbook run, calling out what went wrong and suggesting how to fix it.", | ||
| argsSchema: { | ||
| spaceName: z.string().describe("The Octopus space name (required)"), | ||
| projectName: z.string().describe("The Octopus project name (required)"), | ||
| releaseVersion: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| "Release version to diagnose (optional — omit for the latest failed deployment, or when diagnosing a runbook run)", | ||
| ), | ||
| runbookName: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| "Runbook name to diagnose (optional — supply when diagnosing a failed runbook run rather than a deployment)", | ||
| ), | ||
| }, | ||
| }, | ||
| ({ spaceName, projectName, releaseVersion, runbookName }) => { | ||
| const isRunbook = Boolean(runbookName); | ||
| const subject = isRunbook ? "runbook run" : "deployment"; | ||
|
|
||
| const qualifiers: string[] = []; | ||
| if (runbookName) qualifiers.push(`runbook **${runbookName}**`); | ||
| if (releaseVersion) qualifiers.push(`release **${releaseVersion}**`); | ||
| const qualifierClause = | ||
| qualifiers.length > 0 ? ` (${qualifiers.join(", ")})` : ""; | ||
|
|
||
| const text = `Diagnose the most recent failed ${subject} for project **${projectName}** in space **${spaceName}**${qualifierClause}. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the one I want to diagnose is not the most recently failed? For example, not the lastest runbook run? It could be an intermittent one. It could be possible for a release as well but less likely. |
||
|
|
||
| Follow this diagnostic approach, thinking step-by-step: | ||
| 1. Identify the exact step/target where failure occurred. | ||
| 2. Extract and quote the specific error message(s) from the task log. | ||
| 3. Determine the error category (configuration, connectivity, permissions, resource availability, etc.). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving the range of category open (and not well defined) could lead to unpredictable results, therefore, I wonder if extracting a category here is useful. Maybe we are looking for a summary? |
||
| 4. Provide actionable remediation steps based on the error type. | ||
| 5. Note any cascading failures that resulted from the primary issue. | ||
|
|
||
| Suggested tools on this MCP server: | ||
| - \`${isRunbook ? "find_runbooks" : "find_releases"}\` and \`list_deployments\` to locate the failed task. | ||
| - \`get_task_from_url\` or \`get_deployment_from_url\` if the user supplies a URL. | ||
| - \`read_resource\` against \`octopus://spaces/{spaceName}/tasks/{taskId}/details\` for the structured activity tree (steps, targets, timing). | ||
| - \`grep_task_log\` to search the raw log with GNU-grep semantics — prefer this over reading whole logs. | ||
| - \`find_events\` for surrounding audit context (who triggered what, when). | ||
| - When a parent task references child sub-deployments (e.g. \`Deploy a Release\` steps), drill into the failed child task with the same tools to retrieve the actual error. | ||
|
|
||
| Remember: releases in Octopus snapshot the deployment process and variables at creation time. Changes made afterwards do not affect existing releases — fixes usually require creating a new release. | ||
|
|
||
| Follow the Octopus Deploy writing guide: be concise, direct, and use plain English. Bold step/project/entity names, quote log lines in code blocks, and avoid speculation about manually-cancelled tasks.`; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Follow the Octopus Deploy writing guide" is this guide available in the MCP server? If not it could be dropped. |
||
|
|
||
| return { | ||
| messages: [ | ||
| { | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| }, | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { registerDiagnoseDeploymentFailurePrompt } from "./diagnoseDeploymentFailure.js"; | ||
| import { registerAddReleaseNotesPrompt } from "./addReleaseNotes.js"; | ||
|
|
||
| export function registerPrompts(server: McpServer) { | ||
| registerDiagnoseDeploymentFailurePrompt(server); | ||
| registerAddReleaseNotesPrompt(server); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be more predictable if we split the workflow into
draft_release_notesandset_release_notes.draft_release_notesshould be a non-mutating prompt: take explicit source material, like PRs, commits, tickets, or a user-provided changelog, and return a draft for review.set_release_notesshould probably be a tool, not a prompt. UpdatingReleaseNotesis a concrete Octopus write operation, so it benefits from typed inputs, validation, confirmation, tests, and deterministic behavior. It should update an existing release only and not fall back to creating one.For creating releases, I’d use the existing
create_releasetool and recommend passingreleaseNotesthere when needed. That tool already supports release notes, so a separate “create release with notes” prompt would duplicate behavior.This keeps the boundaries clearer:
create_releasetoolThe current
add_release_notesprompt mixes drafting, release selection, updating, and fallback creation in one flow. That makes the outcome harder to predict, especially whenreleaseVersionis omitted or the release is not found.