<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Octopus blog</title>
  <subtitle>Site description.</subtitle>
  <link href="https://octopus.com/blog/feed.xml" rel="self" />
  <link href="https://octopus.com" />
  <id>https://octopus.com/blog/feed.xml</id>
  <updated>2026-10-28</updated>

    <entry>
      <title>DRY isn't just for application code</title>
      <link href="https://octopus.com/blog/dry-isnt-just-for-app-code" />
      <id>https://octopus.com/blog/dry-isnt-just-for-app-code</id>
      <published>2026-10-28</published>
      <updated>2026-10-28</updated>
      <summary>Reusable GitHub workflows solved our copy-paste pipeline problem, but raised new questions about versioning, testing changes, and what platform teams can actually see and control.</summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the <a href="/blog/from-copy-paste-to-platform-thinking">first article of this series</a>, I shared how we approached modernizing the build and deployment of our microsites at Octopus. One of the biggest sources of friction was how our GitHub Actions workflows were managed. Each of our microsites had its own repository, and each repository had its own copy of four workflows. That’s twenty workflow files, all meant to be identical, all maintained separately, and a problem that only got worse with every new microsite we provisioned.</p>
<p>From a technical perspective, it worked, but it didn’t age well, and the workflows were originally built before reusable workflows were available to us. Making a change like bumping the node version meant opening five pull requests across five repositories, getting each one reviewed, and merging them one by one. Small tweaks made in one repository didn’t always reach the others, and over time, it became trivial for the workflows to drift apart. It’s one of those classic things where each time someone made a change, they knew there was a better way, but the pain experienced for that one-off change wasn’t big enough to take action. However, nobody could say with confidence that every microsite was being built the same way.</p>
<p>Most developers learn “don’t repeat yourself” (DRY) early on as a principle for writing application code. Yet it’s surprisingly common to see the same pipeline copied across repository after repository, as if the rule stops at the edge of the source code. Our microsites were a textbook example.</p>
<p>The fix was to move the build logic into a single central repository and have each microsite call it, so a change made once applies everywhere. GitHub calls these reusable workflows, and getting them in place was one of the project’s quicker wins.</p>
<p>It wasn’t all smooth sailing, though. Reusable workflows solved the copy-paste problem, but they raised new questions about versioning, testing changes safely and without friction, and how much a platform team can see and control once other teams depend on what they’ve built.</p>
<h2 id="a-central-home-for-shared-workflows">A central home for shared workflows</h2>
<p>The first step was deciding where the shared workflows would live. I knew there would likely be other shared elements for the project, so we created a central repository called <code>microsite-deployment</code>. Alongside the reusable workflows, it also holds the Terraform for the hosting infrastructure and the scripts used during deployment.</p>
<p>What makes a workflow reusable is a single trigger. Instead of running on a push or a pull request, it uses <code>on: workflow_call</code>, which tells GitHub it’s designed to be invoked by other workflows. It can then declare inputs, just like a function declares parameters, and the calling workflow supplies the values.</p>
<p>For each microsite, the result is refreshingly small. Here’s the entire calling workflow that runs when a pull request is opened against our blog:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="yml"><code><span class="line"><span style="color:#800000">name</span><span style="color:#000000">: </span><span style="color:#0000FF">Pull Request build and deploy</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0000FF">on</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">  pull_request</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    branches</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#000000">      - </span><span style="color:#0000FF">main</span></span>
<span class="line"></span>
<span class="line"><span style="color:#800000">jobs</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">  branch_validation</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">    uses</span><span style="color:#000000">: </span><span style="color:#0000FF">OctopusDeploy/microsite-deployment/.github/workflows/microsite-deployment-branch.yml@main</span></span>
<span class="line"><span style="color:#800000">    with</span><span style="color:#000000">:</span></span>
<span class="line"><span style="color:#800000">      octopus_project_name</span><span style="color:#000000">: </span><span style="color:#A31515">"Blog Microsite"</span></span>
<span class="line"><span style="color:#800000">      package_id</span><span style="color:#000000">: </span><span style="color:#A31515">"BlogMicrosite"</span></span>
<span class="line"><span style="color:#800000">    secrets</span><span style="color:#000000">: </span><span style="color:#0000FF">inherit</span></span></code></pre>
<p>That’s it. The calling workflow decides <em>when</em> the build runs, and the called workflow decides <em>how</em> it runs. Each microsite provides two inputs: the name of its Octopus project and the package ID for the built site, so it knows which package to reference when GitHub triggers a release in Octopus. Installing dependencies, building and testing the site, packaging it, pushing it to Octopus, and creating a release all live in one place now.</p>
<p>Centralizing the workflows also means the trickier logic only needs to be solved once. Some of our microsite repositories are public and accept contributions from people outside of Octopus. Pull requests from forks don’t have access to the repository’s secrets, so a build from a fork can’t push anything to the Octopus instance that performs deployments. The reusable workflow handles this with a <code>SHOULD_DEPLOY</code> flag, which is set based on whether the Octopus server secret is available in the repo. If it isn’t, the site is still built, tested, and packaged, and the package is uploaded as a workflow artifact instead. Outside contributors still get a validated build, but it doesn’t try to deploy without the credentials to do it. That logic existed before this project, but now it lives in one place, and every public microsite gets it for free.</p>
<p>Adding a new microsite is now a very different experience. Instead of copying four complex workflow files from an existing repo and hoping you picked the latest version, a new repository needs a handful of stable short calling workflows like the one above, and it gets the same build process every other microsite uses from day one.</p>
<h2 id="bring-improvements-in-small-batches">Bring improvements in small batches</h2>
<p>When I started converting the workflows, there was a temptation to go all the way in one hit. Take everything, use Claude to consolidate it into as few files as possible, with whatever logic was needed to handle every incarnation, and be done with it. But moving five microsites onto shared workflows was already a meaningful change, and I didn’t want to stack a redesign of the build logic on top of it. We already know small batches are easier to author and review, easier to test, and much easier to roll back if something goes wrong.</p>
<p>The first step was a fairly direct translation. Each of the four workflows in the microsite repositories became a reusable workflow in the central repository, and each microsite swapped its copies for the short and stable calling workflows. The logic of the workflow stayed the same, but the benefit I was looking for with this change was to consolidate to one version of each workflow, giving us one place to make changes moving forward that would roll out to all callers.</p>
<p>The two build workflows are a good example of where I deliberately held back. One runs when a pull request is opened, and the other runs when changes are merged to main. Both build, test, and package the site, but what happens next is quite different. The main branch workflow is a single job that pushes the package to Octopus and creates a release. The pull request workflow is split into three jobs, deploys to an ephemeral environment, can be skipped with a label, and has a gate job that acts as the required status check for merging.</p>
<p>Could they be a single workflow? Yes. They might even become a single workflow in the future. You could check which event triggered the run, or pass in an input, and use conditions to determine which jobs run. But the two workflows differ in their structure, not just a step or two, and a single file full of conditionals can be harder to read and reason about. Keeping them separate means each one clearly does one job.</p>
<p>I can see what you’re thinking:</p>
<blockquote>
<p>But Matt, we are trying to stop the duplication and the multiple changes!</p>
</blockquote>
<p>Yes, true, but it doesn’t mean the duplication between them was entirely ignored. For example, workflows need to install the same versions of Node and pnpm, and specifying those versions in two places would have reintroduced the exact problem we were trying to escape, just inside the central repository instead of across the microsites.</p>
<p>So the versions for pnpm and Node are specified in a small composite action, which both workflows call:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="yml"><code><span class="line"><span style="color:#000000">- </span><span style="color:#800000">name</span><span style="color:#000000">: </span><span style="color:#0000FF">Retrieve versions</span></span>
<span class="line"><span style="color:#800000">  uses</span><span style="color:#000000">: </span><span style="color:#0000FF">OctopusDeploy/microsite-deployment/.github/composite/versions@main</span></span>
<span class="line"><span style="color:#800000">  id</span><span style="color:#000000">: </span><span style="color:#0000FF">versions</span></span></code></pre>
<p>The composite action exposes the Node and pnpm versions as outputs, giving us one place to bump the toolchain for every workflow at once.</p>
<figure><p><img src="/blog/i/x/dry-isnt-just-for-app-code/reusable-workflows-diagram.png" alt="Diagram of microsite repos calling four reusable workflows in a central repo, which share a composite action for versions." loading="lazy" srcset="/blog/i/600/dry-isnt-just-for-app-code/reusable-workflows-diagram.webp 600w, /blog/i/1000/dry-isnt-just-for-app-code/reusable-workflows-diagram.webp 1000w, /blog/i/2000/dry-isnt-just-for-app-code/reusable-workflows-diagram.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2800" height="1960"></p><figcaption>The flow from caller workflows, to called workflows, to the composite action</figcaption></figure>
<p>I’ll be honest, though: there’s still duplication. The steps to install dependencies, build, test, and package the site are nearly identical in both workflows, and that’s a natural next candidate for a composite action of its own. It’s a good reminder that copy-paste can creep back in even inside the platform that’s meant to eliminate it. But getting the microsites onto shared workflows first, and then improving those workflows from a single place, was a much safer path than trying to do everything at once. Any further consolidation is now a change to one repository, not five.</p>
<h2 id="versioning-and-testing-changes">Versioning and testing changes</h2>
<p>Once the microsites were calling shared workflows, a new question quickly arose. When I change a reusable workflow, which version should each microsite be running?</p>
<h3 id="choosing-a-reference">Choosing a reference</h3>
<p>The calling workflow determines which version of a reusable workflow to uses via the reference after the <code>@</code>. There are three common options, each with a trade-off.</p>
<p>A branch, like <code>@main</code>. This is a floating reference. Whatever is on the main branch of the central repository is what every caller runs on its next build. For a platform team, that’s appealing, because a fix or improvement reaches every consumer immediately. For the consumer, it means a change can land in their build without them doing anything, and if that change breaks something, they find out when their pipeline fails. It’s a similar pattern to using latest with container images, which most of the industry has moved away from for good reason.</p>
<p>A tag, like <code>@v1.2.0</code>. Callers stay on a known version until they choose to update. That’s much more predictable, but git tags are mutable by default. Someone can move a tag to a different commit, accidentally or otherwise, and every caller pinned to it quietly starts running different code. GitHub’s <a href="https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases">immutable releases</a> help here, as the tag attached to an immutable release can’t be changed after it’s published. The catch is that this doesn’t work for floating tags like @v1, which many teams use so consumers automatically get minor updates, because a floating tag has to move to be useful.</p>
<p>A commit SHA. This is the most stable option. A SHA always points to the exact same code, and GitHub’s own documentation recommends it as the safest choice for stability and security. The catch is that a 40-character hash tells you nothing at a glance, so the common convention is to add a comment with the version it represents.</p>
<p>We opted to use <code>@main</code>, effectively forcing every consumer onto the latest version at all times, and accepting that we’re on the hook if we break all the microsites at once. How a workflow is referenced is ultimately set in the calling repository, but the platform team decides which options exist and which one to recommend, so it’s a decision worth making together with your consumers. Think through the implications, and decide whether you’d rather keep everyone on the latest version, or end up with consumers using many versions and choosing when to update, with all the complications that may arise. As we’ll see, GitHub doesn’t have a neat way to notify people there’s a new version, or report on version use so you can track and encourage updates.</p>
<h3 id="keeping-consumers-up-to-date">Keeping consumers up to date</h3>
<p>If you do pin to tags or SHAs, the next problem is ensuring consumers move to newer versions when you publish. Pinned callers don’t automatically pick up changes, so the fix you’ve published might not be running anywhere.</p>
<p>Dependabot can help here. It can track reusable workflow references and raise pull requests when a new version is available, just as it does for marketplace actions. For workflows in private or internal repositories, which is the common case within an organization, they require a little extra setup, either by granting Dependabot access to the central repository in your organization settings or by configuring a Dependabot secret with a token that can read it. It’s also opt-in per calling repository. It makes updates easier to notice and act on, but the decision to update still sits with each consumer for any type of update, whether it be a patch, minor, or major change.</p>
<h3 id="one-tag-for-every-workflow">One tag for every workflow</h3>
<p>Another paper cut I hit when exploring tagging for the reference approach, is that tags apply to the whole repository. A Git tag points to a commit, and that commit contains every workflow in the repository. If I change the pull request workflow and tag it <code>v1.3.0</code>, a consumer of the main branch workflow who moves to <code>v1.3.0</code> picks up whatever state that workflow is in at the same commit, even if nothing changed for them. There’s no built-in way to version reusable workflows independently within one repository. You can work around it with prefixed tags, a repository per workflow, or community tools like the <a href="https://github.com/albertodeago/wire">Workflow Independent Release Engine</a>, but each adds its own overhead.</p>
<h3 id="testing-changes">Testing changes</h3>
<p>This is the part I found most awkward day-to-day. When I want to change a reusable workflow, I make the change on a branch in the central repository. I want to test it works as intended before I merge to main, but I don’t have anything that’s calling the version in my new branch. Currently to test, I create a branch in one of the microsite repositories, change its calling workflow to point at my branch instead of <code>@main</code>, and trigger it. Depending on the trigger, that can mean opening a throwaway pull request just to get the workflow to run.</p>
<p>This rarely happens, but it gets more awkward when the change also touches the composite action. The reusable workflow references the composite at <code>@main</code>, and that nested reference resolves on its own, regardless of which version of the reusable workflow the caller asked for. You might think a local path would solve this, but a local path inside a reusable workflow is resolved against the caller’s workspace, not the central repository. Before the composite action existed, I tried storing the versions in a JSON file in the central repository and reading it from the reusable workflow, but the workflow went looking for the file in the calling microsite repository instead. So testing a change that spans both files means re-pointing two references on my branch, and then remembering to point them back before merging, because I don’t want branch references promoted to main. Pinning to tags or SHAs doesn’t remove that awkwardness. It just moves it somewhere else.</p>
<p>This is an area of the project I haven’t put much thought into solving, as the pain is bearable, but it’s one I’d like to improve. I’ve seen other projects have similar concerns, and one way they’ve resolved this is to have a “test” calling repo - which, in our case, would be a basic microsite - which is primarily used to test changes to called workflows.</p>
<h2 id="visibility-and-governance-for-platform-teams">Visibility and governance for platform teams</h2>
<p>Everything so far has mostly been from the perspective of the person writing and maintaining the shared workflows. But once other teams depend on what you’ve built, a different set of questions comes up. Who’s actually using this workflow? Which version are they on? And if I need everyone to move to a newer version, how do I make that happen?</p>
<h3 id="whos-consuming-your-workflows">Who’s consuming your workflows?</h3>
<p>This is where GitHub gives a platform team surprisingly little. There’s no view from a reusable workflow that shows which repositories call it, or which reference each of the callers is using. On GitHub Enterprise Cloud, which Octopus uses, you can query the audit log through the REST API to see workflow use, but it’s not the best user experience. Outside of that, the options are to search for code across your organization or to write your own script to crawl repositories and parse their workflow files.</p>
<p>When you put this in the context of a platform team with hundreds of repositories calling many workflows, the questions get harder to answer. Is anyone still on an old version? Has a security fix been picked up everywhere? You’d reasonably expect a platform to tell you, and today on GitHub you mostly have to go and find out yourself.</p>
<h3 id="steering-consumers-to-a-new-version">Steering consumers to a new version</h3>
<p>Imagine you’ve published a change that every team needs to pick up, maybe a security fix or a new organizational requirement baked into the build. If consumers reference <code>@main</code>, they get it on their next run. If they’ve pinned to a tag or SHA, as GitHub recommends, it’s up to each of them to update.</p>
<p>GitHub does give administrators some control here, through the allowed actions and reusable workflows policy. Administrators can block a specific action or reusable workflow version by adding it to the policy with a ! prefix, and blocks take priority over anything the policy would otherwise allow. So an administrator can block an old version of a reusable workflow across the organization.</p>
<p>The catch is that it’s a hard stop. There’s no warning phase, no grace period with a “please update by the end of the month” message that teams can see but keep working. The next time a caller on the blocked version runs, it simply fails. That makes sense when you look at why the controls exist. GitHub introduced them to reduce the risk of compromised action repositories, secrets, and malicious tag updates. They’re supply chain security tools, and they do that job well. What they weren’t designed for is managing the lifecycle of your own internal workflows, like guiding teams from one version to the next over a few weeks.</p>
<p>Dependabot, as I covered earlier, helps from the other direction. It can raise pull requests in calling repositories when a new version is available, but it’s configured per repository, and the consumer still decides when to merge. Between the two, a platform team can nudge from the consumer’s side or block from the organization’s side, with not much in between.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>Moving to reusable workflows was one of the quicker wins of this project. Twenty workflow files became a handful of short calling workflows; implementing a change is now one pull request instead of five, and I can say with confidence that every microsite is built the same way. They do come with tradeoffs, though, and if you’re a platform team sharing workflows across many repositories, it’s worth deciding early how consumers should reference your workflows, how you’ll test changes before they land, and how you’ll know who’s using which version, because those answers are much easier to design in than to retrofit.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.12: The platform adoption shock</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e12" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e12</id>
      <published>2026-10-27</published>
      <updated>2026-10-27</updated>
      <summary>Discover what surprised us about our Future of Platform Engineering research in terms of adoption strategies.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the previous episode, we covered <a href="https://octopus.com/blog/continuous-delivery-office-hours-e11">organizational culture</a>, including why posters aren’t the way to change culture, crucial theoretical foundations from Douglas McGregor and Ron Westrum, the impact of arbitrary deadlines, and why feedback keeps organizations healthy.</p>
<p>This episode examines Platform Engineering and the persistent debate between mandatory and optional adoption strategies. The episode was inspired by data from our <a href="https://octopus.com/publications/future-of-platform-engineering-report">Future of Platform Engineering report</a>.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p>You can watch the episode below, or read on to find some of the key discussion points.</p>
<p><a href="https://www.youtube.com/watch?v=m5LkcDJGUOo">Watch Continuous Delivery Office Hours Ep.12</a></p>
<h2 id="the-rise-of-mandatory-platforms">The rise of mandatory platforms</h2>
<p>Mandatory platforms have been an issue in the past, with platform producers and consumers diverging dramatically on whether a platform is successful. Part of the perception gap comes down to platform teams losing the adoption signal: if everyone must use the platform, you’ve lost a way to discover gaps and flaws in what you provide.</p>
<p>Since then, platforms have become broader. Platforms now have almost 6x as many features as they did when the perception gap was present. As platforms take on tasks that developers truly appreciate, like security and compliance, the gap between producers and consumers has closed.</p>
<p>You might think of this like the difference between being forced to use a bicycle to travel both before and after the invention of the bicycle saddle. Riders are far happier to be forced to cycle when they have a cushioned seat.</p>
<p>Alongside the dissipation of perceptual differences, it turns out mandatory platforms outperform optional platforms in terms of goal attainment. However, there are many factors contributing to platform success, like monitoring developer satisfaction and regularly eliciting feedback to help improve the platform.</p>
<p>The other factors of platform success have more impact, and you should still consider optional adoption, but combined with mandatory policies. That means development teams have to meet the same standards they would meet if they adopted the platform, which makes platform adoption more appealing and less likely to cause conflict or friction.</p>
<h2 id="not-just-more-features-but-the-right-ones">Not just more features, but the right ones</h2>
<p>The most common number of features baked into a platform was four, but that has grown to 24. Platforms are getting broader, and these wider platforms are more successful than the tiny ones of the past. Despite this link between platform breadth and goal attainment, it’s important to bear in mind that the volume of features alone is not a sufficient strategy, and that choosing to solve the right problems is the path to success.</p>
<p>Maintaining regular contact with developers, lightly assessing their satisfaction with the platform, and dedicating time to conversations that shape your platform’s direction are all part of the high-performance platform playbook. The usefulness comes from solving the right problems, and you need to focus on developer needs to make the right choices.</p>
<p>Platform teams are rediscovering many of the capabilities of the State of DevOps research and its associated model, like Lean Product Management, which prove useful in building platforms as internal products.</p>
<h2 id="the-invisible-platform-dilemma">The invisible platform dilemma</h2>
<p>Unsurprisingly, platforms that meet more goals have higher budget safety. They are showing a return on investment, so they can be confident in maintaining their funding over the long run. Platforms that fail to meet their goals are more likely to have their budgets redirected to other initiatives.</p>
<p>Platform teams need to show the value back to the organization in terms the organization understands and for measures it cares about. With AI initiatives soaking up money, some organizations will start reallocating funds and focus, leaving platforms underfunded or dropped.</p>
<p>Some of the best platforms will be invisible in daily use because developers don’t have to keep interacting with them. These boring, invisible platforms are the most valuable, but the least visible. Providing metrics and proof of value for these platforms will keep them funded.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>From features to solutions</title>
      <link href="https://octopus.com/blog/features-to-solutions" />
      <id>https://octopus.com/blog/features-to-solutions</id>
      <published>2026-10-26</published>
      <updated>2026-10-26</updated>
      <summary>How to take a series of small features and build a solution</summary>
      <author>
        <name>Ben Pearce, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Octopus Deploy ships dozens of features that are each useful on their own. But the real power comes from combining them. In this post, I will walk through bringing together a collection of smaller, often overlooked features into a practical solution: automatically notifying the responsible team members whenever a deployment hits a manual intervention. It’s a real-world example of how thinking in solutions, not just features, can unlock more value from the tools you already have.</p>
<p>In an application deployment process, a manual intervention step can help provide a checkpoint for sign-offs, or allow validation of the state of the deployment before it is switched over to a live site. Unfortunately, by default, manual interventions only show up in the Octopus user interface, so they’re easy to overlook and can block deployments from completing.</p>
<p>Let’s walk through a few different ways that we can solve this using old and new features of Octopus:</p>
<ul>
<li>Subscriptions</li>
<li>Slack notifications</li>
<li>Webhook triggered runbooks</li>
</ul>
<h2 id="slack-notifications">Slack notifications</h2>
<p>In 2026.3, we added the ability to connect your Octopus Deploy instance to a Slack Workspace. At the time of writing, only one workspace is supported for the whole instance. Configuration is quite straightforward, so I won’t even go into details on how to configure it, just follow your nose from Configuration > Settings > Slack Integration, and the Slack docs on <a href="https://docs.slack.dev/app-manifests/configuring-apps-with-app-manifests/">Creating apps with an app manifest</a>.</p>
<p>Once you have a Slack Workspace configured, you can use a subscription to notify your Slack channel of a pending Manual Intervention being created in the Space the subscription is in. Subscriptions also allow you to narrow the projects that this applies to.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/subscription-with-slack.png" alt="Screenshot showing a subscription to Slack" loading="lazy" srcset="/blog/i/600/features-to-solutions/subscription-with-slack.webp 600w, /blog/i/1000/features-to-solutions/subscription-with-slack.webp 1000w, /blog/i/2000/features-to-solutions/subscription-with-slack.webp 1283w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1283" height="718"></p></figure>
<p>There is a rough edge worth calling out here: subscriptions are per space and it would be nice to have consistent behavior across many or all spaces. That’s where you can use some automation to create the exact same subscription setup.</p>
<p>Use the Octopus Deploy Terraform provider to create all your subscriptions across the required spaces:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="hcl"><code><span class="line"><span style="color:#267F99">terraform</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#267F99">  required_providers</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">    octopusdeploy </span><span style="color:#000000">=</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">      source</span><span style="color:#000000"> = </span><span style="color:#A31515">"OctopusDeploy/octopusdeploy"</span></span>
<span class="line"><span style="color:#001080">      version</span><span style="color:#000000"> = </span><span style="color:#A31515">"1.18.0"</span></span>
<span class="line"><span style="color:#000000">    }</span></span>
<span class="line"><span style="color:#000000">  }</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#267F99">variable</span><span style="color:#0070C1"> "space_ids"</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">  type </span><span style="color:#000000">=</span><span style="color:#0000FF"> list</span><span style="color:#000000">(</span><span style="color:#0000FF">string</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">  default </span><span style="color:#000000">=</span><span style="color:#000000"> [</span><span style="color:#A31515">"Spaces-142"</span><span style="color:#000000">, </span><span style="color:#A31515">"Spaces-1"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#001080">  description </span><span style="color:#000000">=</span><span style="color:#A31515"> "List of Space IDs to create subscriptions in"</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Provider without space_id - space set per-resource</span></span>
<span class="line"><span style="color:#267F99">provider</span><span style="color:#0070C1"> "octopusdeploy"</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">  address </span><span style="color:#000000">=</span><span style="color:#A31515"> "https://example.octopus.app"</span></span>
<span class="line"><span style="color:#001080">  api_key </span><span style="color:#000000">=</span><span style="color:#A31515"> "API-APIKEY12345"</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#267F99">resource</span><span style="color:#0070C1"> "octopusdeploy_subscription"</span><span style="color:#0070C1"> "per_space"</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">  for_each </span><span style="color:#000000">=</span><span style="color:#795E26"> toset</span><span style="color:#000000">(var.</span><span style="color:#001080">space_ids</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">  name </span><span style="color:#000000">=</span><span style="color:#A31515"> "Manual Intervention To Slack"</span></span>
<span class="line"><span style="color:#001080">  space_id </span><span style="color:#000000">=</span><span style="color:#000000"> each.</span><span style="color:#001080">value</span></span>
<span class="line"><span style="color:#001080">  is_disabled </span><span style="color:#000000">=</span><span style="color:#0000FF"> false</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">  event_notification_subscription </span><span style="color:#000000">=</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#008000">    # the id of your target channel</span></span>
<span class="line"><span style="color:#001080">    slack_channel_ids</span><span style="color:#000000"> = [</span><span style="color:#A31515">"C0BG25D5DQV"</span><span style="color:#000000">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000">    # the name of the channel, only required for display purposes</span></span>
<span class="line"><span style="color:#001080">    slack_channel_names</span><span style="color:#000000"> = [</span><span style="color:#A31515">"manual-interventions"</span><span style="color:#000000">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000">    # notify every minute</span></span>
<span class="line"><span style="color:#001080">    slack_frequency_period</span><span style="color:#000000"> = </span><span style="color:#A31515">"00:01:00"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">    filter</span><span style="color:#000000"> = {</span></span>
<span class="line"><span style="color:#001080">      event_categories</span><span style="color:#000000"> = [</span><span style="color:#A31515">"Created"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#001080">      document_types</span><span style="color:#000000"> = [</span><span style="color:#A31515">"Interruptions"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#000000">    }</span></span>
<span class="line"><span style="color:#000000">  }</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>With this subscription in place, the <code>#manual-interventions</code> channel in your Slack workspace will receive a generic message with a link to the audit page in the Octopus instance.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/slack-default-message.png" alt="Default subscription message in Slack" loading="lazy" srcset="/blog/i/600/features-to-solutions/slack-default-message.webp 402w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="402" height="189"></p></figure>
<p>Slack and email communications from a subscription will always be a batch of events covering the subscription frequency, and pressing the <em>View all in Octopus</em> button will take you to the audit page.</p>
<p>This works, but we can probably make it better.</p>
<h2 id="webhooks-and-runbooks">Webhooks and runbooks</h2>
<p>Another new feature available in 2026.3 is the ability to trigger a runbook from a webhook call. Octopus supports being both the webhook provider as well as a consumer for the purposes of executing a runbook via the Runbook Webhook Trigger.</p>
<p>In previous versions, if you wanted to take any action based on an event within Octopus you had two options: polling the event API, which can be expensive and requires somewhere to run the polling code, or standing up a webhook receiver either as a custom function/lambda or using an external SaaS service such as <a href="https://zapier.com/">Zapier</a> or <a href="https://www.workato.com/">Workato</a>. Webhook Triggers let you use a runbook process as the execution location.</p>
<p>Let’s swap out the default behavior from the subscription’s Slack notification and provide our own custom message format that will notify the responsible users.</p>
<h3 id="the-attention-seeking-project">The attention-seeking project</h3>
<p>The project that I am using as an example is very straightforward: it deploys to a staging deployment slot in Azure, uses a Manual Intervention step to get a human check, and performs the slot swap after it has been reviewed.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/deployment-with-manual-intervention.png" alt="Deployment process with manual intervention" loading="lazy" srcset="/blog/i/600/features-to-solutions/deployment-with-manual-intervention.webp 515w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="515" height="338"></p></figure>
<p>To build out this solution, I am going to touch on a number of Octopus features, some only made available recently in 2026.3, and others have been around for much longer:</p>
<ul>
<li>Webhook triggers to Runbooks <strong>NEW</strong></li>
<li>Slack integration and a built-in Slack step <strong>NEW</strong></li>
<li>JSON path variables</li>
<li>Variable filter syntax</li>
<li>Octopus CLI</li>
<li>Read-only roles</li>
<li>Output variables</li>
<li>OIDC auth to Octopus with service accounts</li>
<li>Generic OIDC accounts</li>
<li>Library Variable Sets</li>
<li>Execution containers</li>
<li>Variable testing</li>
</ul>
<h3 id="wiring-up-the-webhook">Wiring up the webhook</h3>
<p>The first thing we are going to need is a new runbook to receive the webhook trigger payload and a webhook trigger.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/runbook-trigger-blur.png" alt="Webhook trigger configuration" loading="lazy" srcset="/blog/i/600/features-to-solutions/runbook-trigger-blur.webp 600w, /blog/i/1000/features-to-solutions/runbook-trigger-blur.webp 1000w, /blog/i/2000/features-to-solutions/runbook-trigger-blur.webp 1191w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1191" height="646"></p></figure>
<p>The trigger requires either a shared secret, supplied in the header <code>X-Octopus-Webhook-Secret</code>, or an API key, supplied in the header <code>X-Octopus-ApiKey</code> for an account that has permission to run the runbook. If you would prefer to have the executions tied to a specific account, for auditing purposes for example, use an API key.</p>
<p>Next, configure a subscription, or reuse the one we created earlier. You will need the endpoint URL for the trigger and the header and value to authenticate the webhook call.</p>
<p>In my case, I have opted for a shared secret:</p>
<figure><p><img src="/blog/i/x/features-to-solutions/subscription-with-webhook.png" alt="Subscription to webhook" loading="lazy" srcset="/blog/i/600/features-to-solutions/subscription-with-webhook.webp 600w, /blog/i/1000/features-to-solutions/subscription-with-webhook.webp 1000w, /blog/i/2000/features-to-solutions/subscription-with-webhook.webp 1129w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1129" height="756"></p></figure>
<h3 id="shape-of-the-event-payload">Shape of the event payload</h3>
<p>When a subscription fires a webhook, you get a pretty generous chunk of JSON. Here’s the shape of it, trimmed down to the bits we actually care about.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#0451A5">    "Timestamp"</span><span style="color:#000000">: </span><span style="color:#A31515">"2026-08-24T06:43:12.310+00:00"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "EventType"</span><span style="color:#000000">: </span><span style="color:#A31515">"SubscriptionPayload"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">    "Payload"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">        "ServerUri"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://my.octopus.app"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">        "ServerAuditUri"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://my.octopus.app/app#/Spaces-142/configuration/audit?..."</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">        "Subscription"</span><span style="color:#000000">: { </span><span style="color:#CD3131">...</span><span style="color:#000000"> },</span></span>
<span class="line"><span style="color:#0451A5">        "Event"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">            "Id"</span><span style="color:#000000">: </span><span style="color:#A31515">"Events-772873"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "RelatedDocumentIds"</span><span style="color:#000000">: [</span><span style="color:#A31515">"Interruptions-43"</span><span style="color:#000000">],</span></span>
<span class="line"><span style="color:#0451A5">            "Category"</span><span style="color:#000000">: </span><span style="color:#A31515">"Created"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "Message"</span><span style="color:#000000">: </span><span style="color:#A31515">"Interruption Check staging deployment was created"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">            "ChangeDetails"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">                "DocumentContext"</span><span style="color:#000000">: {</span></span>
<span class="line"><span style="color:#0451A5">                    "Id"</span><span style="color:#000000">: </span><span style="color:#A31515">"Interruptions-43"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "Title"</span><span style="color:#000000">: </span><span style="color:#A31515">"Check staging deployment"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "Type"</span><span style="color:#000000">: </span><span style="color:#A31515">"ManualIntervention"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "Status"</span><span style="color:#000000">: </span><span style="color:#A31515">"Pending"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "RelatedDocumentIds"</span><span style="color:#000000">: [</span></span>
<span class="line"><span style="color:#A31515">                        "Deployments-53208"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#A31515">                        "ServerTasks-805184"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#A31515">                        "Projects-941"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#A31515">                        "Environments-423"</span></span>
<span class="line"><span style="color:#000000">                    ],</span></span>
<span class="line"><span style="color:#0451A5">                    "ResponsibleTeamIds"</span><span style="color:#000000">: [</span><span style="color:#A31515">"Teams-202"</span><span style="color:#000000">],</span></span>
<span class="line"><span style="color:#0451A5">                    "SpaceId"</span><span style="color:#000000">: </span><span style="color:#A31515">"Spaces-142"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "ProjectId"</span><span style="color:#000000">: </span><span style="color:#A31515">"Projects-941"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">                    "EnvironmentId"</span><span style="color:#000000">: </span><span style="color:#A31515">"Environments-423"</span></span>
<span class="line"><span style="color:#000000">                }</span></span>
<span class="line"><span style="color:#000000">            },</span></span>
<span class="line"><span style="color:#0451A5">            "SpaceId"</span><span style="color:#000000">: </span><span style="color:#A31515">"Spaces-142"</span></span>
<span class="line"><span style="color:#000000">        },</span></span>
<span class="line"><span style="color:#0451A5">        "BatchId"</span><span style="color:#000000">: </span><span style="color:#A31515">"a634600f-0cd0-43f6-9412-14bb1a072fcd"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">        "TotalEventsInBatch"</span><span style="color:#000000">: </span><span style="color:#098658">1</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">        "EventNumberInBatch"</span><span style="color:#000000">: </span><span style="color:#098658">1</span></span>
<span class="line"><span style="color:#000000">    }</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>The event details that we need are buried in <code>Payload.Event.ChangeDetails.DocumentContext</code> - that’s where we find the responsible team IDs, the related document IDs (handy for working out if this was a deployment or a runbook run), and the server task ID for linking people back to the Octopus web UI.</p>
<p>Note: The <code>DocumentContext</code> part shown here is specific to a Manual Intervention event. Other event types will have a different data shape.</p>
<h3 id="the-supporting-cast">The supporting cast</h3>
<p>Before we get to the runbook itself, there are a few pieces of configuration that make the whole thing work.</p>
<h4 id="teams-and-users">Teams and users</h4>
<p>The manual intervention step in our deployment process is configured with a responsible team, creatively named <strong>Manual Intervention</strong>. This team has all the people who can approve or reject the intervention: Annie Developer, Bob Tester, and yours truly. Crucially, these team IDs come through in the webhook event payload - that’s how our runbook figures out who to notify on Slack.</p>
<p>We also need a service account, <strong>readonly-svc</strong>, so the runbook can talk back to the Octopus API and look up who’s actually in those teams. This account lives in a new team called <strong>Read-only</strong> with only the <strong>Read only</strong> role assigned to it. The <strong>Read only</strong> built-in role is another feature added in 2026.3 to support read-only API keys and AI agents.</p>
<h4 id="generic-oidc-account">Generic OIDC account</h4>
<p>Now here’s where it gets a little bit Inception-like: we need Octopus to authenticate with… itself.</p>
<p>Rather than stashing an API key somewhere (which always feels a bit icky), I’m using a <strong>Generic OIDC Account</strong> called “Octopus Loopback”. During the runbook run, the service account gets an OIDC token issued to it, and we use that to authenticate the CLI right back to the same instance.</p>
<p>There are two parts to set up.</p>
<ol>
<li>
<p>Add an OIDC identity to the service account</p>
<p>On the <code>readonly-svc</code> account created earlier, add an OIDC identity with these settings:</p>
<ul>
<li><strong>Issuer type:</strong> Other Issuer</li>
<li><strong>Issuer:</strong> the public HTTPS URL of your Octopus instance</li>
<li><strong>Subject:</strong> <code>space:&#x3C;space-slug>:runbook:&#x3C;runbook-slug></code></li>
</ul>
<p>Slugs start out as the lowercase resource name with spaces replaced by hyphens. Renaming a resource doesn’t update its slug, so check the current value rather than working it out from the name.</p>
<p>Once it’s saved, take note of the audience shown on the OIDC identity. This is the service account’s ID, and it’s what the CLI needs to log in.</p>
</li>
<li>
<p>Create the Generic OIDC account</p>
<p>Create a new Generic OIDC account called “Octopus Loopback” with these settings:</p>
<ul>
<li><strong>Deployments &#x26; Runbooks subject claim:</strong> <code>Space</code> and <code>Runbook</code></li>
<li><strong>Audience:</strong> the audience value from the OIDC identity</li>
</ul>
<div class="hint"><p>The script uses <code>Octopus.Web.ServerUri</code> to log in and to build the link in the Slack message. Octopus Cloud always sets this. On a self-hosted instance, check that <strong>Configuration > Settings > Web Portal</strong> has an HTTPS server URI that’s accessible from the worker where the runbook runs, otherwise, the CLI login will fail.</p></div>
</li>
</ol>
<h4 id="slack-user-mapping">Slack user mapping</h4>
<p>We need a way to go from “Octopus user” to “Slack user” so we can <code>@mention</code> the right people. A library variable set called <strong>Slack Users</strong> does the trick - the variable names are email addresses and the values are Slack user IDs:</p>
<div class="table-wrap">




















<table><thead><tr><th align="left">Variable Name (Email)</th><th align="left">Value (Slack User ID)</th></tr></thead><tbody><tr><td align="left"><code>ben.pearce@octopus.com</code></td><td align="left"><code>U7PE3KF16</code></td></tr><tr><td align="left"><code>annie.developer@example.com</code></td><td align="left"><code>U0BT5BU5GS0</code></td></tr><tr><td align="left"><code>bob.tester@example.com</code></td><td align="left"><code>U0BT1LU7W5Q</code></td></tr></tbody></table></div>
<p>The script in the runbook will find each user’s email and then try to find a Slack User ID to add to the message, failing that it will just use the Display Name.</p>
<h3 id="project-variables-for-extracting-details">Project variables for extracting details</h3>
<p>When the webhook trigger is fired, the request payload is available via the built-in variable <code>Octopus.WebhookTrigger.RequestBody</code>. From there, it’s a matter of fishing out what we need using JSON path notation and a sprinkle of Octostache filter syntax:</p>
<div class="table-wrap">




























































<table><thead><tr><th align="left">Variable</th><th align="left">Value</th></tr></thead><tbody><tr><td align="left"><code>Event</code></td><td align="left"><code>#{Octopus.WebhookTrigger.RequestBody.Payload.Event}</code></td></tr><tr><td align="left"><code>SpaceId</code></td><td align="left"><code>#{Event.SpaceId}</code></td></tr><tr><td align="left"><code>DocumentContext</code></td><td align="left"><code>#{Event.ChangeDetails.DocumentContext}</code></td></tr><tr><td align="left"><code>ResponsibleTeams</code></td><td align="left"><code>#{DocumentContext.ResponsibleTeamIds}</code></td></tr><tr><td align="left"><code>RelatedDocumentIds</code></td><td align="left"><code>#{DocumentContext.RelatedDocumentIds}</code></td></tr><tr><td align="left"><code>DeploymentId</code></td><td align="left"><code>#{each item in RelatedDocumentIds}#{if item | StartsWith Deployments-}#{item}#{/if}#{/each}</code></td></tr><tr><td align="left"><code>RunbookRunId</code></td><td align="left"><code>#{each item in RelatedDocumentIds}#{if item | StartsWith RunbookRuns-}#{item}#{/if}#{/each}</code></td></tr><tr><td align="left"><code>ProjectId</code></td><td align="left"><code>#{each item in RelatedDocumentIds}#{if item | StartsWith Projects-}#{item}#{/if}#{/each}</code></td></tr><tr><td align="left"><code>EnvironmentId</code></td><td align="left"><code>#{each item in RelatedDocumentIds}#{if item | StartsWith Environments-}#{item}#{/if}#{/each}</code></td></tr><tr><td align="left"><code>ServerTaskId</code></td><td align="left"><code>#{each item in RelatedDocumentIds}#{if item | StartsWith ServerTasks-}#{item}#{/if}#{/each}</code></td></tr><tr><td align="left"><code>IsDeployment</code></td><td align="left"><code>#{if DeploymentId}true#{else}false#{/if}</code></td></tr><tr><td align="left"><code>IsRunbook</code></td><td align="left"><code>#{if RunbookRunId}true#{else}false#{/if}</code></td></tr><tr><td align="left"><code>OctopusAccount</code></td><td align="left">Octopus Loopback (Generic OIDC Account type)</td></tr></tbody></table></div>
<p>Those JSON paths are a mouthful!
The <code>DeploymentId</code> and <code>RunbookRunId</code> variables are doing the heavy lifting - they loop over <code>RelatedDocumentIds</code> and use the <code>StartsWith</code> filter to sniff out whether this interruption came from a deployment or a runbook run. This lets the Slack message include the right context so people know what they’re clicking into.</p>
<p>If your project has a deployment process or other runbooks, each of these variables can be scoped to the runbook.</p>
<h3 id="runbook-process">Runbook process</h3>
<p>The runbook itself is fairly simple - just two real steps:</p>
<p><strong>Step 1: Get Details</strong> - A script step that uses the Octopus CLI, authenticates back to the instance using our loopback account, and starts rummaging through team membership. For each responsible team, it grabs the members, looks up their Slack IDs, and stitches together the notification message. The result gets stashed as an output variable.
See the <a href="#getting-the-octopus-cli-on-the-worker">Getting the Octopus CLI on the worker</a> section to ensure the CLI is available for this step.</p>
<p><strong>Step 2: Send a Slack Message</strong> - The built-in Slack step picks up the message via <code>#{Octopus.Action[Get Details].Output.Message}</code> and fires it off to the channel. No fuss, no custom integrations, just a step that does exactly what it says on the tin.</p>
<details data-group="code">
<summary>Bash</summary>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#008000"># Authenticate the CLI back to this Octopus instance. The OIDC token is</span></span>
<span class="line"><span style="color:#008000"># issued to the service account for the life of the runbook run, so there is</span></span>
<span class="line"><span style="color:#008000"># no API key to store anywhere.</span></span>
<span class="line"><span style="color:#795E26">octopus</span><span style="color:#A31515"> login</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">    --server</span><span style="color:#A31515"> "$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "Octopus.Web.ServerUri")"</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">    --service-account-id</span><span style="color:#A31515"> "$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "OctopusAccount.Audience")"</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">    --id-token</span><span style="color:#A31515"> "$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "OctopusAccount.OpenIdConnect.Jwt")"</span><span style="color:#EE0000"> \</span></span>
<span class="line"><span style="color:#0000FF">    --no-prompt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Project variables, already extracted from the webhook payload</span></span>
<span class="line"><span style="color:#001080">spaceId</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "SpaceId"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">projectId</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "ProjectId"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">environmentId</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "EnvironmentId"</span><span style="color:#000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># get the names to use in the message</span></span>
<span class="line"><span style="color:#001080">executionName</span><span style="color:#000000">=$(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api</span><span style="color:#A31515"> /api/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/projects/</span><span style="color:#001080">$projectId</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.Name'</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">environmentName</span><span style="color:#000000">=$(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api</span><span style="color:#A31515"> /api/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/environments/</span><span style="color:#001080">$environmentId</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.Name'</span><span style="color:#000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># An interruption can come from a deployment or a runbook run</span></span>
<span class="line"><span style="color:#001080">resourceType</span><span style="color:#000000">=</span><span style="color:#A31515">"deployment"</span></span>
<span class="line"><span style="color:#AF00DB">if</span><span style="color:#000000"> [[ $(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "IsRunbook"</span><span style="color:#000000">) == </span><span style="color:#A31515">"true"</span><span style="color:#000000"> ]]; </span><span style="color:#AF00DB">then</span></span>
<span class="line"><span style="color:#001080">    resourceType</span><span style="color:#000000">=</span><span style="color:#A31515">"runbook"</span></span>
<span class="line"><span style="color:#001080">    runbookRunId</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "RunbookRunId"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">    runbookId</span><span style="color:#000000">=$(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api</span><span style="color:#A31515"> /api/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/runbookRuns/</span><span style="color:#001080">$runbookRunId</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.RunbookId'</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">    executionName</span><span style="color:#000000">=$(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api</span><span style="color:#A31515"> /api/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/runbooks/</span><span style="color:#001080">$runbookId</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.Name'</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#AF00DB">fi</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Deep link straight to the task in the Octopus web UI</span></span>
<span class="line"><span style="color:#001080">taskUrl</span><span style="color:#000000">=</span><span style="color:#A31515">"$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "Octopus.Web.ServerUri")/app#/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/tasks/$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "ServerTaskId")"</span></span>
<span class="line"><span style="color:#001080">message</span><span style="color:#000000">=</span><span style="color:#A31515">"Manual intervention required for &#x3C;</span><span style="color:#001080">$taskUrl</span><span style="color:#A31515">|</span><span style="color:#001080">$resourceType</span><span style="color:#A31515">> for *</span><span style="color:#001080">$executionName</span><span style="color:#A31515">* to *</span><span style="color:#001080">$environmentName</span><span style="color:#A31515">*"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Fetch the users once, rather than an API call per team member</span></span>
<span class="line"><span style="color:#001080">users</span><span style="color:#000000">=$(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api</span><span style="color:#A31515"> "/api/users?take=2147483647"</span><span style="color:#000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">teams</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "ResponsibleTeams"</span><span style="color:#000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Someone can be in more than one responsible team - only mention them once</span></span>
<span class="line"><span style="color:#0000FF">declare</span><span style="color:#0000FF"> -A</span><span style="color:#001080"> seenUsers</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Loop over each responsible team</span></span>
<span class="line"><span style="color:#AF00DB">while</span><span style="color:#795E26"> read</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> teamId</span><span style="color:#000000">; </span><span style="color:#AF00DB">do</span></span>
<span class="line"><span style="color:#008000">    # Loop over each member of that team</span></span>
<span class="line"><span style="color:#AF00DB">    while</span><span style="color:#795E26"> read</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> userId</span><span style="color:#000000">; </span><span style="color:#AF00DB">do</span></span>
<span class="line"><span style="color:#000000">        [[ -n </span><span style="color:#A31515">"${</span><span style="color:#001080">seenUsers</span><span style="color:#A31515">[</span><span style="color:#001080">$userId</span><span style="color:#A31515">]+</span><span style="color:#001080">x</span><span style="color:#A31515">}"</span><span style="color:#000000"> ]] &#x26;&#x26; </span><span style="color:#AF00DB">continue</span></span>
<span class="line"><span style="color:#001080">        seenUsers</span><span style="color:#000000">[</span><span style="color:#001080">$userId</span><span style="color:#000000">]=</span><span style="color:#A31515">1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">        user</span><span style="color:#000000">=$(</span><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$users</span><span style="color:#A31515">"</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#A31515"> ".Items[] | select(.Id == </span><span style="color:#EE0000">\"</span><span style="color:#001080">$userId</span><span style="color:#EE0000">\"</span><span style="color:#A31515">)"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">        displayName</span><span style="color:#000000">=$(</span><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$user</span><span style="color:#A31515">"</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.DisplayName'</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#001080">        email</span><span style="color:#000000">=$(</span><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$user</span><span style="color:#A31515">"</span><span style="color:#000000"> | </span><span style="color:#795E26">jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.EmailAddress'</span><span style="color:#000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000">        # Try getting the Slack UserId from the map</span></span>
<span class="line"><span style="color:#008000">        # Fall back to the display name when there is no mapping.</span></span>
<span class="line"><span style="color:#001080">        slackUserId</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "</span><span style="color:#001080">$email</span><span style="color:#A31515">"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#AF00DB">        if</span><span style="color:#000000"> [[ -z </span><span style="color:#A31515">"</span><span style="color:#001080">$slackUserId</span><span style="color:#A31515">"</span><span style="color:#000000"> ]]; </span><span style="color:#AF00DB">then</span></span>
<span class="line"><span style="color:#001080">            message</span><span style="color:#000000">+=</span><span style="color:#A31515">$'</span><span style="color:#EE0000">\n</span><span style="color:#A31515">'"${</span><span style="color:#001080">displayName</span><span style="color:#A31515">}"</span></span>
<span class="line"><span style="color:#AF00DB">        else</span></span>
<span class="line"><span style="color:#001080">            message</span><span style="color:#000000">+=</span><span style="color:#A31515">$'</span><span style="color:#EE0000">\n</span><span style="color:#A31515">'"&#x3C;@${</span><span style="color:#001080">slackUserId</span><span style="color:#A31515">}>"</span></span>
<span class="line"><span style="color:#AF00DB">        fi</span></span>
<span class="line"><span style="color:#AF00DB">    done</span><span style="color:#000000"> &#x3C; </span><span style="color:#A31515">&#x3C;(</span><span style="color:#795E26">octopus</span><span style="color:#A31515"> api "/api/teams/</span><span style="color:#001080">$teamId</span><span style="color:#A31515">" </span><span style="color:#000000">|</span><span style="color:#795E26"> jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.MemberUserIds[]')</span></span>
<span class="line"><span style="color:#AF00DB">done</span><span style="color:#000000"> &#x3C; </span><span style="color:#A31515">&#x3C;(</span><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$teams</span><span style="color:#A31515">" </span><span style="color:#000000">|</span><span style="color:#795E26"> jq</span><span style="color:#0000FF"> -r</span><span style="color:#A31515"> '.[]')</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Hand the finished message to the Slack step</span></span>
<span class="line"><span style="color:#795E26">set_octopusvariable</span><span style="color:#A31515"> "Message"</span><span style="color:#A31515"> "</span><span style="color:#001080">$message</span><span style="color:#A31515">"</span></span></code></pre>
</details>
<details data-group="code">
<summary>PowerShell</summary>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="powershell"><code><span class="line"><span style="color:#008000"># Authenticate the CLI back to this Octopus instance. The OIDC token is</span></span>
<span class="line"><span style="color:#008000"># issued to the service account for the life of the runbook run, so there is</span></span>
<span class="line"><span style="color:#008000"># no API key to store anywhere.</span></span>
<span class="line"><span style="color:#000000">&#x26; octopus login `</span></span>
<span class="line"><span style="color:#000000">    --server </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"Octopus.Web.ServerUri"</span><span style="color:#000000">] `</span></span>
<span class="line"><span style="color:#000000">    --service-account-id </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"OctopusAccount.Audience"</span><span style="color:#000000">] `</span></span>
<span class="line"><span style="color:#000000">    --id-token </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"OctopusAccount.OpenIdConnect.Jwt"</span><span style="color:#000000">] `</span></span>
<span class="line"><span style="color:#000000">    --no-prompt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Project variables, already extracted from the webhook payload</span></span>
<span class="line"><span style="color:#001080">$spaceId</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"SpaceId"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#001080">$projectId</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"ProjectId"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#001080">$environmentId</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"EnvironmentId"</span><span style="color:#000000">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># get the names to use in the message</span></span>
<span class="line"><span style="color:#001080">$executionName</span><span style="color:#000000"> = (&#x26; octopus api /api/</span><span style="color:#001080">$spaceId</span><span style="color:#000000">/projects/</span><span style="color:#001080">$projectId</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span><span style="color:#000000">).Name</span></span>
<span class="line"><span style="color:#001080">$environmentName</span><span style="color:#000000"> = (&#x26; octopus api /api/</span><span style="color:#001080">$spaceId</span><span style="color:#000000">/environments/</span><span style="color:#001080">$environmentId</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span><span style="color:#000000">).Name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># An interruption can come from a deployment or a runbook run</span></span>
<span class="line"><span style="color:#001080">$resourceType</span><span style="color:#000000"> = </span><span style="color:#A31515">"deployment"</span></span>
<span class="line"><span style="color:#AF00DB">if</span><span style="color:#000000"> (</span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"IsRunbook"</span><span style="color:#000000">] -eq </span><span style="color:#A31515">"true"</span><span style="color:#000000">) {</span></span>
<span class="line"><span style="color:#001080">    $resourceType</span><span style="color:#000000"> = </span><span style="color:#A31515">"runbook"</span></span>
<span class="line"><span style="color:#001080">    $runbookRunId</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"RunbookRunId"</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#001080">    $runbookId</span><span style="color:#000000"> = (&#x26; octopus api /api/</span><span style="color:#001080">$spaceId</span><span style="color:#000000">/runbookRuns/</span><span style="color:#001080">$runbookRunId</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span><span style="color:#000000">).RunbookId</span></span>
<span class="line"><span style="color:#001080">    $executionName</span><span style="color:#000000"> = (&#x26; octopus api /api/</span><span style="color:#001080">$spaceId</span><span style="color:#000000">/runbooks/</span><span style="color:#001080">$runbookId</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span><span style="color:#000000">).Name</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Deep link straight to the task in the Octopus web UI</span></span>
<span class="line"><span style="color:#001080">$taskUrl</span><span style="color:#000000"> = </span><span style="color:#A31515">"</span><span style="color:#0000FF">$(</span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000FF">[</span><span style="color:#A31515">"Octopus.Web.ServerUri"</span><span style="color:#000000FF">]</span><span style="color:#0000FF">)</span><span style="color:#A31515">/app#/</span><span style="color:#001080">$spaceId</span><span style="color:#A31515">/tasks/</span><span style="color:#0000FF">$(</span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000FF">[</span><span style="color:#A31515">"ServerTaskId"</span><span style="color:#000000FF">]</span><span style="color:#0000FF">)</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#001080">$message</span><span style="color:#000000"> = </span><span style="color:#A31515">"Manual intervention required for &#x3C;</span><span style="color:#001080">$taskUrl</span><span style="color:#A31515">|</span><span style="color:#001080">$resourceType</span><span style="color:#A31515">> for *</span><span style="color:#001080">$executionName</span><span style="color:#A31515">* to *</span><span style="color:#001080">$environmentName</span><span style="color:#A31515">*"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Fetch the users once, rather than an API call per team member</span></span>
<span class="line"><span style="color:#001080">$users</span><span style="color:#000000"> = &#x26; octopus api </span><span style="color:#A31515">"/api/users?take=2147483647"</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">$teams</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#A31515">"ResponsibleTeams"</span><span style="color:#000000">] | </span><span style="color:#795E26">ConvertFrom-Json</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Someone can be in more than one responsible team - only mention them once</span></span>
<span class="line"><span style="color:#001080">$seenUsers</span><span style="color:#000000"> = </span><span style="color:#0000FF">@</span><span style="color:#000000">{}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Loop over each responsible team</span></span>
<span class="line"><span style="color:#AF00DB">foreach</span><span style="color:#000000"> (</span><span style="color:#001080">$teamId</span><span style="color:#AF00DB"> in</span><span style="color:#001080"> $teams</span><span style="color:#000000">) {</span></span>
<span class="line"><span style="color:#001080">    $teamData</span><span style="color:#000000"> = &#x26; octopus api /api/teams/</span><span style="color:#001080">$teamId</span><span style="color:#000000"> | </span><span style="color:#795E26">ConvertFrom-Json</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000">    # Loop over each member of that team</span></span>
<span class="line"><span style="color:#AF00DB">    foreach</span><span style="color:#000000"> (</span><span style="color:#001080">$userId</span><span style="color:#AF00DB"> in</span><span style="color:#001080"> $teamData</span><span style="color:#795E26">.MemberUserIds</span><span style="color:#000000">) {</span></span>
<span class="line"><span style="color:#AF00DB">        if</span><span style="color:#000000"> (</span><span style="color:#001080">$seenUsers</span><span style="color:#795E26">.ContainsKey</span><span style="color:#000000">(</span><span style="color:#001080">$userId</span><span style="color:#000000">)) { </span><span style="color:#AF00DB">continue</span><span style="color:#000000"> }</span></span>
<span class="line"><span style="color:#001080">        $seenUsers</span><span style="color:#000000">[</span><span style="color:#001080">$userId</span><span style="color:#000000">] = </span><span style="color:#0000FF">$true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">        $user</span><span style="color:#000000"> = </span><span style="color:#001080">$users</span><span style="color:#795E26">.Items</span><span style="color:#000000"> | </span><span style="color:#795E26">Where-Object</span><span style="color:#000000"> { </span><span style="color:#001080">$_</span><span style="color:#795E26">.Id</span><span style="color:#000000"> -eq </span><span style="color:#001080">$userId</span><span style="color:#000000"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000">        # Try getting the Slack UserId from the map</span></span>
<span class="line"><span style="color:#008000">        # Fall back to the display name when there is no mapping.</span></span>
<span class="line"><span style="color:#001080">        $slackUserId</span><span style="color:#000000"> = </span><span style="color:#001080">$OctopusParameters</span><span style="color:#000000">[</span><span style="color:#001080">$user</span><span style="color:#795E26">.EmailAddress</span><span style="color:#000000">]</span></span>
<span class="line"><span style="color:#AF00DB">        if</span><span style="color:#000000"> (-not </span><span style="color:#001080">$slackUserId</span><span style="color:#000000">) {</span></span>
<span class="line"><span style="color:#001080">            $message</span><span style="color:#000000"> += </span><span style="color:#A31515">"</span><span style="color:#EE0000">`n</span><span style="color:#0000FF">$(</span><span style="color:#001080">$user</span><span style="color:#795E26">.DisplayName</span><span style="color:#0000FF">)</span><span style="color:#A31515">"</span></span>
<span class="line"><span style="color:#000000">        } </span><span style="color:#AF00DB">else</span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#001080">            $message</span><span style="color:#000000"> += </span><span style="color:#A31515">"</span><span style="color:#EE0000">`n</span><span style="color:#A31515">&#x3C;@</span><span style="color:#001080">$slackUserId</span><span style="color:#A31515">>"</span></span>
<span class="line"><span style="color:#000000">        }</span></span>
<span class="line"><span style="color:#000000">    }</span></span>
<span class="line"><span style="color:#000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Hand the finished message to the Slack step</span></span>
<span class="line"><span style="color:#795E26">Set-OctopusVariable</span><span style="color:#000000"> -Name </span><span style="color:#A31515">"Message"</span><span style="color:#000000"> -Value </span><span style="color:#001080">$message</span></span></code></pre>
</details>
<h3 id="getting-the-octopus-cli-on-the-worker">Getting the Octopus CLI on the worker</h3>
<p>Before you can run this, you will need to ensure that the Octopus CLI is available on the worker. There are a few ways to achieve this:</p>
<ol>
<li>
<p>Use the Octopus Worker Tools execution container</p>
<p>This will require configuring a <a href="https://octopus.com/docs/packaging-applications/package-repositories/guides/container-registries/docker-hub/#adding-docker-hub-as-an-octopus-external-feed">Docker Feed</a>, then configuring the script step to <a href="https://octopus.com/docs/projects/steps/execution-containers-for-workers/#how-to-use-execution-containers-for-workers">run inside the Worker Tools container</a>.</p>
<div class="warning"><p>The script uses the octopus <code>api</code> command, which was added in Octopus CLI 2.21.0. Use the octopusdeploy/worker-tools<ubuntu></ubuntu>.24.04 image or later. The older ubuntu.22.04 image only ships with CLI version 2.20.1.</p></div>
</li>
<li>
<p>Install the Octopus CLI on your worker, and ensure it is in the system path.</p>
<p>This might not be possible, for example, the dynamic workers on Octopus Cloud do not have this installed.</p>
</li>
<li>
<p>Upload the CLI as a package to the built-in feed.</p>
<p>Download the appropriate architecture archive file from a <a href="https://github.com/OctopusDeploy/cli/releases/tag/v2.26.0">CLI release</a>, rename it to the appropriate format, such as <code>octopus.2.26.0.zip</code> or <code>octopus.2.26.0.tar.gz</code>, and upload it to the built-in feed.</p>
<p>Reference this package on the step as a referenced dependency, and update the <code>octopus</code> commands in the script to be <code>octopus/octopus</code> because the package will be extracted to a subdirectory using the package id, and add the line at the top of the script <code>chmod +x octopus/octopus</code> if your script will be running on Linux.</p>
</li>
</ol>
<h3 id="testing-it">Testing it</h3>
<p>Following this guide is all well and good, but what if your solution is based on a different event type? The incoming document context is going to be a different shape.</p>
<p>The first thing you should do is configure your subscription, runbook, and trigger. Put a single script step in your runbook process that writes out the webhook payload to the log file:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#001080">body</span><span style="color:#000000">=$(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "Octopus.WebhookTrigger.RequestBody"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#795E26">echo</span><span style="color:#A31515"> "</span><span style="color:#001080">$body</span><span style="color:#A31515">"</span></span></code></pre>
<p>Publish the runbook and wait for an event to be raised.</p>
<p>Once you have your sample payload, disable the trigger, create a new project variable called <code>Octopus.WebhookTrigger.RequestBody</code>, and set its value to the payload from the task log.</p>
<p>Once you have this in place, you can use the Variable Test page, under <em>Variable Preview</em>, to test the output of your various variable expressions.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/variable-preview.png" alt="Variable preview" loading="lazy" srcset="/blog/i/600/features-to-solutions/variable-preview.webp 600w, /blog/i/1000/features-to-solutions/variable-preview.webp 1000w, /blog/i/2000/features-to-solutions/variable-preview.webp 1019w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1019" height="829"></p></figure>
<h3 id="final-step">Final step</h3>
<p>This is an <strong>important</strong> step: before you can use the runbook you have built, it will need to be published, similar to making a release for a deployment. Both the webhook trigger and the scheduled trigger will only run the published version of a runbook.</p>
<p>The <strong>Publish</strong> button is available on the Runbook Runs page or the task page of a successfully run.</p>
<h3 id="the-result">The result</h3>
<p>The updated message that we end up with after this is simple but includes the details and calls out the users directly in Slack. Including the deployment link, project name, environment, and responsible users makes it a huge difference to the original message.</p>
<figure><p><img src="/blog/i/x/features-to-solutions/slack-custom-message.png" alt="Custom generated Slack message" loading="lazy" srcset="/blog/i/600/features-to-solutions/slack-custom-message.webp 600w, /blog/i/1000/features-to-solutions/slack-custom-message.webp 604w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="604" height="160"></p></figure>
<h2 id="conclusion">Conclusion</h2>
<p>Most of the features outlined in this post aren’t big on their own. A subscription raises an event, a webhook trigger starts a runbook, a few variables rummage through the payload, and a script step and the built-in Slack step do the rest. Together, they turn a manual intervention from something that sits quietly in a corner of the Octopus UI, waiting for someone to notice it, into a Slack message that taps the right people on the shoulder.</p>
<p>The same pattern works well beyond manual interventions. Any event a subscription can raise can now start a runbook, so the next time you find yourself wishing Octopus would just <em>do something</em> when an event happens, have a poke around the features you already have before reaching for a new tool. There may be a solution hiding in plain sight, like a pending manual intervention.</p>
<p>If you need some inspiration, here are a few events that would suit the same pattern:</p>
<ul>
<li><strong>Guided failure:</strong> This is the manual intervention’s slightly more stressed-out cousin. It uses the same interruption payload, so the runbook in this post needs little more than a more urgent message.</li>
<li><strong>Deployment failed:</strong> Rather than waking up the whole channel, look up the commits in the release and politely tap the people who made the changes.</li>
<li><strong>Certificate expiring:</strong> Renew the certificate, upload it to Octopus, and redeploy anything that uses it, long before anyone notices the padlock has gone missing.</li>
<li><strong>Production deployment succeeded:</strong> Post the release notes, update the status page, and move the linked tickets to done, so the fun part of shipping doesn’t come with a pile of admin.</li>
<li><strong>Approvals integration with an ITSM system:</strong> Use our upcoming approvals feature to raise Change Requests with an ITSM system that Octopus doesn’t current support out of the box, such as BMC Helix or ZenDesk. Look out for a future blog post on this topic.</li>
</ul>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Introducing the pipeline explorer</title>
      <link href="https://octopus.com/blog/create-a-pipeline-explorer" />
      <id>https://octopus.com/blog/create-a-pipeline-explorer</id>
      <published>2026-10-23</published>
      <updated>2026-10-23</updated>
      <summary>The pipeline explorer shows you your project's pipeline, its dependencies, and anything that needs fixing, all in one place.</summary>
      <author>
        <name>Jed Lehmann, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The first time you open an unfamiliar Octopus project (inheriting it, joining a new team, or exploring a corner of your own instance you haven’t touched before), you’re looking at someone else’s decisions with none of the context that went into making them. Real projects pick up channels, triggers, approvals, and a growing list of targets over time.</p>
<p>The pipeline explorer brings a project’s configuration together in one place. It helps you understand how an Octopus project works, on one page.</p>
<p>It’s built for anyone who’s new to a project: joining a team, inheriting ownership, or exploring an unfamiliar corner of their own instance.</p>
<p>It visualizes your deployment pipeline and deployment process as workflow diagrams, showing you how a release gets triggered and how it moves through your environments depending on the channel.</p>
<h2 id="what-the-pipeline-explorer-does">What the pipeline explorer does</h2>
<h3 id="1-pipeline-visualization">1. Pipeline visualization</h3>
<p>The pipeline explorer presents your project’s deployment pipeline and deployment process as workflow diagrams. Seeing them visually lets you understand how a release is triggered and how it moves through your environments, without piecing that together from separate tabs.</p>
<figure>
<p><img src="/blog/i/x/create-a-pipeline-explorer/pipeline-visualization.png" alt="Pipeline explorer showing a Git trigger automatically deploying to Staging, then a manual ServiceNow approval before a frozen Production environment" loading="lazy" srcset="/blog/i/600/create-a-pipeline-explorer/pipeline-visualization.webp 600w, /blog/i/1000/create-a-pipeline-explorer/pipeline-visualization.webp 1000w, /blog/i/2000/create-a-pipeline-explorer/pipeline-visualization.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2878" height="736"></p>
</figure>
<h3 id="2-process-visualization">2. Process visualization</h3>
<p>A project’s deployment process depends on settings that live outside its process, like lifecycles, deployment targets, credentials, and approvals. The pipeline explorer surfaces these connections directly, so you don’t need to already know where each one lives.</p>
<figure>
<p><img src="/blog/i/x/create-a-pipeline-explorer/process-visualization.png" alt="Pipeline explorer showing a deployment process step linked to its package, Git artifact, an expiring credential, and Kubernetes targets with health status" loading="lazy" srcset="/blog/i/600/create-a-pipeline-explorer/process-visualization.webp 600w, /blog/i/1000/create-a-pipeline-explorer/process-visualization.webp 1000w, /blog/i/2000/create-a-pipeline-explorer/process-visualization.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3120" height="744"></p>
</figure>
<h3 id="3-configuration-issues">3. Configuration issues</h3>
<p>The pipeline explorer flags conditions that could affect an upcoming deployment directly in the view where you’re already looking.</p>
<figure>
<p><img src="/blog/i/x/create-a-pipeline-explorer/configuration-issues.png" alt="Kubernetes targets in the pipeline explorer flagged as Unhealthy, Warning, and Healthy" loading="lazy" srcset="/blog/i/600/create-a-pipeline-explorer/configuration-issues.webp 600w, /blog/i/1000/create-a-pipeline-explorer/configuration-issues.webp 1000w, /blog/i/2000/create-a-pipeline-explorer/configuration-issues.webp 1040w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1040" height="544"></p>
</figure>
<h2 id="what-it-doesnt-do">What it doesn’t do</h2>
<p>The pipeline explorer doesn’t replace your existing process, variables, or settings pages. It’s a new view built on top of the same data, not a redesign of how you configure a project. Think of it as a project map.</p>
<h2 id="try-it">Try it</h2>
<p>Open any project and select <strong>Pipeline explorer</strong> from the navigation to see it for yourself. Next time you visit an unfamiliar project, this is where you’ll want to start.</p>
<p><a href="https://octopus.com/docs/projects/pipeline-explorer">Read the docs</a> for more details.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Multi-node support for Polling Tentacles</title>
      <link href="https://octopus.com/blog/multi-node-polling-tentacles" />
      <id>https://octopus.com/blog/multi-node-polling-tentacles</id>
      <published>2026-10-21</published>
      <updated>2026-10-21</updated>
      <summary>Polling Tentacles can now connect to any node in a self-hosted Octopus High Availability cluster through a single load-balanced address.</summary>
      <author>
        <name>Luke Butters, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Polling Tentacles no longer need to poll every Octopus Server node in a self-hosted High Availability (HA) cluster. In the next GA release of 2026.4, Polling Tentacles can poll a single address instead.</p>
<p>Polling Tentacles connect out to Octopus Server, so you don’t need to open inbound firewall ports to reach them like you do for Listening Tentacles. That keeps your architecture simpler and more secure. However, if you run Octopus Server across multiple nodes and use Polling Tentacles, every Tentacle has to know about every node. Add a node, and you need to update every Tentacle. Remove a node, and you need to update them all again. With hundreds or thousands of Tentacles, that’s a lot of work just to resize your cluster.</p>
<p>Multi-node support for Polling Tentacles removes that restriction.</p>
<h2 id="why-every-tentacle-had-to-poll-every-node">Why every Tentacle had to poll every node</h2>
<p>A Polling Tentacle connects to Octopus Server and asks, “Do you have any work for me?”</p>
<p>In an HA cluster, any node can pick up a task. When a node runs a deployment that needs a Polling Tentacle, that node holds the work for that Tentacle. The Tentacle must poll that specific node to collect the work.</p>
<p>If the Tentacle is polling a different node, it never sees the work, and the deployment times out.</p>
<p>That’s why <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/poll-every-node">the guidance for Polling Tentacles with HA</a> has previously been to configure each Tentacle to poll every HA node.</p>
<p>This would also prevent the use of a load balancer with Polling Tentacles. The load balancer could send the Tentacle to any node, which might not be the one holding its work.</p>
<h2 id="what-changed-in-octopus-server-202646909">What changed in Octopus Server 2026.4.6909</h2>
<p>With multi-node support for Polling Tentacles, the nodes share a single queue of pending requests. The queue lives in Redis, a shared in-memory data store.</p>
<p>When a node needs a Polling Tentacle to perform an action, it puts the request in Redis. When the Tentacle polls a node for work, that node collects the Tentacle’s next request from Redis. The Tentacle’s response goes back through Redis to the node that originally queued the work.</p>
<p>Thus, it no longer matters which node a Tentacle connects to. Each Tentacle only needs to poll one address, and that address can be a load balancer that spreads Tentacles across all the nodes.</p>
<p>This feature has been in use in <a href="https://octopus.com/docs/octopus-cloud">Octopus Cloud</a> for some time. We’re now making it available for self-hosted customers.</p>
<p>A few details are worth knowing:</p>
<ul>
<li>Octopus encrypts messages with your <a href="https://octopus.com/docs/security/data-encryption">Master Key</a> before storing them in Redis.</li>
<li>To keep Redis memory usage low, Octopus writes larger files, such as packages, to the <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#cluster-shared-storage">cluster shared directory</a> so every node can read them.</li>
<li>A Tentacle’s <strong>Connectivity</strong> page now shows communication logs from every active node, not just the node your browser is connected to.</li>
<li>This change doesn’t affect Listening Tentacles, which Octopus Server connects to directly.</li>
</ul>
<h2 id="what-you-need">What you need</h2>
<p>To use multi-node support for Polling Tentacles, you need:</p>
<ul>
<li><strong>Octopus Server 2026.4.6909 or later</strong> installed on every node in the HA cluster</li>
<li><strong>A <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#redis-requirements">Redis instance</a></strong> that every node can reach, set up as a short-lived queue rather than a database</li>
<li><strong>A <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#cluster-shared-storage">cluster shared directory</a></strong> that every node can read and write</li>
<li><strong>A <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#load-balancer">TCP load balancer</a> (optional)</strong> in front of the Polling Tentacle port, so your Tentacles only need one address</li>
</ul>
<p>To get early access before 2026.4 ships, <a href="https://octopus.com/support">contact support</a>.</p>
<h2 id="getting-started">Getting started</h2>
<p>You enable multi-node support for Polling Tentacles by giving each node the same Redis connection string. You can do this from the command line, with an environment variable in the Linux container, or with the <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#helm-chart">Octopus Deploy Helm chart</a>. The Helm chart can also run Redis and create the load balancer for you.</p>
<p>Once multi-node support for Polling Tentacles is turned on, you can configure Polling Tentacles to connect to one node, all nodes, or anything in between.
You don’t have to reconfigure Tentacles straight away, and you can <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles#register-polling-tentacles">point your Tentacles at a load balancer</a> at your own pace.</p>
<p>The <a href="https://octopus.com/docs/administration/high-availability/polling-tentacles-with-ha/multi-node-polling-tentacles">docs for multi-node support for Polling Tentacles</a> walk you through each step.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Now you can add or remove nodes in your HA cluster without reconfiguring every Tentacle.</p>
<p>If you’re thinking about moving to HA in the first place, read how one customer <a href="https://octopus.com/blog/migrating-to-high-availability-fsi-success-story">migrated to High Availability</a> one safe step at a time.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Apache Ant Deployments with Octopus</title>
      <link href="https://octopus.com/blog/ant-deployments" />
      <id>https://octopus.com/blog/ant-deployments</id>
      <published>2026-10-20</published>
      <updated>2026-10-20</updated>
      <summary>Learn how to deploy Ant applications using Octopus Deploy</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Apache Ant is a Java-based build tool that has been around for over 20 years. Despite its age, Ant is still actively developed and used in many enterprise deployments. Octopus can be used to deploy Ant applications with a few simple steps. This post will demonstrate how to deploy a sample Ant application using Octopus.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Download the sample application from <a href="https://github.com/OctopusSolutionsEngineering/ApacheAntExample">this GitHub repository</a>. The <a href="https://github.com/OctopusSolutionsEngineering/ApacheAntExample/releases">releases</a> provide ZIP files that can be uploaded to the Octopus built-in feed:</p>
<p><img src="/blog/i/x/ant-deployments/built-in-feed.png" alt="Built-in feed" loading="lazy" srcset="/blog/i/600/ant-deployments/built-in-feed.webp 600w, /blog/i/1000/ant-deployments/built-in-feed.webp 1000w, /blog/i/2000/ant-deployments/built-in-feed.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1930"></p>
<h2 id="preparing-the-ant-tooling">Preparing the Ant tooling</h2>
<p>Ant was first released in 2000 and predates many popular DevOps tools like Docker by many years. While <a href="https://octopus.com/docs/projects/steps/execution-containers-for-workers">execution containers</a> are a great solution for exposing Ant and its dependencies to your Octopus workers, we’ll make the assumption here that teams using Ant may not have implemented Docker in their deployment workflows.</p>
<p>Instead, Ant and its dependencies will be exposed to the Octopus workers by extracting portable versions of Ant and Java into the working directory as a step is executed.</p>
<p>Start by downloading a binary distribution of Ant from the <a href="https://ant.apache.org/bindownload.cgi">Apache Ant website</a>. At the time of writing, the file you will download is called <code>apache-ant-1.10.18-bin.zip</code>.</p>
<p>Octopus requires a strict filename structure for packages uploaded to the built-in feed. The filename must be in the format <code>&#x3C;package-name>.&#x3C;version>.&#x3C;extension></code>. For example, <code>apache-ant.1.10.18.zip</code>. So rename the file you downloaded to <code>apache-ant.1.10.18.zip</code> before uploading it to the built-in feed.</p>
<p>Ant requires a JDK to run. There are many JDK distributions available, but for this example, we’ll use the <a href="https://www.azul.com/downloads/#downloads-table-zulu">Azul Zulu OpenJDK</a> distribution. We’ll assume that the workers are running Windows, so download the ZIP distribution of the JDK. At the time of writing, the file you will download is called <code>zulu27.28.101-ca-jdk27.0.0-win_x64.zip</code>. Rename the file to <code>zulu27.28.101.zip</code> before uploading it to the built-in feed.</p>
<p>You must now have 3 packages in the built-in feed:</p>
<p><img src="/blog/i/x/ant-deployments/all-packages.png" alt="Built-in feed with all packages" loading="lazy" srcset="/blog/i/600/ant-deployments/all-packages.webp 600w, /blog/i/1000/ant-deployments/all-packages.webp 1000w, /blog/i/2000/ant-deployments/all-packages.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1930"></p>
<h2 id="running-ant-in-a-deployment-step">Running Ant in a deployment step</h2>
<p>Ant is run as part of a script step in an Octopus deployment. The script step will extract the Ant and JDK packages into the working directory, set the <code>JAVA_HOME</code> environment variable, and append both Ant and Java to the session <code>Path</code> variable. This allows Ant to be executed on a worker without requiring any installation of Ant or Java on the worker itself:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span># 1. Define explicit working directories</span></span>
<span class="line"><span># Note that the Ant and JDK packages include their own versioned subfolders, so we need to include those in the path.</span></span>
<span class="line"><span># The versioned subfolders are unique to the package version, so if you update the package version, you'll need to update the path here as well.</span></span>
<span class="line"><span>$workDir   = Get-Location</span></span>
<span class="line"><span>$antBinDir = "$workDir\apache-ant\apache-ant-1.10.18\bin"</span></span>
<span class="line"><span>$javaDir   = "$workDir\zulu\zulu27.28.101-ca-jdk27.0.0-win_x64"</span></span>
<span class="line"><span></span></span>
<span class="line"><span># 2. Set JAVA_HOME and explicitly append BOTH Java and Ant to the session Path</span></span>
<span class="line"><span>$env:JAVA_HOME = $javaDir</span></span>
<span class="line"><span>$env:Path = "$antBinDir;$javaDir\bin;" + $env:Path</span></span>
<span class="line"><span></span></span>
<span class="line"><span># 3. Verify the binary actually exists where we expect it</span></span>
<span class="line"><span>if (-not (Test-Path "$antBinDir\ant.bat")) {</span></span>
<span class="line"><span>    Write-Error "Could not find ant.bat at: $antBinDir"</span></span>
<span class="line"><span>    Exit 1</span></span>
<span class="line"><span>}</span></span>
<span class="line"><span></span></span>
<span class="line"><span># 4. Change directory to your app folder</span></span>
<span class="line"><span>cd antdemo</span></span>
<span class="line"><span></span></span>
<span class="line"><span># 5. Run Ant using the call operator (&#x26;) to force PowerShell to resolve the executable path</span></span>
<span class="line"><span>&#x26; ant.bat compile</span></span>
<span class="line"><span>&#x26; ant.bat jar</span></span>
<span class="line"><span>&#x26; ant.bat run</span></span></code></pre>
<p>The three packages (the Ant Demo application, the Ant binary distribution, and the JDK) are included as Referenced Dependencies. This ensures the packages are downloaded and extracted into the step’s working directory:</p>
<p><img src="/blog/i/x/ant-deployments/referenced-dependencies.png" alt="Referenced dependencies" loading="lazy" srcset="/blog/i/600/ant-deployments/referenced-dependencies.webp 600w, /blog/i/1000/ant-deployments/referenced-dependencies.webp 1000w, /blog/i/2000/ant-deployments/referenced-dependencies.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1930"></p>
<h2 id="running-the-deployment">Running the deployment</h2>
<p>At this point, you can create a release of the project and deploy it to an environment. The deployment log will show the output of the Ant commands as they are executed:</p>
<p><img src="/blog/i/x/ant-deployments/ant-output.png" alt="Ant output" loading="lazy" srcset="/blog/i/600/ant-deployments/ant-output.webp 600w, /blog/i/1000/ant-deployments/ant-output.webp 1000w, /blog/i/2000/ant-deployments/ant-output.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1930"></p>
<h2 id="conclusion">Conclusion</h2>
<p>Executing Ant as part of an Octopus deployment is straightforward. By using the built-in feed to provide Ant and its dependencies, you can run Ant on any worker without requiring any installation of Ant or Java on the worker itself. This approach allows teams to deploy Ant applications consistently and repeatably by leveraging Octopus Deploy.</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.11: Haute culture</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e11" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e11</id>
      <published>2026-10-20</published>
      <updated>2026-10-20</updated>
      <summary>Find out why culture is such a crucial requirement of high-performance software delivery.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the previous episode, we discussed <a href="https://octopus.com/blog/continuous-delivery-office-hours-e10">modernizing legacy architecture</a>, including the hazards of the rewrite, architectural strategies, and team design considerations.</p>
<p>This episode looks at “haute culture”, or high-performance organizational culture, and its profound impact on software delivery.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p>You can watch the episode below, or read on to find some of the key discussion points.</p>
<p><a href="https://www.youtube.com/watch?v=BDbegubJrgU">Watch Continuous Delivery Office Hours Ep.11</a></p>
<h2 id="posters-arent-culture">Posters aren’t culture</h2>
<p>Everyone has worked for an organization that attempted to define their identity through poster campaigns, entrance halls decorated with inspirational quotes or word clouds, and mugs adorned with the “five pillars of our culture”.</p>
<p>I worked for an organization that launched the RON campaign. Do it “right”, do it “once”, do it “now”. Of course, none of these happened, as the slogans bore no relation to the reality of the work.</p>
<p>In too many cases, there’s a systematic gap between the organization’s aspirational, publicly stated culture and employees’ lived experience. In fact, when an organization is dysfunctional, the list of values becomes an admission of exactly what is missing: “We value trust, respect, and work-life balance” means you’ll be micromanaged, treated like a child, and made to work long hours.</p>
<p>Meanwhile, the real culture is built from tiny interactions that reinforce what leaders value and what they don’t.</p>
<p>Every tiny interaction reinforces the real culture. The way your boss talks to you, whether they listen to your opinions, and how they respond when you flag a potential problem all set the scene for culture. Even when interactions aren’t aggressive, they can still stifle the crucial component of a healthy culture: information flow.</p>
<p>Here’s an example. You go to your boss and say, “Our test automation is missing a few really crucial areas that could cause us a problem in the future.” Your boss responds by tasking you with filling that gap. While you’re pleased to have permission to fix this, you now have a bunch of more work to juggle, and maybe it would have been better not to say anything.</p>
<p>As soon as people hesitate to share potential problems, you switch from fixing things before they go wrong to firefighting afterward.</p>
<h2 id="theoretical-foundations">Theoretical foundations</h2>
<p>High-performing software teams understand the old models of organizational sociology. They understand Douglas McGregor’s principles of human motivation (from <em>The Human Side of Enterprise</em>, though more often encountered in Dan Pink’s book, <em>Drive</em>), and they understand Ron Westrum’s <em>Typology of Organizational Culture</em>.</p>
<p>You can use these models to shift focus to the structural mechanics of workplace culture, and retire the posters, mugs, and mousepads.</p>
<p>McGregor challenged the view of traditional management, which uses rewards and punishment as the tools of motivation. Instead, McGregor asserted that individuals want to do good work and are more satisfied when they deliver quality outcomes, which means they don’t need to be cajoled by management.</p>
<p>The perfect complement to McGregor’s views on motivation is the three categories of culture shared by Ron Westrum:</p>
<ul>
<li><strong>Pathological</strong>: A culture with high anxiety, information hoarding, and strict power hierarchies. Messengers are punished or otherwise discouraged from sharing uncomfortable truths.</li>
<li><strong>Bureaucratic</strong>: These organizations have rigid channels, compartmentalization, and protocols, so information moves slowly along inflexible routes.</li>
<li><strong>Generative</strong>: The ideal culture has optimal information flow and cross-functional collaboration. Everyone is encouraged to share risks early, without fear of repercussions.</li>
</ul>
<p>Information flow is the signal of organizational health, with generative environments actively protecting “messengers”; people who flag potential risks and issues. This helps identify problems early and resolve them before they cause damage. Conversely, pathological leaders destroy information flow through intimidation, like an executive engaging in silent staring contests when they are told a date can’t be met unless scope is changed. This coercive dynamic works its way down the hierarchy, becoming the default mode of operation for managers and supervisors.</p>
<p>Suppressing information flow doesn’t change reality. Dates are missed, software ships with known faults, and the fundamentals of physics still apply. The only difference is the realization comes far later, often when it’s too late to prevent damage.</p>
<p>Bad news is crucial operational data, not the result of difficult or negative employees. A leader’s response to this information should be: “Thank you for bringing this up, because sharing this information will help us stop this from becoming a major mistake.”</p>
<h2 id="fixed-scope-and-arbitrary-deadlines">Fixed scope and arbitrary deadlines</h2>
<p>There’s a classic iron triangle in project management that says quality is constrained by three variables: scope, time, and cost, and you can pick any two. This idea presents a neat view of a classic problem in software engineering: the notion that fixing time, cost, and scope doesn’t work.</p>
<p>The reality is a little different from this picture, which leads many project managers to believe you can trade quality for reduced time and cost. The failure demand that results increases future costs and time, so it doesn’t work as a medium- or long-term strategy. Many teams have fallen into the trap of rushing to meet an arbitrary deadline, resulting in a flood of bugs and rework, which increases pressure on the next set of tasks. This cycle is destructive if repeated.</p>
<p>The mismanagement of software tasks, particularly the use of project management approaches designed to cajole teams into unreasonable delivery expectations, inevitably leads to reduced information flow. Project managers share the plan is at risk and are ordered to “make it happen”; developers flag that some work items won’t be completed on time and are told: “well, they <em>have</em> to be.” Eventually, nobody shares these risks, but they happen all the same.</p>
<p>Traditional project management techniques were created to surface the reality of a project, but were used in attempts to force impossible realities. Instead of being cartographers, diligently mapping the territory they explored, project managers believed they could draw the map first, then change millions of hectares of land to match what they imagined.</p>
<h2 id="feedback-is-oxygen">Feedback is oxygen</h2>
<p>Our founder, Paul Stovell, said this to me: “Feedback is oxygen.” That’s true when you’re developing software and need to know whether what you’re building helps users achieve their goals. It’s true throughout the process, whether that’s feedback from linters, tests, security scans, or employees who have spotted trouble ahead.</p>
<p>While it’s true that a healthy generative culture is better for workers, it’s also better for the organization. When information flows, everyone gets better results. Even your CFO.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Controlled Delivery: A Strong Opinion</title>
      <link href="https://octopus.com/blog/controlled-delivery" />
      <id>https://octopus.com/blog/controlled-delivery</id>
      <published>2026-10-19</published>
      <updated>2026-10-19</updated>
      <summary>What is Controlled Delivery, and why is it distinct from Continuous Delivery?</summary>
      <author>
        <name>Heidi Waterhouse, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Sometimes it feels like there are as many modifiers on “Delivery” as there are on a fancy coffee order, and why do they all have to start with C?</p>
<p>I’m here to add another to the list.</p>
<h2 id="what-is-controlled-delivery">What is Controlled Delivery?</h2>
<p>Controlled Delivery is the set of practices that ensure you’re delivering valuable changes to software behavior, not just detonating a change across an entire ecosystem. The goal of controlled delivery is to get the value of a change to the person who benefits from it. This is not about the change itself, because sometimes a change is not the point. Thinking of change as value is a mistake we all make, because that’s what we’re evaluated on, but it’s not what matters to the user.</p>
<p>Here are the things that we think make up Controlled Delivery</p>
<h3 id="composable-content">Composable content</h3>
<p>Software isn’t a one-way flow from the maker to the user, it’s a collaborative work object, and it’s only a small part of the user’s day. As software makers, we need to give users the option to choose the parts that add value to their day and leave aside the parts that they neither need nor want. That means that our concepts of modularity and microservices can’t stay locked inside our own architecture, they have to be obvious and manipulable to the user. That’s why APIs, and now MCPs, are such a powerful concept.</p>
<p>From a sales and adoption perspective, of course it would be great if people used all our features all the time, but that’s not very realistic. I’d bet that you’re using less than 20% of the available features in your daily software, and still getting good value from it. And probably, you’d have an easier time using it if there were fewer options available.</p>
<p>Composable content is what <a href="https://bsky.app/profile/jordo.bsky.social">Jordan Mallory</a> is asking for in this 2020 post.</p>
<figure>
<p><img src="/blog/i/x/controlled-delivery/shorter-games-jordan-mallory.png" alt="Screenshot of a tweet by Jordan Mallory saying &#x27;i want shorter games with worse graphics made by people paid more to work less and i&#x27;m not kidding&#x27;" loading="lazy" srcset="/blog/i/600/controlled-delivery/shorter-games-jordan-mallory.webp 512w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="512" height="137"></p>
</figure>
<p>Big video games require a lot of expensive hardware to run them, and dedicated time to play them. Stardew Valley was a hugely successful game with objectively rudimentary graphics and skills requirements, exactly because anyone could get rewarding play out of it, even on an old cell phone.</p>
<p>If you’re reading articles about CI/CD, you’re probably not One Guy (™) working at a table in the corner of your bedroom, but the point remains - the value is not in what you deliver, but in what the user can do.</p>
<h3 id="mindful-rollout">Mindful rollout</h3>
<p>Change, especially rapid change, is hard for people to deal with. Alvin Toffler wrote <em>Future Shock</em> in 1965-1970, and it was about how much people were struggling to adapt to the rate of change they were facing. That problem is not, I’m sure you’ve noticed, going away.</p>
<p>That means that whenever we deliver software to our users, we’re asking them to accept change, which they don’t like. We owe it to them to make sure it’s a change that benefits them, and that it’s a good delivery.</p>
<p>We have a lot of very good evidence that deploying rapidly/continuously makes software development safer and more robust. Let’s keep doing that. But as a user, I would much rather have all those changes packaged up into a coherent and periodic bundle, so I only have to get my process disrupted once in a while.</p>
<p>How do we make sure what we’re sending out is good? We want our product to be</p>
<ul>
<li>Safe</li>
<li>Compliant</li>
<li>Robust</li>
<li>Valuable</li>
</ul>
<p>It needs to not create new problems, it needs to fit in with the existing software, it needs to operate at the scales it will face, and it needs to be a net addition to their workflow.</p>
<p>We use feature flags in a variety of patterns to accomplish these goals.</p>
<ul>
<li>To make it safe, we do staged rollout or ring deployment to test on subsets of users before we push it to everyone. We also include an andon cord in case a deployment should be stopped and rolled forward.</li>
<li>To make it compliant, we make sure our deployment tests include legacy users and people on old hardware, as well as early adopters</li>
<li>To make it robust, we run shadow versions in parallel with the visible versions and blue/green overlaps</li>
<li>To make it valuable, we collect feedback on who uses new features, and communicate directly with them on how it works and what they need.</li>
</ul>
<p>Many of the tools of mindful rollout already exist and are being used for load evaluation, user entitlements, or A/B testing. What makes it truly mindful rollout is that the people doing the deployment are actively monitoring and watching the changes - not just to preserve operational stability, but to understand how and why people are using the changes.</p>
<h3 id="verifiable-receipt">Verifiable receipt</h3>
<p>Everything that is deployed goes through the same deterministic steps, and is recorded and hashed. That means that every change has a signature that can be traced and affirmed. That signature carries through from the initial developer change to the release on the user’s machine. No surprises, no flex. Controlled Delivery has to include knowing what actually arrived, as well as what we intended to arrive.</p>
<h2 id="what-isnt-controlled-delivery">What isn’t Controlled Delivery?</h2>
<p>Controlled Delivery isn’t a compliance system in itself, although it helps you do compliance.
Controlled Delivery is not an unwatched automation.
Controlled Delivery is not an excuse to deploy all the time.</p>
<p>Controlled Delivery isn’t about the software creator’s experience, it’s about the software user’s experience.</p>
<h2 id="how-does-that-fit-into-cicd">How does that fit into CI/CD?</h2>
<p>I know a bunch of you just flinched at the thought of having to do yet another process in your SDLC. Especially one that doesn’t have a track record of success or a cool conference talk (yet). But this is actually built out of parts you probably already have, just used in a different way. Ring deployment, trunk-based development, feature flags, signed changes, those are all mature technologies with over a decade of best practices behind them. When we say Controlled Delivery, we’re inviting you to combine them in a way that works for both you and your users.</p>
<h2 id="whats-my-action-item">What’s my action item?</h2>
<p>First, decide if Controlled Delivery will help you deliver your software better and more effectively. If it doesn’t meet your goals, it’s not a good tool, no matter how nifty it seems.</p>
<p>Then survey your SDLC and see how many of these patterns you already have in place. What can you chain together to make a continuous, predictable, sensible flow from code change to user value? What runbooks, approval gates, automations, and release rituals already exist, and could they be incorporated into Controlled Delivery?</p>
<p>Finally, build or buy the parts you’re missing. I can’t tell you how that tradeoff will break down. Things you build are perfectly tuned for your current processes and people, but you’ll have to maintain and update them internally forever. Things you buy will require integration and adaptation, but will be maintained by the vendor.</p>
<p>Controlled Delivery is part of the seamless software motion we’ve all been working toward, and I hope you see the foundations in your own work.</p>]]></content>
    </entry>
    <entry>
      <title>From copy-paste to platform thinking</title>
      <link href="https://octopus.com/blog/from-copy-paste-to-platform-thinking" />
      <id>https://octopus.com/blog/from-copy-paste-to-platform-thinking</id>
      <published>2026-10-15</published>
      <updated>2026-10-15</updated>
      <summary>Copy-paste pipelines, slow deployments, and contested staging environments. None of it was a crisis, but together it made the case for modernizing how we deliver our microsites.</summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>At Octopus Deploy, we maintain a collection of static websites we call microsites. They host public-facing content like our product documentation, our external handbook, and the blog you’re reading this article on. Contributors come from across the organization, including support, engineering, product, marketing, and our people team.</p>
<p>Each microsite is built on the Astro framework and lives in its own GitHub repository. For years, they were built, tested, and deployed using GitHub Actions and Octopus Deploy. The setup worked, but over time, the gap between “it works” and “it’s working well” kept growing, especially when we stepped back to consider what great software delivery looks like today.</p>
<p>We continuously release functionality in Octopus to help teams improve software delivery, and we wanted to bring those improvements to our microsites. Eating your own dog food and all that! We also knew the build had friction, and there were parts of GitHub Actions we wanted to improve. What started as “let’s enable ephemeral environments for the microsites” became a series of adjustments to our deployment pipeline.</p>
<p>They were all for the better, but along the way, we hit paper cuts, had fascinating discussions, and faced decisions that seemed simple on the surface yet quickly made us think differently on topics like where the platform team’s responsibility ends and the application team’s begins, how opinionated a shared process should be, and how much flexibility is too much.</p>
<p>This article anchors a series of posts that go deeper into specific pipeline improvements. Here, I want to set the scene, while the pieces that follow get into the technical detail, the decisions we made, and the rough edges we hit along the way.</p>
<p>Crucially, while I’ll be using our microsites to tell the story, the patterns and considerations apply well beyond static sites, and they surfaced when I worked with customers building and delivering software. If you’ve ever tried to standardize processes across multiple teams, manage shared delivery infrastructure, or define the right lines of ownership, this will likely feel familiar.</p>
<h2 id="the-way-things-were">The way things were</h2>
<p>None of what I’m about to describe was a crisis. The microsites were online, content was being published, builds and releases were functioning, and things were fine on the surface. But fine has a way of masking friction, and friction has a way of compounding in the background until someone decides to act. What started with “let’s enable ephemeral environments on the microsites” led us to look more broadly at how we build and deploy changes, and that surfaced several areas we felt could be improved.</p>
<p>I want to give you a peek behind the curtain at what things looked like before we started. I’m not going to cover every paper cut and rough edge here, as those get their own dedicated articles. But I want you to see that, on their own, none of these things were really that big of a deal. It was only when we stepped back and looked at the delivery cycle holistically that the case for change became clear.</p>
<h3 id="the-copy-paste-loop">The copy-paste loop</h3>
<p>Each microsite lived in its own GitHub repository, and each repository had its own set of GitHub Actions workflows to build, test, and validate changes. We had four core GitHub Actions workflows, which at a high level achieved the following tasks:</p>
<ol>
<li>Spellcheck (branch)</li>
<li>Markdown linting (branch)</li>
<li>Build, test, package, push the package to Octopus, and trigger the release (branch)</li>
<li>Build, test, package, push the package to Octopus, and trigger the release (main)</li>
</ol>
<p>Four workflows per repo across five repos meant 20 workflow files in total. Any future microsites would only make it worse, as another repo meant another set of workflow copies to maintain. And this was just for the CI side. The idea behind this was that if a change was made to one of these workflows, it would be abstract enough to be easily copied and pasted into the same workflow file in the other microsite repositories. This pattern will be familiar to many software teams. A build process works well for a particular technology stack, so when the next application comes along using the same stack, an engineer copies the working example as a starting point. It’s pragmatic, it’s fast, and on day one, it makes complete sense.</p>
<p>The problem with copy-paste, even if you’re copying from a template file, is that it’s a snapshot from a point in time, not an ongoing linked relationship. The moment you paste something into a second place, you have two things to maintain instead of one. Over time, changes are made in one repo but not in the others, sometimes intentionally, sometimes not. A node version gets bumped in one workflow and forgotten in another. Someone tweaks a step while testing something and doesn’t propagate it across. Another engineer just isn’t aware of all the different applications using the same workflow, and before long, five repos that were supposed to be identical have drifted apart, and nobody has a clear picture of which one is the source of truth.</p>
<p>The same pattern played out for deployments. Each microsite had its own project and deployment process, defined in Octopus Configuration Language (OCL) and stored in the repository. This is great, as the deployment process is stored in code alongside the application, and benefits from all the great things we’ve defined as an industry by putting configuration in version control. For the microsites, the deployment process was identical across all five sites. Or at least it was supposed to be. Making a change meant updating one project, verifying it worked, and then manually propagating the change to the other four projects. In practice, this meant five pull requests, five reviews, and five merges.</p>
<p>Even though we designed variables and the deployment process to be project-agnostic, the overhead of something as small as changing the format of a Slack notification message is hard to justify and only gets worse at scale. When the overhead of making a change is too high, people stop making them. Over time, that means fewer improvements, and fewer improvements mean everything starts to feel slow and heavy. The pipeline becomes something people work around rather than something they invest in.</p>
<p>The deeper problem is the drift. Copy-paste gives you the illusion of consistency without the guarantee of it. It is very easy for a human to forget to update one of the files or test a change against one of the sites, and then completely forget to update the other sites. From a Platform Engineering and compliance perspective, saying “this is how we build and deploy” with confidence when operating this way is impossible.</p>
<h3 id="deployment-times-that-test-our-patience">Deployment times that test our patience</h3>
<p>Deploying a microsite should be straightforward. At the end of the day, it is a static website; it’s not as complex as much of today’s software development and architecture. Build the site, push the files to the destination, clear the cache if needed, and send a notification. In practice, a full deployment of one of our larger microsites, like the blog or docs, would take more than 40 minutes.</p>
<p>The culprit was the way files were transferred to S3. The step responsible for the upload compares each file in the source against the existing files in the destination before deciding whether to copy it. For a site with thousands of images, each generating multiple derivative files during the build, the comparison loop took too long.</p>
<p>Alternatives were explored briefly. SFTP was considered, and questions were raised about whether the upload step could be improved or replaced with a more efficient alternative. While the answer to that was almost certainly “yes,” the deployment was working; it wasn’t a priority to dig into further, and the impression at the time was “that’s how S3 works.” When something is functioning well enough, and the path to a better solution isn’t obvious, a targeted workaround tends to win.</p>
<p>That workaround was a new function in the build - allow me to introduce <code>pnpm thin</code>. Rather than deploying the full site on every run, <code>pnpm thin</code> stripped out any image assets that hadn’t been modified in the last 14 days. Running pnpm thin was added to the build process (well, all 5 copies of the build processes!), and fewer files meant fewer comparisons, which brought deployment times down to around 10 minutes.</p>
<p>An issue with <code>pnpm thin</code> was the assumption it came with. It assumed the destination already contained all the images stripped during the build. For an incremental deployment to a long-running production environment, that assumption was fine. For a fresh deployment to a brand new environment, like an ephemeral environment spun up for a pull request, it wasn’t. Most of the site’s images would simply be missing.</p>
<p>It’s a good example of how workarounds can quietly constrain future decisions. The <code>pnpm thin</code> function solved the immediate problem, but it made a full, clean deployment effectively impossible without first understanding why half the images weren’t showing up.</p>
<h3 id="the-fading-static-staging-environment">The fading static staging environment</h3>
<p>Each microsite had two static environments that Octopus deployed to - staging and production. Early on, staging was a genuine part of the process, proposed changes were deployed to the staging environment, and people relied on it. But over time, use faded, and the reasons why are worth understanding because they’re not unique to our microsites.</p>
<p>Two things eroded it. The first was contention. When someone needed to do more thorough testing, they’d ask others not to overwrite the staging environment while they worked. I’m sure you’ve seen similar messages bounce around your Slack or Teams channels:</p>
<blockquote>
<p>Please don’t use the staging environment today, I’m testing out some changes I’ve been working on. Thanks heaps!</p>
</blockquote>
<p>It seems like a reasonable request, but realistically, it meant everyone else had to find another way to check their changes. Local previews filled that gap, and once people found a faster workaround that didn’t require coordinating with anyone, they stuck with it. Interestingly, relying on running the sites locally introduces other pain points, which I’ll touch on below when discussing how to preview changes.</p>
<p>This also meant that, as someone who wanted to deploy your change to the staging environment, you felt like you had to ask around first to see if you were about to interrupt someone else. Messages like “Anyone in the blog staging environment at the moment?” weren’t unusual to see flying around Slack, though admittedly, those were more pleasant than seeing the “Who just overwrote my deployment in the blog staging environment!?” messages! Add in a globally distributed team and the friction that is time zones to the equation, and you’re in for a world of hurt.</p>
<p>The second was the feedback loop. Build times already stretched to around 10 minutes. A deployment to staging added more to that. When you can verify something locally in a few seconds, waiting 20 or more minutes for a staging deployment is hard to justify, especially for a content change where the diff is right there in the GitHub UI.</p>
<p>Delayed feedback loops are the conditions in which workarounds grow. The longer you wait for a build, a deployment, or a shared environment to free up, the more attractive any shortcut becomes. It’s a rational response to friction, and once the shortcut becomes the default, the original agreed and documented process falls out of the team’s workflow.</p>
<p>By the time we looked at this properly, most contributors were testing locally if they checked at all, and reviewers were reading diffs in GitHub’s portal rather than seeing changes live. The first time many changes were seen in a real hosted environment was in production.</p>
<h3 id="the-effort-to-preview-changes">The effort to preview changes</h3>
<p>The staging situation pointed to a broader gap. Unless the static staging environment was used, there was no easy way for a contributor to share a live, hosted version of their change with someone else. No way for a reviewer to click a link and see the proposed change running in a real environment. To do it in staging meant:</p>
<ol>
<li>Knowing you were all clear to use the staging environment without annoying someone else who may be testing/reviewing their change</li>
<li>Knowing which Octopus Deploy instance and project to go to</li>
<li>Having access to the target project in Octopus Deploy</li>
<li>Knowing how to perform a deployment to the staging environment</li>
<li>Knowing the URL for the staging environment to share with others</li>
</ol>
<p>Technical contributors would often just run the site locally on their machine. Clone the repo, install dependencies, run <code>pnpm dev</code>, and open localhost. It’s not onerous if you know what you’re doing. But many people contributing to the microsites aren’t developers. For them, the local preview instructions in the readme present a barrier, not a solution.</p>
<p>For reviewers, the situation isn’t much better. To see a proposed change live, you’d need to check out the branch, follow the same local setup instructions, and run the site yourself. In practice, most reviewers looked at the diff in GitHub and called it done. That works for typo fixes or, honestly, even new documentation pages or blog posts. However, it’s less reassuring for layout changes, new components, or anything where seeing it rendered matters.</p>
<p>What we wanted was simple. Open a pull request, get a URL, and see your change in a real hosted environment. Ideally, one that mirrored the production infrastructure, allowing us to test changes in a production-like environment. While static website hosting is straightforward, there are nuances with different hosting options. We wanted to provide this to contributors without them needing to know anything about the underlying infrastructure, without needing access to Octopus, without needing to run anything locally. That experience didn’t exist yet, and building it was what set this whole project in motion, which admittedly was a bit of eat your own dog food when we <a href="https://octopus.com/blog/introducing-ephemeral-environments">introduced ephemeral environments</a> in Octopus.</p>
<h3 id="organically-growing-infrastructure">Organically growing infrastructure</h3>
<p>The microsites were provisioned and were running in AWS, using S3 as the endpoint and CloudFront distributions in front of them. It worked, but they were initially provisioned and configured manually and had been left largely untouched since.</p>
<p>Without Infrastructure as Code (IaC), there was no reliable way to know whether the configuration of each bucket and distribution was consistent across all sites. If a change was required, it had to be made manually, one resource at a time. If something ever needed to be recreated from scratch, like deploying to a new AWS region or provisioning a fresh environment for a pull request, there was no automated way to achieve it.</p>
<p>We knew we wanted to move to Azure, where most of Octopus’s infrastructure lives, and that the migration was an opportunity to put the infrastructure management in a better shape. Automating the provisioning and deprovisioning of infrastructure was something we knew we had to get right, particularly if we were going to support ephemeral environments and speed up deployments.</p>
<h3 id="what-we-changed">What we changed</h3>
<p>The pain points above are just the surface. Each one has a dedicated article that goes into the details, the decisions, the trade-offs, and the edges we hit along the way. If you’ve felt any of this in your own work, the deeper articles are worth your time.</p>
<div class="hint"><p>New articles in this series are being published over the coming weeks. As each one goes live, the topics below will link through to it.</p></div>
<p>The copy-paste problem across both GitHub Actions and Octopus was solved by leaning into templating. Twenty workflow files spread across five repositories were <a href="/blog/dry-isnt-just-for-app-code">consolidated into a set of reusable GitHub workflows</a>. In Octopus, five individual deployment processes were consolidated into a single versioned process template that all five microsites now consume, along with templated runbooks for provisioning and deprovisioning infrastructure. Both solved the copy-paste problem, but if you read the reusable GitHub workflows article, you might come away thinking Octopus has set a pretty high bar for what templating should look like.</p>
<p>Slow deployments and the pnpm thin workaround that papered over them were addressed during the migration to Azure. We evaluated several hosting options before landing on Blob Storage, swapped out the file transfer tooling for AzCopy, and in doing so, made pnpm thin unnecessary. The full story is in finding the right hosting for a static site.</p>
<p>Ephemeral environments were the catalyst for this project, and while Octopus does much of the heavy lifting, it requires a mature foundation underneath. Every pull request now gets its own hosted environment, with a preview URL posted as a comment on the PR. Along the way, we hit some interesting edges, including managing Terraform state for short-lived environments and a subtle race condition with short-lived PRs that took a pragmatic workaround to resolve.</p>
<p>Two of the more interesting conversations in the project weren’t really about tooling at all. Where ownership lines fall between a platform team and the teams consuming what they build is something many practitioners wrestle with, and it came up repeatedly here. And rather than installing tools at deployment runtime - something I’ve seen a lot of teams do in the field - we took an approach of keeping workers lean with execution containers, which made deployments faster and more predictable.</p>
<p>Finally, not every pull request needs a full environment spun up. Based on some conversations I had at conferences and internal feedback, we made the whole thing optional using a simple GitHub PR label, without compromising branch protection rules.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>None of the pain points in this article were dramatic on their own. A few copied workflow files, a slow deployment, a staging environment people stopped using. But together, they made it harder to improve, harder to trust what we were shipping, and harder for contributors to see their changes before they went live.</p>
<p>It’s also worth thinking about what happens as these patterns scale. Five microsites is a small footprint, but the same copy-paste approach across dozens or hundreds of applications makes it close to impossible to answer simple questions like “are all our deployments following the same process?” or “has everyone picked up the latest security change?” That’s where platform thinking earns its keep. Shared templates, consistent infrastructure, and guardrails built into the process mean compliance and risk are handled by default, rather than relying on every team to remember to do the right thing based on a knowledge base article people rarely revisit.</p>
<p>For us, the realization was that the tooling we rely on had moved on, and our pipelines hadn’t kept up. Taking the time to step back, look at the whole delivery cycle, and bring it in line with what modern software delivery looks like today has been worth it. If some of this sounded familiar, it might be worth taking that same step back with your own pipelines.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Introducing remote MCP for Octopus Deploy</title>
      <link href="https://octopus.com/blog/introducing-remote-mcp" />
      <id>https://octopus.com/blog/introducing-remote-mcp</id>
      <published>2026-10-13</published>
      <updated>2026-10-13</updated>
      <summary>Octopus now includes a built-in remote MCP server. There's nothing to install or maintain: point your agent at your instance's /mcp endpoint.</summary>
      <author>
        <name>Gin Atkins, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Until now, connecting an agent to Octopus meant running our local Model Context Protocol (MCP) server on your machine. You had to install it on every machine, for every user, and keep every installation up to date.</p>
<p>There’s an easier option now. Octopus includes a built-in remote MCP server, so there’s nothing to install or maintain. Point your agent at your instance’s <code>/mcp</code> endpoint, and it can talk to Octopus over HTTP.</p>
<h2 id="what-can-the-remote-mcp-do">What can the remote MCP do?</h2>
<p>The remote MCP initially supports a selected set of core Octopus features, and we’ll add more over time. <a href="https://srfjzffylk.zite.so">Share your feedback</a> to help us decide what comes next.</p>
<p>Depending on the authentication mechanism and the permissions you grant, the remote MCP can:</p>
<ul>
<li>
<p><strong>Navigate and orient:</strong> Find and provide details on spaces, projects, tenants, and tenant tags.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">List the projects in the Payments space and the tenants each one deploys to.</span></span></code></pre>
</li>
<li>
<p><strong>Answer deployment questions:</strong> Show a pull request’s deployment status, including when it reached a tenant or environment, and describe the lifecycle phases and progression rules behind it.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">When did PR #4821 reach the Brisbane Bank tenant, and which environment is it in now?</span></span></code></pre>
</li>
<li>
<p><strong>Deploy and operate:</strong> Create and deploy a release, promote a release through its lifecycle, and run runbooks. Then generate and attach release notes to a release.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Create a release of Payments Service, deploy it to production, then draft and attach release notes.</span></span></code></pre>
</li>
<li>
<p><strong>Investigate and diagnose:</strong> Search task logs, deployment processes, and variables to work out why something failed. List or filter running, queued, and failed tasks across the instance.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">The last deployment of Payments Service to production failed. What was the error, and which step failed?</span></span></code></pre>
</li>
<li>
<p><strong>Audit and report:</strong> Query audit events, report on deployment health, and triage incidents by tracing what was deployed to which targets and when.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">What changed in the Payments space in the last hour?</span></span></code></pre>
</li>
<li>
<p><strong>Check tenant configuration:</strong> Find variables with incomplete configuration across tenants.</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="markdown"><code><span class="line"><span style="color:#000000">Which tenants have missing variable values for the Payments project?</span></span></code></pre>
</li>
</ul>
<h2 id="get-started">Get started</h2>
<p>Octopus Cloud customers can use the remote MCP now. Point your agent at your instance’s <code>/mcp</code> endpoint to try it.</p>
<p>Octopus Server customers will get it in the upcoming 2026.3 release.</p>
<p>Read the <a href="https://octopus.com/docs/octopus-ai/mcp/remote">remote MCP docs</a> to learn how to connect your agent and set up authentication.</p>
<h2 id="whats-next">What’s next</h2>
<p>We know that for many teams, easily connecting an agent to Octopus is only the first step. The next is governance.</p>
<p>The remote MCP is one part of our broader vision for agent governance. In the coming months, we’re launching:</p>
<ul>
<li><strong>Scoped API keys:</strong> Tighter control over what an agent can reach</li>
<li><strong>OAuth for the remote MCP:</strong> A cleaner, more secure connection flow</li>
<li><strong>Agent-aware policies:</strong> Visibility into whether an agent initiated a deployment or runbook run, to inform Platform Hub policies</li>
<li><strong>Improved audit log:</strong> Easier filtering and viewing of audit events related to agent activity</li>
</ul>
<p>Thank you to the 19 users who voted for and commented on remote MCP in our <a href="https://octopusdeploy.productboard.com/detail/MTpQbUVudGl0eTpjZWM2MWFiOS0yYWEzLTQwZTgtODhhOC1mOTczNjUwMWVjNWE=/portal">roadmap</a>. You helped shape this feature.</p>]]></content>
    </entry>
    <entry>
      <title>Salesforce Deployments with Octopus</title>
      <link href="https://octopus.com/blog/salesforce-deployments" />
      <id>https://octopus.com/blog/salesforce-deployments</id>
      <published>2026-10-10</published>
      <updated>2026-10-10</updated>
      <summary>Learn how to deploy Salesforce applications using Octopus Deploy</summary>
      <author>
        <name>Matthew Casperson, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p><a href="https://developer.salesforce.com/developer-centers/developer-experience">Salesforce DX</a> (Developer Experience) is a set of tools and features that bring modern, source-driven development and DevOps practices to the Salesforce platform. It allows teams to build Salesforce applications using Git-based workflows and deploy them via CI/CD pipelines.</p>
<p>Salesforce DX provides a command-line interface (CLI) to automate application deployments, and Octopus Deploy can integrate with it to deliver a seamless deployment experience.</p>
<p>In this post, you’ll learn how to deploy Salesforce applications using Octopus Deploy and the Salesforce CLI.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Download the sample application from <a href="https://github.com/OctopusSolutionsEngineering/SalesforceSampleApp">this GitHub repository</a>. The <a href="https://github.com/OctopusSolutionsEngineering/SalesforceSampleApp/releases">releases</a> provide Zip files that can be uploaded to the Octopus built-in feed:</p>
<p><img src="/blog/i/x/salesforce-deployments/built-in-feed.png" alt="Built-in feed" loading="lazy" srcset="/blog/i/600/salesforce-deployments/built-in-feed.webp 600w, /blog/i/1000/salesforce-deployments/built-in-feed.webp 1000w, /blog/i/2000/salesforce-deployments/built-in-feed.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3758" height="1930"></p>
<p>You must install the Salesforce CLI locally by following the <a href="https://developer.salesforce.com/tools/salesforcecli">Salesforce CLI installation instructions</a>.</p>
<p>You can create a free account on the <a href="https://developer.salesforce.com/">Salesforce Developer site</a>. This post will use a Salesforce Developer Edition account to deploy the sample application.</p>
<h2 id="log-in-locally">Log in locally</h2>
<p>Log in to your Salesforce Developer Edition account using the Salesforce CLI:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> login</span><span style="color:#A31515"> web</span><span style="color:#0000FF"> --set-default</span><span style="color:#0000FF"> --alias</span><span style="color:#A31515"> Dev</span></span></code></pre>
<p>The authentication URL, which is used as a secret to log in to Salesforce, is shown with this command:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> auth</span><span style="color:#A31515"> show-sfdx-auth-url</span></span></code></pre>
<p>The <code>SFDX Auth URL</code> is then displayed in the format <code>force://PlatformCLI::&#x3C;secret>@&#x3C;hostname>.develop.my.salesforce.com</code>. This URL is used to authenticate the Salesforce CLI with your Salesforce organization. Take note of this URL, as it must be saved as a secret in Octopus Deploy.</p>
<div class="hint"><p>You can use the <a href="https://developer.salesforce.com/docs/platform/sfdx-dev/guide/sfdx-dev-auth-jwt-flow.html">JWT Flow</a> to authenticate the Salesforce CLI without requiring a web-based login. This allows deployments to be automated without user interaction. However, this post will use the web-based login for simplicity.</p></div>
<h2 id="deployment-process">Deployment process</h2>
<p>Deploying a Salesforce application is done via a script step that runs the Salesforce CLI commands.</p>
<p>To automate Salesforce login, the <code>SFDX Auth URL</code> is stored as a secret variable named <code>Salesforce.Auth</code>.</p>
<p>Then add a script step to the deployment process that runs the following commands.</p>
<p>You’ll make use of an Execution Container to expose the Salesforce CLI to the script step. In the <code>Execution Container Image</code> section, select the option <code>Runs inside a container on a worker</code>, and then the <code>Build container from an inline Dockerfile</code> option. Paste the following Dockerfile into the text box:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="dockerfile"><code><span class="line"><span style="color:#0000FF">FROM</span><span style="color:#000000"> node:22-bookworm-slim</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0000FF">RUN</span><span style="color:#000000"> apt-get update &#x26;&#x26; apt-get install -y --no-install-recommends \</span></span>
<span class="line"><span style="color:#000000">    ca-certificates \</span></span>
<span class="line"><span style="color:#000000">    curl \</span></span>
<span class="line"><span style="color:#000000">    gnupg \</span></span>
<span class="line"><span style="color:#000000">    libicu-dev \</span></span>
<span class="line"><span style="color:#000000">    &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>
<span class="line"></span>
<span class="line"><span style="color:#0000FF">RUN</span><span style="color:#000000"> npm install -g @salesforce/cli</span></span></code></pre>
<p>Paste the following script as an inline script in the script step:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#008000"># Stop the CLI from losing its auth storage context</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_USE_GENERIC_UNIX_KEYCHAIN</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Deactivate the CLI's DNS check, which frequently fails in CI networks</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_DISABLE_DNS_CHECK</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SFDX_DISABLE_DNS_CHECK</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Keep telemetry off</span></span>
<span class="line"><span style="color:#0000FF">export</span><span style="color:#001080"> SF_DISABLE_TELEMETRY</span><span style="color:#000000">=</span><span style="color:#001080">true</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Enter the directory with the sample application</span></span>
<span class="line"><span style="color:#795E26">cd</span><span style="color:#A31515"> SampleApp</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Log into Salesforce using the auth URL stored in Octopus Deploy</span></span>
<span class="line"><span style="color:#795E26">echo</span><span style="color:#000000"> $(</span><span style="color:#795E26">get_octopusvariable</span><span style="color:#A31515"> "Salesforce.Auth"</span><span style="color:#000000">) > </span><span style="color:#A31515">./authfile.txt</span></span>
<span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> org</span><span style="color:#A31515"> login</span><span style="color:#A31515"> sfdx-url</span><span style="color:#0000FF"> --sfdx-url-file</span><span style="color:#A31515"> ./authfile.txt</span><span style="color:#0000FF"> --set-default</span><span style="color:#0000FF"> --alias</span><span style="color:#A31515"> CiOrg</span></span>
<span class="line"><span style="color:#795E26">rm</span><span style="color:#A31515"> ./authfile.txt</span></span>
<span class="line"></span>
<span class="line"><span style="color:#008000"># Perform the deployment</span></span>
<span class="line"><span style="color:#795E26">sf</span><span style="color:#A31515"> project</span><span style="color:#A31515"> deploy</span><span style="color:#A31515"> start</span><span style="color:#0000FF"> --target-org</span><span style="color:#A31515"> CiOrg</span></span></code></pre>
<p>Finally, add the sample application to the project as a referenced package:</p>
<p><img src="/blog/i/x/salesforce-deployments/referenced-package.png" alt="Referenced package" loading="lazy" srcset="/blog/i/600/salesforce-deployments/referenced-package.webp 600w, /blog/i/1000/salesforce-deployments/referenced-package.webp 1000w, /blog/i/2000/salesforce-deployments/referenced-package.webp 1434w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1434" height="1492"></p>
<h2 id="using-the-new-component">Using the new component</h2>
<p>To use the new component from the sample application, navigate to the <code>Setup</code> menu in Salesforce:</p>
<p><img src="/blog/i/x/salesforce-deployments/setup-menu.png" alt="Setup" loading="lazy" srcset="/blog/i/600/salesforce-deployments/setup-menu.webp 600w, /blog/i/1000/salesforce-deployments/setup-menu.webp 740w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="740" height="900"></p>
<p>Open the Lightning App Builder item in the left-hand menu, and click the <code>New</code> button to create a new Lightning app:</p>
<p><img src="/blog/i/x/salesforce-deployments/lightning-app-builder.png" alt="New Lightning App" loading="lazy" srcset="/blog/i/600/salesforce-deployments/lightning-app-builder.webp 600w, /blog/i/1000/salesforce-deployments/lightning-app-builder.webp 1000w, /blog/i/2000/salesforce-deployments/lightning-app-builder.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></p>
<p>Complete the wizard to create a new Lightning app:</p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-1.png" alt="New Lightning App Wizard Page 1" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-1.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-1.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-1.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2008" height="1218"></p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-2.png" alt="New Lightning App Wizard Page 2" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-2.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-2.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-2.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2036" height="1214"></p>
<p><img src="/blog/i/x/salesforce-deployments/new-lightning-page-3.png" alt="New Lightning App Wizard Page 3" loading="lazy" srcset="/blog/i/600/salesforce-deployments/new-lightning-page-3.webp 600w, /blog/i/1000/salesforce-deployments/new-lightning-page-3.webp 1000w, /blog/i/2000/salesforce-deployments/new-lightning-page-3.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2014" height="1194"></p>
<p>Select the new component and drag it onto the page:</p>
<p><img src="/blog/i/x/salesforce-deployments/sample-app.png" alt="Gui Builder" loading="lazy" srcset="/blog/i/600/salesforce-deployments/sample-app.webp 600w, /blog/i/1000/salesforce-deployments/sample-app.webp 1000w, /blog/i/2000/salesforce-deployments/sample-app.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="3840" height="1924"></p>
<h2 id="going-further">Going further</h2>
<p>This <a href="https://developer.salesforce.com/blogs/2020/01/using-salesforce-dx-with-github-actions">Salesforce post</a> describes a high-level deployment pipeline, with this diagram showing the breakdown between the CI and CD processes:</p>
<p><img src="/blog/i/x/salesforce-deployments/example-flow.png" alt="Example Flow" loading="lazy" srcset="/blog/i/600/salesforce-deployments/example-flow.webp 442w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="442" height="742"></p>
<p>All the steps in the CD process are implemented via the <code>sf</code> CLI and can be automated using Octopus Deploy.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Octopus provides a convenient way to deploy Salesforce applications using the Salesforce CLI. The CLI handles the interactions with the Salesforce platform, while Octopus provides a user-friendly interface for managing deployments, variables, and secrets. By combining these tools, teams can implement a robust CI/CD pipeline for Salesforce applications.</p>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.10: Modernizing legacy software</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e10" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e10</id>
      <published>2026-10-06</published>
      <updated>2026-10-06</updated>
      <summary>Find out why big-bang rewrites are the path to failure and what to do instead.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In <a href="https://octopus.com/blog/continuous-delivery-office-e9">CD Office Hours episode 9</a>, we discussed the compliance ratchet and how accelerating software development without the right practices can lead to an irreversible increase in controls. In this episode, we look at a classic software problem: the lure of the software rewrite and the inevitable hazards such endeavors bring.</p>
<p>When the cost to maintain the software and add new features becomes burdensome, a decision is often made to rewrite it rather than improve it “in place”. This is sometimes a valid approach, but more often it’s based on an exaggeration of how bad the code is and an understatement of the work involved.</p>
<p>In some cases, the qualities of the codebase that a team dislikes stem from an architectural decision they don’t understand, either because there’s no record of it or because the records aren’t put to use. In many cases, the layers of change applied to the code over time have made it messy and hard to comprehend.</p>
<p>When considering a big-bang rewrite, it is practically impossible to determine whether the rewrite is required or will provide a return on investment. That’s why big-bang rewrites have such a high failure rate and should be avoided.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p><a href="https://www.youtube.com/watch?v=1CgHK2I7k1Y">Watch Continuous Delivery Office Hours Ep.10</a></p>
<h2 id="legacy-or-heritage">Legacy or heritage</h2>
<p>The term “legacy” has become associated with old and unusable software, but more often, legacy software is the long-running backbone of an organization. It captures vast institutional knowledge and endures for so long because it works. In many cases, software inevitably becomes legacy software, as the alternative is that it fails.</p>
<p>That’s why we prefer to use the term “heritage” software, as it’s an artifact of special value to the organization. The code for heritage software has captured not just the essence of the solution, but hundreds or even thousands of edge cases and enhancements based on years of learning.</p>
<h2 id="the-hazards-of-the-rewrite">The hazards of the rewrite</h2>
<p>Most codebases have parts that are untidy or don’t make sense. There’s a tipping point that makes us want to rewrite the whole thing, but this is often the wrong reaction. It’s very rare for the cost of maintenance to exceed the cost of wholesale replacement.</p>
<p>When teams consider a big-bang rewrite, they tend to estimate the task based on the obvious use cases. That means the new software is naturally cleaner, because it doesn’t handle any of the less obvious problems. When the new software version goes live, the team begins rediscovering the many edge cases that must be handled.</p>
<p>By the time these edge cases have been incorporated into the software, it has started to look complicated once more, because the complexity came from the domain rather than a careless implementation. The team may find more graceful ways to handle edge cases, but by this time, the rewrite will have vastly exceeded all estimates of effort and expense.</p>
<p>When the rewrite is complete, the organization will effectively be “back to square one”, which means they will have fallen behind the competition. That means the anticipated cost of maintenance and new features must be substantially lower to allow the software to catch up and pass competitor offerings.</p>
<p>Essentially, wholesale rewrites should be avoided; a different approach is needed.</p>
<h2 id="architectural-and-team-strategies">Architectural and team strategies</h2>
<p>The alternative to the big-bang rewrite is to identify a specific component to replace. You might take a calculation routine and move it out of a monolith into a service, creating the missing tests and refining the routines as you extract it. The result is a new, highly maintainable component and a slightly smaller monolith that calls it rather than doing the work itself.</p>
<p>This is the strangler fig pattern, named for the plants that cheat the competition for sunlight by growing high up on a host tree, eventually sending down their own roots and often remaining after the host tree has died and rotted away. In the same way, the heritage software can continue to support users while an increasing number of components are rewritten and deleted from the monolith.</p>
<p>You can use both domain-driven design and team topologies to inform your gradual rewrite. If you can identify a seam that would carve off a meaningful part of the domain and have a team that can own the domain and the component that serves it, you can create a meaningful interface for the software and improve the organization’s communication structure.</p>
<p>Combining the strangler fig pattern with architectural and team design results in teams with deep knowledge of their specific areas and the ability to move with relative independence. The number of teams you have can be instructive when considering how far you want to break down a monolith into cohesive components.</p>
<p>Wholesale rewrites invite disaster, but working in small steps and considering organizational design as well as architectural design is the path to success.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Configuring generic OIDC provider for authentication</title>
      <link href="https://octopus.com/blog/auth0-oidc-authentication-provider" />
      <id>https://octopus.com/blog/auth0-oidc-authentication-provider</id>
      <published>2026-10-05</published>
      <updated>2026-10-05</updated>
      <summary>Learn how to configure Auth0 as a generic OpenID Connect (OIDC) authentication provider for Octopus Deploy.</summary>
      <author>
        <name>Shawn Sesna, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The use of OpenID Connect (OIDC) has gained popularity as a standard for authentication and authorization.  Octopus Deploy supports using OIDC for integrating with <a href="https://octopus.com/docs/packaging-applications/package-repositories">External Feeds</a>, <a href="https://octopus.com/docs/api/examples/accounts">Accounts</a>, and as an authentication provider.  In this post, I will cover how to configure an OIDC provider as an authentication mechanism for Octopus Deploy.  This post uses Auth0 as the OIDC provider.</p>
<h2 id="setting-up-auth0">Setting up Auth0</h2>
<p>To get started, you’ll need to configure an <code>Application</code> in Auth0.</p>
<h3 id="create-an-auth0-application">Create an Auth0 application</h3>
<p>Auth0 makes the process of creating an <code>Application</code> pretty painless:</p>
<ol>
<li>After logging in, expand the <strong>Applications</strong> category within the Auth0 navigation</li>
<li>Click on <strong>Applications</strong></li>
<li>Click <strong>Create Application</strong></li>
<li>Give the <code>Application</code> a name (eg., Octopus Deploy)</li>
<li>Choose <code>Regular Web Application</code></li>
<li>Click <strong>Create</strong></li>
</ol>
<h3 id="configure-the-auth0-application">Configure the Auth0 application</h3>
<p>Once the <code>Application</code> has been created, there are a couple of settings that need to be configured so it will work with Octopus Deploy:</p>
<ol>
<li>
<p>Navigate to the <strong>Settings</strong> tab on your newly created <code>Application</code></p>
</li>
<li>
<p>Scroll down to the <strong>Application URIs</strong> section</p>
</li>
<li>
<p>In the <strong>Allowed Callback URLs</strong>, enter the following: <code>https://&#x3C;your Octopus Deploy Server URL>/api/users/authenticatedToken/GenericOidc</code>, for example:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://shawnsesna.octopusdemos.app/api/users/authenticatedToken/GenericOidc</span></span></code></pre>
</li>
<li>
<p>(Optional) In the <strong>Allowed Logout URLs</strong> enter the following: <code>https://&#x3C;your Octopus Deploy Server URL>/app#/users/sign-out</code>, for example:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://shawnsesna.octopusdemos.app/app#/users/sign-out</span></span></code></pre>
</li>
<li>
<p>Click <strong>Save</strong></p>
</li>
</ol>
<p>Before navigating to Octopus Deploy, record the values of <code>Domain</code>, <code>Client ID</code>, and <code>Client Secret</code>.  These values are needed to configure OIDC authentication in Octopus Deploy.</p>
<h2 id="configure-openid-connect-in-octopus-deploy">Configure OpenID Connect in Octopus Deploy</h2>
<p>Configuration of OIDC authentication can be completed in just a few minutes:</p>
<ol>
<li>Navigate to <strong>Configuration</strong> (cog) in Octopus Deploy</li>
<li>Click on <strong>Settings</strong> -> <strong>OpenID Connect</strong></li>
<li>Enable the integration by ticking the <strong>Is Enabled</strong> checkbox</li>
<li>Set <strong>Username Claim Type</strong> to <code>name</code></li>
<li>Set <strong>Display Name</strong> to something meaningful (eg. <code>Auth0</code>)</li>
<li>Set <strong>Issuer</strong> to <code>https://&#x3C;Domain value from Auth0>/</code> <em>Note the trailing slash</em></li>
<li>Set <strong>Client ID</strong> to the <code>Client ID</code> value from Auth0</li>
<li>Set <strong>Client Secret</strong> to the <code>Client Secret</code> value from Auth0</li>
<li>(Optional) Tick <strong>Allow Auto User Creation</strong> to automatically create an Octopus user account when a new user logs in using OpenID Connect</li>
<li>Click <strong>Save</strong></li>
</ol>
<p>Configuration is now complete!  Users will now see the OpenID Connect login option when signing into Octopus Deploy.</p>
<figure>
<p><img src="/blog/i/x/auth0-oidc-authentication-provider/octopus-deploy-openid-connect-login.png" alt="Octopus Deploy login screen showing the OpenID Connect authentication option" srcset="/blog/i/600/auth0-oidc-authentication-provider/octopus-deploy-openid-connect-login.webp 360w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="360" height="644"></p>
</figure>
<h2 id="including-roles-from-auth0">Including roles from Auth0</h2>
<p>Unlike other providers such as Microsoft Entra or Okta, Auth0 does not include role assignments by default.  Instead, you must insert a step into the process after login, but before the token is issued.</p>
<h3 id="create-an-auth0-role">Create an Auth0 role</h3>
<p>If you don’t already have roles in Auth0, creating a role in Auth0 is a straightforward process:</p>
<ol>
<li>Expand the <code>User Management</code> section and click <code>Roles</code></li>
<li>Click the <strong>+ Create Role</strong> button</li>
<li>Give the role a <code>Name</code> and <code>Description</code></li>
<li>Click on the <strong>Users</strong> tab and add some users to the role</li>
</ol>
<h3 id="create-an-auth0-actions-trigger">Create an Auth0 Actions Trigger</h3>
<p>Auth0 provides a mechanism for inserting the role information into the OIDC token called an <code>Action</code>.  An <code>Action</code> can be added to a <code>Trigger</code> such as the <code>post-login</code>, which is executed after the user logs in, but before the token is issued.</p>
<ol>
<li>
<p>Expand the <code>Actions</code> section and click <code>Triggers</code></p>
</li>
<li>
<p>Under <strong>Sign Up &#x26; Login</strong>, choose the <code>post-login</code> trigger</p>
</li>
<li>
<p>The process will consist of two steps: <strong>Start</strong> and <strong>Complete</strong>.  Click on the <strong>+</strong> button next to <code>Add Action</code> and select <code>Create Custom Action</code></p>
</li>
<li>
<p>Give the <code>Action</code> a name (eg., Include Roles)</p>
</li>
<li>
<p>For this example, leave the <code>Runtime</code> as the recommended <code>Node 22</code> value</p>
</li>
<li>
<p>Update the code to the following</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#267F99">exports</span><span style="color:#000000">.</span><span style="color:#795E26">onExecutePostLogin</span><span style="color:#000000"> = </span><span style="color:#0000FF">async</span><span style="color:#000000"> (</span><span style="color:#001080">event</span><span style="color:#000000">, </span><span style="color:#001080">api</span><span style="color:#000000">) </span><span style="color:#0000FF">=></span><span style="color:#000000"> {</span></span>
<span class="line"><span style="color:#0000FF">  const</span><span style="color:#0070C1"> namespace</span><span style="color:#000000"> = </span><span style="color:#A31515">'https://shawnsesna.octopusdemos.app'</span><span style="color:#000000">;</span></span>
<span class="line"><span style="color:#0000FF">  const</span><span style="color:#0070C1"> roles</span><span style="color:#000000"> = </span><span style="color:#001080">event</span><span style="color:#000000">.</span><span style="color:#001080">authorization</span><span style="color:#000000">?.</span><span style="color:#001080">roles</span><span style="color:#000000"> ?? </span><span style="color:#0000FF">null</span><span style="color:#000000">;</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color:#001080">  api</span><span style="color:#000000">.</span><span style="color:#001080">idToken</span><span style="color:#000000">.</span><span style="color:#795E26">setCustomClaim</span><span style="color:#000000">(</span><span style="color:#A31515">`</span><span style="color:#0000FF">${</span><span style="color:#001080">namespace</span><span style="color:#0000FF">}</span><span style="color:#A31515">/roles`</span><span style="color:#000000">, </span><span style="color:#001080">roles</span><span style="color:#000000"> ?? []);</span></span>
<span class="line"><span style="color:#000000">};</span></span></code></pre>
<p>This code inserts a JSON array payload containing assigned roles into the issued token.  In this case, <code>${namespace}/roles</code> acts as the key with the <code>roles</code> array as the value.  The <code>namespace</code> value is important, as this is what Octopus needs to determine where to look for role assignments.  In the above example, the <code>namespace</code> value is <code>https://shawnsesna.octopusdemos.app/roles</code>.  The <code>namespace</code> can be whatever you want; it doesn’t have to be the URL to your Octopus instance.</p>
</li>
<li>
<p>Click <strong>Deploy</strong> to save the <code>Action</code> to the <code>Library</code>.</p>
</li>
<li>
<p>Navigate back to <code>Triggers</code></p>
</li>
<li>
<p>Under <strong>Sign Up &#x26; Login</strong>, choose the <code>post-login</code> trigger</p>
</li>
<li>
<p>Your newly created <code>Action</code> will now be listed; click and drag the <code>Action</code> and drop it between <strong>Start</strong> and <strong>Complete</strong></p>
</li>
<li>
<p>Click <strong>Apply</strong></p>
</li>
</ol>
<p>Auth0 is now configured to include role assignments with the OIDC token.  This will allow you to add an Auth0 role as an external role to an Octopus Team.</p>
<h3 id="update-the-openid-connect-octopus-configuration">Update the OpenID Connect Octopus configuration</h3>
<p>Now that roles are included within the token, you’ll need to update the OpenID Connect configuration in Octopus to be able to “see” them.</p>
<ol>
<li>Navigate to <strong>Configuration</strong> (cog) in Octopus Deploy</li>
<li>Click on <strong>Settings</strong> -> <strong>OpenID Connect</strong></li>
<li>Update <strong>Role Claim Type</strong> to the key from the JSON.  In the above example, this value is <code>https://shawnsesna.octopusdemos.app/roles</code></li>
<li>Click Save</li>
</ol>
<p>Octopus now knows where in the token to locate role membership.  Assigning the name of the role to an Octopus Deploy Team will grant anyone who has the role within their token access to the resources that the Team has been granted, removing the need to add individual users to the Team.</p>
<h3 id="troubleshooting-roles">Troubleshooting roles</h3>
<p>OIDC token exchange takes place server-to-server, making the ability to see what was included in the JSON Web Token (JWT) difficult.  Fortunately, there is an easy way to pass the token to jwt.io for debugging purposes.</p>
<ol>
<li>In Auth0, navigate to your <code>Application</code></li>
<li>Scroll to the <strong>Applications URIs</strong> section</li>
<li>Add <code>https://jwt.io</code> to the <strong>Allowed Callback URLs</strong></li>
<li>Open a new browser window (Incognito/InPrivate is best to ensure cache doesn’t interfere) and go to:</li>
</ol>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>https://&#x3C;Your Auth0 domain>/authorize?client_id=&#x3C;Your Client ID>&#x26;response_type=id_token&#x26;redirect_uri=https://jwt.io&#x26;scope=openid%20profile%20email&#x26;nonce=12345</span></span></code></pre>
<p>You will be prompted to log in to Auth0, then redirected to <code>https://jwt.io</code>.  Jwt.io will automatically decode the returned JWT so you can see what is included.  Below is an example:</p>
<p>Decoded payload</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="json"><code><span class="line"><span style="color:#000000">{</span></span>
<span class="line"><span style="color:#0451A5">  "https://shawnsesna.octopusdemos.app/roles"</span><span style="color:#000000">: [</span></span>
<span class="line"><span style="color:#A31515">    "Demo-Argo-CD-Apps"</span></span>
<span class="line"><span style="color:#000000">  ],</span></span>
<span class="line"><span style="color:#0451A5">  "nickname"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "name"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt@fake.com"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "picture"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://s.gravatar.com/avatar/30daab2c90a676d8ab323d0eab8c8f45?s=480&#x26;r=pg&#x26;d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fjo.png"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "updated_at"</span><span style="color:#000000">: </span><span style="color:#A31515">"2026-09-17T19:04:13.380Z"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "email"</span><span style="color:#000000">: </span><span style="color:#A31515">"joe.dirt@fake.com"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "email_verified"</span><span style="color:#000000">: </span><span style="color:#0000FF">true</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "iss"</span><span style="color:#000000">: </span><span style="color:#A31515">"https://[Your Auth0 domain]/"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "aud"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your Client ID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "sub"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your User ID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "iat"</span><span style="color:#000000">: </span><span style="color:#098658">1789671854</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "exp"</span><span style="color:#000000">: </span><span style="color:#098658">1789707854</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "sid"</span><span style="color:#000000">: </span><span style="color:#A31515">"&#x3C;Your SID>"</span><span style="color:#000000">,</span></span>
<span class="line"><span style="color:#0451A5">  "nonce"</span><span style="color:#000000">: </span><span style="color:#A31515">"12345"</span></span>
<span class="line"><span style="color:#000000">}</span></span></code></pre>
<p>From our example, you can see that the key <code>https://shawnsesna.octopusdemos.app</code> has been assigned the value of the roles the user <code>joe.dirt@fake.com</code> belongs to: Demo-Argo-CD-Apps.  The user <code>joe.dirt@fake.com</code> will gain access to any resource where the Octopus Deploy Team has included the external role Demo-Argo-CD-Apps.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post covered how to add Auth0 as a generic OpenID Connect authentication provider for Octopus Deploy.  Some of the steps were specific to Auth0; however, the overall instructions will work for any OpenID Connect provider.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Build your own Octopus dashboards with llms.txt and an AI coding agent</title>
      <link href="https://octopus.com/blog/build-your-own-octopus-dashboards-with-llms-txt" />
      <id>https://octopus.com/blog/build-your-own-octopus-dashboards-with-llms-txt</id>
      <published>2026-10-02</published>
      <updated>2026-10-02</updated>
      <summary>See how I used Octopus Server's llms.txt file and an AI coding agent to create realistic test data and build 3 custom dashboards with the Octopus API.</summary>
      <author>
        <name>Egor Pavlikhin, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>The built-in Octopus dashboard shows you which releases are deployed to each environment. When you manage deployments across many tenants and teams, you might want a more focused view. Which tenants are 2 releases behind? Which approvals are holding up the Payments team? How far has a release progressed across environments and tenants?</p>
<p>You can build these views with the Octopus REST API, but first you need to understand its endpoints, request bodies, and deployment step properties. Octopus’s <code>llms.txt</code> file brings that information together in a format AI coding agents can read and understand.</p>
<p>For this blog post I gave the file to a coding agent and asked it to:</p>
<ol>
<li>Create realistic test data for a multi-tenant company with several teams</li>
<li>Build custom dashboards for those teams</li>
<li>Visualize release progress in a new and interesting way</li>
</ol>
<p>Everything below ran against a local Octopus install. I used Claude Code, gave it the URL of the <code>llms.txt</code> file, an <a href="https://octopus.com/docs/api/authentication/create-an-api-key/#creating-an-agent-api-key">agent API key</a>, and the prompts quoted throughout this post.</p>
<h2 id="what-is-llmstxt">What is llms.txt</h2>
<p><a href="https://llmstxt.org/">llms.txt</a> is a convention for publishing a plain Markdown file at <code>/llms.txt</code> that condenses information about a website or a collection of documents into a single file. This can also work similarly for API documentation. In essence, it fills the same role for agents that a well-written README does for people.</p>
<p>Octopus serves its own file at <code>https://your-octopus-server/llms.txt</code>. As of the time of this writing the file is about 400 KB of Markdown and has 5 sections:</p>
<div class="table-wrap">




























<table><thead><tr><th>Section</th><th>What it contains</th></tr></thead><tbody><tr><td>Authentication</td><td>API keys, the <code>X-Octopus-ApiKey</code> header, cookie login, and cross-site request forgery (CSRF) protection</td></tr><tr><td>Space selection</td><td>The 2 URL prefix forms, <code>/api/Spaces-1/...</code> and <code>/api/spaces/default/...</code></td></tr><tr><td>Endpoints</td><td>Documented routes grouped by resource, with query parameters, request body type, and response type</td></tr><tr><td>Types</td><td>One line per type listing its properties and their child types</td></tr><tr><td>Steps and their properties</td><td>Every <code>ActionType</code> with its <code>Octopus.Action.*</code> property keys, defaults, and allowed values</td></tr></tbody></table></div>
<p>A typical endpoint line then looks like this:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>- `POST /deployments` - Create a Deployment | Prefixes (pick one): `/{spaceId}`, `/spaces/{spaceIdentifier}` | Body: CreateDeploymentCommand → DeploymentResource</span></span></code></pre>
<p>And the matching type line describing the underlying types:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>CreateDeploymentCommand: SpaceId (string), ReleaseId (string), ChannelId (string), DeploymentProcessId (string), EnvironmentId (string), TenantId (string), ForcePackageDownload (boolean), ...</span></span></code></pre>
<p>Together, these lines give the agent an endpoint and the fields for its request body. It can search for <code>POST /deployments</code>, find the matching type definition, and construct a request.</p>
<p>The Steps section is particularly useful. To build a deployment process through the API, for example, the agent needs to know that a script step uses the <code>ActionType</code> value <code>Octopus.Script</code> and stores its script in <code>Octopus.Action.Script.ScriptBody</code>. The Steps section provides these property keys alongside the API information derived from our existing Swagger docs.</p>
<p>Why is this useful? The <code>llms.txt</code> file gives agents a succinct overview of everything available in the Octopus API, making it easy to build:</p>
<ul>
<li>Custom views ands dashboards</li>
<li>Automation scripts</li>
<li>Custom integrations with Octopus</li>
</ul>
<h2 id="part-1-seeding-realistic-data">Part 1: Seeding realistic data</h2>
<p>First, I wanted to fill my Octopus instance with data. I started with an almost empty space: one project, 3 environments, no tenants, no targets. The prompt was:</p>
<blockquote>
<p>Use the Octopus API to create test data. Projects with deployment processes, fake targets, project variables, tenants, and anything else you can think of. Make the projects look realistic.</p>
</blockquote>
<p>The agent fetched <code>llms.txt</code>, searched the sections it needed, and wrote a small Python helper for calling the API securely (to avoid pasting the API key in the agent’s logs):</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#0000FF">def</span><span style="color:#795E26"> call</span><span style="color:#000000">(</span><span style="color:#001080">method</span><span style="color:#000000">, </span><span style="color:#001080">path</span><span style="color:#000000">, </span><span style="color:#001080">body</span><span style="color:#000000">=</span><span style="color:#0000FF">None</span><span style="color:#000000">):</span></span>
<span class="line"><span style="color:#000000">    req = urllib.request.Request(BASE + </span><span style="color:#A31515">"/"</span><span style="color:#000000"> + SPACE + path, </span><span style="color:#001080">data</span><span style="color:#000000">=json.dumps(body).encode() </span><span style="color:#AF00DB">if</span><span style="color:#000000"> body </span><span style="color:#AF00DB">else</span><span style="color:#0000FF"> None</span><span style="color:#000000">, </span><span style="color:#001080">method</span><span style="color:#000000">=method)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"X-Octopus-ApiKey"</span><span style="color:#000000">, KEY)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"Content-Type"</span><span style="color:#000000">, </span><span style="color:#A31515">"application/json"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#AF00DB">    with</span><span style="color:#000000"> urllib.request.urlopen(req) </span><span style="color:#AF00DB">as</span><span style="color:#000000"> r:</span></span>
<span class="line"><span style="color:#AF00DB">        return</span><span style="color:#000000"> json.loads(r.read().decode())</span></span></code></pre>
<p>The agent created a fictional company called Harborline, with 5 teams running a platform for retailers. Each retailer is represented as a separate tenant. It then seeded the instance with the deployment processes, runbooks, variables, deployment targets, etc.</p>
<h3 id="how-the-llmstxt-file-helped">How the llms.txt file helped</h3>
<p>Here are some examples of where <code>llms.txt</code> was useful for my agent:</p>
<ul>
<li><strong>Space prefixes.</strong> The agent could understand the Spaces scoping concept and apply the correct URL prefixes to the API calls, like <code>/api/Spaces-1/...</code>.</li>
<li><strong>Deployment process authoring.</strong> Keyed step properties like <code>Octopus.Action.TargetRoles</code>, <code>Octopus.Action.RunOnServer</code>, and <code>Octopus.Action.Manual.ResponsibleTeamIds</code> were documented in the file and the agent used those to create the deployment processes.</li>
<li><strong>Backdating releases.</strong> <code>CreateReleaseCommand</code> has an optional <code>Assembled</code> field. The agent used it to spread 31 releases across August and September so the dashboards would have history to show.</li>
<li><strong>Breadth of the API.</strong> As Octopus Deploy is an API-first product, the agent was able to understand what Octopus is and what it offers just by reading the available API endpoints.</li>
</ul>
<p>While the text file gave the agent enough ground to start, there were still issues where it had to make requests to the API first to understand the right shape of things. The agent worked through these issues by making API requests and reading back the detailed error and validation messages.</p>
<h2 id="part-2-custom-dashboards">Part 2: Custom dashboards</h2>
<p>One of the best scenarios for using the Octopus REST API directly is creating customized dashboards tailored to your specific requirements. This is much easier to achieve now that you can just point your coding agent to the <code>llms.txt</code> and ask it to build it for you.</p>
<h3 id="keep-the-api-key-out-of-the-browser">Keep the API key out of the browser</h3>
<p>To avoid embedding your API key into the dashboard, you can use a small proxy script. The proxy serves the static HTML and forwards requests under <code>/api/</code> to Octopus, adding the API key header. This gives the browser a single origin for both the page and its API requests, solving Cross-Origin Resource Sharing (CORS) issues.</p>
<p>Here’s is an example that forwards a request:</p>
<pre class="astro-code light-plus" style="background-color:#FFFFFF;color:#000000; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#0000FF">def</span><span style="color:#795E26"> proxy</span><span style="color:#000000">(</span><span style="color:#001080">self</span><span style="color:#000000">):</span></span>
<span class="line"><span style="color:#000000">    req = urllib.request.Request(OCTOPUS_URL + </span><span style="color:#0000FF">self</span><span style="color:#000000">.path)</span></span>
<span class="line"><span style="color:#000000">    req.add_header(</span><span style="color:#A31515">"X-Octopus-ApiKey"</span><span style="color:#000000">, API_KEY)</span></span>
<span class="line"><span style="color:#AF00DB">    with</span><span style="color:#000000"> urllib.request.urlopen(req) </span><span style="color:#AF00DB">as</span><span style="color:#000000"> r:</span></span>
<span class="line"><span style="color:#000000">        body = r.read()</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.send_response(</span><span style="color:#098658">200</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.send_header(</span><span style="color:#A31515">"Content-Type"</span><span style="color:#000000">, </span><span style="color:#A31515">"application/json"</span><span style="color:#000000">)</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.end_headers()</span></span>
<span class="line"><span style="color:#0000FF">    self</span><span style="color:#000000">.wfile.write(body)</span></span></code></pre>
<p>Now here are some examples of the dashboards you could build.</p>
<h3 id="the-tenant-rollout-board">The tenant rollout board</h3>
<p>The first dashboard is a matrix. Tenants are rows, grouped by any tenant tag set. Projects are columns, grouped by team. Each cell shows the release a tenant is running in the selected environment, and how many releases behind the newest one it is.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.png" alt="Tenant rollout board grouped by region, showing release versions, deployment states, and how far tenants are behind" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/rollout-board.webp 1485w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1485" height="1071"></p></figure>
<p>The board uses these endpoints:</p>
<div class="table-wrap">




























<table><thead><tr><th>Endpoint</th><th>Used for</th></tr></thead><tbody><tr><td><code>GET /dashboard?showAll=true</code></td><td>Current deployment per project, environment, and tenant, plus projects, tenants, and environments in one call</td></tr><tr><td><code>GET /projects/{id}/releases</code></td><td>The newest release per project and per channel, to work out how far behind a tenant is</td></tr><tr><td><code>GET /tagsets/all?scopes=Tenant</code></td><td>Tag sets for grouping and filtering</td></tr><tr><td><code>GET /tenants/variables-missing</code></td><td>Tenants that cannot be deployed to because a required variable has no value</td></tr><tr><td><code>GET /interruptions?pendingOnly=true</code></td><td>Deployments paused for approval</td></tr></tbody></table></div>
<p>The board calculates how far behind a tenant is in its channel, such that a tenant on the latest hotfix isn’t flagged as behind the mainline.</p>
<p>The summary tiles highlight where a release manager needs to act. In this example, 51 tenant deployments are behind the latest release in their channel, and 5 tenants have missing variables. Selecting <strong>Only tenants with a problem</strong> narrows the board to the affected tenants.</p>
<h3 id="the-team-health-board">The team health board</h3>
<p>The second dashboard groups work by team, with one card for each. Each card starts with deployments waiting for approval and tasks that failed in the last 24 hours. Every item links to its task in Octopus, so you can go back to the Octopus UI at any point.</p>
<p>Below these items, a table shows each project’s releases across environments. Version labels like <code>2026.3.3 ×5</code> summarize tenanted deployments. The footer shows target health by environment, and a banner at the top of the dashboard shows active or upcoming deployment freezes.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.png" alt="Team health board showing each team&#x27;s pending approvals, recent failures, and project versions across environments" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/team-health-board.webp 1485w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1485" height="1335"></p></figure>
<div class="table-wrap">
































<table><thead><tr><th>Endpoint</th><th>Used for</th></tr></thead><tbody><tr><td><code>GET /dashboard?showAll=true</code></td><td>Current deployments and the project group for each project</td></tr><tr><td><code>GET /interruptions?pendingOnly=true</code></td><td>Pending approvals</td></tr><tr><td><code>GET /tasks?states=Failed,TimedOut&#x26;fromCompletedDate=...</code></td><td>Failures in the last 24 hours</td></tr><tr><td><code>GET /machines/all</code></td><td>Target health and deactivated targets</td></tr><tr><td><code>GET /teams/all</code></td><td>Mapping project groups to the team that owns them</td></tr><tr><td><code>GET /deploymentfreezes</code></td><td>Active and upcoming freezes</td></tr></tbody></table></div>
<p>The agent sorted the cards by an attention score: 3 points per failure, 2 per pending approval. The team with the highest score appears at the top left.</p>
<h2 id="part-3-a-release-flow-diagram">Part 3: A release flow diagram</h2>
<p>And for the last one, just for fun, I asked the agent:</p>
<blockquote>
<p>Use the API via llms.txt to create one other custom dynamic HTML representation of Octopus data in a creative way, perhaps a visualization.</p>
</blockquote>
<p>The agent chose a Sankey diagram, which uses ribbons to show connections between groups. Here, the ribbons connect a project’s releases to their current environments and the tenants or tenant groups they serve.</p>
<p>Ribbon width represents the number of tenants. You can color the ribbons by release to follow a version across the diagram, or by deployment state to highlight failures and pending approvals.</p>
<figure><p><img src="/blog/i/x/build-your-own-octopus-dashboards-with-llms-txt/release-flow.png" alt="Release flow diagram connecting Storefront Web releases to environments and tenant regions" loading="lazy" srcset="/blog/i/600/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 600w, /blog/i/1000/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 1000w, /blog/i/2000/build-your-own-octopus-dashboards-with-llms-txt/release-flow.webp 1500w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1500" height="1000"></p></figure>
<p>With this you could easily create interactive charts that help you analyze a complex matrix of deployments to spot trends and anomalies.</p>
<h2 id="try-it-yourself">Try it yourself</h2>
<p>You will need a publicly accessible instance of Octopus, an <a href="https://octopus.com/docs/api/authentication/create-an-api-key/#creating-an-agent-api-key">agent API key</a>, and a coding agent that can fetch URLs and run scripts.</p>
<ol>
<li>Open <code>https://your-octopus-server/llms.txt</code> (or <code>https://your-octopus-server/api/experimental/llms.txt</code>) in a browser to confirm your version serves it.</li>
<li>Give your agent the URL and an API key with permissions scoped to a test space. Ask it to fetch the file before doing anything else.</li>
<li>Start with a read-only task, like “list every tenant that is more than one release behind in Production.” Check the answer against the portal.</li>
<li>Move on to writing the real code. When the agent makes a mistake, the API response will explain why. Use this loop to build the functionality you want.</li>
</ol>
<p>Keep the API key on the server side. If you build dashboards, create a service account, and put a proxy between the browser and Octopus, like the one above. Never paste the key into the page source. If you are using an external authentication provider, you could also extend this to use a proper authentication mechanism rather than just an API key, though the implementation could become quite complex.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>The Future of Platform Engineering report</title>
      <link href="https://octopus.com/blog/future-of-platform-engineering" />
      <id>https://octopus.com/blog/future-of-platform-engineering</id>
      <published>2026-10-01</published>
      <updated>2026-10-01</updated>
      <summary>How organizations are evolving Platform Engineering, what's working, what isn't, and why</summary>
      <author>
        <name>Charlotte Fleming, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>We're excited to announce the release of the <a href="https://octopus.com/publications/future-of-platform-engineering-report">2026 Future of Platform Engineering report</a>. The report examines why organizations adopt platforms, what shapes their decisions along the way, and what high-performing platform teams do differently.</p>
<p>Platform Engineering has moved from a frontier practice to a mainstream industry standard. It's now an established discipline with recognizable patterns and a maturing body of evidence. The report draws on primary survey data from 379 technical platform practitioners worldwide, and a review of the current literature to establish where the discipline now stands.</p>
<p>A consistent theme runs through the findings, separating platforms that succeed from those that struggle. The decisions and practices around the technology determine success: how adoption is managed, how clearly the strategy is set, and how engaged the leadership is.</p>
<h2>Why teams actually build platforms</h2>
<p>Organizations adopt platforms primarily for automation and efficiency purposes. Developer experience is still a consideration, but less of a priority for most. When they explain why the initiative was funded, automation comes first. This was a consistent finding in our previous report, the <a href="https://octopus.com/publications/platform-engineering-pulse">2025 Platform Engineering Pulse report</a>.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/benefits-reasons.png" alt="Top 10 reasons for adopting platforms" loading="lazy" }
:figcaption[Top 10 reasons for adopting platforms (multi-select)]</p>
<p>:::</p>
<p>The motivation behind platform adoption is important as it shapes how success is measured. A platform funded for efficiency reasons and later judged on developer experience will look like a failure that isn't one. Teams that name their primary motivation early have a clearer basis for knowing whether the platform worked.</p>
<h2>The organizational constraint to success is strategy</h2>
<p>Struggling teams most often report a lack of clear strategy as their obstacle, ahead of the technical complexity of building a platform.</p>
<p>Organizations that predominantly build their platform features from scratch are also the most likely to report this gap. Of respondents building from scratch, 57.9% also cite a lack of a clear strategy as an obstacle. While this may suggest that teams are building without a plan, many engineers build to work out a problem, which makes the build a method rather than a problem. This would explain why these two often turn up together.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/obstacles-build-buy.png" alt="Heatmap of obstacles by dominant build approach" loading="lazy" }
:figcaption[Build-versus-buy feature implementation due to a lack of a clear strategy and technical complexity. Heatmap showing the percentage of respondents selecting the obstacle by dominant build approach.]</p>
<p>:::</p>
<h2>A result that caught us off guard</h2>
<p>Among teams reporting mandatory adoption, 62.2% achieved most of their goals (high-performers). For respondents reporting optional platform adoption, this sits at roughly 27.5%.</p>
<p>This was an unexpected finding. Industry advice that comes up again and again in Platform Engineering is, don't force adoption, treat the platform like a product, and let it earn its users. We assumed that optional adoption would track more with high-performing teams and strong outcomes, but we found the opposite.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/goals-adoption-strategy.png" alt="Goal delivery by adoption strategy" loading="lazy" }
:figcaption[Goal delivery by adoption strategy]</p>
<p>:::</p>
<p>The tension here is real, but it doesn't mean that everyone else is doing it wrong. It's two different questions getting two different answers. Mandatory adoption is the stronger signal for whether a platform delivers on the goals it set. Optional adoption speaks to something else entirely: a platform that earns its place because developers choose it rather than because they're asked to, which is a success worth having in its own right.</p>
<p>Mandatory adoption also doesn't move on its own. In the network analysis, it clusters with stronger leadership direction, higher job satisfaction, and greater confidence in budget safety, and those measures correlate with one another about as much as each correlates with goal delivery. Mandatory adoption without the leadership engagement and budget security is unlikely to produce the same result. What appears as a decision about adoption is usually a decision about how much of the organization is behind the platform.</p>
<h2>Who's actually getting value from AI?</h2>
<p>Teams reporting the most positive impacts from AI were the platforms offering features we classified as advanced, like <strong>ephemeral environments</strong>, <strong>cost control</strong>, and <strong>code coverage</strong>. Adopting these features actually shifted platforms from no AI impact to a positive AI impact on software delivery speed and stability. The difference was in which capabilities a platform had, not how many it had.</p>
<p>:::figure</p>
<p>:img{ src="/blog/img/future-of-platform-engineering/ai-impact-features-software.png" alt="Features ranked by AI impact on software delivery speed and stability" loading="lazy" }
:figcaption[Features ranked by AI impact on software delivery speed and stability]</p>
<p>:::</p>
<p>That is consistent with how <a href="https://dora.dev/research/2025/dora-report/">DORA described AI in 2025</a>, as an amplifier of the system it enters. Where the foundations are solid, the gains compound, and where they aren't, the friction is amplified instead. Teams on mature, well-designed platforms get more out of AI than teams with ad hoc setups.</p>
<h2>What this means for platforms</h2>
<p>The patterns associated with successful platform teams emerged in organizational decisions: how clearly the strategy is set, how engaged the leadership is, and how adoption is handled. This is an encouraging result. The platforms that deliver well are the ones where people agreed what the platform was for and committed to it.</p>
<p>Platform Engineering is maturing, and the patterns are becoming clear enough to learn from. <a href="https://octopus.com/publications/future-of-platform-engineering-report">Read the report</a> for the full analysis, including how teams measure success and what high-performing teams do differently.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Octopus Approvals</title>
      <link href="https://octopus.com/blog/octopus-approvals" />
      <id>https://octopus.com/blog/octopus-approvals</id>
      <published>2026-09-29</published>
      <updated>2026-09-29</updated>
      <summary>Use approvals to gate deployments to sensitive production environments, prevent changes to QA environments while they're in use, or block the person who created a deployment from approving it themselves.</summary>
      <author>
        <name>Michelle O'Brien, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Octopus Approvals is a lightweight approval workflow that lets users gate deployments to environments that are sensitive to change. For customers that can’t justify the cost of a full ITSM implementation, it provides approval gating to ensure the right people review before a deployment progresses to the specified environments. This allows deployments to progress safely and satisfies standard compliance requirements.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approvals-main.png" alt="Home page for Octopus approvals showing two change requests waiting approval" loading="lazy" srcset="/blog/i/600/octopus-approvals/approvals-main.webp 600w, /blog/i/1000/octopus-approvals/approvals-main.webp 1000w, /blog/i/2000/octopus-approvals/approvals-main.webp 1261w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1261" height="555"></p>
</figure>
<h2 id="the-current-landscape">The current landscape</h2>
<p>Manual interventions were initially built to provide the ability to pause a deployment at crucial points to let a human review the state before progressing. We’ve seen customers build a makeshift approval system by stitching together a couple of manual intervention steps. This works for basic use cases but not particularly well at scale and has enforcement gaps.</p>
<p>The next step up is to use ITSM providers like SNOW and JSM. These are perfect for those in highly regulated industries with complex requirements. If you have strict redeployment processes or need to configure a subset of approvers, then that is still the right solution for you; we are not trying to replace these providers.</p>
<h2 id="why-weve-built-octopus-approvals">Why we’ve built Octopus Approvals</h2>
<p>Not everyone needs a full ITSM change management system, but many still want to know that a release has the buy-in of key stakeholders before it goes out the door. Octopus Approvals have been designed for customers occupying the middle ground, with more complex approval requirements than manual interventions can meet, but who can’t yet justify the cost of a full ITSM implementation.</p>
<p>Our aim with Octopus Approvals is to bridge this chasm as your compliance requirements grow and help you reach the next level. Octopus Approvals occur before deployment begins; approval is not a step that can be skipped, and it won’t take up a spot in your task queue until it has been approved. Within Approvals, you can explicitly set the parameters required to approve a deployment, including preventing the deployment creator from approving and specifying how many people must approve and from which team(s).</p>
<h2 id="octopus-approvals-walk-through">Octopus Approvals walk-through</h2>
<p>I want to set up an approval rule requiring at least two team members to approve a deployment before it goes to production. This applies across all projects in my team’s ownership, but only for production; I don’t want to slow down deployments to non-production environments.</p>
<p>Approvals are set at a space level; a single approval rule can span multiple projects. I’m going to use tags to determine which projects and environments are affected, enabling a more dynamic selection. Any new projects tagged as owned by my team will automatically have this rule applied.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-create.png" alt="Screenshot showing how to create an approval rule using tag sets" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-create.webp 600w, /blog/i/1000/octopus-approvals/approval-create.webp 1000w, /blog/i/2000/octopus-approvals/approval-create.webp 1298w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1298" height="956"></p>
</figure>
<p>When a project owned by Fire &#x26; Motion has a deployment created for a production environment, the task will wait for all required approvals. Change requests can be reviewed and approved from the deployment task and the change request tab within the approvals page. Once all required approvals have been received, the deployment will continue.</p>
<p>Each release requires only one approval; redeployments of the same release to the same environment do not require another approval. This ensures that rollbacks to approved versions are not blocked.</p>
<p>For scheduled deployments, an approval will be available as soon as the deployment is created, so you can schedule a deployment outside working hours but approve it before you clock off. This ensures that deployments stay within the approved change windows, but the checks can happen when it’s practical for the team.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-change-request.png" alt="Screenshot showing a change request awaiting review" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-change-request.webp 600w, /blog/i/1000/octopus-approvals/approval-change-request.webp 1000w, /blog/i/2000/octopus-approvals/approval-change-request.webp 1523w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1523" height="796"></p>
</figure>
<h2 id="rejected-change-requests">Rejected change requests</h2>
<p>If any approver rejects the change request, the deployment will not proceed and will be marked as failed. You can not revive a rejected change request; you’ll need to create a new deployment, which will create a new change request. You can use your normal process to identify failed deployments, or set up a Subscription to be notified of change request status.</p>
<h2 id="reviewing-and-approving-change-requests">Reviewing and approving change requests</h2>
<p>Change requests can be viewed on the Approvals page. From here, you can see who approved the deployment and when, along with deployment details. Approvals and changes to rules can also be tracked via the audit page; there are ‘Approval Rule’ and ‘Server Task Approval’ document types that the audit page can be filtered by.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-complete.png" alt="Screenshot showing a complete change request" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-complete.webp 600w, /blog/i/1000/octopus-approvals/approval-complete.webp 1000w, /blog/i/2000/octopus-approvals/approval-complete.webp 1936w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1936" height="1678"></p>
</figure>
<h2 id="notifications">Notifications</h2>
<p>The same event types used to filter the audit page can also be used to set up targeted notifications. Using our subscriptions feature, you can subscribe to notifications about rule changes to help ensure your team remains compliant. Notifications can be sent via email, Slack, Teams or webhook. Note the below screenshot is the first iteration of our Slack integration, we are currently working on providing a more actionable message.</p>
<figure>
<p><img src="/blog/i/x/octopus-approvals/approval-notification.png" alt="Screenshot showing a Slack notification following a change to an Approval Rule" loading="lazy" srcset="/blog/i/600/octopus-approvals/approval-notification.webp 300w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="300" height="163"></p>
</figure>
<p>We will be releasing a separate blog post detailing how you can use subscriptions and webhooks to receive more targeted notifications, so that users who need to approve the deployment are the ones who receive the notification.
<a href="https://octopus.com/software-delivered">Subscribe to our newsletter</a> to get this in your inbox.</p>
<h2 id="known-limitations">Known limitations</h2>
<ul>
<li>Any of the required approvers are counted towards the total. If you specified two teams within the approvers, any two approvals across those teams will suffice.</li>
<li>This cannot be used to automate your approval process.</li>
<li>Approvals only work for deployments; this functionality is not available for runbooks.</li>
<li>Approvals, unfortunately, cannot be used to finally get your mother-in-law’s approval.</li>
</ul>
<p>Octopus Approvals is now available as a Public Preview for cloud customers and in the 2026.3 server release.</p>
<p><a href="https://octopus.com/docs/approvals/octopus-approvals">Read more about getting started with Approvals here</a>.</p>
<p>Happy deployments!</p>]]></content>
    </entry>
    <entry>
      <title>Inside Platform Engineering with Kian Rogerdi</title>
      <link href="https://octopus.com/blog/inside-platform-engineering-kian-rogerdi" />
      <id>https://octopus.com/blog/inside-platform-engineering-kian-rogerdi</id>
      <published>2026-09-25</published>
      <updated>2026-09-25</updated>
      <summary>Kian Rogerdi joins Matt Allford to talk about connecting Platform Engineering to business value, why platform teams should be enablers rather than gatekeepers.</summary>
      <author>
        <name>Matthew Allford, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>If you’ve got the best engineers in the world building the greatest product nobody uses, what was the point? That’s a core question that was lingering in my mind in this episode of Inside Platform Engineering, with Kian Rogerdi. Kian has spent over 20 years in software, the last 10 of them in leadership, running cloud engineering, SRE, and Platform Engineering departments at companies ranging from startups to big enterprises.</p>
<p>What stood out to me most was how much of this conversation ended up being about mindset rather than tooling. In fact, it’s been a common theme in many conversations I’ve had on Inside Platform Engineering, and I’m seeing it as a crucial element in successful platform teams. We didn’t talk much about specific platforms or products. We talked about how easy it is for a platform team to lose sight of who they’re serving, and what that looks like when it goes wrong.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p>You can watch the episode with Kian below.</p>
<p><a href="https://www.youtube.com/watch?v=omCDrSlp-5E">Inside Platform Engineering with Kian Rogerdi</a></p>
<h2 id="business-value-is-the-point">Business value is the point</h2>
<p>Kian’s framing is simple, but as a technologist, can be easy to lose sight of. Everything a platform team does only matters if it’s actually helping someone else move faster and safer. He pointed out that plenty of teams still measure themselves on technical excellence alone, building the most secure, most powerful, most elegant system, without asking whether anyone’s using it or whether it’s solving a real problem. Product teams sorted this out years ago because they have a customer staring back at them. Platform teams don’t get that same forcing function, since their customers are internal, which is exactly why it’s harder and why it gets missed. That mindset needs to be a deliberate choice from day 1 when forming a platform team, backed by sensible success metrics to help platform teams showcase their value.</p>
<h2 id="treat-your-internal-teams-like-customers">Treat your internal teams like customers</h2>
<p>This idea of treating internal teams as real customers has come up in previous conversations, even even as far back as <a href="https://youtu.be/gKReR1YYMgU">Inside Platform Engineering episode 001</a> with Luke Philips. Kian leaned on Team Topologies, and it’s worth a read if you haven’t. His point was that platform teams are supposed to reduce cognitive load for stream-aligned teams, but that only works if you’re listening to those teams the way a product team listens to its customers. He talked about embedding platform engineers with the teams they serve, just to watch what actually happens, which templates get ignored, which pipelines get quietly replaced with something homegrown. I really love this take, and I’ve seen versions of this myself, where teams don’t abandon your tooling because they’re difficult, they abandon it because it doesn’t solve their problem.</p>
<p>It reminds me of when I was in a consultative role as a DevOps Engineer, and we were a little too on the reactive side where we were waiting for customers to come to us when they needed help. We decided to flip the model on its head, and asked to sit in with our customers’ developers on their daily standup. We weren’t in every standup, just one or two a week. Just doing this paved the way for a much better way of working <em>with</em> their teams. We could ask questions like “why are you doing it that way” or suggest “we can make that a lot easier”, and because we were in the room, they would ask for our opinion on the topics of discussion in that meeting. Dev and Ops coming together, who would have thought! I see this same pattern as crucial for platform teams, who need to spend time with the teams they’re trying to enable.</p>
<h2 id="enablers-not-gatekeepers">Enablers, not gatekeepers</h2>
<p>Speaking of enabling, Kian described a pattern he’s seen play out at companies he’s worked at, where a platform team that starts dictating instead of enabling, vetoing decisions, and effectively becomes a gate that other teams have to get through. He drew a clear line around genuine compliance requirements in regulated industries like fintech or healthcare, which are legitimately non-negotiable, but said everything beyond that is a red flag. He also noted this tends to get worse as companies scale, because politics creeps in and platform work becomes a source of power over other teams rather than a service to them. The fix, in his words, isn’t complicated; it’s remembering the purpose. It just takes deliberate effort to keep remembering it.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Inside Platform Engineering is a series of conversations with Matt Allford and a guest, bringing their own experience and perspective from the world of Platform Engineering.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw24Y-7jTcw09jbzsLw5uL9X">YouTube</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Continuous Delivery Office Hours Ep.9: The compliance ratchet</title>
      <link href="https://octopus.com/blog/continuous-delivery-office-hours-e9" />
      <id>https://octopus.com/blog/continuous-delivery-office-hours-e9</id>
      <published>2026-09-22</published>
      <updated>2026-09-22</updated>
      <summary>Find out why compliance is a one-way ratchet that only ever adds controls and never removes them.</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>In the last episode, we talked about AI efficiency and effectiveness. That meant thinking not just as a beneficiary user but also as an end user. In this episode, we bring the authority to bear when we discuss <a href="https://octopus.com/blog/the-compliance-ratchet">the compliance ratchet</a>. It turns out that there is an equal and opposite reaction in software, just like physics. If you speed up your software delivery velocity without the right practices in place, you encounter trip hazards that cause your organization to tighten controls.</p>
<p>The most dangerous part of this reaction is that compliance controls are added very easily, but are difficult or impossible to remove. That means you can trigger a one-way ratchet that permanently hampers your software delivery performance.</p>
<p>At the moment, it’s common for teams to use AI to increase the volume and pace of their software development. If this results in outages or high-profile bugs, compliance controls are slapped on the team. This often pushes the pace of change back to pre-AI levels, preventing any return on the AI investment.</p>
<h2 id="watch-the-episode">Watch the episode</h2>
<p><a href="https://www.youtube.com/watch?v=bHlXI26jbug">Watch Continuous Delivery Office Hours Ep.9</a></p>
<h2 id="balance-the-ai-investment">Balance the AI investment</h2>
<p>One of the mistakes of introducing AI coding tools is expecting immediate gains. This puts pressure on teams to produce more changes and discourages them from introducing the pipelines and practices that will unlock the velocity and safety of sustainable high-performance software delivery.</p>
<p>On top of balancing the need for enabling practices, it’s also crucial to balance where you apply AI. If you only use AI to increase coding speed, the increased change volume will be absorbed by downstream bottlenecks. That means you need to consider where the investment has the greatest impact on the end-to-end flow of value, rather than speeding up individual stages and pushing more work into queues for review, approval, deployment, and feedback.</p>
<h2 id="unlock-the-ratchet">Unlock the ratchet</h2>
<p>The controls you have in place for software delivery were likely created for a different time. They accrue as a result of organizational trauma: a bad deployment during a peak period means a 3-month deployment freeze, or a bad dependency means every change needs a senior developer review. Even if the team has learned from those mistakes, the controls remain.</p>
<p>Now might be a good time to revisit your path from a developer’s laptop to production, documenting each control and deciding whether it’s still necessary, and whether there’s a better way to manage the controls you decide to keep, optimizing for both speed and stability. Each past decision made on the assumption that there was a trade-off between velocity and safety should be reconsidered to find a way to achieve both. Automated validation is often faster and more repeatable than manual validation, so you likely have a way to achieve both easily. Replacing manual stages with automation provides far better control than adding additional slow checks.</p>
<h2 id="direction-often-beats-speed">Direction often beats speed</h2>
<p>If you’ve been hanging around in software development for any length of time, you’d think all people cared about was speed. This is one of the great distractions of the past 3 decades. Teams adopted Agile for speed, they adopted DevOps for speed, and now they’re buying AI coding tools for speed. As described in <a href="https://octopus.com/blog/the-bullseye-model-for-product-velocity">The Bullseye Model</a>, the ultimate goal isn’t speed, but a combination of speed and direction. When you aren’t sure what users want, delivering software versions early and often helps you discover their needs and what works. Ultimately, though, you’re trying to hit the bullseye with perfect software, and that means direction is at least as important as speed, perhaps more so.</p>
<p>All of these insights point to a necessary shift in software delivery; one that is long overdue. For a long time, we’ve treated technical practices as optional, but with the velocity and scale of modern software, that’s no longer true. We’ll be talking more about this in the future.</p>
<p>Happy deployments!</p>
<div class="hint"><p>Continuous Delivery Office Hours is a series of conversations about software delivery, with Tony Kelly, Bob Walker, and Steve Fenton.</p><p>You can find more episodes on <a href="https://www.youtube.com/playlist?list=PLAGskdGvlaw3CrxkUOAMmiy928lr5D4oh">YouTube</a>, <a href="https://podcasts.apple.com/us/podcast/continuous-delivery-office-hours/id1872101651">Apple Podcasts</a>, and <a href="https://pca.st/hwjaox59">Pocket Casts</a>.</p></div>]]></content>
    </entry>
    <entry>
      <title>Let's not water down the terms Continuous Integration and Continuous Delivery</title>
      <link href="https://octopus.com/blog/watering-down-ci-cd" />
      <id>https://octopus.com/blog/watering-down-ci-cd</id>
      <published>2026-09-08</published>
      <updated>2026-09-08</updated>
      <summary>Find out why CI/CD aren't vague buzzwords. Here's what Continuous Integration and Continuous Delivery actually mean, and the common misconceptions worth correcting</summary>
      <author>
        <name>Steve Fenton, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>One of the most perplexing discussions I’ve had in recent times was about CI/CD. I’ve earned my CI/CD scout badge at this stage in my career, so the confusion caught me off guard. This post briefly sets out what Continuous Integration (CI) and Continuous Delivery (CD) mean and addresses the most common misconceptions in the discussion.</p>
<p>My point isn’t just semantic. These terms carry decades of hard-won engineering practice, and when we let their meaning drift or decay, we lose crucial wisdom.</p>
<h2 id="continuous-integration">Continuous Integration</h2>
<p>Continuous Integration is well named compared to many concepts in our industry. The term “integration” refers to merging your changes into the main branch in version control, and the term “continuous” means you do it often. When Kent Beck popularized the term in <a href="https://www.oreilly.com/library/view/extreme-programming-explained/0201616416/">Extreme Programming Explained</a> (1999), he set an upper limit of a day. After 26 years of progress, you should expect to be doing it far more frequently than that.</p>
<p>You might understand the concept better if we look at examples that don’t fit the definition. If you have code on your machine that’s more than a day old, you’re not practicing Continuous Integration. If you have a branch that’s more than a day old, you’re not practicing Continuous Integration. It doesn’t matter how often you merge from main into your branch, because main is completely unaware of changes, like yours, that haven’t been merged yet. That means you have diverging software versions, and at some point, you have to make them all converge.</p>
<h2 id="continuous-delivery">Continuous Delivery</h2>
<p>Continuous Delivery is an approach focused on delivering all changes to users quickly, safely, and sustainably. You keep your code in a deployable state at all times and reject the idea of separate integration, testing, and hardening phases. There are specific technical practices involved here because you need a deployment pipeline that reduces risk and increases your confidence that the software version works.</p>
<p>At this stage in our industry’s history, almost everything involved in answering the question “Is this deployable?” should be automated as part of the deployment pipeline. A decade ago, perhaps, you could reasonably list a few stages that were economically unviable for automation. The scale and pace of modern software delivery mean this is rarely the case now.</p>
<p>The simple test is to ask, “Is the software deployable?” If you can’t answer this question every day, you’re not doing Continuous Delivery. In their book on <a href="https://www.oreilly.com/library/view/continuous-delivery-reliable/9780321670250/">Continuous Delivery</a> (2010), Dave Farley and Jez Humble detail the principles and practices that let you answer this question, and the research has increased our confidence that this is what good software delivery looks like.</p>
<h2 id="misconceptions">Misconceptions</h2>
<p>Let’s turn our attention to where the conversation left the trail.</p>
<h3 id="ci-doesnt-just-mean-automated-builds">CI doesn’t just mean automated builds</h3>
<p>One of the most common sources of confusion is that CI just means automated builds and tests. While it’s difficult to practice Continuous Integration without automation, the crucial property of Continuous Integration is that all changes are in a shared mainline, not on developer machines or in long-lived branches. Having more than 3 branches or keeping branches open for more than a day slows delivery throughput and makes the software less stable, as reinforced by <a href="https://dora.dev/research/">long-running research</a>.</p>
<p>There’s a human process at the heart of CI: each developer chooses to commit their changes frequently to main. Without people deciding to do this, there is no CI.</p>
<h3 id="ci-doesnt-allow-feature-branching">CI doesn’t allow feature branching</h3>
<p>There’s a misconception that you can do CI and choose a branching strategy, like feature branches. Any branching strategy that keeps a change out of main for more than a day cannot be called CI. You may still choose to use feature branches despite the drawbacks, but you have to stop calling it Continuous Integration, as you aren’t continuously integrating all changes.</p>
<h3 id="cd-doesnt-mean-continuous-deployment-though-it-can">CD doesn’t mean Continuous Deployment, though it can</h3>
<p>CD means “Continuous Delivery”. It means your software is deployable at all times, and you can test this daily by asking, “Can we deploy the software?” If you don’t have a way to answer the question confidently, you’re not doing Continuous Delivery. You may choose to deploy every validated software version automatically, which is Continuous Deployment, or you may need to be more tactical about when you deploy. Either is fine.</p>
<p>For both Continuous Delivery and Continuous Deployment, you need high levels of automation and sufficient risk coverage within that automation so you know you can deploy. Continuous Delivery should mean you can deploy at the press of a button, and Continuous Deployment removes the requirement to press the button.</p>
<h3 id="you-cant-do-cd-without-ci">You can’t do CD without CI</h3>
<p>Some folks were confident that you can do Continuous Delivery without doing Continuous Integration, but the literature on this is very clear. Farley and Humble explicitly include Continuous Integration as a required practice. There’s a whole chapter dedicated to it (p. 55 of Continuous Delivery) that adequately explains how it should work and why it’s not optional.</p>
<h3 id="cicd-isnt-vague-or-open-to-interpretation">CI/CD isn’t vague or open to interpretation</h3>
<p>Another claim that came up more than once was that CI/CD is vague, open to interpretation, or lacks the detail on “how” you should apply the approach. This does a disservice to those who conscientiously captured the approach, whether that’s Kent Beck describing Continuous Integration in <a href="https://www.oreilly.com/library/view/extreme-programming-explained/0201616416/"><em>Extreme Programming Explained</em></a> (1999) or Farley and Humble detailing both Continuous Integration and Continuous Delivery in their book <a href="https://www.oreilly.com/library/view/continuous-delivery-reliable/9780321670250/"><em>Continuous Delivery</em></a> (2010).</p>
<h2 id="resist-the-diffusion">Resist the diffusion</h2>
<p>There is a constant temptation in the tech industry to allow <a href="https://martinfowler.com/bliki/SemanticDiffusion.html">semantic diffusion</a> to erode our progress. When we package up a cartload of wisdom into a term, we have to resist the diffusion that follows when people oversimplify our term, remove crucial parts of the whole, or try to brand something with the label that pushes us backward.</p>
<p>We have a history of giving up on these battles, as we seem to have done with Agile and DevOps. These terms had an important meaning that represented a significant step forward for our industry, and losing our energy in their defense lets the perpetual “Late Majority” and “Laggards” (from <a href="https://geoffreyamoore.com/book/crossing-the-chasm/">Crossing the Chasm</a>) drag us all back into their old ways.</p>
<p>CI and CD deserve better.</p>]]></content>
    </entry>
    <entry>
      <title>Debunking the hotfix pipeline myth</title>
      <link href="https://octopus.com/blog/the-hotfix-myth" />
      <id>https://octopus.com/blog/the-hotfix-myth</id>
      <published>2026-08-27</published>
      <updated>2026-08-27</updated>
      <summary>Why it is more important to making the default pipelines as fast and as efficient as possible over hotfix pipelines.</summary>
      <author>
        <name>Bob Walker, Octopus Deploy</name>
      </author>
      <content type="html"><![CDATA[<p>Hotfix pipelines are like an extended warranty; they make sense in principle, but in reality, they aren’t necessary.  And just like an extended warranty, you spend a lot of money (and time) building a hotfix pipeline that you should use only rarely.  This article will first walk through the fundamental flaws of hotfix pipelines.  Then it will explain why it is better to focus on improving the standard Production deployment pipeline.</p>
<h2 id="defining-hotfix-pipelines">Defining hotfix pipelines</h2>
<p>Hotfix pipelines are a “fast lane” to the normal Production deployment pipeline. Their purpose is to skip specific steps to get changes into Production as soon as possible.  Typically, they are:</p>
<ul>
<li>Production deployment pipeline: Development → Test → Staging → Production</li>
<li>Hotfix pipeline: Staging → Production</li>
</ul>
<p>They exist because of this scenario:</p>
<ol>
<li>A critical bug appears in Production hours or days after a deployment.</li>
<li>A rollback is impossible, as that will cause more problems than it solves.</li>
<li>There is work in flight in lower environments.</li>
<li>The hotfix pipeline skips the lower environments.</li>
</ol>
<p>The core idea behind hotfix pipelines is that they allow normal work to continue while providing a path to fix a critical bug as soon as possible.  Someone, either within the development team or the business, determines that a bug is important enough to fix and can skip use the hotfix pipeline.</p>
<h2 id="hotfixes-in-the-real-world">Hotfixes in the real world</h2>
<p>From experience, hotfix pipelines are created in response to a suboptimal Production deployment pipeline.  There is a step, or several steps, that either add little value or take so much time that everyone is fine skipping them when certain conditions are met.</p>
<p>I experienced a suboptimal process before joining Octopus Deploy.  Before Octopus Deploy, I was a software engineer for almost 15 years.  This was a deployment process at a company I worked for before we fully automated it (using Octopus Deploy, of course!).</p>
<ul>
<li>Four environments: Development, QA, Integration, and Production.</li>
<li>Shared database for the same application for all in-flight work.</li>
<li>The build server only monitored the main branch.</li>
<li>The build server automatically deployed to Development and QA.</li>
<li>Deploying to Integration and Production used different tooling and was a mix of manual and automated.</li>
</ul>
<p>As you can see, deploying to Integration and Production was completely different from Development and QA.  Some notable new steps include placing code build artifacts in “hot folders” to automatically deploy code, placing database delta scripts in designated network folders, and hand-typing instructions for the Web Admins and DBAs.</p>
<p>Deployments to Production took hours due to missing database and configuration changes, and occasional failures to update a server with the latest code.  Because deployments to Production took hours and were error-prone, the application I was responsible for deployed new functionality and most bug fixes to Production once a quarter.  Those deployments were always off-hours.</p>
<p>Integration was slightly better; we could deploy to that in the middle of the day, but it was still just as error-prone.  We didn’t want to deal with that headache.  Integration was often updated only days before a Production deployment.  As a result, Integration and Production were almost always running the same version.  This created a clear hotfix path for us.</p>
<p>Generally, but not always, hotfixes could be placed into one of four buckets.  In each case, we could run the proposed fix in Integration before Production.</p>
<ul>
<li>Fix the issue by changing a record in the database to “trick” the code into the right path.</li>
<li>Solve the problem by making a change to the database schema or a stored procedure.  Sometimes it was as simple as adding an index.</li>
<li>Make a configuration change, either in a web.config file or the operating system host.</li>
<li>If it required a code fix, we could configure the build server to create the build artifacts but not deploy to Development and QA.  We’d copy the artifacts to the “hot folders” for a deployment to Integration.</li>
</ul>
<p>The common workflow when a bug was reported was for the on-call engineer to triage it.  Once they isolated the root cause, they’d pair with a DBA or Web Admin until the issue was resolved.  While issues were resolved, it led to other problems.</p>
<ol>
<li>Database schema hotfix changes were rarely “copied down” to lower environments.  The delta in the database schema between Dev/QA and Integration/Production was significant.</li>
<li>Configuration changes sometimes didn’t make it into version control.  The next deployment to Integration and Production would often overwrite the hotfix, requiring another hotfix.</li>
<li>Because of the once-a-quarter releases, finding an appropriate point in time in version control that just had what was in Integration/Production took a lot of effort.</li>
</ol>
<h2 id="impact-of-automation-on-hotfix-pipelines">Impact of automation on hotfix pipelines</h2>
<p>Automating a terrible process doesn’t make it any less terrible.  The core problems must be addressed.  Our core problem was two different processes to deploy software from Development through to Production.  Those two processes were required because the build server was used for deployments and shared application database.  The build server lacked the appropriate RBAC and configuration controls to satisfy security and audit requirements.  The shared database encouraged the merging of unfinished code.</p>
<p>Using a combination of local database development, git, Redgate’s tooling, Octopus Deploy, and Azure DevOps (or Visual Studio Team Services as it was called back then), we rebuilt the entire deployment pipeline.</p>
<ul>
<li>All changes, code, and database are made in a branch.</li>
<li>Changes are merged to the main using a Pull Request.</li>
<li>Azure DevOps / VSTS builds the code and packages database changes.</li>
<li>Octopus + Redgate deploys those build artifacts to Development.</li>
<li>Automated Verification, then Octopus + Redgate, promotes those artifacts to QA.</li>
<li>QA verifies.</li>
<li>Promote those artifacts to Integration using Octopus + Redgate.</li>
<li>Final sign-off.</li>
<li>Promote those artifacts to Production using Octopus + Redgate.</li>
</ul>
<p>Rebuilding the deployment pipeline had numerous benefits.</p>
<ol>
<li>Consistent deployments: No more surprises when deploying to Integration and Production, as it was the same process for Development and QA</li>
<li>Eliminated post-deployment emergency fixes: Automation stopped the dumb mistakes that led to many hotfixes.</li>
<li>Reduced manual work: No one had to copy files to specific network folders and write up instructions.</li>
<li>Increased release cadence: Production deployments increased from once a quarter to once a week.</li>
<li>Smaller changeset: Instead of dozens or 100s of changes, each deployment had fewer than a dozen.  That meant there was a lower chance that something could go wrong.</li>
</ol>
<p>The most surprising thing to us was the more tolerant users.  When it took two or three months to get a bug fix into production, users would classify most bugs as high/must-fix because they couldn’t wait that long. With the new pipeline, unless the bug was critical, most users were willing to wait a day or two for a fix.  With smaller change sets, the number of critical bugs dropped significantly.</p>
<p>While we were deploying once a week, we still had major features that would take over a month to develop.  All that work would occur in a branch.  When we merged in those major changes, it would sometimes take a week or two to fully test it (our actual average was deploying every 10 days).  During that time, there was “no clear path to production” for hotfixes.</p>
<ul>
<li>For major features, we’d often do a few deployments to Integration as a final test with Production-like data.</li>
<li>Once major features were pushed to Integration, there was no easy way to roll back those changes.</li>
<li>We used a Service-Oriented Architecture (SOA) to achieve loose coupling, but there was still some coupling.  Whenever anyone rolled back Integration to match Production, other applications would break because they were expecting a specific version.</li>
</ul>
<p>Because we knew there was a chance of bugs after deploying a major feature to Production, we would implement a merge freeze, except for critical bugs, for a week after the Production deployment.</p>
<p>The merge freeze was the best solution we could come up with at the time.  We looked into creating a hotfix pipeline, but with deployments to Production occurring once a week (or so), we kept running into the same roadblocks:</p>
<ul>
<li>How do we qualify a bug to justify using the hotfix pipeline?</li>
<li>What if a pending change is in Integration?</li>
<li>How do we configure the build server to tell Octopus to push to Integration instead of Development?</li>
<li>How do we know which branch would be in Production when another bug occurred with a hotfix pipeline?</li>
</ul>
<p>The decision tree of if/then/elseif/else for when to use a hotfix pipeline became extremely confusing.  In addition, fewer critical bugs were released.  The work to create a hotfix pipeline for my team was de-prioritized.  Until I left, I never saw that becoming a priority.  Every once in a while, when we had a merge freeze, someone would bring it up.  But it was more of a passing comment.  Not a dictate to make a change.</p>
<h2 id="hotfixes-dont-fit-in-modern-software-delivery">Hotfixes don’t fit in modern software delivery</h2>
<p>The story from above took place over 10 years ago.  It involved deploying .NET applications to static environments hosted by Windows Servers.  There were many Continuous Delivery techniques we didn’t implement at the time.  In addition, many technologies and techniques have been introduced.</p>
<p>Below are the core principles every software delivery pipeline should follow.  See <a href="http://ContinuousDelivery.com">Continuous Delivery</a> and <a href="https://octopus.com/whitepapers/achieving-continuous-delivery-with-tpf">Achieving Continuous Delivery with TPF</a>, and <a href="https://trunkbaseddevelopment.com">Trunk-Based Development</a> for more details.</p>
<ul>
<li>The main branch must always be in a deployable state.</li>
<li>Create the build artifacts from the main branch once and promote them through the necessary environments to production.</li>
<li>How you deploy to Production should be exactly the same as how you deploy to Development, Testing/QA, and Staging/Integration.</li>
<li>Automate as much testing as possible, including unit testing, integration testing, and post-deployment smoke and soak tests.</li>
<li>Store environment configuration in version control and use that to keep all environments similar.  Compute resources and external access can differ by environment.</li>
<li>Separate deploying new code from releasing new functionality by using feature flags.</li>
</ul>
<p>With those core principles in mind, the delivery pipeline is the following:</p>
<ol>
<li>Make any changes in a short-lived branch.</li>
<li>Checking in changes to a branch creates a pre-release artifact and deploys to an ephemeral environment or a static Development environment.</li>
<li>Merge those changes into the main branch via a pull request.</li>
<li>The pull request should also be verified on an ephemeral environment or a static PR environment.</li>
<li>After merging into the main branch, create the release artifacts.</li>
<li>Promote those release artifacts through any static testing environments (Test, QA, Staging, Pre-Production, etc.) to Production.</li>
<li>Once a feature is ready, enable the new functionality for a subset of users in Production via feature flags.  Start with internal teams, then slowly add users until you have enabled it for all users.</li>
</ol>
<figure><p><img src="/blog/i/x/hotfix-myth/branching-diagram-with-ephemeral-environments.png" alt="Diagram demonstrating when ephemeral environments will be used in a trunk-based or GitHub Flow based branching strategy" loading="lazy" srcset="/blog/i/600/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 600w, /blog/i/1000/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 1000w, /blog/i/2000/hotfix-myth/branching-diagram-with-ephemeral-environments.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2219" height="1091"></p></figure>
<p>With the appropriate guardrails around the main Production deployment pipeline, a hotfix pipeline for just Staging → Production raises a lot of questions.</p>
<ul>
<li>Branching and Deploying
<ul>
<li>Will the hotfix branch use an ephemeral environment for testing before going to Staging?</li>
<li>How will the build server know to skip the Test environment and move to Staging → Production?</li>
<li>What will the version number be for the hotfix release?  If Production is 2026.8.1, does that mean the hotfix is 2026.8.1-Hotfix, 2026.8.1.1, or 2026.8.1.1-hotfix?</li>
<li>The main branch is supposed to represent production. How will the appropriate hotfix be communicated to the rest of the engineering team?</li>
<li>When will the hotfix changes merge into the main branch?  How much of a delta is there between what is in the main branch and production?  Can the fix even be merged into the main branch without serious modifications?</li>
</ul>
</li>
<li>Testing and Risk
<ul>
<li>What is preventing the main branch from being deployed to Production?</li>
<li>If there is new functionality that’ll likely have many edge cases and potentially show-stopping bugs, why wasn’t it behind a feature toggle?</li>
<li>Were there changes already in Staging that were overwritten by the hotfix?  Will that impact other teams or applications?</li>
<li>What steps and tests are being skipped in the Test environment?</li>
<li>How much time is really being saved by skipping the Test environment?</li>
<li>What if the hotfix requires a hotfix?  How long is it acceptable to block the normal pipeline from deploying to Staging → Production?</li>
<li>How often is the hotfix pipeline tested and verified?</li>
</ul>
</li>
</ul>
<h2 id="hotfix-pipelines-no-longer-make-sense">Hotfix pipelines no longer make sense</h2>
<p>All of the challenges listed above are solvable with enough time and money.  But to quote Ian Malcolm from Jurassic Park, “Your scientists were so preoccupied with whether they could, they didn’t stop to think if they should.”</p>
<p>To put it bluntly:</p>
<ul>
<li>Is it worth spending time to work through all those issues to create a hotfix pipeline?</li>
<li>How often do you need to push a hotfix?</li>
<li>If it is a regular occurrence, is the hotfix process masking a suboptimal process like the one I described earlier?</li>
</ul>
<p>The time spent creating a hotfix pipeline is better spent making the primary software delivery pipeline as efficient as possible.  A good goal is to take less than an hour from pull request acceptance to being ready to deploy to Production.  That includes builds, testing, linting, scanning, deploying to lower environments, and verification.</p>
<p>Achieving that requires addressing some hard problems:</p>
<ul>
<li>If the main branch is regularly in an undeployable state, what testing and verification should be moved earlier in the pipeline to ensure it is always in a deployable state?</li>
<li>Is the branching strategy Trunk Based Development or GitHub Flow (not to be confused with GitFlow)?  If not, why not?  Even the creator of GitFlow has said <a href="https://nvie.com/posts/a-successful-git-branching-model/">not to use it for most applications.</a></li>
<li>Showstopping bugs, ones that typically require a hotfix, are typically the result of new features and functionality.  How can <a href="https://openfeature.dev">Feature Flags</a> be introduced into the pipeline to separate deploying new code from releasing new features?</li>
<li>If manual review and approval processes are the primary bottleneck, which steps in that process are prime candidates for automation to speed up approvals?</li>
</ul>
<p>The primary advantage of focusing on the items above is that they have a net positive for any change.  New features, security patches, and bug fixes will be deployable faster.</p>
<p>**Disclaimer:*- The one hour is a goal to aim for.  It isn’t a hard rule.  One hour for some applications, like monoliths, is impossible.  That doesn’t mean you shouldn’t try.  Small improvements add up over time.  Improving a monolith’s pipeline from one day to two hours is a huge accomplishment.</p>
<h2 id="configuring-octopus-deploy">Configuring Octopus Deploy</h2>
<p>By this point, you might be asking yourself, how does this impact my configuration of Octopus Deploy?</p>
<p>Unfortunately, lifecycles (and channels) are among the most misconfigured constructs within Octopus Deploy.  To make onboarding easier, the default lifecycle is built using conventions.  If you were to create Development, Test, Staging, and Production environments, the default lifecycle automatically becomes:</p>
<ul>
<li>Development → Test → Staging → Production</li>
</ul>
<p>That default lifecycle encourages bad behavior, necessitating a hotfix lifecycle.</p>
<ul>
<li>It doesn’t represent how developers work in branches.  Because it includes Production, it has to represent the main branch.  But it also includes Development.  To get feedback, developers are forced to use their local machine or merge unfinished changes into main.</li>
<li>With unfinished changes in the main branch, it is currently undeployable.  It could remain there for days and sometimes weeks.</li>
<li>Enforcing SemVer versioning rules becomes nearly impossible.  For example, releases that are not ready for Production typically receive a pre-release tag because the lifecycle includes Development and Production; that rule cannot exist.</li>
<li>There isn’t a clear path to push a fix to Production.  To prevent that from happening, teams will implement merge freezes for a period of time after a major release (like I did at a previous job).  Or create a hotfix lifecycle that skips Development and Test.</li>
</ul>
<p>The root cause of that is having the static Development environment included in the same lifecycle as Production.  Development is for unfinished changes, Production is for finished changes.  A Production lifecycle must never include Development.</p>
<p>If a static Development environment is required, my recommended lifecycles are:</p>
<ul>
<li>Default: Development</li>
<li>Release: Test → Staging → Production</li>
</ul>
<figure><p><img src="/blog/i/x/hotfix-myth/recommended-octopus-lifecycles.png" alt="Screenshot of Octopus Deploy interface showing the recommended lifecycles of default and release." loading="lazy" srcset="/blog/i/600/hotfix-myth/recommended-octopus-lifecycles.webp 600w, /blog/i/1000/hotfix-myth/recommended-octopus-lifecycles.webp 1000w, /blog/i/2000/hotfix-myth/recommended-octopus-lifecycles.webp 1951w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="1951" height="558"></p></figure>
<p>**Disclaimer:*- The release lifecycle should include all static testing environments required to reach Production.  You might only need Test → Production, or Staging → Production.  I included Test → Staging → Production because, as an industry, we have coalesced around four environments.</p>
<p>The subsequent Project Channels are:</p>
<ul>
<li>Default (uses the default lifecycle or an ephemeral environment): build artifacts require a pre-release tag and can only be created from non-main branches.</li>
<li>Release (uses release lifecycle): build artifacts cannot have a pre-release tag and can only come from the main branch.</li>
</ul>
<figure><p><img src="/blog/i/x/hotfix-myth/recommended-octopus-channels.png" alt="Screenshot of Octopus Deploy interface showing the recommended default and release channels for a specific project." loading="lazy" srcset="/blog/i/600/hotfix-myth/recommended-octopus-channels.webp 600w, /blog/i/1000/hotfix-myth/recommended-octopus-channels.webp 1000w, /blog/i/2000/hotfix-myth/recommended-octopus-channels.webp 2000w" sizes="(min-width: 1680px) 1000px, (min-width: 940px) calc(71.81vw - 192px), calc(100vw - 32px)" class="resp-img" width="2033" height="1055"></p></figure>
<p>GitHub Actions (or really any build server) doesn’t make dynamically selecting channels based on branches any easier.  They require using a hard-to-decipher if/then/else command in the build definition.  For example, ${{ github.ref == ‘refs/heads/main’ &#x26;&#x26; vars.OCTOPUS_RELEASE_CHANNEL || vars.OCTOPUS_DEFAULT_CHANNEL }}.</p>
<h3 id="running-multiple-versions-in-production">Running multiple versions in Production</h3>
<p>Occasionally, REST APIs and other backend services must run multiple versions in Production for backward compatibility.  For the recommendations below, my example application has three versions: v1.x, v2.x, and v3.x.</p>
<ul>
<li>The main branch represents the latest version (v3.x)</li>
<li>Separate branches for each version (v1.x and v2.x)</li>
<li>Each version branch is treated like a “trunk”
<ul>
<li>Changes are made in short-lived branches that were branched off the version branch.</li>
<li>Merging into those version branches requires a pull request.</li>
</ul>
</li>
</ul>
<p>Within Octopus, you’ll only need one lifecycle:</p>
<ul>
<li>Release: Test → Staging → Production</li>
</ul>
<p>But the Project will have four Channels:</p>
<ul>
<li>Default
<ul>
<li>Uses an ephemeral environment</li>
<li>Build artifacts require a pre-release tag and can only come from non-version or main branches.</li>
</ul>
</li>
<li>vCurrent
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the main branch</li>
<li>Build artifacts version must be &#x3C;= 3.x</li>
</ul>
</li>
<li>V2
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the v2 branch</li>
<li>Build artifacts version must be between 2 and 2.999999</li>
</ul>
</li>
<li>V1
<ul>
<li>Uses release lifecycle</li>
<li>Build artifacts cannot have a pre-release tag</li>
<li>Build artifacts must come from the v1 branch</li>
<li>Build artifacts version must be between 1 and 1.999999</li>
</ul>
</li>
</ul>
<p>Ephemeral environments make this significantly easier, as you can spin up a sandbox for a change for any version and verify it before merging into the appropriate branch.  If you cannot use ephemeral environments, I’d recommend setting up a couple of static development environments and configuring a lifecycle that lets you deploy to any of them.  The downside is that a person must determine which static development environment to use.</p>
<h2 id="conclusion">Conclusion</h2>
<p>A Production incident is not the time to improvise a deployment pipeline.  Steps shouldn’t be skipped to “go faster.”  All too often, a “simple change” that isn’t properly vetted causes a bigger issue.  But that is essentially what a hotfix pipeline is designed to do.  It skips important steps in the normal Production deployment pipeline to save time.  To resolve a Production incident, you want a well-tested and well-used pipeline, so you know you aren’t introducing even more risk.  The time required to create and improve a hotfix pipeline is better spent improving the Production deployment pipeline.  Once the Production deployment pipeline takes less than an hour to be ready for deployment to Production, the need for a hotfix pipeline will be all but eliminated.</p>]]></content>
    </entry>
</feed>