Stylized image of two people doing maintenance work on a DevOps infinity symbol with a cog and timer at its center.

From copy-paste to platform thinking

How we modernized our microsite delivery

Matthew Allford
Matthew Allford

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.

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.

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.

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.

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.

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.

The way things were

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.

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.

The copy-paste loop

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:

  1. Spellcheck (branch)
  2. Markdown linting (branch)
  3. Build, test, package, push the package to Octopus, and trigger the release (branch)
  4. Build, test, package, push the package to Octopus, and trigger the release (main)

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.

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.

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.

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.

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.

Deployment times that test our patience

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.

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.

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.

That workaround was a new function in the build - allow me to introduce pnpm thin. Rather than deploying the full site on every run, pnpm thin 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.

An issue with pnpm thin 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.

It’s a good example of how workarounds can quietly constrain future decisions. The pnpm thin 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.

The fading static staging environment

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.

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:

Please don’t use the staging environment today, I’m testing out some changes I’ve been working on. Thanks heaps!

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.

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.

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.

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.

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.

The effort to preview changes

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:

  1. Knowing you were all clear to use the staging environment without annoying someone else who may be testing/reviewing their change
  2. Knowing which Octopus Deploy instance and project to go to
  3. Having access to the target project in Octopus Deploy
  4. Knowing how to perform a deployment to the staging environment
  5. Knowing the URL for the staging environment to share with others

Technical contributors would often just run the site locally on their machine. Clone the repo, install dependencies, run pnpm dev, 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.

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.

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 introduced ephemeral environments in Octopus.

Organically growing infrastructure

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.

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.

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.

What we changed

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.

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.

The copy-paste problem across both GitHub Actions and Octopus was solved by leaning into templating. Twenty workflow files spread across five repositories were consolidated into a set of reusable GitHub workflows. 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.

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.

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.

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.

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.

Wrapping up

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.

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.

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.

Happy deployments!

Matthew Allford

A technologist and content creator who combines 15+ years of deep technical experience with a passion for community education.

Related posts