<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mindbyte.nl/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mindbyte.nl/" rel="alternate" type="text/html" /><updated>2026-04-08T00:57:34+02:00</updated><id>https://mindbyte.nl/feed.xml</id><title type="html">MindByte</title><subtitle>CTO at Freelance.nl, founder of leftsize.com and scitor.io</subtitle><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><entry><title type="html">Dynamic IIS Server Deployments with GitHub Actions</title><link href="https://mindbyte.nl/2025/06/02/dynamic-iis-server-deployments-github-actions.html" rel="alternate" type="text/html" title="Dynamic IIS Server Deployments with GitHub Actions" /><published>2025-06-02T00:00:00+02:00</published><updated>2025-06-02T00:00:00+02:00</updated><id>https://mindbyte.nl/2025/06/02/dynamic-iis-server-deployments-github-actions</id><content type="html" xml:base="https://mindbyte.nl/2025/06/02/dynamic-iis-server-deployments-github-actions.html"><![CDATA[<p>When we began migrating our application infrastructure to the cloud, we relied on AWS and traditional Windows servers running IIS. Although containers and Kubernetes dominate today’s conversations, there are organizations that still depend on familiar, “old-school” architectures. In one recent project, we found ourselves needing to deploy a web application across multiple IIS servers behind an AWS load balancer. Our challenge was simple in statement but complex in execution: how do you run the same GitHub Actions deployment workflow on every server in a scalable, maintainable way?</p>

<h2 id="background">Background</h2>

<p>Our environment consisted of a load balancer distributing traffic to several Windows servers hosting IIS. These servers (EC2 instances) were spun up via CloudFormation, each automatically registering a GitHub self-hosted runner. In essence, every server became a target for GitHub Actions workflows.</p>

<p>To deploy across the entire group of servers with the same tag (for example, all IIS-Server machines), we needed the workflow to run on each of them, not just on a single available runner. While setting <code class="language-plaintext highlighter-rouge">runs-on: [self-hosted, &lt;label&gt;]</code> directs GitHub Actions to run the job on a runner matching that label, it does not instruct it to execute on all such servers simultaneously.</p>

<p>As <a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions">documented by GitHub</a>, <code class="language-plaintext highlighter-rouge">runs-on</code> selects only one matching runner; to execute a job on multiple machines, you need to use a matrix strategy.</p>

<p>Hard-coding runner names would defeat our goal of dynamic scaling, since servers could be added or removed without updating the workflow.</p>

<p>We looked for an approach that would adapt dynamically as servers joined or left our environment. Azure DevOps offered “<a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/release/deployment-groups/deploying-azure-vms-deployment-groups?view=azure-devops">deployment groups</a>”, but GitHub Actions does not include a direct equivalent. Instead, we needed to leverage the GitHub API to query our organization’s runners at runtime, filter them by environment labels, and build a dynamic matrix of targets for our workflow. This blog post describes the story of how we achieved that goal.</p>

<h2 id="gathering-runner-information">Gathering Runner Information</h2>

<p>The first obstacle we encountered was obtaining an authentication token capable of listing all self-hosted runners in our organization. The default <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> provided to workflows only has repository scope, so it cannot retrieve organization-wide runner details. Our solution was to create a GitHub App with “Read access to organization self hosted runners”, then install it on our repository. By storing the App’s ID and private key as repository secrets (<code class="language-plaintext highlighter-rouge">RUNNER_TOKEN_APP_ID</code> and <code class="language-plaintext highlighter-rouge">RUNNER_TOKEN_APP_PRIVATE_KEY</code>), we could exchange these credentials for a short-lived token that the GitHub CLI (<code class="language-plaintext highlighter-rouge">gh</code>) could use to query the runners API.</p>

<p>Below is the YAML snippet for the <code class="language-plaintext highlighter-rouge">determine-runners</code> job. It runs on <code class="language-plaintext highlighter-rouge">ubuntu-latest</code>, invokes a third-party action to fetch an application token, then uses <code class="language-plaintext highlighter-rouge">gh api</code> and <code class="language-plaintext highlighter-rouge">jq</code> to filter runners with a specific environment label (e.g., <code class="language-plaintext highlighter-rouge">staging</code> or <code class="language-plaintext highlighter-rouge">production</code>) and a fixed label (e.g., <code class="language-plaintext highlighter-rouge">IIS-Server</code>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">determine-runners</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Determine target runners</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">runners_json</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">env</span><span class="pi">:</span>
      <span class="na">ORG_NAME</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">TARGET_ENV_LABEL</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">REQUIRED_LABEL_1</span><span class="pi">:</span> <span class="s">IIS-Server</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get Organization API Token</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">get_workflow_token</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">peter-murray/workflow-application-token-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">application_id</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">application_private_key</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">organization</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get matching runners (including offline)</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">get_runners</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># Retrieve all runners for the organization (up to 100 per page)</span>
          <span class="s">runners_data=$(gh api "orgs/$/actions/runners?per_page=100" --jq '.runners[]')</span>

          <span class="s"># Filter runners by both the fixed label and environment label</span>
          <span class="s">matching_runners=$(echo "$runners_data" | jq -c \</span>
            <span class="s">--arg label1 "$" \</span>
            <span class="s">--arg label2 "$" \</span>
            <span class="s">'select(.labels | map(.name) | (contains([$label1]) and contains([$label2])))'</span>
          <span class="s">)</span>

          <span class="s"># Build a JSON array of runner names</span>
          <span class="s">runners_json=$(echo "$matching_runners" | jq -cs '[.[].name]')</span>

          <span class="s">if [ -z "$runners_json" ] || [ "$runners_json" == "[]" ]; then</span>
            <span class="s">echo "::error::No runners found with labels: '$' and '$'."</span>
            <span class="s">exit 1</span>
          <span class="s">else</span>
            <span class="s">echo "runners_json=$runners_json" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">fi</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GH_TOKEN</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<h2 id="building-the-deployment-matrix">Building the Deployment Matrix</h2>

<p>Once we had a JSON list of runner names in the output variable <code class="language-plaintext highlighter-rouge">runners_json</code>, we could construct a dynamic matrix for our deployment job. The second job, <code class="language-plaintext highlighter-rouge">deployment-web-app</code>, depends on <code class="language-plaintext highlighter-rouge">determine-runners</code>. It uses <code class="language-plaintext highlighter-rouge">fromJson(...)</code> to transform the JSON string into an array for the matrix. Each array element corresponds to a runner name, resulting in one parallel job per server.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">deployment-web-app</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to $ on $</span>
    <span class="na">environment</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">determine-runners</span><span class="pi">]</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">fail-fast</span><span class="pi">:</span> <span class="no">false</span>
      <span class="na">matrix</span><span class="pi">:</span>
        <span class="na">runner</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clean Workspace Folder</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">Remove-Item -Recurse -Force $\*</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Download artifact</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/download-artifact@v4</span>

      <span class="c1"># Additional steps to deploy the IIS website go here</span>
</code></pre></div></div>

<p>In our case, deploying to IIS involved copying build artifacts, stopping the IIS site, replacing the files, and restarting the service. By running these steps on each runner in parallel, we reduced total deployment time and avoided manual coordination.</p>

<h2 id="lessons-learned">Lessons Learned</h2>

<p>When we first set out, we worried that this dynamic approach would introduce more complexity than it solved. However, by automating the discovery of runners, we created a resilient pipeline that adapts as servers come online or go offline. Below are some key takeaways:</p>

<ul>
  <li>
    <p><strong>Use Labels Strategically</strong><br />
By combining environment labels (e.g., <code class="language-plaintext highlighter-rouge">staging</code>, <code class="language-plaintext highlighter-rouge">production</code>) with a fixed label (e.g., <code class="language-plaintext highlighter-rouge">IIS-Server</code>), we narrowed down the runner list precisely. Labels become powerful selectors when applied consistently.</p>
  </li>
  <li>
    <p><strong>Manage GitHub App Credentials Carefully</strong><br />
The process requires a GitHub App with appropriate permissions. You must secure its private key and ensure it remains installed on your repository. Losing this App or its credentials would break the token exchange and halt deployments.</p>
  </li>
  <li>
    <p><strong>CLI Tools Preinstalled</strong><br />
We used a GitHub-hosted runner to run this workflow, which already has <code class="language-plaintext highlighter-rouge">gh</code> and <code class="language-plaintext highlighter-rouge">jq</code> installed by default.</p>
  </li>
  <li>
    <p><strong>Account for Pagination</strong><br />
Our example handles only the first 100 runners. If your organization has more, implement pagination logic by iterating over pages until no runners remain.</p>
  </li>
  <li>
    <p><strong>Embrace Parallelism</strong><br />
Running deployments in parallel saved us time and reduced the risk of inconsistent state. We could also monitor each runner’s status separately, making troubleshooting easier.</p>
  </li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>GitHub Actions does not natively support a construct like deployment groups. By querying the GitHub API, filtering runners with <code class="language-plaintext highlighter-rouge">jq</code>, and building a dynamic matrix, we created a solution that scales automatically as servers change. Although it requires extra setup, a GitHub App, additional CLI tools, and careful label management, the result is a more flexible, resilient deployment pipeline.</p>

<p>While this post uses IIS server deployments as the example, the same technique can be applied to any deployment scenario. By defining tags or labels on your runners and building a dynamic matrix, you can select machines based on any condition, such as environment, role, or geographic region, and run workflows in parallel across them.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="github-actions" /><category term="iis" /><category term="windows" /><category term="aws" /><category term="devops" /><category term="self-hosted-runners" /><summary type="html"><![CDATA[When we began migrating our application infrastructure to the cloud, we relied on AWS and traditional Windows servers running IIS. Although containers and Kubernetes dominate today’s conversations, there are organizations that still depend on familiar, “old-school” architectures. In one recent project, we found ourselves needing to deploy a web application across multiple IIS servers behind an AWS load balancer. Our challenge was simple in statement but complex in execution: how do you run the same GitHub Actions deployment workflow on every server in a scalable, maintainable way?]]></summary></entry><entry><title type="html">A Practical Approach to Hiring Great Full-Stack Engineers</title><link href="https://mindbyte.nl/2024/12/28/practical-approach-hiring-great-full-stack-engineers.html" rel="alternate" type="text/html" title="A Practical Approach to Hiring Great Full-Stack Engineers" /><published>2024-12-28T00:00:00+01:00</published><updated>2024-12-28T00:00:00+01:00</updated><id>https://mindbyte.nl/2024/12/28/practical-approach-hiring-great-full-stack-engineers</id><content type="html" xml:base="https://mindbyte.nl/2024/12/28/practical-approach-hiring-great-full-stack-engineers.html"><![CDATA[<p>Hiring great software developers is always challenging, but the stakes are even higher when building small, versatile teams. Recently, I conducted a series of interviews for a client looking to expand their team by adding two full-stack engineers. The team setup required individuals who could work across the entire application lifecycle—designing, building, deploying, and monitoring systems—without being restricted to just frontend, backend, or DevOps.</p>

<p>It’s no secret that finding true “full-stack” engineers is difficult. Most developers lean towards one area—be it backend, frontend, cloud, or pipelines—and that’s okay. But in this case, the organization needed engineers who understood the big picture. They had to be able to contribute meaningfully across the board in a small team setup, where there’s no room to say, “That’s someone else’s job.”</p>

<h2 id="the-process-lengthy-but-effective">The Process: Lengthy, But Effective</h2>

<p>For this reason, we committed to lengthy interviews, typically lasting 1.5 to 2 hours. It’s time-consuming, both for the candidates and for us, but it allowed us to truly assess whether someone could fit the team’s needs.</p>

<p>The interview structure was straightforward:</p>

<ol>
  <li><strong>Introductions</strong>: We started with casual conversation—interests, hobbies, passions—to get a sense of the candidate as a person. This also helped build rapport and gave us an early impression of their communication skills.</li>
  <li><strong>Problem Discussion</strong>: I presented a simple but open-ended problem: designing an API to serve products. The task wasn’t about writing code; it was about having a meaningful conversation.</li>
</ol>

<h2 id="the-flow-from-idea-to-production">The Flow: From Idea to Production</h2>

<p>The exercise mimicked how the team worked internally: starting with a vague business problem and analyzing it to build a working system. The candidate’s job was to walk through each stage of the process with me, while I m drawing on a whiteboard:</p>
<ul>
  <li><strong>Understanding the Problem</strong>: Did they ask the right questions to clarify the requirements and constraints?</li>
  <li><strong>API Design</strong>: Could they explain HTTP verbs, REST principles, and how to handle authentication (e.g., what a JWT is and why it’s secure)?</li>
  <li><strong>Coding</strong>: How would they implement this API? What language, framework, or libraries would they use? How would they structure the code and how to ensure quality? Did they consider testing and if so, what kind?</li>
  <li><strong>Database Considerations</strong>: Did they think about indexes or the implications of their schema design?</li>
  <li><strong>Production Readiness</strong>: How would they host the API? Did they mention pipelines, monitoring, or scalability? And how to get the code from their machine to production?</li>
</ul>

<p>The goal wasn’t to see how many buzzwords they could throw out or whether they knew every detail of every tool. Instead, we focused on their reasoning, communication, and ability to problem-solve collaboratively.</p>

<h2 id="the-results-the-good-the-bad-and-the-lessons-learned">The Results: The Good, the Bad, and the Lessons Learned</h2>

<p>Out of all the interviews, we only passed three candidates to the next round. Here’s what set them apart:</p>

<ol>
  <li><strong>They Could Have a Conversation</strong>: These candidates were curious, asked great questions, and actively participated in the discussion. Even when they didn’t know everything, they admitted it honestly and showed an understanding of the broader picture. For example, they recognized the need for hosting, securing, and monitoring the API—even if they weren’t experts in every tool.</li>
  <li><strong>They Thought Beyond the Immediate Problem</strong>: One standout candidate designed the system with cost optimization and scalability in mind, leveraging PaaS solutions to keep it efficient. This kind of forward-thinking demonstrated a level of maturity and ownership we rarely see.</li>
  <li><strong>They Showed Accountability</strong>: In small teams, everyone needs to pitch in across the lifecycle. These candidates understood the importance of being able to handle a variety of tasks. By contrast, many other candidates relied too heavily on the idea of separate teams handling pipelines, quality control, or rollouts.</li>
</ol>

<p>Unfortunately, we also encountered several candidates with impressive CVs who struggled to articulate their understanding of the concepts they claimed to know. Some couldn’t design any part of the system or explain the reasoning behind their choices. Others had limited exposure to the end-to-end development lifecycle because they worked in large organizations where tasks were heavily siloed.</p>

<h2 id="the-takeaway-real-conversations-trump-cvs">The Takeaway: Real Conversations Trump CVs</h2>

<p>While resumes and buzzwords can get candidates through the door, they rarely tell the full story. A collaborative, problem-solving interview process, though exhausting, gave us the confidence to hire engineers who could truly add value to the team.</p>

<p>For the candidates, the process also served as a preview of the company’s expectations. By focusing on real-world problems and reasoning, we ensured alignment between the team’s needs and the candidate’s strengths.</p>

<p>Ultimately, hiring isn’t just about filling a role—it’s about finding the right person who can thrive in the unique environment of your organization.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="hiring" /><category term="full-stack" /><category term="engineering" /><category term="interview" /><summary type="html"><![CDATA[Hiring great software developers is always challenging, but the stakes are even higher when building small, versatile teams. Recently, I conducted a series of interviews for a client looking to expand their team by adding two full-stack engineers. The team setup required individuals who could work across the entire application lifecycle—designing, building, deploying, and monitoring systems—without being restricted to just frontend, backend, or DevOps.]]></summary></entry><entry><title type="html">Streamlining Helpdesk Operations: Leveraging GitHub Issues with Custom Workflows and Templates</title><link href="https://mindbyte.nl/2024/01/02/streamlining-helpdesk-operations-leveraging-github-issues-custom-workflows-templates.html" rel="alternate" type="text/html" title="Streamlining Helpdesk Operations: Leveraging GitHub Issues with Custom Workflows and Templates" /><published>2024-01-02T00:00:00+01:00</published><updated>2024-01-02T00:00:00+01:00</updated><id>https://mindbyte.nl/2024/01/02/streamlining-helpdesk-operations-leveraging-github-issues-custom-workflows-templates</id><content type="html" xml:base="https://mindbyte.nl/2024/01/02/streamlining-helpdesk-operations-leveraging-github-issues-custom-workflows-templates.html"><![CDATA[<h2 id="setting-the-scene-choosing-github-issues-for-internal-helpdesk-needs">Setting the Scene: Choosing GitHub Issues for Internal Helpdesk Needs.</h2>

<p>In the realm of helpdesk and ticketing systems, the market is brimming with options. From Zohodesk and Freshdesk to Zendesk and Jira, the choices are many, some even offering enticing free entry points. However, a common catch with these platforms is the pricing model, which typically scales based on the number of agents, leading to a significant cost increase over time. This challenge became evident in a recent customer project I was involved in. While we successfully implemented Freshdesk as the support system for the customer’s SaaS application, our internal requirements painted a different picture.</p>

<p>Our needs revolved around internal operations like account management, onboarding/offboarding procedures, addressing hardware issues, and managing licenses—tasks distinctly separate from customer interactions and not fitting neatly into the development team’s responsibilities. Adding to the complexity was the customer’s ISO certification, necessitating meticulous tracking of changes and actions. This scenario required a separate system from the customer-facing helpdesk, yet something that was both minimalistic and easy to manage.</p>

<p>The solution lay closer than we thought. With all team members already well-versed in GitHub, leveraging its environment was a logical step. GitHub Issues, when combined with customized workflows and templates, presented a unique opportunity. It offered a familiar platform that was both cost-effective and adaptable to our specific internal needs. In this post, I’ll guide you through the process of setting up GitHub Issues as an efficient, internal helpdesk system, elucidating how it can be tailored to streamline your organizational processes, just as it did for ours.</p>

<h2 id="laying-the-foundations-initial-setup-of-your-github-helpdesk">Laying the Foundations: Initial Setup of Your GitHub Helpdesk</h2>

<p>In the journey of transforming GitHub into an effective internal helpdesk, the initial setup plays a crucial role. GitHub offers two distinct systems for tracking activities: Issues and Discussions. Understanding the nuances of each is key to leveraging them effectively.</p>

<h3 id="issues-vs-discussions">Issues vs. Discussions</h3>

<ul>
  <li><strong>Issues</strong>: This feature in GitHub is straightforward. Each issue comprises a title, a descriptive body, optional labels, and assignments. It’s the go-to choice for tracking specific tasks, bugs, or requests.</li>
  <li><strong>Discussions</strong>: In contrast, Discussions provide a forum-like environment. They’re ideal for cases where you want to engage in a broader conversation before possibly converting the discussion into a more formal issue. Not everything that crops up is immediately an issue, and Discussions provide the flexibility to explore topics more broadly.</li>
</ul>

<p>For the purpose of an internal helpdesk, where submissions are likely to be direct issues, focusing on the Issues feature is the most practical approach.</p>

<h2 id="setting-up-your-repository">Setting Up Your Repository</h2>

<ol>
  <li><strong>Create a New Repository</strong>: Start with a fresh slate by setting up a new repository within your organization. A simple and descriptive name like ‘servicedesk’ or ‘helpdesk’ works well. Remember to keep it internal/private to maintain confidentiality. <img src="/images/create-helpdesk-repo.png" alt="Create Repository" /></li>
  <li><strong>Craft Your README.md</strong>: The <code class="language-plaintext highlighter-rouge">README.md</code> file is the first point of contact for users visiting your repository. It’s a Markdown file that can be used to provide essential instructions and guidelines. Here’s how you can structure it:</li>
</ol>

<p><em>Introduction</em>: Briefly describe the purpose of the helpdesk and what types of issues should be submitted here.</p>

<p><em>How to Submit an Issue</em>: Give clear, step-by-step instructions on how to create a new issue, including guidance on writing a good title and description.</p>

<p><em>Label Usage</em>: Explain how labels are used within the repository to categorize and prioritize issues.</p>

<p><em>Response Times and Process</em>: Outline what users can expect in terms of response times and the process their issues will go through.</p>

<p>For example, here’s a sample README.md file:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Service Desk</span>

Service Desk system for X.

<span class="gu">## What is this?</span>

This repository can be used to record issue requests to the service organisation operated by Y. Like requesting access to or gaining new accounts, having issues with hardware, or have problems with your workstation or phone.

This is not for software production issues; that is for the devteam, reachable in <span class="p">[</span><span class="nv">Slack</span><span class="p">](</span><span class="sx">link</span> to slack).

Customers having questions or problems with the usage of Z, will need to go to <span class="p">[</span><span class="nv">Freshdesk</span><span class="p">](</span><span class="sx">link</span> to freshdesk).

<span class="gu">## How does it work?</span>

When you want to register an item, create a <span class="p">[</span><span class="nv">new issue</span><span class="p">](</span><span class="sx">https://github.com/yourorg/yourrepo/issues/new/choose</span><span class="p">)</span> and use the most applicable template. 

The issue will be automatically assigned to the service team at Y and they will react via the issue. 

</code></pre></div></div>

<p>Creating this foundational setup ensures a streamlined experience for both those managing the helpdesk and those using it, setting the stage for efficient internal support management.</p>

<h2 id="optimizing-issue-reporting-implementing-issue-templates">Optimizing Issue Reporting: Implementing Issue Templates</h2>

<p>A common challenge in issue tracking is receiving submissions that lack essential information. This often leads to a back-and-forth to gather the necessary details, causing delays and inefficiencies. To address this, GitHub offers a powerful tool: issue templates.</p>

<h3 id="creating-issue-templates">Creating Issue Templates</h3>

<ol>
  <li>
    <p><strong>Template Files</strong>: Start by creating your issue templates. These templates are files that reside in the <code class="language-plaintext highlighter-rouge">.github/ISSUE_TEMPLATE</code> folder of your repository. You can create multiple files here to cater to different types of requests or issues.</p>
  </li>
  <li>
    <p><strong>Design Your Templates</strong>: Each template should be designed to collect specific data pertinent to the type of issue it represents. This structured approach ensures that when a user submits an issue, they provide all the necessary information upfront.</p>
  </li>
</ol>

<p>An example of a template for requesting a new account:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Accounts</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Onboarding or offboarding, getting access to a system</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[Account]:</span><span class="nv"> </span><span class="s">"</span>
<span class="na">labels</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">account"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">triage"</span><span class="pi">]</span>
<span class="na">assignees</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">userx</span>
  <span class="pi">-</span> <span class="s">usery</span>
<span class="na">body</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">markdown</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">value</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">Use this template to request access to a system, onboard or offboard an employee or ask for a change in permissions. </span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">input</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">person</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">Contact Details</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">For who is the change intended?</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">ex. email@example.net</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">textarea</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">what-need-to-change</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">What need to change?</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">What is the action that needs to be taken?</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">Tell us what needs to be done</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">This</span><span class="nv"> </span><span class="s">person</span><span class="nv"> </span><span class="s">need</span><span class="nv"> </span><span class="s">access</span><span class="nv"> </span><span class="s">to..."</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">input</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">when</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">When</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">When does it need to be done?</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">Leave empty for ASAP or specify a date</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">false</span>
  
</code></pre></div></div>

<p>Or for hardware issues:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Hardware</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Issues with hardware, like replacing, defects</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[Hardware]:</span><span class="nv"> </span><span class="s">"</span>
<span class="na">labels</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">hardware"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">triage"</span><span class="pi">]</span>
<span class="na">assignees</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">userx</span>
  <span class="pi">-</span> <span class="s">usery</span>
<span class="na">body</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">markdown</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">value</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">Use this template to request support on anykind of hardware you have; broken phone, PC not working, lost mouse, use this to get us involved.</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">input</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">person</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">Contact Details</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">Who has issues with the hardware</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">ex. email@example.net</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">true</span>

  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">dropdown</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">area</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">Area</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">Desktop</span>
        <span class="pi">-</span> <span class="s">Phone</span>  
      <span class="na">description</span><span class="pi">:</span> <span class="s">Indicate which area is impacted</span> 
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">textarea</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">what-need-to-change</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">What need to change?</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">What is the action that needs to be taken?</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">Tell us what needs to be done</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">This</span><span class="nv"> </span><span class="s">piece</span><span class="nv"> </span><span class="s">of</span><span class="nv"> </span><span class="s">equipement</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">no</span><span class="nv"> </span><span class="s">longer</span><span class="nv"> </span><span class="s">working..."</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">true</span>    
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">input</span>
    <span class="na">id</span><span class="pi">:</span> <span class="s">when</span>
    <span class="na">attributes</span><span class="pi">:</span>
      <span class="na">label</span><span class="pi">:</span> <span class="s">When</span>
      <span class="na">description</span><span class="pi">:</span> <span class="s">When does it need to be done?</span>
      <span class="na">placeholder</span><span class="pi">:</span> <span class="s">Leave empty for ASAP or specify a date</span>
    <span class="na">validations</span><span class="pi">:</span>
      <span class="na">required</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<h3 id="enhancing-clarity-with-emoticons-in-titles">Enhancing Clarity with Emoticons in Titles</h3>

<ul>
  <li><strong>Visual Differentiation</strong>: Incorporating emoticons in issue template titles can significantly aid in distinguishing between different types of issues, especially in a helpdesk context.</li>
  <li><strong>Examples</strong>:
    <ul>
      <li>Use a 💻 emoji for hardware-related issues.</li>
      <li>A 🔑 emoji could denote account-related problems.</li>
    </ul>
  </li>
  <li><strong>Immediate Recognition</strong>: Emoticons allow team members to quickly recognize the category of an issue, aiding in the swift identification and prioritization of tasks.</li>
  <li><strong>Consistency</strong>: Maintain consistency in the use of emoticons across all templates to ensure clarity and prevent confusion.</li>
</ul>

<p>By integrating emoticons into your issue titles, you bring an element of visual distinction to your GitHub helpdesk system. This not only makes the issue list more engaging but also enhances the efficiency and intuitiveness of issue categorization and management.</p>

<h3 id="configuring-the-template-selector">Configuring the Template Selector</h3>

<p><strong>Create a config.yml File</strong>: This file goes in the same <code class="language-plaintext highlighter-rouge">.github/ISSUE_TEMPLATE</code> folder. It’s used to configure how users select an issue template.
For example, here’s a sample config.yml file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">blank_issues_enabled</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">contact_links</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Freshdesk</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">https://support.x.nl/a/dashboard/default</span>
    <span class="na">about</span><span class="pi">:</span> <span class="s">For customer issues.</span>
</code></pre></div></div>

<p><strong>Configuration Options</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">blank_issues_enabled: true</code> - This setting allows users to still create blank issues. However, disabling this feature means users can only use your predefined templates.</li>
  <li><strong>Contact Links</strong>: These are additional options you can provide for users. For example:
    <ul>
      <li>Freshdesk for customer issues with a direct link to your Freshdesk dashboard.</li>
      <li>Slack for devteam issues with a direct link to your Slack workspace.</li>
    </ul>

    <p>These links not only guide users to the right resources but also help in segregating different types of issues efficiently.</p>
  </li>
</ul>

<p>By implementing these templates and configuration settings, you streamline the issue submission process. Users are guided to provide all necessary information, reducing the need for follow-up and speeding up the resolution process. To direct users to the template selector, use a URL like <code class="language-plaintext highlighter-rouge">https://github.com/yourorg/yourrepo/issues/new/choose</code>. This ensures a more organized and efficient approach to managing internal helpdesk requests.</p>

<p><img src="/images/select-issue-template.png" alt="Select an issue template" /></p>

<h2 id="effective-issue-management-workflows-and-automation">Effective Issue Management: Workflows and Automation</h2>

<p>As the number of issues in your GitHub helpdesk repository grows, effective management and maintenance become crucial. To ensure efficiency and order, we utilize a combination of GitHub workflows and Probot actions.</p>

<h3 id="preventing-empty-issues">Preventing Empty Issues</h3>

<p><strong>Request-Info Bot</strong>: One of the common frustrations in issue tracking is receiving issues with a title but no descriptive content. To address this, we implement the Request-Info bot (<a href="https://probot.github.io/apps/request-info/">Request Info Probot</a>). This bot is configured to prompt for more information on such issues. Install it in your helpdesk repository and create a <code class="language-plaintext highlighter-rouge">.github/config.yml</code> file with the following contents:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Configuration for request-info - https://github.com/behaviorbot/request-info</span>

<span class="c1"># *Required* Comment to reply with</span>
<span class="na">requestInfoReplyComment</span><span class="pi">:</span> <span class="pi">&gt;</span>
  <span class="s">We would appreciate it if you could provide us with more info about this issue!</span>

<span class="c1"># *OPTIONAL* default titles to check against for lack of descriptiveness</span>
<span class="c1"># MUST BE ALL LOWERCASE</span>
<span class="na">requestInfoDefaultTitles</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">update readme.md</span>
  <span class="pi">-</span> <span class="s">updates</span>

<span class="c1"># *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given</span>
<span class="na">requestInfoLabelToAdd</span><span class="pi">:</span> <span class="s">needs-more-info</span>
</code></pre></div></div>

<h3 id="automated-assignment-of-issues">Automated Assignment of Issues</h3>

<p><strong>Auto-Assign Issue Action</strong>: To ensure issues are promptly addressed, we use the <a href="https://github.com/marketplace/actions/auto-assign-issue">Auto-Assign Issue Action</a>. This action automatically assigns new issues to the responsible team members. While issue templates allow for assigning users, this action covers scenarios where blank issues are created.</p>

<h3 id="labeling-for-triage">Labeling for Triage</h3>

<p><strong>Triage-New-Issues App</strong>: Apply a ‘Triage’ label to new issues using the <a href="https://github.com/apps/triage-new-issues">Triage New Issues App</a>. This helps in quickly identifying and categorizing new submissions for further action.</p>

<h3 id="managing-stale-issues">Managing Stale Issues</h3>

<p><strong>Stale Issue Workflow</strong>: Keeping issues open for too long without activity can clutter your helpdesk. To manage this, we implement a workflow to mark stale issues:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Close</span><span class="nv"> </span><span class="s">stale</span><span class="nv"> </span><span class="s">issues</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">PRs'</span>
   <span class="na">on</span><span class="pi">:</span>
     <span class="na">schedule</span><span class="pi">:</span>
       <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">30</span><span class="nv"> </span><span class="s">1</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*'</span>

   <span class="na">jobs</span><span class="pi">:</span>
     <span class="na">stale</span><span class="pi">:</span>
       <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
       <span class="na">steps</span><span class="pi">:</span>
         <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/stale@v9</span>
           <span class="na">with</span><span class="pi">:</span>
             <span class="na">stale-issue-message</span><span class="pi">:</span> <span class="s1">'</span><span class="s">This</span><span class="nv"> </span><span class="s">issue</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">stale</span><span class="nv"> </span><span class="s">because</span><span class="nv"> </span><span class="s">it</span><span class="nv"> </span><span class="s">has</span><span class="nv"> </span><span class="s">been</span><span class="nv"> </span><span class="s">open</span><span class="nv"> </span><span class="s">30</span><span class="nv"> </span><span class="s">days</span><span class="nv"> </span><span class="s">with</span><span class="nv"> </span><span class="s">no</span><span class="nv"> </span><span class="s">activity.</span><span class="nv"> </span><span class="s">Remove</span><span class="nv"> </span><span class="s">stale</span><span class="nv"> </span><span class="s">label</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">comment</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">this</span><span class="nv"> </span><span class="s">will</span><span class="nv"> </span><span class="s">be</span><span class="nv"> </span><span class="s">closed</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">5</span><span class="nv"> </span><span class="s">days.'</span>
             <span class="na">days-before-stale</span><span class="pi">:</span> <span class="m">30</span>
             <span class="na">days-before-close</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>
<p>This workflow runs daily, identifying issues open for more than 30 days and marking them as stale. This process helps maintain a cleaner, more manageable issue list and ensures issues don’t linger unresolved. You do want to consider the impact of closing stale issues, however. For example, if an issue is still relevant, you might want to keep it open, even if it’s stale. However, this can help with SLA compliance and ensure issues are addressed in a timely manner.</p>

<p>By integrating these tools and workflows, you create a dynamic and responsive helpdesk system within GitHub, capable of handling issues efficiently and ensuring nothing falls through the cracks.</p>

<h2 id="harnessing-data-implementing-analytics-in-your-github-helpdesk">Harnessing Data: Implementing Analytics in Your GitHub Helpdesk</h2>

<p>One of the advantages of professional helpdesk systems is their capability to provide analytics, such as resolution times and the duration for which issues remain open. While GitHub might not offer these features out-of-the-box, we can achieve similar analytics through a custom workflow.</p>

<h3 id="setting-up-monthly-issue-metrics">Setting Up Monthly Issue Metrics</h3>

<p>This workflow is designed to run monthly and collect data on issues created and resolved within that period, providing valuable insights into the performance of your helpdesk system.</p>

<p><strong>Workflow Configuration</strong>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Monthly issue metrics</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3</span><span class="nv"> </span><span class="s">2</span><span class="nv"> </span><span class="s">1</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*'</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">issue metrics</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">actions</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">issues</span><span class="pi">:</span> <span class="s">write</span>
    <span class="na">steps</span><span class="pi">:</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get dates for last month</span>
      <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
      <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s"># Script to calculate date range of the previous month</span>
        <span class="s">[Date calculation script here]</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run issue-metrics tool</span>
      <span class="na">uses</span><span class="pi">:</span> <span class="s">github/issue-metrics@v2</span>
      <span class="na">env</span><span class="pi">:</span>
        <span class="na">GH_TOKEN</span><span class="pi">:</span> <span class="s">${{ secrets.GITHUB_TOKEN }}</span>
        <span class="na">SEARCH_QUERY</span><span class="pi">:</span> <span class="s1">'</span><span class="s">repo:yourrepo/servicedesk</span><span class="nv"> </span><span class="s">is:issue</span><span class="nv"> </span><span class="s">created:${{</span><span class="nv"> </span><span class="s">env.last_month</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">-reason:"not</span><span class="nv"> </span><span class="s">planned"'</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create issue</span>
      <span class="na">uses</span><span class="pi">:</span> <span class="s">peter-evans/create-issue-from-file@v4</span>
      <span class="na">with</span><span class="pi">:</span>
        <span class="na">title</span><span class="pi">:</span> <span class="s">Monthly issue metrics report</span>
        <span class="na">content-filepath</span><span class="pi">:</span> <span class="s">./issue_metrics.md</span>
        <span class="na">assignees</span><span class="pi">:</span> <span class="s">mivano</span>
</code></pre></div></div>

<p><strong>Key Components</strong>:</p>
<ul>
  <li><strong>Date Calculation</strong>: The workflow begins by calculating the date range for the previous month. This range is used to filter the issues for the metrics report.</li>
  <li><strong>Issue Metrics Tool</strong>: Utilizing the ‘github/issue-metrics@v2’ action, the workflow gathers data on issues created within the specified date range.</li>
  <li><strong>Report Creation</strong>: An issue is automatically created containing the metrics, which can then be labeled, assigned, and reviewed like any other issue.</li>
</ul>

<p>This workflow transforms GitHub into a more robust helpdesk tool, providing monthly analytics similar to those offered by specialized helpdesk systems. The generated report gives a clear picture of helpdesk performance, helping identify areas for improvement. More information on this action and its capabilities can be found on the <a href="https://github.blog/2023-07-19-metrics-for-issues-pull-requests-and-discussions/">GitHub Blog</a>. By incorporating this workflow, you enhance the functionality of your GitHub helpdesk, leveraging data to drive efficiency and effectiveness.</p>

<h2 id="automating-recurring-tasks-workflow-for-scheduled-issues">Automating Recurring Tasks: Workflow for Scheduled Issues</h2>

<p>In the management of any helpdesk or support system, certain tasks are bound to recur regularly. These can range from routine checks to periodic backups. To handle such recurring issues efficiently within GitHub, we can leverage the power of automated workflows. This approach ensures that these tasks are consistently addressed without fail.</p>

<h3 id="creating-a-workflow-for-recurring-issues">Creating a Workflow for Recurring Issues</h3>

<p>Here’s an example of a workflow designed to create a new issue for quarterly checks of users and authorizations:</p>

<p><strong>Workflow Definition</strong>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Quarterly check users and authorisations</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>    
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s">0 12 1 */3 *</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">create_issue</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Create issue - Quarterly check users and authorisations</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create issue</span> 
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">new_issue_url=$(gh issue create \</span>
            <span class="s">--title "$TITLE" \</span>
            <span class="s">--label "$LABELS" \</span>
            <span class="s">--body "$BODY" \</span>
            <span class="s">--assignee "$ASSIGNEE")</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GITHUB_TOKEN</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">GH_REPO</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">TITLE</span><span class="pi">:</span> <span class="s">Quarterly check users and authorisations</span>
          <span class="na">LABELS</span><span class="pi">:</span> <span class="s">iso</span>
          <span class="na">ASSIGNEE</span><span class="pi">:</span> <span class="s">userX,userY</span>
          <span class="na">BODY</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">Perform the quarterly checks for users and the authorisations. See the ISMS for more details.</span>
          <span class="na">PINNED</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p><strong>Key Components</strong>:</p>
<ul>
  <li><strong>Cron Schedule</strong>: Set to trigger quarterly (<code class="language-plaintext highlighter-rouge">0 12 1 */3 *</code>). This timing can be adjusted based on the frequency needed for the particular task.</li>
  <li><strong>Issue Creation Step</strong>: Automates the creation of a new GitHub issue with specific details like title, labels, body content, and assignees.</li>
</ul>

<h3 id="expanding-the-workflow">Expanding the Workflow</h3>

<p>The beauty of this system lies in its flexibility. By modifying the trigger and the content of the issue, and saving it as a new file in the <code class="language-plaintext highlighter-rouge">.github/workflows</code> folder, you can create workflows for various other recurring issues. This method is not only efficient but also ensures consistency and reliability in performing routine tasks that are crucial for your organization.</p>

<p>Implementing such automated workflows for recurring issues in your GitHub helpdesk repository aids in maintaining regularity and ensures that important tasks are not overlooked. This level of automation brings a new dimension of efficiency to your internal helpdesk operations.</p>

<h2 id="conclusion-github-as-an-ideal-internal-helpdesk-solution">Conclusion: GitHub as an Ideal Internal Helpdesk Solution</h2>

<p>In conclusion, leveraging GitHub for your internal helpdesk system offers numerous advantages, especially when considering the ecosystem and tooling already familiar to many teams. This approach not only streamlines internal support processes but also adds a layer of efficiency and integration that traditional helpdesk tools may not provide.</p>

<h3 id="key-advantages-of-using-github-for-internal-helpdesk">Key Advantages of Using GitHub for Internal Helpdesk:</h3>

<ol>
  <li>
    <p><strong>Unified Ecosystem</strong>: Staying within GitHub means no need to juggle between different platforms, reducing the learning curve and integration complexities.</p>
  </li>
  <li>
    <p><strong>Cost-Effective</strong>: Eliminates the additional costs associated with third-party helpdesk tools, as GitHub already forms part of many organizations’ toolsets.</p>
  </li>
  <li>
    <p><strong>Robust Issue Tracking Features</strong>: Utilize GitHub’s native issue features like templates, formatting options, attachments, mentions, and cross-referencing, enhancing communication and tracking efficiency.</p>
  </li>
  <li>
    <p><strong>Customizable Workflows and Apps</strong>: The ability to add workflows and apps further tailors the experience to your organization’s specific needs.</p>
  </li>
  <li>
    <p><strong>Project Boards for Visualization</strong>: Employ GitHub’s project boards for a visual representation of tasks, allowing you to effortlessly manage and move items through to completion.</p>
  </li>
</ol>

<p>However, it’s important to note a key aspect regarding issue visibility.</p>

<p><strong>Visibility of Issues:</strong></p>
<ul>
  <li>In GitHub, issues in a repository are visible to all team members with write access, as this access level is required to create issues.</li>
  <li>Consequently, all tickets are visible to these team members. While this promotes transparency and team collaboration, it also means sensitive information could potentially be exposed to a wider internal audience.</li>
  <li>Remember, as this is an internal system, it’s generally expected that confidential or sensitive information should not be shared in these tickets. Nonetheless, it’s vital to consider this aspect of visibility.</li>
</ul>

<p><strong>Securing Your Helpdesk:</strong></p>
<ul>
  <li>If complete privacy for issues is a priority, consider securing the repository. This adds a layer of confidentiality but requires alternative methods for issue creation.</li>
  <li>One such method is using email integrations for issue creation, like the solution offered by <a href="https://www.scitor.io">scitor.io</a>. This allows for a more controlled and private submission of issues while still benefiting from GitHub’s issue management capabilities.</li>
</ul>

<p>By adding these considerations into your strategy, you can tailor your GitHub helpdesk to meet both the operational and privacy needs of your organization. This enhanced approach ensures that you not only leverage GitHub for its efficiency and familiarity but also maintain the necessary level of security and privacy for internal operations.</p>

<h3 id="external-use-considerations">External Use Considerations:</h3>

<p>While GitHub excels for internal use, there are challenges when considering external helpdesk applications. Non-GitHub users cannot create issues, and external access to a private helpdesk repository is not advisable. Additionally, GitHub doesn’t natively handle email-based ticket creation or manage attachments from external sources effectively.</p>

<h3 id="solution-for-external-use-scitorio">Solution for External Use: Scitor.io</h3>

<p>For those seeking to extend GitHub’s capabilities to an external audience, <a href="https://www.scitor.io">Scitor.io</a> offers a solution. It transforms GitHub into a more traditional helpdesk system, bridging the gap for external user interactions.</p>

<h3 id="wrapping-up">Wrapping Up</h3>

<p>For internal team use, GitHub stands out as a practical, cost-effective, and efficient tool for helpdesk management. It consolidates various aspects of issue tracking and resolution into a single, integrated platform, familiar to most developers and IT professionals. By following the outlined steps and considerations, you can effectively transform GitHub into a powerful tool for managing your internal helpdesk needs, harnessing its full potential to streamline and enhance your support processes.</p>

<blockquote>
  <p>Photo by <a href="https://unsplash.com/@cdc?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">CDC</a> on <a href="https://unsplash.com/photos/man-in-black-and-white-checkered-dress-shirt-using-computer-_XLJy3h77cw?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></p>
</blockquote>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="github" /><category term="github-actions" /><category term="helpdesk" /><summary type="html"><![CDATA[Setting the Scene: Choosing GitHub Issues for Internal Helpdesk Needs.]]></summary></entry><entry><title type="html">Apply caching when restoring NuGet packages using a GitHub hosted runner</title><link href="https://mindbyte.nl/2023/09/22/apply-caching-restoring-nuget-packages-github-hosted-runner.html" rel="alternate" type="text/html" title="Apply caching when restoring NuGet packages using a GitHub hosted runner" /><published>2023-09-22T00:00:00+02:00</published><updated>2023-09-22T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/09/22/apply-caching-restoring-nuget-packages-github-hosted-runner</id><content type="html" xml:base="https://mindbyte.nl/2023/09/22/apply-caching-restoring-nuget-packages-github-hosted-runner.html"><![CDATA[<p>When you are using a GitHub hosted runner to build your project, you can apply caching to speed up the build process. This is especially useful when you are using NuGet packages. 
As you get a new runner every time you run a build, you will need to restore all NuGet packages every time. This can take a lot of time as it is not the most efficient network calls.</p>

<p>To speed up the process, you can cache the NuGet packages. This will make sure that the next time you run a build, the NuGet packages will be restored from the cache instead of the NuGet website. 
To make sure that the cache is invalidated when a new version of a NuGet package is referenced, you can use the hash of the project files as part of the cache key. This will make sure that the cache is invalidated when changes are made to the project files.</p>

<p>For completness sake, I have included a full example of a workflow file that uses caching to speed up the build process. GitHub will automatically add a post step to store the files in the cache.</p>

<p>By setting the <code class="language-plaintext highlighter-rouge">NUGET_PACKAGES</code> environment variable, you can make sure that the NuGet packages are restored to the same location as the cache. This will make sure that the cache is used when restoring the NuGet packages.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build Services</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">windows-latest</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build services</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
    <span class="na">env</span><span class="pi">:</span>
      <span class="na">NUGET_PACKAGES</span><span class="pi">:</span> <span class="s">${{ github.workspace }}/.nuget/packages</span>    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup .NET</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-dotnet@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">dotnet-version</span><span class="pi">:</span> <span class="s">7.0.x</span>

      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/cache@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">${{ github.workspace }}\.nuget\packages</span>
          <span class="na">key</span><span class="pi">:</span> <span class="s">${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}</span> <span class="c1">#hash of project files</span>
          <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">${{ runner.os }}-nuget-</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Restore dependencies</span>
        <span class="na">run</span><span class="pi">:</span>  <span class="s">dotnet restore solution.sln</span> 

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">dotnet build solution.sln  --no-restore</span>
        
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Test</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">dotnet test solution.sln  --no-build --verbosity normal</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Publish</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span> 
          <span class="s">dotnet publish solution.sln -c Release --no-restore  -o publish </span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload a Build Artifact</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">services</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">publish/**</span>
          <span class="na">if-no-files-found</span><span class="pi">:</span> <span class="s">error</span> 
</code></pre></div></div>

<p>This workflow will restore, build, test and package the solution. The resulting package will be uploaded as an artifact and can be deployed in subsequent steps.</p>

<p>Do use the logs to verify that the cache is used and validate if the amount of time that is used is worthwhile.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="GitHub Actions" /><category term="GitHub" /><summary type="html"><![CDATA[When you are using a GitHub hosted runner to build your project, you can apply caching to speed up the build process. This is especially useful when you are using NuGet packages. As you get a new runner every time you run a build, you will need to restore all NuGet packages every time. This can take a lot of time as it is not the most efficient network calls.]]></summary></entry><entry><title type="html">Sending Emails On Behalf of Someone Else in SaaS Solutions Using SendGrid and .NET</title><link href="https://mindbyte.nl/2023/08/29/sending-emails-behalf-saas-solutions-sendgrid-net.html" rel="alternate" type="text/html" title="Sending Emails On Behalf of Someone Else in SaaS Solutions Using SendGrid and .NET" /><published>2023-08-29T00:00:00+02:00</published><updated>2023-08-29T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/29/sending-emails-behalf-saas-solutions-sendgrid-net</id><content type="html" xml:base="https://mindbyte.nl/2023/08/29/sending-emails-behalf-saas-solutions-sendgrid-net.html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>In the modern SaaS landscape, flexibility is key. One commonly sought-after feature in Helpdesk and similar software is the ability to send emails on behalf of a different domain—your customer’s domain, for instance. However, spam prevention and domain authentication can make this tricky. If you’re building a SaaS solution that deals with email services, like my current project <a href="https://www.scitor.io">Scitor</a>, this blog post is for you. In it, I’ll explain how I implemented a secure way to send emails from my customer’s domain, using SendGrid and .NET.</p>

<h2 id="the-problem">The Problem</h2>
<p>When implementing <a href="https://www.scitor.io">Scitor</a>, a Helpdesk SaaS that accepts emails and converts them into GitHub discussions, I ran into a challenge. The solution needed to send replies back to the original recipient’s email address, and many customers wanted those emails to come from their own domain.</p>

<p>However, due to spam prevention measures, you can’t simply send emails from a domain you don’t own or haven’t authenticated. This is not only bad for your spam reputation; SendGrid explicitly forbids it unless the ‘from’ email address has been verified.</p>

<h2 id="the-solution-domain-authentication">The Solution: Domain Authentication</h2>

<p>I found a way to allow customers to authenticate their domain without giving them access to my SendGrid account. I also did not have a GUI or admin interface for my solution, so I needed to automate the process as much as possible within the GitHub discussion.</p>

<h3 id="step-1-the-authenticate-domain-command">Step 1: The <code class="language-plaintext highlighter-rouge">/authenticate-domain</code> Command</h3>

<p>The first step is to create a command—<code class="language-plaintext highlighter-rouge">/authenticate-domain</code> that customers can use to begin the domain authentication process. When a customer issues this command by adding it to a comment in the discussions, the following code creates a domain registration at SendGrid via the StrongGrid library:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">DomainRegistrationResult</span><span class="p">&gt;</span> <span class="nf">CreateDomain</span><span class="p">(</span><span class="kt">string</span> <span class="n">domain</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_strongGridClient</span><span class="p">.</span><span class="n">SenderAuthentication</span><span class="p">.</span><span class="nf">CreateDomainAsync</span><span class="p">(</span><span class="n">domain</span><span class="p">);</span>

    <span class="k">return</span> <span class="k">new</span> <span class="nf">DomainRegistrationResult</span><span class="p">(</span>
        <span class="n">result</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
        <span class="n">result</span><span class="p">.</span><span class="n">IsValid</span><span class="p">,</span>
        <span class="k">new</span> <span class="nf">DNSRecordResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim1</span><span class="p">.</span><span class="n">Data</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim1</span><span class="p">.</span><span class="n">Type</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim1</span><span class="p">.</span><span class="n">Host</span><span class="p">),</span>
        <span class="k">new</span> <span class="nf">DNSRecordResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim2</span><span class="p">.</span><span class="n">Data</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim2</span><span class="p">.</span><span class="n">Type</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">Dkim2</span><span class="p">.</span><span class="n">Host</span><span class="p">),</span>
        <span class="k">new</span> <span class="nf">DNSRecordResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">MailCName</span><span class="p">.</span><span class="n">Data</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">MailCName</span><span class="p">.</span><span class="n">Type</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">DNS</span><span class="p">.</span><span class="n">MailCName</span><span class="p">.</span><span class="n">Host</span><span class="p">)</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">DomainRegistrationResult</span><span class="p">(</span><span class="kt">long</span> <span class="n">DomainId</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">IsValid</span><span class="p">,</span> <span class="n">DNSRecordResult</span> <span class="n">Dkim1</span><span class="p">,</span> <span class="n">DNSRecordResult</span> <span class="n">Dkim2</span><span class="p">,</span> <span class="n">DNSRecordResult</span> <span class="n">CName</span><span class="p">);</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">DNSRecordResult</span><span class="p">(</span><span class="kt">string</span> <span class="n">Data</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Type</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Host</span><span class="p">);</span>
</code></pre></div></div>

<p>The function returns DNS records that the customer needs to configure on their end.</p>

<h3 id="step-2-configuring-dns-settings">Step 2: Configuring DNS Settings</h3>

<p>I output these DNS settings into the GitHub discussion as an additional comment. The customer is then responsible for adding these DNS records, a process that can take some time and differ per DNS provider.</p>

<p><img src="/images/domain-auth.png" alt="" /></p>

<h3 id="step-3-the-verify-domain-command">Step 3: The <code class="language-plaintext highlighter-rouge">/verify-domain</code> Command</h3>

<p>Once the customer has configured their DNS settings, they issue the <code class="language-plaintext highlighter-rouge">/verify-domain</code> command. This function verifies the domain using SendGrid’s API:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">DomainValidationResult</span><span class="p">&gt;</span> <span class="nf">VerifyDomain</span><span class="p">(</span><span class="kt">long</span> <span class="n">domainId</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_strongGridClient</span><span class="p">.</span><span class="n">SenderAuthentication</span><span class="p">.</span><span class="nf">ValidateDomainAsync</span><span class="p">(</span><span class="n">domainId</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">DomainValidationResult</span><span class="p">(</span>
        <span class="n">result</span><span class="p">.</span><span class="n">DomainId</span><span class="p">,</span>
        <span class="n">result</span><span class="p">.</span><span class="n">IsValid</span><span class="p">,</span>
        <span class="k">new</span> <span class="nf">DNSRecordValidationResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Dkim1</span><span class="p">.</span><span class="n">IsValid</span><span class="p">,</span>
            <span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Dkim1</span><span class="p">.</span><span class="n">Reason</span><span class="p">),</span>
        <span class="k">new</span> <span class="nf">DNSRecordValidationResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Dkim2</span><span class="p">.</span><span class="n">IsValid</span><span class="p">,</span>
            <span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Dkim2</span><span class="p">.</span><span class="n">Reason</span><span class="p">),</span>
        <span class="k">new</span> <span class="nf">DNSRecordValidationResult</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Mail</span><span class="p">.</span><span class="n">IsValid</span><span class="p">,</span>
            <span class="n">result</span><span class="p">.</span><span class="n">ValidationResults</span><span class="p">.</span><span class="n">Mail</span><span class="p">.</span><span class="n">Reason</span><span class="p">)</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">DomainValidationResult</span><span class="p">(</span><span class="kt">long</span> <span class="n">DomainId</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">IsValid</span><span class="p">,</span> <span class="n">DNSRecordValidationResult</span> <span class="n">Dkim1</span><span class="p">,</span> <span class="n">DNSRecordValidationResult</span> <span class="n">Dkim2</span><span class="p">,</span> <span class="n">DNSRecordValidationResult</span> <span class="n">CName</span><span class="p">);</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">DNSRecordValidationResult</span><span class="p">(</span><span class="kt">bool</span> <span class="n">IsValid</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Reason</span><span class="p">);</span>
</code></pre></div></div>

<p>When all records are validated successfully, the customer can start using their own domain as the sender. If not, I list the problems that still need to be fixed.</p>

<p><img src="/images/domain-verify.png" alt="" /></p>

<p>I do store the returned <code class="language-plaintext highlighter-rouge">domainId</code> in my database, so I can use it to remove the domain later if needed and check if the verification has been completed or not. There is also an explicit <code class="language-plaintext highlighter-rouge">/delete-domain</code> command that customers can use to remove the domain from my SendGrid account.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Domain authentication may sound complex, but with a bit of code and the help of SendGrid’s StrongGrid library, it can be automated to a large extent, even for customers who don’t have direct access to your SendGrid account. This ensures that your SaaS can send emails from different domains while maintaining good spam reputation and adhering to SendGrid’s rules.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="dotnet" /><category term="Coding" /><summary type="html"><![CDATA[Introduction In the modern SaaS landscape, flexibility is key. One commonly sought-after feature in Helpdesk and similar software is the ability to send emails on behalf of a different domain—your customer’s domain, for instance. However, spam prevention and domain authentication can make this tricky. If you’re building a SaaS solution that deals with email services, like my current project Scitor, this blog post is for you. In it, I’ll explain how I implemented a secure way to send emails from my customer’s domain, using SendGrid and .NET.]]></summary></entry><entry><title type="html">Making API calls more resilient</title><link href="https://mindbyte.nl/2023/08/28/making-api-calls-resilient.html" rel="alternate" type="text/html" title="Making API calls more resilient" /><published>2023-08-28T00:00:00+02:00</published><updated>2023-08-28T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/28/making-api-calls-resilient</id><content type="html" xml:base="https://mindbyte.nl/2023/08/28/making-api-calls-resilient.html"><![CDATA[<p>In the world of networked applications, call failures are a common occurrence due to the inherent unreliability of networks. When working with the Azure Cost API, challenges such as indefinite retries, excessive wait times, and rate limitations can impede efficiency and efficacy. This post explores a method to make API calls more resilient, taking into account these critical factors.</p>

<h2 id="unreliable-network-and-rate-limitations">Unreliable Network and Rate Limitations</h2>

<p>When making requests to an API, calls can fail for various reasons ranging from network unreliability to server-side rate limits. Repeated retries, lengthy waiting periods, or disregard for server-specified limitations can lead to inefficiencies or even service denial.</p>

<h2 id="polly---net-resilience-library">Polly - .NET Resilience Library</h2>

<p>To address these challenges, <a href="https://www.thepollyproject.org">Polly</a>, a .NET resilience and transient-fault-handling library, was employed. It enables developers to define policies like Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback, thereby increasing the resilience of API calls.</p>

<h2 id="defining-a-resilient-policy-with-polly">Defining a Resilient Policy with Polly</h2>

<p>The core of this approach lies in defining a resilient policy that combines a wait-and-retry policy with a timeout policy. Below is the code snippet that showcases this process:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">PollyPolicyExtensions</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">IAsyncPolicy</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span> <span class="nf">GetRetryAfterPolicy</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// Define WaitAndRetry policy</span>
        <span class="kt">var</span> <span class="n">waitAndRetryPolicy</span> <span class="p">=</span> <span class="n">Policy</span><span class="p">.</span><span class="n">HandleResult</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;(</span><span class="n">msg</span> <span class="p">=&gt;</span> 
                <span class="n">msg</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">TooManyRequests</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">WaitAndRetryAsync</span><span class="p">(</span>
                <span class="n">retryCount</span><span class="p">:</span> <span class="m">5</span><span class="p">,</span>
                <span class="n">sleepDurationProvider</span><span class="p">:</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
                    <span class="kt">var</span> <span class="n">headers</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">Result</span><span class="p">?.</span><span class="n">Headers</span><span class="p">;</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">headers</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">header</span> <span class="k">in</span> <span class="n">headers</span><span class="p">)</span>
                        <span class="p">{</span>
                            <span class="k">if</span> <span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Key</span><span class="p">.</span><span class="nf">ToLower</span><span class="p">().</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"retry-after"</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="n">header</span><span class="p">.</span><span class="n">Value</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
                            <span class="p">{</span>
                                <span class="k">if</span> <span class="p">(</span><span class="kt">int</span><span class="p">.</span><span class="nf">TryParse</span><span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Value</span><span class="p">.</span><span class="nf">First</span><span class="p">(),</span> <span class="k">out</span> <span class="kt">int</span> <span class="n">seconds</span><span class="p">))</span>
                                <span class="p">{</span>
                                    <span class="k">return</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="n">seconds</span><span class="p">);</span>
                                <span class="p">}</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                    <span class="c1">// If no header with a retry-after value is found, fall back to 2 seconds.</span>
                    <span class="k">return</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">2</span><span class="p">);</span>
                <span class="p">},</span>
                <span class="n">onRetryAsync</span><span class="p">:</span> <span class="p">(</span><span class="n">msg</span><span class="p">,</span> <span class="n">time</span><span class="p">,</span> <span class="n">retries</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span>
            <span class="p">);</span>

        <span class="c1">// Define Timeout policy</span>
        <span class="kt">var</span> <span class="n">timeoutPolicy</span> <span class="p">=</span> <span class="n">Policy</span><span class="p">.</span><span class="n">TimeoutAsync</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">60</span><span class="p">));</span>

        <span class="c1">// Wrap WaitAndRetry with Timeout</span>
        <span class="kt">var</span> <span class="n">resilientPolicy</span> <span class="p">=</span> <span class="n">Policy</span><span class="p">.</span><span class="nf">WrapAsync</span><span class="p">(</span><span class="n">timeoutPolicy</span><span class="p">,</span> <span class="n">waitAndRetryPolicy</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">resilientPolicy</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This policy intelligently handles HTTP 429 (Too Many Requests) responses by parsing the “retry-after” header and waiting for the specified duration before retrying, up to five times. If the header isn’t found, it falls back to a two-second wait time. Additionally, a timeout policy ensures that calls do not wait longer than 60 seconds.</p>

<h2 id="applying-the-policy">Applying the Policy</h2>

<p>The defined policy is then applied to the HTTP client, as shown below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">registrations</span><span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"CostApi"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
<span class="p">{</span>
  <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://management.azure.com/"</span><span class="p">);</span>
  <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"Accept"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
<span class="p">}).</span><span class="nf">AddPolicyHandler</span><span class="p">(</span><span class="n">PollyPolicyExtensions</span><span class="p">.</span><span class="nf">GetRetryAfterPolicy</span><span class="p">());</span>
</code></pre></div></div>

<p>This registration is placed in the <code class="language-plaintext highlighter-rouge">ConfigureServices</code> method of the <code class="language-plaintext highlighter-rouge">Startup</code> class, ensuring that the policy is applied to all HTTP calls made by the application when they request this <code class="language-plaintext highlighter-rouge">CostApi</code> httpclient from the factory.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The combination of Polly’s features offers a robust and scalable solution to the common challenges faced when making resilient API calls. By leveraging this method, developers can create a more stable and efficient communication with the Azure Cost API, providing a seamless experience even in unpredictable network conditions.</p>

<p>For those looking to enhance their API interactions, exploring Polly and the resilient policies it supports can be a game-changer. The flexibility and reliability it offers make it an essential tool for modern developers navigating the complexities of network communication.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="Azure" /><category term="REST" /><category term="API" /><summary type="html"><![CDATA[In the world of networked applications, call failures are a common occurrence due to the inherent unreliability of networks. When working with the Azure Cost API, challenges such as indefinite retries, excessive wait times, and rate limitations can impede efficiency and efficacy. This post explores a method to make API calls more resilient, taking into account these critical factors.]]></summary></entry><entry><title type="html">Retrieving the active Azure Subscription ID from the AZ CLI Context</title><link href="https://mindbyte.nl/2023/08/26/retrieving-active-azure-subscription-id-az-cli-context.html" rel="alternate" type="text/html" title="Retrieving the active Azure Subscription ID from the AZ CLI Context" /><published>2023-08-26T00:00:00+02:00</published><updated>2023-08-26T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/26/retrieving-active-azure-subscription-id-az-cli-context</id><content type="html" xml:base="https://mindbyte.nl/2023/08/26/retrieving-active-azure-subscription-id-az-cli-context.html"><![CDATA[<p>Managing Azure subscriptions can be a delicate task, especially when dealing with various tools and applications that require context-specific information. In building the <a href="https://github.com/mivano/azure-cost-cli">Azure Cost CLI</a> a requirement arose to determine the current active subscription ID when it’s not explicitly passed in. This post will dive into the process of retrieving this information, similar to the Azure CLI’s default behavior set by <code class="language-plaintext highlighter-rouge">az account set -s</code>.</p>

<h2 id="default-subscription-id">Default Subscription ID</h2>

<p>The Azure CLI allows users to set a default subscription ID, and mimicking this behavior in the Azure Cost CLI tool brings in alignment and familiarity. The question was, how could this be achieved programmatically?</p>

<h2 id="executing-the-az-command">Executing the AZ Command</h2>

<p>The solution lay in executing the Azure CLI’s <code class="language-plaintext highlighter-rouge">az account show</code> command, which returns information about the current account, including the active subscription ID.</p>

<p>The following C# code is used to execute the command and extract the subscription ID:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">AzCommand</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">GetDefaultAzureSubscriptionId</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">startInfo</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ProcessStartInfo</span>
        <span class="p">{</span>
            <span class="n">FileName</span> <span class="p">=</span> <span class="s">"az"</span><span class="p">,</span>
            <span class="n">Arguments</span> <span class="p">=</span> <span class="s">"account show"</span><span class="p">,</span>
            <span class="n">RedirectStandardOutput</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">RedirectStandardError</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">UseShellExecute</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span>
            <span class="n">CreateNoWindow</span> <span class="p">=</span> <span class="k">true</span>
        <span class="p">};</span>

        <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">process</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Process</span> <span class="p">{</span> <span class="n">StartInfo</span> <span class="p">=</span> <span class="n">startInfo</span> <span class="p">})</span>
        <span class="p">{</span>
            <span class="n">process</span><span class="p">.</span><span class="nf">Start</span><span class="p">();</span>
            <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">process</span><span class="p">.</span><span class="n">StandardOutput</span><span class="p">.</span><span class="nf">ReadToEnd</span><span class="p">();</span>
            <span class="n">process</span><span class="p">.</span><span class="nf">WaitForExit</span><span class="p">();</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">process</span><span class="p">.</span><span class="n">ExitCode</span> <span class="p">!=</span> <span class="m">0</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="kt">string</span> <span class="n">error</span> <span class="p">=</span> <span class="n">process</span><span class="p">.</span><span class="n">StandardError</span><span class="p">.</span><span class="nf">ReadToEnd</span><span class="p">();</span>
                <span class="k">throw</span> <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">$"Error executing 'az account show': </span><span class="p">{</span><span class="n">error</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">jsonDocument</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">output</span><span class="p">))</span>
            <span class="p">{</span>
                <span class="n">JsonElement</span> <span class="n">root</span> <span class="p">=</span> <span class="n">jsonDocument</span><span class="p">.</span><span class="n">RootElement</span><span class="p">;</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">root</span><span class="p">.</span><span class="nf">TryGetProperty</span><span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="k">out</span> <span class="n">JsonElement</span> <span class="n">idElement</span><span class="p">))</span>
                <span class="p">{</span>
                    <span class="kt">string</span> <span class="n">subscriptionId</span> <span class="p">=</span> <span class="n">idElement</span><span class="p">.</span><span class="nf">GetString</span><span class="p">();</span>
                    <span class="k">return</span> <span class="n">subscriptionId</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="k">else</span>
                <span class="p">{</span>
                    <span class="k">throw</span> <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">"Unable to find the 'id' property in the JSON output."</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s what the code does, step by step:</p>

<ol>
  <li><strong>Creating Process Start Information</strong>: A <code class="language-plaintext highlighter-rouge">ProcessStartInfo</code> object is initialized with the required command and settings.</li>
  <li><strong>Executing the Process</strong>: A new process is started to execute the command, and the standard output is read.</li>
  <li><strong>Error Handling</strong>: If the command execution fails, an error is thrown with the details.</li>
  <li><strong>Parsing the Output</strong>: The JSON output is parsed to retrieve the <code class="language-plaintext highlighter-rouge">id</code> property, which holds the subscription ID.</li>
</ol>

<h2 id="seamless-integration">Seamless Integration</h2>

<p>The above code snippet integrates seamlessly into the Azure Cost CLI tool, providing an efficient and consistent way to retrieve the current active Azure subscription ID. By leveraging existing CLI commands and C# process management, developers can easily obtain context-specific information.</p>

<p>This example further illustrates how familiar tools and libraries can be used to build sophisticated features, bridging gaps between different systems and providing a cohesive user experience.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><summary type="html"><![CDATA[Managing Azure subscriptions can be a delicate task, especially when dealing with various tools and applications that require context-specific information. In building the Azure Cost CLI a requirement arose to determine the current active subscription ID when it’s not explicitly passed in. This post will dive into the process of retrieving this information, similar to the Azure CLI’s default behavior set by az account set -s.]]></summary></entry><entry><title type="html">How to get a token to access the Azure cost API</title><link href="https://mindbyte.nl/2023/08/22/token-access-azure-cost-api.html" rel="alternate" type="text/html" title="How to get a token to access the Azure cost API" /><published>2023-08-22T00:00:00+02:00</published><updated>2023-08-22T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/22/token-access-azure-cost-api</id><content type="html" xml:base="https://mindbyte.nl/2023/08/22/token-access-azure-cost-api.html"><![CDATA[<p>The <a href="https://github.com/mivano/azure-cost-cli">Azure Cost CLI</a> is a command-line dotnet tool created to facilitate interaction with Azure’s cloud costs. While the development of the tool was a technical endeavor, it posed a significant challenge in one particular area: authentication.</p>

<h2 id="authentication-complexity">Authentication Complexity</h2>

<p>Obtaining a token to access the Azure Cost API without burdening users with usernames, passwords, or the creation of service accounts was a challenging issue. The need for a straightforward application that could retrieve the account information directly from the environment necessitated a unique approach.</p>

<h2 id="utilizing-defaultazurecredentials">Utilizing DefaultAzureCredentials</h2>

<p>The answer was found in <code class="language-plaintext highlighter-rouge">DefaultAzureCredentials</code>. This functionality attempts <a href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredential.cs">various credential providers</a> to find a valid one, offering a seamless way to authenticate. The approach enabled the retrieval of a token without adding complex authentication steps.</p>

<h2 id="chainedtokencredential-with-azureclicredential">ChainedTokenCredential with AzureCliCredential</h2>

<p>To refine the process, a new <code class="language-plaintext highlighter-rouge">ChainedTokenCredential</code> was created, placing the <code class="language-plaintext highlighter-rouge">AzureCliCredential</code> at the start and then falling back to the default options if necessary. Below is the code snippet that encapsulates this elegant solution:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Get the token by using the DefaultAzureCredential, but try the AzureCliCredential first</span>
<span class="kt">var</span> <span class="n">tokenCredential</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ChainedTokenCredential</span><span class="p">(</span>
    <span class="k">new</span> <span class="nf">AzureCliCredential</span><span class="p">(),</span>
    <span class="k">new</span> <span class="nf">DefaultAzureCredential</span><span class="p">());</span>

<span class="c1">// Fetch the token and ask explicitly for the Azure Cost API scope</span>
<span class="kt">var</span> <span class="n">token</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tokenCredential</span><span class="p">.</span><span class="nf">GetTokenAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">TokenRequestContext</span><span class="p">(</span><span class="k">new</span><span class="p">[]</span>
    <span class="p">{</span> <span class="s">$"https://management.azure.com/.default"</span> <span class="p">}));</span>

<span class="c1">// Set the resulting token as the bearer token for the HTTP client</span>
<span class="n">_client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">token</span><span class="p">.</span><span class="n">Token</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="simplicity-and-flexibility">Simplicity and Flexibility</h3>

<p>This method’s success lies in its simplicity and flexibility, enhancing user experience and leveraging existing Azure CLI setups across different environments.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The Azure Cost CLI tool is more than a utility; it represents a thoughtful response to a real-world problem. By innovatively combining <code class="language-plaintext highlighter-rouge">ChainedTokenCredential</code> with <code class="language-plaintext highlighter-rouge">AzureCliCredential</code>, a solution was crafted that is not only efficient and secure but also simple to use.</p>

<p>For developers and administrators seeking a user-friendly way to interact with the Azure Cost API, this approach provides a practical and elegant path forward.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="Azure" /><category term="Cost" /><summary type="html"><![CDATA[The Azure Cost CLI is a command-line dotnet tool created to facilitate interaction with Azure’s cloud costs. While the development of the tool was a technical endeavor, it posed a significant challenge in one particular area: authentication.]]></summary></entry><entry><title type="html">From Full Stack to Full Circle: Embracing the Cyclical Nature of Software Development</title><link href="https://mindbyte.nl/2023/08/20/full-stack-full-circle-embracing-cyclical-nature-software-development.html" rel="alternate" type="text/html" title="From Full Stack to Full Circle: Embracing the Cyclical Nature of Software Development" /><published>2023-08-20T00:00:00+02:00</published><updated>2023-08-20T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/20/full-stack-full-circle-embracing-cyclical-nature-software-development</id><content type="html" xml:base="https://mindbyte.nl/2023/08/20/full-stack-full-circle-embracing-cyclical-nature-software-development.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>In a recent conversation with a senior colleague, I jokingly referred to myself not as a full stack developer, but as a “full circle developer”. While it started as a humorous remark, the more I thought about it, the more the concept resonated with me. In the fast-evolving world of technology, we often find ourselves coming full circle, revisiting the same technologies and challenges we thought we had left behind.</p>

<h2 id="the-reality-of-being-a-full-stack-developer">The Reality of Being a Full Stack Developer</h2>

<p>The term “full stack developer” implies a mastery of everything from front-end interfaces to back-end infrastructure, databases, cloud services, and beyond. It’s an attractive idea but often an unrealistic one. Our field has become so complex that mastering every aspect is virtually impossible. And yet, there’s an underlying continuity to the work we do, a cyclical nature that reveals itself in surprising ways.</p>

<h2 id="full-circle-back-to-basics">Full Circle: Back to Basics</h2>

<p>Currently, I find myself working on a project that has brought me back to where I started 20 years ago. I’m working with an IIS server, installing Windows Services, and communicating with MS SQL server — things I hoped I wouldn’t be doing anymore with the advent of cloud technology.</p>

<p>Despite all the advances in our field, the cutting-edge tools, and the cloud-first approach that dominates today’s landscape, some things remain surprisingly constant. The technologies I thought were relics of the past are still very much alive, still solving problems, still serving a purpose.</p>

<h2 id="the-full-circle-developer-embracing-the-past-present-and-future">The Full Circle Developer: Embracing the Past, Present, and Future</h2>

<p>As a Full Circle Developer, I’ve learned to embrace this cyclical nature. I value not only the latest trends and tools but also the lessons learned from decades of software development history. I understand that trends come and go, but underlying principles endure. I see the connections between past and present and draw on that broad perspective to solve problems and innovate.</p>

<p>This approach is not about clinging to the past or rejecting the new. It’s about recognizing that the old and the new are often two sides of the same coin. It’s about being adaptable, forward-looking, and yet grounded in the enduring truths of our craft.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The concept of the Full Circle Developer offers a balanced and sustainable approach to a career in software development. It acknowledges the complexity and diversity of our field while emphasizing the timeless principles that make us effective, regardless of the current trends.</p>

<p>So as I find myself working with technologies I used 20 years ago, I don’t see it as a step back but as a reminder of the continuity and resilience of our field. The tools may change, but the craft remains the same.</p>

<p>What about you? Do you see yourself as a Full Stack Developer, a Full Circle Developer, or something else entirely? How has your perspective on your career evolved over time? I’d love to hear your thoughts.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="Coding" /><summary type="html"><![CDATA[In a recent conversation with a senior colleague, I jokingly referred to myself not as a full stack developer, but as a "full circle developer". While it started as a humorous remark, the more I thought about it, the more the concept resonated with me. In the fast-evolving world of technology, we often find ourselves coming full circle, revisiting the same technologies and challenges we thought we had left behind.]]></summary></entry><entry><title type="html">Direct download from Azure Blob storage using Content-Disposition header to set the filename</title><link href="https://mindbyte.nl/2023/08/12/direct-download-from-azure-blob-storage-using-contentdisposition-header-to-set-the-filename.html" rel="alternate" type="text/html" title="Direct download from Azure Blob storage using Content-Disposition header to set the filename" /><published>2023-08-12T00:00:00+02:00</published><updated>2023-08-12T00:00:00+02:00</updated><id>https://mindbyte.nl/2023/08/12/direct-download-from-azure-blob-storage-using-contentdisposition-header-to-set-the-filename</id><content type="html" xml:base="https://mindbyte.nl/2023/08/12/direct-download-from-azure-blob-storage-using-contentdisposition-header-to-set-the-filename.html"><![CDATA[<p>For an application I m building, I store attachments on Azure in blob storage. This service is a relatively cheap and scalable solution for files that need to be retrieved again by others.</p>

<p>I have set the container to be a public one, and by generating random file names, I have some obfuscation so that iterating over them becomes complicated, but the URL is still sharable. These GUIDs also allow me to avoid name collisions as it is user input where I have no saying in what they upload.</p>

<p>But when you now want to retrieve the file, you get that random file name and not the original one it was uploaded with.</p>

<p>I can download the file to my server first and then return it with a correct name, but this will bring the additional IO to my system. I rather have people directly hit the Azure infrastructure.</p>

<p>Luckily you can set the <code class="language-plaintext highlighter-rouge">content-disposition</code> header. This header tells the client what to do with the file, like showing it inline or downloading it, and what the filename needs to be. This property is in there for some years, so you can set this directly using the SDK when you create a file:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">var</span> <span class="n">blobName</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">cId</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">rId</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">():</span><span class="n">N</span><span class="p">}</span><span class="s">.</span><span class="p">{</span><span class="n">extension</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
 
 <span class="n">BlobClient</span> <span class="n">blobClient</span> <span class="p">=</span> <span class="n">containerClient</span><span class="p">.</span><span class="nf">GetBlobClient</span><span class="p">(</span><span class="n">blobName</span><span class="p">);</span>

 <span class="c1">// Upload a byte[] to a blob</span>
 <span class="k">await</span> <span class="n">blobClient</span><span class="p">.</span><span class="nf">UploadAsync</span><span class="p">(</span>
 <span class="k">new</span> <span class="nf">MemoryStream</span><span class="p">(</span><span class="n">attachmentContent</span><span class="p">),</span>
 <span class="k">new</span> <span class="n">BlobHttpHeaders</span>
 <span class="p">{</span>
 <span class="n">ContentType</span> <span class="p">=</span> <span class="n">attachmentType</span><span class="p">,</span>
 <span class="n">ContentDisposition</span> <span class="p">=</span> <span class="s">$"attachment; filename=\"</span><span class="p">{</span><span class="n">attachmentName</span><span class="p">}</span><span class="s">\""</span>
 <span class="p">});</span>

 <span class="kt">string</span> <span class="n">downloadUrl</span> <span class="p">=</span> <span class="n">blobClient</span><span class="p">.</span><span class="n">Uri</span><span class="p">.</span><span class="n">AbsoluteUri</span><span class="p">;</span>

</code></pre></div></div>

<p>However, when I now used the public URL, I still got the long GUID-like name back, and the <code class="language-plaintext highlighter-rouge">content-disposition</code> header was missing. It appears that you need to specify the <code class="language-plaintext highlighter-rouge">x-ms-version: 2019-12-12</code> header as well. This is a bit annoying to set on a GET request in a browser as you cannot specify any headers, so we need to force the storage account to always use this version.</p>

<p>There is no easy dropdown in the portal, and as it is a once-off action, you can use the Cloud Shell (with Powershell) to do this:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ctx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-AzureStorageContext</span><span class="w"> </span><span class="nt">-StorageAccountName</span><span class="w"> </span><span class="nx">name-of-storageaccount</span><span class="w"> </span><span class="nt">-StorageAccountKey</span><span class="w"> </span><span class="nx">your-access-key</span><span class="w">
</span><span class="n">Update-AzureStorageServiceProperty</span><span class="w"> </span><span class="nt">-ServiceType</span><span class="w"> </span><span class="nx">Blob</span><span class="w"> </span><span class="nt">-DefaultServiceVersion</span><span class="w"> </span><span class="nx">2019-12-12</span><span class="w"> </span><span class="nt">-Context</span><span class="w"> </span><span class="nv">$ctx</span><span class="w">
</span></code></pre></div></div>

<p>When you now fetch the file using the public url, you get the header included and as such the correct filename. But it took me some while to figure out that this is not the default behaviour of the storage account, so make sure to check the <code class="language-plaintext highlighter-rouge">DefaultServiceVersion</code>.</p>]]></content><author><name>Michiel van Oudheusden</name><uri>https://mindbyte.nl</uri></author><category term="Azure" /><summary type="html"><![CDATA[For an application I m building, I store attachments on Azure in blob storage. This service is a relatively cheap and scalable solution for files that need to be retrieved again by others.]]></summary></entry></feed>