<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <title>Polar Squad Blog</title>
    <link>https://www.polarsquad.com/blog</link>
    <description>Sharing our views on what’s going on in DevOps, what we’ve learned working in DevOps and what we think is the future of DevOps.</description>
    <language>en</language>
    <pubDate>Wed, 03 Jun 2026 11:11:09 GMT</pubDate>
    <dc:date>2026-06-03T11:11:09Z</dc:date>
    <dc:language>en</dc:language>
    <item>
      <title>Check your Kubernetes deployments! — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/check-your-kubernetes-deployments</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/check-your-kubernetes-deployments" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/chuttersnap-kyCNGGKCvyw-unsplash-1.webp" alt="Check your Kubernetes deployments! — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-hidden
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;&lt;em&gt;Here’s a post about deploying applications to Kubernetes and associated things to take into account. This post was originally published in 2019, but is good stuff today –&amp;nbsp;if you encounter something that should be updated, please let us know!&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;&lt;em&gt;—&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;When writing and setting up software, it’s natural for us to focus on just the happy path. After all, that’s the path that everyone wants. Unfortunately, software can fail quite often, so we need to give the unhappy paths some attention as well.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Kubernetes is no exception here. When deploying software to Kubernetes, it’s easy to focus on the happy path without properly checking that everything went as expected. In this article, I’ll talk about what is typically missing when deploying applications to Kubernetes, and demonstrate how to improve it.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Typical flow for deploying applications to Kubernetes&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In Kubernetes, most service-style applications use&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"&gt;&lt;span style="text-decoration: underline;"&gt;Deployments&lt;/span&gt;&lt;/a&gt;&amp;nbsp;to run applications on Kubernetes. Using Deployments, you can describe how to run your application container as a Pod in Kubernetes and how many replicas of the application to run. Kubernetes will then take care of running as many replicas as specified.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Here’s an example deployment manifest in YAML format for running three instances of a simple hello world web app:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;apps/v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;Deployment&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; labels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; replicas:&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt;
&lt;span class="hljs-attr"&gt; selector:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; matchLabels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; template:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; metadata:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; labels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; spec:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; containers:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; - image:&lt;/span&gt; &lt;span class="hljs-string"&gt;polarsquad/hello-world-app:master&lt;/span&gt;
&lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;hello-world&lt;/span&gt;
&lt;span class="hljs-attr"&gt; ports:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; - containerPort:&lt;/span&gt; &lt;span class="hljs-number"&gt;3000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;One of the key features of Deployments is how it manages application updates. By default, updating the Deployment manifest in Kubernetes causes the application to be updated in a rolling fashion. This way you’ll have the previous version of the deployment running while the new one is brought up. In the Deployment manifest, you can specify how many replicas to bring up and down at once during updates.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For example, we can add a rolling update strategy to the spec section of the manifest where we bring one replica up at a time, and make sure there are no missing healthy replicas at any point during the upgrade.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;spec&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
 &lt;span class="hljs-attr"&gt;strategy&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
   &lt;span class="hljs-attr"&gt;type&lt;/span&gt;: &lt;span class="hljs-string"&gt;RollingUpdate&lt;/span&gt;
   &lt;span class="hljs-attr"&gt;rollingUpdate&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
     &lt;span class="hljs-attr"&gt;maxUnavailable&lt;/span&gt;: &lt;span class="hljs-string"&gt;0&lt;/span&gt;
     &lt;span class="hljs-attr"&gt;maxSurge&lt;/span&gt;: &lt;span class="hljs-string"&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;The update is usually performed either by&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/"&gt;&lt;span style="text-decoration: underline;"&gt;patching the manifest directly&lt;/span&gt;&lt;/a&gt;&amp;nbsp;or by&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/"&gt;&lt;span style="text-decoration: underline;"&gt;applying a full Deployment manifest from the file system&lt;/span&gt;&lt;/a&gt;. From Kubernetes’ point of view, it makes no difference. If the contents of the manifest update are valid, then Kubernetes will happily accept the update. Most of the time, an application update mostly contains a change in the container image tag or some of the environment variable configurations you might have.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;To automate the process, you might choose to deploy your app in your CI pipeline using kubectl.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs css"&gt;&lt;span class="hljs-selector-tag"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;apply&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;-f&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;deployment&lt;/span&gt;&lt;span class="hljs-selector-class"&gt;.yaml&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;So now you have a pattern and a flow for getting your app to run on Kubernetes. Everything good, right? Unfortunately,&amp;nbsp;&lt;a href="https://www.youtube.com/watch?v=GM-e46xdcUo"&gt;&lt;span style="text-decoration: underline;"&gt;no!&lt;/span&gt;&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;It’s a great start, but it’s usually not enough. Applying a deployment to Kubernetes finishes once Kubernetes has accepted the deployment, not when it has finished. Kubectl apply does not verify that your application even starts. This deployment flow is demonstrated in the picture below.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In order to properly check that the update proceeds as expected, we need assistance from another kubectl command.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Rollout to the rescue!&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;This is where kubectl’s rollout command becomes handy! We can use it to check how our deployment is doing.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;By default, the command waits until all of the Pods in the deployment have been started successfully. When the deployment succeeds, the command exits with return code zero to indicate success.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;$ kubectl rollout status deployment myapp
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;2&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; successfully rolled out&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;If the deployment fails, the command exits with a non-zero return code to indicate a failure.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;If you’re already using kubectl to deploy applications from CI, using rollout to verify your deployment in CI will be a breeze. By running rollout directly after deploying changes, we can block the CI task from completing until the application deployment finishes. We can then use the return code from rollout to either pass or fail the CI task.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;So far so good, but how does Kubernetes know when an application deployment succeeds?&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Readiness probes and deadlines&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In order for Kubernetes to know when an application is ready, it needs some help from the application. Kubernetes uses&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/"&gt;&lt;span style="text-decoration: underline;"&gt;readiness probes&lt;/span&gt;&lt;/a&gt;&amp;nbsp;to examine how the application is doing. Once an application instance starts responding to the readiness probe with a positive response, the instance is considered ready for use.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For web services, the most simple implementation is an&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#define-a-liveness-http-request"&gt;&lt;span style="text-decoration: underline;"&gt;HTTP GET endpoint&lt;/span&gt;&lt;/a&gt;&amp;nbsp;that starts responding with a 200 OK status code when the server starts. In our hello world app, we could consider the app healthy when the index page can be loaded. Here’s the readiness probe configuration for our hello world app:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;readinessProbe&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;httpGet&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;path&lt;/span&gt;: &lt;span class="hljs-string"&gt;/&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;port&lt;/span&gt;: &lt;span class="hljs-string"&gt;3000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;A more sophisticated implementation of the health check might perform some background checks to verify that everything is ready for the application to serve requests, and serve that information through a dedicated health endpoint (e.g. /health or /ready). It’s up to the application developers to figure out when the application is ready, and how to respond back to probes.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Readiness probes tell Kubernetes when an application is ready, but not if the application will ever become ready. If the application keeps failing, it may never respond with a positive response to Kubernetes. How does Kubernetes then know when the deployment is going nowhere?&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In our Deployment manifest, we can specify how long Kubernetes should&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progress-deadline-seconds"&gt;&lt;span style="text-decoration: underline;"&gt;wait for deployment to progress until it considers the deployment to have failed&lt;/span&gt;&lt;/a&gt;. If the deployment doesn’t proceed until the deadline is met, Kubernetes marks the deployment status as failed, which the rollout status command will be able to pick up.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;$ kubectl rollout status deployment myapp
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;1&lt;/span&gt; out &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; replicas have been updated…
error: deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; exceeded its progress deadline&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;What makes the deadline fantastic is that if the deployment manages to proceed within the deadline, Kubernetes will reset the deadline timer, and start waiting again. This way you don’t have to estimate a deadline for the entire deployment, but just a single instance of the application.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For example, if we set a deadline of 30 seconds, Kubernetes will wait 30 seconds for the application to become ready. If the application becomes ready, Kubernetes will wait another 30 seconds for the next instance to become ready.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Scripting automated rollback&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Currently, when a deployment fails in Kubernetes, the deployment process stops, but the pods from the failed deployment are kept around. On deployment failure, your environment may contain pods from both the old and new deployments.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;To get back to a stable, working state, we can use the&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-back-a-deployment"&gt;&lt;span style="text-decoration: underline;"&gt;rollout undo&lt;/span&gt;&lt;/a&gt;&amp;nbsp;command to bring back the working pods and clean up the failed deployment.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs ruby"&gt;$ kubectl rollout undo deployment myapp
deployment.extensions/myapp
$ kubectl rollout status deployment myapp
deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; successfully rolled out&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Awesome! Now that we have a way to determine when our deployments fail and how to revert the deployment, we can automate the deployment and rollback process with a simple shell script.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;apply -f myapp.yaml&lt;/span&gt;
&lt;span class="hljs-attr"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;! kubectl rollout status deployment myapp; then&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;rollout undo deployment myapp&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;rollout status deployment myapp&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;exit&lt;/span&gt; &lt;span class="hljs-string"&gt;1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;We first rollout the changes, and then immediately wait for the rollout status. If the rollout succeeds, we continue normally. If it fails, we undo the deployment, wait for undo to finish, and report back a failure with exit code 1. This flow is demonstrated in the picture below.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;There is one major caveat not addressed in the script: kubectl commands may fail because of the network conditions! The script above doesn’t account for any connection failures, which means that the script may interpret a network failure in the rollout command as a failed deployment. Kubectl does retry retriable errors automatically, but it will fail eventually if the Kubernetes API is not available for a long period of time.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Conclusion&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In this article, I’ve talked about the typical deployment flow used with service style applications in Kubernetes, and how it’s not enough to ensure safe deployments. I’ve presented a way to extend the deployment flow with status checks and an automated rollback procedure.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;One area I haven’t covered is how the same checks and automated rollback are achieved in&amp;nbsp;&lt;a href="https://helm.sh/"&gt;&lt;span style="text-decoration: underline;"&gt;Helm&lt;/span&gt;&lt;/a&gt;. Helm deployments have their own additional quirks when it comes to detecting failed deployments. I’ve covered these quirks in &lt;a href="https://polarsquad.com/blog/check-your-helm-deployments"&gt;this article&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;I’ve published the code examples in a&amp;nbsp;&lt;a href="https://gist.github.com/jkpl/1d6b5dfbc6660021557ad1e56029e48a"&gt;&lt;span style="text-decoration: underline;"&gt;GitHub Gist&lt;/span&gt;&lt;/a&gt;. Thanks for reading!&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block html-block sqs-block-html"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="sqs-html-content"&gt; 
       &lt;p class="" style="white-space: pre-wrap;"&gt;Jaakko is the CTO of Polar Squad, a software developer turned SRE / DevOps Consultant. He is dedicated to bringing people together to deliver exactly what is needed in a rapid, reliable, and stress-free way. Given a bit of time, he’s probably able to solve &lt;em&gt;all&lt;/em&gt; the problems.&lt;/p&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/check-your-kubernetes-deployments" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/chuttersnap-kyCNGGKCvyw-unsplash-1.webp" alt="Check your Kubernetes deployments! — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-hidden
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;&lt;em&gt;Here’s a post about deploying applications to Kubernetes and associated things to take into account. This post was originally published in 2019, but is good stuff today –&amp;nbsp;if you encounter something that should be updated, please let us know!&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;&lt;em&gt;—&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;When writing and setting up software, it’s natural for us to focus on just the happy path. After all, that’s the path that everyone wants. Unfortunately, software can fail quite often, so we need to give the unhappy paths some attention as well.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Kubernetes is no exception here. When deploying software to Kubernetes, it’s easy to focus on the happy path without properly checking that everything went as expected. In this article, I’ll talk about what is typically missing when deploying applications to Kubernetes, and demonstrate how to improve it.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Typical flow for deploying applications to Kubernetes&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In Kubernetes, most service-style applications use&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"&gt;&lt;span style="text-decoration: underline;"&gt;Deployments&lt;/span&gt;&lt;/a&gt;&amp;nbsp;to run applications on Kubernetes. Using Deployments, you can describe how to run your application container as a Pod in Kubernetes and how many replicas of the application to run. Kubernetes will then take care of running as many replicas as specified.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Here’s an example deployment manifest in YAML format for running three instances of a simple hello world web app:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;apps/v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;Deployment&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; labels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; replicas:&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt;
&lt;span class="hljs-attr"&gt; selector:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; matchLabels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; template:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; metadata:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; labels:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; app:&lt;/span&gt; &lt;span class="hljs-string"&gt;myapp&lt;/span&gt;
&lt;span class="hljs-attr"&gt; spec:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; containers:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; - image:&lt;/span&gt; &lt;span class="hljs-string"&gt;polarsquad/hello-world-app:master&lt;/span&gt;
&lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;hello-world&lt;/span&gt;
&lt;span class="hljs-attr"&gt; ports:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; - containerPort:&lt;/span&gt; &lt;span class="hljs-number"&gt;3000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;One of the key features of Deployments is how it manages application updates. By default, updating the Deployment manifest in Kubernetes causes the application to be updated in a rolling fashion. This way you’ll have the previous version of the deployment running while the new one is brought up. In the Deployment manifest, you can specify how many replicas to bring up and down at once during updates.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For example, we can add a rolling update strategy to the spec section of the manifest where we bring one replica up at a time, and make sure there are no missing healthy replicas at any point during the upgrade.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;spec&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
 &lt;span class="hljs-attr"&gt;strategy&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
   &lt;span class="hljs-attr"&gt;type&lt;/span&gt;: &lt;span class="hljs-string"&gt;RollingUpdate&lt;/span&gt;
   &lt;span class="hljs-attr"&gt;rollingUpdate&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
     &lt;span class="hljs-attr"&gt;maxUnavailable&lt;/span&gt;: &lt;span class="hljs-string"&gt;0&lt;/span&gt;
     &lt;span class="hljs-attr"&gt;maxSurge&lt;/span&gt;: &lt;span class="hljs-string"&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;The update is usually performed either by&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/"&gt;&lt;span style="text-decoration: underline;"&gt;patching the manifest directly&lt;/span&gt;&lt;/a&gt;&amp;nbsp;or by&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/"&gt;&lt;span style="text-decoration: underline;"&gt;applying a full Deployment manifest from the file system&lt;/span&gt;&lt;/a&gt;. From Kubernetes’ point of view, it makes no difference. If the contents of the manifest update are valid, then Kubernetes will happily accept the update. Most of the time, an application update mostly contains a change in the container image tag or some of the environment variable configurations you might have.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;To automate the process, you might choose to deploy your app in your CI pipeline using kubectl.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs css"&gt;&lt;span class="hljs-selector-tag"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;apply&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;-f&lt;/span&gt; &lt;span class="hljs-selector-tag"&gt;deployment&lt;/span&gt;&lt;span class="hljs-selector-class"&gt;.yaml&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;So now you have a pattern and a flow for getting your app to run on Kubernetes. Everything good, right? Unfortunately,&amp;nbsp;&lt;a href="https://www.youtube.com/watch?v=GM-e46xdcUo"&gt;&lt;span style="text-decoration: underline;"&gt;no!&lt;/span&gt;&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;It’s a great start, but it’s usually not enough. Applying a deployment to Kubernetes finishes once Kubernetes has accepted the deployment, not when it has finished. Kubectl apply does not verify that your application even starts. This deployment flow is demonstrated in the picture below.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In order to properly check that the update proceeds as expected, we need assistance from another kubectl command.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Rollout to the rescue!&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;This is where kubectl’s rollout command becomes handy! We can use it to check how our deployment is doing.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;By default, the command waits until all of the Pods in the deployment have been started successfully. When the deployment succeeds, the command exits with return code zero to indicate success.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;$ kubectl rollout status deployment myapp
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;2&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; updated replicas are available…
deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; successfully rolled out&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;If the deployment fails, the command exits with a non-zero return code to indicate a failure.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;If you’re already using kubectl to deploy applications from CI, using rollout to verify your deployment in CI will be a breeze. By running rollout directly after deploying changes, we can block the CI task from completing until the application deployment finishes. We can then use the return code from rollout to either pass or fail the CI task.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;So far so good, but how does Kubernetes know when an application deployment succeeds?&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Readiness probes and deadlines&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In order for Kubernetes to know when an application is ready, it needs some help from the application. Kubernetes uses&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/"&gt;&lt;span style="text-decoration: underline;"&gt;readiness probes&lt;/span&gt;&lt;/a&gt;&amp;nbsp;to examine how the application is doing. Once an application instance starts responding to the readiness probe with a positive response, the instance is considered ready for use.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For web services, the most simple implementation is an&amp;nbsp;&lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#define-a-liveness-http-request"&gt;&lt;span style="text-decoration: underline;"&gt;HTTP GET endpoint&lt;/span&gt;&lt;/a&gt;&amp;nbsp;that starts responding with a 200 OK status code when the server starts. In our hello world app, we could consider the app healthy when the index page can be loaded. Here’s the readiness probe configuration for our hello world app:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;readinessProbe&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;httpGet&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;path&lt;/span&gt;: &lt;span class="hljs-string"&gt;/&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;port&lt;/span&gt;: &lt;span class="hljs-string"&gt;3000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;A more sophisticated implementation of the health check might perform some background checks to verify that everything is ready for the application to serve requests, and serve that information through a dedicated health endpoint (e.g. /health or /ready). It’s up to the application developers to figure out when the application is ready, and how to respond back to probes.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Readiness probes tell Kubernetes when an application is ready, but not if the application will ever become ready. If the application keeps failing, it may never respond with a positive response to Kubernetes. How does Kubernetes then know when the deployment is going nowhere?&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In our Deployment manifest, we can specify how long Kubernetes should&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progress-deadline-seconds"&gt;&lt;span style="text-decoration: underline;"&gt;wait for deployment to progress until it considers the deployment to have failed&lt;/span&gt;&lt;/a&gt;. If the deployment doesn’t proceed until the deadline is met, Kubernetes marks the deployment status as failed, which the rollout status command will be able to pick up.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;$ kubectl rollout status deployment myapp
Waiting &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; rollout to finish: &lt;span class="hljs-number"&gt;1&lt;/span&gt; out &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-number"&gt;3&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; replicas have been updated…
error: deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; exceeded its progress deadline&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;What makes the deadline fantastic is that if the deployment manages to proceed within the deadline, Kubernetes will reset the deadline timer, and start waiting again. This way you don’t have to estimate a deadline for the entire deployment, but just a single instance of the application.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;For example, if we set a deadline of 30 seconds, Kubernetes will wait 30 seconds for the application to become ready. If the application becomes ready, Kubernetes will wait another 30 seconds for the next instance to become ready.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Scripting automated rollback&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Currently, when a deployment fails in Kubernetes, the deployment process stops, but the pods from the failed deployment are kept around. On deployment failure, your environment may contain pods from both the old and new deployments.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;To get back to a stable, working state, we can use the&amp;nbsp;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#rolling-back-a-deployment"&gt;&lt;span style="text-decoration: underline;"&gt;rollout undo&lt;/span&gt;&lt;/a&gt;&amp;nbsp;command to bring back the working pods and clean up the failed deployment.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs ruby"&gt;$ kubectl rollout undo deployment myapp
deployment.extensions/myapp
$ kubectl rollout status deployment myapp
deployment &lt;span class="hljs-string"&gt;"myapp"&lt;/span&gt; successfully rolled out&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;Awesome! Now that we have a way to determine when our deployments fail and how to revert the deployment, we can automate the deployment and rollback process with a simple shell script.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;apply -f myapp.yaml&lt;/span&gt;
&lt;span class="hljs-attr"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;! kubectl rollout status deployment myapp; then&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;rollout undo deployment myapp&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;kubectl&lt;/span&gt; &lt;span class="hljs-string"&gt;rollout status deployment myapp&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;exit&lt;/span&gt; &lt;span class="hljs-string"&gt;1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;We first rollout the changes, and then immediately wait for the rollout status. If the rollout succeeds, we continue normally. If it fails, we undo the deployment, wait for undo to finish, and report back a failure with exit code 1. This flow is demonstrated in the picture below.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;There is one major caveat not addressed in the script: kubectl commands may fail because of the network conditions! The script above doesn’t account for any connection failures, which means that the script may interpret a network failure in the rollout command as a failed deployment. Kubectl does retry retriable errors automatically, but it will fail eventually if the Kubernetes API is not available for a long period of time.&lt;/p&gt; 
     &lt;h2 style="white-space: pre-wrap;"&gt;Conclusion&lt;/h2&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;In this article, I’ve talked about the typical deployment flow used with service style applications in Kubernetes, and how it’s not enough to ensure safe deployments. I’ve presented a way to extend the deployment flow with status checks and an automated rollback procedure.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;One area I haven’t covered is how the same checks and automated rollback are achieved in&amp;nbsp;&lt;a href="https://helm.sh/"&gt;&lt;span style="text-decoration: underline;"&gt;Helm&lt;/span&gt;&lt;/a&gt;. Helm deployments have their own additional quirks when it comes to detecting failed deployments. I’ve covered these quirks in &lt;a href="https://polarsquad.com/blog/check-your-helm-deployments"&gt;this article&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space: pre-wrap;"&gt;I’ve published the code examples in a&amp;nbsp;&lt;a href="https://gist.github.com/jkpl/1d6b5dfbc6660021557ad1e56029e48a"&gt;&lt;span style="text-decoration: underline;"&gt;GitHub Gist&lt;/span&gt;&lt;/a&gt;. Thanks for reading!&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black);"&gt;&lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block html-block sqs-block-html"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="sqs-html-content"&gt; 
       &lt;p class="" style="white-space: pre-wrap;"&gt;Jaakko is the CTO of Polar Squad, a software developer turned SRE / DevOps Consultant. He is dedicated to bringing people together to deliver exactly what is needed in a rapid, reliable, and stress-free way. Given a bit of time, he’s probably able to solve &lt;em&gt;all&lt;/em&gt; the problems.&lt;/p&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcheck-your-kubernetes-deployments&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Wed, 03 Jun 2026 11:11:09 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/check-your-kubernetes-deployments</guid>
      <dc:date>2026-06-03T11:11:09Z</dc:date>
      <dc:creator>Polar Squad</dc:creator>
    </item>
    <item>
      <title>Cloud cost and resource optimization: How Polar Squad can save you money while reducing your carbon footprint — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/cloud-cost-and-resource-optimization</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/cloud-cost-and-resource-optimization" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/citynature.webp" alt="Cloud cost and resource optimization: How Polar Squad can save you money while reducing your carbon footprint — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In these challenging times, many businesses from the travel sector to supply chain and retail platforms are looking to reduce costs while ensuring their actions are sustainable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Yet when it comes to tech, the actual cloud is often overlooked as a source of emissions – and major potential savings.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“You wouldn’t leave your faucets open when you leave home, or your car running when you’re not driving it, but many businesses treat their clouds like limitless resources,” says Polar Squad DevOps consultant &lt;strong&gt;Katariina (Kata) Vakkuri.&lt;/strong&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A &lt;a href="https://www.cell.com/patterns/fulltext/S2666-3899(21)00188-4?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS2666389921001884%3Fshowall%3Dtrue"&gt;recent peer-reviewed study&lt;/a&gt; suggests that global emissions from ICT are as high as 3.9 per cent, exceeding the emissions of commercial aviation, which is estimated to be about 2.4 per cent.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Perhaps the word ‘cloud’ conjures up something eco-friendly,” says Vakkuri, “but the reality is far from that. Major resources such as upkeep of buildings, heating and cooling go into keeping your data in the cloud. We need to educate people about the environmental impacts of data storage and offer viable solutions.”&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;DevOps solutions&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Getting DevOps support pays for itself many times over, as one of its main goals is improving existing systems and structures.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Instead of just building new systems, which is what many consultancies do, we often help to improve and optimize what already exists,” says &lt;strong&gt;Tero Kiminki&lt;/strong&gt;, a senior Polar Squad DevOps consultant.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;DevOps cloud optimization can save up to 10 per cent of costs a month, which can add up to a significant amount of money, according to Kiminki. Large companies’ monthly cloud costs can run to several hundred thousand euros.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of the current challenges is that many businesses have outsourced data management and platform knowledge, says Kiminki. This means no one is responsible for staying on top of costs and sustainability issues, let alone basic management.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“As not all partners building solutions know how to get the most out of the cloud and there’s often a lack of knowledge about the cloud within an organization, the result can be non-optimal set-ups that waste time, money and the environment,” he says.&amp;nbsp;&amp;nbsp;&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Polar Squad to the rescue&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This is where Polar Squad steps in by helping clients optimize both old and new and get back their knowledge (in the form of data).&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When companies have outsourced their knowledge, it’s not always a priority for a partner to question how the system has been built. It’s easy to take shortcuts and just follow through on someone else’s orders. Polar Squad helps clients to challenge solutions set up or proposed by others that may not be optimal or even in the client’s best interests.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“We also help clients to get forgotten or lost knowledge back and raise their awareness for the future so they can utilize cost and sustainable solutions from Day One going forward,” says Kiminki.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Knowing what you’re running in the cloud and understanding your business also frees up resources for developers, who can then focus on developing and creating new things,” he adds.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When applications are done correctly from the beginning, it reduces the need for optimization work which can take away resources from future projects. According to &lt;strong&gt;Erno Aapa&lt;/strong&gt;, Polar Squad co-founder and COO, when implementing FinOps (financial operations) for managing cloud costs, “it’s important &lt;em&gt;not&lt;/em&gt; to kill innovation, for example, by not allowing testing of new services. When there’s good communication and all the costs are visible to the teams, everyone can make better financial choices.”&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You can also learn more about optimising&lt;a href="https://polarsquad.com/cloud-optimization"&gt; cloud infra costs&lt;/a&gt; with this quick checklist we compiled with our friends at NetApp.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;—&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you think you might benefit from evaluating the possibilities to save costs in your cloud environment, &lt;a href="https://www.polarsquad.com/contact"&gt;contact us!&lt;/a&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/cloud-cost-and-resource-optimization" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/citynature.webp" alt="Cloud cost and resource optimization: How Polar Squad can save you money while reducing your carbon footprint — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In these challenging times, many businesses from the travel sector to supply chain and retail platforms are looking to reduce costs while ensuring their actions are sustainable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Yet when it comes to tech, the actual cloud is often overlooked as a source of emissions – and major potential savings.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“You wouldn’t leave your faucets open when you leave home, or your car running when you’re not driving it, but many businesses treat their clouds like limitless resources,” says Polar Squad DevOps consultant &lt;strong&gt;Katariina (Kata) Vakkuri.&lt;/strong&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A &lt;a href="https://www.cell.com/patterns/fulltext/S2666-3899(21)00188-4?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS2666389921001884%3Fshowall%3Dtrue"&gt;recent peer-reviewed study&lt;/a&gt; suggests that global emissions from ICT are as high as 3.9 per cent, exceeding the emissions of commercial aviation, which is estimated to be about 2.4 per cent.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Perhaps the word ‘cloud’ conjures up something eco-friendly,” says Vakkuri, “but the reality is far from that. Major resources such as upkeep of buildings, heating and cooling go into keeping your data in the cloud. We need to educate people about the environmental impacts of data storage and offer viable solutions.”&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;DevOps solutions&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Getting DevOps support pays for itself many times over, as one of its main goals is improving existing systems and structures.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Instead of just building new systems, which is what many consultancies do, we often help to improve and optimize what already exists,” says &lt;strong&gt;Tero Kiminki&lt;/strong&gt;, a senior Polar Squad DevOps consultant.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;DevOps cloud optimization can save up to 10 per cent of costs a month, which can add up to a significant amount of money, according to Kiminki. Large companies’ monthly cloud costs can run to several hundred thousand euros.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of the current challenges is that many businesses have outsourced data management and platform knowledge, says Kiminki. This means no one is responsible for staying on top of costs and sustainability issues, let alone basic management.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“As not all partners building solutions know how to get the most out of the cloud and there’s often a lack of knowledge about the cloud within an organization, the result can be non-optimal set-ups that waste time, money and the environment,” he says.&amp;nbsp;&amp;nbsp;&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Polar Squad to the rescue&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This is where Polar Squad steps in by helping clients optimize both old and new and get back their knowledge (in the form of data).&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When companies have outsourced their knowledge, it’s not always a priority for a partner to question how the system has been built. It’s easy to take shortcuts and just follow through on someone else’s orders. Polar Squad helps clients to challenge solutions set up or proposed by others that may not be optimal or even in the client’s best interests.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“We also help clients to get forgotten or lost knowledge back and raise their awareness for the future so they can utilize cost and sustainable solutions from Day One going forward,” says Kiminki.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;“Knowing what you’re running in the cloud and understanding your business also frees up resources for developers, who can then focus on developing and creating new things,” he adds.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When applications are done correctly from the beginning, it reduces the need for optimization work which can take away resources from future projects. According to &lt;strong&gt;Erno Aapa&lt;/strong&gt;, Polar Squad co-founder and COO, when implementing FinOps (financial operations) for managing cloud costs, “it’s important &lt;em&gt;not&lt;/em&gt; to kill innovation, for example, by not allowing testing of new services. When there’s good communication and all the costs are visible to the teams, everyone can make better financial choices.”&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You can also learn more about optimising&lt;a href="https://polarsquad.com/cloud-optimization"&gt; cloud infra costs&lt;/a&gt; with this quick checklist we compiled with our friends at NetApp.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;—&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you think you might benefit from evaluating the possibilities to save costs in your cloud environment, &lt;a href="https://www.polarsquad.com/contact"&gt;contact us!&lt;/a&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcloud-cost-and-resource-optimization&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Wed, 03 Jun 2026 10:52:09 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/cloud-cost-and-resource-optimization</guid>
      <dc:date>2026-06-03T10:52:09Z</dc:date>
      <dc:creator>Polar Squad</dc:creator>
    </item>
    <item>
      <title>Craftsperson's Guide to GitHub Actions #3: Building and Releasing — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-3-building-and-releasing</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-3-building-and-releasing" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Craftspersons%20Guide%20to%20GitHub%20Actions%20part%203%20Building%20and%20Releasing.webp" alt="Craftsperson's Guide to GitHub Actions #3: Building and Releasing — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the previous chapter, we gained confidence through comprehensive testing, including unit tests, property-based tests, and mutation tests. Our action quality is rock-solid.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But quality means nothing until the product reaches users. An action that only exists on your machine is just an expensive science experiment. In this final chapter, we'll build for production and create an automated release pipeline that safely delivers your action to the world.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Building Your Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For JavaScript-based GitHub Actions, we need a build process. GitHub doesn't compile or bundle the code before execution, which means we must ship a ready-to-run artefact.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This means transpiling TypeScript to JavaScript, bundling all dependencies, and checking the build artefact into Git. Yes, you read that correctly. Unlike typical projects where you ignore your build artefacts from version control, GitHub Actions requires you to commit them to the repository.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Use whichever bundler you prefer. The &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; utilises &lt;a href="https://bun.sh/"&gt;&lt;span style="text-decoration:underline"&gt;Bun&lt;/span&gt;&lt;/a&gt; for its excellent bundling feature, but esbuild or Rolldown can also be used with similar results.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As discussed in Chapter 1, keep your action entry point separate from source files and tests. This separation makes configuring bundling easier.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The key is defining a reproducible build command that works identically everywhere — on your laptop, on your colleague's machine, in GitHub Actions. I use &lt;a href="https://taskfile.dev/"&gt;&lt;span style="text-decoration:underline"&gt;Taskfile&lt;/span&gt;&lt;/a&gt; for orchestration, but npm scripts, Makefiles, or shell scripts are equally valid choices. Pick your tool; just make it consistent.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;build&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;desc&lt;/span&gt;: &lt;span class="hljs-string"&gt;Build GitHub Action&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;sources&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;bin/**/*.ts&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;src/**/*.ts&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;generates&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;dist/index.js&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;cmd&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;bun&lt;/span&gt; &lt;span class="hljs-string"&gt;build bin/index.ts&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;--production&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--target&lt;/span&gt; &lt;span class="hljs-string"&gt;node&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--outdir&lt;/span&gt; &lt;span class="hljs-string"&gt;dist&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--format&lt;/span&gt; &lt;span class="hljs-string"&gt;esm&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This builds our TypeScript entry point into a minified Node.js script in modern ESM format.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Running task build produces a 510 KB bundle containing all dependencies. That might seem large for a simple ROT-13 action, but GitHub Actions runners download it in no time.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The hardest part is to remember to commit the build artefact. Being human — and thus forgetful — we automate this with a pre-commit hook using &lt;a href="https://typicode.github.io/husky/"&gt;&lt;span style="text-decoration:underline"&gt;Husky&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;task&lt;/span&gt; &lt;span class="hljs-string"&gt;-p lint build&lt;/span&gt;
&lt;span class="hljs-attr"&gt;git&lt;/span&gt; &lt;span class="hljs-string"&gt;add dist README.md&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This hook runs linting and building in parallel, then stages both the dist directory and &lt;a href="http://readme.md"&gt;&lt;span style="text-decoration:underline"&gt;README.md&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Why the README? We generate action documentation during the build using &lt;a href="https://github.com/npalm/action-docs"&gt;&lt;span style="text-decoration:underline"&gt;action-docs&lt;/span&gt;&lt;/a&gt;, keeping documentation synchronised with code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Building and committing is just the first step. Now we need to verify the action actually works and release it safely.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Trust, But Verify the Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our CI/CD pipeline runs the same tests you run locally: unit tests, property-based tests, and mutation tests. But that's not enough.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Despite covering a lot of ground with existing tests, we still need to verify the action works in its actual runtime environment: GitHub Actions. Let's create an acceptance testing workflow. It's verbose, so we'll break it down piece by piece.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs http"&gt;&lt;span class="hljs-attribute"&gt;name&lt;/span&gt;: Acceptance Tests
  
&lt;span class="hljs-attribute"&gt;on&lt;/span&gt;:  
  &lt;span class="hljs-attribute"&gt;pull_request&lt;/span&gt;:  
    &lt;span class="hljs-attribute"&gt;branches&lt;/span&gt;: [main]  
  &lt;span class="hljs-attribute"&gt;push&lt;/span&gt;:  
    &lt;span class="hljs-attribute"&gt;branches&lt;/span&gt;: [main]

&lt;span class="http"&gt;&lt;span class="hljs-attribute"&gt;env&lt;/span&gt;: &lt;span class="hljs-attribute"&gt;original&lt;/span&gt;: "Hello, World!" &lt;span class="hljs-attribute"&gt;transformed&lt;/span&gt;: "Uryyb, Jbeyq!" &lt;span class="yaml"&gt;&lt;span class="hljs-attr"&gt;jobs:&lt;/span&gt; &lt;span class="hljs-attr"&gt; test-unit:&lt;/span&gt; &lt;span class="hljs-comment"&gt;# Unit, property-based, and mutation tests&lt;/span&gt; &lt;span class="hljs-string"&gt;...&lt;/span&gt; &lt;span class="hljs-attr"&gt; test-local-action:&lt;/span&gt; &lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;local&lt;/span&gt; &lt;span class="hljs-string"&gt;action&lt;/span&gt; &lt;span class="hljs-attr"&gt; permissions:&lt;/span&gt; &lt;span class="hljs-attr"&gt; contents:&lt;/span&gt; &lt;span class="hljs-string"&gt;read&lt;/span&gt; &lt;span class="hljs-attr"&gt; strategy:&lt;/span&gt; &lt;span class="hljs-attr"&gt; matrix:&lt;/span&gt; &lt;span class="hljs-attr"&gt; runner:&lt;/span&gt; &lt;span class="hljs-string"&gt;[ubuntu-latest,&lt;/span&gt; &lt;span class="hljs-string"&gt;macos-latest,&lt;/span&gt; &lt;span class="hljs-string"&gt;windows-latest]&lt;/span&gt; &lt;span class="hljs-attr"&gt; runs-on:&lt;/span&gt; &lt;span class="hljs-string"&gt;$&lt;/span&gt; &lt;span class="hljs-attr"&gt; steps:&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Checkout&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;actions/checkout@v5&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;with&lt;/span&gt; &lt;span class="hljs-string"&gt;valid&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;./&lt;/span&gt; &lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;valid&lt;/span&gt; &lt;span class="hljs-attr"&gt; with:&lt;/span&gt; &lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;$&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Fail&lt;/span&gt; &lt;span class="hljs-string"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;output&lt;/span&gt; &lt;span class="hljs-string"&gt;is&lt;/span&gt; &lt;span class="hljs-string"&gt;incorrect&lt;/span&gt; &lt;span class="hljs-attr"&gt; if:&lt;/span&gt; &lt;span class="hljs-string"&gt;steps.valid.outputs.result&lt;/span&gt; &lt;span class="hljs-string"&gt;!=&lt;/span&gt; &lt;span class="hljs-string"&gt;env.transformed&lt;/span&gt; &lt;span class="hljs-attr"&gt; run:&lt;/span&gt; &lt;span class="hljs-string"&gt;| echo "::error::Expected result of transformation was '$&lt;span class="hljs-template-variable"&gt;&lt;/span&gt;', but got '$&lt;span class="hljs-template-variable"&gt;&lt;/span&gt;'" exit 1 &lt;/span&gt;&lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;with&lt;/span&gt; &lt;span class="hljs-string"&gt;empty&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;./&lt;/span&gt; &lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;invalid&lt;/span&gt; &lt;span class="hljs-attr"&gt; continue-on-error:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt; &lt;span class="hljs-attr"&gt; with:&lt;/span&gt; &lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;""&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Fail&lt;/span&gt; &lt;span class="hljs-string"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;empty&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-string"&gt;succeeds&lt;/span&gt; &lt;span class="hljs-attr"&gt; if:&lt;/span&gt; &lt;span class="hljs-string"&gt;steps.invalid.outcome&lt;/span&gt; &lt;span class="hljs-string"&gt;!=&lt;/span&gt; &lt;span class="hljs-string"&gt;'failure'&lt;/span&gt; &lt;span class="hljs-attr"&gt; run:&lt;/span&gt; &lt;span class="hljs-string"&gt;| echo "::error::Expected action to fail when given empty input, but it succeeded." exit 1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Why testing matrices? While our action is platform-agnostic, many aren't. File system operations, for instance, often work similarly on Linux and macOS but break on Windows. Developing on a single platform while mocking the entire filesystem effectively hides these issues. Thus, we catch defects before our users do by testing across many platforms.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Since we haven't released the action yet, we use relative notation to reference the repository root. Remember to check out the repository first. GitHub Actions won't do it automatically.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We test both valid input (should succeed) and invalid input (should fail). The assertion steps use conditional execution. They only run when verification fails, resulting in the workflow failing.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Semantic Versioning Done Right&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When the verification passes, it’s time to release. Unlike some release processes that feel like organising a conference, releasing GitHub Actions is refreshingly simple: we tag the verified commit and push.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;GitHub Actions recommends semantic versioning with a simple twist. Instead of one, we publish three tags for each release:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Full version: v1.2.3 (patch-level precision)&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Minor version: v1.2 (minor updates included)&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Major version: v1 (the convenient default)&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This approach lets users choose their comfort level. Want automatic updates? Use v1. Need stability? Pin to v1.2.3. The major version tag is what most users reference, and we keep it updated automatically.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's the workflow:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs sql"&gt;jobs:
 test-unit: ...
 test-local-action: ...
 
 &lt;span class="hljs-keyword"&gt;release&lt;/span&gt;:
   &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Release&lt;/span&gt;
   &lt;span class="hljs-keyword"&gt;if&lt;/span&gt;: github.event_name == &lt;span class="hljs-string"&gt;'push'&lt;/span&gt; &amp;amp;&amp;amp; github.ref == &lt;span class="hljs-string"&gt;'refs/heads/main'&lt;/span&gt;
   needs: [&lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-unit, &lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;local&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;]
   runs-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;: ubuntu-latest
   permissions:
     &lt;span class="hljs-keyword"&gt;contents&lt;/span&gt;: write
   
   steps:
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: Checkout
       uses: actions/checkout@v5  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;fetch&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;depth&lt;/span&gt;: &lt;span class="hljs-number"&gt;0&lt;/span&gt;  
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: Determine &lt;span class="hljs-keyword"&gt;next&lt;/span&gt; &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       uses: mathieudutour/github-tag-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v6&lt;span class="hljs-number"&gt;.2&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         github_token: $  
         default_bump: &lt;span class="hljs-keyword"&gt;patch&lt;/span&gt;  
         create_annotated_tag: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
         dry_run: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Release&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;if&lt;/span&gt;: steps.version.outputs.new_version != steps.version.outputs.previous_version  
       run: |  
         &lt;span class="hljs-keyword"&gt;function&lt;/span&gt; push() {  
           &lt;span class="hljs-keyword"&gt;local&lt;/span&gt; tag=&lt;span class="hljs-string"&gt;"$1"&lt;/span&gt;  
           git tag -fa &lt;span class="hljs-string"&gt;"$tag"&lt;/span&gt; -m &lt;span class="hljs-string"&gt;"Release $tag"&lt;/span&gt;  
           git push origin &lt;span class="hljs-string"&gt;"$tag"&lt;/span&gt; &lt;span class="hljs-comment"&gt;--force &lt;/span&gt;
         }  
           
         git config user.name &lt;span class="hljs-string"&gt;"$USERNAME"&lt;/span&gt;  
         git config user.email &lt;span class="hljs-string"&gt;"$EMAIL"&lt;/span&gt;  
           
         push &lt;span class="hljs-string"&gt;"$TAG"&lt;/span&gt;  
         push &lt;span class="hljs-string"&gt;"$(echo "&lt;/span&gt;$TAG&lt;span class="hljs-string"&gt;" | cut -d . -f 1)"&lt;/span&gt;  
         push &lt;span class="hljs-string"&gt;"$(echo "&lt;/span&gt;$TAG&lt;span class="hljs-string"&gt;" | cut -d . -f 1-2)"&lt;/span&gt;  
           
         gh &lt;span class="hljs-keyword"&gt;release&lt;/span&gt; &lt;span class="hljs-keyword"&gt;create&lt;/span&gt; &lt;span class="hljs-string"&gt;"$TAG"&lt;/span&gt; \
           &lt;span class="hljs-comment"&gt;--title "Release $TAG" \&lt;/span&gt;
           &lt;span class="hljs-comment"&gt;--notes "$CHANGELOG" \&lt;/span&gt;
           &lt;span class="hljs-comment"&gt;--verify-tag &lt;/span&gt;
       env:
         USERNAME: github-actions[bot]  
         EMAIL: github-actions[bot]@users.noreply.github.com  
         GITHUB_TOKEN: $  
         TAG: $  
         CHANGELOG: $&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We use mathieudutour/github-tag-action to parse the next version from Conventional Commit messages. It runs in dry-run mode to generate the version without actually pushing it. If your organisation bans external actions, you'll need to implement version logic with a custom action yourself.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The release step creates three tags and force-pushes them. Yes, force-pushing is usually considered extreme, but we're moving tag pointers, not rewriting history. This is safe in the pipeline, but don’t do it on your machine. The consequence of tag mutation is that other developers need to run git pull --force to sync updated tags.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We generate a basic changelog from commits. I don't endorse maintaining CHANGELOG.md files as they're often out of sync. Instead, create a commit log during release, and edit the release notes afterwards if needed.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Post-Release Verification: Test Like a User&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The release is tagged and pushed. But we're not done yet. We need one final check: verifying the action works exactly as users will use it by referencing the released tag, not local files.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs sql"&gt;jobs:
 test-unit: ...
 test-local-action: ...
 &lt;span class="hljs-keyword"&gt;release&lt;/span&gt;: ...

 &lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-tagged-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;:
   &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; tagged &lt;span class="hljs-keyword"&gt;action&lt;/span&gt;  
   runs-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;: ubuntu-latest
   needs: [&lt;span class="hljs-keyword"&gt;release&lt;/span&gt;]  
   permissions:  
     &lt;span class="hljs-keyword"&gt;contents&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;read&lt;/span&gt;  
   
   steps:  
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; happy &lt;span class="hljs-keyword"&gt;case&lt;/span&gt;
       uses: nikoheikkila/rot&lt;span class="hljs-number"&gt;-13&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v1  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: valid  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;: $  
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; sad &lt;span class="hljs-keyword"&gt;case&lt;/span&gt;
       uses: nikoheikkila/rot&lt;span class="hljs-number"&gt;-13&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v1  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: invalid  
       continue-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;error&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;: &lt;span class="hljs-string"&gt;""&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Notice the critical difference from earlier verification: we reference the action using nikoheikkila/rot-13-action@v1, not ./. This tests exactly what users will run.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If this job is successful, you can have high confidence that your release works correctly. It's not just tagged, but also verified.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion: Actions Are Software&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When this series began, you might have viewed GitHub Actions as simple automation scripts too trivial for serious software engineering practices.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I hope you now see the light: GitHub Actions are software. They deserve the same software engineering rigour as any production system: clean architecture, comprehensive testing, automated verification, and safe delivery pipelines.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The investment pays off in reliability, maintainability, and confidence. Instead of "push and pray" development, you have a fast feedback loop that catches bugs before users do. Instead of fragile scripts that break mysteriously, you have well-tested components that adapt to change.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;What You've Learned&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Through this series, you've mastered:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Design:&lt;/strong&gt; Separating business logic from infrastructure using dependency injection&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Testing:&lt;/strong&gt; Unit tests, property-based tests, and mutation testing for genuine confidence&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Building:&lt;/strong&gt; Creating reproducible production artifacts&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Releasing:&lt;/strong&gt; Semantic versioning with automated verification&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Delivery:&lt;/strong&gt; Safe deployment with post-release verification&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Next Steps&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Clone the &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; and use it as a foundation for your own actions. The code is production-ready, battle-tested, and follows the principles we've discussed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Found something to improve? Submit an issue or pull request. All contributions are welcome. After all, continuous improvement is what software craftsmanship is all about.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Now go build something great. Your users will thank you for the reliability, and your future self will thank you for the maintainability.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-3-building-and-releasing" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Craftspersons%20Guide%20to%20GitHub%20Actions%20part%203%20Building%20and%20Releasing.webp" alt="Craftsperson's Guide to GitHub Actions #3: Building and Releasing — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the previous chapter, we gained confidence through comprehensive testing, including unit tests, property-based tests, and mutation tests. Our action quality is rock-solid.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But quality means nothing until the product reaches users. An action that only exists on your machine is just an expensive science experiment. In this final chapter, we'll build for production and create an automated release pipeline that safely delivers your action to the world.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Building Your Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For JavaScript-based GitHub Actions, we need a build process. GitHub doesn't compile or bundle the code before execution, which means we must ship a ready-to-run artefact.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This means transpiling TypeScript to JavaScript, bundling all dependencies, and checking the build artefact into Git. Yes, you read that correctly. Unlike typical projects where you ignore your build artefacts from version control, GitHub Actions requires you to commit them to the repository.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Use whichever bundler you prefer. The &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; utilises &lt;a href="https://bun.sh/"&gt;&lt;span style="text-decoration:underline"&gt;Bun&lt;/span&gt;&lt;/a&gt; for its excellent bundling feature, but esbuild or Rolldown can also be used with similar results.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As discussed in Chapter 1, keep your action entry point separate from source files and tests. This separation makes configuring bundling easier.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The key is defining a reproducible build command that works identically everywhere — on your laptop, on your colleague's machine, in GitHub Actions. I use &lt;a href="https://taskfile.dev/"&gt;&lt;span style="text-decoration:underline"&gt;Taskfile&lt;/span&gt;&lt;/a&gt; for orchestration, but npm scripts, Makefiles, or shell scripts are equally valid choices. Pick your tool; just make it consistent.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;build&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;desc&lt;/span&gt;: &lt;span class="hljs-string"&gt;Build GitHub Action&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;sources&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;bin/**/*.ts&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;src/**/*.ts&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;generates&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-meta"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;dist/index.js&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;cmd&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;bun&lt;/span&gt; &lt;span class="hljs-string"&gt;build bin/index.ts&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;--production&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--target&lt;/span&gt; &lt;span class="hljs-string"&gt;node&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--outdir&lt;/span&gt; &lt;span class="hljs-string"&gt;dist&lt;/span&gt;
      &lt;span class="hljs-meta"&gt;--format&lt;/span&gt; &lt;span class="hljs-string"&gt;esm&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This builds our TypeScript entry point into a minified Node.js script in modern ESM format.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Running task build produces a 510 KB bundle containing all dependencies. That might seem large for a simple ROT-13 action, but GitHub Actions runners download it in no time.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The hardest part is to remember to commit the build artefact. Being human — and thus forgetful — we automate this with a pre-commit hook using &lt;a href="https://typicode.github.io/husky/"&gt;&lt;span style="text-decoration:underline"&gt;Husky&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-attr"&gt;task&lt;/span&gt; &lt;span class="hljs-string"&gt;-p lint build&lt;/span&gt;
&lt;span class="hljs-attr"&gt;git&lt;/span&gt; &lt;span class="hljs-string"&gt;add dist README.md&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This hook runs linting and building in parallel, then stages both the dist directory and &lt;a href="http://readme.md"&gt;&lt;span style="text-decoration:underline"&gt;README.md&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Why the README? We generate action documentation during the build using &lt;a href="https://github.com/npalm/action-docs"&gt;&lt;span style="text-decoration:underline"&gt;action-docs&lt;/span&gt;&lt;/a&gt;, keeping documentation synchronised with code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Building and committing is just the first step. Now we need to verify the action actually works and release it safely.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Trust, But Verify the Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our CI/CD pipeline runs the same tests you run locally: unit tests, property-based tests, and mutation tests. But that's not enough.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Despite covering a lot of ground with existing tests, we still need to verify the action works in its actual runtime environment: GitHub Actions. Let's create an acceptance testing workflow. It's verbose, so we'll break it down piece by piece.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs http"&gt;&lt;span class="hljs-attribute"&gt;name&lt;/span&gt;: Acceptance Tests
  
&lt;span class="hljs-attribute"&gt;on&lt;/span&gt;:  
  &lt;span class="hljs-attribute"&gt;pull_request&lt;/span&gt;:  
    &lt;span class="hljs-attribute"&gt;branches&lt;/span&gt;: [main]  
  &lt;span class="hljs-attribute"&gt;push&lt;/span&gt;:  
    &lt;span class="hljs-attribute"&gt;branches&lt;/span&gt;: [main]

&lt;span class="http"&gt;&lt;span class="hljs-attribute"&gt;env&lt;/span&gt;: &lt;span class="hljs-attribute"&gt;original&lt;/span&gt;: "Hello, World!" &lt;span class="hljs-attribute"&gt;transformed&lt;/span&gt;: "Uryyb, Jbeyq!" &lt;span class="yaml"&gt;&lt;span class="hljs-attr"&gt;jobs:&lt;/span&gt; &lt;span class="hljs-attr"&gt; test-unit:&lt;/span&gt; &lt;span class="hljs-comment"&gt;# Unit, property-based, and mutation tests&lt;/span&gt; &lt;span class="hljs-string"&gt;...&lt;/span&gt; &lt;span class="hljs-attr"&gt; test-local-action:&lt;/span&gt; &lt;span class="hljs-attr"&gt; name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;local&lt;/span&gt; &lt;span class="hljs-string"&gt;action&lt;/span&gt; &lt;span class="hljs-attr"&gt; permissions:&lt;/span&gt; &lt;span class="hljs-attr"&gt; contents:&lt;/span&gt; &lt;span class="hljs-string"&gt;read&lt;/span&gt; &lt;span class="hljs-attr"&gt; strategy:&lt;/span&gt; &lt;span class="hljs-attr"&gt; matrix:&lt;/span&gt; &lt;span class="hljs-attr"&gt; runner:&lt;/span&gt; &lt;span class="hljs-string"&gt;[ubuntu-latest,&lt;/span&gt; &lt;span class="hljs-string"&gt;macos-latest,&lt;/span&gt; &lt;span class="hljs-string"&gt;windows-latest]&lt;/span&gt; &lt;span class="hljs-attr"&gt; runs-on:&lt;/span&gt; &lt;span class="hljs-string"&gt;$&lt;/span&gt; &lt;span class="hljs-attr"&gt; steps:&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Checkout&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;actions/checkout@v5&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;with&lt;/span&gt; &lt;span class="hljs-string"&gt;valid&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;./&lt;/span&gt; &lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;valid&lt;/span&gt; &lt;span class="hljs-attr"&gt; with:&lt;/span&gt; &lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;$&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Fail&lt;/span&gt; &lt;span class="hljs-string"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;output&lt;/span&gt; &lt;span class="hljs-string"&gt;is&lt;/span&gt; &lt;span class="hljs-string"&gt;incorrect&lt;/span&gt; &lt;span class="hljs-attr"&gt; if:&lt;/span&gt; &lt;span class="hljs-string"&gt;steps.valid.outputs.result&lt;/span&gt; &lt;span class="hljs-string"&gt;!=&lt;/span&gt; &lt;span class="hljs-string"&gt;env.transformed&lt;/span&gt; &lt;span class="hljs-attr"&gt; run:&lt;/span&gt; &lt;span class="hljs-string"&gt;| echo "::error::Expected result of transformation was '$&lt;span class="hljs-template-variable"&gt;&lt;/span&gt;', but got '$&lt;span class="hljs-template-variable"&gt;&lt;/span&gt;'" exit 1 &lt;/span&gt;&lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Test&lt;/span&gt; &lt;span class="hljs-string"&gt;with&lt;/span&gt; &lt;span class="hljs-string"&gt;empty&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;./&lt;/span&gt; &lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;invalid&lt;/span&gt; &lt;span class="hljs-attr"&gt; continue-on-error:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt; &lt;span class="hljs-attr"&gt; with:&lt;/span&gt; &lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;""&lt;/span&gt; &lt;span class="hljs-attr"&gt; - name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Fail&lt;/span&gt; &lt;span class="hljs-string"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;empty&lt;/span&gt; &lt;span class="hljs-string"&gt;input&lt;/span&gt; &lt;span class="hljs-string"&gt;succeeds&lt;/span&gt; &lt;span class="hljs-attr"&gt; if:&lt;/span&gt; &lt;span class="hljs-string"&gt;steps.invalid.outcome&lt;/span&gt; &lt;span class="hljs-string"&gt;!=&lt;/span&gt; &lt;span class="hljs-string"&gt;'failure'&lt;/span&gt; &lt;span class="hljs-attr"&gt; run:&lt;/span&gt; &lt;span class="hljs-string"&gt;| echo "::error::Expected action to fail when given empty input, but it succeeded." exit 1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Why testing matrices? While our action is platform-agnostic, many aren't. File system operations, for instance, often work similarly on Linux and macOS but break on Windows. Developing on a single platform while mocking the entire filesystem effectively hides these issues. Thus, we catch defects before our users do by testing across many platforms.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Since we haven't released the action yet, we use relative notation to reference the repository root. Remember to check out the repository first. GitHub Actions won't do it automatically.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We test both valid input (should succeed) and invalid input (should fail). The assertion steps use conditional execution. They only run when verification fails, resulting in the workflow failing.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Semantic Versioning Done Right&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When the verification passes, it’s time to release. Unlike some release processes that feel like organising a conference, releasing GitHub Actions is refreshingly simple: we tag the verified commit and push.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;GitHub Actions recommends semantic versioning with a simple twist. Instead of one, we publish three tags for each release:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Full version: v1.2.3 (patch-level precision)&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Minor version: v1.2 (minor updates included)&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Major version: v1 (the convenient default)&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This approach lets users choose their comfort level. Want automatic updates? Use v1. Need stability? Pin to v1.2.3. The major version tag is what most users reference, and we keep it updated automatically.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's the workflow:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs sql"&gt;jobs:
 test-unit: ...
 test-local-action: ...
 
 &lt;span class="hljs-keyword"&gt;release&lt;/span&gt;:
   &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Release&lt;/span&gt;
   &lt;span class="hljs-keyword"&gt;if&lt;/span&gt;: github.event_name == &lt;span class="hljs-string"&gt;'push'&lt;/span&gt; &amp;amp;&amp;amp; github.ref == &lt;span class="hljs-string"&gt;'refs/heads/main'&lt;/span&gt;
   needs: [&lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-unit, &lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;local&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;]
   runs-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;: ubuntu-latest
   permissions:
     &lt;span class="hljs-keyword"&gt;contents&lt;/span&gt;: write
   
   steps:
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: Checkout
       uses: actions/checkout@v5  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;fetch&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;depth&lt;/span&gt;: &lt;span class="hljs-number"&gt;0&lt;/span&gt;  
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: Determine &lt;span class="hljs-keyword"&gt;next&lt;/span&gt; &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       uses: mathieudutour/github-tag-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v6&lt;span class="hljs-number"&gt;.2&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         github_token: $  
         default_bump: &lt;span class="hljs-keyword"&gt;patch&lt;/span&gt;  
         create_annotated_tag: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
         dry_run: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Release&lt;/span&gt; &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-keyword"&gt;version&lt;/span&gt;  
       &lt;span class="hljs-keyword"&gt;if&lt;/span&gt;: steps.version.outputs.new_version != steps.version.outputs.previous_version  
       run: |  
         &lt;span class="hljs-keyword"&gt;function&lt;/span&gt; push() {  
           &lt;span class="hljs-keyword"&gt;local&lt;/span&gt; tag=&lt;span class="hljs-string"&gt;"$1"&lt;/span&gt;  
           git tag -fa &lt;span class="hljs-string"&gt;"$tag"&lt;/span&gt; -m &lt;span class="hljs-string"&gt;"Release $tag"&lt;/span&gt;  
           git push origin &lt;span class="hljs-string"&gt;"$tag"&lt;/span&gt; &lt;span class="hljs-comment"&gt;--force &lt;/span&gt;
         }  
           
         git config user.name &lt;span class="hljs-string"&gt;"$USERNAME"&lt;/span&gt;  
         git config user.email &lt;span class="hljs-string"&gt;"$EMAIL"&lt;/span&gt;  
           
         push &lt;span class="hljs-string"&gt;"$TAG"&lt;/span&gt;  
         push &lt;span class="hljs-string"&gt;"$(echo "&lt;/span&gt;$TAG&lt;span class="hljs-string"&gt;" | cut -d . -f 1)"&lt;/span&gt;  
         push &lt;span class="hljs-string"&gt;"$(echo "&lt;/span&gt;$TAG&lt;span class="hljs-string"&gt;" | cut -d . -f 1-2)"&lt;/span&gt;  
           
         gh &lt;span class="hljs-keyword"&gt;release&lt;/span&gt; &lt;span class="hljs-keyword"&gt;create&lt;/span&gt; &lt;span class="hljs-string"&gt;"$TAG"&lt;/span&gt; \
           &lt;span class="hljs-comment"&gt;--title "Release $TAG" \&lt;/span&gt;
           &lt;span class="hljs-comment"&gt;--notes "$CHANGELOG" \&lt;/span&gt;
           &lt;span class="hljs-comment"&gt;--verify-tag &lt;/span&gt;
       env:
         USERNAME: github-actions[bot]  
         EMAIL: github-actions[bot]@users.noreply.github.com  
         GITHUB_TOKEN: $  
         TAG: $  
         CHANGELOG: $&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We use mathieudutour/github-tag-action to parse the next version from Conventional Commit messages. It runs in dry-run mode to generate the version without actually pushing it. If your organisation bans external actions, you'll need to implement version logic with a custom action yourself.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The release step creates three tags and force-pushes them. Yes, force-pushing is usually considered extreme, but we're moving tag pointers, not rewriting history. This is safe in the pipeline, but don’t do it on your machine. The consequence of tag mutation is that other developers need to run git pull --force to sync updated tags.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We generate a basic changelog from commits. I don't endorse maintaining CHANGELOG.md files as they're often out of sync. Instead, create a commit log during release, and edit the release notes afterwards if needed.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Post-Release Verification: Test Like a User&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The release is tagged and pushed. But we're not done yet. We need one final check: verifying the action works exactly as users will use it by referencing the released tag, not local files.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs sql"&gt;jobs:
 test-unit: ...
 test-local-action: ...
 &lt;span class="hljs-keyword"&gt;release&lt;/span&gt;: ...

 &lt;span class="hljs-keyword"&gt;test&lt;/span&gt;-tagged-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;:
   &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; tagged &lt;span class="hljs-keyword"&gt;action&lt;/span&gt;  
   runs-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;: ubuntu-latest
   needs: [&lt;span class="hljs-keyword"&gt;release&lt;/span&gt;]  
   permissions:  
     &lt;span class="hljs-keyword"&gt;contents&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;read&lt;/span&gt;  
   
   steps:  
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; happy &lt;span class="hljs-keyword"&gt;case&lt;/span&gt;
       uses: nikoheikkila/rot&lt;span class="hljs-number"&gt;-13&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v1  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: valid  
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;: $  
   
     - &lt;span class="hljs-keyword"&gt;name&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;Test&lt;/span&gt; sad &lt;span class="hljs-keyword"&gt;case&lt;/span&gt;
       uses: nikoheikkila/rot&lt;span class="hljs-number"&gt;-13&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;action&lt;/span&gt;@v1  
       &lt;span class="hljs-keyword"&gt;id&lt;/span&gt;: invalid  
       continue-&lt;span class="hljs-keyword"&gt;on&lt;/span&gt;-&lt;span class="hljs-keyword"&gt;error&lt;/span&gt;: &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
       &lt;span class="hljs-keyword"&gt;with&lt;/span&gt;:  
         &lt;span class="hljs-keyword"&gt;string&lt;/span&gt;: &lt;span class="hljs-string"&gt;""&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Notice the critical difference from earlier verification: we reference the action using nikoheikkila/rot-13-action@v1, not ./. This tests exactly what users will run.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If this job is successful, you can have high confidence that your release works correctly. It's not just tagged, but also verified.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion: Actions Are Software&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When this series began, you might have viewed GitHub Actions as simple automation scripts too trivial for serious software engineering practices.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I hope you now see the light: GitHub Actions are software. They deserve the same software engineering rigour as any production system: clean architecture, comprehensive testing, automated verification, and safe delivery pipelines.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The investment pays off in reliability, maintainability, and confidence. Instead of "push and pray" development, you have a fast feedback loop that catches bugs before users do. Instead of fragile scripts that break mysteriously, you have well-tested components that adapt to change.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;What You've Learned&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Through this series, you've mastered:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Design:&lt;/strong&gt; Separating business logic from infrastructure using dependency injection&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Testing:&lt;/strong&gt; Unit tests, property-based tests, and mutation testing for genuine confidence&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Building:&lt;/strong&gt; Creating reproducible production artifacts&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Releasing:&lt;/strong&gt; Semantic versioning with automated verification&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Delivery:&lt;/strong&gt; Safe deployment with post-release verification&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Next Steps&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Clone the &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; and use it as a foundation for your own actions. The code is production-ready, battle-tested, and follows the principles we've discussed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Found something to improve? Submit an issue or pull request. All contributions are welcome. After all, continuous improvement is what software craftsmanship is all about.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Now go build something great. Your users will thank you for the reliability, and your future self will thank you for the maintainability.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcraftspersons-guide-to-github-actions-3-building-and-releasing&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Wed, 07 Jan 2026 22:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-3-building-and-releasing</guid>
      <dc:date>2026-01-07T22:00:00Z</dc:date>
      <dc:creator>Niko Heikkilä</dc:creator>
    </item>
    <item>
      <title>Craftsperson's Guide to GitHub Actions #2: Scaling Up the Testing — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-2-scaling-up-the-testing</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-2-scaling-up-the-testing" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad_Blog_Niko-Craftsperson.webp" alt="Craftsperson's Guide to GitHub Actions #2: Scaling Up the Testing — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the previous post, we improved the design of our GitHub Action to make it testable. But having testable code is just the beginning. We're still far from delivering a truly reliable solution to our users.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this chapter, we'll enhance our testing approach with two powerful techniques that many developers overlook: property-based testing and mutation testing. These aren't just advanced techniques for the sake of it. They're practical tools that uncover bugs traditional testing misses.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Before diving into advanced testing techniques, let me introduce the example we'll use throughout this post.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The ROT-13 Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our example action is deliberately simple: it transforms an input string using ROT-13 and outputs the result. ROT-13 is a letter substitution cipher that replaces each letter with the letter 13 positions after it in the alphabet. Simple enough to understand, yet complex enough to demonstrate testing challenges.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using the action in a workflow is straightforward:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs yaml"&gt;&lt;span class="hljs-attr"&gt;- name:&lt;/span&gt; &lt;span class="hljs-string"&gt;ROT-13&lt;/span&gt;
&lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;nikoheikkila/rot-13-action@v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;rot-13&lt;/span&gt;
&lt;span class="hljs-attr"&gt; with:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;"Hello, world!"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We set a display name for the logs, assign an ID for accessing output in subsequent steps, and reference the action using its repository name and version tag.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When executed, the action logs its transformation:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-meta"&gt;▽&lt;/span&gt; &lt;span class="hljs-string"&gt;Run nikoheikkila/rot-13-action@v1&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;with&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;string&lt;/span&gt;: &lt;span class="hljs-string"&gt;Hello, World!&lt;/span&gt;
    
&lt;span class="hljs-meta"&gt;Hello,&lt;/span&gt; &lt;span class="hljs-string"&gt;World! -&amp;gt; Uryyb, Jbeyq!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This represents the minimum functionality we need to test. However, as professional software engineers, we should strive for more than the bare minimum.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;GitHub Actions also supports &lt;em&gt;composite actions&lt;/em&gt;, which are essentially shell scripts split into multiple steps. While they can simplify long workflows, they're notoriously difficult to test correctly. I only recommend them for trivial use cases.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let's start with practical unit tests.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Unit Testing: Fast Feedback Without Waiting&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Yes, the ultimate test environment for GitHub Actions is GitHub Actions itself. But that doesn't mean we should test there exclusively.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As we discussed in the previous chapter, the key challenge is testing action logic without building and pushing code after every change. With proper design, testing becomes fast and even enjoyable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Every non-trivial GitHub Action has a core where the business logic lives. You might be tempted to test only this core in isolation, but that's a mistake. We need to verify the complete behaviour from input to output.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I'm not talking about end-to-end tests that span multiple systems. Instead, think of this as &lt;em&gt;behaviour-focused sociable testing&lt;/em&gt;. These tests verify how components work together to produce the expected behaviour.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We're not testing individual components in isolation. We're verifying complete behaviour from input to output while the components interact. Hence, we refer to it as sociable testing.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;"&lt;em&gt;But is this unit testing or integration testing?&lt;/em&gt;", you might ask.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the question bothers you, I highly recommend &lt;a href="https://ted.dev/articles/2023/04/02/i-m-done-with-unit-and-integration-tests/"&gt;&lt;span style="text-decoration:underline"&gt;Ted M. Young's article on why the distinction doesn't matter as much as you think&lt;/span&gt;&lt;/a&gt;. What matters is that our tests are fast, reliable, and verify the expected behaviour.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's what happy path tests look like for our action:&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;These tests arrange the action with predefined input, execute it, assert the output matches expectations, and verify the log message. No spies or mocks needed. The tests are data-driven, readable, and blazingly fast.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs cs"&gt;it.each([
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"A"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"N"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"M"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Z"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"N"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"A"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Z"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"M"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"a"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"n"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"m"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"z"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"n"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"a"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"z"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"m"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"HELLO"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"URYYB"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"WORLD"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"JBEYQ"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"ROT13"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"EBG13"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"123"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"123"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"!@#$%"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"!@#$%"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Hello, World!"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Uryyb, Jbeyq!"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Héllo"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Uéyyb"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"&#x1f512; secret"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"&#x1f512; frperg"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Тест"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Тест"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"مرحبا"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"مرحبا"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"こんにちは"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"こんにちは"&lt;/span&gt;&lt;/span&gt;],
])(&lt;span class="hljs-string"&gt;"transforms %s to %s"&lt;/span&gt;, (input, expectedResult) =&amp;gt; {
 core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

 action.run();

 &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; actualResult = core.getOutput(&lt;span class="hljs-string"&gt;"result"&lt;/span&gt;);
 expect(actualResult).toBe(expectedResult);
 expect(core.eventsOf(&lt;span class="hljs-string"&gt;"info"&lt;/span&gt;)).toContain(`${input} -&amp;gt; ${expectedResult}`);
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Edge cases matter, too. While the ROT-13 transformation of an empty string is technically valid, let's demonstrate input validation by requiring input length between 1 and 1,048,576 characters (1 MB).&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"fails with empty string input"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
  const input = &lt;span class="hljs-string"&gt;""&lt;/span&gt;;
  core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

  expect(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; action.run()).toThrowError(
    &lt;span class="hljs-string"&gt;"input field 'string' cannot be empty"&lt;/span&gt;,
  );
});

it(&lt;span class="hljs-string"&gt;"fails with input exceeding 1 MB"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
  const maxSize = &lt;span class="hljs-number"&gt;1024&lt;/span&gt; * &lt;span class="hljs-number"&gt;1024&lt;/span&gt;;
  const input = &lt;span class="hljs-string"&gt;"*"&lt;/span&gt;.repeat(maxSize + &lt;span class="hljs-number"&gt;1&lt;/span&gt;);
  core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

  expect(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; action.run()).toThrowError(
    `&lt;span class="javascript"&gt;input field &lt;span class="hljs-string"&gt;'string'&lt;/span&gt; cannot exceed ${maxSize} characters&lt;/span&gt;`,
  );
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our unit tests now pass with 100% coverage. Time to celebrate? Not quite. Traditional code coverage is a vanity metric: it tells us which lines were executed, not whether our tests actually verify the correct behaviour. Let's do better.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Property-Based Testing: Testing What You Can't Imagine&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Think about the essential properties of ROT-13 transformation:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Length preservation:&lt;/strong&gt; transformation doesn't alter the input length&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Inverse operation:&lt;/strong&gt; applying ROT-13 twice returns the original string&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Case preservation:&lt;/strong&gt; uppercase letters stay uppercase, lowercase letters stay lowercase&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Character selectivity:&lt;/strong&gt; only alphabetic characters rotate&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You could write hundreds of example-based tests to cover these properties and still miss a myriad of edge cases. Even LLMs would struggle to generate comprehensive enough examples.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There's a better way, and it’s called property-based testing. Libraries like Hypothesis (Python) or QuickCheck (Haskell) have demonstrated the power of this approach. For JavaScript, we'll use &lt;a href="https://fast-check.dev/"&gt;&lt;span style="text-decoration:underline"&gt;fast-check&lt;/span&gt;&lt;/a&gt;, which generates test data automatically based on properties we define.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A property-based test in fast-check is elegant:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;type Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s: string)&lt;/span&gt; =&amp;gt;&lt;/span&gt; boolean;

it(&lt;span class="hljs-string"&gt;"does not change text length"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const preservesLength: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(s).length === s.length;

 assert(property(string(), preservesLength));
});&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We &lt;em&gt;assert&lt;/em&gt; that a &lt;em&gt;property&lt;/em&gt; holds true for all &lt;em&gt;strings&lt;/em&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The predicate function returns true when the transformation preserves length. Fast-check then generates hundreds of random strings and checks our predicate against each one. If all pass, the test passes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When a test fails, fast-check doesn't just throw up its hands. It &lt;em&gt;shrinks&lt;/em&gt; the failing input to find the minimal example that demonstrates the bug. Instead of debugging "xKj9!@mNqP#$wZ", you might get "A". This shrinking process is invaluable for understanding the causes of failures.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once you've identified the minimal failing case, write it as a traditional unit test, fix it, then return to property-based testing to verify the fix. This workflow integrates beautifully with test-driven development.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Testing the inverse property is equally straightforward:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"is its own inverse"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isItsOwnInverse: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(transform(s)) === s;

 assert(property(string(), isItsOwnInverse));
});&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sometimes we need to constrain inputs to test-specific properties. Fast-check's string().filter() method lets us generate only strings matching certain criteria:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"preserves uppercase"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isUpperCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; s === s.toUpperCase();
 const preservesUpperCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt;
  [...transform(s)].every(isUpperCase);

 assert(property(string().filter(isUpperCase), preservesUpperCase));
});

it(&lt;span class="hljs-string"&gt;"preserves lowercase"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isLowerCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; s === s.toLowerCase();
 const preservesLowercase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt;
  [...transform(s)].every(isLowerCase);

 assert(property(string().filter(isLowerCase), preservesLowercase));
});

it(&lt;span class="hljs-string"&gt;"only transforms alphabetic characters"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isSpecialCharacter: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; !&lt;span class="hljs-regexp"&gt;/[A-Za-z]/&lt;/span&gt;.test(s);
 const skipsTransformation: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(s) === s;

 assert(property(string().filter(isSpecialCharacter), skipsTransformation));
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Not every GitHub Action benefits from property-based testing. If your action doesn't involve mathematical properties or transformations, traditional tests might suffice.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, for many actions involving data, cryptography, parsing, or any domain with distinct invariants, fast-check saves enormous amounts of time while uncovering bugs you'd never have imagined.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We now have more test code than production code. "Isn't this overkill for a simple ROT-13 transformation?" you might ask.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;No. Quality software often has significantly more test code than production code. We're building confidence that our action behaves correctly under all circumstances. And we're not done yet: the most powerful testing technique is still ahead.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mutation Testing: The Ultimate Reality Check&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation testing is the most humbling technique in a developer's toolkit. Why? Because it &lt;em&gt;tests your tests&lt;/em&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Testing tests might sound recursive and pointless, but it's critical, especially if you write tests after the code. We've all been there until we learn &lt;strong&gt;Test-Driven Development&lt;/strong&gt;. Mutation testing reveals whether your tests actually verify behaviour or just exercise code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's how it works: A mutation testing tool modifies your source code in subtle ways — switching &amp;gt; to &amp;gt;=, changing &amp;amp;&amp;amp; to ||, removing conditionals, or tweaking regular expressions. These modifications are called &lt;a href="https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/"&gt;&lt;span style="text-decoration:underline"&gt;mutators&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;After mutation, the tool runs your tests. If tests fail, the so-called &lt;em&gt;mutant is killed&lt;/em&gt;, which is good. If tests still pass, the mutant &lt;em&gt;survives&lt;/em&gt;, which is bad since your tests didn't catch the bug. The mutation score is the percentage of killed mutants out of all mutants.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation testing exposes the harsh truth: traditional code coverage is a vanity metric. Many teams treat 100% coverage as proof of quality, but mutation testing tells a different story.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've seen codebases that enforce up to 100% coverage scores through quality gates, yet when I run mutation tests, numerous mutants still survive because the tests were written hastily. You might have experienced this yourself: refactor some code — or let an LLM do it — and then see all tests pass, only to watch bugs appear in production. Your tests touched every line but verified little.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Improving mutation scores in legacy codebases is a challenging task. Even modern LLMs struggle with this. The pragmatic approach is to start with a lower threshold and gradually increase it as you improve tests. For new projects, aim for 100% from the start and enforce it throughout your pipeline.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our ROT-13 action is new, so we'll choose perfection.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Setting up mutation testing for our GitHub Action is straightforward with &lt;a href="https://stryker-mutator.io/docs/stryker-js/introduction"&gt;&lt;span style="text-decoration:underline"&gt;StrykerJS&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs css"&gt;{
  &lt;span class="hljs-attribute"&gt;commandRunner&lt;/span&gt;: {
    command: &lt;span class="hljs-string"&gt;"bun test"&lt;/span&gt;,
  },
    &lt;span class="hljs-selector-tag"&gt;checkers&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["typescript"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;mutate&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["src/**/*.ts"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;reporters&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["clear-text", "progress"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;thresholds&lt;/span&gt;: {
      &lt;span class="hljs-attribute"&gt;high&lt;/span&gt;: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
      low: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
      break: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
  }
}
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Key configuration points:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Command runner:&lt;/strong&gt; Specifies the test command. We use Bun, but any test runner works.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Checkers:&lt;/strong&gt; TypeScript checker eliminates mutants that cause type errors, saving time.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mutate:&lt;/strong&gt; Defines which source files to mutate. Be specific: mutating tests or dependencies wastes time.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Thresholds:&lt;/strong&gt; We set all thresholds to 100%, meaning anything less than perfect fails the build.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The high/low thresholds control report colours (green/yellow/red), but since ours are identical, we get either green or failure. There is no middle ground.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation tests add only a few seconds to our test suite, making them perfect for pre-push hooks. Run unit tests, property-based tests, and mutation tests together. Push the commit only when everything is green.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Building quality into GitHub Actions requires discipline, but the payoff is worth it. Follow this testing pyramid:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Start with unit tests using test-driven development&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Add property-based tests to uncover edge cases you'd never think to write&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Verify with mutation testing to ensure your tests actually test what they claim to test&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Add a few workflow tests to verify your action is callable&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The foundation for all of this is writing actions that do one thing well. Simple logic is more straightforward to test than complex logic. Introduce these practices early, ideally before writing much production code. Retrofitting quality is always more difficult.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Remember that GitHub Actions are just functions. They take inputs and produce outputs. The same testing principles that apply to your backend or frontend code apply here. No exceptions, no excuses.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the next chapter, we'll address the final piece: building and releasing your action. We'll create a CI/CD pipeline that verifies your action works correctly in a real GitHub Actions environment, then releases it safely to users. We'll also explore how to handle external dependencies and asynchronous operations without sacrificing test speed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;It’s where all the design and testing practices come together to create automation you can trust in production. This is the kind of delivery culture we help teams build every day at Polar Squad.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let us ship this action!&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-2-scaling-up-the-testing" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad_Blog_Niko-Craftsperson.webp" alt="Craftsperson's Guide to GitHub Actions #2: Scaling Up the Testing — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the previous post, we improved the design of our GitHub Action to make it testable. But having testable code is just the beginning. We're still far from delivering a truly reliable solution to our users.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this chapter, we'll enhance our testing approach with two powerful techniques that many developers overlook: property-based testing and mutation testing. These aren't just advanced techniques for the sake of it. They're practical tools that uncover bugs traditional testing misses.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Before diving into advanced testing techniques, let me introduce the example we'll use throughout this post.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The ROT-13 Action&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our example action is deliberately simple: it transforms an input string using ROT-13 and outputs the result. ROT-13 is a letter substitution cipher that replaces each letter with the letter 13 positions after it in the alphabet. Simple enough to understand, yet complex enough to demonstrate testing challenges.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using the action in a workflow is straightforward:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs yaml"&gt;&lt;span class="hljs-attr"&gt;- name:&lt;/span&gt; &lt;span class="hljs-string"&gt;ROT-13&lt;/span&gt;
&lt;span class="hljs-attr"&gt; uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;nikoheikkila/rot-13-action@v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt; id:&lt;/span&gt; &lt;span class="hljs-string"&gt;rot-13&lt;/span&gt;
&lt;span class="hljs-attr"&gt; with:&lt;/span&gt;
&lt;span class="hljs-attr"&gt; string:&lt;/span&gt; &lt;span class="hljs-string"&gt;"Hello, world!"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We set a display name for the logs, assign an ID for accessing output in subsequent steps, and reference the action using its repository name and version tag.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When executed, the action logs its transformation:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs properties"&gt;&lt;span class="hljs-meta"&gt;▽&lt;/span&gt; &lt;span class="hljs-string"&gt;Run nikoheikkila/rot-13-action@v1&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;with&lt;/span&gt;:&lt;span class="hljs-string"&gt;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;string&lt;/span&gt;: &lt;span class="hljs-string"&gt;Hello, World!&lt;/span&gt;
    
&lt;span class="hljs-meta"&gt;Hello,&lt;/span&gt; &lt;span class="hljs-string"&gt;World! -&amp;gt; Uryyb, Jbeyq!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This represents the minimum functionality we need to test. However, as professional software engineers, we should strive for more than the bare minimum.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;GitHub Actions also supports &lt;em&gt;composite actions&lt;/em&gt;, which are essentially shell scripts split into multiple steps. While they can simplify long workflows, they're notoriously difficult to test correctly. I only recommend them for trivial use cases.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let's start with practical unit tests.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Unit Testing: Fast Feedback Without Waiting&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Yes, the ultimate test environment for GitHub Actions is GitHub Actions itself. But that doesn't mean we should test there exclusively.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As we discussed in the previous chapter, the key challenge is testing action logic without building and pushing code after every change. With proper design, testing becomes fast and even enjoyable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Every non-trivial GitHub Action has a core where the business logic lives. You might be tempted to test only this core in isolation, but that's a mistake. We need to verify the complete behaviour from input to output.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I'm not talking about end-to-end tests that span multiple systems. Instead, think of this as &lt;em&gt;behaviour-focused sociable testing&lt;/em&gt;. These tests verify how components work together to produce the expected behaviour.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We're not testing individual components in isolation. We're verifying complete behaviour from input to output while the components interact. Hence, we refer to it as sociable testing.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;"&lt;em&gt;But is this unit testing or integration testing?&lt;/em&gt;", you might ask.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the question bothers you, I highly recommend &lt;a href="https://ted.dev/articles/2023/04/02/i-m-done-with-unit-and-integration-tests/"&gt;&lt;span style="text-decoration:underline"&gt;Ted M. Young's article on why the distinction doesn't matter as much as you think&lt;/span&gt;&lt;/a&gt;. What matters is that our tests are fast, reliable, and verify the expected behaviour.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's what happy path tests look like for our action:&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;These tests arrange the action with predefined input, execute it, assert the output matches expectations, and verify the log message. No spies or mocks needed. The tests are data-driven, readable, and blazingly fast.&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs cs"&gt;it.each([
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"A"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"N"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"M"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Z"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"N"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"A"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Z"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"M"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"a"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"n"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"m"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"z"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"n"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"a"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"z"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"m"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"HELLO"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"URYYB"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"WORLD"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"JBEYQ"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"ROT13"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"EBG13"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"123"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"123"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"!@#$%"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"!@#$%"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Hello, World!"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Uryyb, Jbeyq!"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Héllo"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Uéyyb"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"&#x1f512; secret"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"&#x1f512; frperg"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"Тест"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"Тест"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"مرحبا"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"مرحبا"&lt;/span&gt;&lt;/span&gt;],
  [&lt;span class="hljs-meta"&gt;&lt;span class="hljs-meta-string"&gt;"こんにちは"&lt;/span&gt;, &lt;span class="hljs-meta-string"&gt;"こんにちは"&lt;/span&gt;&lt;/span&gt;],
])(&lt;span class="hljs-string"&gt;"transforms %s to %s"&lt;/span&gt;, (input, expectedResult) =&amp;gt; {
 core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

 action.run();

 &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; actualResult = core.getOutput(&lt;span class="hljs-string"&gt;"result"&lt;/span&gt;);
 expect(actualResult).toBe(expectedResult);
 expect(core.eventsOf(&lt;span class="hljs-string"&gt;"info"&lt;/span&gt;)).toContain(`${input} -&amp;gt; ${expectedResult}`);
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Edge cases matter, too. While the ROT-13 transformation of an empty string is technically valid, let's demonstrate input validation by requiring input length between 1 and 1,048,576 characters (1 MB).&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"fails with empty string input"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
  const input = &lt;span class="hljs-string"&gt;""&lt;/span&gt;;
  core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

  expect(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; action.run()).toThrowError(
    &lt;span class="hljs-string"&gt;"input field 'string' cannot be empty"&lt;/span&gt;,
  );
});

it(&lt;span class="hljs-string"&gt;"fails with input exceeding 1 MB"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
  const maxSize = &lt;span class="hljs-number"&gt;1024&lt;/span&gt; * &lt;span class="hljs-number"&gt;1024&lt;/span&gt;;
  const input = &lt;span class="hljs-string"&gt;"*"&lt;/span&gt;.repeat(maxSize + &lt;span class="hljs-number"&gt;1&lt;/span&gt;);
  core.setInput(&lt;span class="hljs-string"&gt;"string"&lt;/span&gt;, input);

  expect(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; action.run()).toThrowError(
    `&lt;span class="javascript"&gt;input field &lt;span class="hljs-string"&gt;'string'&lt;/span&gt; cannot exceed ${maxSize} characters&lt;/span&gt;`,
  );
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our unit tests now pass with 100% coverage. Time to celebrate? Not quite. Traditional code coverage is a vanity metric: it tells us which lines were executed, not whether our tests actually verify the correct behaviour. Let's do better.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Property-Based Testing: Testing What You Can't Imagine&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Think about the essential properties of ROT-13 transformation:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Length preservation:&lt;/strong&gt; transformation doesn't alter the input length&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Inverse operation:&lt;/strong&gt; applying ROT-13 twice returns the original string&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Case preservation:&lt;/strong&gt; uppercase letters stay uppercase, lowercase letters stay lowercase&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Character selectivity:&lt;/strong&gt; only alphabetic characters rotate&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You could write hundreds of example-based tests to cover these properties and still miss a myriad of edge cases. Even LLMs would struggle to generate comprehensive enough examples.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There's a better way, and it’s called property-based testing. Libraries like Hypothesis (Python) or QuickCheck (Haskell) have demonstrated the power of this approach. For JavaScript, we'll use &lt;a href="https://fast-check.dev/"&gt;&lt;span style="text-decoration:underline"&gt;fast-check&lt;/span&gt;&lt;/a&gt;, which generates test data automatically based on properties we define.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A property-based test in fast-check is elegant:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;type Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s: string)&lt;/span&gt; =&amp;gt;&lt;/span&gt; boolean;

it(&lt;span class="hljs-string"&gt;"does not change text length"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const preservesLength: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(s).length === s.length;

 assert(property(string(), preservesLength));
});&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We &lt;em&gt;assert&lt;/em&gt; that a &lt;em&gt;property&lt;/em&gt; holds true for all &lt;em&gt;strings&lt;/em&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The predicate function returns true when the transformation preserves length. Fast-check then generates hundreds of random strings and checks our predicate against each one. If all pass, the test passes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When a test fails, fast-check doesn't just throw up its hands. It &lt;em&gt;shrinks&lt;/em&gt; the failing input to find the minimal example that demonstrates the bug. Instead of debugging "xKj9!@mNqP#$wZ", you might get "A". This shrinking process is invaluable for understanding the causes of failures.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once you've identified the minimal failing case, write it as a traditional unit test, fix it, then return to property-based testing to verify the fix. This workflow integrates beautifully with test-driven development.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Testing the inverse property is equally straightforward:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"is its own inverse"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isItsOwnInverse: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(transform(s)) === s;

 assert(property(string(), isItsOwnInverse));
});&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sometimes we need to constrain inputs to test-specific properties. Fast-check's string().filter() method lets us generate only strings matching certain criteria:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs coffeescript"&gt;it(&lt;span class="hljs-string"&gt;"preserves uppercase"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isUpperCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; s === s.toUpperCase();
 const preservesUpperCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt;
  [...transform(s)].every(isUpperCase);

 assert(property(string().filter(isUpperCase), preservesUpperCase));
});

it(&lt;span class="hljs-string"&gt;"preserves lowercase"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isLowerCase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; s === s.toLowerCase();
 const preservesLowercase: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt;
  [...transform(s)].every(isLowerCase);

 assert(property(string().filter(isLowerCase), preservesLowercase));
});

it(&lt;span class="hljs-string"&gt;"only transforms alphabetic characters"&lt;/span&gt;, &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
 const isSpecialCharacter: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; !&lt;span class="hljs-regexp"&gt;/[A-Za-z]/&lt;/span&gt;.test(s);
 const skipsTransformation: Predicate = &lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;(s)&lt;/span&gt; =&amp;gt;&lt;/span&gt; transform(s) === s;

 assert(property(string().filter(isSpecialCharacter), skipsTransformation));
});
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Not every GitHub Action benefits from property-based testing. If your action doesn't involve mathematical properties or transformations, traditional tests might suffice.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, for many actions involving data, cryptography, parsing, or any domain with distinct invariants, fast-check saves enormous amounts of time while uncovering bugs you'd never have imagined.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We now have more test code than production code. "Isn't this overkill for a simple ROT-13 transformation?" you might ask.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;No. Quality software often has significantly more test code than production code. We're building confidence that our action behaves correctly under all circumstances. And we're not done yet: the most powerful testing technique is still ahead.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mutation Testing: The Ultimate Reality Check&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation testing is the most humbling technique in a developer's toolkit. Why? Because it &lt;em&gt;tests your tests&lt;/em&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Testing tests might sound recursive and pointless, but it's critical, especially if you write tests after the code. We've all been there until we learn &lt;strong&gt;Test-Driven Development&lt;/strong&gt;. Mutation testing reveals whether your tests actually verify behaviour or just exercise code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's how it works: A mutation testing tool modifies your source code in subtle ways — switching &amp;gt; to &amp;gt;=, changing &amp;amp;&amp;amp; to ||, removing conditionals, or tweaking regular expressions. These modifications are called &lt;a href="https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/"&gt;&lt;span style="text-decoration:underline"&gt;mutators&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;After mutation, the tool runs your tests. If tests fail, the so-called &lt;em&gt;mutant is killed&lt;/em&gt;, which is good. If tests still pass, the mutant &lt;em&gt;survives&lt;/em&gt;, which is bad since your tests didn't catch the bug. The mutation score is the percentage of killed mutants out of all mutants.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation testing exposes the harsh truth: traditional code coverage is a vanity metric. Many teams treat 100% coverage as proof of quality, but mutation testing tells a different story.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've seen codebases that enforce up to 100% coverage scores through quality gates, yet when I run mutation tests, numerous mutants still survive because the tests were written hastily. You might have experienced this yourself: refactor some code — or let an LLM do it — and then see all tests pass, only to watch bugs appear in production. Your tests touched every line but verified little.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Improving mutation scores in legacy codebases is a challenging task. Even modern LLMs struggle with this. The pragmatic approach is to start with a lower threshold and gradually increase it as you improve tests. For new projects, aim for 100% from the start and enforce it throughout your pipeline.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Our ROT-13 action is new, so we'll choose perfection.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Setting up mutation testing for our GitHub Action is straightforward with &lt;a href="https://stryker-mutator.io/docs/stryker-js/introduction"&gt;&lt;span style="text-decoration:underline"&gt;StrykerJS&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs css"&gt;{
  &lt;span class="hljs-attribute"&gt;commandRunner&lt;/span&gt;: {
    command: &lt;span class="hljs-string"&gt;"bun test"&lt;/span&gt;,
  },
    &lt;span class="hljs-selector-tag"&gt;checkers&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["typescript"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;mutate&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["src/**/*.ts"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;reporters&lt;/span&gt;: &lt;span class="hljs-selector-attr"&gt;["clear-text", "progress"]&lt;/span&gt;,
    &lt;span class="hljs-selector-tag"&gt;thresholds&lt;/span&gt;: {
      &lt;span class="hljs-attribute"&gt;high&lt;/span&gt;: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
      low: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
      break: &lt;span class="hljs-number"&gt;100&lt;/span&gt;,
  }
}
&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Key configuration points:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Command runner:&lt;/strong&gt; Specifies the test command. We use Bun, but any test runner works.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Checkers:&lt;/strong&gt; TypeScript checker eliminates mutants that cause type errors, saving time.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mutate:&lt;/strong&gt; Defines which source files to mutate. Be specific: mutating tests or dependencies wastes time.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Thresholds:&lt;/strong&gt; We set all thresholds to 100%, meaning anything less than perfect fails the build.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The high/low thresholds control report colours (green/yellow/red), but since ours are identical, we get either green or failure. There is no middle ground.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Mutation tests add only a few seconds to our test suite, making them perfect for pre-push hooks. Run unit tests, property-based tests, and mutation tests together. Push the commit only when everything is green.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Building quality into GitHub Actions requires discipline, but the payoff is worth it. Follow this testing pyramid:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Start with unit tests using test-driven development&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Add property-based tests to uncover edge cases you'd never think to write&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Verify with mutation testing to ensure your tests actually test what they claim to test&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Add a few workflow tests to verify your action is callable&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The foundation for all of this is writing actions that do one thing well. Simple logic is more straightforward to test than complex logic. Introduce these practices early, ideally before writing much production code. Retrofitting quality is always more difficult.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Remember that GitHub Actions are just functions. They take inputs and produce outputs. The same testing principles that apply to your backend or frontend code apply here. No exceptions, no excuses.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the next chapter, we'll address the final piece: building and releasing your action. We'll create a CI/CD pipeline that verifies your action works correctly in a real GitHub Actions environment, then releases it safely to users. We'll also explore how to handle external dependencies and asynchronous operations without sacrificing test speed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;It’s where all the design and testing practices come together to create automation you can trust in production. This is the kind of delivery culture we help teams build every day at Polar Squad.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let us ship this action!&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcraftspersons-guide-to-github-actions-2-scaling-up-the-testing&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Tue, 09 Dec 2025 22:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-2-scaling-up-the-testing</guid>
      <dc:date>2025-12-09T22:00:00Z</dc:date>
      <dc:creator>Niko Heikkilä</dc:creator>
    </item>
    <item>
      <title>Craftsperson's Guide to GitHub Actions #1: Designing for Success — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad%20of%20Blog%20thumbnails_GitHub%20guide.webp" alt="Craftsperson's Guide to GitHub Actions #1: Designing for Success — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Discover how to design testable GitHub Actions by avoiding common pitfalls like implicit dependencies and global state. Learn to separate business logic from infrastructure using dependency injection for fast, reliable testing.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Like many other types of scripts, &lt;strong&gt;GitHub Actions&lt;/strong&gt; suffer from a common problem: it's tempting to take a legacy Bash or PowerShell script and convert it into JavaScript without considering best practices like modularity and separation of concerns. The result is code that's difficult to understand, test, and maintain.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Typically, actions become tightly coupled with the underlying infrastructure. They make network requests haphazardly, write to and read from disk, and invoke arbitrary shell commands — all without proper abstraction.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even seemingly straightforward actions, such as downloading and adding a binary executable to the path, quickly reveal their complexity. What if the download fails? What if permissions prevent writing to the directory? What if we download the wrong version? What if the executable doesn't launch at all?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you've ever written a script for installing custom software from the internet, you know how exhaustive the logic can be, even when it looks as simple as piping curl output to bash. This is why we can't afford to skip quality practices, even for simple scripts.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Hidden Cost of Implicit Dependencies&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;To streamline action development, GitHub provides the @actions/toolkit monorepo, which includes several helpful packages. While these tools are powerful, I've seen them used in ways that create maintenance nightmares.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Take a look at this code snippet from GitHub's official tutorial. If you're familiar with software design principles, it might make you wince:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; core &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/core"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; github &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/github"&lt;/span&gt;;

&lt;span class="hljs-keyword"&gt;try&lt;/span&gt; {
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; nameToGreet = core.getInput(&lt;span class="hljs-string"&gt;"who-to-greet"&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`Hello &lt;span class="hljs-subst"&gt;${nameToGreet}&lt;/span&gt;!`&lt;/span&gt;);

  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; time = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Date&lt;/span&gt;().toTimeString();
  core.setOutput(&lt;span class="hljs-string"&gt;"time"&lt;/span&gt;, time);

  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; payload = &lt;span class="hljs-built_in"&gt;JSON&lt;/span&gt;.stringify(github.context.payload, &lt;span class="hljs-literal"&gt;undefined&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`The event payload: &lt;span class="hljs-subst"&gt;${payload}&lt;/span&gt;`&lt;/span&gt;);
} &lt;span class="hljs-keyword"&gt;catch&lt;/span&gt; (error) {
  core.setFailed(error.message);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Three significant issues stand out:&lt;/p&gt; 
     &lt;ol&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Direct third-party library usage without abstraction&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Global mutable state dependencies&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Uncontrolled side effects (date construction)&lt;/p&gt;&lt;/li&gt; 
     &lt;/ol&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While I won't judge a tutorial too harshly, these patterns are spreading rapidly, especially as developers use LLMs to generate action code. The resulting actions become difficult to test and maintain.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Import Smells: When Dependencies Use You&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #1&lt;/strong&gt; prevents safe side-effect handling and clean testing. The core object performs multiple responsibilities: logging to the console, setting output data, and terminating the script with exit codes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Running this code in tests without a proper abstraction creates a miserable experience. Environment variables for inputs are missing, outputs can't be asserted, and your terminal floods with logs.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;"&lt;em&gt;But we can always use spies and mocks!&lt;/em&gt;" some might argue.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;True, but at what cost? Your business logic is still entangled with infrastructure, tests become fragile, and refactoring turns into a risky endeavour. When tests depend heavily on implementation details, they require increasingly complex mocking setups. Coming back six months later to fix a bug through tests? Good luck.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Importing functionality from third-party packages without abstraction creates what I call &lt;em&gt;import smells&lt;/em&gt; — dependencies that pollute your code and make testing painful.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Global Mutable State: The Silent Killer&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #2&lt;/strong&gt; introduces dependency on &lt;em&gt;global mutable state&lt;/em&gt; that changes during pipeline runs. This creates the dreaded scenario: tests pass locally but fail after pushing. Debugging becomes a guessing game.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Worse still, your action might behave differently between pull request and release workflows because the global context contains different data. What worked in one scenario breaks in another.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using context for simple debug logging? That's fine. However, once complexity increases, confidence in your actions tends to evaporate. As a maintainer, prepare yourself for a steady stream of bug reports and confused users.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Time Problem&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #3&lt;/strong&gt; is subtler but equally important.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When business logic calculates the current timestamp directly, testing becomes unreliable because each test run produces a different timestamp.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You can't test the timestamp cleanly and end up either mocking the system clock, or writing weak assertions that check the string vaguely resembles a timestamp. That's not testing but wishful thinking.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This ties back to the first issue: even without explicit imports, you have an implicit dependency on the Date class.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;To address the issues above, we need to design actions that are easy to test and not coupled to the infrastructure. How do we solve that?&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Solution: Dependency Injection&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The fix is simpler than you might think. Let's separate business logic from infrastructure by splitting our code into two files:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-comment"&gt;// action.mjs&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; &lt;span class="hljs-title"&gt;run&lt;/span&gt;(&lt;span class="hljs-params"&gt;{ core, github, date }&lt;/span&gt;) &lt;/span&gt;{
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; nameToGreet = core.getInput(&lt;span class="hljs-string"&gt;"who-to-greet"&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`Hello &lt;span class="hljs-subst"&gt;${nameToGreet}&lt;/span&gt;!`&lt;/span&gt;);
  
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; time = date.toTimeString();
  core.info(&lt;span class="hljs-string"&gt;`The current time is &lt;span class="hljs-subst"&gt;${time}&lt;/span&gt;`&lt;/span&gt;);
  
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; payload = github.payload.dump();
  core.info(&lt;span class="hljs-string"&gt;`The event payload: &lt;span class="hljs-subst"&gt;${payload}&lt;/span&gt;`&lt;/span&gt;);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-comment"&gt;// index.mjs&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; core &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/core"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; github &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/github"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; action &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;'./action.js'&lt;/span&gt;;

&lt;span class="hljs-keyword"&gt;try&lt;/span&gt; {
  action.run({ core, github, &lt;span class="hljs-attr"&gt;date&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Date&lt;/span&gt;() });
} &lt;span class="hljs-keyword"&gt;catch&lt;/span&gt; (error) {
  core.setFailed(error.message);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The structure looks similar, but the improvement is dramatic. Our business logic is now infrastructure-independent. The logic doesn't import anything from the Actions toolkit. This makes the code portable to other systems with similar interfaces.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The infrastructure hasn't disappeared; it's been separated through dependency injection. The core, github, and date objects are now parameters we can easily substitute in tests.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;That's it! Our business logic and dependencies are now cleanly separated, and, most importantly, they are testable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In place of infrastructure dependencies, we use infrastructure test doubles — fake objects — which don’t contain side effects making them ideal for tests.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've added a few helper methods to the fake objects to make testing more ergonomic. JavaScript and TypeScript allow this flexibility as long as objects implement the required interface. Since these test doubles never leave the test suite, there's no risk of them appearing in production code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The assertions simply inspect an in-memory log, ensuring tests pass for the right reasons, produce clean reports, and allow confident refactoring of business logic.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Dependency injection scales extremely well. No matter how complex dependency you have, you can always pick out the interesting parts, write an abstraction, and use a test double in its place.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Before writing your next GitHub Action, ask yourself one question:&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Can I test this logic without pushing to GitHub and running a workflow?&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the answer is yes — using the techniques described here — you've unlocked fast feedback loops that reduce defects and make changing business logic cheaper and safer.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But we're just getting started. In the next chapter, we'll explore advanced testing techniques, including property-based testing to uncover edge cases you might never think to write, and mutation testing to verify that your tests actually test what they claim to test.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;It’s the next step toward building automation you can truly trust.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;See you there!&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad%20of%20Blog%20thumbnails_GitHub%20guide.webp" alt="Craftsperson's Guide to GitHub Actions #1: Designing for Success — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Discover how to design testable GitHub Actions by avoiding common pitfalls like implicit dependencies and global state. Learn to separate business logic from infrastructure using dependency injection for fast, reliable testing.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Like many other types of scripts, &lt;strong&gt;GitHub Actions&lt;/strong&gt; suffer from a common problem: it's tempting to take a legacy Bash or PowerShell script and convert it into JavaScript without considering best practices like modularity and separation of concerns. The result is code that's difficult to understand, test, and maintain.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Typically, actions become tightly coupled with the underlying infrastructure. They make network requests haphazardly, write to and read from disk, and invoke arbitrary shell commands — all without proper abstraction.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even seemingly straightforward actions, such as downloading and adding a binary executable to the path, quickly reveal their complexity. What if the download fails? What if permissions prevent writing to the directory? What if we download the wrong version? What if the executable doesn't launch at all?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you've ever written a script for installing custom software from the internet, you know how exhaustive the logic can be, even when it looks as simple as piping curl output to bash. This is why we can't afford to skip quality practices, even for simple scripts.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Hidden Cost of Implicit Dependencies&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;To streamline action development, GitHub provides the @actions/toolkit monorepo, which includes several helpful packages. While these tools are powerful, I've seen them used in ways that create maintenance nightmares.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Take a look at this code snippet from GitHub's official tutorial. If you're familiar with software design principles, it might make you wince:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; core &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/core"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; github &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/github"&lt;/span&gt;;

&lt;span class="hljs-keyword"&gt;try&lt;/span&gt; {
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; nameToGreet = core.getInput(&lt;span class="hljs-string"&gt;"who-to-greet"&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`Hello &lt;span class="hljs-subst"&gt;${nameToGreet}&lt;/span&gt;!`&lt;/span&gt;);

  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; time = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Date&lt;/span&gt;().toTimeString();
  core.setOutput(&lt;span class="hljs-string"&gt;"time"&lt;/span&gt;, time);

  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; payload = &lt;span class="hljs-built_in"&gt;JSON&lt;/span&gt;.stringify(github.context.payload, &lt;span class="hljs-literal"&gt;undefined&lt;/span&gt;, &lt;span class="hljs-number"&gt;2&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`The event payload: &lt;span class="hljs-subst"&gt;${payload}&lt;/span&gt;`&lt;/span&gt;);
} &lt;span class="hljs-keyword"&gt;catch&lt;/span&gt; (error) {
  core.setFailed(error.message);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Three significant issues stand out:&lt;/p&gt; 
     &lt;ol&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Direct third-party library usage without abstraction&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Global mutable state dependencies&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Uncontrolled side effects (date construction)&lt;/p&gt;&lt;/li&gt; 
     &lt;/ol&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While I won't judge a tutorial too harshly, these patterns are spreading rapidly, especially as developers use LLMs to generate action code. The resulting actions become difficult to test and maintain.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Import Smells: When Dependencies Use You&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #1&lt;/strong&gt; prevents safe side-effect handling and clean testing. The core object performs multiple responsibilities: logging to the console, setting output data, and terminating the script with exit codes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Running this code in tests without a proper abstraction creates a miserable experience. Environment variables for inputs are missing, outputs can't be asserted, and your terminal floods with logs.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;"&lt;em&gt;But we can always use spies and mocks!&lt;/em&gt;" some might argue.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;True, but at what cost? Your business logic is still entangled with infrastructure, tests become fragile, and refactoring turns into a risky endeavour. When tests depend heavily on implementation details, they require increasingly complex mocking setups. Coming back six months later to fix a bug through tests? Good luck.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Importing functionality from third-party packages without abstraction creates what I call &lt;em&gt;import smells&lt;/em&gt; — dependencies that pollute your code and make testing painful.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Global Mutable State: The Silent Killer&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #2&lt;/strong&gt; introduces dependency on &lt;em&gt;global mutable state&lt;/em&gt; that changes during pipeline runs. This creates the dreaded scenario: tests pass locally but fail after pushing. Debugging becomes a guessing game.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Worse still, your action might behave differently between pull request and release workflows because the global context contains different data. What worked in one scenario breaks in another.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using context for simple debug logging? That's fine. However, once complexity increases, confidence in your actions tends to evaporate. As a maintainer, prepare yourself for a steady stream of bug reports and confused users.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Time Problem&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Issue #3&lt;/strong&gt; is subtler but equally important.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When business logic calculates the current timestamp directly, testing becomes unreliable because each test run produces a different timestamp.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;You can't test the timestamp cleanly and end up either mocking the system clock, or writing weak assertions that check the string vaguely resembles a timestamp. That's not testing but wishful thinking.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This ties back to the first issue: even without explicit imports, you have an implicit dependency on the Date class.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;To address the issues above, we need to design actions that are easy to test and not coupled to the infrastructure. How do we solve that?&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Solution: Dependency Injection&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The fix is simpler than you might think. Let's separate business logic from infrastructure by splitting our code into two files:&lt;/p&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-comment"&gt;// action.mjs&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;function&lt;/span&gt; &lt;span class="hljs-title"&gt;run&lt;/span&gt;(&lt;span class="hljs-params"&gt;{ core, github, date }&lt;/span&gt;) &lt;/span&gt;{
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; nameToGreet = core.getInput(&lt;span class="hljs-string"&gt;"who-to-greet"&lt;/span&gt;);
  core.info(&lt;span class="hljs-string"&gt;`Hello &lt;span class="hljs-subst"&gt;${nameToGreet}&lt;/span&gt;!`&lt;/span&gt;);
  
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; time = date.toTimeString();
  core.info(&lt;span class="hljs-string"&gt;`The current time is &lt;span class="hljs-subst"&gt;${time}&lt;/span&gt;`&lt;/span&gt;);
  
  &lt;span class="hljs-keyword"&gt;const&lt;/span&gt; payload = github.payload.dump();
  core.info(&lt;span class="hljs-string"&gt;`The event payload: &lt;span class="hljs-subst"&gt;${payload}&lt;/span&gt;`&lt;/span&gt;);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;pre&gt;&lt;code class="hljs javascript"&gt;&lt;span class="hljs-comment"&gt;// index.mjs&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; core &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/core"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; github &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;"@actions/github"&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; * &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; action &lt;span class="hljs-keyword"&gt;from&lt;/span&gt; &lt;span class="hljs-string"&gt;'./action.js'&lt;/span&gt;;

&lt;span class="hljs-keyword"&gt;try&lt;/span&gt; {
  action.run({ core, github, &lt;span class="hljs-attr"&gt;date&lt;/span&gt;: &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; &lt;span class="hljs-built_in"&gt;Date&lt;/span&gt;() });
} &lt;span class="hljs-keyword"&gt;catch&lt;/span&gt; (error) {
  core.setFailed(error.message);
}&lt;/code&gt;&lt;/pre&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The structure looks similar, but the improvement is dramatic. Our business logic is now infrastructure-independent. The logic doesn't import anything from the Actions toolkit. This makes the code portable to other systems with similar interfaces.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The infrastructure hasn't disappeared; it's been separated through dependency injection. The core, github, and date objects are now parameters we can easily substitute in tests.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;That's it! Our business logic and dependencies are now cleanly separated, and, most importantly, they are testable.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In place of infrastructure dependencies, we use infrastructure test doubles — fake objects — which don’t contain side effects making them ideal for tests.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've added a few helper methods to the fake objects to make testing more ergonomic. JavaScript and TypeScript allow this flexibility as long as objects implement the required interface. Since these test doubles never leave the test suite, there's no risk of them appearing in production code.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The assertions simply inspect an in-memory log, ensuring tests pass for the right reasons, produce clean reports, and allow confident refactoring of business logic.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Dependency injection scales extremely well. No matter how complex dependency you have, you can always pick out the interesting parts, write an abstraction, and use a test double in its place.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Before writing your next GitHub Action, ask yourself one question:&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Can I test this logic without pushing to GitHub and running a workflow?&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the answer is yes — using the techniques described here — you've unlocked fast feedback loops that reduce defects and make changing business logic cheaper and safer.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But we're just getting started. In the next chapter, we'll explore advanced testing techniques, including property-based testing to uncover edge cases you might never think to write, and mutation testing to verify that your tests actually test what they claim to test.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;It’s the next step toward building automation you can truly trust.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;See you there!&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcraftspersons-guide-to-github-actions-1-designing-for-success&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Wed, 03 Dec 2025 22:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success</guid>
      <dc:date>2025-12-03T22:00:00Z</dc:date>
      <dc:creator>Niko Heikkilä</dc:creator>
    </item>
    <item>
      <title>Craftsperson's Guide to GitHub Actions  — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad%20of%20Blog%20thumbnails.webp" alt="Craftsperson's Guide to GitHub Actions  — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Learn to build production-ready GitHub Actions with clean architecture, comprehensive testing strategies, and reliable release pipelines, transforming fragile automation scripts into maintainable software.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;Have you ever rushed to push a GitHub Action, only to watch it fail in the pipeline? Or inherited an action so tightly coupled to infrastructure that testing it locally felt impossible? You're not alone.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; have become the backbone of modern CI/CD workflows; yet, we often treat them as throwaway scripts rather than production-grade software. What you usually get is: brittle automation, slow feedback loops, and extensive trial and error in your pipelines.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This year at Polar Squad, I've spent considerable time assisting a platform engineering team in building reusable GitHub Actions used daily by hundreds of developers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's what I learned: the difference between fragile scripts and reliable automation lies in applying the same modern software engineering principles you'd use for any critical production system.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;What You'll Learn&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This series demonstrates how to build GitHub Actions that are:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Testable:&lt;/strong&gt; Run comprehensive tests locally in seconds, not minutes.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Maintainable:&lt;/strong&gt; Separate business logic from infrastructure concerns using clean architecture.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Reliable:&lt;/strong&gt; Achieve confidence through unit tests, property-based tests, and mutation testing.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Production-ready:&lt;/strong&gt; Build, release, and verify your actions with automated pipelines.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The techniques I share aren't specific to GitHub Actions. They're fundamental software engineering practices applied to CI/CD. Clean architecture ensures your code is easy to understand and cheap to change. Fast-running tests provide immediate feedback on changes. The ability to refactor safely means you can adapt to evolving requirements without fear.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Who This Guide Is For&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you're a software engineer building JavaScript or TypeScript-based GitHub Actions and want to move beyond the "push and pray" development, this guide is for you. Whether you're creating actions for your team or publishing them publicly, these practices will save you time and headaches.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've prepared an &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; with a fully functional GitHub Action that demonstrates every principle discussed in this series. Clone it, experiment with it, and use it as a foundation for your own actions.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Series Chapters&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I have divided the series into three focused chapters, each building on the previous one:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 1: &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success"&gt;Designing for Success&lt;/a&gt;&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 2: Scaling Up the Testing&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 3: Building and Releasing&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Follow along over the next few weeks as we explore how to build automation that’s testable, maintainable and actually enjoyable to work with. &lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Polar%20Squad%20of%20Blog%20thumbnails.webp" alt="Craftsperson's Guide to GitHub Actions  — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Learn to build production-ready GitHub Actions with clean architecture, comprehensive testing strategies, and reliable release pipelines, transforming fragile automation scripts into maintainable software.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;Have you ever rushed to push a GitHub Action, only to watch it fail in the pipeline? Or inherited an action so tightly coupled to infrastructure that testing it locally felt impossible? You're not alone.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; have become the backbone of modern CI/CD workflows; yet, we often treat them as throwaway scripts rather than production-grade software. What you usually get is: brittle automation, slow feedback loops, and extensive trial and error in your pipelines.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This year at Polar Squad, I've spent considerable time assisting a platform engineering team in building reusable GitHub Actions used daily by hundreds of developers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's what I learned: the difference between fragile scripts and reliable automation lies in applying the same modern software engineering principles you'd use for any critical production system.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;What You'll Learn&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This series demonstrates how to build GitHub Actions that are:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Testable:&lt;/strong&gt; Run comprehensive tests locally in seconds, not minutes.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Maintainable:&lt;/strong&gt; Separate business logic from infrastructure concerns using clean architecture.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Reliable:&lt;/strong&gt; Achieve confidence through unit tests, property-based tests, and mutation testing.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Production-ready:&lt;/strong&gt; Build, release, and verify your actions with automated pipelines.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The techniques I share aren't specific to GitHub Actions. They're fundamental software engineering practices applied to CI/CD. Clean architecture ensures your code is easy to understand and cheap to change. Fast-running tests provide immediate feedback on changes. The ability to refactor safely means you can adapt to evolving requirements without fear.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Who This Guide Is For&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If you're a software engineer building JavaScript or TypeScript-based GitHub Actions and want to move beyond the "push and pray" development, this guide is for you. Whether you're creating actions for your team or publishing them publicly, these practices will save you time and headaches.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I've prepared an &lt;a href="https://github.com/nikoheikkila/rot-13-action"&gt;&lt;span style="text-decoration:underline"&gt;example repository&lt;/span&gt;&lt;/a&gt; with a fully functional GitHub Action that demonstrates every principle discussed in this series. Clone it, experiment with it, and use it as a foundation for your own actions.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Series Chapters&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I have divided the series into three focused chapters, each building on the previous one:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 1: &lt;a href="https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions-1-designing-for-success"&gt;Designing for Success&lt;/a&gt;&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 2: Scaling Up the Testing&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Chapter 3: Building and Releasing&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Follow along over the next few weeks as we explore how to build automation that’s testable, maintainable and actually enjoyable to work with. &lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fcraftspersons-guide-to-github-actions&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Tue, 02 Dec 2025 22:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/craftspersons-guide-to-github-actions</guid>
      <dc:date>2025-12-02T22:00:00Z</dc:date>
      <dc:creator>Niko Heikkilä</dc:creator>
    </item>
    <item>
      <title>In a struggle to tame Large Language Models — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/taming-llms</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/taming-llms" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/In%20a%20struggle%20to%20tame%20Large%20Language%20Models.webp" alt="In a struggle to tame Large Language Models — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-6 span-6"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-6 span-6"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Initial excitement around Artificial Intelligence (AI) predictive abilities led to rapid investment in machine learning research and development of Large Language Models (LLMs) which sparked a wave of innovation in a variety of streams and at the same time revealed both the potential and limitations of early AI systems.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Introduction&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This blog post will shortly recap what has been happening in the AI’s LLM space since the beginning from the author’s perspective and we will dive a little deeper into MCP, which stands for Model Context Protocol and has been in the hype recently in this space. MCP was introduced by Anthropic, a California based AI startup in late 2024. Why is MCP getting so popular and why are many of the Large language model (LLM) researchers, companies and individuals diving into this topic? Let’s dive in.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;The initial WOW phase&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When researchers and scientists found that LLMs can predict the next word or phrase reliably, that was the eye-opening moment. An example of how LLMs can predict next tokens is shown below, when the user just types the letters “Glo” , the user is offered with relevant suggestions.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This simple word prediction trick when applied to huge datasets unlocked new and valuable application areas. For end users this meant that the LLMs were not only able to predict what a user might think, but also answer hard questions. From that point till now we have been trying to tame LLMs to make them work the way we want.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Initially, the focus was on chat-based language models because they excelled at tokens/words prediction. These models were trained on vast amounts of text data and learned to understand patterns in language, making them highly effective at tasks like conversation, text completion, and natural language understanding.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As the technology progressed, researchers and developers began expanding beyond pure text applications leading to the development of models that could understand, generate and manipulate visual content.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Augmentation phase&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;At the beginning, it became clear that retraining large models was not feasible for everyone due to the immense computational resources and expertise required. As a solution, a method called Retrieval-Augmented Generation (RAG) was developed, which enables the model to access relevant data sources in real time rather than relying solely on pre-trained knowledge.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This approach allowed the model to retrieve relevant information from private datasets and incorporate that context into its responses, improving accuracy and relevance. By using RAG, the model can generate more informed and contextually aware outputs, without the need for constant retraining, making it more flexible and adaptable to specific tasks or queries. However, RAG has challenges with retrieval accuracy, adds more latency and is harder to debug.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Function-calling Phase&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Another significant breakthrough occurred with the introduction of function/tool calling, where LLMs were able to invoke functions dynamically, based on specific needs or contexts. This addressed some of the drawbacks with RAG, for example, the model can invoke precise methods, the retrieval accuracy improved with lesser latency and ease of debugging.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Eventually Anthropic standardized the way the invocation happens and MCP came into existence. This integration opened up new possibilities for automating workflows and enhanced decision-making processes by enabling the model to seamlessly interact with external systems and execute tasks autonomously or on demand. Let's dive a little deeper.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;MCP under the hood&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;How are AI models able to invoke tools, also known as functions?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Models are trained to do a "function call" when they need more data. It is important to know that models in use need to be trained for this purpose, though this is more common with most of the models these days. This can be configured to auto or explicit modes. Models do this by generating a JSON output that represents the input parameters that this function needed and get in return the function output. And the function call needs to be declared to the model using a JSON Schema so the model can understand the features it represents, required input and what it gets in return. An example snippet below:&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Also most of the time you may add some system prompt to guide the model to use the functions you made available.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;So let's take a look at what steps are needed to get an MCP server in place.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;So, essentially, in this case, the MCP server is an API server which can perform some git actions. To make your model call this one, firstly a MCP server needs to be created and accessible over the network. They can be community hosted as well. After that, config JSON needs to be created and updated to the LLM. After which, the LLM invokes the server if need be based on the user’s query. &lt;a href="https://platform.openai.com/docs/guides/function-calling?api-mode=responses"&gt;Learn more about function calling in OpenAI documentation&lt;/a&gt;. &lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Also, this started to be an eco-system and loads of MCP servers were implemented from the commercial and community groups. &lt;a href="https://github.com/modelcontextprotocol/servers"&gt;Some of those are collected under MCP servers collection in GitHub&lt;/a&gt;.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Applications of MCP&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Multiple MCP servers can be connected to a single LLM or multiple LLMs, each capable of being autonomously triggered with different parameters, working together in a coherent way. They could be a Search Engine Optimiser (SEO) with MCP for a single website or an aggregator across a bunch of websites. Technically speaking, any software product can implement an MCP server to enable direct integration to LLMs and LLMs can augment the knowledge, make additions, modifications directly to those products.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;An example below shows a git tool, a web tool and a file tool configured to use for a given LLM. Imagine as a developer, if you just provide the requirements in simple plain text, the model tries to use its own knowledge and reaches out to the needed tools and finally gets back with a fully finished work available in version control. This could be placed in a continuous loop to obtain better results which LLM itself can improve on without human intervention autonomously. Developing a feature in software development used to take days if not weeks before this concept existed. As a result, certain coding editors like Cursor, Windsurf, Microsoft Copilot and Bolt gained popularity and this type of coding was named &lt;em&gt;vibecoding&lt;/em&gt; by Andrey Karpathy.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Where are we today&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Taking a step back, so far, we have reached a level where we can invoke functions based on the scenario or input provided to LLMs. This capability has fundamentally transformed how AI systems can interact with the world around them. This has already exploded with wide possibilities, enabling LLMs to perform complex actions based on natural language understanding. Now there are systems which have multimodal and multiagent systems that can process images alongside text, understand spoken commands, and generate content across different formats simultaneously. Multi-agent architectures have further revolutionized the field by enabling specialized AI entities to collaborate, debate, and collectively solve problems that would be challenging for a single person or even a development team.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Challenges&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This being still in a very early stage, there are a significant number of errors, bugs, vulnerabilities, license violations, and other issues around this. Given the nature of these technologies, robust solutions addressing security vulnerabilities and privacy concerns remain in early development phases. Also, as the lack of tooling for monitoring and troubleshooting around these systems cause challenges with traceability, predictability etc. Considering the hype, these agents are not autonomous enough to perform complex tasks without human intervention. This is also due to the fact that AI agents are not able to have a thought process like humans as of today. This makes AI still not very useful for mission-critical or common real-life use cases involving emotional intelligence, creative ability and ethical reasoning or judgements.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Where are we heading to&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The environment is still dynamic and moving at a fast pace. It's hard to predict which direction it will take in the coming weeks, months or years. Some companies like Nvidia, Google claim that we will be reaching AGI (Artificial General Intelligence, sometimes called human level intelligence AI) in less than 5 years, but the feeling I have is that we are still in the process of taming the LLMs to make it work in a way we want.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;That said, companies like OpenAI, Anthropic, Meta, Google and others are trying hard to release amazing features every month improving their models on a mission to become first to achieve AGI to lead the AI market space.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;More reading&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;1. Introduction for MCP - &lt;a href="https://modelcontextprotocol.io/introduction"&gt;Model context protocol introduction&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;2. Function calling - &lt;a href="https://platform.openai.com/docs/guides/function-calling?api-mode=responses"&gt;OpenAI function calling explained&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;3. MCP servers collection - &lt;a href="https://github.com/modelcontextprotocol/servers"&gt;Github repository with the list of MCP servers&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;4. Vibe-coding - Andrey Karpathy - &lt;a href="https://modelcontextprotocol.io/introduction"&gt;Introduction to MCP from Anthropic&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="row sqs-row"&gt; 
     &lt;div class="col sqs-col-6 span-6"&gt; 
      &lt;div class="sqs-block html-block sqs-block-html"&gt; 
       &lt;div class="sqs-block-content"&gt; 
        &lt;div class="sqs-html-content"&gt; 
         &lt;p class="" style="white-space:pre-wrap;"&gt;Anoop Vijayan is a Senior Cloud Architect who has decades of experience working in Cloud, DevOps, Continuous Integration and Continuous Delivery systems.&lt;/p&gt; 
        &lt;/div&gt; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
     &lt;div class="col sqs-col-2 span-2"&gt; 
      &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
       &lt;div class="sqs-block-content"&gt;
         &amp;nbsp; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/taming-llms" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/In%20a%20struggle%20to%20tame%20Large%20Language%20Models.webp" alt="In a struggle to tame Large Language Models — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-6 span-6"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-6 span-6"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Initial excitement around Artificial Intelligence (AI) predictive abilities led to rapid investment in machine learning research and development of Large Language Models (LLMs) which sparked a wave of innovation in a variety of streams and at the same time revealed both the potential and limitations of early AI systems.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Introduction&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This blog post will shortly recap what has been happening in the AI’s LLM space since the beginning from the author’s perspective and we will dive a little deeper into MCP, which stands for Model Context Protocol and has been in the hype recently in this space. MCP was introduced by Anthropic, a California based AI startup in late 2024. Why is MCP getting so popular and why are many of the Large language model (LLM) researchers, companies and individuals diving into this topic? Let’s dive in.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;The initial WOW phase&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When researchers and scientists found that LLMs can predict the next word or phrase reliably, that was the eye-opening moment. An example of how LLMs can predict next tokens is shown below, when the user just types the letters “Glo” , the user is offered with relevant suggestions.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This simple word prediction trick when applied to huge datasets unlocked new and valuable application areas. For end users this meant that the LLMs were not only able to predict what a user might think, but also answer hard questions. From that point till now we have been trying to tame LLMs to make them work the way we want.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Initially, the focus was on chat-based language models because they excelled at tokens/words prediction. These models were trained on vast amounts of text data and learned to understand patterns in language, making them highly effective at tasks like conversation, text completion, and natural language understanding.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As the technology progressed, researchers and developers began expanding beyond pure text applications leading to the development of models that could understand, generate and manipulate visual content.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Augmentation phase&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;At the beginning, it became clear that retraining large models was not feasible for everyone due to the immense computational resources and expertise required. As a solution, a method called Retrieval-Augmented Generation (RAG) was developed, which enables the model to access relevant data sources in real time rather than relying solely on pre-trained knowledge.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This approach allowed the model to retrieve relevant information from private datasets and incorporate that context into its responses, improving accuracy and relevance. By using RAG, the model can generate more informed and contextually aware outputs, without the need for constant retraining, making it more flexible and adaptable to specific tasks or queries. However, RAG has challenges with retrieval accuracy, adds more latency and is harder to debug.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Function-calling Phase&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Another significant breakthrough occurred with the introduction of function/tool calling, where LLMs were able to invoke functions dynamically, based on specific needs or contexts. This addressed some of the drawbacks with RAG, for example, the model can invoke precise methods, the retrieval accuracy improved with lesser latency and ease of debugging.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Eventually Anthropic standardized the way the invocation happens and MCP came into existence. This integration opened up new possibilities for automating workflows and enhanced decision-making processes by enabling the model to seamlessly interact with external systems and execute tasks autonomously or on demand. Let's dive a little deeper.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;MCP under the hood&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;How are AI models able to invoke tools, also known as functions?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Models are trained to do a "function call" when they need more data. It is important to know that models in use need to be trained for this purpose, though this is more common with most of the models these days. This can be configured to auto or explicit modes. Models do this by generating a JSON output that represents the input parameters that this function needed and get in return the function output. And the function call needs to be declared to the model using a JSON Schema so the model can understand the features it represents, required input and what it gets in return. An example snippet below:&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Also most of the time you may add some system prompt to guide the model to use the functions you made available.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;So let's take a look at what steps are needed to get an MCP server in place.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;So, essentially, in this case, the MCP server is an API server which can perform some git actions. To make your model call this one, firstly a MCP server needs to be created and accessible over the network. They can be community hosted as well. After that, config JSON needs to be created and updated to the LLM. After which, the LLM invokes the server if need be based on the user’s query. &lt;a href="https://platform.openai.com/docs/guides/function-calling?api-mode=responses"&gt;Learn more about function calling in OpenAI documentation&lt;/a&gt;. &lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Also, this started to be an eco-system and loads of MCP servers were implemented from the commercial and community groups. &lt;a href="https://github.com/modelcontextprotocol/servers"&gt;Some of those are collected under MCP servers collection in GitHub&lt;/a&gt;.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Applications of MCP&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Multiple MCP servers can be connected to a single LLM or multiple LLMs, each capable of being autonomously triggered with different parameters, working together in a coherent way. They could be a Search Engine Optimiser (SEO) with MCP for a single website or an aggregator across a bunch of websites. Technically speaking, any software product can implement an MCP server to enable direct integration to LLMs and LLMs can augment the knowledge, make additions, modifications directly to those products.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;An example below shows a git tool, a web tool and a file tool configured to use for a given LLM. Imagine as a developer, if you just provide the requirements in simple plain text, the model tries to use its own knowledge and reaches out to the needed tools and finally gets back with a fully finished work available in version control. This could be placed in a continuous loop to obtain better results which LLM itself can improve on without human intervention autonomously. Developing a feature in software development used to take days if not weeks before this concept existed. As a result, certain coding editors like Cursor, Windsurf, Microsoft Copilot and Bolt gained popularity and this type of coding was named &lt;em&gt;vibecoding&lt;/em&gt; by Andrey Karpathy.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
     &lt;div class="image-block-wrapper"&gt; 
      &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
      &lt;/div&gt; 
     &lt;/div&gt;  
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Where are we today&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Taking a step back, so far, we have reached a level where we can invoke functions based on the scenario or input provided to LLMs. This capability has fundamentally transformed how AI systems can interact with the world around them. This has already exploded with wide possibilities, enabling LLMs to perform complex actions based on natural language understanding. Now there are systems which have multimodal and multiagent systems that can process images alongside text, understand spoken commands, and generate content across different formats simultaneously. Multi-agent architectures have further revolutionized the field by enabling specialized AI entities to collaborate, debate, and collectively solve problems that would be challenging for a single person or even a development team.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Challenges&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;This being still in a very early stage, there are a significant number of errors, bugs, vulnerabilities, license violations, and other issues around this. Given the nature of these technologies, robust solutions addressing security vulnerabilities and privacy concerns remain in early development phases. Also, as the lack of tooling for monitoring and troubleshooting around these systems cause challenges with traceability, predictability etc. Considering the hype, these agents are not autonomous enough to perform complex tasks without human intervention. This is also due to the fact that AI agents are not able to have a thought process like humans as of today. This makes AI still not very useful for mission-critical or common real-life use cases involving emotional intelligence, creative ability and ethical reasoning or judgements.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Where are we heading to&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The environment is still dynamic and moving at a fast pace. It's hard to predict which direction it will take in the coming weeks, months or years. Some companies like Nvidia, Google claim that we will be reaching AGI (Artificial General Intelligence, sometimes called human level intelligence AI) in less than 5 years, but the feeling I have is that we are still in the process of taming the LLMs to make it work in a way we want.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;That said, companies like OpenAI, Anthropic, Meta, Google and others are trying hard to release amazing features every month improving their models on a mission to become first to achieve AGI to lead the AI market space.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;More reading&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;1. Introduction for MCP - &lt;a href="https://modelcontextprotocol.io/introduction"&gt;Model context protocol introduction&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;2. Function calling - &lt;a href="https://platform.openai.com/docs/guides/function-calling?api-mode=responses"&gt;OpenAI function calling explained&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;3. MCP servers collection - &lt;a href="https://github.com/modelcontextprotocol/servers"&gt;Github repository with the list of MCP servers&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;4. Vibe-coding - Andrey Karpathy - &lt;a href="https://modelcontextprotocol.io/introduction"&gt;Introduction to MCP from Anthropic&lt;/a&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="row sqs-row"&gt; 
     &lt;div class="col sqs-col-6 span-6"&gt; 
      &lt;div class="sqs-block html-block sqs-block-html"&gt; 
       &lt;div class="sqs-block-content"&gt; 
        &lt;div class="sqs-html-content"&gt; 
         &lt;p class="" style="white-space:pre-wrap;"&gt;Anoop Vijayan is a Senior Cloud Architect who has decades of experience working in Cloud, DevOps, Continuous Integration and Continuous Delivery systems.&lt;/p&gt; 
        &lt;/div&gt; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
     &lt;div class="col sqs-col-2 span-2"&gt; 
      &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
       &lt;div class="sqs-block-content"&gt;
         &amp;nbsp; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Ftaming-llms&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Tue, 13 May 2025 21:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/taming-llms</guid>
      <dc:date>2025-05-13T21:00:00Z</dc:date>
      <dc:creator>Anoop Vijayan</dc:creator>
    </item>
    <item>
      <title>Horseless carriage: AI is not just for faster coding — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/horseless-carriage-ai</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/horseless-carriage-ai" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/7U1A8223_01.webp" alt="Horseless carriage: AI is not just for faster coding — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
       &lt;div class="image-caption"&gt; 
        &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Vibe coding in progress.&lt;/em&gt;&lt;/p&gt; 
       &lt;/div&gt;   
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;We often talk about AI in software development as a way to write code faster, but that’s only part of the story. As tools evolve, so do our workflows, expectations, and roles. From low-stakes experimentation through “vibe coding” to structured multi-agent systems, this blog post explores how AI-augmented programming is reshaping how we code and think about building software altogether.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There’s no shortage of opinions on AI-augmented programming. Either it will, or it won’t replace software engineers. It’s either the best thing ever for productivity or leads to wasting time debugging weird bugs created by the AI. Opinions on which specific large language model is best are usually based on the person’s anecdotal experience.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One recurring theme in many of these opinions is that they are based on, at best, the current state of AI-augmented programming or, at worst, an outdated understanding of the state from last year. The field is moving so rapidly that it is challenging to keep up and make good arguments about where it is headed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Perhaps because it’s hard to understand the direction of AI-augmented programming, many arguments look at it in terms of how things have worked in the past. By now, it should be clear that with such a massive paradigm shift, this doesn’t always work.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;More than just faster coding&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Just like people of the past thought of cars as horseless carriages, people today might think of AI-augmented programming as something that makes you write code faster. This thinking is partly correct but is somewhat limited and can be misleading. Writing code faster does not automatically lead to delivering higher-quality software faster, and it certainly does not lead to better outcomes on its own.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The software industry has a long-standing tradition of overemphasizing output over outcomes. New methodologies like agile development and DevOps have often been seen by many as tools for delivering more software faster. It looks like this same trend is also continuing with AI-augmented programming, with a lot of the conversation revolving around productivity as measured by lines of code produced. Less attention is given to how we could make use of AI for better outcomes. However, a simple program with good product-to-market fit is better than a complex one without.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Rather than thinking about AI-augmented programming in terms of how programming has worked in the past, we could also look at what it enables that wasn’t possible before. What new use cases does it allow that were either impossible or impractical in the past? How should we change our behaviors due to these new use cases? Thinking this way can lead to better insights.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Explosion of experimentation&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There’s been a lot of talk lately about vibe coding. The term was coined by Andrej Karpathy and defined as follows:&lt;/p&gt; 
     &lt;blockquote&gt; 
      &lt;p class="" style="white-space:pre-wrap;"&gt;“There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. … It's not too bad for throwaway weekend projects, but still quite amusing. I'm building a project or webapp, but it's not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.”&lt;/p&gt; 
     &lt;/blockquote&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Commentators often dismiss this type of coding as producing low-quality code and various security issues. That’s true, at least with the current models – you probably shouldn’t run this code in production, though some will do so anyway. But what if we think of what new use cases this type of programming enables?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of the core principles of DevOps listed in &lt;a href="https://itrevolution.com/product/the-devops-handbook-second-edition/"&gt;&lt;span style="text-decoration:underline"&gt;The DevOps Handbook&lt;/span&gt;&lt;/a&gt; is a culture of continual experimentation and learning. When you can generate code much faster, you can build many more demos, prototypes, and proofs-of-concept. This experimentation allows you to discover the right thing to build quicker and to demonstrate and communicate ideas more effectively to your coworkers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, you need to be mindful not to consider these experiments production-quality and be sure to throw away the code once it has served its purpose. Sometimes, there can be an aversion to throwing away the prototype since it’s basically working, and effort went into building it. If the effort needed is significantly reduced, that might also help to let go of the prototypes more easily.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Show, don’t just tell&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As a practical example of this approach, I recently created a demo of a web application for a customer using what could be called a structured approach to vibe coding. I had a concrete description of the data model I wanted to use in JSON format. I specified the technologies I wanted to use based on my familiarity with them, and iteratively built the web app with the AI writing most of the code. I finished the application in about one workday’s worth of hours while also working on other stuff, and I recorded a demo of the application and shared it in Slack for comments. I could have done this without AI, but it probably would have taken a week or two, which is quite a lot for what I intended to be a throwaway demo. The exercise served its purpose well: Showing what the intended workflow with this application could look like in the future, and getting people excited about the possibilities of automation.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Another benefit is that you don’t need to be a programmer to create these prototypes. Anyone can create something to communicate an idea quickly or to turn a vague idea in their head into something more concrete. You can start with a prompt to generate your concept, then refine the app the AI created a couple of times before showing it to someone else for feedback. You can either do this from scratch with a fresh codebase or add features to an existing codebase.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once you know better what should be built based on some quick vibe coding experiments, you can write the production code more easily. Of course, you can still have the AI assist with the production code. However, the productivity increase there won’t be as dramatic since you need to understand the code and be more mindful of architecture and maintainability.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;From a programmer to a product manager&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When moving beyond demos and experimentation, vibe coding doesn’t cut it anymore, and a more structured approach is needed. For common problems and programming languages, the current state-of-the-art large language models are already good enough to write significant portions of code, leaving the programmer to review the code and set up rules and workflows for the AI. Managing the context for the AI and providing good guardrails and clear instructions are essential. The work of a programmer moves closer to that of a product manager: Create an unambiguous roadmap for what the product you’re building should do and provide context and feedback for the AI to implement it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In addition to defining a roadmap for the AI, the programmer must provide a clear set of rules as guardrails. Agentic coding tools like Cursor, Roo Code, and Cline implement a rules system for this. For example, the programmer can use these rules to enforce a specific workflow for the AI agent, like using test-driven development or a particular coding style. They can also prevent the AI from making certain common mistakes that these agentic tools will make without explicit guidance.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this phase, you do need to understand what good code looks like, how to test it, and how to structure it. At least for now, the AI will regularly get these parts wrong, though good rules and context help a lot to reduce clear mistakes. Experience in software engineering is still needed to review code, write it yourself when necessary, and give good instructions for the AI.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Multi-agentic workflows&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Taking a step further from the programmer becoming a product manager to an AI assistant, future and already some current workflows involve orchestrating a set of AI agents with different roles. For instance, one model writes the code while another reviews it, and a third model is responsible for planning and breaking down the implementation into logical, incremental steps. What’s left to the programmer is to supervise the AI agents and provide good, clear instructions. With its recently released &lt;a href="https://docs.roocode.com/features/boomerang-tasks"&gt;&lt;span style="text-decoration:underline"&gt;Boomerang Tasks&lt;/span&gt;&lt;/a&gt;, Roo Code is an example of a tool that is already implementing this workflow.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using multiple models allows for selecting the most suitable model for each type of task. For example, some models may be best suited for planning, while others are excellent for producing code. This leads to better results with fewer mistakes, and can also be cheaper since the most expensive model doesn’t need to be used for everything.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;What’s left for humans to do?&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Software is ultimately built for humans; only humans can tell what they want from that software. Quite often, we don’t even know exactly what we want from software. In a way, articulating good instructions and managing context for a bunch of AI agents is a different interface for programming computers. In his blog post “&lt;a href="https://www.oreilly.com/radar/the-end-of-programming-as-we-know-it/"&gt;&lt;span style="text-decoration:underline"&gt;The End of Programming as We Know It&lt;/span&gt;&lt;/a&gt;,” Tim O’Reilly argues that there have been similar shifts in programming paradigms before, like moving from flipping switches on a computer to writing assembly and later higher-level compiled languages, and that large language models are just the next iteration of this development.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Not all software engineering fundamentals will change when AI agents are introduced into the workflow. My colleague Niko recently wrote &lt;a href="https://polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments"&gt;&lt;span style="text-decoration:underline"&gt;an excellent blog post&lt;/span&gt;&lt;/a&gt; about how lean principles interact with AI-augmented coding and that more output doesn’t necessarily lead to better outcomes.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="row sqs-row"&gt; 
     &lt;div class="col sqs-col-6 span-6"&gt; 
      &lt;div class="sqs-block html-block sqs-block-html"&gt; 
       &lt;div class="sqs-block-content"&gt; 
        &lt;div class="sqs-html-content"&gt; 
         &lt;p class="" style="white-space:pre-wrap;"&gt;Risto Laurikainen is a DevOps consultant who has worked on platform engineering before it was called platform engineering. He has worked on building and using these platforms for more than a decade in various roles from architecture to team leading.&lt;/p&gt; 
        &lt;/div&gt; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
     &lt;div class="col sqs-col-2 span-2"&gt; 
      &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
       &lt;div class="sqs-block-content"&gt;
         &amp;nbsp; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/horseless-carriage-ai" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/7U1A8223_01.webp" alt="Horseless carriage: AI is not just for faster coding — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
       &lt;div class="image-caption"&gt; 
        &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Vibe coding in progress.&lt;/em&gt;&lt;/p&gt; 
       &lt;/div&gt;   
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;We often talk about AI in software development as a way to write code faster, but that’s only part of the story. As tools evolve, so do our workflows, expectations, and roles. From low-stakes experimentation through “vibe coding” to structured multi-agent systems, this blog post explores how AI-augmented programming is reshaping how we code and think about building software altogether.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There’s no shortage of opinions on AI-augmented programming. Either it will, or it won’t replace software engineers. It’s either the best thing ever for productivity or leads to wasting time debugging weird bugs created by the AI. Opinions on which specific large language model is best are usually based on the person’s anecdotal experience.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One recurring theme in many of these opinions is that they are based on, at best, the current state of AI-augmented programming or, at worst, an outdated understanding of the state from last year. The field is moving so rapidly that it is challenging to keep up and make good arguments about where it is headed.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Perhaps because it’s hard to understand the direction of AI-augmented programming, many arguments look at it in terms of how things have worked in the past. By now, it should be clear that with such a massive paradigm shift, this doesn’t always work.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;More than just faster coding&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Just like people of the past thought of cars as horseless carriages, people today might think of AI-augmented programming as something that makes you write code faster. This thinking is partly correct but is somewhat limited and can be misleading. Writing code faster does not automatically lead to delivering higher-quality software faster, and it certainly does not lead to better outcomes on its own.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The software industry has a long-standing tradition of overemphasizing output over outcomes. New methodologies like agile development and DevOps have often been seen by many as tools for delivering more software faster. It looks like this same trend is also continuing with AI-augmented programming, with a lot of the conversation revolving around productivity as measured by lines of code produced. Less attention is given to how we could make use of AI for better outcomes. However, a simple program with good product-to-market fit is better than a complex one without.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Rather than thinking about AI-augmented programming in terms of how programming has worked in the past, we could also look at what it enables that wasn’t possible before. What new use cases does it allow that were either impossible or impractical in the past? How should we change our behaviors due to these new use cases? Thinking this way can lead to better insights.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Explosion of experimentation&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There’s been a lot of talk lately about vibe coding. The term was coined by Andrej Karpathy and defined as follows:&lt;/p&gt; 
     &lt;blockquote&gt; 
      &lt;p class="" style="white-space:pre-wrap;"&gt;“There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. … It's not too bad for throwaway weekend projects, but still quite amusing. I'm building a project or webapp, but it's not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.”&lt;/p&gt; 
     &lt;/blockquote&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Commentators often dismiss this type of coding as producing low-quality code and various security issues. That’s true, at least with the current models – you probably shouldn’t run this code in production, though some will do so anyway. But what if we think of what new use cases this type of programming enables?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of the core principles of DevOps listed in &lt;a href="https://itrevolution.com/product/the-devops-handbook-second-edition/"&gt;&lt;span style="text-decoration:underline"&gt;The DevOps Handbook&lt;/span&gt;&lt;/a&gt; is a culture of continual experimentation and learning. When you can generate code much faster, you can build many more demos, prototypes, and proofs-of-concept. This experimentation allows you to discover the right thing to build quicker and to demonstrate and communicate ideas more effectively to your coworkers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, you need to be mindful not to consider these experiments production-quality and be sure to throw away the code once it has served its purpose. Sometimes, there can be an aversion to throwing away the prototype since it’s basically working, and effort went into building it. If the effort needed is significantly reduced, that might also help to let go of the prototypes more easily.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Show, don’t just tell&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As a practical example of this approach, I recently created a demo of a web application for a customer using what could be called a structured approach to vibe coding. I had a concrete description of the data model I wanted to use in JSON format. I specified the technologies I wanted to use based on my familiarity with them, and iteratively built the web app with the AI writing most of the code. I finished the application in about one workday’s worth of hours while also working on other stuff, and I recorded a demo of the application and shared it in Slack for comments. I could have done this without AI, but it probably would have taken a week or two, which is quite a lot for what I intended to be a throwaway demo. The exercise served its purpose well: Showing what the intended workflow with this application could look like in the future, and getting people excited about the possibilities of automation.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Another benefit is that you don’t need to be a programmer to create these prototypes. Anyone can create something to communicate an idea quickly or to turn a vague idea in their head into something more concrete. You can start with a prompt to generate your concept, then refine the app the AI created a couple of times before showing it to someone else for feedback. You can either do this from scratch with a fresh codebase or add features to an existing codebase.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once you know better what should be built based on some quick vibe coding experiments, you can write the production code more easily. Of course, you can still have the AI assist with the production code. However, the productivity increase there won’t be as dramatic since you need to understand the code and be more mindful of architecture and maintainability.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;From a programmer to a product manager&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When moving beyond demos and experimentation, vibe coding doesn’t cut it anymore, and a more structured approach is needed. For common problems and programming languages, the current state-of-the-art large language models are already good enough to write significant portions of code, leaving the programmer to review the code and set up rules and workflows for the AI. Managing the context for the AI and providing good guardrails and clear instructions are essential. The work of a programmer moves closer to that of a product manager: Create an unambiguous roadmap for what the product you’re building should do and provide context and feedback for the AI to implement it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In addition to defining a roadmap for the AI, the programmer must provide a clear set of rules as guardrails. Agentic coding tools like Cursor, Roo Code, and Cline implement a rules system for this. For example, the programmer can use these rules to enforce a specific workflow for the AI agent, like using test-driven development or a particular coding style. They can also prevent the AI from making certain common mistakes that these agentic tools will make without explicit guidance.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this phase, you do need to understand what good code looks like, how to test it, and how to structure it. At least for now, the AI will regularly get these parts wrong, though good rules and context help a lot to reduce clear mistakes. Experience in software engineering is still needed to review code, write it yourself when necessary, and give good instructions for the AI.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Multi-agentic workflows&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Taking a step further from the programmer becoming a product manager to an AI assistant, future and already some current workflows involve orchestrating a set of AI agents with different roles. For instance, one model writes the code while another reviews it, and a third model is responsible for planning and breaking down the implementation into logical, incremental steps. What’s left to the programmer is to supervise the AI agents and provide good, clear instructions. With its recently released &lt;a href="https://docs.roocode.com/features/boomerang-tasks"&gt;&lt;span style="text-decoration:underline"&gt;Boomerang Tasks&lt;/span&gt;&lt;/a&gt;, Roo Code is an example of a tool that is already implementing this workflow.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Using multiple models allows for selecting the most suitable model for each type of task. For example, some models may be best suited for planning, while others are excellent for producing code. This leads to better results with fewer mistakes, and can also be cheaper since the most expensive model doesn’t need to be used for everything.&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;What’s left for humans to do?&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Software is ultimately built for humans; only humans can tell what they want from that software. Quite often, we don’t even know exactly what we want from software. In a way, articulating good instructions and managing context for a bunch of AI agents is a different interface for programming computers. In his blog post “&lt;a href="https://www.oreilly.com/radar/the-end-of-programming-as-we-know-it/"&gt;&lt;span style="text-decoration:underline"&gt;The End of Programming as We Know It&lt;/span&gt;&lt;/a&gt;,” Tim O’Reilly argues that there have been similar shifts in programming paradigms before, like moving from flipping switches on a computer to writing assembly and later higher-level compiled languages, and that large language models are just the next iteration of this development.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Not all software engineering fundamentals will change when AI agents are introduced into the workflow. My colleague Niko recently wrote &lt;a href="https://polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments"&gt;&lt;span style="text-decoration:underline"&gt;an excellent blog post&lt;/span&gt;&lt;/a&gt; about how lean principles interact with AI-augmented coding and that more output doesn’t necessarily lead to better outcomes.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="row sqs-row"&gt; 
     &lt;div class="col sqs-col-6 span-6"&gt; 
      &lt;div class="sqs-block html-block sqs-block-html"&gt; 
       &lt;div class="sqs-block-content"&gt; 
        &lt;div class="sqs-html-content"&gt; 
         &lt;p class="" style="white-space:pre-wrap;"&gt;Risto Laurikainen is a DevOps consultant who has worked on platform engineering before it was called platform engineering. He has worked on building and using these platforms for more than a decade in various roles from architecture to team leading.&lt;/p&gt; 
        &lt;/div&gt; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
     &lt;div class="col sqs-col-2 span-2"&gt; 
      &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
       &lt;div class="sqs-block-content"&gt;
         &amp;nbsp; 
       &lt;/div&gt; 
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fhorseless-carriage-ai&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Sun, 04 May 2025 21:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/horseless-carriage-ai</guid>
      <dc:date>2025-05-04T21:00:00Z</dc:date>
      <dc:creator>Risto Laurikainen</dc:creator>
    </item>
    <item>
      <title>AI-augmented Software Development: Hype, Vibes and Smoking Production Environments — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Blog%20thumbnail_Niko%20Heikkila.webp" alt="AI-augmented Software Development: Hype, Vibes and Smoking Production Environments — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Generative AI tools have changed how we developers approach our daily work. Today, headlines tout the arrival of AI-augmented software development and &lt;em&gt;vibe coding&lt;/em&gt; as silver bullets, making development teams orders of magnitude more effective. However, the promised gains are shallow if teams do not also pay attention to the software delivery aspects.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As a software engineer actively using GitHub Copilot and regularly consulting Anthropic's Claude, I have witnessed the power of AI in specific contexts where frequent experimentation and prototyping are valued. AI recalibrates knowledge work so that what once was necessary for humans to handle is now secondary. Meanwhile, the importance of the rest of the knowledge work aspects has grown a thousandfold.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, it's crucial to acknowledge that using AI comes with tradeoffs we must not overlook. While AI-augmented software &lt;em&gt;development&lt;/em&gt; can be beneficial, it's equally important to recognize that AI-augmented software &lt;em&gt;delivery&lt;/em&gt; is still a distant goal. Understanding these limitations is key to being well-prepared in our work.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The True Nature of Software Delivery&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There are significant hazards when using AI to accelerate your development stemming from the fact that programming is only a tiny part of the software delivery process. This process, or more suitably referred to as a value stream, encompasses all the thinking, discussing, experimenting, and learning involved in delivering a working software product to end users.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Understanding the Value Streams in Software Delivery&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As defined by&lt;a href="https://www.lean.org/lexicon-terms/value-stream/"&gt; &lt;span style="text-decoration:underline"&gt;Lean Enterprise Institute&lt;/span&gt;&lt;/a&gt;, the value stream includes all of the actions, both value-creating and nonvalue-creating, required to bring a product from concept to launch (&lt;em&gt;development value stream&lt;/em&gt;) and from order to delivery (&lt;em&gt;operational value stream&lt;/em&gt;). Nonvalue-creating actions include unnecessary handoffs, rework due to poor initial design, or delays due to resource constraints.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In software delivery, the value stream encompasses the entire lifecycle from ideation to production deployment and operations. Programming, or codifying requirements and expectations, is only a step. It typically accounts for less than half of the overall delivery. The remaining work involves understanding user needs, designing solutions, testing hypotheses, collaborating across disciplines, and, most importantly, operating amidst organizational barriers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;These non-programming activities are precisely where the most significant delivery challenges and non-value-creating actions emerge. Teams struggle with requirement ambiguity, stakeholder alignment, integration issues, and organizational power dynamics that ultimately determine whether software delivers the intended value. Organizational barriers could include resistance to change, lack of cross-functional collaboration, or unclear decision-making processes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI excels at brainstorming mostly syntactically correct code with some logical defects but remains fundamentally limited in navigating these human-centered aspects of software delivery.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Lean Principles and Identifying Waste in Software Delivery&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The most notable impediments to optimizing value streams and team flow have been researched as part of Lean manufacturing and are called waste (&lt;em&gt;muda&lt;/em&gt;). We can regard everything that does not create customer value as waste.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mary and Tom Poppendieck&lt;/strong&gt;, in their book&lt;a href="http://www.amazon.com/Lean-Software-Development-Agile-Toolkit/dp/0321150783"&gt;&lt;span style="text-decoration:underline"&gt; &lt;em&gt;Lean Software Development: An Agile Toolkit&lt;/em&gt;&lt;/span&gt;&lt;/a&gt;, popularised the mapping of different types of Lean waste to software delivery impediments.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;According to the Poppendiecks, the primary waste in our work includes:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Partially done work (work-in-progress) or every backlog item sitting in the work queue between the backlog and production.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Overproduction, or all the extra features we write while solving the problem.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Relearning and reworking involved for the backlog items moving back and forth between inspection points.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Internal and external handoffs.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Context switching when you need to drop a task and focus on a new one.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Defects that hinder user experience and endanger businesses.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There is only so little that AI can do to help overcome this waste. On the contrary, it often only exacerbates it. For instance, AI may generate code that solves a problem but introduces new bugs, leading to rework. It may also encourage overproduction by suggesting unnecessary features. Understanding these limitations is crucial for effectively integrating AI into software delivery processes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As we enter a new era in software development, we must remain vigilant about the effectiveness of our delivery processes and value streams. While AI-augmented development provides the potential to streamline coding and ignite creativity, it cannot replace the fundamental human elements that drive successful software delivery.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Acknowledging AI limitations and committing to optimizing our value streams, we can use it not as a crutch but as a companion for success. In doing so, we not only enhance our effectiveness as developers but also ensure that the software we deliver meets the needs of our users.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The path forward demands a novel approach integrating AI's strengths while exposing and eliminating the waste typically associated with software delivery projects.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Partially Done Work&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thanks to its powerful autocomplete features, AI has the potential to revolutionize software development by completing tasks orders of magnitude faster. However, this added speed also carries multiple risks.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Completing one work cycle faster—in Lean manufacturing terms, moving work from one station to another—can jam the team flow as bottlenecks emerge. The quicker we work in the implementation phase, the more work we pile in front of the bottleneck ahead.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Following &lt;strong&gt;Eli Goldratt's&lt;/strong&gt; &lt;a href="https://www.leanproduction.com/theory-of-constraints/"&gt;&lt;span style="text-decoration:underline"&gt;&lt;em&gt;Theory of Constraints&lt;/em&gt;&lt;/span&gt;&lt;/a&gt;, AI-augmented programming is a perfect candidate for the illusion of &lt;a href="https://fortelabs.com/blog/theory-of-constraints-102-local-optima/"&gt;&lt;span style="text-decoration:underline"&gt;local optima&lt;/span&gt;&lt;/a&gt; where we attempt to improve the total performance of the system by improving the performance of an individual cog only to find out it doesn’t yield the expected results.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Bottlenecks and the Illusion of Speed&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let's see an example of how that can happen. Consider a team enthusiastically embracing AI coding assistance. Within a couple of weeks, the developers generate code at incredible velocity. However, their testing and review columns are piling up unfinished work. Usually, many pull requests are waiting for review, which the team resolves by rubber-stamping and disbanding the value of code review altogether. Meanwhile, the UI/UX designers are struggling to keep pace and stress that the developers are moving too fast for them.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Without exposing and improving the bottleneck found in testing and review capacity, the team effectively turns their work into a warehouse of partially done work, delaying delivery. Even further delay happens when the code passes the review and it's time to merge the work, but it proves slow and painful due to numerous merge conflicts.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Can AI help with merge conflicts? It is unlikely unless you provide it with full context from both sides of the merge, which is not trivial. Furthermore, having AI review and test features has been the talk of the town for years now, but at the same time, many regulated companies follow the so-called &lt;a href="https://www.openriskmanual.org/wiki/Four_Eyes_Principle"&gt;&lt;span style="text-decoration:underline"&gt;principle of four eyes&lt;/span&gt;&lt;/a&gt;, where a human outside the work context must review the changes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As DevOps people have been trying to sell continuous delivery mechanisms and reliable pipelines to these companies for a decade with varying success, I'm skeptical that these companies would activate AI code review and testing and automatically let all the changes move to production.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Overproduction&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI answers often provide excessive detail and redundant lines of code when we desire simplicity. While experienced developers can fine-tune their prompts, many users, especially juniors, could unquestioningly accept and use overly complex solutions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While I have nothing against explaining the answers in detail, I seek only a few lines of code when using coding assistance. The less code I receive, the better I follow its intent. Due to the overly helpful nature baked into AI system prompts, they often spit out large parts resembling online tutorials aiding with project setup, file structure, and documentation.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even though you can work around this problem with careful prompting, junior developers often have little experience nor second thoughts about the answers. So, they are happy to copy and paste the answers to their work.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A &lt;a href="https://www.mdpi.com/2075-4698/15/1/6"&gt;&lt;span style="text-decoration:underline"&gt;recent study&lt;/span&gt;&lt;/a&gt; showed that especially younger people with higher reliance on AI tools had more problems with critical thinking than older people. Therefore, it’s not unusual to state that they could treat AI-generated code without criticism, using code that technically looks feasible, but in reality, delivers too much. The result is gross overproduction, over-polishing, and often tight coupling between components. Most of the models have not been trained well with clean software design principles and, by default, tend to produce complete solutions instead of small, iterable experiments.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;AI's Tendency Towards Complexity&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Suppose I ask the AI for help while implementing a date picker component. If it generates hundreds of lines of code, including validation logic, internationalization support, and calendar-like navigation, what should have been an MVP allowing users to select a date has become a tangled mess of features you won't need now—if ever.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The above example perfectly defines overproduction. The code works but includes features users didn't request or need. When the requirements change soon enough, the team modifies the complex initial implementation three times longer than if they had built a minimal solution. The AI optimized for completeness rather than simplicity, creating waste that had to be carried forward or refactored away.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Relearning and Rework&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Accepting AI-generated code without careful review can lead to more work in the future. It's crucial to understand and be comfortable with the code you're working with, as the person changing the code is often someone else from your team.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Having to relearn often leads to heavy code refactoring, which becomes more complex as more overproduction occurs. Of course, you can have AI do the rework, but without pausing to understand the changes you're about to make, you're only placing an order for more rework in the future.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Cost of Unreviewed AI-Generated Code&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A junior developer might use AI to generate a user authentication system. Without fully understanding the generated code, they integrate it into the codebase. Sometime later, when the team needs to add social login capabilities, the team can't grasp the architecture embedded in the generated code. Thus, the team spends days refactoring the logic before extending it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The team could have saved the time investment had they engaged more deeply with the design in the first place. This pattern repeats in worst codebases with black box AI solutions requiring extensive rework whenever they need modification.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Handoffs&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;No matter how effectively we use AI, handoffs to other people will inevitably happen for many teams. Often, our products move from the initial development team to the maintenance team so the original team can focus on building new business-critical features. In more old-fashioned environments, products move from development to operations for deploying and running those.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Imagine handing over a codebase where AI has generated 90 % of the code. If anything, that is a ticking time bomb. Sure, for handover, you can generate the required documentation and align the roadmap with AI, but with what context and cost?&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Challenge of Domain-Specific Knowledge Transfer&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI struggles with domain-specific knowledge transfer, which is the most critical information during handoffs between teams. This limitation stems from how we train AI. While it excels at identifying patterns and generating coherent text, it often lacks sufficient understanding of specialized domain contexts.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For example, a payroll system handling the salaries for municipal employees isn't your average code as it embodies significant regulatory knowledge, compliance requirements, and institution-specific business rules of which AI is unaware. Likewise, healthcare has a bottomless pit of laws, regulations, and essential complexity. Training custom models is possible, but the return on investment is not likely to be profitable as we uncover more complexity during the training.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When AI generates documentation for complex systems, it can describe the technical architecture and surface-level functionality but cannot capture the many whys behind critical decisions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the payroll example, why are transactions handled differently on weekends? Why must specific hour reports undergo additional verification steps? These domain-specific rationales are firmly rooted in the institutional knowledge of the team and domain experts who built the system. Past incidents and interpretations often shape this knowledge, which might not be public information.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The most valuable documentation for handoffs addresses these domain-specific nuances: the edge cases, historical context, and business justifications. They explain why a system works as it does. Here, AI falls short, creating a dangerous knowledge gap during team transitions.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As I explained with partially done work, AI-augmented development creates review bottlenecks, forcing team members to switch contexts between other work items rather than focusing on their work.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even if we adjust the AI to solve the waste of overproduction and produce only minimum code, we aggravate the problem because a higher number of smaller batches demanding review chokes the throughput and causes even more context-switching.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;More Pull Requests, Less Throughput&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In a team heavily utilizing AI, we see a troubling pattern in the pull request process. Team members submit smaller and more frequent pull requests — averaging 12 per week instead of the previous 3–4.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While smaller pull requests generally sound better, the increased volume quickly overwhelms the system capacity. Other team members hop between multiple pull requests daily while their work suffers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The team suffers from a bottleneck where people write code faster than they can review. Everyone suffers from a high cognitive load and feels exhausted despite AI supposedly making their work &lt;em&gt;easier&lt;/em&gt;. We had optimized AI tools for individual speed at the expense of team flow.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Defects&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Whenever I've asked AI to generate a solution to a problem, it has consistently left out unit tests unless I specifically asked it to write them.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I'm unsurprised, having seen many hastily written codebases during my career. The company behind the AI model has likely trained it with a significant subset of public codebases lacking tests. How could it know tests are needed in every serious programming context if many teams do not lead with an example?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If AI does not, by default, write tests for you, then all the AI-augmented 10x developers will only deliver more defects than value. Reflecting on the possibility of AI-reviewed code, I doubt it would know to block a pull request when it lacks tests. That is solely the responsibility of the continuous delivery pipeline. Furthermore, when automated tests mean AI testing the code written and reviewed by another AI, it's another ticking time bomb causing havoc and defects in your production environment.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Risks of Neglecting Tests&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In an example scenario, a project using AI-generated modules extensively, passed all the quality assurance checks, and the team deployed the changes to production. Within the first week, the team had discovered critical vulnerabilities in the API layer the AI had implemented.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Despite looking professional and working correctly for the happy path, the code lacked functional verification for edge cases, error handling, and security headers. The developers had asked for an API fulfilling acceptance criteria but hadn't specifically defined the level of testing they needed. The AI obliged by generating code that functionally worked, but its fundamental insecurities were not caught in the delivery pipeline. The rework cost the team weeks of emergency patching and a security incident review, not to mention losing their reputation along with clients.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Making AI Work Meaningful&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Though this blog post might make the current situation look bleak, , there is still plenty of hope. Making AI-augmented software delivery work is a matter of using the correct tools correctly. That means instead of accruing waste we must use AI to expose and eliminate it.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Establishing Quality and Guidelines&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Teams must establish guardrails that ensure AI builds quality rather than accelerates output. Have clear guidelines for how and where AI tools fit your delivery workflow.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;First, we must establish quality gates dictating that the AI-generated code must pass before integration. Those include automated checks in the form of unit and preferably mutation tests, complexity metrics via static analysis, scanning of security vulnerabilities and software bill of materials, and adherence to architectural patterns. When you practice continuous delivery and automate these gates into your CI/CD pipeline, you have created safeguards against the quality issues inherent in AI-generated solutions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Second, implement and document a team-wide protocol for AI tools usage. For example, consider a policy of not deploying AI-generated code without peer-reviewing your prompt and generated answers. Teams should require their developers to explain the functionality of AI-generated code, ensuring they understand what they're adding to the product. Using agentic tools such as &lt;a href="https://www.cursor.com/"&gt;&lt;span style="text-decoration:underline"&gt;Cursor&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://www.warp.dev/"&gt;&lt;span style="text-decoration:underline"&gt;Warp&lt;/span&gt;&lt;/a&gt;, which propose and implement changes incrementally while keeping the developers in the loop is helpful. Defining and documenting recommended prompting techniques as a shared library is another trick worth considering.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Third, invest in AI literacy across your delivery pipeline. If you have quality assurance specialists, they should share an understanding of how and why they augmented test automation code with AI. Product owners should learn to write requirements without embedding a hallucinated well of wishes from AI. Most importantly, tech leads should become experts in identifying and removing AI waste described in this post.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Path Forward with AI-Augmented Software Delivery&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thinking of AI as a companion instead of a driver plays a key role here. We have already seen increased perceived throughput using AI while sacrificing quality and stability. We must train, tune, and prompt AI to provide quality solutions to balance the situation. At the same time, we still need to continuously educate ourselves on modern software engineering principles. Regard AI as someone with access to virtually unlimited knowledge resources but lacking a deep understanding of how to apply those to your context.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thus, the lesson here is that quality does not fall into our hands unless we demand it. To be able to demand it, we must learn to recognize it. More than training the AI, we still need to train ourselves in fundamentals. Tools amplify our capabilities but also our limitations. An undisciplined team with access to a powerful AI produces poorly architected systems faster. Conversely, a team grounded in solid engineering principles will benefit even from a contextually limited AI.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;At &lt;strong&gt;Polar Squad&lt;/strong&gt;, treating AI as a companion rather than a driver, using it responsibly, and using it to serve the entire delivery lifecycle instead of being a mere coding assistant can make working with it meaningful. If you require human assistance to master AI assistance, we are ready to help you.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Seriously, can we help you out with AI assistance? Let’s chat and see how we can help you ! &lt;/em&gt;&lt;a href="mailto:tuomas.lindholm@polarsquad.com"&gt;&lt;em&gt;tuomas.lindholm@polarsquad.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt; – &lt;/em&gt;&lt;a href="tel:+358401771719"&gt;+358 40 177 1719&lt;/a&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Blog%20thumbnail_Niko%20Heikkila.webp" alt="AI-augmented Software Development: Hype, Vibes and Smoking Production Environments — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Generative AI tools have changed how we developers approach our daily work. Today, headlines tout the arrival of AI-augmented software development and &lt;em&gt;vibe coding&lt;/em&gt; as silver bullets, making development teams orders of magnitude more effective. However, the promised gains are shallow if teams do not also pay attention to the software delivery aspects.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As a software engineer actively using GitHub Copilot and regularly consulting Anthropic's Claude, I have witnessed the power of AI in specific contexts where frequent experimentation and prototyping are valued. AI recalibrates knowledge work so that what once was necessary for humans to handle is now secondary. Meanwhile, the importance of the rest of the knowledge work aspects has grown a thousandfold.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;However, it's crucial to acknowledge that using AI comes with tradeoffs we must not overlook. While AI-augmented software &lt;em&gt;development&lt;/em&gt; can be beneficial, it's equally important to recognize that AI-augmented software &lt;em&gt;delivery&lt;/em&gt; is still a distant goal. Understanding these limitations is key to being well-prepared in our work.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The True Nature of Software Delivery&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There are significant hazards when using AI to accelerate your development stemming from the fact that programming is only a tiny part of the software delivery process. This process, or more suitably referred to as a value stream, encompasses all the thinking, discussing, experimenting, and learning involved in delivering a working software product to end users.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Understanding the Value Streams in Software Delivery&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As defined by&lt;a href="https://www.lean.org/lexicon-terms/value-stream/"&gt; &lt;span style="text-decoration:underline"&gt;Lean Enterprise Institute&lt;/span&gt;&lt;/a&gt;, the value stream includes all of the actions, both value-creating and nonvalue-creating, required to bring a product from concept to launch (&lt;em&gt;development value stream&lt;/em&gt;) and from order to delivery (&lt;em&gt;operational value stream&lt;/em&gt;). Nonvalue-creating actions include unnecessary handoffs, rework due to poor initial design, or delays due to resource constraints.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In software delivery, the value stream encompasses the entire lifecycle from ideation to production deployment and operations. Programming, or codifying requirements and expectations, is only a step. It typically accounts for less than half of the overall delivery. The remaining work involves understanding user needs, designing solutions, testing hypotheses, collaborating across disciplines, and, most importantly, operating amidst organizational barriers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;These non-programming activities are precisely where the most significant delivery challenges and non-value-creating actions emerge. Teams struggle with requirement ambiguity, stakeholder alignment, integration issues, and organizational power dynamics that ultimately determine whether software delivers the intended value. Organizational barriers could include resistance to change, lack of cross-functional collaboration, or unclear decision-making processes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI excels at brainstorming mostly syntactically correct code with some logical defects but remains fundamentally limited in navigating these human-centered aspects of software delivery.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Lean Principles and Identifying Waste in Software Delivery&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The most notable impediments to optimizing value streams and team flow have been researched as part of Lean manufacturing and are called waste (&lt;em&gt;muda&lt;/em&gt;). We can regard everything that does not create customer value as waste.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Mary and Tom Poppendieck&lt;/strong&gt;, in their book&lt;a href="http://www.amazon.com/Lean-Software-Development-Agile-Toolkit/dp/0321150783"&gt;&lt;span style="text-decoration:underline"&gt; &lt;em&gt;Lean Software Development: An Agile Toolkit&lt;/em&gt;&lt;/span&gt;&lt;/a&gt;, popularised the mapping of different types of Lean waste to software delivery impediments.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;According to the Poppendiecks, the primary waste in our work includes:&lt;/p&gt; 
     &lt;ul&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Partially done work (work-in-progress) or every backlog item sitting in the work queue between the backlog and production.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Overproduction, or all the extra features we write while solving the problem.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Relearning and reworking involved for the backlog items moving back and forth between inspection points.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Internal and external handoffs.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Context switching when you need to drop a task and focus on a new one.&lt;/p&gt;&lt;/li&gt; 
      &lt;li&gt;&lt;p class="" style="white-space:pre-wrap;"&gt;Defects that hinder user experience and endanger businesses.&lt;/p&gt;&lt;/li&gt; 
     &lt;/ul&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;There is only so little that AI can do to help overcome this waste. On the contrary, it often only exacerbates it. For instance, AI may generate code that solves a problem but introduces new bugs, leading to rework. It may also encourage overproduction by suggesting unnecessary features. Understanding these limitations is crucial for effectively integrating AI into software delivery processes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As we enter a new era in software development, we must remain vigilant about the effectiveness of our delivery processes and value streams. While AI-augmented development provides the potential to streamline coding and ignite creativity, it cannot replace the fundamental human elements that drive successful software delivery.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Acknowledging AI limitations and committing to optimizing our value streams, we can use it not as a crutch but as a companion for success. In doing so, we not only enhance our effectiveness as developers but also ensure that the software we deliver meets the needs of our users.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The path forward demands a novel approach integrating AI's strengths while exposing and eliminating the waste typically associated with software delivery projects.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Partially Done Work&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thanks to its powerful autocomplete features, AI has the potential to revolutionize software development by completing tasks orders of magnitude faster. However, this added speed also carries multiple risks.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Completing one work cycle faster—in Lean manufacturing terms, moving work from one station to another—can jam the team flow as bottlenecks emerge. The quicker we work in the implementation phase, the more work we pile in front of the bottleneck ahead.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Following &lt;strong&gt;Eli Goldratt's&lt;/strong&gt; &lt;a href="https://www.leanproduction.com/theory-of-constraints/"&gt;&lt;span style="text-decoration:underline"&gt;&lt;em&gt;Theory of Constraints&lt;/em&gt;&lt;/span&gt;&lt;/a&gt;, AI-augmented programming is a perfect candidate for the illusion of &lt;a href="https://fortelabs.com/blog/theory-of-constraints-102-local-optima/"&gt;&lt;span style="text-decoration:underline"&gt;local optima&lt;/span&gt;&lt;/a&gt; where we attempt to improve the total performance of the system by improving the performance of an individual cog only to find out it doesn’t yield the expected results.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Bottlenecks and the Illusion of Speed&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Let's see an example of how that can happen. Consider a team enthusiastically embracing AI coding assistance. Within a couple of weeks, the developers generate code at incredible velocity. However, their testing and review columns are piling up unfinished work. Usually, many pull requests are waiting for review, which the team resolves by rubber-stamping and disbanding the value of code review altogether. Meanwhile, the UI/UX designers are struggling to keep pace and stress that the developers are moving too fast for them.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Without exposing and improving the bottleneck found in testing and review capacity, the team effectively turns their work into a warehouse of partially done work, delaying delivery. Even further delay happens when the code passes the review and it's time to merge the work, but it proves slow and painful due to numerous merge conflicts.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Can AI help with merge conflicts? It is unlikely unless you provide it with full context from both sides of the merge, which is not trivial. Furthermore, having AI review and test features has been the talk of the town for years now, but at the same time, many regulated companies follow the so-called &lt;a href="https://www.openriskmanual.org/wiki/Four_Eyes_Principle"&gt;&lt;span style="text-decoration:underline"&gt;principle of four eyes&lt;/span&gt;&lt;/a&gt;, where a human outside the work context must review the changes.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As DevOps people have been trying to sell continuous delivery mechanisms and reliable pipelines to these companies for a decade with varying success, I'm skeptical that these companies would activate AI code review and testing and automatically let all the changes move to production.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Overproduction&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI answers often provide excessive detail and redundant lines of code when we desire simplicity. While experienced developers can fine-tune their prompts, many users, especially juniors, could unquestioningly accept and use overly complex solutions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While I have nothing against explaining the answers in detail, I seek only a few lines of code when using coding assistance. The less code I receive, the better I follow its intent. Due to the overly helpful nature baked into AI system prompts, they often spit out large parts resembling online tutorials aiding with project setup, file structure, and documentation.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even though you can work around this problem with careful prompting, junior developers often have little experience nor second thoughts about the answers. So, they are happy to copy and paste the answers to their work.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A &lt;a href="https://www.mdpi.com/2075-4698/15/1/6"&gt;&lt;span style="text-decoration:underline"&gt;recent study&lt;/span&gt;&lt;/a&gt; showed that especially younger people with higher reliance on AI tools had more problems with critical thinking than older people. Therefore, it’s not unusual to state that they could treat AI-generated code without criticism, using code that technically looks feasible, but in reality, delivers too much. The result is gross overproduction, over-polishing, and often tight coupling between components. Most of the models have not been trained well with clean software design principles and, by default, tend to produce complete solutions instead of small, iterable experiments.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;AI's Tendency Towards Complexity&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Suppose I ask the AI for help while implementing a date picker component. If it generates hundreds of lines of code, including validation logic, internationalization support, and calendar-like navigation, what should have been an MVP allowing users to select a date has become a tangled mess of features you won't need now—if ever.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The above example perfectly defines overproduction. The code works but includes features users didn't request or need. When the requirements change soon enough, the team modifies the complex initial implementation three times longer than if they had built a minimal solution. The AI optimized for completeness rather than simplicity, creating waste that had to be carried forward or refactored away.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Relearning and Rework&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Accepting AI-generated code without careful review can lead to more work in the future. It's crucial to understand and be comfortable with the code you're working with, as the person changing the code is often someone else from your team.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Having to relearn often leads to heavy code refactoring, which becomes more complex as more overproduction occurs. Of course, you can have AI do the rework, but without pausing to understand the changes you're about to make, you're only placing an order for more rework in the future.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Cost of Unreviewed AI-Generated Code&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;A junior developer might use AI to generate a user authentication system. Without fully understanding the generated code, they integrate it into the codebase. Sometime later, when the team needs to add social login capabilities, the team can't grasp the architecture embedded in the generated code. Thus, the team spends days refactoring the logic before extending it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The team could have saved the time investment had they engaged more deeply with the design in the first place. This pattern repeats in worst codebases with black box AI solutions requiring extensive rework whenever they need modification.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Handoffs&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;No matter how effectively we use AI, handoffs to other people will inevitably happen for many teams. Often, our products move from the initial development team to the maintenance team so the original team can focus on building new business-critical features. In more old-fashioned environments, products move from development to operations for deploying and running those.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Imagine handing over a codebase where AI has generated 90 % of the code. If anything, that is a ticking time bomb. Sure, for handover, you can generate the required documentation and align the roadmap with AI, but with what context and cost?&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Challenge of Domain-Specific Knowledge Transfer&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;AI struggles with domain-specific knowledge transfer, which is the most critical information during handoffs between teams. This limitation stems from how we train AI. While it excels at identifying patterns and generating coherent text, it often lacks sufficient understanding of specialized domain contexts.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For example, a payroll system handling the salaries for municipal employees isn't your average code as it embodies significant regulatory knowledge, compliance requirements, and institution-specific business rules of which AI is unaware. Likewise, healthcare has a bottomless pit of laws, regulations, and essential complexity. Training custom models is possible, but the return on investment is not likely to be profitable as we uncover more complexity during the training.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;When AI generates documentation for complex systems, it can describe the technical architecture and surface-level functionality but cannot capture the many whys behind critical decisions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In the payroll example, why are transactions handled differently on weekends? Why must specific hour reports undergo additional verification steps? These domain-specific rationales are firmly rooted in the institutional knowledge of the team and domain experts who built the system. Past incidents and interpretations often shape this knowledge, which might not be public information.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The most valuable documentation for handoffs addresses these domain-specific nuances: the edge cases, historical context, and business justifications. They explain why a system works as it does. Here, AI falls short, creating a dangerous knowledge gap during team transitions.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;As I explained with partially done work, AI-augmented development creates review bottlenecks, forcing team members to switch contexts between other work items rather than focusing on their work.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Even if we adjust the AI to solve the waste of overproduction and produce only minimum code, we aggravate the problem because a higher number of smaller batches demanding review chokes the throughput and causes even more context-switching.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;More Pull Requests, Less Throughput&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In a team heavily utilizing AI, we see a troubling pattern in the pull request process. Team members submit smaller and more frequent pull requests — averaging 12 per week instead of the previous 3–4.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;While smaller pull requests generally sound better, the increased volume quickly overwhelms the system capacity. Other team members hop between multiple pull requests daily while their work suffers.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;The team suffers from a bottleneck where people write code faster than they can review. Everyone suffers from a high cognitive load and feels exhausted despite AI supposedly making their work &lt;em&gt;easier&lt;/em&gt;. We had optimized AI tools for individual speed at the expense of team flow.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Defects&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Whenever I've asked AI to generate a solution to a problem, it has consistently left out unit tests unless I specifically asked it to write them.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I'm unsurprised, having seen many hastily written codebases during my career. The company behind the AI model has likely trained it with a significant subset of public codebases lacking tests. How could it know tests are needed in every serious programming context if many teams do not lead with an example?&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If AI does not, by default, write tests for you, then all the AI-augmented 10x developers will only deliver more defects than value. Reflecting on the possibility of AI-reviewed code, I doubt it would know to block a pull request when it lacks tests. That is solely the responsibility of the continuous delivery pipeline. Furthermore, when automated tests mean AI testing the code written and reviewed by another AI, it's another ticking time bomb causing havoc and defects in your production environment.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Risks of Neglecting Tests&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In an example scenario, a project using AI-generated modules extensively, passed all the quality assurance checks, and the team deployed the changes to production. Within the first week, the team had discovered critical vulnerabilities in the API layer the AI had implemented.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Despite looking professional and working correctly for the happy path, the code lacked functional verification for edge cases, error handling, and security headers. The developers had asked for an API fulfilling acceptance criteria but hadn't specifically defined the level of testing they needed. The AI obliged by generating code that functionally worked, but its fundamental insecurities were not caught in the delivery pipeline. The rework cost the team weeks of emergency patching and a security incident review, not to mention losing their reputation along with clients.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Making AI Work Meaningful&lt;/strong&gt;&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Though this blog post might make the current situation look bleak, , there is still plenty of hope. Making AI-augmented software delivery work is a matter of using the correct tools correctly. That means instead of accruing waste we must use AI to expose and eliminate it.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;Establishing Quality and Guidelines&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Teams must establish guardrails that ensure AI builds quality rather than accelerates output. Have clear guidelines for how and where AI tools fit your delivery workflow.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;First, we must establish quality gates dictating that the AI-generated code must pass before integration. Those include automated checks in the form of unit and preferably mutation tests, complexity metrics via static analysis, scanning of security vulnerabilities and software bill of materials, and adherence to architectural patterns. When you practice continuous delivery and automate these gates into your CI/CD pipeline, you have created safeguards against the quality issues inherent in AI-generated solutions.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Second, implement and document a team-wide protocol for AI tools usage. For example, consider a policy of not deploying AI-generated code without peer-reviewing your prompt and generated answers. Teams should require their developers to explain the functionality of AI-generated code, ensuring they understand what they're adding to the product. Using agentic tools such as &lt;a href="https://www.cursor.com/"&gt;&lt;span style="text-decoration:underline"&gt;Cursor&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://www.warp.dev/"&gt;&lt;span style="text-decoration:underline"&gt;Warp&lt;/span&gt;&lt;/a&gt;, which propose and implement changes incrementally while keeping the developers in the loop is helpful. Defining and documenting recommended prompting techniques as a shared library is another trick worth considering.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Third, invest in AI literacy across your delivery pipeline. If you have quality assurance specialists, they should share an understanding of how and why they augmented test automation code with AI. Product owners should learn to write requirements without embedding a hallucinated well of wishes from AI. Most importantly, tech leads should become experts in identifying and removing AI waste described in this post.&lt;/p&gt; 
     &lt;h3 style="white-space:pre-wrap;"&gt;&lt;strong&gt;The Path Forward with AI-Augmented Software Delivery&lt;/strong&gt;&lt;/h3&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thinking of AI as a companion instead of a driver plays a key role here. We have already seen increased perceived throughput using AI while sacrificing quality and stability. We must train, tune, and prompt AI to provide quality solutions to balance the situation. At the same time, we still need to continuously educate ourselves on modern software engineering principles. Regard AI as someone with access to virtually unlimited knowledge resources but lacking a deep understanding of how to apply those to your context.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Thus, the lesson here is that quality does not fall into our hands unless we demand it. To be able to demand it, we must learn to recognize it. More than training the AI, we still need to train ourselves in fundamentals. Tools amplify our capabilities but also our limitations. An undisciplined team with access to a powerful AI produces poorly architected systems faster. Conversely, a team grounded in solid engineering principles will benefit even from a contextually limited AI.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;At &lt;strong&gt;Polar Squad&lt;/strong&gt;, treating AI as a companion rather than a driver, using it responsibly, and using it to serve the entire delivery lifecycle instead of being a mere coding assistant can make working with it meaningful. If you require human assistance to master AI assistance, we are ready to help you.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block horizontalrule-block sqs-block-horizontalrule"&gt; 
   &lt;div class="sqs-block-content"&gt;  
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;Seriously, can we help you out with AI assistance? Let’s chat and see how we can help you ! &lt;/em&gt;&lt;a href="mailto:tuomas.lindholm@polarsquad.com"&gt;&lt;em&gt;tuomas.lindholm@polarsquad.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt; – &lt;/em&gt;&lt;a href="tel:+358401771719"&gt;+358 40 177 1719&lt;/a&gt;&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fai-augmented-software-development-hype-vibes-and-smoking-production-environments&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Mon, 24 Mar 2025 22:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/ai-augmented-software-development-hype-vibes-and-smoking-production-environments</guid>
      <dc:date>2025-03-24T22:00:00Z</dc:date>
      <dc:creator>Niko Heikkilä</dc:creator>
    </item>
    <item>
      <title>Where's the undo button? (Part III) — Polar Squad</title>
      <link>https://www.polarsquad.com/blog/wheres-the-undo-button-part-iii</link>
      <description>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/wheres-the-undo-button-part-iii" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Psychological%20Safety%20blog%20post%20series%20Q1-Q2%202024%20(1).webp" alt="Where's the undo button? (Part III) — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;This is the third part of the blog series where we examine the relationship between DevOps and safety. My name is Tuomo Niemelä and I work as a DevOps consultant at Polar Squad which operates in the intersection of people and technology.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;You can read the first part from &lt;/em&gt;&lt;a href="https://polarsquad.com/blog/wheres-the-undo-button-part-i"&gt;&lt;em&gt;here&lt;/em&gt;&lt;/a&gt;&lt;em&gt; and the second part &lt;/em&gt;&lt;a href="https://polarsquad.com/blog/wheres-the-undo-button-part-ii"&gt;&lt;em&gt;here&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Everyday safety&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the previous parts were too technical or academic, don’t worry! There’s still plenty of ways to “do DevOps” in everyday situations. I’m going to list a couple of real life examples relating to the points I listed in the previous part. While some might see these methods and ideas as obvious I still think these things are good to be said out loud.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block quote-block sqs-block-quote"&gt; 
   &lt;div class="sqs-block-content"&gt;  
    &lt;blockquote&gt; 
     &lt;span&gt;“&lt;/span&gt;Safety is not mere emotional weather but rather the foundation on which strong culture is built. The deeper questions are, Where does it come from? And how do you go about building it? 
     &lt;span&gt;”&lt;/span&gt; 
    &lt;/blockquote&gt;  — Daniel Coyle   
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Getting over fear of failure&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of my all time favorites happens during daily meetups. You’re going through who is focusing on what today and are there any blockers. Imagine a situation where your colleague says in a lower tone that one fix he is implementing isn’t going his way. He might even be hinting there’s something wrong with his intelligence or skills. Pause here: This is the exact moment when you need to catch the exposed vulnerability happening in milliseconds and embrace it. You could say something like “That’s ok, I feel such things hard all the time - there was this one time when…” and continue there with something which expresses your vulnerability in exchange.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Previous scenario can happen the other way around also. Once the time is right, I myself might ridicule my own doing in order to give my team members a chance to pick my vulnerability and meet me halfway. Sometimes it works, sometimes not. These things take time and some trust that people want to do the right thing most of the time.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Something about innovation and creativity&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I love brainstorming! You know the situations when it’s allowed to throw ideas no matter how crazy or stupid. Sometimes your colleague catches something essential from that and synthesizes a whole new solution from new ideas combined! Maybe while conducting an ordinary planning or problem solving session we’re not allowed to be as wild as in brainstorming sessions since the amount of useless or incorrect information could distract us from the actual solution.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Still there is something to learn from brainstorming: there is more freedom to fail. When you and your teammates are starting to jell this freedom emerges into any session. Just start with the classics like “I know this sounds stupid but…” or “I know I’m an idiot but…”, works like a charm! All team members should remember the following: Don't get hung up on little details or some syntax errors right away, there is always a chance to refine the solution after.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;About team learning&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sometimes there can be this stigma towards two or more people doing the same thing. Since time is money and to maximize throughput every expensive developer should do only their own thing all the time right? Wrong! If we’re going to work as a team - an antifragile team - there needs to be cooperation and knowledge sharing. It can even happen in any mundane task.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For example while doing a bigger (small is better I know) production deployment with database manipulation I might ask a teammate to join me as an extra pair of eyes. Just in case. Not only does this reduce stress by sharing the burden, it also offers us a chance to share our viewpoints about the whole process and the state of the system or tools.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;It starts with communication&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sarcasm: it’s an art form. I’m sure your intention isn’t to hurt anyone. Anyway if you don’t know when it’s the right place and time to use it on people, then don’t. There is this unwritten rule for when to use sarcasm among close friends or colleagues. Just note that even though you might feel that it is okay to use sarcasm on a person, that person might not feel the same. Sarcasm is dangerous. “But that person just doesn’t understand humor!” No! Now you’re just an asshole.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Lastly there's a couple of things we need to remember: we need to stop downplaying the problems at hand and the successes in the end. Firstly, there's no such thing as a trivial problem. If there were, we wouldn't call them problems. Every person is moving in a different stage in their career path. Something which is trivial to you might not be so trivial to others. Secondly, remember to celebrate even the small wins out loud - together. Software is never going to be fully ready or perfect. Its life cycle continues to evolve after production launch. My point being: don’t ever rob people of feeling good about themselves and don’t get blinded by the continuous improvement cycles.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;It comes down to trust&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But how do we people build trust? That could be yet another topic on its own. In the meantime all I can say is that it usually helps if one isn't a complete asshole. Listen to people and build from there. Showing vulnerability is a leap of faith but around the right people it’s always worth it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once the team starts to jell together, these details mentioned earlier become more natural and automatic. I still would recommend keeping an eye on them especially while the team is new or a new team member is introduced to the pack. Also: take care of your juniors, and someday they might be your seniors. If you want to dig deeper in these topics I highly recommend a book “The Culture Code” by Daniel Coyle. Now let’s end this.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;There’s no such thing as 100% safety: the unspeakable happens&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this journey I wanted to share my viewpoint that DevOps represents first and foremost safety. We’ve been examining some technological practices and solutions which one could implement into systems to gain more safety, possibly making the system more welcoming and manageable for anyone who wishes to learn it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We went through 5 key points in the psychological safety framework and ended it all with real life examples and practical ideas. Since psychological safety is such a huge part of the whole picture, I could almost call technological safety as “everything else”-safety just for balance. Now I want you to go through one last thought experiment.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block quote-block sqs-block-quote"&gt; 
   &lt;div class="sqs-block-content"&gt;  
    &lt;blockquote&gt; 
     &lt;span&gt;“&lt;/span&gt;Everyone has a plan until they get punched in the mouth. 
     &lt;span&gt;”&lt;/span&gt; 
    &lt;/blockquote&gt;  — Mike Tyson   
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I want you to imagine a situation where you are making some major changes to the production environment. Similar changes have been going well to the test environment earlier, tests show green and you feel pretty confident that everything will go just fine. There were processes in place and you followed them. There were some safety measurements in place, but somehow by accident you went around them. Maybe some critical part of the automation like the database backup failed also. The inevitable human error happens and now you wrecked the live production environment big time beyond repair.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's where processes and tools end, and culture begins. How does your team and organization react to these kinds of happenings? Is there a redemption? In what company are you in? What are your values? Systems will fail but that doesn't mean people around you must also fail you.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Bonus tip:&lt;/strong&gt; If you’re conducting “blameless postmortems” while being tense or with overly mechanical efficiency you might still be doing just plain postmortems.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;</description>
      <content:encoded>&lt;div class="hs-featured-image-wrapper"&gt; 
 &lt;a href="https://www.polarsquad.com/blog/wheres-the-undo-button-part-iii" title="" class="hs-featured-image-link"&gt; &lt;img src="https://www.polarsquad.com/hubfs/Imported_Blog_Media/Psychological%20Safety%20blog%20post%20series%20Q1-Q2%202024%20(1).webp" alt="Where's the undo button? (Part III) — Polar Squad" class="hs-featured-image" style="width:auto !important; max-width:50%; float:left; margin:0 15px 15px 0;"&gt; &lt;/a&gt; 
&lt;/div&gt; 
&lt;div class="row sqs-row"&gt; 
 &lt;div class="col sqs-col-12 span-12"&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;This is the third part of the blog series where we examine the relationship between DevOps and safety. My name is Tuomo Niemelä and I work as a DevOps consultant at Polar Squad which operates in the intersection of people and technology.&lt;/em&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;em&gt;You can read the first part from &lt;/em&gt;&lt;a href="https://polarsquad.com/blog/wheres-the-undo-button-part-i"&gt;&lt;em&gt;here&lt;/em&gt;&lt;/a&gt;&lt;em&gt; and the second part &lt;/em&gt;&lt;a href="https://polarsquad.com/blog/wheres-the-undo-button-part-ii"&gt;&lt;em&gt;here&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;Everyday safety&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;If the previous parts were too technical or academic, don’t worry! There’s still plenty of ways to “do DevOps” in everyday situations. I’m going to list a couple of real life examples relating to the points I listed in the previous part. While some might see these methods and ideas as obvious I still think these things are good to be said out loud.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block quote-block sqs-block-quote"&gt; 
   &lt;div class="sqs-block-content"&gt;  
    &lt;blockquote&gt; 
     &lt;span&gt;“&lt;/span&gt;Safety is not mere emotional weather but rather the foundation on which strong culture is built. The deeper questions are, Where does it come from? And how do you go about building it? 
     &lt;span&gt;”&lt;/span&gt; 
    &lt;/blockquote&gt;  — Daniel Coyle   
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
         sqs-narrow-width"&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-4 span-4"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Getting over fear of failure&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;One of my all time favorites happens during daily meetups. You’re going through who is focusing on what today and are there any blockers. Imagine a situation where your colleague says in a lower tone that one fix he is implementing isn’t going his way. He might even be hinting there’s something wrong with his intelligence or skills. Pause here: This is the exact moment when you need to catch the exposed vulnerability happening in milliseconds and embrace it. You could say something like “That’s ok, I feel such things hard all the time - there was this one time when…” and continue there with something which expresses your vulnerability in exchange.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Previous scenario can happen the other way around also. Once the time is right, I myself might ridicule my own doing in order to give my team members a chance to pick my vulnerability and meet me halfway. Sometimes it works, sometimes not. These things take time and some trust that people want to do the right thing most of the time.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;Something about innovation and creativity&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I love brainstorming! You know the situations when it’s allowed to throw ideas no matter how crazy or stupid. Sometimes your colleague catches something essential from that and synthesizes a whole new solution from new ideas combined! Maybe while conducting an ordinary planning or problem solving session we’re not allowed to be as wild as in brainstorming sessions since the amount of useless or incorrect information could distract us from the actual solution.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Still there is something to learn from brainstorming: there is more freedom to fail. When you and your teammates are starting to jell this freedom emerges into any session. Just start with the classics like “I know this sounds stupid but…” or “I know I’m an idiot but…”, works like a charm! All team members should remember the following: Don't get hung up on little details or some syntax errors right away, there is always a chance to refine the solution after.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;About team learning&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sometimes there can be this stigma towards two or more people doing the same thing. Since time is money and to maximize throughput every expensive developer should do only their own thing all the time right? Wrong! If we’re going to work as a team - an antifragile team - there needs to be cooperation and knowledge sharing. It can even happen in any mundane task.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;For example while doing a bigger (small is better I know) production deployment with database manipulation I might ask a teammate to join me as an extra pair of eyes. Just in case. Not only does this reduce stress by sharing the burden, it also offers us a chance to share our viewpoints about the whole process and the state of the system or tools.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;It starts with communication&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Sarcasm: it’s an art form. I’m sure your intention isn’t to hurt anyone. Anyway if you don’t know when it’s the right place and time to use it on people, then don’t. There is this unwritten rule for when to use sarcasm among close friends or colleagues. Just note that even though you might feel that it is okay to use sarcasm on a person, that person might not feel the same. Sarcasm is dangerous. “But that person just doesn’t understand humor!” No! Now you’re just an asshole.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Lastly there's a couple of things we need to remember: we need to stop downplaying the problems at hand and the successes in the end. Firstly, there's no such thing as a trivial problem. If there were, we wouldn't call them problems. Every person is moving in a different stage in their career path. Something which is trivial to you might not be so trivial to others. Secondly, remember to celebrate even the small wins out loud - together. Software is never going to be fully ready or perfect. Its life cycle continues to evolve after production launch. My point being: don’t ever rob people of feeling good about themselves and don’t get blinded by the continuous improvement cycles.&lt;/p&gt; 
     &lt;h2 style="white-space:pre-wrap;"&gt;It comes down to trust&lt;/h2&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;But how do we people build trust? That could be yet another topic on its own. In the meantime all I can say is that it usually helps if one isn't a complete asshole. Listen to people and build from there. Showing vulnerability is a leap of faith but around the right people it’s always worth it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Once the team starts to jell together, these details mentioned earlier become more natural and automatic. I still would recommend keeping an eye on them especially while the team is new or a new team member is introduced to the pack. Also: take care of your juniors, and someday they might be your seniors. If you want to dig deeper in these topics I highly recommend a book “The Culture Code” by Daniel Coyle. Now let’s end this.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="row sqs-row"&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-8 span-8"&gt; 
    &lt;div class="sqs-block image-block sqs-block-image sqs-text-ready"&gt; 
     &lt;div class="sqs-block-content"&gt; 
      &lt;div class="
          image-block-outer-wrapper
          layout-caption-below
          design-layout-inline
          combination-animation-none
          individual-animation-none
          individual-text-animation-none
        "&gt;  
       &lt;div class="image-block-wrapper"&gt; 
        &lt;div class="sqs-image-shape-container-element
              
          
        
              has-aspect-ratio
            " style="position: relative; overflow: hidden; -webkit-mask-image: -webkit-radial-gradient(white, black)"&gt; 
        &lt;/div&gt; 
       &lt;/div&gt;  
      &lt;/div&gt; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
   &lt;div class="col sqs-col-2 span-2"&gt; 
    &lt;div class="sqs-block website-component-block sqs-block-website-component sqs-block-spacer spacer-block sized vsize-1"&gt; 
     &lt;div class="sqs-block-content"&gt;
       &amp;nbsp; 
     &lt;/div&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;h1 style="white-space:pre-wrap;"&gt;There’s no such thing as 100% safety: the unspeakable happens&lt;/h1&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;In this journey I wanted to share my viewpoint that DevOps represents first and foremost safety. We’ve been examining some technological practices and solutions which one could implement into systems to gain more safety, possibly making the system more welcoming and manageable for anyone who wishes to learn it.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;We went through 5 key points in the psychological safety framework and ended it all with real life examples and practical ideas. Since psychological safety is such a huge part of the whole picture, I could almost call technological safety as “everything else”-safety just for balance. Now I want you to go through one last thought experiment.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block quote-block sqs-block-quote"&gt; 
   &lt;div class="sqs-block-content"&gt;  
    &lt;blockquote&gt; 
     &lt;span&gt;“&lt;/span&gt;Everyone has a plan until they get punched in the mouth. 
     &lt;span&gt;”&lt;/span&gt; 
    &lt;/blockquote&gt;  — Mike Tyson   
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div class="sqs-block html-block sqs-block-html"&gt; 
   &lt;div class="sqs-block-content"&gt; 
    &lt;div class="sqs-html-content"&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;I want you to imagine a situation where you are making some major changes to the production environment. Similar changes have been going well to the test environment earlier, tests show green and you feel pretty confident that everything will go just fine. There were processes in place and you followed them. There were some safety measurements in place, but somehow by accident you went around them. Maybe some critical part of the automation like the database backup failed also. The inevitable human error happens and now you wrecked the live production environment big time beyond repair.&amp;nbsp;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;Here's where processes and tools end, and culture begins. How does your team and organization react to these kinds of happenings? Is there a redemption? In what company are you in? What are your values? Systems will fail but that doesn't mean people around you must also fail you.&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;br&gt;&lt;/p&gt; 
     &lt;p class="" style="white-space:pre-wrap;"&gt;&lt;strong&gt;Bonus tip:&lt;/strong&gt; If you’re conducting “blameless postmortems” while being tense or with overly mechanical efficiency you might still be doing just plain postmortems.&lt;/p&gt; 
    &lt;/div&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;  
&lt;img src="https://track-eu1.hubspot.com/__ptq.gif?a=27038360&amp;amp;k=14&amp;amp;r=https%3A%2F%2Fwww.polarsquad.com%2Fblog%2Fwheres-the-undo-button-part-iii&amp;amp;bu=https%253A%252F%252Fwww.polarsquad.com%252Fblog&amp;amp;bvt=rss" alt="" width="1" height="1" style="min-height:1px!important;width:1px!important;border-width:0!important;margin-top:0!important;margin-bottom:0!important;margin-right:0!important;margin-left:0!important;padding-top:0!important;padding-bottom:0!important;padding-right:0!important;padding-left:0!important; "&gt;</content:encoded>
      <pubDate>Tue, 23 Apr 2024 21:00:00 GMT</pubDate>
      <guid>https://www.polarsquad.com/blog/wheres-the-undo-button-part-iii</guid>
      <dc:date>2024-04-23T21:00:00Z</dc:date>
      <dc:creator>Polar Squad</dc:creator>
    </item>
  </channel>
</rss>
