<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Ordinary Tech]]></title><description><![CDATA[This blog focuses on "how" to implement great engineering practices in the ordinary tech companies most of us work at, where budgets are tight, teams are lean, ]]></description><link>https://ordinarytech.blog</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 16:12:43 GMT</lastBuildDate><atom:link href="https://ordinarytech.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Speak Up Without Making People Lose Their Sh*t]]></title><description><![CDATA[I don’t know where you work or how long you’ve been working, but I’m fairly sure you’ve experienced at least one of these situations:

Your manager proposed a new process completely disconnected from ]]></description><link>https://ordinarytech.blog/how-to-speak-up-without-making-people-lose-their-sh-t</link><guid isPermaLink="true">https://ordinarytech.blog/how-to-speak-up-without-making-people-lose-their-sh-t</guid><category><![CDATA[communication]]></category><category><![CDATA[teamwork]]></category><category><![CDATA[conflict]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Sun, 21 Jun 2026 19:47:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65e452e0abe81f98eda7baa1/f24f0673-6529-463e-b375-c522cfb20929.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I don’t know where you work or how long you’ve been working, but I’m fairly sure you’ve experienced at least one of these situations:</p>
<ul>
<li><p>Your manager proposed a new process completely disconnected from reality.</p>
</li>
<li><p>A product manager brought a long list of questionable features while the project was collapsing under technical debt.</p>
</li>
<li><p>A teammate opened a pull request with a quality bar far below what you consider acceptable.</p>
</li>
</ul>
<p>Have you ever stayed silent because you didn’t know how to disagree without starting a conflict? Or maybe you tried to speak up, but the conversation somehow turned into a disaster?</p>
<p>Most of us have such experience.</p>
<p>We often avoid conflict because silence feels safer. But it’s hard to love your work when you’re surrounded by decisions you don’t respect. Great products rarely emerge when nobody challenges the status quo. Teams stop growing when nobody raises the bar.</p>
<p>Even more, unspoken issues rarely disappear. They repeat. Initially small pinch eventually becomes a painful crunch.</p>
<p>Conflict is always an opportunity for improvement. The ability to raise concerns without fear, disagree constructively, and prevent poor decisions is an essential ingredient of a healthy, growing environment.</p>
<p>But if conflict can be a good thing, why do we avoid it?</p>
<p>Because conflict is only productive until it becomes <em>personal</em>, and that happens surprisingly often.</p>
<h1>Why Do Work Discussions Become Personal Conflicts?</h1>
<p>Have you ever noticed how quickly discussions about seemingly ordinary work topics can become emotional? Why does this happen?</p>
<p>Human psychology is complicated and full of nuance, but one explanation appears surprisingly often: <em>many of us associate our work with our identity</em>.</p>
<p>Most engineers don’t just work for eight hours and leave the problem behind when they close their laptop. We think about problems while walking. We solve bugs in the shower. We wake up with solutions at 3 a.m.</p>
<p>That happens because we care. The feeling of “I am a good engineer, I am a professional” becomes part of our identity. Being someone who can solve complex problems with practical, elegant solutions matters to us. And we often treat the outcomes of our work as evidence of our competence, something that reinforces that identity.</p>
<p>And you know what? This is not unique to engineers. Designers, product managers, founders - many people feel the same connection to their work. When we care deeply about what we do, that becomes a part of our definition of who we are.</p>
<p>Now imagine someone criticizing other's work. To the person receiving the criticism, it often doesn't feel like criticism of the work. Sometimes it feels like <em>attack to their identity</em>.</p>
<p>This happens especially often under stress. The amygdala, the part of the brain responsible for threat detection, becomes more active under stress, while the prefrontal cortex, which supports reasoning and self-control, has less influence. The amygdala is fast, but not very accurate: it is designed to notice possible threats quickly, even at the cost of false alarms. Evolutionarily, this is a successful survival strategy: it was safer to react to a threat that was not there than to miss one that was.</p>
<p>Because of that, people under stress <em>may perceive attacks even when none were intended</em>. They stop listening and start defending, counterattacking, or withdrawing - anything except having a useful conversation. This is simply how our brain is wired.</p>
<p>There are two important conclusions from this:</p>
<ol>
<li><p>If you criticize someone’s work, they may become defensive instead of hearing you, and you'll not succeed.</p>
</li>
<li><p>Even if you do not intend criticism, people may still hear it that way.</p>
</li>
</ol>
<p>So, let’s look at a few techniques that can help you express disagreement <em>clearly</em>, but without threatening people’s identity.</p>
<h1>Four Techniques for Productive Disagreement</h1>
<h2>A Dream Behind the Complaint</h2>
<p>This is the simplest technique, and surprisingly often, it’s enough to change the direction of a conversation.</p>
<p>Whenever you notice that something is bad, you usually also have an idea of what “good” would look like. So instead of describing how bad things are, describe the better version you want to see.</p>
<p>Instead of:</p>
<blockquote>
<p>This process makes no sense.</p>
</blockquote>
<p>Try:</p>
<blockquote>
<p>I’d love a process where engineers can deploy routine changes the same day, unless there’s a clear reason for extra approval by 3 persons.</p>
</blockquote>
<p>Instead of:</p>
<blockquote>
<p>This pull request is impossible to review.</p>
</blockquote>
<p>Try:</p>
<blockquote>
<p>I’d find this much easier to review if we split it into a few smaller changes.</p>
</blockquote>
<p>This works because you’re no longer judging the person’s work. You’re describing a desired outcome. The conversation becomes less about blame and more about improvement. It also becomes much easier to act on because they're more specific.</p>
<h2>Anticipate the Fundamental Attribution Error</h2>
<p><a href="https://en.wikipedia.org/wiki/Fundamental_attribution_error">Fundamental Attribution Error</a> is a cognitive bias that became widely known thanks to Daniel Kahneman's book "Thinking, fast and slow".</p>
<p>It is a mental shortcut where we overemphasize a person’s character traits when explaining others behavior. At the same time, when we make mistakes ourselves, we usually blame the situation instead of our own personality.</p>
<p>When my colleague is late, it’s because they're disorganized and don’t respect our time. When I’m late, it’s because traffic was bad.</p>
<p>We often do this unconsciously. And we naturally assume that other people judge our personal qualities. We may overestimate how likely that is, but it still creates stress. This is simply how our brain works.</p>
<p>One of the easiest ways to reduce defensiveness is to explicitly acknowledge the circumstances around the situation.</p>
<blockquote>
<p>I know you’re working on this PR under a tight deadline.</p>
</blockquote>
<blockquote>
<p>I see you created this PR late in the evening.</p>
</blockquote>
<blockquote>
<p>The requirements for this task seem unusually ambiguous.</p>
</blockquote>
<blockquote>
<p>I know we don’t have many good examples of tests in our codebase at the moment.</p>
</blockquote>
<p>By acknowledging situational factors you remove an unspoken fear: the fear that you’re secretly judging. It turns the conversation into a partnership.</p>
<h2>Express Your Need. Clearly</h2>
<p>A surprisingly large number of disagreements come from unmet needs. The problem is that we often hide those needs behind criticism. Instead of explaining what is difficult for us, we explain what is wrong with someone else.</p>
<p>For example, there are many reasons why you might be unhappy with a pull request. Is it too large to review? Did someone send it late in the evening and ask for urgent feedback? Are you concerned about code quality? Do you disagree with the task requirements themselves?</p>
<p>These are very different problems, and they require different actions. And it is worth explaining why you struggle clearly.</p>
<p>When we express our need, people are willing to help, much more often than we can expect. So, try shifting the sentiment from "You’re doing it wrong" to "I’m struggling because..."</p>
<p>Instead of</p>
<blockquote>
<p>Why are you sending a PR at 10 p.m. and expecting everyone else to drop what they’re doing for your poor planning?</p>
</blockquote>
<p>try</p>
<blockquote>
<p>This is critical functionality, and I’m too tired to review it well tonight. I could do it tomorrow with a fresh mind.</p>
</blockquote>
<p>Instead of</p>
<blockquote>
<p>It is poorly designed</p>
</blockquote>
<p>try:</p>
<blockquote>
<p>I’m worried this hidden behavior will increase support load for us</p>
</blockquote>
<p>If the situation is tense and you’re struggling, let others see that. There’s nothing wrong with it.</p>
<p>This way, you’re not attacking their identity, you’re asking for help which creates a very different kind of conversation. The focus moves away from judging another person and toward solving <em>a specific problem</em> you experience.</p>
<p>It also creates empathy. People generally enjoy helping each other, but they can only help when you express what you need. So, do it clearly.</p>
<h2>Be Part of the Solution, Not a Passive Observer</h2>
<p>Nothing kills a discussion faster than criticism without action.</p>
<p>When expressing disagreement, propose specific actions, even small ones, instead of passively judging or arguing for a perfect but unreachable state.</p>
<blockquote>
<p>Please make this a separate PR so it’s easier to review.</p>
</blockquote>
<p>or</p>
<blockquote>
<p>I’m okay with this workaround, but let’s identify the long-term solution before merging.</p>
</blockquote>
<p>Even if you don't know the exact solution, you can still offer partnership:</p>
<blockquote>
<p>Let’s spend 30 minutes together to untangle that. I'm available at 4 p.m.</p>
</blockquote>
<p>I often think about disagreements as a ball of thread. As long as the thread keeps unrolling, the conversation can continue. Statements like these cut the thread:</p>
<blockquote>
<p>I don't like it.</p>
</blockquote>
<blockquote>
<p>I disagree.</p>
</blockquote>
<blockquote>
<p>This is wrong.</p>
</blockquote>
<p>They end the conversation instead of advancing it. A better alternative is:</p>
<blockquote>
<p>I disagree with X, but I see Y and Z as possible alternatives. What do you think?</p>
</blockquote>
<p>The thread continues, and that's where progress happens.</p>
<p>It works because you offer a concrete next step, which can move the conversation out of endless debate or analysis paralysis.</p>
<p>It also creates a partnership dynamic. You become collaborators solving a problem together, instead of one person evaluating the other.</p>
<h1>When You Feel Emotionally Involved</h1>
<p>Everything above helps reduce the chance that the other person becomes emotional. But what to do when someone involved emotionally is you?</p>
<p>The short answer: <em>pause</em>.</p>
<p>First, take a breake and <a href="https://lostechies.com/sharoncichelli/2015/07/10/trust-in-positive-intentions/">remind yourself of positive intentions</a>: you are on the same side, and the person in front of you is not your enemy.</p>
<p>That may sound obvious, but in the heat of an argument, we often start treating colleagues as if they are actively sabotaging us. In reality, they are usually trying to solve the same problem, but from a different perspective. The article I linked above was a game changer for me when I first read it 10 years ago, and I still reread it regularly.</p>
<p>Second, make sure you have enough emotional capacity to carry the conversation through to the end. If you haven’t slept properly for the last few days, your chances of having a constructive conversation are close to zero.</p>
<p>Wait at least until morning.</p>
<p>This advice works surprisingly well outside work too: <em>don’t discuss difficult topics in the evening</em>. In the evening our self-control is depleted, and it becomes harder to regulate ourselves. Casinos open in the evening not by accident. Children’s parties happen in the morning for a reason too.</p>
<p>Third trick that helps enormously is writing before speaking. Draft the message, prepare the opening. Writing forces your brain to slow down and apply a more thoughtful filter. And a good opening often sets the tone for the entire conversation.</p>
<h1>Is It Possible to Never Have Personal Conflict?</h1>
<p>No.</p>
<p>These techniques cover 90% of situations, but there is still the remaining 10%. Personal conflict will still happen from time to time.</p>
<p>That’s why one of the most important skills is learning how to <em>repair</em> relationships after conflict.</p>
<p>Don’t pretend the conflict never happened. Talk about the moment where communication broke down and take responsibility for your behavior - acknowledge the impact it had on the other person, even if that was not intentional. Then explain what <em>you</em> will do differently next time.</p>
<p>The best way I know to repair a relationship at work is to solve the original problem together. For example, instead of continuing to exchange pull request comments until everyone is exhausted, finish the pull request in a pair-coding session.</p>
<p>It is impossible to get rid of conflicts completely, but it is possible to recover from them. Often that makes the relationship even stronger.</p>
<h1>Summary</h1>
<p>Conflict itself is useful. The problem starts when conflict becomes personal.</p>
<p>And that can happen surprisingly easily. Many of us connect our work with our identity. So when someone criticizes our work, we may hear it as criticism of who we are, especially when we’re under pressure.</p>
<p>The goal is not to avoid disagreement. The goal is to express it clearly, but without damaging others:</p>
<ul>
<li><p>describe the better outcome you want, not just what is wrong</p>
</li>
<li><p>acknowledge the context before criticizing</p>
</li>
<li><p>express your actual need clearly</p>
</li>
<li><p>propose a concrete next step instead of passively judging</p>
</li>
</ul>
<p>When you feel emotionally involved, pause. Remind yourself that others have positive intentions. Have some rest before starting a difficult conversation. Write your thoughts down before speaking.</p>
<p>You won’t avoid personal conflict completely. Nobody can. But you can reduce how often it happens, and learn to repair the relationship when it does. That could turn difficult conversations into moments of trust, clarity, and better work.</p>
<h2>Useful resources</h2>
<ul>
<li><p><a href="https://lostechies.com/sharoncichelli/2015/07/10/trust-in-positive-intentions/">Trust in Positive Intentions</a></p>
</li>
<li><p><a href="https://alexturek.com/2022-03-18-How-to-criticize-coworkers/">How To Criticize Coworkers</a></p>
</li>
<li><p><a href="https://lethain.com/constraints-on-giving-feedback/">Constraints on giving feedback</a></p>
</li>
<li><p><a href="https://howtocenterdiv.com/beyond-the-div/nobody-pushed-back">Nobody Pushed Back: Why Engineers Stay Silent Until It's Too Late</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=PHpPtdk9rco">Ted Talk: The Single Most Important Parenting Strategy</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=Cew9-GlC_yk">Inside Standford's "Touchy feely class"</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Empower an Overwhelmed Team.]]></title><description><![CDATA[Imagine inheriting a 25-year-old project weighed down by technical debt and a team exhausted from constant firefighting. That was my reality in 2020 when I joined a new company to take over a project that upper management had labeled "stuck."
Years o...]]></description><link>https://ordinarytech.blog/empower-an-overwhelmed-team</link><guid isPermaLink="true">https://ordinarytech.blog/empower-an-overwhelmed-team</guid><category><![CDATA[EngineeringLeadership]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[kanban]]></category><category><![CDATA[legacy-systems]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[developer experience]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Thu, 03 Apr 2025 21:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746511355980/fd955958-267b-4924-9f67-4f3163d271e7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine inheriting a 25-year-old project weighed down by technical debt and a team exhausted from constant firefighting. That was my reality in 2020 when I joined a new company to take over a project that upper management had labeled "stuck."</p>
<p>Years of mergers and shifting requirements had left the project chaotic, with key domain experts long gone. But over time, my team and I turned things around — today, we’re considered the most effective team in the company. That transformation took a couple of years and taught us valuable lessons.</p>
<p>In <a target="_blank" href="https://ordinarytech.blog/overcoming-teammates-overload">my previous article</a>, I shared how we helped overloaded individuals. But it wasn’t just a few people — the whole team was under pressure. This piece focuses on how we addressed that team-wide overload.</p>
<p>When I joined, teammates often said, “Stakeholders won’t let us recover the project, they just keep piling on requests.” But were the stakeholders the villains? I don’t think so. The real issue was that <strong>stakeholders didn’t know the team’s real capacity</strong>. And to be fair, <strong>how could they if the team didn’t either</strong>?</p>
<p>Once we started examining our own work, we uncovered a lot to improve. It started by identifying the gap between expectations and what the team could actually deliver.</p>
<h1 id="heading-what-was-the-team-expected-to-deliver">What Was the Team Expected to Deliver?</h1>
<p>When I joined, there wasn’t much clarity around how work was structured. That’s not unusual. When you’re constantly putting out fires, it’s hard to find time to zoom out and look at the bigger picture.</p>
<p>So I spent my first three months observing everyday work and what stakeholders typically expected from us. Here’s how work was <em>supposed</em> to be divided among our six engineers:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746089957106/276f78b1-5d64-4eab-a71e-8cac2a1a7f6e.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746089957106/276f78b1-5d64-4eab-a71e-8cac2a1a7f6e.png" alt="A chart with three sections labeled &quot;Epics,&quot; &quot;Standard,&quot; and &quot;Support+Intangible.&quot; Each section has two or three items, each prefixed with a person emoji, with labels like &quot;Epic 1,&quot; &quot;Standard 1,&quot; and &quot;Support ticket 1.&quot;" class="image--center mx-auto" /></a></p>
<center><small>Initial expectations from the team</small></center>

<ul>
<li><p><strong>Epics</strong>: large, high-priority initiatives. They always consist of multiple tasks, and come with deadlines. My team was expected to work on three Epics at once.</p>
</li>
<li><p><strong>Standards</strong>: smaller feature requests without natural deadlines. However, the team followed a Kanban SLA practice that tasks shouldn't stay in the backlog for more than six weeks. This rule distracted the team, leading them to focus on low-value work just to meet the SLA.</p>
</li>
<li><p><strong>Support</strong> included customer issues that frontline support couldn’t solve and ad hoc requests from other teams using our system. The expectation here was a quick turnaround — basically, “fix it now”.</p>
</li>
<li><p><strong>Intangible work</strong> was another <a target="_blank" href="https://www.scrum.org/resources/blog/classes-service-kanban-what-are-they">class of service borrowed from Kanban</a>. For us, it meant technical debt. One engineer was supposed to handle it, but only if there were no Support tickets waiting which, of course, happened rarely.</p>
</li>
</ul>
<p>The first clear problem with this setup was that <strong>every task came with some kind of commitment</strong> — a deadline, an SLA, or the urgency of a support ticket. In practice, this meant each engineer was always tied up. So when someone got stuck or an incident happened (which was often), the only option was to drop some commitment. We had no flexibility to absorb surprises, and since surprises were routine, we routinely failed to deliver on our promises. To put it plainly, <strong>our initial setup made failing commitments almost inevitable</strong>.</p>
<p><strong>The second issue was siloed work.</strong> Each engineer had their own tasks and mostly worked alone, which meant limited knowledge sharing. Since we were always under pressure, tasks were assigned to those who already had the necessary expertise, leading to even more knowledge silos over time. This, in turn, led to multitasking because it was rare for the most knowledgeable person to be available when an urgent request came up.</p>
<p>The third problem was that <strong>all of this was considered the baseline</strong>. So whenever someone took a vacation, got sick, or left the team, something would fall behind. If you do the math — six engineers, each with four weeks of vacation — almost half the year, someone is unavailable. Yet, the expectations never changed.</p>
<p>These are classic signs of an <a target="_blank" href="https://longform.asmartbear.com/utilization/">overutilized team</a>, and that was considered a minimal expectation for us.</p>
<p>But the reality was even worse because those four categories didn't cover everything.</p>
<h1 id="heading-facing-reality-the-unseen-workload">Facing Reality: The Unseen Workload</h1>
<p>While clarifying expectations, I also tracked what was actually happening in the team's daily work. It didn't take long for the numbers to reveal their own story: on average, we dealt with <strong>five to six urgent tasks per month</strong>, whether it was a production incident or a last-minute business request.</p>
<p>For example, our Kafka mirroring cluster went down three times during my first two weeks on the project. Each time, the team hurried to get it running again. The better solution was to move a couple of services to the other data center and get rid of this mirroring cluster, but that required more effort. When you're always in emergency mode, there's no time for strategic fixes.</p>
<p>According to the data I gathered, such tasks took an average of four days to resolve, meaning that <strong>the team was dealing with at least one unexpected, urgent issue at any given time.</strong> So, the reality was that we had an ongoing stream of unseen work that wasn't considered in the expectations, yet it took up a significant amount of our time and energy.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746259853405/a5aa84c7-db69-41c3-8c46-9ecf17abac7f.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746259853405/a5aa84c7-db69-41c3-8c46-9ecf17abac7f.png" alt="The same tasks board with additional category of Expedites. Expedites highlighted with a note saying &quot;6 expedites per month according to stats.&quot;" class="image--center mx-auto" /></a></p>
<center><small>The reality: we have 6 Expedites monthly and no planned capacity to address them</small></center>

<h1 id="heading-aligning-expectations-with-reality">Aligning Expectations with Reality</h1>
<p>Given the lack of trust from stakeholders, I knew we couldn’t overhaul everything overnight. We needed to take small, deliberate steps.</p>
<h2 id="heading-step-1-minimizing-distractions">Step 1: Minimizing Distractions</h2>
<p>The first priority was to <strong>carve out space for the team to focus</strong> by reducing the constant stream of expedites.</p>
<p>I shared my findings with the VP of Engineering using the same images you saw above. To address the issue, I proposed reducing the number of simultaneous Epics from three to two and introducing a weekly rotating <strong>Quarterback</strong> role within the team.</p>
<p><strong>The Quarterback would</strong> <strong>handle anything that threatened the team’s progress</strong>, whether it was an urgent incident, a teammate stuck on a task, or an unexpected blocker. Essentially, the Quarterback’s role was to manage chaos so the rest of the team could remain focused.</p>
<p>In the short term, this should help us to shield the team from distractions. In the long term, the goal was to get ahead of recurring problems and fix them at the root so we’d stop getting pulled into emergency mode so often.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746344060834/7f4bebb3-70cb-4421-bafb-90abf6e6ced1.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746344060834/7f4bebb3-70cb-4421-bafb-90abf6e6ced1.png" alt="The same tasks board with added &quot;Quarterback&quot; emoji that is connected with arrows with each of the swimlanes meaning that this roile is intended to work on any of the swimlanes." class="image--center mx-auto" /></a></p>
<center><small>Quarterback "attacks" everything unexpected, keeping the rest of the team focused.</small></center>

<p>We also started tracking where the Quarterback spent their time each week.</p>
<p>At first, the quarterback role was fully occupied with the “red swimlane” — urgent issues and constant firefighting. But instead of just applying quick fixes, we started tackling the root causes behind these incidents. That shift to a systemic approach paid off. We <strong>reduced the number of monthly incidents from six to one</strong> (and, as of 2025, we have one incident per quarter). In addition, we’re now resolving incidents much faster, thanks to having a dedicated teammate to solve them and to improve our most painful troubleshooting tools.</p>
<p>Eventually, the Quarterback role began shifting toward less urgent "yellow swimlane" tasks, indicating we were ready for the next step.</p>
<h2 id="heading-step-2-shifting-from-risk-reaction-to-risk-prevention">Step 2: Shifting from Risk Reaction to Risk Prevention</h2>
<p>The next thing to tackle was our approach to technical debt.</p>
<p>Stakeholders often complained that the team spent too much time on “technical stuff” instead of delivering visible progress. At the same time, engineers were frustrated that they <em>couldn’t</em> spend time on technical debt at all.</p>
<p>As usual, the truth was somewhere in the middle.</p>
<p>Officially, one engineer was supposed to handle technical debt (tasks labeled as “Intangible”), but only if there were no Support tickets in the queue. <strong>According to the data, we averaged four Support tickets at any given time.</strong> <strong>As a result, technical debt was never addressed in a planned manner.</strong></p>
<p>This wasn't new. A couple of years earlier, leadership decided to delay addressing the technical debt "until things got better." Of course, things didn't improve; they got worse. The team still dealt with tech debt, but only when it blew up as production issues — another instance of unmet expectations, leaving stakeholders frustrated. So, we <em>actually</em> <em>were</em> spending time on technical issues, but only <em>reactively</em>, when they’d already caused damage.</p>
<p>One example really highlighted this issue: we had a backlog item to upgrade CentOS 6, which reached its end-of-life in November 2020. We knew about this issue, but never had time to prioritize it. When November 30 arrived, CentOS 6 was removed from official repositories, and our delivery pipelines broke.</p>
<p>We got the pipelines working again with a custom image. But here was the bigger issue: running production on an unsupported OS lowered our company’s security rating, hurting its reputation and raising insurance costs.</p>
<p>So we had to drop everything and urgently upgrade the OS. Except it wasn’t just a simple upgrade — some of our customers were still using outdated SSL/TLS protocols that newer OS versions didn’t support. That meant we had to coordinate changes with customers before proceeding. It turned into a months-long distraction, all because we hadn’t addressed a well-known risk in a planned way.</p>
<p>This made one thing painfully clear: the so-called “intangible” work had very <em>tangible</em> consequences.</p>
<p>I went back to our VP of Engineering with a new proposal. We would remove the unclear "Intangible" label and stop using the overused term "technical debt." Instead, we would <strong>divide technical work into two clear categories: risk reaction and risk prevention</strong>. We would allocate planned capacity to focus on risk prevention, encouraging the Quarterback to spend as much time on this area as possible.</p>
<p>The goal was simple: tackle potential issues before they became emergencies.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746261633929/8b6fae1c-033a-4048-9c17-f32d28b1578d.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746261633929/8b6fae1c-033a-4048-9c17-f32d28b1578d.png" alt="The same tasks board, but the numer of Expedites reduced to 1 per month from 6. New swimlane named &quot;Risks prevention&quot; added." class="image--center mx-auto" /></a></p>
<center><small>The number of Expedites has been reduced to one per month. Now we are able to proactively eliminate technical risks.</small></center>

<h2 id="heading-step-3-empowering-the-team-with-autonomy">Step 3: Empowering the Team with Autonomy</h2>
<p>Once we reduced the number of emergencies, the team finally had room to breathe. We could meet our commitments more consistently and, just as importantly, start rebuilding trust with stakeholders.</p>
<p>The Quarterback began spending more and more time in the risk prevention swimlane. They addressed long-standing technical issues, resolved major security problems, improved diagnostics and support tools, and fixed weak areas in our delivery pipeline. These changes made the entire team more productive and helped rebuild stakeholders' trust even more.</p>
<p>With that gained trust, we made three more important changes:</p>
<ol>
<li><p><strong>We got rid of the six-week SLA for Standard tasks.</strong> It had been pushing the team toward overcommitment and inflexible planning. After a lot of discussion, we agreed this practice was doing more harm than good.</p>
</li>
<li><p><strong>We stopped requiring approval for each individual technical task.</strong> At this point, we were consistently delivering what stakeholders expected, which gave us enough credibility to take more ownership. We no longer had to negotiate each technical task. Instead, we began focusing on what was most valuable at the moment, with an agreed-upon capacity of one teammate, rather than sticking to decisions made weeks earlier.</p>
</li>
<li><p><strong>We shifted from the initial “push” approach</strong> — assigning tasks to the team based on a monthly plan — <strong>to a “pull” model</strong>, where each teammate picks up the next task only after completing their current one. If someone is blocked, they don't take on a new task. Instead, the team focuses on resolving the blocker. If the blocker will be present for a long time and can't be addressed immediately, a teammate first offers help to others with their tasks before taking on a new one.</p>
</li>
</ol>
<p>By now, most of the technical risks had been addressed, which meant we could shift more energy toward planned <em>development</em> work.</p>
<p>And since this was a 25-year-old system that had been under constant pressure for years, there was no shortage of messy problems that needed fixing. One example: our end-to-end tests were running on a version of Chrome that was 40 versions behind the current one. Obviously, those tests couldn’t reliably tell us if the app worked for real users.</p>
<p>Now we've got both the time and the autonomy to clean up that kind of stuff, too.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746262473121/604fad2d-2527-4d30-8d1c-df2b72cea8bf.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746262473121/604fad2d-2527-4d30-8d1c-df2b72cea8bf.png" alt="The same tasks board, but there is no more Expedites swimlane and Quarterback emoji. &quot;Risks prevention&quot; swimlane is renamed to &quot;Technical development&quot;. Each of the swimlanes has at least one dedicated teammate." class="image--center mx-auto" /></a></p>
<center><small>The Quarterback role is no longer needed. We can plan our work and be confident that the plan is realistic.</small></center>

<h2 id="heading-step-4-achieving-more-with-less">Step 4: Achieving More with Less</h2>
<p>The changes we’d made so far helped a lot. We improved our technical foundation, reduced emergencies, and started delivering more predictably. But some deeper issues remained.</p>
<p>First, <strong>everyone was still working solo</strong>. That limited knowledge sharing and made the team fragile — if someone was out sick or on vacation, progress stalled. Also, some of our Epics could’ve moved faster if we had worked together, but we weren’t set up for that.</p>
<p>Second, we had already dealt with the easy parts of technical debt. What remained were large, high-impact projects, like moving all secrets (passwords, tokens, certificates) to the <a target="_blank" href="https://www.hashicorp.com/en/products/vault">Vault</a> to secure our delivery chain and improve our audit readiness. <strong>But these efforts required months of focused, coordinated work — something our current setup just wasn’t built to support.</strong></p>
<p>So I went back to the VP of Engineering and pitched a new approach:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746093086280/0857d2e0-70f6-49a2-aa3b-d3dbd69f9dd5.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746093086280/0857d2e0-70f6-49a2-aa3b-d3dbd69f9dd5.png" alt="The same tasks board but with only two swimlanes left: &quot;Epics&quot; and &quot;Standards&quot;. &quot;Epics&quot; has four people on it, &quot;Standards&quot; swimlane has two people. " class="image--center mx-auto" /></a></p>
<center><small>Now work is completed within small teams, knowledge is shared, and no one works alone.</small></center>

<p><strong>1. Fewer activities but approached as teams</strong></p>
<p>We collapsed our work into two swimlanes: <strong>Epics</strong> and <strong>Standards</strong>, not based on priority, but on <em>how</em> the work should be done.</p>
<ul>
<li><p><strong>Epics</strong> are major initiatives that require focus, flexibility, and collaboration. Success means delivering on time and with high quality. So, we assigned four of our six engineers to this swimlane and let them self-organize. They could choose to work as one team of four or split into two teams of two. They could decide whether to pair-program or mob-program to share knowledge or work on tasks in parallel. We kept a hard limit of two concurrent Epics to prevent overstretching.</p>
</li>
<li><p><strong>Standards</strong> became a catch-all for smaller work: feature requests, minor tech tasks, and support tickets. Two engineers handled this swimlane and had full autonomy to decide how to balance the workload. If Support tickets were piling up, they could team up to get through them faster. If things were quiet, they could knock out Standard tasks.</p>
</li>
</ul>
<p><strong>2. "Standards" is an adjustable swimlane</strong></p>
<p>If the Standards swimlane wasn’t busy, we had the flexibility to temporarily move one or both engineers to help with a certain Epic. This gave us the agility to handle high-impact work in the best possible way.</p>
<p><strong>3. Rotations for growth and resilience</strong></p>
<p>To avoid burnout and build shared knowledge, we introduced regular rotation between the two swimlanes. Everyone got the chance to work on both Epics and Standards swimlanes. This encouraged skill growth, avoided silos, and promoted natural knowledge sharing through pair or even mob programming.</p>
<p>The biggest benefit of these three changes was that they gave us the <strong>autonomy to decide how to do our work</strong>. Of course, autonomy requires a certain level of team maturity. But the thoughtful steps we’d taken over the past two years helped us get there. Each team member now understands the value of our work, how we make and keep commitments, and how we handle risk.</p>
<p>Another significant change was that <strong>we started doing fewer tasks, but with more focus</strong>. We reduced our commitments from six (sometimes more) to just four. This allowed us to complete tasks faster and with higher quality. It also made the team more resilient: if someone working on an Epic went on vacation, progress didn’t stop. Planning time off became easier and less stressful.</p>
<p>This is how “doing more with less” looks in action: fewer things in progress, but more things getting done.</p>
<p>And it worked.</p>
<p>We started consistently meeting — and often exceeding — business expectations. The team gained a reputation across the company as one of the most effective, reliable, and forward-thinking. We became a go-to example of how to build mature development practices: <a target="_blank" href="https://ordinarytech.blog/integrating-security-into-development-process">integrating security into development</a>, <a target="_blank" href="https://ordinarytech.blog/dora-metrics">adopting continuous delivery</a>, and systematically eliminating whole classes of risk.</p>
<h1 id="heading-what-was-the-impact-of-these-changes">What Was the Impact of These Changes?</h1>
<p>I believe the most meaningful measure of success is the change in sentiment. Upper management no longer labels us a “stuck” team—in fact, we’re now often cited as the most efficient team in the company. Burnout has noticeably declined, and team members report feeling far more satisfied with their work. They now see their contributions as impactful and meaningful.</p>
<p>However, I intended to give you some numbers.</p>
<p>On average, we now complete tasks <strong>five times faster than before</strong>. Reducing multitasking, enabling deep focus, sharing knowledge, improving the codebase, and investing in tooling helped us speed up every phase of delivery: development, code review, testing, and deployment.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746603904615/b102be3d-ea4f-435d-854a-379d71566f81.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746603904615/b102be3d-ea4f-435d-854a-379d71566f81.png" alt class="image--center mx-auto" /></a></p>
<center><small>Average Task Cycle Time (from implementation to deployment): Summer 2023 vs. Summer 2020</small></center>

<p>Even more importantly, our <strong>predictability</strong> has improved. The standard deviation of task completion time has <strong>dropped 4×</strong>, thanks to reduced uncertainty in the codebase and tooling, more pairing, and fewer interruptions. Today, when we commit to something, we do it with much more confidence.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746604097599/94101326-968e-4cb3-9e80-c3c491278099.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746604097599/94101326-968e-4cb3-9e80-c3c491278099.png" alt class="image--center mx-auto" /></a></p>
<center><small>Task Cycle Time – Standard Deviation: Summer 2023 vs. Summer 2020</small></center>

<p>Perhaps the most illustrative metric is our <strong>average pull request lead time</strong> — the time from the first commit to merging into the main branch. It captures the full story: the dip in performance after the company split and subsequent acquisition, the impact of losing key engineers, and then the steady improvement after we reshaped how we work. When I joined, PR lead time averaged 17 days. Today, it’s just 1 day:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1746606067483/7146ac65-ca02-4ea3-83e9-f763b92c3dd9.jpeg"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746606067483/7146ac65-ca02-4ea3-83e9-f763b92c3dd9.jpeg" alt class="image--center mx-auto" /></a></p>
<center><small>Pull Request Lead Time – From First Commit to Merge into “main”</small></center>

<p>Of course, that single metric doesn’t capture everything, but it shows how much is possible when you stop overwhelming a team and give them the space to focus.</p>
<p>It’s difficult — if not impossible — to isolate the impact of any single change. The improvements we’ve seen are the result of many adjustments compounding over time. For example, <a target="_blank" href="https://ordinarytech.blog/integrating-security-into-development-process">integrating security into development</a> and <a target="_blank" href="https://ordinarytech.blog/dora-metrics">achieving measurable delivery improvements with DORA metrics</a> (among others, I haven’t written about yet) significantly boosted our effectiveness, and none of it would’ve been possible without the process changes described in this article.</p>
<p>Are you curious about the other changes we introduced to make this happen? That’s exactly why I created this blog. I’ll be sharing more stories and lessons — stay tuned.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>When a team struggles to meet expectations, the reflex is to tighten control. But in reality, the opposite is often needed: <strong>teams need some slack to absorb risks</strong>.</p>
<p>What I hope this story shows is that meaningful change doesn’t have to start big. You can shift a team out of firefighting and rebuild trust through small, deliberate steps: aligning expectations, proactively resolving tech debt, letting the team shape how they work, and doing fewer things better.</p>
<p>Much of what worked for us reflects core Kanban practices: start with what you have, limit work in progress, make process policies explicit, and let the team self-manage and focus on outcomes.</p>
<p>Is our current approach perfect? Not at all. Swimlane rotations rarely line up cleanly — people finish work at different times, and deep-focus tasks don’t pause just because it’s rotation day. Moreover, Support and Standard tracks often drift apart again, with one person handling all the Support requests while another works through Standard tasks alone.</p>
<p>But that’s okay — this isn’t a fixed configuration. Continuous improvement is about learning, adapting, and making the system a little better. Repeatedly.</p>
<hr />
<p>Life is so beautiful,</p>
<p>Fedor</p>
]]></content:encoded></item><item><title><![CDATA[Relying on Heroes Is Not a Strategy]]></title><description><![CDATA[Imagine inheriting a 25-year-old project weighed down by technical debt and a team exhausted from constant firefighting. That was my reality in 2020 when I joined a new company to take over a project ]]></description><link>https://ordinarytech.blog/overcoming-teammates-overload</link><guid isPermaLink="true">https://ordinarytech.blog/overcoming-teammates-overload</guid><category><![CDATA[Healthy Environment ]]></category><category><![CDATA[Bus factor]]></category><category><![CDATA[Team Wellbeing]]></category><category><![CDATA[Toil Reduction]]></category><category><![CDATA[efficiency]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Sat, 01 Feb 2025 15:54:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728293847028/9913f5e6-17e7-4d3f-b434-3ee1297e2b72.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine inheriting a 25-year-old project weighed down by technical debt and a team exhausted from constant firefighting. That was my reality in 2020 when I joined a new company to take over a project that upper management had labeled "stuck."</p>
<p>Years of mergers, acquisitions, and losing domain experts had left the project in disarray. Outdated tools made even the simplest tasks difficult. Yet, despite these challenges, my team and I turned things around. The project runs smoothly today, and we take pride in what we’ve built. But getting here was no easy feat — it was a journey full of lessons about managing burnout, technical debt, and teamwork.</p>
<p>One of my first challenges was dealing with the crushing workload on certain team members. Many were stuck in "firefighting" mode, unable to focus on structured, strategic work. This not only burned them out but also held the entire project back.</p>
<p>In this article, I'll explain how we helped one overwhelmed team member regain control. I also wrote another piece about <a href="https://ordinarytech.blog/empower-an-overwhelmed-team">how to help the whole team</a>.</p>
<h1>The Signs of Trouble</h1>
<p>My first day started with a flood of emails about the same issue: our logging system was down, triggering nonstop alerts that annoyed the team and a dozen other managers.</p>
<p>What happened? Our DevOps engineer (let’s call him Andrew) was updating our logging tool, Graylog, when the Kafka mirroring cluster on production went down. Andrew, the only person who knew how to fix it, had to drop everything to resolve the issue, leaving the Graylog update unfinished and still firing alerts.</p>
<p>It didn’t take long to see that Andrew was under immense pressure. During our first one-on-one, he admitted, "I’m feeling a bit worn out." He constantly bounced between crises, with no time to address root causes or make lasting improvements.</p>
<p>Worse, we had a <a href="https://en.wikipedia.org/wiki/Bus_factor">bus factor</a> of one for Andrew. If one day he decided to leave (and the chances of that were higher because he was overwhelmed), the whole project would be in serious trouble.</p>
<p>This was a typical antipattern often described as “<em>DevOps is a culture, not the role</em>”. Relying too heavily on one person is a way to a fragile, unsustainable system.</p>
<p>Hiring another DevOps engineer seemed simple, but we had strict staffing limits. But even if we could, it wouldn’t fix the core issue: Development and Operations were working in silos despite being part of the same team.</p>
<h1>Diagnosing a Root Cause: a Pile of Unfinished Work</h1>
<p>So, why was Andrew so overwhelmed?</p>
<p>The root issue was a mountain of unfinished work—quick fixes and half-implemented solutions that had built up over the years. These incomplete tasks required constant maintenance, catching Andrew in a <a href="https://en.wikipedia.org/wiki/Vicious_circle">vicious cycle</a> of firefighting.</p>
<p><a href="https://cdn.hashnode.com/res/hashnode/image/upload/v1738534690336/1decb464-a0d0-4569-bfdf-ce3005aba5c2.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738534690336/1decb464-a0d0-4569-bfdf-ce3005aba5c2.png" alt="" style="display:block;margin:0 auto" /></a></p>
Vicious cycle of firefighting

<p>Take the abovementioned Kafka mirroring cluster, for example. It crashed three times in my first two weeks. After the third crash, I reached out to Andrew to find a systemic solution, and 15 minutes later, we realized we could move two services to the primary data center, making this Kafka mirror unnecessary. Years ago, this mirror was introduced as a temporary solution to migrate some services to the primary data center for GDPR compliance. But like many temporary solutions, it stuck around indefinitely.</p>
<p>Another example was our messy production setup. We had four different deployment methods: RPM installations on VMs, Docker, self-managed Kubernetes, and AWS. Only Andrew knew how to manage all of them. Over the years, these tools were introduced but never fully adopted, creating a <a href="https://en.wikipedia.org/wiki/Lava_flow_%28programming%29/">"Lava Flow" anti-pattern</a> — an incomplete, messy setup that hardened into permanent. We simplified deployments by standardizing on Docker and cutting 70% of our Jenkins code, reducing cognitive load and allowing the whole team to become proficient in our delivery pipelines.</p>
<p>And, of course, there were plenty of undocumented tasks that only Andrew knew how to do.</p>
<p>The pattern was clear: unfinished work was the cause and the effect of Andrew’s overload.</p>
<h1>The Solution: Identifying and Distributing Workload</h1>
<p>The solution was obvious:</p>
<ul>
<li><p>Identify all unfinished tasks.</p>
</li>
<li><p>Spread responsibilities across the team to reduce reliance on a single person.</p>
</li>
<li><p>Free up Andrew’s time for high-value work instead of constant interruptions.</p>
</li>
<li><p>Ensure the team can operate smoothly even when Andrew is away.</p>
</li>
</ul>
<p>This is easier said than done, starting from identifying unfinished work. When someone is overwhelmed, it’s hard to see the whole picture and identify long-term solutions. Andrew could only recall the latest fires he had put out, a common experience for those stuck in reactive mode.</p>
<p>I turned to David Allen’s <em>Getting Things Done</em> method and its “trigger list” technique. This method helps to offload mental clutter and identify everything demanding attention. For us, it was a way to detect unfinished work.</p>
<p>I customized <a href="https://miro.com/app/board/uXjVLZMKDOM=/">my "triggers list"</a> for Andrew, adding all our services and tools.</p>
<p><a href="https://cdn.hashnode.com/res/hashnode/image/upload/v1727955367815/b75327d8-8e7b-4de0-bfa8-715d67986eee.jpeg"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727955367815/b75327d8-8e7b-4de0-bfa8-715d67986eee.jpeg" alt="" style="display:block;margin:0 auto" /></a></p>
Triggers list inspired by David Allen’s Getting Things Done (GTD) methodology

<p>Then, I asked Andrew to:</p>
<ol>
<li><p>Set aside a few hours without distractions—no phone, no messaging apps.</p>
</li>
<li><p>Go through a list of triggers and jot down any tasks that come to mind. Don’t try to organize them — aim for quantity, not quality. Refining comes later.</p>
</li>
<li><p>When your mind feels clear, take a break. Avoid doing mental work—go for a walk or have a meal instead. More tasks will likely come to mind, and they should be written down, too.</p>
</li>
</ol>
<p>Andrew was on board with the idea since he felt overwhelmed by work and personal matters. We agreed he’d go through this exercise and share the work-related part with me to tackle them together.</p>
<p>But unsurprisingly, he struggled to find the time. So, I suggested he take two days off solely to focus on creating the list. The next day, he returned with a stack of notes we finally turned into 200 tasks!</p>
<h1>Sharing the Work and Spreading Knowledge</h1>
<p>We broke that massive list into manageable categories and identified an approach for each:</p>
<ol>
<li><p><strong>Teach:</strong> Many tasks, like restoring Kafka, weren’t difficult — just undocumented. We started informal "morning coffee" sessions where we broke production and Andrew guided us through hands-on exercises. We recorded each session, creating a reference library for the future.</p>
</li>
<li><p><strong>Document:</strong> we had a massive lack of documentation about our day-to-day maintenance routines. Instead of forcing full documentation upfront, we wrote instructions as tasks arose. Then, another teammate would follow the steps from the doc and refine it, ensuring clarity and usability.</p>
</li>
<li><p><strong>Do:</strong> We divided tasks into three categories: those only Andrew could do, those requiring his guidance, and those the team could handle independently. Interestingly, some of Andrew’s routine tasks were exciting challenges for others. We assigned tasks based on each person’s interests to contribute meaningfully and learn.</p>
</li>
</ol>
<h1>The Results</h1>
<p>Over the next few months, we redistributed Andrew’s workload. For example, I took over our delivery pipelines, reworking them and <a href="https://ordinarytech.blog/dora-metrics">improving delivery speed and stability with DORA metrics</a>.</p>
<p>Our litmus test was whether Andrew could spend a vacation without our emergency calls.</p>
<p>It took nearly a year, but we got there. The project became significantly more stable, and the team gained the confidence to handle long-standing issues independently. We fully embraced Continuous Integration and Delivery (CI/CD), transitioned to Infrastructure as Code (IaC), and improved our observability tools. DevOps was no longer just Andrew’s responsibility—it became embedded in our team culture.</p>
<p>The impact was not just surface-level adjustments. We delivered real, measurable results:</p>
<ul>
<li><p><strong>Incident frequency dropped</strong>: We used to have 1–2 urgent incidents per week. Within a year, this was reduced to one every 2–3 months, allowing us to focus on long-term goals and improving our ability to meet expectations, restoring the business’s trust in our commitments.</p>
</li>
<li><p><strong>Proactive risk mitigation</strong>: Instead of scrambling to fix last-minute critical vulnerabilities, we managed risks ahead of time, preventing disruptions before they occurred.</p>
</li>
<li><p><strong>Risk Reaction vs. Prevention:</strong> Previously, we mainly dealt with problems only after they blew up, like updating an outdated OS only after the vendor removed the image. A year later, we were handling most risks proactively, preventing disruptions before they could occur.</p>
</li>
</ul>
<h1>Conclusion</h1>
<p>Burnout and over-reliance on a single person are signs of an unhealthy work environment, not individual failings. The reason is often piles of unfinished work that repeatedly disrupt the team.</p>
<p>If some of your teammates are in a similar situation, take action:</p>
<ul>
<li><p>Identify and prioritize unfinished tasks.</p>
</li>
<li><p>Distribute responsibilities across the team.</p>
</li>
<li><p>Foster a culture of knowledge sharing.</p>
</li>
<li><p>Ensure the team can function smoothly, even in the absence of key members.</p>
</li>
</ul>
<p>This transformation won’t happen overnight, but each step makes a difference. Aim for the ultimate sign of success: you and your team can take a well-earned break without emergency calls.</p>
<hr />
<p>Life is a lot better when the fires stop burning.</p>
<p>Fedor</p>
]]></content:encoded></item><item><title><![CDATA[Revitalizing a 25-Year-Old Project with DORA]]></title><description><![CDATA[Imagine inheriting a project burdened with technical debt and a team exhausted by firefighting. This was my reality in 2020 when I took over a 25-year-old project labeled as "stuck" by top management. Overwhelmed by years of mergers, losses of domain...]]></description><link>https://ordinarytech.blog/dora-metrics</link><guid isPermaLink="true">https://ordinarytech.blog/dora-metrics</guid><category><![CDATA[Devops]]></category><category><![CDATA[Continuous Integration]]></category><category><![CDATA[continuous deployment]]></category><category><![CDATA[devex]]></category><category><![CDATA[dorametrics]]></category><category><![CDATA[leadership]]></category><category><![CDATA[legacy]]></category><category><![CDATA[developer experience]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Sun, 10 Nov 2024 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719059457545/15899edb-53d3-4ed2-92ed-1354401c4748.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine inheriting a project burdened with technical debt and a team exhausted by firefighting. This was my reality in 2020 when I took over a 25-year-old project labeled as "stuck" by top management. Overwhelmed by years of mergers, losses of domain experts, and outdated tools, even basic tasks had become a challenge.</p>
<p>However, my team and I managed to turn things around, and adopting DORA metrics was pivotal in this journey.</p>
<p>In this article, I’ll share how these metrics helped us detect bottlenecks, redesign our tools, and streamline processes, transforming a project once thought unsalvageable into a well-oiled machine.</p>
<h1 id="heading-what-are-dora-metrics">What Are DORA Metrics?</h1>
<p>Before diving into the details of our story, let's quickly review DORA metrics and why they matter.</p>
<p>The <a target="_blank" href="https://dora.dev/research/">DevOps Research and Assessment (DORA)</a> is a multi-year research that examines thousands of companies to explore the connection between software development practices and business outcomes.</p>
<p>The book <em>Accelerate: The Science of Lean Software and DevOps</em> explains this research in-depth and is a must-read for anyone looking to improve software development efficiency.</p>
<p>DORA's research identified a set of <a target="_blank" href="https://dora.dev/devops-capabilities/">engineering, process, and cultural capabilities</a> closely tied to company performance. Also, it introduced four key metrics to measure software delivery performance, which fall into two categories:</p>
<p><strong>Throughput Metrics:</strong></p>
<ul>
<li><p><strong>Deployment Frequency</strong>: How often the team deploys code to production.</p>
</li>
<li><p><strong>Lead Time for Changes</strong>: The time it takes for a commit to reach production.</p>
</li>
</ul>
<p><strong>Stability Metrics:</strong></p>
<ul>
<li><p><strong>Change Failure Rate</strong>: The percentage of deployments that result in a failure.</p>
</li>
<li><p><strong>Time to Restore Service</strong>: How long it takes to recover from a failure.</p>
</li>
</ul>
<p>According to the DORA research, these metrics reveal underlying issues like poor design, excessive manual work, or bureaucratic obstacles. They provide insights into the overall efficiency of the development process.</p>
<h3 id="heading-a-vital-note-on-metrics">💡A vital note on metrics</h3>
<p>The idea of measuring and improving development efficiency isn’t new.</p>
<p>However, traditional metrics like lines of code or test coverage have often become the subject of jokes rather than true success indicators. For example, linking compensation to lines of code leads to bloated, inefficient software. Similarly, setting test coverage as a target can result in excess tests without meaningful assertions.</p>
<p>DORA metrics are a breath of fresh air because they focus on team performance rather than individual output. They balance speed and stability, helping us avoid the common trap of over-prioritizing one at the expense of the other. Furthermore, DORA metrics show real work, making them more valuable than velocity-based metrics like story points or the number of tickets, which can be "improved" without changing the actual work.</p>
<p>That said, like any metrics, they can be misused, leading to unintended and harmful outcomes. I highly recommend reading the <em>Accelerate</em> book and watching Bryan Finster's talk "<a target="_blank" href="https://www.youtube.com/watch?v=0vi7XW15UIg">How to Misuse DORA DevOps Metrics</a>" before introducing any efficiency metrics to your team.</p>
<h1 id="heading-assessing-the-state-of-our-project"><strong>Assessing the State of Our Project</strong></h1>
<p>When we first collected DORA metrics from our CI system and visualized them in Grafana, the results were alarming:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824807730/a2fa2d27-a584-43aa-be34-8e3bf722e9fa.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824807730/a2fa2d27-a584-43aa-be34-8e3bf722e9fa.png" alt="DORA Metrics for our project for the first month of collecting them" class="image--center mx-auto" /></a></p>
<center><small>DORA Metrics for our project for the first month of collecting them</small></center>

<ul>
<li><p><strong>Deployment Frequency:</strong> every two days</p>
</li>
<li><p><strong>Lead Time for Changes:</strong> 10 days</p>
</li>
<li><p><strong>Change Failure Rate:</strong> 25%</p>
</li>
<li><p><strong>Time to Restore Service:</strong> 29 hours</p>
</li>
</ul>
<p>While deployment frequency had already improved from the monthly "big-bang" releases <a target="_blank" href="https://ordinarytech.blog/empower-an-overwhelmed-team">when we were optimizing our team processes</a>, the other metrics painted a grim picture. A <strong>25% failure rate</strong> and a <strong>29-hour recovery time</strong> meant that one in four releases would break the system and take a full day and night to fix.</p>
<p>Imagine being a developer in this situation. You want to make a slight improvement, but you know it will take <strong>ten days</strong> for the change to reach production, and there’s a <strong>1 in 4 chance</strong> that the release will fail. If it does, you'll spend an entire day and night fixing it, all while juggling your current work. Would you take the risk of introducing additional changes to the code in such conditions? Probably not. Even without knowing the exact numbers, experience tells you it’s better not to.</p>
<p>Even worse, this fear of change compounds over time. Flaws pile up, the codebase becomes harder to understand, and the fear of breaking things escalates. This is a <a target="_blank" href="https://en.wikipedia.org/wiki/Vicious_circle"><strong>vicious cycle</strong></a>.</p>
<p>But just as vicious cycles exist, so do virtuous ones. The <a target="_blank" href="https://martinfowler.com/bliki/FrequencyReducesDifficulty.html">more often we make changes</a>, the easier they become. This gradually reduces our fear of change and increases our confidence and ability to improve in smaller and smaller steps with lower and lower risk.</p>
<p>Our 25-year-old project had its share of architectural and code quality issues, but DORA metrics revealed another big problem: our delivery pipelines were causing significant overhead and risk with every change. We decided to focus on optimizing the delivery pipeline first, making it easier to clean up the project later.</p>
<p>So, we focused on improving the delivery pipeline and making it easier to troubleshoot deployment failures.</p>
<h1 id="heading-uncovering-the-major-delivery-bottleneck"><strong>Uncovering the Major Delivery Bottleneck</strong></h1>
<p>Our project was structured as a monolith with 13 services, which inherited a delivery pipeline from the original monolith years ago.</p>
<p>Here is how the code went through git branches and CI pipelines:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1719067957410/7f2a900a-9228-4cde-9618-e44c91de70d2.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719067957410/7f2a900a-9228-4cde-9618-e44c91de70d2.png" alt="Our delivery pipeline before any changes are applied" class="image--center mx-auto" /></a></p>
<center><small>Our delivery pipeline before any changes are applied</small></center>

<p>One red flag in our pipeline was the repetition of identical steps. Moreover, our end-to-end (e2e) tests that we ran repeatedly were a known bottleneck.</p>
<p>These tests were comprehensive, running 3,500 tests across four production-like clusters and verifying all the critical business cases. However, they had serious drawbacks:</p>
<ul>
<li><p><strong>Limited hardware</strong>: Only one e2e-test pipeline could run at a time.</p>
</li>
<li><p><strong>Unstable execution times</strong>: Tests could take anywhere from 30 to 90 minutes.</p>
</li>
<li><p><strong>Flakiness</strong>: Failure rates averaged 25%, spiking to 50% during bad periods.</p>
</li>
<li><p><strong>Debugging difficulties</strong>: There was no way to run or debug tests locally, and according to the metrics we gathered, fixing a flaky test could take up to five days.</p>
</li>
</ul>
<p>Our e2e-tests were a constant source of frustration. Developers frequently said, "I finished my changes yesterday, but I’m still waiting for the e2e-tests to pass."</p>
<p>To see how such tests can turn even simple changes into arduous toil, let’s imagine that we need to quickly fix the infamous <a target="_blank" href="https://nvd.nist.gov/vuln/detail/CVE-2021-44228">Log4j critical vulnerability</a> across our 14 services. Our steps will be the following:</p>
<ol>
<li><p><strong>Create 14 pull requests and get 14 “green” e2e-test runs</strong>. The up to 50% failure rate of tests combined with 14 services meant <strong>up to 28 test runs.</strong> Considering the ability to make only one test run at a time, this alone took up to <strong>four workdays</strong>.</p>
</li>
<li><p><strong>Repeat the same process for the</strong> <code>develop</code> <strong>and</strong> <code>main</code> <strong>branches</strong>: this added another <strong>eight days</strong> of testing and waiting.</p>
</li>
<li><p><strong>Release all 14 services</strong>: this took another <strong>half day</strong>.</p>
</li>
</ol>
<p>Finally, it takes a grueling <strong>12 workdays</strong> to update a dependency and fix a critical vulnerability! Something as simple became an overwhelming ordeal.</p>
<h1 id="heading-streamlining-delivery"><strong>Streamlining Delivery</strong></h1>
<p>So, we identified two root problems to address: the instability and complexity of our delivery pipeline and the challenges with our e2e tests.</p>
<p>For the delivery pipeline, we removed all the redundant steps, such as creating dedicated release branches, and automated the rest, like preparing release notes and notifying stakeholders.</p>
<p>Our commitment to pipeline optimization has gone so far that we have built a Slack bot integrated with the Jenkins pipeline to manage releases (I’ve detailed this <a target="_blank" href="https://ordinarytech.blog/integrate-jenkins-and-slack">in a separate article</a>). This bot automates everything from changelog collection to health checks and rollbacks. Additionally, it allowed us to deploy the entire system at once when needed.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1720599682955/153c4afc-ba67-483b-87a8-b62b16047df5.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720599682955/153c4afc-ba67-483b-87a8-b62b16047df5.png" alt="Release initiation with Slack bot" class="image--center mx-auto" /></a></p>
<center><small>Release initiation with Slack bot</small></center>

<p>Regarding the e2e-tests, we did the following:</p>
<ul>
<li><p><strong>Reducing unnecessary e2e-tests demand</strong>: Instead of running e2e-tests on each service individually, we implemented a pipeline that runs tests and generates release artifacts for all 14 services at once. In cases like updating dependency across all services it reduces test demand by <strong>up to</strong> <strong>14 times</strong>. Additionally, we eliminated the <code>develop</code> branch that was practically redundant to us. That helped us avoid running e2e-tests twice on identical code and reduced unnecessary merges.</p>
</li>
<li><p><strong>Making tests optional</strong>: initially, e2e-tests were running automatically on each branch. We decided to run e2e-tests for feature branches only if the developer explicitly specified that. After three months of monitoring, we confirmed that the stability of the <code>main</code> branch was unaffected by this change. Unit tests, which we began prioritizing, proved sufficient for verifying changes before merging.</p>
</li>
<li><p><strong>Optimizing test execution</strong>: We stabilized test execution time from 30-90 minutes to the stable <strong>25 minutes.</strong></p>
</li>
</ul>
<p>Let’s look at how we would handle the same Log4j vulnerability after applying all these changes:</p>
<ol>
<li><p><strong>Create 14 pull requests and get 14 successful builds</strong>: Without the need for e2e-tests on each branch, it takes about <strong>1 hour in total</strong>.</p>
</li>
<li><p><strong>Merge the 14 pull requests into the</strong> <code>main</code> <strong>branch and get one “green” e2e-tests</strong>: While we've optimized the test execution time, we still face occasional stability issues. As a result, this step may take up to <strong>two e2e-test runs,</strong> resulting in <strong>30-60 minutes</strong>.</p>
</li>
<li><p><strong>Deploying all 14 services</strong>: now, with the Slack bot and the ability to deploy all services in a row, this step takes <strong>30 minutes</strong>.</p>
</li>
</ol>
<p>Thus, what once took <strong>12 long workdays</strong> now only takes <strong>a couple of hours</strong>. Isn’t that great?</p>
<p>We implemented all described changes throughout December 2022-January 2023. The graphs with 4 DORA-metrics below show clearly that starting from February 2023, the situation has changed dramatically:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600682253/35a36c6c-abc1-406f-bc84-9c978c33d69c.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600682253/35a36c6c-abc1-406f-bc84-9c978c33d69c.png" alt="Deployment frequency. December 2022-December 2023" class="image--center mx-auto" /></a></p>
<center><small>Deployment frequency by months</small></center>

<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600770145/72312470-38e0-434b-996b-2708bf7a68b4.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600770145/72312470-38e0-434b-996b-2708bf7a68b4.png" alt="Commit delivery lead time. December 2022-December 2023" class="image--center mx-auto" /></a></p>
<center><small>Commit delivery lead time by months</small></center>

<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600805303/6432c8c7-ea52-4971-9e0d-3b6d1b6d4cef.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600805303/6432c8c7-ea52-4971-9e0d-3b6d1b6d4cef.png" alt="Deployment failure rate. December 2022-December 2023" class="image--center mx-auto" /></a></p>
<center><small>Deployment failure rate by months</small></center>

<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600849703/850f2d38-55e4-41da-ab8b-52e9c118752b.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720600849703/850f2d38-55e4-41da-ab8b-52e9c118752b.png" alt="Mean time to recover. December 2022-December 2023" class="image--center mx-auto" /></a></p>
<center><small>Mean time to recover by months</small></center>

<p>These changes opened the door to continuous improvement and we aimed to make our pipeline even more efficient. Here's what we did:</p>
<ul>
<li><p><strong>Implemented health checks and alerts</strong>: We added additional checks to detect issues immediately after releases. We scrutinized every deployment failure over the next three months and steadily made our process more robust.</p>
</li>
<li><p><strong>Enhanced e2e-test debugging</strong>: we invested in the e2e-tests debugging experience and reduced the average time to fix flaky tests <strong>from</strong> <strong>five days to one day</strong>.</p>
</li>
<li><p><strong>Optimized e2e-test triggers</strong>: We discovered that most of our e2e-test runs generated release artifacts for the monolith. Even when a push to a microservice repository triggered e2e-tests, another push to the monolith often existed, triggering the tests again. So, we limited the e2e-test triggers to just two scenarios: pushes to the <strong>monolith’s</strong> <code>main</code> <strong>branch</strong> and <strong>twice-daily scheduled runs</strong>. This simple adjustment additionally reduced demand for e2e-tests without compromising our throughput or overall efficiency.</p>
</li>
</ul>
<p>To tell the full story, these improvements significantly boosted our throughput, but this increased the frequency of changes and caused e2e-tests to remain a bottleneck. Because of hardware limitations, we could only run one e2e-test at a time, and the average idle time before starting e2e-tests was about 30 minutes.</p>
<p>So, we reused the hardware set to be retired and built a second test environment, which reduced the wait time for running e2e-tests from 30 minutes to 0 minutes. This persuaded leadership to acquire more hardware, enabling us to run two e2e-test pipelines simultaneously and remove the bottleneck completely.</p>
<p>Another point worth mentioning is that we haven’t been able to eliminate flaky tests — they still crop up occasionally. Our long-term strategy is to reduce reliance on e2e-tests by gradually replacing them with more efficient integration and unit tests.</p>
<h1 id="heading-the-outcome">The outcome</h1>
<p>To illustrate the transformation, here’s a comparison between our initial metrics and where we stand now:</p>
<ul>
<li><p><strong>Deployment Frequency</strong>: Once every two days (before) → multiple times a day (now)</p>
</li>
<li><p><strong>Lead Time for Changes</strong>: 10 days (before) → 14 hours (now)</p>
</li>
<li><p><strong>Change Failure Rate</strong>: 25% (before) → 0.7% (now)</p>
</li>
<li><p><strong>Time to Restore Service</strong>: 29 hours (before) → 26 minutes (now)</p>
</li>
</ul>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824807730/a2fa2d27-a584-43aa-be34-8e3bf722e9fa.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824807730/a2fa2d27-a584-43aa-be34-8e3bf722e9fa.png" alt="DORA-metrics for the first month of measurements (April 2024)" class="image--center mx-auto" /></a></p>
<center><small>DORA-metrics before the changes applied</small></center>

<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824356388/3f16c4c4-1da0-48d0-9733-98d31854b7e5.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719824356388/3f16c4c4-1da0-48d0-9733-98d31854b7e5.png" alt="DORA-metrics for March-June 2024" class="image--center mx-auto" /></a></p>
<center><small>DORA-metrics ath the time of writing</small></center>

<p>Beyond the improvement in DORA metrics, I’d like to share my personal observations on how these changes impacted the team and the project as a whole:</p>
<ul>
<li><p><strong>Team morale</strong> has drastically improved. Without the constant overhead and firefighting, we can make steady improvements while meeting business demands. We still have technical debt from the past 25 years to tackle, but now we have the confidence and momentum to resolve these issues over time.</p>
</li>
<li><p><strong>Releases no longer require all-hand coordination</strong>. Previously, we had to schedule releases for specific times, and the whole team had to gather together. The necessity to release in the evening, on Friday, or before the holiday was highly stressful. Now, any team member can initiate a release at any convenient time.</p>
</li>
<li><p><strong>Development throughput</strong> also has increased:</p>
<ul>
<li><p>The time from the first commit on a feature branch to merging into the main branch has dropped from 17 days to less than a day — a clear indicator of improved code maintainability and tool quality.</p>
</li>
<li><p>Task completion has gone from an average of 15 days to 5 days.</p>
</li>
</ul>
</li>
<li><p><strong>Emergency fixes</strong> have all but disappeared. Previously, we handled about 6 "expedited" monthly tasks to fix critical issues. These disruptions are so rare and swiftly managed now that we no longer log them in Jira.</p>
</li>
<li><p>We’ve achieved things that seemed impossible before. In just five months, we <a target="_blank" href="https://ordinarytech.blog/caring-for-your-team-user-experience">resolved 97% of the security debt</a> accumulated over the previous 25 years. During that period, we deployed over <strong>400 releases</strong> without disturbing a single user. This level of throughput and stability was unimaginable before we applied DORA.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>The right approach and tools can revive even the most challenging project. I hope this story inspires you to take on technical debt in your projects confidently.</p>
<p>In addition to the <em>Accelerate</em> book, here are some resources I found invaluable:</p>
<ul>
<li><p>Brian Finster's talk: <a target="_blank" href="https://www.youtube.com/watch?v=0vi7XW15UIg"><em>How to Misuse DORA DevOps Metrics</em></a></p>
</li>
<li><p><em>Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation</em> book by Jez Humble and David Farley</p>
</li>
<li><p><em>Continuous Delivery Pipelines: How to Build Better Software Faster</em> by David Farley</p>
</li>
</ul>
<p>Finally, if you're interested, <a target="_blank" href="https://github.com/plesk/engineering-efficiency-assets">here’s the GitHub repository</a> containing the database structure and Grafana dashboard we implemented to track and visualize our DORA metrics.</p>
<hr />
<p>Life is so beautiful,</p>
<p>Fedor</p>
]]></content:encoded></item><item><title><![CDATA[How We Integrated Security into Development (On the Second Try)]]></title><description><![CDATA[Have you ever heard, "Our users just don’t get how great our product is, and there’s nothing we can do about it"? I guess not. That’s never a winning mindset. We know that for a product to succeed, we should invest time and energy into creating a smo...]]></description><link>https://ordinarytech.blog/integrating-security-into-development-process</link><guid isPermaLink="true">https://ordinarytech.blog/integrating-security-into-development-process</guid><category><![CDATA[developer experience]]></category><category><![CDATA[Security]]></category><category><![CDATA[Grafana]]></category><category><![CDATA[leadership]]></category><category><![CDATA[devex]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Tue, 15 Oct 2024 21:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728574311723/757d25e1-a5e0-449e-bfa5-5ad8b8ae9fc5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever heard, <em>"Our users just don’t get how great our product is, and there’s nothing we can do about it"</em>? I guess not. That’s never a winning mindset. We know that for a product to succeed, we should invest time and energy into creating a smooth user experience.</p>
<p>But what about your team’s experience? Have you ever considered how often your developers feel bogged down by their tools? Have you ever introduced a new tool or process to your team only to watch it flop for mysterious reasons?</p>
<p>The truth is, poor usability might be holding your team back, just like it can with your users. We would be much better prepared to tackle complex challenges if we evaluated and improved our team's tools from a usability standpoint, just as we do for our customers.</p>
<p>I’ll share how we integrated security into our development process in this article. It didn’t go well initially, but things turned around when we focused on improving the team's experience.</p>
<p>Surprisingly, Grafana was the tool that played a key role in solving our usability challenges. By leveraging it, we transformed the entire process into something far smoother and more effective for the team. Let me show you how a simple shift in approach made a world of difference.</p>
<h1 id="heading-a-big-change-that-didnt-happen">A Big Change That Didn’t Happen</h1>
<p>Our project was 25 years old, and over the years, a significant number of security issues had accumulated. Luckily, our company’s security team had already set up a vulnerability management tool called <a target="_blank" href="https://www.defectdojo.org/">DefectDojo</a> and linked it to 17 scanners to catch all sorts of problems—vulnerable dependencies, server misconfigurations, leaked secrets, you name it.</p>
<p>The plan was for each team to start fixing security issues in a couple of repositories. As the team progressed, the security team would expand the scans to more repositories until we had improved security posture across the company.</p>
<p>But after a year, neither my team nor others had really made much progress. The security team had done their part, so why weren’t we fixing the issues?</p>
<p>Maybe I need to start micromanaging and pushing my team harder to focus on security? I prefer to believe people have good intentions. Instead of asking, “<em>Why don’t my teammates want to improve security?</em>” I asked, “Do the tools we have help to take action?”</p>
<p>A quick check confirmed the problem. Then, I reworked the given tools. And guess what? In the next four months, we eliminated 7700 findings (97% of our security issues) and made security a usual part of our development process.</p>
<h1 id="heading-spotting-the-problem-with-the-tools">Spotting the Problem with the Tools</h1>
<p>I spent some time using DefectDojo myself to try and fix security issues. I quickly saw why the team was struggling.</p>
<h2 id="heading-1-high-cognitive-load">1. High Cognitive Load</h2>
<p>DefectDojo was overwhelming, especially for those of us who aren’t security experts. The workflow wasn’t intuitive, and even filtering issues to figure out what to tackle next felt complicated:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1722673575969/f67d6df5-b453-4f1b-95b1-faf84dd4cd9d.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722673575969/f67d6df5-b453-4f1b-95b1-faf84dd4cd9d.png" alt="A detailed interface displaying filter options for open findings, including fields for date, severity, status, and tags. Results show a table listing entries with severity, name, vulnerability ID, and more, indicating a high impact test finding. Various navigation and export options are available" /></a></p>
<center><small>DefectDojo UI to search findings.</small></center>

<p>The scanners flagged around 800 issues with just two repositories configured and that was already overwhelming. But we had 30 more repositories to configure!</p>
<h2 id="heading-2-poor-performance">2. Poor Performance</h2>
<p>Our initial DefectDojo setup wasn’t optimized, so each user action took 10–15 seconds. The security team improved this over the next six months, but we needed to clear our backlog faster, and waiting wasn’t an option. So, I offered the security team our help with database tuning but continued researching alternatives.</p>
<h2 id="heading-3-no-feedback-loop-or-sense-of-progress">3. No Feedback Loop or Sense of Progress</h2>
<p>After a week of fixing 50 issues, I felt like I’d barely made a dent. It was like taking a cup of water from a lake. With 30 more repos to configure, I knew we’d find thousands more issues, and in front of such a volume, I simply gave up.</p>
<p>Moreover, new issues kept popping up while I worked on the old ones. I had no idea if my effort was even making a difference. I couldn’t answer the simplest questions: <em>Am I making progress? Are new issues appearing faster than I can fix them?</em></p>
<h1 id="heading-reducing-the-friction">Reducing the Friction</h1>
<p>To fix these issues, I dove into DefectDojo’s <a target="_blank" href="https://defectdojo.github.io/django-DefectDojo/usage/models/">domain model</a>, <a target="_blank" href="https://defectdojo.github.io/django-DefectDojo/integrations/api-v2-docs/">API</a>, and internal database structure. Eventually, I decided to pull the data into Grafana for better visualization.</p>
<p>I created an SQL view to safeguard sensitive data and grant limited access, ensuring the process was secure. I was also concerned about the tight coupling with DefectDojo's database structure. Still, after a year and several DefectDojo updates, the created view never broke, proving this solution is reliable enough.</p>
<p>It took me two days to implement and demonstrate the initial dashboard to my teammates. I then spent another couple of days tweaking it based on their feedback and sharing it with our managers to ensure they could see our progress, too.</p>
<p>Here are the essential sections of the resulting dashboard:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1723901645813/b675cb9a-5326-4ddf-b4df-9be54b7bbe27.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723901645813/b675cb9a-5326-4ddf-b4df-9be54b7bbe27.png" alt="Dashboard showing data on security findings and vulnerability index. Key metrics include 70 currently active findings, 8,103 resolved findings, 7,587 created findings, a delta of 516, and a current vulnerability index of 6,523. Graph displays trends in active findings and vulnerability index over time." class="image--center mx-auto" /></a></p>
<center><small>The Overview section displays the key metrics and their dynamics for the selected period</small></center>

<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1722845020215/9fcb6966-022f-41b1-b394-88cd33a0433a.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722845020215/9fcb6966-022f-41b1-b394-88cd33a0433a.png" alt="A table showing active findings with details like severity, time to fix, and components. Severity levels range from medium to high. Below, a line graph displays average remediation time by finding date." class="image--center mx-auto" /></a></p>
<center><small>The Details section enables the exploration of findings without opening the DefectDojo UI
</small></center>

<p>The dashboard completely solved performance issues due to direct database reads. Additionally, it masked DefectDojo’s complexity and minimized cognitive load since we were familiar with Grafana and loved it.</p>
<p>The biggest win was how easy it became to filter issues based on what we needed to work on next. Now, the team could glance at the dashboard, see what needed attention, and get started—no more complicated filtering or jumping between tools.</p>
<p>But the final piece of the puzzle was providing a real sense of progress.</p>
<h3 id="heading-making-progress-feel-tangible">Making Progress Feel Tangible</h3>
<p>I wanted simple yet meaningful indicators to capture both the number and severity of security issues. Fixing a complex, high-severity issue should be as rewarding as fixing dozens of low-severity ones.</p>
<p>So, I added a “Vulnerability Index,” where each issue had a numeric weight based on severity: Info/Low = 1, Medium = 10, High = 100, Critical = 1000. We began tracking it alongside the raw number of findings.</p>
<p>Next, I decided to make the history of previous efforts very visible.</p>
<p><a target="_blank" href="https://en.wikipedia.org/wiki/Goodhart%27s_law">Goodhart's Law</a> warns that <em>when a metric becomes a goal, people tend to manipulate it, intentionally or not</em>.</p>
<p>So, If we only track the <em>current</em> findings count and Vulnerability index, we might be tempted to keep these numbers low by... not adopting new scanners.</p>
<p>I wanted to encourage curiosity and proactivity instead. So, an increased metric shouldn't be discouraging. By looking at our past ups and downs, we can say, "This spike is from when we integrated Scanner X, and here's how long it took to fix all the issues it found."</p>
<p>With this in mind, let’s return to the dashboard's "Overview" section. It displayed the findings count (in orange), the Vulnerability Index (in blue), and their historical trends:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1723901645813/b675cb9a-5326-4ddf-b4df-9be54b7bbe27.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723901645813/b675cb9a-5326-4ddf-b4df-9be54b7bbe27.png" alt="Graph displaying findings and vulnerability index. It shows resolved findings, created findings, active findings count, and vulnerability index trends over time. Key stats include 70 active findings, 8,103 resolved findings, and a vulnerability index of 6,523." class="image--center mx-auto" /></a></p>
<center><small>The Overview section of the dashboard</small></center>

<p>This visual representation of progress made everything feel more tangible. We’d run a security scan every time we fixed something and see the numbers drop. It was incredibly satisfying, almost like a game — the dopamine hit kept us motivated!</p>
<p>Armed with this dashboard, we activated every available scanner across all 30 repositories and started improving our security posture as of August 2023.</p>
<p>Of course, creating the dashboard was just the beginning. While it made investigating issues much easier, the next challenge was streamlining the process of solving them.</p>
<h1 id="heading-streamlining-the-issue-solving-process">Streamlining the Issue-Solving Process</h1>
<p>Our idea of streamlining was simple: gradually move to a point where fixing a security issue is the easiest choice, easier than discussing its importance or priority.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To streamline security, you first need to streamline your delivery pipeline. The benefits are wasted if it only takes five minutes to fix a vulnerability but weeks to get the fix into production. A system is only as fast as its slowest part. While this article is about security, I <a target="_self" href="https://ordinarytech.blog/dora-metrics">shared another piece on how we improved our delivery process</a>.</div>
</div>

<p>One area we focused on was third-party tools. When a vulnerability is found, we can check its impact, review network access to our tool, and decide whether to fix this vulnerability. Another option is to leverage Infrastructure as Code (IaC) and make a no-brainer update to the latest version instead of analyzing the vulnerability. This is a very beneficial strategy, considering the repeatable nature of security efforts. If moving to IaC seems too much work, at least you can containerize the tool using Docker and track new versions that way.</p>
<p>We took the same approach with secrets management. We could investigate every secret found to check whether it was a test secret, whether the production value differs, whether this secret appeared in release artifacts, and whether there are chances that it leaked outside. Another option is to adopt a secrets management system like <a target="_blank" href="https://www.vaultproject.io/">Vault</a> and reroll such secrets in the first place. We chose the latter.</p>
<p>The same works with dependency management. However, this topic deserves a separate section in the article.</p>
<h2 id="heading-automating-dependency-management">Automating Dependency Management</h2>
<p>About 40% of our security issues were related to vulnerable dependencies. Updating dependencies is repetitive, so the more we can automate, the better. Ideally, we wanted to "shift left" and update all dependencies on day zero—before vulnerabilities became widely known.</p>
<p>To achieve this, we adopted the <a target="_blank" href="https://docs.renovatebot.com/">Renovate bot</a>. This powerful tool tracks all our dependencies—Java, Python, JavaScript, Docker, and Gradle—and automatically creates pull requests whenever a new version is available.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1722680983290/2104ad8d-8b56-4c2c-8158-aab66880e31e.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722680983290/2104ad8d-8b56-4c2c-8158-aab66880e31e.png" alt="Screenshot of a Bitbucket pull request created by a Renovate Bot. It includes minor dependency updates. Release notes for ArchUnit 1.3.0 detail bug fixes and enhancements." class="image--center mx-auto" /></a></p>
<center><small>Example of the pull-request created by the Renovate bot</small></center>

<p>Even though Renovate’s documentation is great, configuring it for a large-scale project like ours took time. I spent two weeks setting it up and working through dependency updates to ensure the team would get everything they needed. Once I felt confident, I introduced Renovate to the team, made myself available for questions, and fine-tuned the configuration based on teammates’ feedback.</p>
<p>I recommend following their <a target="_blank" href="https://docs.renovatebot.com/upgrade-best-practices/">best practices</a> and <a target="_blank" href="https://docs.renovatebot.com/user-stories/swissquote/">user stories if you're starting with Renovate</a>. Below are a couple of additional tips from our experience.</p>
<h3 id="heading-weekly-batch-updates">Weekly Batch Updates</h3>
<p>We run Renovate weekly and set it up to group all non-major dependency updates into one pull request per repository. We also use a consistent branch name for these pull requests. This setup lets us automatically create a staging environment and run unit and end-to-end (e2e) tests on the proposed updates.</p>
<p>This approach helps us catch potential issues, including runtime errors while sleeping. By the time the team begins their work, all checks are already completed, reducing the noise from constant updates.</p>
<p>Here’s the <a target="_blank" href="https://docs.renovatebot.com/presets-group/">grouping preset</a> we used:</p>
<pre><code class="lang-json">{
      <span class="hljs-attr">"groupName"</span>: <span class="hljs-string">"all non-major dependencies"</span>,
      <span class="hljs-attr">"matchDatasources"</span>: [<span class="hljs-string">"maven"</span>, <span class="hljs-string">"npm"</span>, <span class="hljs-string">"pypi"</span>],
      <span class="hljs-attr">"matchUpdateTypes"</span>: [<span class="hljs-string">"digest"</span>, <span class="hljs-string">"patch"</span>, <span class="hljs-string">"minor"</span>],
      <span class="hljs-attr">"matchPackagePatterns"</span>: [<span class="hljs-string">"*"</span>],
      <span class="hljs-attr">"branchName"</span>: <span class="hljs-string">"renovate-non-major-updates-batch"</span>
}
</code></pre>
<h3 id="heading-codeowners-plugin-for-reviewer-assignments">Codeowners Plugin for Reviewer Assignments</h3>
<p>Another great addition to our setup was the <strong>Codeowners</strong> plugin. This tool works with all popular source control management systems and automates the assignment of reviewers for pull requests. We configured it as follows:</p>
<ul>
<li><p>If a person manually starts the Renovate pipeline, they are automatically assigned the related pull requests.</p>
</li>
<li><p>If a scheduler starts the pipeline, the pull requests are assigned to the engineer on third-line support duty.</p>
</li>
<li><p>For larger or more complex repositories, the reviewer is randomly selected from the <strong>Codeowners</strong> list to ensure no one is overwhelmed by constant Renovate updates.</p>
</li>
</ul>
<p>With this setup, we avoided burdening individual team members with repetitive PRs while ensuring all dependencies were regularly updated.</p>
<h2 id="heading-integrating-developed-tools-into-existing-workflows">Integrating Developed Tools Into Existing Workflows</h2>
<p>One of the biggest challenges in software development is dealing with constant context switching. Developers often have to juggle a variety of disjointed tools—CI/CD systems, source control, task trackers, observability tools—and this back-and-forth can really slow things down.</p>
<p>When we started our security journey, we already had a custom Slack bot in place, which was a great foundation for improving our overall security workflow. I previously <a target="_blank" href="https://ordinarytech.blog/integrate-jenkins-and-slack">wrote on how you can create your bot from scratch</a>. Here’s how we improved this bot to streamline our security efforts:</p>
<ul>
<li><p><strong>Slack Commands for Renovate</strong>: We set up Slack commands to run Renovate scans and check on existing pull requests, eliminating the need to switch between the CI tool and the source control system. Everything could be done from within Slack.</p>
</li>
<li><p><strong>Integrated Renovate PRs management</strong>: Whenever Renovate creates a pull request, the assigned reviewer receives an instant notification in Slack, complete with release notes. The reviewer was also kept in the loop about build statuses and test results.</p>
</li>
<li><p><strong>Integrated Notifications:</strong> The team got periodic reminders about pending Renovate PRs that needed attention. In addition, we configured Grafana to send alerts about emerging security issues directly to Slack, allowing us to respond quickly.</p>
</li>
</ul>
<p>These changes drastically reduced the need to switch between different systems. We don’t have to remember to check DefectDojo or Grafana to stay on top of security issues — they come to us in Slack. Plus, we can update dependencies without ever leaving Slack, making the whole process more efficient and keeping the team more focused.</p>
<p>Over the past year, Renovate has created over 1,200 pull requests for us. This helps us keep up with vulnerabilities and update dependencies easily, allowing the team to focus on more important tasks.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In just 12 months, we eliminated 99% of the security debt accumulated over the past 25 years. More importantly, we integrated security into our development process to ensure continuous improvement moving forward.</p>
<p>Here’s a snapshot of the entire journey in one simple graph showing <em>Created</em> (red) vs. <em>Resolved</em> (green) issues:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1728399797464/5d3b5ac0-8875-4400-830e-7c5b90b244de.jpeg"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728399797464/5d3b5ac0-8875-4400-830e-7c5b90b244de.jpeg" alt="Line graph showing created versus resolved issues from January 2023 to August 2024. The red line represents created issues, and the green line represents resolved issues. Key events include enabling scans of all repositories, focused mitigation, adding new scanners, configuring a renovate bot, and fixing server misconfigurations. Progress peaks at 99% of issues solved by August 2024." class="image--center mx-auto" /></a></p>
<center><small>Created vs. Resolved graph for security issues count</small></center>

<p>The key to success was providing the team with the right tools and making ongoing improvements based on their feedback.</p>
<p>All the changes I’ve mentioned took just 17 workdays — a small fraction of the overall security efforts. Plus, it wasn’t just my team that benefited — other teams have also adopted and benefitted from our tools.</p>
<p>If you're interested in learning from our experience, feel free to explore <a target="_blank" href="https://github.com/plesk/engineering-efficiency-assets/tree/main/security-index">the Grafana dashboard, SQL view, and Renovate pipelines</a> mentioned in this article.</p>
<p>Do you have your own story of improving developer experience to drive success? Please share it in the comments!</p>
<hr />
<p>Life is so beautiful,</p>
<p>Fedor</p>
]]></content:encoded></item><item><title><![CDATA[Joining the Disjoint Tools: A Step-by-Step Guide to Integrate Jenkins and Slack]]></title><description><![CDATA[As engineers, we rely on various tools to support our work: IDEs, source control systems, task trackers, CI/CD pipelines, and observability platforms. While each of these tools is invaluable, together, they create a significant drawback: there are si...]]></description><link>https://ordinarytech.blog/integrate-jenkins-and-slack</link><guid isPermaLink="true">https://ordinarytech.blog/integrate-jenkins-and-slack</guid><category><![CDATA[slack]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[devex]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Fedor Shchudlo]]></dc:creator><pubDate>Tue, 24 Sep 2024 08:46:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726663344033/4f8a8a2d-4cfe-4484-8917-254884468ac6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As engineers, we rely on various tools to support our work: IDEs, source control systems, task trackers, CI/CD pipelines, and observability platforms. While each of these tools is invaluable, together, they create a significant drawback: <em>there are simply too many of them</em>.</p>
<p>How many different tools do you need to roll out a change to production and verify that it was deployed successfully?</p>
<p>This abundance introduces context switching, which can seriously harm developer productivity. Some tools offer plugins to bridge the gaps, but these are often too generic and still require developers to hop between them to complete a meaningful task.</p>
<p>Fortunately, we can address this issue by building custom integrations tailored to our needs. But where should these integrations live?</p>
<p>For my team, Slack has become the perfect hub for these integrations. Unlike the IDE, where focus is key, Slack is where we naturally transition into collaborative or operational tasks. Additionally, it has robust API and SDK, making it an ideal platform to centralize workflows, automate tasks, and streamline processes without disrupting developers’ focus.</p>
<p>The Slack API has a learning curve, but adding new integrations becomes straightforward once you overcome it. This article aims to guide you through that initial learning curve with a comprehensive, step-by-step approach, helping you quickly reach a point where implementing the automation you need is easy. We’ll integrate Slack with Jenkins to manage pipeline executions entirely within Slack.</p>
<p>The complete source code for this project is <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector">available on GitHub</a>. While I used TypeScript, Slack also provides <a target="_blank" href="https://slack.dev/">Java and Python SDKs</a>.</p>
<p>Below is a preview of a basic scenario: a user sends a Slack command, selects a pipeline, fills in parameters via a generated form, and triggers the pipeline—all without leaving Slack.</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725790363593/afb3def0-00ba-4e22-b835-b884332bf433.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725790363593/afb3def0-00ba-4e22-b835-b884332bf433.gif" alt="Example of running parameterized Jenkins pipeline with the Slack bot" class="image--center mx-auto" /></a></p>
<h1 id="heading-preparing-the-environment">Preparing the Environment</h1>
<h3 id="heading-registering-a-new-slack-application">Registering a new Slack application</h3>
<p>To get started, we need to create a new Slack application. I recommend setting up a separate Slack workspace for testing and following the <a target="_blank" href="https://slack.dev/bolt-js/getting-started/#create-an-app">"Create an app"</a> section in the Slack Bolt Getting Started guide.</p>
<p>To streamline the process, you can import a <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector/blob/main/.assets/slack_app_manifest.yml">prepared manifest file</a> during configuration. This file will automatically configure the necessary scopes, enable event handling, and register the required Slack command.</p>
<p>Once you've registered and installed the app in your Slack workspace, you’ll need to obtain <a target="_blank" href="https://slack.dev/bolt-js/getting-started/#tokens-and-installing-apps">two tokens</a>:</p>
<ul>
<li><p><strong>App Token</strong> can be generated on the <strong>Basic Information</strong> page.</p>
</li>
<li><p><strong>Bot Token</strong> can be obtained from the <strong>OAuth &amp; Permissions</strong> page.</p>
</li>
</ul>
<h3 id="heading-bootstrapping-an-application-repository">Bootstrapping an Application Repository</h3>
<p>You can either <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector">clone my repository</a> and follow the steps in README.md or continue with the <a target="_blank" href="https://slack.dev/bolt-js/getting-started/#setting-up-your-project">Slack Bolt guide</a> to bootstrap the app manually.</p>
<p>Start by installing the required npm dependencies:</p>
<p><code>npm install @slack/bolt axios dotenv ts-node typescript</code></p>
<p>Next, set up the Slack Bolt app in the <code>app.ts</code> file. Here’s an example setup:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {App} <span class="hljs-keyword">from</span> <span class="hljs-string">"@slack/bolt"</span>;
<span class="hljs-keyword">import</span> {AppConfig} <span class="hljs-keyword">from</span> <span class="hljs-string">"./app.config"</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> App({
    token: AppConfig.SLACK_BOT_TOKEN,
    appToken: AppConfig.SLACK_APP_TOKEN,
    socketMode: <span class="hljs-literal">true</span>
});

(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> app.start(AppConfig.SLACK_BOT_PORT);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"⚡️ Jenkins-Slack connector is running!"</span>);
})();
</code></pre>
<p>The <code>AppConfig</code> is a simple wrapper for environment variables:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">"dotenv/config"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AppConfig = {
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
    SLACK_APP_TOKEN: process.env.SLACK_APP_TOKEN,
    SLACK_BOT_PORT: process.env.SLACK_BOT_PORT,
    JENKINS_URL: process.env.JENKINS_URL,
    JENKINS_CREDENTIALS: {
        username: process.env.JENKINS_API_USER,
        password: process.env.JENKINS_API_TOKEN
    }
};
</code></pre>
<p>You'll also need to export the Slack tokens from the previous step or place them in the <code>.env</code> file:</p>
<ul>
<li><p><code>SLACK_APP_TOKEN</code> (App token starts with <code>xapp-</code>)</p>
</li>
<li><p><code>SLACK_BOT_TOKEN</code> (Bot token starts with <code>xoxb-</code>)</p>
</li>
</ul>
<h3 id="heading-preparing-jenkins">Preparing Jenkins</h3>
<p>Next, we need to configure Jenkins to allow API access. Follow these steps to get the required token:</p>
<ol>
<li><p>Log in to Jenkins.</p>
</li>
<li><p>Click your profile name in the upper-right corner.</p>
</li>
<li><p>Navigate to <strong>Configure</strong> in the left-hand menu.</p>
</li>
<li><p>Click the <strong>Add New Token</strong> button to generate a new token.</p>
</li>
<li><p>Copy the token for later use.</p>
</li>
</ol>
<p>Now, store the token and your Jenkins login information in environment variables:</p>
<ul>
<li><p><code>JENKINS_API_TOKEN</code> (your generated API token)</p>
</li>
<li><p><code>JENKINS_API_USER</code> (your Jenkins username)</p>
</li>
<li><p><code>JENKINS_URL</code> (base URL of your Jenkins instance)</p>
</li>
</ul>
<p>Finally, to test your setup, you can add the following sample pipelines to Jenkins:</p>
<p>Finally, we need some pipelines to test. You can simply add these <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector/tree/main/.assets/sample-pipelines">three sample pipelines</a> to your Jenkins.</p>
<p>Now that everything is set up, we’re ready to dive into the code!</p>
<h1 id="heading-creating-the-first-slack-command">Creating the First Slack Command</h1>
<p>The <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector/blob/main/.assets/slack_app_manifest.yml">manifest file</a> we used earlier adds a <code>/run_jenkins_pipeline</code> Slack command. Now, let’s write a handler for this command that will display a form allowing users to select and run a Jenkins pipeline.</p>
<pre><code class="lang-typescript">app.command(<span class="hljs-string">"/run_jenkins_pipeline"</span>, <span class="hljs-keyword">async</span> ({ack, respond}) =&gt; {
    <span class="hljs-keyword">await</span> ack();

    <span class="hljs-keyword">const</span> blocks = [
        section(<span class="hljs-string">"Please, choose pipeline to run"</span>),
        divider(),
        pipelinesDropdown(),
        actionsBlock(<span class="hljs-string">"submit"</span>, [
            button(<span class="hljs-string">"Run"</span>, ActionKeys.RUN_JENKINS_PIPELINE),
            cancelButton()
        ])
    ];

    <span class="hljs-keyword">await</span> respond({blocks: blocks});
});
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Slack forms are built using <a target="_blank" href="https://app.slack.com/block-kit-builder">Slack Block Kit</a>, which can be quite verbose. To simplify things, I’ve created helper functions like <code>divider()</code>, <code>button()</code>, and <code>cancelButton()</code> to make the code more compact. If you're new to Block Kit, I recommend using the <a target="_blank" href="https://app.slack.com/block-kit-builder">Slack Block Kit Builder</a> to streamline form creation. <a target="_blank" href="https://app.slack.com/block-kit-builder#%7B%22blocks%22:%5B%7B%22type%22:%22section%22,%22text%22:%7B%22type%22:%22mrkdwn%22,%22text%22:%22Please,%20choose%20pipeline%20to%20run%22%7D%7D,%7B%22type%22:%22divider%22%7D,%7B%22type%22:%22input%22,%22label%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Select%20an%20item%22%7D,%22element%22:%7B%22type%22:%22static_select%22,%22options%22:%5B%7B%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Parameterless%20pipeline%20example%22%7D,%22value%22:%22job/parameterless-pipeline-example%22%7D,%7B%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Parameterized%20pipeline%20example%22%7D,%22value%22:%22job/parameterized-pipeline-example%22%7D,%7B%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Interactive%20pipeline%20example%22%7D,%22value%22:%22job/interactive-pipeline-example%22%7D%5D,%22action_id%22:%22pipeline_name%22%7D%7D,%7B%22type%22:%22actions%22,%22block_id%22:%22submit%22,%22elements%22:%5B%7B%22type%22:%22button%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Run%22%7D,%22style%22:%22primary%22,%22value%22:%22run_jenkins_pipeline%22,%22action_id%22:%22run_jenkins_pipeline%22%7D,%7B%22type%22:%22button%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Cancel%22%7D,%22style%22:%22danger%22,%22value%22:%22display_cancellation_message%22,%22action_id%22:%22display_cancellation_message%22%7D%5D%7D%5D%7D">Here is the preview of our form</a> in pure Block Kit syntax</div>
</div>

<p>The code snippet below registers a <a target="_blank" href="https://slack.dev/bolt-js/concepts/commands">command handler</a> for the <code>/run_jenkins_pipeline</code> command and performs the following steps:</p>
<ul>
<li><p>Calls the <code>ack()</code> to let Slack know that our app is processing the command.</p>
</li>
<li><p>Builds a form with a header, a dropdown menu to select a Jenkins pipeline, and two buttons: <strong>Run</strong> and <strong>Cancel</strong>.</p>
</li>
<li><p>Sends form back to Slack by calling the <code>respond()</code> function, allowing the user to interact with it.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Slack API offers much more than just the <code>ack</code> and <code>respond</code> functions. Look at the <a target="_blank" href="https://slack.dev/bolt-js/reference/#listener-function-arguments">Listener function arguments</a> to explore other options for more advanced interactions.</div>
</div>

<p>Here’s what happens when you run the bot and type <code>/run_jenkins_pipeline</code> in Slack:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725788422626/b3a510fd-db83-4329-8c37-a7ee78c5e685.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725788422626/b3a510fd-db83-4329-8c37-a7ee78c5e685.gif" alt="Chosing Jenkins pipeline with a Slack action and from built on top of Slack Blocks Kit" class="image--center mx-auto" /></a></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">For simplicity, we’ve hardcoded the pipeline names in this example. However, you can leverage Slack's advanced <a target="_blank" href="https://api.slack.com/reference/block-kit/block-elements#select">Select controls</a> to implement paging, filtering, and dynamic loading from Jenkins. To explore Jenkins API endpoints and retrieve specific data, append <code>/api</code> to any Jenkins page URL (e.g., <a target="_blank" href="http://jenkins-server/job-name/api"><code>http://jenkins-server/job-name/api</code></a>).</div>
</div>

<p>With the form now displaying in response to the Slack command, we can move to the next step: teaching the bot how to execute the Jenkins pipeline when the form is submitted.</p>
<h1 id="heading-running-a-parameterless-pipeline">Running a Parameterless Pipeline</h1>
<p>We’ve added the <strong>Run</strong> and <strong>Cancel</strong> buttons to the form, but they aren’t functional yet. Let’s fix that by using <a target="_blank" href="https://slack.dev/bolt-js/concepts/action-respond">action handlers</a> to trigger the desired behavior when these buttons are clicked.</p>
<p>The <strong>Cancel</strong> button is straightforward to implement. Its handler simply responds with a message without taking further action. Here's how it works:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.DISPLAY_CANCELLATION_MESSAGE, <span class="hljs-keyword">async</span> ({ack, respond}) =&gt; {
    <span class="hljs-keyword">await</span> ack();
    <span class="hljs-keyword">await</span> respond(<span class="hljs-string">"Ok, I've canceled the operation. See you soon."</span>);
});
</code></pre>
<p>The <strong>Run</strong> button is where the magic happens. When a user clicks <strong>Run</strong>, we need to do the following:</p>
<ol>
<li><p>Call <code>ack()</code> to notify Slack that we received the action.</p>
</li>
<li><p>Extract the selected pipeline name from <code>body.state.values</code>.</p>
</li>
<li><p>Start the specified Jenkins Pipeline and return a link to the running job or an error if something goes wrong.</p>
</li>
</ol>
<p>Here’s the handler for the <strong>Run</strong> button:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.RUN_JENKINS_PIPELINE, <span class="hljs-keyword">async</span> ({context, body, ack, respond}: AllMiddlewareArgs &amp; SlackActionMiddlewareArgs) =&gt; {
    <span class="hljs-keyword">await</span> ack();
    <span class="hljs-keyword">const</span> selectedPipelineOption = body.state.values.pipeline_name.pipeline_name.selected_option;

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> startedJobUrl = <span class="hljs-keyword">await</span> context.jenkinsAPI.startJob(selectedPipelineOption.value);
        <span class="hljs-keyword">await</span> respond(<span class="hljs-string">`:rocket: Here is the running <span class="hljs-subst">${link(pipelineDisplayName, startedJobUrl)}</span> pipeline`</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">await</span> respond({blocks: errorMessage(<span class="hljs-string">`Unable to run \`<span class="hljs-subst">${pipelineDisplayName}</span>\` pipeline`</span>, error)});
    }
});
</code></pre>
<p>The <code>JenkinsAPI.startJob</code> function is responsible for triggering the pipeline in Jenkins. It sends a <strong>POST</strong> request to the Jenkins <code>/build</code> API, which returns a <strong>queue URL</strong> for the job in the <code>location</code> header. After a brief delay, we send a request to the queue URL to retrieve the <strong>running job URL</strong> from the <code>queuedJobInfo.executable.url</code> property:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JenkinsAPI {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> auth: BasicAuthCredentials;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> jenkinsUrl: <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">auth: BasicAuthCredentials, jenkinsUrl: <span class="hljs-built_in">string</span></span>) {
        <span class="hljs-built_in">this</span>.auth = auth;
        <span class="hljs-built_in">this</span>.jenkinsUrl = jenkinsUrl;
    }

    <span class="hljs-keyword">async</span> startJob(jobName: <span class="hljs-built_in">string</span>, jobParams?: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt; {
        <span class="hljs-keyword">const</span> runJobUrl = <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.jenkinsUrl}</span>/<span class="hljs-subst">${jobName}</span>/build`</span>;

        <span class="hljs-keyword">const</span> queuedJobUrl = (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.request(runJobUrl, <span class="hljs-string">"POST"</span>, jobParams)).headers.location;

        <span class="hljs-comment">// give Jenkins time to start the job</span>
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">300</span>));

        <span class="hljs-keyword">const</span> startedJob = (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.request(<span class="hljs-string">`<span class="hljs-subst">${queuedJobUrl}</span>/api/json`</span>, <span class="hljs-string">"GET"</span>)).data;

        <span class="hljs-keyword">if</span> (startedJob.blocked) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.reject(startedJob.why);
        }
        <span class="hljs-keyword">return</span> startedJob.executable.url;
    }
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> request(url: <span class="hljs-built_in">string</span>, method: Method = <span class="hljs-string">"GET"</span>, params?: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">Promise</span>&lt;AxiosResponse&gt; {
        <span class="hljs-keyword">return</span> axios.request({
            method,
            url,
            params,
            auth: <span class="hljs-built_in">this</span>.auth,
            headers: {<span class="hljs-string">"accept"</span>: <span class="hljs-string">"application/json"</span>, <span class="hljs-string">"accept-encoding"</span>: <span class="hljs-string">"gzip, deflate, br"</span>}
        });
    }
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <code>setTimeout</code> is necessary to allow Jenkins time to move the job from the queue to execution. While it’s a bit of a hack caused by the <a target="_blank" href="https://www.pluralsight.com/tech-blog/forms-of-temporal-coupling/">temporal coupling</a>, it’s unavoidable here due to how Jenkins handles job queuing.</div>
</div>

<p>In some cases, Jenkins may be unable to start the job (e.g., no available build agent or <a target="_blank" href="https://www.jenkins.io/doc/book/pipeline/syntax/#options">concurrent job execution</a> is disabled). When this happens, we return an error message to Slack. The <code>queuedJobInfo.why</code> property contains the reason for the failure and is displayed to the user.</p>
<p>So, let's see how it looks in action:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725789052807/72804be2-a5fc-44e1-889b-8a0328c8d5a4.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725789052807/72804be2-a5fc-44e1-889b-8a0328c8d5a4.gif" alt="Example of running parameterless Jenkins pipeline with the Slack bot" class="image--center mx-auto" /></a></p>
<p>The demo shows a user receives a notification when the pipeline is completed. This is achieved using the <a target="_blank" href="https://www.jenkins.io/doc/pipeline/steps/slack/">Jenkins Slack Notification plugin</a>, which allows us to notify users directly in Slack when the job finishes.</p>
<p>The plugin allows to map the build starter’s email to their Slack user ID and send the notification:</p>
<pre><code class="lang-typescript">  pipeline {
    ...
    post {
        always {
            script {
                def authorId = slackUserIdFromEmail(currentBuild.rawBuild.getCause(Cause.UserIdCause)?.getUserId())
                <span class="hljs-keyword">if</span> (authorId) {
                    def message = currentBuild.result == <span class="hljs-string">'SUCCESS'</span> ? <span class="hljs-string">":white_check_mark: `${env.JOB_BASE_NAME}` build &lt;${env.BUILD_URL}|completed successfully&gt;"</span> : <span class="hljs-string">":octagonal_sign: `${env.JOB_BASE_NAME}` build &lt;${env.BUILD_URL}|had been failed&gt;"</span>
                    slackSend(message: message, sendAsText: <span class="hljs-literal">true</span>, channel: authorId, botUser: <span class="hljs-literal">true</span>, tokenCredentialId: <span class="hljs-string">'jenkins-slack-connector-bot-token'</span>)
                }
            }
        }
    }
}
</code></pre>
<p>We also use the <code>botUser</code> and <code>tokenCredentialId</code> parameters to ensure the bot sends the message, creating a seamless experience where all messages appear in the same conversation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can send the <code>/run_jenkins_pipeline</code> command in any Slack channel or private message, and the bot will respond with <a target="_blank" href="https://api.slack.com/surfaces/messages#ephemeral">ephemeral messages</a> that only you can see, ensuring minimal disruption to others. Meanwhile, Jenkins notifications will be sent directly to you via the bot chat.</div>
</div>

<h1 id="heading-running-pipelines-on-behalf-of-the-user">Running Pipelines On Behalf Of the User</h1>
<p>Currently, we use a single token to interact with the Jenkins API, which means that if our bot is shared with the entire team, all pipelines will be executed under the same user account. This creates a problem: notifications and messages about job statuses will only go to the user who provided the token rather than the individual who triggered the pipeline.</p>
<p>To resolve this, each user should provide their own Jenkins API token. This allows users to run pipelines under their accounts and receive personalized notifications. We can achieve this by using <a target="_blank" href="https://slack.dev/bolt-js/concepts/listener-middleware">Slack middleware</a> that will:</p>
<ul>
<li><p><strong>Checks for Saved Credentials:</strong> If the user has already provided their Jenkins credentials, we initialize the Jenkins API instance and <a target="_blank" href="https://tools.slack.dev/bolt-js/concepts/context/">attach it to the context</a>.</p>
</li>
<li><p><strong>Prompt for Credentials:</strong> If no credentials are found, we open a <a target="_blank" href="https://slack.dev/bolt-js/concepts/creating-modals">modal form</a> that asks the user to input their Jenkins API token.</p>
</li>
</ul>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725788103293/fb003f32-54d4-4613-a086-44019832ba21.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725788103293/fb003f32-54d4-4613-a086-44019832ba21.gif" alt="Example of token reqest form built with Slack Bolt SDK and Slack Blocks Kit" class="image--center mx-auto" /></a></p>
<p>Below is the implementation of the middleware:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">constructJenkinsAPI</span>(<span class="hljs-params">{context, client, body, next}</span>) </span>{
    <span class="hljs-keyword">const</span> credentials = CredentialsStore.getCredentials(context.userId);
    <span class="hljs-keyword">if</span> (credentials) {
        context.jenkinsAPI = <span class="hljs-keyword">new</span> JenkinsAPI(credentials, AppConfig.JENKINS_URL);
        <span class="hljs-keyword">await</span> next();
        <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">await</span> client.views.open({
        trigger_id: body.trigger_id,
        view: buildJenkinsTokenModalForm()
    });
    <span class="hljs-keyword">await</span> next();
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildJenkinsTokenModalForm</span>(<span class="hljs-params"></span>): <span class="hljs-title">ModalView</span> </span>{
    <span class="hljs-keyword">return</span> {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"modal"</span>,
        callback_id: ActionKeys.SAVE_ACCESS_TOKENS,
        title: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"plain_text"</span>,
            text: <span class="hljs-string">"Access tokens required"</span>
        },
        submit: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"plain_text"</span>,
            text: <span class="hljs-string">"Submit"</span>,
            emoji: <span class="hljs-literal">true</span>
        },
        blocks: [
            section(<span class="hljs-string">"Hi! Please, give me access token to perform useful stuff for you"</span>),
            divider(),
            textbox(<span class="hljs-string">"Jenkins API token"</span>, <span class="hljs-string">"jenkins_token"</span>)
        ]
    };
}
</code></pre>
<p>Also, we need to attach the middleware to all listeners that interact with Jenkins. Here’s how you register it with Slack commands and actions:</p>
<pre><code class="lang-typescript">app.command(<span class="hljs-string">"/run_jenkins_pipeline"</span>, constructJenkinsAPI, <span class="hljs-keyword">async</span> ({ack, respond}) =&gt; {
...
});

app.action(ActionKeys.RUN_JENKINS_PIPELINE, constructJenkinsAPI, <span class="hljs-keyword">async</span> ({context, body, ack, respond}) =&gt; {
...
});
</code></pre>
<p>The middleware will ensure every user has valid credentials before proceeding with the command or action.</p>
<p>The next piece is implementing the <a target="_blank" href="https://slack.dev/bolt-js/concepts/view-submissions">Slack view listener</a> called when a user submits the token form. here, we need to save user credentials and confirm the process was successful:</p>
<pre><code class="lang-typescript">app.view(ActionKeys.SAVE_ACCESS_TOKENS, constructUser, <span class="hljs-keyword">async</span> ({ack, body, context})=&gt; {
    <span class="hljs-keyword">const</span> respond = <span class="hljs-keyword">async</span> (title: <span class="hljs-built_in">string</span>, ...blocks: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
        <span class="hljs-keyword">await</span> ack({
            response_action: <span class="hljs-string">"update"</span>,
            view: {
                <span class="hljs-keyword">type</span>: <span class="hljs-string">"modal"</span>,
                title: {<span class="hljs-keyword">type</span>: <span class="hljs-string">"plain_text"</span>, text: title},
                blocks: blocks
            }
        });
    };

    <span class="hljs-keyword">const</span> payload: <span class="hljs-built_in">any</span> = getSlackFormValues(body.view.state.values);
    <span class="hljs-keyword">const</span> userInfo = (<span class="hljs-keyword">await</span> client.users.info({user: context.userId})).user;

    CredentialsStore.saveCredentials(context.userId, {username: userInfo.profile.email, password: payload.jenkins_token});

    <span class="hljs-keyword">await</span> respond(<span class="hljs-string">"Success"</span>, section(<span class="hljs-string">":thumbsup: Token was saved. Now you can use any of my features"</span>));
});
</code></pre>
<p>Let's also look to the <code>CredentialsStore</code> used above. It simply saves tokens in memory:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {BasicAuthCredentials} <span class="hljs-keyword">from</span> <span class="hljs-string">"./jenkins-api/BasicAuthCredentials"</span>;

<span class="hljs-keyword">const</span> CREDENTIALS_STORE: Record&lt;<span class="hljs-built_in">string</span>, BasicAuthCredentials&gt; = {}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CredentialsStore {
    <span class="hljs-keyword">static</span> saveCredentials(userId: <span class="hljs-built_in">string</span>, credentials: BasicAuthCredentials): <span class="hljs-built_in">void</span> {
        CREDENTIALS_STORE[userId] = credentials;
    }

    <span class="hljs-keyword">static</span> getCredentials(userId: <span class="hljs-built_in">string</span>): BasicAuthCredentials | <span class="hljs-literal">null</span> {
        <span class="hljs-keyword">if</span> (userId <span class="hljs-keyword">in</span> CREDENTIALS_STORE) {
            <span class="hljs-keyword">return</span> CREDENTIALS_STORE[userId];
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This in-memory storage is just for demonstration purposes. In a production environment, you should use secure storage like <a target="_new" href="https://www.vaultproject.io/">HashiCorp Vault</a><a target="_blank" href="https://www.vaultproject.io/"> or another sec</a>ret management tool to store credentials safely. Also, implementing secret rotation policy for enhanced security is a good idea.</div>
</div>

<p>With this setup, each team member can now run Jenkins pipelines using their own credentials.</p>
<h1 id="heading-running-a-parameterized-pipeline">Running a Parameterized Pipeline</h1>
<p>Now that we've handled simple, parameterless pipelines, let’s modify our bot to support running parameterized pipelines as well.</p>
<p>In the updated handler for the <code>/run_jenkins_pipeline</code> command, we first check whether the pipeline requires parameters. If it does, we present the user with a form to input those parameters. If not, we simply run the pipeline as before:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.RUN_JENKINS_PIPELINE, <span class="hljs-keyword">async</span> ({body, ack, respond}: AllMiddlewareArgs &amp; SlackActionMiddlewareArgs) =&gt; {
    ...
    <span class="hljs-keyword">const</span> pipelineParameters = <span class="hljs-keyword">await</span> jenkinsAPI.getJobParameters(pipelineId);

    <span class="hljs-keyword">if</span> (pipelineParameters) {
        <span class="hljs-keyword">const</span> parametersForm = buildPipelineParametersForm(pipelineParameters, pipelineDisplayName, pipelineId);        
        <span class="hljs-keyword">await</span> respond({blocks: parametersForm});
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">await</span> runParameterlessPipeline(context.jenkinsAPI, pipelineId, pipelineDisplayName, respond);
    }
});
</code></pre>
<p>The key part here is calling the <code>getJobParameters</code> method from our Jenkins API class to retrieve the pipeline’s parameter definitions:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JenkinsAPI {
    ...
    <span class="hljs-keyword">async</span> getJobParameters(jobPath: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;JenkinsParameterDefinition[] | <span class="hljs-literal">null</span>&gt; {
        <span class="hljs-keyword">const</span> url = <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.jenkinsUrl}</span>/<span class="hljs-subst">${jobPath}</span>/<span class="hljs-subst">${JENKINS_API_POSTFIX}</span>`</span>;

        <span class="hljs-keyword">const</span> jobInfo = (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.request(url, <span class="hljs-string">"GET"</span>)).data;
        <span class="hljs-keyword">const</span> parametersDefinition = jobInfo.property.find(<span class="hljs-function">(<span class="hljs-params">item: <span class="hljs-built_in">object</span></span>) =&gt;</span> <span class="hljs-string">"parameterDefinitions"</span> <span class="hljs-keyword">in</span> item);

        <span class="hljs-keyword">return</span> parametersDefinition?.parameterDefinitions || <span class="hljs-literal">null</span>;
    }
}
</code></pre>
<p>If no parameters are needed, we run the pipeline immediately by calling the <code>runParameterlessPipeline</code> function. It runs parameterless pipeline the same way we implemented previously:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">runParameterlessPipeline</span>(<span class="hljs-params">jenkinsAPI: JenkinsAPI, pipelineId: <span class="hljs-built_in">string</span>, pipelineDisplayName: <span class="hljs-built_in">string</span>, respond: RespondFn</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> startedJobUrl = <span class="hljs-keyword">await</span> jenkinsAPI.startJob(pipelineId);
        <span class="hljs-keyword">await</span> respond(<span class="hljs-string">`:rocket: Here is the <span class="hljs-subst">${link(pipelineDisplayName, startedJobUrl)}</span> pipeline that you run`</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">await</span> respond({blocks: errorMessage(<span class="hljs-string">"Unable to run pipeline"</span>, error)});
    }
}
</code></pre>
<p>For parameterized pipelines, we display a parameters form to the user. Here’s the <code>buildPipelineParametersForm</code> function that builds that form:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildPipelineParametersForm</span>(<span class="hljs-params">pipelineParameters: JenkinsParameterDefinition[], pipelineDisplayName: <span class="hljs-built_in">string</span>, pipelineId: <span class="hljs-built_in">string</span></span>) </span>{
    <span class="hljs-keyword">return</span> [
        section(<span class="hljs-string">"Please, specify parameters"</span>),
        divider(),

        ...mapJenkinsParametersToSlackControls(pipelineParameters),

        actionsBlock(pipelineId, [
            button(<span class="hljs-string">"Run"</span>, ActionKeys.RUN_PARAMETERIZED_JENKINS_PIPELINE, pipelineDisplayName),
            cancelButton()
        ])
    ];
}
</code></pre>
<p>The <code>mapJenkinsParametersToSlackControls</code> function maps each Jenkins parameter type to the appropriate Slack UI control. While the <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector/blob/main/src/actions/helpers/mapJenkinsParametersToSlackControls.ts">complete implementation</a> is more complex, here’s a simplified version that handles common parameter types:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mapJenkinsParametersToSlackControls</span>(<span class="hljs-params">parametersDefinition</span>) </span>{
    <span class="hljs-keyword">return</span> parametersDefinition.map(<span class="hljs-function"><span class="hljs-params">parameter</span> =&gt;</span> {
        <span class="hljs-keyword">const</span> controlName = humanizeParameterName(parameter.name);

        <span class="hljs-keyword">switch</span> (parameter.type) {
            <span class="hljs-keyword">case</span> <span class="hljs-string">"StringParameterDefinition"</span>:
                <span class="hljs-keyword">return</span> textbox(controlName, parameter.name, parameter.initialValue);
            <span class="hljs-keyword">case</span> <span class="hljs-string">"TextParameterDefinition"</span>:
                <span class="hljs-keyword">return</span> textarea(controlName, parameter.name, parameter.initialValue);
            <span class="hljs-keyword">case</span> <span class="hljs-string">"ChoiceParameterDefinition"</span>:
                <span class="hljs-keyword">return</span> staticSelect(parameter.name, getSelectOptions(parameter), parameter.name);
            <span class="hljs-keyword">case</span> <span class="hljs-string">"BooleanParameterDefinition"</span>:
                <span class="hljs-keyword">const</span> initialOption = {[parameter.name]: humanizeParameterName(parameter.name)};
                <span class="hljs-keyword">return</span> checkboxGroup(<span class="hljs-string">" "</span>, parameter.name, initialOption, parameter.initialValue ? initialOption : <span class="hljs-literal">undefined</span>);
            <span class="hljs-keyword">default</span>:
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Unknown control type <span class="hljs-subst">${parameter.<span class="hljs-keyword">type</span>}</span> of the field <span class="hljs-subst">${parameter.name}</span>`</span>);
        }
    });
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This switch case is a classic example of violating the <a target="_blank" href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle">Open-Closed Principle</a>. If you plan to handle many control types, consider refactoring with the <a target="_blank" href="https://en.wikipedia.org/wiki/Strategy_pattern">strategy</a> or <a target="_blank" href="https://en.wikipedia.org/wiki/Command_pattern">command</a> pattern for a more scalable solution.</div>
</div>

<p>The last step is implementing another action handler that triggers the pipeline with the provided parameters once the user submits the form. The submitted values are collected with the <code>getSlackFormValues</code> function and passed to the <code>jenkinsAPI.startJob</code> method:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.RUN_PARAMETERIZED_JENKINS_PIPELINE, <span class="hljs-keyword">async</span> ({context, body, payload, ack, respond}) =&gt; {
    ack();

    <span class="hljs-keyword">const</span> pipelineId = payload.block_id;
    <span class="hljs-keyword">const</span> submittedValues = getSlackFormValues(body.state.values);

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> startedJobUrl = <span class="hljs-keyword">await</span> context.jenkinsAPI.startJob(pipelineId, submittedValues);
        <span class="hljs-keyword">await</span> respond(<span class="hljs-string">`:rocket: Here is the <span class="hljs-subst">${link(payload.value, startedJobUrl)}</span> pipeline that you run`</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        respond({blocks: errorMessage(<span class="hljs-string">"Unable to run pipeline"</span>, error)});
    }
});
</code></pre>
<p>Let's look at the complete use case in action:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725790363593/afb3def0-00ba-4e22-b835-b884332bf433.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725790363593/afb3def0-00ba-4e22-b835-b884332bf433.gif" alt="Example of running parameterized Jenkins pipeline from Slack bolt built on top of Slack Bolt SDK and Slack Blocks Kit" class="image--center mx-auto" /></a></p>
<p>Now, with the parameterized pipeline support in place, we can:</p>
<ol>
<li><p>Run simple, parameterless pipelines.</p>
</li>
<li><p>Receive Slack forms for parameterized pipelines, fill them out, and trigger the pipeline with their custom input.</p>
</li>
</ol>
<p>Next, we’ll explore a more complex use case for handling interactive pipelines.</p>
<h1 id="heading-adding-interactivity-with-jenkins-input-step">Adding Interactivity With Jenkins Input Step</h1>
<p>The <a target="_blank" href="https://www.jenkins.io/doc/pipeline/steps/pipeline-input-step/">Jenkins Input Step</a> allows you to create interactive pipelines by asking for user input during pipeline execution. If you have never seen it in action, here is how it looks in Jenkins UI:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725636025169/99a7db4a-d4f6-4a28-bb42-9e2fdae77f84.png"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725636025169/99a7db4a-d4f6-4a28-bb42-9e2fdae77f84.png" alt="Pipeline interface for an interactive example showing stages and a form for user input, with options to continue or abort." class="image--center mx-auto" /></a></p>
<p>This can be highly useful for complex deployment processes, allowing teams to make decisions in real-time, such as determining which services to deploy or whether to roll back a deployment.</p>
<p>This feature was especially useful when <a target="_blank" href="https://ordinarytech.blog/dora-metrics">developing a new delivery pipeline</a> for my project. It allowed me to implement a complex deployment process, allowing the team to make real-time decisions on whether to send release notes and what Jira tasks to mention, what environments to update, which services to deploy, and whether to roll back to a previous version if the deployment was unsuccessful.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">A sudden benefit of the Slack bot I mentioned is that the Jenkins UI for Input Requests has broken a couple of times during Jenkins upgrades. At the same time, the bot has continued to work well since it leverages only input API.</div>
</div>

<p>So, let's integrate this awesome feature into Slack, too!</p>
<h3 id="heading-1-declaring-input-step-in-the-pipeline">1. Declaring Input Step in the Pipeline</h3>
<p>First, let's see how the Input Step is defined in the Jenkins pipeline:</p>
<pre><code class="lang-typescript">pipeline {
...
stage(<span class="hljs-string">'Test stage 2'</span>) {
  steps {
      script {
         input(
            id: <span class="hljs-string">'some-approval'</span>,
            message: <span class="hljs-string">'Please, choose parameters and press "Continue"'</span>,
            parameters: [
                stringParam(name: <span class="hljs-string">'someStringParam'</span>, defaultValue: <span class="hljs-string">''</span>, description: <span class="hljs-string">'Some string popup param'</span>),
                booleanParam(name: <span class="hljs-string">'someBooleanPopupParam'</span>, description: <span class="hljs-string">'Some boolean popup param'</span>)
            ],
            ok: <span class="hljs-string">'Continue'</span>)
    }
  }
}
...
}
</code></pre>
<p>Note that we explicitly pass the unique <code>id</code> parameter. If the pipeline has multiple input steps, this prevents submitting a form for the first input while the pipeline has already requested the second one.</p>
<h3 id="heading-2-notifying-slack-bot-about-input-request">2. Notifying Slack Bot About Input Request</h3>
<p>We should somehow notify the Slack bot that the pipeline requested user input. We will do that by sending a Slack message. Even though we will intercept this message in the next step, I prefer to make it human-readable. This will work as a fallback, allowing the user to follow the link and provide input with Jenkins UI ​​even if the bot fails to complete its task.</p>
<pre><code class="lang-typescript">def sendInputRequestToTheSlack() {
    def authorId = slackUserIdFromEmail(currentBuild.rawBuild.getCause(Cause.UserIdCause)?.getUserId())
    <span class="hljs-keyword">if</span> (authorId == <span class="hljs-literal">null</span>) {
        <span class="hljs-keyword">return</span>
    }

    def message = <span class="hljs-string">":keyboard: &lt;${env.BUILD_URL}|${env.JOB_BASE_NAME}&gt; requested some input. &lt;${env.RUN_DISPLAY_URL}|Please, provide it here&gt;"</span>

    slackSend(message: message, sendAsText: <span class="hljs-literal">true</span>, channel: authorId, botUser: <span class="hljs-literal">true</span>, tokenCredentialId: <span class="hljs-string">'jenkins-slack-connector-bot-token'</span>)
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We should call the <code>sendInputRequestToTheSlack</code> function <strong>before</strong> calling the Input Step because Jenkins stops execution until the user gives input. Again, this causes a temporal coupling issue because the Slack listener might start the notification handling before Jenkins shows the input form. The solution is the same as before - use <code>setTimeout</code> on the listener side.</div>
</div>

<p>Now, we need to configure the Slack bot to listen to messages.</p>
<p>First, we should disable the <code>ignoreSelf</code> option so the bot can listen to its own messages:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> App({
    <span class="hljs-attr">token</span>: AppConfig.SLACK_BOT_TOKEN,
    <span class="hljs-attr">appToken</span>: AppConfig.SLACK_APP_TOKEN,
    <span class="hljs-attr">socketMode</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">ignoreSelf</span>: <span class="hljs-literal">false</span>
});
</code></pre>
<p>We have already enabled event listening with the <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector/blob/main/.assets/slack_app_manifest.yml">manifest file</a>. It’s disabled by default since Slack generates tons of events.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">settings:</span>
  <span class="hljs-attr">event_subscriptions:</span>
    <span class="hljs-attr">bot_events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">message.im</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><code>message.im</code> event works only for direct messages. If you want to broadcast Jennkins input requests to channels so any teammate can respond, add the <code>message.channels</code> event to that manifest section.</div>
</div>

<h3 id="heading-3-listen-to-the-notification-in-the-slack-bot">3. Listen to the Notification in the Slack Bot</h3>
<p>Now, we're ready to implement the <a target="_blank" href="https://api.slack.com/events/message">message listener</a>:</p>
<pre><code class="lang-typescript">app.message(<span class="hljs-keyword">async</span> ({context, message, client}) =&gt; {
    <span class="hljs-keyword">if</span> (context.botId != message.bot_id &amp;&amp; message.text.indexOf(<span class="hljs-string">":keyboard:"</span>)!==<span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">const</span> [{url: buildUrl, text: buildName}, {url: buildDisplayUrl}] = message.blocks.flatMap(<span class="hljs-function"><span class="hljs-params">b</span> =&gt;</span> b.elements).flatMap(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.elements).filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.type == <span class="hljs-string">"link"</span>);

    <span class="hljs-comment">// give Jenkins time to render the input</span>
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">300</span>));

    <span class="hljs-keyword">const</span> jenkinsInputDefinition = <span class="hljs-keyword">await</span> context.jenkinsAPI.getJobPendingInput(buildUrl);
    <span class="hljs-keyword">const</span> formBlocks = jenkinsInputDefinition? <span class="hljs-keyword">await</span> buildJenkinsPendingInputForm(buildUrl, jenkinsInputDefinition) : buildInputHandlingFailureForm(buildName, buildUrl);

    <span class="hljs-keyword">await</span> client.chat.update({
        channel: message.channel,
        ts: message.ts,
        blocks: formBlocks,
    });
});
</code></pre>
<p>This code does the following:</p>
<ul>
<li><p>It checks if the message is from the bot and starts with a ⌨️ emoticon. This way, it only handles messages about Jenkins Input Requests.</p>
</li>
<li><p>It extracts the running job's URL and name from the message blocks.</p>
</li>
<li><p>It fetches the pending input definition by calling the <code>jenkinsAPI.getJobPendingInput</code> , which is very similar to already known <code>jenkinsAPI.getJobParameters</code>.</p>
</li>
<li><p>If the input definition is empty, someone has already submitted input, or a timeout was reached. In that case, it shows the error form by calling <code>buildInputHandlingFailureForm</code> function.</p>
</li>
<li><p>Otherwise, we show the parameters form by calling the <code>buildJenkinsPendingInputForm</code> function. This maps Jenkins parameters to Slack controls, as we saw in the “Running Parameterized Pipeline” section.</p>
</li>
</ul>
<h3 id="heading-4-handling-input-form-submit">4. Handling Input Form Submit</h3>
<p>We're almost done. All that's left is to submit or cancel pending input depending on which button the user clicks on the rendered input form. Here is the submit handler:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.SUBMIT_PENDING_JENKINS_INPUT, <span class="hljs-keyword">async</span> ({context, body, ack, respond}) =&gt;{
    ack();

    <span class="hljs-keyword">const</span> submitUrl = body.actions[<span class="hljs-number">0</span>].value;
    <span class="hljs-keyword">const</span> buildUrl = body.actions[<span class="hljs-number">0</span>].block_id;

    <span class="hljs-keyword">const</span> submittedValues = getSlackFormValues(body.state.values);

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> context.jenkinsAPI.submitJobPendingInput(buildUrl, submitUrl, submittedValues);

        <span class="hljs-keyword">await</span> respond(<span class="hljs-string">`Input was submitted to the <span class="hljs-subst">${link(<span class="hljs-string">"build"</span>, buildUrl)}</span> by <span class="hljs-subst">${body.user_name}</span>`</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">await</span> respond({blocks: errorMessage(<span class="hljs-string">`Unable to submit input for the <span class="hljs-subst">${link(<span class="hljs-string">"build"</span>, buildUrl)}</span>`</span>, error)});
    }
});
</code></pre>
<p>As you can see, it is almost identical to the handler we implemented to run the parameterized pipeline. The only difference is that we call the <code>jenkinsAPI.submitJobPendingInput</code> instead of the <code>jenkinsAPI.startJob</code>.</p>
<p>Here is the listing of the <code>jenkinsAPI.submitJobPendingInput</code> method:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JenkinsAPI {
    ...
    <span class="hljs-keyword">async</span> submitJobPendingInput(buildUrl: <span class="hljs-built_in">string</span>, submitUrl: <span class="hljs-built_in">string</span>, formValues: <span class="hljs-built_in">any</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">const</span> inputDefinition = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getJobPendingInput(buildUrl);

        <span class="hljs-keyword">if</span> (!inputDefinition || !submitUrl.includes(<span class="hljs-string">`inputSubmit?inputId=<span class="hljs-subst">${inputDefinition.id}</span>`</span>)) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Input was already submitted or timed out"</span>);
        }

        <span class="hljs-keyword">const</span> convertedValues = <span class="hljs-built_in">Object</span>.entries(formValues).map(<span class="hljs-function">(<span class="hljs-params">[key, value]</span>) =&gt;</span> ({name: key, value}));
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.request(<span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.jenkinsUrl}</span><span class="hljs-subst">${submitUrl}</span>`</span>, <span class="hljs-string">"POST"</span>, {json: <span class="hljs-built_in">JSON</span>.stringify({parameter: convertedValues})});
    }
}
</code></pre>
<p>We request pending input from Jenkins again to ensure that the input is still expected and its ID matches the submitted input's ID. Then, we convert the submitted values to the Jenkins API format and post them.</p>
<h3 id="heading-5-handling-input-request-abortion">5. Handling Input Request Abortion</h3>
<p>The last step is to handle input abortion. Here is the code of the handler:</p>
<pre><code class="lang-typescript">app.action(ActionKeys.ABORT_PENDING_JENKINS_INPUT, <span class="hljs-keyword">await</span> ({ context, body, ack, respond }) =&gt; {
    <span class="hljs-keyword">await</span> ack();

    <span class="hljs-keyword">const</span> abortUrl = body.actions[<span class="hljs-number">0</span>].value;

    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> context.jenkinsAPI.abortJobPendingInput(abortUrl);
        <span class="hljs-keyword">await</span> respond(<span class="hljs-string">`Input was aborted by <span class="hljs-subst">${body.user_name}</span>`</span>);
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">await</span> respond({ blocks: errorMessage(<span class="hljs-string">"Input abort failed"</span>, error) });
    }
};
</code></pre>
<p>Here, we simply call the <code>jenkinsAPI.abortJobPendingInput</code> method and notify the user about the abortion.</p>
<p>The code of the <code>jenkinsAPI.abortJobPendingInput</code> method is very straightforward:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JenkinsAPI {
    <span class="hljs-keyword">async</span> abortJobPendingInput(abortUrl: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.request(<span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.jenkinsUrl}</span>/<span class="hljs-subst">${abortUrl}</span>`</span>, <span class="hljs-string">"POST"</span>);
    }
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">What happens with the running job after input abortion depends on the pipeline implementation. The job will be aborted by default, but it is possible to catch <a target="_blank" href="https://javadoc.jenkins.io/plugin/workflow-step-api/org/jenkinsci/plugins/workflow/steps/FlowInterruptedException.html">FlowInterruptedException</a> and implement some custom logic.</div>
</div>

<p>Well, we're finally done! Let's see the magic in action:</p>
<p><a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1725789485490/100290b8-399b-4c70-b389-fed9e2859735.gif"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725789485490/100290b8-399b-4c70-b389-fed9e2859735.gif" alt="Screenshot of Slack bot displaying interactive input steps from the running Jenkins pipeline" class="image--center mx-auto" /></a></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The initial message stays on the screen long before the Slack bot updates it, which can look awkward. Usually, this isn't a problem because users don't watch the screen constantly. But if it bothers you, you can change the <code>client.chat.update</code> call to <code>client.chat.postMessage({channel, blocks})</code>. This way, messages will be sent one after another.</div>
</div>

<p>Well, we implemented the latest use case!</p>
<p>Generally, we've covered many features of the Slack Bolt SDK and built a comprehensive automation. However, to keep the article easy to read, I skipped error handling, monitoring, logging, security, and testing (but you can check <a target="_blank" href="https://github.com/fshchudlo/jenkins-slack-connector">the source code</a> for tests).</p>
<h1 id="heading-wrapping-up">Wrapping Up</h1>
<p>Integrating Slack with your development tools can elevate your workflow by enabling seamless communication, automation, and collaboration. The true power of such an integration is its ability to connect multiple systems and surface relevant information when needed. Imagine starting your workday with a Slack bot that does the following:</p>
<ul>
<li><p>It lists tasks you've recently closed and suggests follow-up actions like updating documentation or notifying relevant stakeholders.</p>
</li>
<li><p>Displays pull requests you’ve participated in that are awaiting review or haven’t been merged yet, ensuring nothing slips through the cracks.</p>
</li>
<li><p>Shows your next task, along with its recent updates or comments, so you're always in sync with ongoing discussions.</p>
</li>
<li><p>Lists branches you've worked on that lack pull requests, helping you identify abandoned work or unfinished experiments.</p>
</li>
<li><p>Presents recent findings from automated security scans, ensuring security issues are addressed quickly.</p>
</li>
<li><p>Highlights unresolved threads from Slack channels that need your or your team's attention, streamlining internal communication.</p>
</li>
<li><p>If the number of unresolved support tickets spikes, the bot could offer you to help your on-call teammate.</p>
</li>
</ul>
<p>By automating these workflows and integrating them into Slack, you can create a cohesive environment tailored to your team’s needs, improving productivity and collaboration.</p>
<p>I hope this guide has equipped you with the tools and knowledge to build your Slack-powered development environment, streamlining your daily operations and enhancing your team's effectiveness.</p>
<hr />
<p>Life is so beautiful,</p>
<p>Fedor</p>
]]></content:encoded></item></channel></rss>