<?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://marcusfelling.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://marcusfelling.com/" rel="alternate" type="text/html" /><updated>2026-07-17T13:14:09-04:00</updated><id>https://marcusfelling.com/feed.xml</id><title type="html">Home</title><subtitle>A blog about things I learn at the keyboard: DevOps, CI/CD, Cloud, Automation, to name a few...</subtitle><author><name>Marcus Felling</name></author><entry><title type="html">An AI Analyst That Shows Its Work</title><link href="https://marcusfelling.com/blog/2026/ai-analyst-that-shows-its-work" rel="alternate" type="text/html" title="An AI Analyst That Shows Its Work" /><published>2026-07-07T00:00:00-04:00</published><updated>2026-07-07T00:00:00-04:00</updated><id>https://marcusfelling.com/blog/2026/vibe-analytics-should-scare-you</id><content type="html" xml:base="https://marcusfelling.com/blog/2026/ai-analyst-that-shows-its-work"><![CDATA[<p>Ask an AI tool a data question and you can get an answer that sounds finished while using the wrong measure, skipping assumed filters, or querying the wrong source. <a href="https://mitsloan.mit.edu/ideas-made-to-matter/working-definitions/what-is-vibe-analytics">Vibe analytics</a> means taking that answer at face value. The name borrows from <a href="https://en.wikipedia.org/wiki/Vibe_coding">vibe coding</a>, Andrej Karpathy’s term for letting AI write code and accepting it without reading the diffs.</p>

<p>I built an AI analyst to keep the speed and expose the work. For a focused question, it finds the governed semantic model that owns the answer. For a broader briefing, it can pull evidence together across the data landscape. The analyst runs queries under the user’s identity and includes its sources, periods, and filters with each answer. Type <code class="language-plaintext highlighter-rouge">show dax</code> to see the query. The LLM handles the conversation; institutional knowledge defines what a correct answer requires.</p>

<p>I started this as a side project around three months ago. People across our org use it, and I want to explain how it works.</p>

<p>I work at Microsoft on an analytics team supporting a large ecommerce business. We have plenty of data. Finding where it lives, stitching it together across reports and one-off spreadsheets, and crafting a story can be a pain in the ass.</p>

<p>The AI analyst complements reports and human analysts. It makes the governed semantic models built over our Microsoft Fabric gold layer easier to use, cuts the manual stitching, captures the institutional knowledge that makes a number trustworthy, and gives us more time to make decisions.</p>

<h2 id="using-it">Using it</h2>

<p>I use it for three sizes of question in one place.</p>

<p><strong>Small: a single metric.</strong> “What was yesterday’s value for Metric A?” It picks the right semantic model and hands you the number.</p>

<p><strong>Medium: a health check.</strong> One command gets you a cross-source briefing across a set of business areas, with each line graded against a threshold. It answers “Is anything on fire right now?” on one screen.</p>

<p><strong>Large: an executive readout.</strong> One prompt pulls data across each relevant model, grades each area against a performance framework, and writes a decision brief: what happened, why it matters, and what to do about it, ending in a ranked action list with owners.</p>

<p>The executive readout saves me the most time. Before the agent, putting one together meant opening a stack of reports, lining up the numbers, writing the narrative, and formatting it for leadership. The agent can do that in a couple of minutes and attach the sources. I can inspect the query behind any answer and verify its filters and measures.</p>

<p>Ask a plain-English question and the answer includes its receipt: the source, filters, and period used.</p>

<blockquote>
  <p><strong>you:</strong> what was the value of Metric A in Region A last quarter?</p>

  <p><strong>analyst:</strong> Metric A in Region A was <strong>X.X units</strong>.
<em>Source: semantic model A | Filters: Region = Region A, Scenario = Actuals | Period: Previous quarter</em></p>
</blockquote>

<p>Type <code class="language-plaintext highlighter-rouge">show dax</code> and it returns the query it ran. I replaced the real table and measure names in this example:</p>

<pre><code class="language-dax">EVALUATE
ROW( "Metric A",
    CALCULATE(
        [Metric A],
        'Geography'[Region] = "Region A",
        'Calendar'[Relative Quarter] = "Previous Quarter"
    )
)
</code></pre>

<h2 id="how-it-works">How it works</h2>

<p>The LLM handles the conversation. Routing, model guidance, and query execution are what make the answers useful.</p>

<p>I support three hosts: GitHub Copilot, Microsoft 365 Copilot Cowork, and Microsoft Scout. I started with a <a href="https://code.visualstudio.com/docs/agent-customization/custom-agents">GitHub Copilot custom agent</a> wired up with several <a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">MCP servers</a>. <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a>, the Model Context Protocol, is the open standard that lets an agent talk to external tools in a consistent way.</p>

<h3 id="the-mcp-servers">The MCP servers</h3>

<ul>
  <li>The core is <a href="https://learn.microsoft.com/en-us/power-bi/developer/mcp/remote-mcp-server-get-started">Power BI’s remote MCP server</a> (in preview). It separates two steps: generating a <a href="https://learn.microsoft.com/en-us/dax/">DAX</a> query from a plain-English question and executing it against a <a href="https://learn.microsoft.com/en-us/power-bi/connect-data/service-datasets-understand">semantic model</a> under your identity. For a one-off question, the server generates a query within guardrails supplied by the model guidance: the right measures and required filters. For bigger briefings, the agent runs prewritten DAX from those files.</li>
  <li>Other tools extend the workflow:
    <ul>
      <li>The <a href="https://github.com/microsoft/azure-devops-mcp">Azure DevOps MCP Server</a> files a bug when something looks broken.</li>
      <li><a href="https://webiq.microsoft.ai/">Web IQ</a> fetches external market intelligence.</li>
      <li><a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/work-iq/">Work IQ</a> brings in work context from Microsoft 365, such as files, meetings, mail, and chats. It runs under your identity and reaches content you can access.</li>
    </ul>
  </li>
</ul>

<h3 id="the-governed-model">The governed model</h3>

<p>I rely on governed semantic models for trustworthy answers. We encode certified business logic and agreed definitions for core business measures in each model. Row-level security limits each user to the data they can access.</p>

<p>In a medallion architecture, data engineers store raw data in bronze tables, clean and conform it in silver, then shape it into business-ready gold tables. My team’s semantic models use that Microsoft Fabric gold layer. I built a thin natural-language layer that queries those models in place.</p>

<p>The analyst follows this path for each request:</p>

<blockquote>
  <p><strong>Question</strong> <span aria-hidden="true">→</span> <strong>agent routing</strong> <span aria-hidden="true">→</span> <strong>registry and model guidance</strong> <span aria-hidden="true">→</span> <strong>governed semantic model</strong> <span aria-hidden="true">→</span> <strong>query result and source metadata</strong> <span aria-hidden="true">→</span> <strong>answer or briefing</strong></p>
</blockquote>

<h3 id="the-main-components">The main components</h3>

<p><strong>The agent file.</strong> Defines routing, workflow, and every mode and command.</p>

<p><strong>A model registry.</strong> Maps each report to its exact workspace and semantic-model IDs. After the agent chooses a model, the registry resolves the target up front, so the agent does not wander around discovering what exists at runtime.</p>

<p><strong>Per-model guidance.</strong> One prompt file per report, in plain Markdown, encodes the institutional knowledge: which filters must always be present, how measure names differ across tables, which dimension value looks like a total but is not, and which measures must never feed a narrative. These files carry the accumulated product knowledge; the LLM provides a flexible interface.</p>

<p>The exact rules vary by model, but a sanitized sketch of the shape looks like this:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## Required filters</span>
<span class="p">-</span> Scope: required on every query
<span class="p">-</span> Scenario: Actuals

<span class="gu">## Measure guidance</span>
<span class="p">-</span> Metric A: use the certified measure
<span class="p">-</span> Never narrate from measures listed as unsafe
</code></pre></div></div>

<p><strong>The execution layer.</strong> The Power BI remote MCP server runs the query in GitHub Copilot; <a href="https://learn.microsoft.com/en-us/fabric/iq/overview">Fabric IQ</a> runs it in Cowork.</p>

<h2 id="one-brain-three-front-doors">One brain, three front doors</h2>

<p>I shipped it first as the GitHub Copilot custom agent. The first practical question was, “Great, how do I use this without opening an IDE?”</p>

<p>It has three front doors:</p>

<ul>
  <li>The <a href="https://code.visualstudio.com/docs/agent-customization/custom-agents">GitHub Copilot custom agent</a> in VS Code, with the full set of MCP servers.</li>
  <li>A plugin for <a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/">Microsoft 365 Copilot Cowork</a>, which uses <a href="https://learn.microsoft.com/en-us/fabric/iq/overview">Fabric IQ</a> for Power BI access.</li>
  <li>A plugin for <a href="https://learn.microsoft.com/en-us/microsoft-scout/overview">Microsoft Scout</a>, an OpenClaw-like desktop app for Windows and macOS that acts across your files, shell, browser, and Microsoft 365. Scout supports a compatible agent-and-plugin shape built on the GitHub Copilot CLI, so I ported the Markdown files with few changes; the plugin adds the MCP servers the host does not ship.</li>
</ul>

<p>The agent file and per-report prompt files form one canonical spec. A script generates the Scout desktop plugin from it by rewriting the front matter, re-anchoring file paths, and transforming the MCP configuration for that host. The Cowork edition reuses the same prompt files. These hosts are converging on the same basic shape: MCP for tools plus similar agent and skill formats. I can keep the host-specific glue small.</p>

<h2 id="where-it-is-going">Where it is going</h2>

<p>Next, I plan to host it as a <a href="https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/hosted-agents">Foundry hosted agent</a>. Foundry Agent Service can run a packaged agent on managed infrastructure that handles scaling and session state. I could publish a hosted version to Microsoft Teams and Microsoft 365 Copilot Chat, where people already work.</p>

<p>I also plan to add data sources and connect the analyst to daily workflows.</p>

<h2 id="stuff-i-learned">Stuff I learned</h2>

<p><strong>Institutional knowledge took most of the work.</strong> A bare LLM can return a plausible but wrong number. I spent most of the effort writing down or referencing the institutional knowledge that makes an answer correct.</p>

<p><strong>“Show me the query” makes review concrete.</strong> People do not trust a black box, and they are right not to. Letting someone inspect the filters and measures does more for trust than a claim about accuracy can.</p>

<p><strong>Static checks protect the contract today.</strong> For relevant changes, CI checks that golden questions route to the right model, model files keep their required filters, known-bad measures stay quarantined, and safety sections remain intact. The checks need no credentials or live queries and run in seconds. They catch regressions before the analyst starts returning garbage.</p>

<p><strong>End-to-end trace evals are next.</strong> The next tier will record real runs, check generated DAX for required filters, and trace each narrative number to a query result. I need unattended agent runs to build those evals, another reason to host it on Foundry.</p>

<p><strong>Adding another supported semantic model requires configuration.</strong> It needs a registry entry, per-model guidance, and routing and eval cases. I wrote a <code class="language-plaintext highlighter-rouge">CONTRIBUTING.md</code> with step-by-step instructions and practices that make the process repeatable.</p>

<h2 id="wrapping-up">Wrapping up</h2>

<p>This is how I want data questions to work: ask in plain English, pull the answer from the right governed models, and keep the evidence attached. Each answer keeps its source, filters, and query attached for review.</p>

<p>If you are building something similar, I would love to compare notes on how you encode business rules and keep the work reviewable.</p>]]></content><author><name>Marcus Felling</name></author><category term="AI" /><category term="Power BI" /><category term="Fabric" /><category term="GitHub Copilot" /><summary type="html"><![CDATA[A natural-language analyst that connects evidence across the data landscape, routes questions to governed models, applies the required filters, runs queries under your identity, and shows its work. The LLM handles the conversation; governed models and model-specific guidance determine how each question is answered.]]></summary></entry><entry><title type="html">I Automated Meeting Action Items Into Azure DevOps Using GitHub Copilot and Work IQ MCP</title><link href="https://marcusfelling.com/blog/2026/automating-meeting-action-items-into-azure-devops-with-github-copilot-and-work-iq-mcp" rel="alternate" type="text/html" title="I Automated Meeting Action Items Into Azure DevOps Using GitHub Copilot and Work IQ MCP" /><published>2026-03-24T00:00:00-04:00</published><updated>2026-03-24T00:00:00-04:00</updated><id>https://marcusfelling.com/blog/2026/automating-meeting-action-items-into-azure-devops-with-mcp-and-copilot</id><content type="html" xml:base="https://marcusfelling.com/blog/2026/automating-meeting-action-items-into-azure-devops-with-github-copilot-and-work-iq-mcp"><![CDATA[<p>My meetings produce action items, and many belong in Azure DevOps. Between a meeting ending and me opening ADO to create them, there’s a gap where my motivation goes to die.</p>

<p>I wired together GitHub Copilot, Work IQ, and Azure DevOps to pull meeting transcripts, extract action items, and draft work items for my review. I approve each draft before GitHub Copilot creates or changes a work item.</p>

<hr />

<p><strong>TL;DR</strong>: I connected Work IQ MCP (reads Microsoft 365 meeting data) and Azure DevOps MCP (creates work items) to GitHub Copilot in VS Code. I record most of my meetings, so GitHub Copilot reads the transcript, extracts action items, and drafts ADO work items for me to review. I sign off before GitHub Copilot creates or changes anything.</p>

<hr />

<h2 id="the-toolchain">The Toolchain</h2>

<p>The workflow combines four pieces:</p>

<ol>
  <li><strong><a href="https://code.visualstudio.com/docs/copilot/overview">GitHub Copilot</a></strong> in VS Code: the orchestration layer. Runs the prompt, calls the MCP tools, handles the back-and-forth.</li>
  <li><strong><a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/workiq-overview">Work IQ MCP</a></strong>: reads my Microsoft 365 data (meetings, emails, Teams messages). This is where meeting context comes from.</li>
  <li><strong><a href="https://github.com/microsoft/azure-devops-mcp">Azure DevOps MCP Server</a></strong>: creates work items, sets fields, assigns owners.</li>
  <li><strong><a href="https://code.visualstudio.com/docs/copilot/customization/custom-instructions">Custom instructions</a> and <a href="https://code.visualstudio.com/docs/copilot/customization/prompt-files">prompt files</a></strong>: the glue that tells GitHub Copilot what to extract and how to structure the output.</li>
</ol>

<h2 id="work-iq-reading-meeting-data">Work IQ: Reading Meeting Data</h2>

<p><a href="https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/workiq-overview">Work IQ</a> is a Microsoft-built, first-party MCP server that exposes your Microsoft 365 work data as MCP tools: mail, calendar, Teams, files, and people.</p>

<p>Work IQ takes a natural-language question about your M365 data, translates it into Microsoft Graph requests, and returns structured data. It uses user-delegated permissions and accesses content available to you.</p>

<p>One MCP server handles all M365 domains. You query it the way you’d ask a person: “what happened in my last meeting with the platform team?” and it figures out the right Graph calls to make. Work IQ remains in public preview, so the specific tools and APIs may shift.</p>

<p>I use meeting transcripts for this project. I record most of my meetings, so the transcripts sit in M365. Work IQ pulls the full transcript, giving GitHub Copilot context about who said what, what people agreed to, and which items remained unresolved. I get enough detail to act on a draft three weeks later.</p>

<h2 id="azure-devops-mcp-creating-real-work-items">Azure DevOps MCP: Creating Real Work Items</h2>

<p>The <a href="https://github.com/microsoft/azure-devops-mcp">Azure DevOps MCP server</a> lets GitHub Copilot create work items and set fields such as title, description, assigned to, area path, iteration, and priority. It writes the items to the backlog, so I do not have to paste JSON somewhere and tidy it up by hand.</p>

<h2 id="how-it-works">How It Works</h2>

<p>I use this six-step flow:</p>

<ol>
  <li>I record most meetings, which makes their transcripts available in M365.</li>
  <li>I ask GitHub Copilot to pull the meeting transcript via Work IQ MCP.</li>
  <li>GitHub Copilot extracts the action items: who’s responsible, what needs to happen, any deadlines mentioned.</li>
  <li>GitHub Copilot drafts the ADO work items and presents them to me for review.</li>
  <li>I review the draft, edit titles and descriptions, and remove anything that should not be there.</li>
  <li>After I sign off, GitHub Copilot calls the ADO MCP server to create the work items.</li>
</ol>

<p>I don’t let GitHub Copilot create work items unsupervised. Meetings are messy: speculative ideas, tangents, and the time-honored move of someone volunteering a colleague for work that colleague hasn’t heard about yet. I was in the room, so I can separate real decisions from speculation during review.</p>

<p>Two details reduce cleanup:</p>

<p><strong>Reference your ADO org and project in your instructions file.</strong> Add your default organization URL and project name to your custom instructions or <code class="language-plaintext highlighter-rouge">.github/copilot-instructions.md</code>. GitHub Copilot can then target the right project without another round trip.</p>

<p><strong>Point to a parent work item.</strong> I include the Epic I’m working under in my prompt. Copilot can then create child Features, User Stories, or Tasks beneath it. Otherwise, Copilot may create orphaned work items that I have to reparent. If your team structures work under Epics or Features, give GitHub Copilot that context up front.</p>

<p>My prompt looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Read the transcript from my most recent meeting with 
the platform team. Extract action items and draft 
Azure DevOps work items in the Platform project under 
Epic #4521. Show me the draft before creating anything.
</code></pre></div></div>

<p>GitHub Copilot makes the Work IQ tool call, gets the transcript back, parses out action items, and shows me a structured draft. I make edits, say “looks good,” and GitHub Copilot makes the ADO MCP calls. You can watch the tool calls in GitHub Copilot chat.</p>

<p>GitHub Copilot orchestrates two separate MCP servers in one conversation: it reads from one system, waits for my approval, and writes to another.</p>

<h2 id="choosing-github-copilot-over-copilot-cowork">Choosing GitHub Copilot Over Copilot Cowork</h2>

<p>Microsoft has <a href="https://www.microsoft.com/en-us/microsoft-365/blog/2026/03/09/copilot-cowork-a-new-way-of-getting-work-done/">Copilot Cowork</a>, built on <a href="https://www.anthropic.com/product/claude-cowork">Claude Cowork</a>, an enterprise work orchestration agent that can <a href="https://www.microsoft.com/en-us/microsoft-365/blog/2026/03/09/powering-frontier-transformation-with-copilot-and-agents/">plan multi-step tasks across M365</a> across calendar, Teams, Excel, and email with little setup. It also supports <a href="https://claude.com/skills">skills</a>, <a href="https://claude.com/plugins">plugins</a>, and <a href="https://claude.com/connectors">MCP connectors</a>. For one-off knowledge work across M365, it works well.</p>

<p>I chose GitHub Copilot + MCP because it lives where my dev tools are.</p>

<p><strong>Everything is in the repo.</strong> My custom instructions, prompt files, and skill definitions are files that live in version control. I can diff them, review them in PRs, and share them across the team through normal git workflows. Cowork’s skills and plugins exist in their own ecosystem outside of source control.</p>

<p><strong>Built for developer workflows.</strong> Cowork is designed for knowledge workers doing non-technical tasks across M365, and it’s good at that. GitHub Copilot + MCP is designed for people who want to wire systems together through code, inspect tool calls, and iterate on prompts the way they iterate on code.</p>

<p>Cowork handles M365 orchestration with built-in guardrails. GitHub Copilot + MCP gives you a developer-native platform where the customization lives in your repo and the workflow runs in your IDE. For repeatable workflows that need to reach into ADO and stay under version control, I’ll take GitHub Copilot + MCP.</p>

<h2 id="current-results">Current Results</h2>

<p>I’ve run the setup against a handful of real meetings. The drafts land around 80-90% right: I fix a title, delete an item that was never an action item, and tweak an assignment or two. The review step keeps me in control.</p>

<p>I keep the prompt file in version control and tune the instructions for my ADO setup. The workflow takes about 30 seconds of GitHub Copilot chat plus a minute of review. I used to spend 15 minutes copying and pasting, or zero minutes when I forgot the work.</p>

<h2 id="packaging-it-for-the-team">Packaging It for the Team</h2>

<p>The workflow lives in my prompt files and custom instructions. To share it with the team, I need to package it as a reusable Copilot customization.</p>

<p>GitHub Copilot offers three mechanisms for this: <a href="https://code.visualstudio.com/docs/copilot/customization/custom-agents">custom agents</a> (<code class="language-plaintext highlighter-rouge">.agent.md</code>), <a href="https://code.visualstudio.com/docs/copilot/customization/prompt-files">prompt files</a> (<code class="language-plaintext highlighter-rouge">.prompt.md</code>), and <a href="https://code.visualstudio.com/docs/copilot/customization/agent-skills">skills</a> (<code class="language-plaintext highlighter-rouge">SKILL.md</code>). All three live in the repo, use git for versioning, and give the team a single file to iterate on. Their differences matter for a workflow that creates work items in an external system.</p>

<p>I plan to use a <a href="https://code.visualstudio.com/docs/copilot/customization/custom-agents">custom agent</a> because it supports tool restriction. An <code class="language-plaintext highlighter-rouge">.agent.md</code> file can restrict Copilot’s tool set to declarations in frontmatter. When you invoke <code class="language-plaintext highlighter-rouge">@meeting-actions</code>, Copilot uses the Work IQ and ADO MCP tools without access to unrelated codebase tools. For a workflow that writes to a real system, scoped tool access matters from day one.</p>

<p>Agents also let you include custom instructions, model preferences, and the human review step in the agent definition. The team invokes it with <code class="language-plaintext highlighter-rouge">@meeting-actions</code>. One file contains the extraction logic, field mappings, definition of an action item, and the “show me the draft before creating anything” gate.</p>

<p>A prompt file (<code class="language-plaintext highlighter-rouge">.prompt.md</code>) is the lighter alternative. It can declare MCP tool dependencies in frontmatter, and you invoke it with <code class="language-plaintext highlighter-rouge">/meeting-work-items</code>. Prompt files leave Copilot’s other tools available. If you want to prototype the instructions first and don’t need tool scoping yet, a prompt file has less conceptual overhead. Moving from a prompt file to an agent costs little because the instructions body uses the same Markdown.</p>

<p>I considered skills first, but they fit this workflow least well. Copilot auto-discovers a <code class="language-plaintext highlighter-rouge">SKILL.md</code> when it matches your request, which helps with guidance and reusable patterns. Auto-discovery adds risk when the result writes work items to ADO. You can disable it with <code class="language-plaintext highlighter-rouge">disable-model-invocation: true</code> in the frontmatter. Doing so turns a skill into a slash command without the tool restriction or model preference support that agents and prompt files offer. Skills also can’t declare tool dependencies in machine-readable frontmatter. They reference tools in body text.</p>]]></content><author><name>Marcus Felling</name></author><category term="AI" /><category term="Azure DevOps" /><summary type="html"><![CDATA[How I wired together GitHub Copilot, Work IQ MCP, and Azure DevOps MCP to turn meeting transcripts into draft work items I review and approve.]]></summary></entry><entry><title type="html">Building an AI Agent Squad for Your Repo</title><link href="https://marcusfelling.com/blog/2026/building-an-ai-agent-squad-for-your-repo" rel="alternate" type="text/html" title="Building an AI Agent Squad for Your Repo" /><published>2026-02-26T00:00:00-05:00</published><updated>2026-02-26T00:00:00-05:00</updated><id>https://marcusfelling.com/blog/2026/building-an-ai-agent-squad-for-your-repo</id><content type="html" xml:base="https://marcusfelling.com/blog/2026/building-an-ai-agent-squad-for-your-repo"><![CDATA[<p><a href="https://github.com/bradygaster/squad">Squad</a> gives your repo a team of AI agents: a lead, a frontend dev, a tester, and a content writer, each with a separate context window, persistent memory, and defined boundaries.</p>

<p><a href="https://github.com/bradygaster">Brady Gaster</a> created the open-source framework to build AI development teams through GitHub Copilot. You describe what you’re building, and Squad proposes a team of specialists that live in your repo as files. They persist across sessions, learn your codebase, share decisions, and improve as you use them.</p>

<p>Each team member runs in its own context, reads its own knowledge, and writes back what it learned.</p>

<p>I set it up for the repo behind this blog.</p>

<h2 id="installing-squad">Installing Squad</h2>

<p>Getting started takes two commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-g</span> @bradygaster/squad-cli
squad
</code></pre></div></div>

<p>For additional installation options (including npx and cloning from source), see the <a href="https://bradygaster.github.io/squad/docs/get-started/installation/">official installation guide</a>.</p>

<p>Then open Copilot in VS Code, type <code class="language-plaintext highlighter-rouge">@squad</code>, and tell it what you’re building:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I'm starting a new project. Set up the team.
</code></pre></div></div>

<p>Squad proposes a team with names from a persistent thematic cast. Once you approve it, the team is ready.</p>

<h2 id="the-team">The Team</h2>

<p>Squad generated a team tailored to my blog’s needs:</p>

<ul>
  <li>🏗️ <strong>Mal</strong> / Lead: architecture, code review, cross-agent coordination</li>
  <li>⚛️ <strong>Kaylee</strong> / Designer/Dev: CSS, layout, responsive design</li>
  <li>📝 <strong>Wash</strong> / Content Dev: blog posts, frontmatter, SEO</li>
  <li>🧪 <strong>Zoe</strong> / Tester: Playwright tests, accessibility, performance</li>
  <li>📋 <strong>Scribe</strong> / Session logger: decisions, session summaries</li>
  <li>🔄 <strong>Ralph</strong> / Work monitor: backlog, issue triage, CI monitoring</li>
</ul>

<p>The casting system keeps each name after assignment, so a teammate who clones the repo gets the same team and cast.</p>

<p>Each agent has a <strong>charter</strong> (<code class="language-plaintext highlighter-rouge">charter.md</code>) that defines scope and boundaries: what they own, what files they can modify, and what they <em>don’t</em> touch. Kaylee owns CSS but never writes tests. Wash owns blog posts but never touches layout. These boundaries prevent agents from stepping on each other.</p>

<h2 id="what-gets-created">What Gets Created</h2>

<p>Everything lives in a <code class="language-plaintext highlighter-rouge">.squad/</code> directory:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.squad/
├── team.md            # Roster
├── routing.md         # Who handles what
├── decisions.md       # Shared brain
├── ceremonies.md      # Design reviews, retros
├── casting/
│   ├── policy.json
│   ├── registry.json
│   └── history.json
├── agents/
│   ├── kaylee/
│   │   ├── charter.md # Identity, expertise, voice
│   │   └── history.md # Project-specific learnings
│   ├── wash/
│   │   ├── charter.md
│   │   └── history.md
│   └── zoe/
│       ├── charter.md
│       └── history.md
└── log/               # Session history
</code></pre></div></div>

<p>Commit this folder, and the team’s names and knowledge persist in git.</p>

<h2 id="parallel-agents-not-sequential">Parallel Agents, Not Sequential</h2>

<p>I did not expect Squad to run work in parallel. When you give it a task, the coordinator launches all relevant agents at once:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You: "Team, redesign the blog"
  🏗️ Mal    → analyzing architecture requirements
  ⚛️ Kaylee → building new layout             (all launched
  🧪 Zoe    → writing test cases from spec     in parallel)
  📋 Scribe → logging everything
</code></pre></div></div>

<p>As agents finish, the coordinator chains follow-up work. A test can expose an edge case, and another agent can pick it up without waiting for you to ask.</p>

<p>Each agent gets its own context window. With Claude Sonnet 4 or Claude Opus 4’s 200K token window, a lightweight coordinator leaves each agent ~78–83% of its context for project work. Fan out to 5 agents and you’re working with ~1M tokens of total reasoning capacity.</p>

<h2 id="knowledge-that-compounds">Knowledge That Compounds</h2>

<p>Every time an agent works, it writes lasting learnings to its <code class="language-plaintext highlighter-rouge">history.md</code>. After a few sessions, agents know your conventions, your preferences, your architecture. They stop asking questions they’ve already answered.</p>

<p>Team-wide decisions live in <code class="language-plaintext highlighter-rouge">decisions.md</code>, which every agent reads before working. Personal knowledge stays in each agent’s <code class="language-plaintext highlighter-rouge">history.md</code>. The Scribe keeps session logs searchable in <code class="language-plaintext highlighter-rouge">log/</code>.</p>

<p><strong>Frontend agent knowledge over time:</strong>
Stack, framework → Components, routing → Design system, a11y conventions</p>

<p><strong>Lead agent knowledge over time:</strong>
Scope, roster → Trade-offs, risks → Full project history, tech debt map</p>

<p><strong>Tester agent knowledge over time:</strong>
Framework, first cases → Edge case catalog → Regression patterns, coverage gaps</p>

<h2 id="issue-integration">Issue Integration</h2>

<p>Squad ties into GitHub Issues with a labeling workflow:</p>

<ol>
  <li>Label an issue <code class="language-plaintext highlighter-rouge">squad</code>. The Lead triages it, determines who should handle it, and applies the right <code class="language-plaintext highlighter-rouge">squad:{member}</code> label.</li>
  <li>The assigned member picks up the issue in their next Copilot session. Copilot coding agent can pick it up sooner when enabled.</li>
  <li>The <code class="language-plaintext highlighter-rouge">sync-squad-labels</code> workflow syncs labels from your team roster.</li>
</ol>

<h2 id="lessons-from-using-it">Lessons From Using It</h2>

<p><strong>The first session is the least capable.</strong> Knowledge compounds. By the third or fourth session, agents were making decisions based on prior context without me having to repeat anything.</p>

<p><strong>Clear charters need explicit exclusions.</strong> Define what each agent owns and what it cannot touch. That separation keeps agents from duplicating or conflicting with one another.</p>

<p><strong>The Scribe does the most valuable work for me.</strong> Its searchable log of decisions and sessions saves me from reconstructing what past-me was thinking the next day.</p>

<p>If you’re using GitHub Copilot for your repo today, give Squad a try. It splits work across separate contexts while keeping the team configuration in git.</p>]]></content><author><name>Marcus Felling</name></author><category term="AI" /><summary type="html"><![CDATA[How I set up Squad, an open-source AI agent team framework, and what I learned along the way]]></summary></entry><entry><title type="html">Shipping an AI Agent MVP: What Worked</title><link href="https://marcusfelling.com/blog/2026/three-day-hackathon-shipping-ai-agent-mvps" rel="alternate" type="text/html" title="Shipping an AI Agent MVP: What Worked" /><published>2026-01-20T00:00:00-05:00</published><updated>2026-01-20T00:00:00-05:00</updated><id>https://marcusfelling.com/blog/2026/what-three-day-hackathon-taught-me-shipping-ai-agent-mvps-real-stakeholders</id><content type="html" xml:base="https://marcusfelling.com/blog/2026/three-day-hackathon-shipping-ai-agent-mvps"><![CDATA[<p>I led a three-day hackathon to build an AI solution that streamlines monthly business reviews for our Microsoft direct-sales ecommerce store, including Surface devices, Microsoft 365, and Xbox. Our team had spent too much time on high-level planning. The hackathon gave us three days to move from planning to execution.</p>

<p>Our team builds on <a href="https://learn.microsoft.com/en-us/fabric/fundamentals/microsoft-fabric-overview">Microsoft Fabric</a>, a data analytics SaaS platform. Microsoft announced <a href="https://learn.microsoft.com/en-us/fabric/iq/overview">Fabric IQ</a>, including Ontology and Data Agents, before the hackathon, so we tested those capabilities in a real scenario.</p>

<hr />

<h2 id="tldr">TL;DR</h2>

<ul>
  <li><strong>Start with the business problem</strong></li>
  <li><strong>Cut scope to a useful MVP</strong></li>
  <li><strong>Use a simple delivery path for the first test</strong></li>
  <li><strong>Break big agents into smaller</strong>, more focused sub agents</li>
  <li><strong>Engage stakeholders</strong> throughout the process</li>
  <li><strong>Test often</strong> and iterate fast</li>
</ul>

<h2 id="begin-with-a-clear-business-problem">Begin With a Clear Business Problem</h2>

<p>We began by defining the problem. Before the hack, we listed candidate use cases and chose monthly business review preparation: analyzing KPI shifts and writing narratives. The team spends hundreds of hours on this work each month. We used that scope to identify what needed automation and what an AI agent could deliver in three days.</p>

<p>Stakeholders asked us to spot the biggest KPI moves, connect those changes to contextual factors, and provide concise summaries for leadership review. We used those requirements to prioritize KPI analysis and commentary prototypes.</p>

<h2 id="reduce-scope-and-create-a-practical-mvp">Reduce Scope and Create a Practical MVP</h2>

<p>We needed a way for stakeholders to receive the agent’s output. For the MVP, we scheduled insight generation and delivered the result by email. Email put outputs in stakeholders’ hands fast, so they could validate the content before we invested in a polished interface.</p>

<p>We also chose tools we knew. Logic Apps orchestrated the Fabric data agents, passed outputs as variables, and merged them into the final email. Learning Copilot Studio or AI Foundry and setting up their environments would have consumed most of the three-day hackathon.</p>

<h2 id="break-big-agents-into-smaller-more-focused-sub-agents">Break Big Agents Into Smaller, More Focused Sub Agents</h2>

<p>We started with a plan for two big agents: one for KPI analysis and one for writing narratives. As we added data sources, their accuracy turned to slop. We split them into specialized subagents with narrow scopes. We stripped out tables and measures we didn’t need, cleaned up the filtering logic, and set the agents to use narrower data sources. Narrowing the scope this way stabilized the output and improved accuracy. The prototype grew to six specialized agents.</p>

<h3 id="lessons-learned-fabric-data-agents">Lessons Learned: Fabric Data Agents</h3>

<p>Fabric data agents sometimes generated incorrect DAX or referenced tables and columns outside their scope. We responded with four practices:</p>

<blockquote>
  <p><strong>Good Practices for Fabric Data Agents:</strong></p>

  <ul>
    <li>Keep the semantic model and its custom DAX measures clear</li>
    <li>Use the <a href="https://learn.microsoft.com/en-us/power-bi/create-reports/copilot-prepare-data-ai">Prepare your data for AI</a> tool on the published semantic model</li>
    <li>Select the essential measures and tables when creating the agent</li>
    <li>Provide explicit instructions to the agent about when it should perform its own calculations</li>
  </ul>
</blockquote>

<h2 id="engage-stakeholders-throughout-the-process">Engage Stakeholders Throughout the Process</h2>

<p>Stakeholders reviewed the work throughout the hackathon, which shortened the feedback loop. They told us which KPI moves mattered, how they wanted the summaries written, and what context was missing. Their early feedback kept us from building features nobody wanted and let us adjust the prototypes during the hackathon.</p>

<hr />

<h2 id="closing-thoughts">Closing Thoughts</h2>

<p>In three days, we tested AI agents against a real monthly-review workflow. A clear problem and tight scope let us ship a working prototype. Stakeholder feedback guided each adjustment.</p>

<p>The three-day prototype gave us the starting point for the solution we continued to build.</p>]]></content><author><name>Marcus Felling</name></author><category term="AI" /><summary type="html"><![CDATA[Shipping an AI Agent MVP: What Worked]]></summary></entry><entry><title type="html">⏩ Optimizing GitHub Actions Workflows for Speed and Efficiency</title><link href="https://marcusfelling.com/blog/2025/optimizing-github-actions-workflows-for-speed" rel="alternate" type="text/html" title="⏩ Optimizing GitHub Actions Workflows for Speed and Efficiency" /><published>2025-02-28T00:00:00-05:00</published><updated>2025-02-28T00:00:00-05:00</updated><id>https://marcusfelling.com/blog/2025/optimizing-github-actions-workflows-for-speed</id><content type="html" xml:base="https://marcusfelling.com/blog/2025/optimizing-github-actions-workflows-for-speed"><![CDATA[<p>Slow CI/CD pipelines directly impact developer productivity and deployment frequency. When pull request checks take 15+ minutes to complete, developers context-switch to other tasks, breaking their flow and reducing productivity. Additionally, GitHub Actions charges by the minute, so inefficient workflows literally cost you money.</p>

<p>I’ll go through some optimization techniques I’ve found to be effective:</p>

<ul>
  <li><a href="#caching-dependencies">Caching Dependencies</a></li>
  <li><a href="#optimize-docker-usage">Optimize Docker Usage</a></li>
  <li><a href="#strategic-job-splitting-and-parallelization">Strategic Job Splitting and Parallelization</a></li>
  <li><a href="#matrix-builds-for-testing-efficiently">Matrix Builds for Testing Efficiently</a></li>
  <li><a href="#selective-testing-with-path-filtering">Selective Testing with Path Filtering</a></li>
  <li><a href="#self-hosted-runners-for-specialized-workloads">Self-hosted Runners for Specialized Workloads</a></li>
  <li><a href="#monitoring-your-workflow-performance">Monitoring Your Workflow Performance</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="caching-dependencies">Caching Dependencies</h2>

<p>The most immediate win for most workflows is proper dependency caching:</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">build</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/checkout@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Node.js</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">node-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">16'</span>
          
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cache dependencies</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="pi">|</span>
            <span class="s">~/.npm</span>
            <span class="s">node_modules</span>
          <span class="na">key</span><span class="pi">:</span> <span class="s">${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}</span>
          <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">${{ runner.os }}-node-</span>
            
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">npm ci --prefer-offline --no-audit</span>
</code></pre></div></div>

<p>The above example includes several optimizations:</p>
<ul>
  <li>Caching both <code class="language-plaintext highlighter-rouge">~/.npm</code> and <code class="language-plaintext highlighter-rouge">node_modules</code></li>
  <li>Using lockfile hashing for cache keys</li>
  <li>Fallback cache keys for partial hits</li>
  <li><code class="language-plaintext highlighter-rouge">--prefer-offline</code> flag to prioritize cached packages</li>
</ul>

<h2 id="optimize-docker-usage">Optimize Docker Usage</h2>

<p>For workflows using Docker, image layering and caching are critical:</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">docker-build</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/checkout@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v2</span>
        
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cache Docker layers</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">/tmp/.buildx-cache</span>
          <span class="na">key</span><span class="pi">:</span> <span class="s">${{ runner.os }}-buildx-${{ github.sha }}</span>
          <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">${{ runner.os }}-buildx-</span>
            
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/build-push-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
          <span class="na">push</span><span class="pi">:</span> <span class="no">false</span>
          <span class="na">cache-from</span><span class="pi">:</span> <span class="s">type=local,src=/tmp/.buildx-cache</span>
          <span class="na">cache-to</span><span class="pi">:</span> <span class="s">type=local,dest=/tmp/.buildx-cache-new,mode=max</span>
</code></pre></div></div>

<p>After your build, add this step to prevent cache size explosion:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Move cache</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">rm -rf /tmp/.buildx-cache</span>
          <span class="s">mv /tmp/.buildx-cache-new /tmp/.buildx-cache</span>
</code></pre></div></div>

<h2 id="strategic-job-splitting-and-parallelization">Strategic Job Splitting and Parallelization</h2>

<p>Instead of sequential steps, split work into parallel jobs when possible:</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">lint</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/checkout@v3</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v3</span>
        <span class="c1"># ...lint setup and execution</span>
        
  <span class="na">test</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/checkout@v3</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v3</span>
        <span class="c1"># ...test setup and execution</span>
        
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">lint</span><span class="pi">,</span> <span class="nv">test</span><span class="pi">]</span>
    <span class="c1"># This job only runs after lint and test complete successfully</span>
</code></pre></div></div>

<h2 id="matrix-builds-for-testing-efficiently">Matrix Builds for Testing Efficiently</h2>

<p>For multi-environment testing, use matrix builds:</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">test</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">matrix</span><span class="pi">:</span>
        <span class="na">node-version</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">14</span><span class="pi">,</span> <span class="nv">16</span><span class="pi">,</span> <span class="nv">18</span><span class="pi">]</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">Use Node.js ${{ matrix.node-version }}</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">node-version</span><span class="pi">:</span> <span class="s">${{ matrix.node-version }}</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm ci</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm test</span>
</code></pre></div></div>

<h2 id="selective-testing-with-path-filtering">Selective Testing with Path Filtering</h2>

<p>Avoid running unnecessary jobs by filtering based on changed paths:</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">frontend-tests</span><span class="pi">:</span>
    <span class="na">if</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">github.event_name == 'pull_request' &amp;&amp;</span>
      <span class="s">(github.event.pull_request.base.ref == 'main' ||</span>
       <span class="s">contains(github.event.pull_request.labels.*.name, 'run-all-tests'))</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/checkout@v3</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">dorny/paths-filter@v2</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">filter</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">filters</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">frontend:</span>
              <span class="s">- 'frontend/**'</span>
              <span class="s">- 'shared/**'</span>
            <span class="no">  </span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Frontend Tests</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">steps.filter.outputs.frontend == 'true'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">npm test</span>
</code></pre></div></div>

<h2 id="self-hosted-runners-for-specialized-workloads">Self-hosted Runners for Specialized Workloads</h2>

<p>For specific needs like GPU access or large compute requirements, <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners">self-hosted runners</a> can dramatically improve performance:</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">gpu-training</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">self-hosted-gpu</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="c1"># GPU-intensive tasks here</span>
</code></pre></div></div>

<h2 id="monitoring-your-workflow-performance">Monitoring Your Workflow Performance</h2>

<p>GitHub provides insights into workflow run times. Use the GitHub API to track this data over time:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">getWorkflowRunTimes</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">repo</span><span class="p">,</span> <span class="nx">workflow_id</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">Octokit</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@octokit/rest</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">octokit</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Octokit</span><span class="p">({</span> <span class="na">auth</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">GITHUB_TOKEN</span> <span class="p">});</span>
  
  <span class="kd">const</span> <span class="nx">runs</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">listWorkflowRuns</span><span class="p">({</span>
    <span class="nx">owner</span><span class="p">,</span>
    <span class="nx">repo</span><span class="p">,</span>
    <span class="nx">workflow_id</span><span class="p">,</span>
  <span class="p">});</span>
  
  <span class="k">return</span> <span class="nx">runs</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">workflow_runs</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">run</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">run</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="na">duration</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">run</span><span class="p">.</span><span class="nx">updated_at</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">run</span><span class="p">.</span><span class="nx">created_at</span><span class="p">),</span>
    <span class="na">conclusion</span><span class="p">:</span> <span class="nx">run</span><span class="p">.</span><span class="nx">conclusion</span>
  <span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>

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

<p>Optimizing GitHub Actions workflows is an ongoing process. Start with the high-impact items like caching and job parallelization, then iteratively improve based on metrics. The reward is not just faster builds but happier developers and lower costs.</p>]]></content><author><name>Marcus Felling</name></author><category term="GitHub Actions" /><category term="CICD" /><summary type="html"><![CDATA[Optimizing GitHub Actions Workflows for Speed and Efficiency]]></summary></entry><entry><title type="html">Control Your Windows VPN Connections from VS Code</title><link href="https://marcusfelling.com/blog/2025/vpn-toggle-vscode-extension" rel="alternate" type="text/html" title="Control Your Windows VPN Connections from VS Code" /><published>2025-02-12T00:00:00-05:00</published><updated>2025-02-12T00:00:00-05:00</updated><id>https://marcusfelling.com/blog/2025/vpn-toggle-vscode-extension</id><content type="html" xml:base="https://marcusfelling.com/blog/2025/vpn-toggle-vscode-extension"><![CDATA[<p>Ever get annoyed having to click through Windows settings just to toggle your VPN connection? Yeah, me too. That’s why I built a VS Code extension to handle it.</p>

<h2 id="why-i-built-this">Why I Built This</h2>

<p>The more I can do without touching my mouse, the happier I am. Switching VPN connections was one of those small but frequent tasks that bugged me - too many clicks just to turn a VPN on or off. Since I’m often already in VS Code when a VPN connection is required, why not control it from there?</p>

<h2 id="what-it-does">What It Does</h2>

<p>The <a href="https://marketplace.visualstudio.com/items?itemName=MFelling.vpn-toggle">VPN Toggle extension</a> lets you:</p>
<ul>
  <li>Toggle any Windows VPN connection on/off with commands</li>
  <li>Works with any VPN connection set up in Windows</li>
</ul>

<p><img src="https://github.com/user-attachments/assets/e699a3af-c323-4fec-9ac8-1b67fcf4dae1" alt="demo" class="img-fluid" /></p>

<h2 id="how-to-use-it">How to Use It</h2>

<ol>
  <li>Install the extension from the <a href="https://marketplace.visualstudio.com/items?itemName=MFelling.vpn-toggle">VS Code Marketplace</a></li>
  <li>Press <code class="language-plaintext highlighter-rouge">Ctrl+Shift+P</code> and type “VPN” to see the available commands:
    <ul>
      <li>VPN: <code class="language-plaintext highlighter-rouge">Select and Connect</code> - Shows a list of available VPN connections to choose from</li>
      <li>VPN: <code class="language-plaintext highlighter-rouge">Connect to Last Used</code> - Connects to the most recently used VPN</li>
      <li>VPN: <code class="language-plaintext highlighter-rouge">Disconnect Current</code> - Disconnects the current VPN connection</li>
    </ul>
  </li>
</ol>

<h2 id="under-the-hood">Under the Hood</h2>

<p>If you’re curious about how it works, the extension uses PowerShell commands to interact with Windows VPN connections. All the code is <a href="https://github.com/MarcusFelling/vpn-toggle">open source on GitHub</a> if you want to take a peek or contribute.</p>

<h2 id="feedback-please">Feedback Please!</h2>

<p>Let me know what you think! Feel free to <a href="https://github.com/MarcusFelling/vpn-toggle/issues">open an issue</a> if you have any suggestions or run into problems.</p>]]></content><author><name>Marcus Felling</name></author><category term="VS Code Extensions" /><category term="Windows" /><summary type="html"><![CDATA[I built a VS Code extension to toggle VPN connections because clicking is too much work]]></summary></entry><entry><title type="html">Removing Sensitive Data from Git History with BFG and VS Code</title><link href="https://marcusfelling.com/blog/2024/removing-sensitive-data-from-git-history-with-bfg-and-vs-code/" rel="alternate" type="text/html" title="Removing Sensitive Data from Git History with BFG and VS Code" /><published>2024-04-23T10:06:19-04:00</published><updated>2024-04-23T10:06:19-04:00</updated><id>https://marcusfelling.com/blog/2024/removing-sensitive-data-from-git-history-with-bfg-and-vs-code</id><content type="html" xml:base="https://marcusfelling.com/blog/2024/removing-sensitive-data-from-git-history-with-bfg-and-vs-code/"><![CDATA[<p><strong>TL; DR:</strong> I created a VS Code extension that makes it easier to remove credentials from Git History.</p>

<p>I was recently notified that an old API key was discovered in one of the repos I own. Even if you remove the sensitive data in a new commit, it can still be found in the Git history.</p>

<p>To remove the API key, I decided to use the <a href="https://rtyley.github.io/bfg-repo-cleaner/">BFG Repo-Cleaner</a> for cleansing bad data out of your Git repository history. However, I found myself fumbling around with the BFG CLI and spending way too much time trying to remove the key from the Git history.</p>

<p>That’s when I realized there had to be a better way. As a frequent user of Visual Studio Code, I thought, “Why not create a VS Code extension that simplifies this process?”.</p>

<h2 id="introducing-the-bfg-vs-code-extension">Introducing the BFG VS Code Extension</h2>

<p>The <a href="https://marketplace.visualstudio.com/items?itemName=MFelling.bfg-vscode">BFG VS Code Extension</a> is a wrapper for the BFG Repo-Cleaner that makes it easy to remove credentials from your Git history. It guides you through the process step by step using the Command Palette, so you don’t have to remember complex CLI commands.</p>

<p>Here is how it works:</p>

<ol>
  <li>Clones a fresh copy of your repo using the –mirror flag.</li>
  <li>Installs BFG: This step downloads the BFG jar file from the official repository and saves it in the workspace folder.</li>
  <li>Enter credential to remove: This step prompts the user to enter the credential to remove, writes this credential to a file in the workspace folder, and uses the <code class="language-plaintext highlighter-rouge">--replace-text</code> option of BFG Repo-Cleaner to replace this credential with <code class="language-plaintext highlighter-rouge">***REMOVED***</code> in the repository’s history.</li>
  <li>Remove credentials: This step runs the BFG Repo-Cleaner with the <code class="language-plaintext highlighter-rouge">--replace-text</code> option to replace the specified credential with <code class="language-plaintext highlighter-rouge">***REMOVED***</code> in the repository’s history.</li>
  <li>Clean your repository: This step runs the <code class="language-plaintext highlighter-rouge">git reflog expire --expire=now --all &amp;&amp; git gc --prune=now --aggressive</code> command to clean the repository.</li>
  <li>Push the changes: This step runs the <code class="language-plaintext highlighter-rouge">git push --force</code> command to push the changes to the remote repository</li>
</ol>

<p>You can find the BFG VS Code Extension on the <a href="https://marketplace.visualstudio.com/items?itemName=MFelling.bfg-vscode">VS Code Marketplace</a> and the source code on <a href="https://github.com/MarcusFelling/bfg-vscode">GitHub</a>. If you have any questions or feedback, feel free to open an issue on GitHub.</p>

<p>The best way to prevent sensitive data from being exposed in your Git history is to never commit it in the first place. Always use environment variables or configuration files that are ignored by Git to store sensitive data. But, if you do accidentally commit sensitive data, the BFG VS Code Extension is here to help you clean it up!</p>]]></content><author><name>Marcus Felling</name></author><category term="VS Code Extensions" /><summary type="html"><![CDATA[TL; DR: I created a VS Code extension that makes it easier to remove credentials from Git History.]]></summary></entry><entry><title type="html">Using Azure Test Plans with Playwright</title><link href="https://marcusfelling.com/blog/2023/using-azure-test-plans-with-playwright/" rel="alternate" type="text/html" title="Using Azure Test Plans with Playwright" /><published>2023-09-17T17:01:58-04:00</published><updated>2023-09-17T17:01:58-04:00</updated><id>https://marcusfelling.com/blog/2023/using-azure-test-plans-with-playwright</id><content type="html" xml:base="https://marcusfelling.com/blog/2023/using-azure-test-plans-with-playwright/"><![CDATA[<p>In 2020, I blogged about associating <a href="https://marcusfelling.com/blog/2020/associating-automated-tests-with-azure-test-cases/">automated tests with Azure Test Cases</a>. The post had 18 questions, which indicates there is still confusion on how this works, especially how to set it up with Playwright (which was pre-stable release at the time).</p>

<p>In this post, I’ll walk through how to configure both Playwright Test (JavaScript/TypeScript) and Playwright .NET to get test results in Azure Test Plans. Each option uses abstractions built on the Azure DevOps <a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/test/?view=azure-devops-rest-5.0">REST API</a> so you don’t have to write additional code to accomplish this.</p>

<h2 id="why">Why?</h2>

<p>Azure Test Plans is a popular service that many teams are using for manual testing. By publishing your automated Playwright tests to the service, you get a couple of benefits:</p>

<ol>
  <li><strong>Traceability</strong>. This gives you the option to link your requirements (Azure Boards) to automated tests and the pipeline that ran them. By mapping the two, you can establish the quality of the requirements based on test results. Ideally, a test case is created for each of the acceptance criteria listed for the requirement.</li>
  <li><strong>History</strong>. Drilling into every pipeline run to see test results over time is a pain. Azure Test Plans allows you to see results through features like the <a href="https://learn.microsoft.com/en-us/azure/devops/test/progress-report?view=azure-devops">progress report</a> and <a href="https://learn.microsoft.com/en-us/azure/devops/test/track-test-status?view=azure-devops#track-testing-progress">charts</a>.</li>
  <li><strong>Test inventory.</strong> By tracking automated AND manual test cases, you can do things like <a href="https://learn.microsoft.com/en-us/azure/devops/test/track-test-status?view=azure-devops#track-test-case-status">track the status of a test case</a> (not automated, planned to be automated, or automated). This makes it easy to track the progress of automated testing efforts, e.g. how many manual tests have been converted to automated, how many remain, etc.</li>
</ol>

<h2 id="what-are-the-options">What are the options?</h2>

<p>I’ll show working code examples for both Playwright Test (TypeScript) and Playwright .NET using NUnit. If you’re already sick of reading and want to see them in action, here are some links.</p>

<p><strong>TypeScript:</strong> <a href="https://dev.azure.com/marcusfelling/Playground/_git/PlaywrightTest?path=/tests">tests</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_build?definitionId=24">pipeline to run tests</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_testPlans/execute?planId=442&amp;suiteId=443">test plan</a></p>

<p><strong>.NET</strong>: <a href="https://dev.azure.com/marcusfelling/Playground/_git/PlaywrightDotnet?path=/PlaywrightTests/Header.cs">tests</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_build?definitionId=24">build to publish binaries</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_release?_a=releases&amp;view=mine&amp;definitionId=2">release to run tests</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_testPlans/execute?planId=432&amp;suiteId=433">test plan</a></p>

<h2 id="playwright-test-typescript">Playwright Test (TypeScript)</h2>

<p><a href="https://www.npmjs.com/package/@alex_neo/playwright-azure-reporter">playwright-azure-reporter</a> is a custom reporter (npm package) that allows you to post test results by annotating your test case name with the Azure test plan ID. The README has instructions for installing the package and adding the reporter to <code class="language-plaintext highlighter-rouge">playwright.config.ts</code></p>

<p>My example project’s config looks like this: <a href="https://gist.github.com/MarcusFelling/66356db19ecb20ff798150ddd91900da">playwright</a><a href="https://dev.azure.com/marcusfelling/Playground/_git/PlaywrightTest?path=/playwright.config.ts&amp;version=GBmain&amp;line=31&amp;lineEnd=32&amp;lineStartColumn=1&amp;lineEndColumn=1&amp;lineStyle=plain&amp;_a=contents">.config.ts</a>.</p>

<p>Once that is in place:</p>

<ol>
  <li>Manually create new test cases in Azure Test Plans taking note of the ID (planID in query string of URL)</li>
  <li>Add the ID in brackets to the test case title. 444, 445 in this example:</li>
</ol>

<p><img src="/content/uploads/2023/09/annotation-test-id-1024x329.webp" alt="Annotation test ID showing Playwright tests with Azure Test Plan IDs in brackets" class="img-fluid" /></p>

<p>When these tests get run, you will then be able to see the outcome for each test case:</p>

<p><img src="/content/uploads/2023/09/outcome-1024x388.webp" alt="" class="img-fluid" /></p>

<p>My example pipeline runs these tests for every commit on main and also uses the JUnit reporter to publish results to the pipeline’s Test tab:</p>

<p><img src="/content/uploads/2023/09/test-tab.webp" alt="" class="img-fluid" /></p>

<h2 id="playwright-net">Playwright .NET</h2>

<p>This option works out of the box but has some caveats and complexity: A Windows runner and a release pipeline are required to use the Visual Studio test platform installer and Visual Studio Test tasks. Also, Visual Studio must be used to associate test cases.</p>

<p>Here is how I set this up in my example project:</p>

<ol>
  <li>Manually create new Azure Test Plans test cases</li>
  <li>
    <p>Use Visual Studio’s test explorer to associate the automated test cases:</p>

    <p><img src="/content/uploads/2023/09/associate-test-case.webp" alt="" class="img-fluid" /></p>

    <p>This will change the Automation status field on the test case work item to automated:</p>

    <p><img src="/content/uploads/2023/09/automation-status.webp" alt="" class="img-fluid" /></p>

    <p>Once the test cases are configured, we can set up our pipelines to run the tests.</p>
  </li>
  <li>
    <p>Create a build pipeline that runs <code class="language-plaintext highlighter-rouge">dotnet publish</code> (using Windows agent) in order to create an artifact with the Playright binaries: <a href="https://dev.azure.com/marcusfelling/Playground/_git/PlaywrightDotnet?path=/playwright-dotnet.yml">playwright-dotnet.yml</a></p>
  </li>
  <li>
    <p>Create a <a href="https://dev.azure.com/marcusfelling/Playground/_releaseDefinition?definitionId=2&amp;_a=definition-tasks&amp;environmentId=4">release pipeline</a> referencing the artifact created in the previous step:</p>

    <p><img src="/content/uploads/2023/09/artifact.webp" alt="" class="img-fluid" /></p>
  </li>
  <li>
    <p>Add install tasks (that run on Windows agent) for “Visual Studio Test Platform Installer” (prereq for VS Test task), .NET, and Playwright browsers:</p>

    <p><img src="/content/uploads/2023/09/tasks.webp" alt="" class="img-fluid" /></p>
  </li>
  <li>
    <p>Add the VS Test task and reference your test plan:</p>

    <p><img src="/content/uploads/2023/09/vstest-task.webp" alt="" class="img-fluid" /></p>
  </li>
  <li>
    <p>Create a new release to run the tests. Example results: <a href="https://dev.azure.com/marcusfelling/Playground/_releaseProgress?_a=release-environment-extension&amp;releaseId=12&amp;environmentId=12&amp;extensionId=ms.vss-test-web.test-result-in-release-environment-editor-tab">Test tab</a>, <a href="https://dev.azure.com/marcusfelling/Playground/_testPlans/_results?testCaseId=434&amp;contextPointId=31">test plan results</a>.</p>

    <p><img src="/content/uploads/2023/09/test-case-results.webp" alt="" class="img-fluid" /></p>
  </li>
</ol>

<h2 id="summary">Summary</h2>

<p>Hopefully, you were able to follow my examples to get this set up in your own environment. I’d love to hear feedback on anything I may have missed, new features you’d like to see from the product team at Microsoft, or interesting use cases you have experience with.</p>

<p>Happy testing,
Marcus</p>]]></content><author><name>Marcus Felling</name></author><category term="Azure DevOps" /><category term="Playwright" /><summary type="html"><![CDATA[In 2020, I blogged about associating automated tests with Azure Test Cases. The post had 18 questions, which indicates there is still confusion on how this works, especially how to set it up with Playwright (which was pre-stable release at the time).]]></summary></entry><entry><title type="html">Measuring Website Performance with Playwright Test and Navigation Timing API</title><link href="https://marcusfelling.com/blog/2023/measuring-website-performance-with-playwright-test-and-navigation-timing-api/" rel="alternate" type="text/html" title="Measuring Website Performance with Playwright Test and Navigation Timing API" /><published>2023-04-27T10:40:19-04:00</published><updated>2023-04-27T10:40:19-04:00</updated><id>https://marcusfelling.com/blog/2023/measuring-website-performance-with-playwright-test-and-navigation-timing-api</id><content type="html" xml:base="https://marcusfelling.com/blog/2023/measuring-website-performance-with-playwright-test-and-navigation-timing-api/"><![CDATA[<p>I was recently tasked with measuring the impact of a Redis cache on an e-commerce site. This was pretty simple with <a href="https://azure.microsoft.com/en-us/products/load-testing/">Azure Load Testing</a>, by comparing the results of 2 sites, one with cache, and one without. However, to better exercise the site and understand the user experience, I wanted also to use Playwright.</p>

<p>Playwright already has useful features built in to report on things like the HTML report test step timing and the trace viewer that includes the call duration of each action.</p>

<p><img src="/content/uploads/2023/04/test-step-example.webp" alt="" class="img-fluid" /></p>

<p>HTML report test step duration</p>

<p>I wanted to take this a step further by using the Navigation Timing API, measuring start to loadEventEnd. All of the examples I found online used <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/timing">performance.timing</a>, which is now deprecated. This is a very simple code snippet, but posting this will hopefully help others find a solution faster.</p>

<p>Here we have a function <strong>measurePerformance</strong> that can be called inside any test case to get navigation start to load event end times. This could easily be wrapped in a conditional to fail the test based on times. In my case, I just wanted it to be surfaced in the HTML report as a custom annotation to compare between sites, by toggling baseURL.</p>

<script src="https://gist.github.com/MarcusFelling/88f8ddde9941ec1cef19667892dbe2d0.js"></script>

<p>As a result, this is what the HTML report looks like:</p>

<p><img src="/content/uploads/2023/04/performance-playwright-html-report.webp" alt="" class="img-fluid" /></p>

<p>Happy testing!</p>]]></content><author><name>Marcus Felling</name></author><category term="Playwright" /><summary type="html"><![CDATA[I was recently tasked with measuring the impact of a Redis cache on an e-commerce site. This was pretty simple with Azure Load Testing, by comparing the results of 2 sites, one with cache, and one without. However, to better exercise the site and understand the user experience, I wanted also to use Playwright.]]></summary></entry><entry><title type="html">6 Nifty GitHub Actions Features 🚀</title><link href="https://marcusfelling.com/blog/2023/6-nifty-github-actions-features/" rel="alternate" type="text/html" title="6 Nifty GitHub Actions Features 🚀" /><published>2023-03-08T14:23:17-05:00</published><updated>2023-03-08T14:23:17-05:00</updated><id>https://marcusfelling.com/blog/2023/6-nifty-github-actions-features</id><content type="html" xml:base="https://marcusfelling.com/blog/2023/6-nifty-github-actions-features/"><![CDATA[<p>I’ve been having a lot of fun with GitHub Actions lately and wanted to document some of the features I regularly use, including some tips and tricks.</p>

<h2 id="1-create-separate-environments-for-development-staging-and-production">1. Create separate environments for development, staging, and production</h2>

<p>GitHub Actions has an <a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment">environments feature</a> to describe a deployment target such as dev, staging, or production. By referencing the environment in a job, you can take advantage of <a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#environment-protection-rules">protection rules</a> and/or <a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#environment-secrets">secrets</a> that get scoped to the environment. Some potential use cases include requiring a particular person or team to approve workflow jobs that reference an environment (e.g. manual approval before production deploy), or limiting which branches can deploy to a particular environment. I also like to set the environment URL so it’s easily accessible from the summary page:</p>

<p><img src="/content/uploads/2023/03/image.webp" alt="" class="img-fluid" /></p>

<h2 id="2-establish-workflow-breakpoints-with-dependencies">2. Establish workflow breakpoints with dependencies</h2>

<p>By default, GitHub Actions runs multiple commands simultaneously. However, you can utilize the <code class="language-plaintext highlighter-rouge">needs</code> keyword to <a href="https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows#creating-dependent-jobs">create dependencies between jobs</a>, meaning that if a job fails (e.g. tests), dependent jobs won’t run. This also helps you control jobs which jobs run in parallel; if there aren’t dependencies between steps, break them out into separate jobs, then set their <code class="language-plaintext highlighter-rouge">needs</code> to the next step in the process.</p>

<p>e.g. my app, database, and infra as code projects can be built at the same time before deploying to dev:</p>

<p><img src="/content/uploads/2023/03/image-1.webp" alt="" class="img-fluid" /></p>

<h2 id="3-use-secrets-to-store-sensitive-workflow-data">3. Use secrets to store sensitive workflow data</h2>

<p>GitHub’s secrets allow you to securely store sensitive data, including passwords, tokens, certificates, etc. You can directly reference secrets in workflows, meaning that you can create and share workflows with colleagues that employ secrets for secure values without hardcoding them directly into YAML workflow files. I like to scope the secrets close to the steps that require them. For example, rather than setting a secret for the entire workflow to access, it can be set for the job that contains steps that reference the secret.</p>

<p>e.g. Only the Playwright test job needs to reference AzureAD creds:</p>

<p><img src="/content/uploads/2023/03/image-2.webp" alt="" class="img-fluid" /></p>

<h2 id="4-conditionals-can-aid-in-differences-between-environments">4. Conditionals can aid in differences between environments</h2>

<p>GitHub Actions allows you to use conditionals that employ the “if” keyword to decide whether a step should run. You can use this feature to develop dependencies so that if a dependent job fails, the workflow can continue running. You can also use specific built-in functions for data operations, as well as leverage status check functions to determine whether preceding steps have succeeded, failed, canceled, or disrupted. Moreover, you can use conditionals to share workflow data among different branches and forks, with steps tailored to different triggers or environments. The conditions can also be set in reusable workflows to toggle different steps between environments:</p>

<p>e.g. I want reusable workflows to be uniform across environments, with the exception of steps that are only based on environmentName conditionals:</p>

<script src="https://gist.github.com/MarcusFelling/a24904731e73dd9b2bddeade2c459948.js"></script>

<h2 id="5-share-data-between-jobs-to-aid-in-build-once-deploy-many">5. Share data between jobs to aid in “build once, deploy many”</h2>

<p>GitHub Actions enables you to share data between jobs in any workflow as <a href="https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts">artifacts</a>, which are linked to the workflow run where they are produced. This can help simplify the creation of workflows and facilitate the development of more complex automation where one workflow informs another via dependencies or conditionals. This also helps enable the mantra “build once, deploy many”. In other words, build projects in an environment-agnostic fashion, upload them as artifacts, then all deployment jobs use the same set of artifacts across environments.</p>

<h2 id="6-use-contexts-to-access-workflow-information">6. Use contexts to access workflow information</h2>

<p><a href="https://docs.github.com/en/actions/learn-github-actions/contexts">Contexts </a>represent a group of variables that can access details about workflow runs, runner environments, jobs, and steps to help derive key information about workflow operations. Contexts use expression syntax such as ${{ }}, and you can use most of them at any point in the workflow. I like to dump the entire context at the beginning of jobs to aid in troubleshooting:</p>

<script src="https://gist.github.com/MarcusFelling/01d9e6ed08b3677b9aad5adb3a624aca.js"></script>

<h2 id="wrapping-up">Wrapping up</h2>

<p>I’m curious to learn about other ways folks are leveraging GitHub Actions features. Add a comment to this post with any tips or tricks you’ve used with GitHub Actions!</p>]]></content><author><name>Marcus Felling</name></author><category term="GitHub Actions" /><category term="CICD" /><summary type="html"><![CDATA[I’ve been having a lot of fun with GitHub Actions lately and wanted to document some of the features I regularly use, including some tips and tricks.]]></summary></entry></feed>