michaelheap.com Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order) 2024-01-10T15:10:39Z https://michaelheap.com Michael Heap m@michaelheap.com Workspace layouts with Hammerspoon Grid 2024-01-10T15:10:39Z https://michaelheap.com/hammerspoon-layout/ <p>Since I switched from Arch to MacOS there’s been an i3 shaped hole in my life. I’ve used apps like Divvy to approximate it, but it’s not the same.</p> <p>I tried out some of the MacOS tiling window managers, but couldn’t get to grips with them. Between having to trigger things using external apps (Yabai and <code>skhd</code>) and having to disable system integrity protection, things just didn’t click. One of the things I loved about i3 was its simplicity.</p> <p>After many years of searching, I think I’ve found a solution in Hammerspoon.</p> <h2 id="hammerspoon%3F" tabindex="-1">Hammerspoon?</h2> <p>Hammerspoon is an automation framework for MacOS that allows you to script various things using Lua.</p> <p>I’ve written about <a href="https://michaelheap.com/topic/hammerspoon/">Hammerspoon</a> before, and have been using it for window management since I abandoned <a href="https://apps.apple.com/fi/app/divvy-window-manager/id413857545">Divvy</a> a couple of years ago (when I configured a dedicated <code>hyper</code> key on my keyboard).</p> <p>I was recently browsing through <a href="https://github.com/jakubdyszkiewicz/dotfishy/blob/master/hammerspoon/init.lua">Jakub’s Hammerspoon config</a> and spotted his usage of <code>hs.grid</code> to configure predefined layouts.</p> <p>Predefined layouts were one of my big use cases for i3, and so I went digging.</p> <h2 id="hs.grid" tabindex="-1">hs.grid</h2> <p><code>hs.grid</code> splits your screen into an <code>x</code> by <code>y</code> grid. You can place windows by specifying a width/height along with an offset for the <code>x</code> and <code>y</code> values.</p> <p>I have split my screen in to an 8x2 grid, with zero margin. I also have a shortcut to show the grid overlay on screen in case I want to position a single window in a non-standard layout.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setMargins</span><span style="color: #D8DEE9FF">(hs.</span><span style="color: #D8DEE9">geometry</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">size</span><span style="color: #D8DEE9FF">(</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">,</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">))</span></div><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setGrid</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">8x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">hotkey</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">bind</span><span style="color: #D8DEE9FF">(hyper,</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">g</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">,</span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">()</span></div><div class="line"><span style="color: #D8DEE9FF"> hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">show</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #81A1C1">end</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <p>To align a window to the grid, you call <code>hs.grid.set</code> and pass an instance of <code>hs.window</code> as the first parameter. The window is being placed on the right hand side of the screen (4 cells offset on the x-axis, 0 on the y-axis, and it covers a 4x2 space on an 8x2 grid).</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">hs.</span><span style="color: #D8DEE9">grid</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">set</span><span style="color: #D8DEE9FF">(window, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">4,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span></div></code></div></pre> <h2 id="automatic-layouts" tabindex="-1">Automatic layouts</h2> <p>One of the things that I loved about Jakub’s Hammerspoon config is that he had <a href="https://github.com/jakubdyszkiewicz/dotfishy/blob/master/hammerspoon/init.lua#L99-L110">a helper function</a> to discover all of the windows for a certain app before reflowing windows in to the grid.</p> <p>I borrowed this idea and built <a href="https://github.com/mheap/dotfiles/blob/main/dot_hammerspoon/Layouts.lua#L2-L12">some convenience functions</a> which allow you to define layouts like this:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">lua</div><div class="code-container"><code><div class="line"><span style="color: #88C0D0">defineLayout</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Writing</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">1</span><span style="color: #D8DEE9FF">, {</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Bear</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">0,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Arc</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">2,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">true</span><span style="color: #D8DEE9FF">}</span></div><div class="line"><span style="color: #D8DEE9FF">});</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">defineLayout</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Code</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">2</span><span style="color: #D8DEE9FF">, {</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Arc</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">0,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Code - Insiders</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">2,0 4x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">true</span><span style="color: #D8DEE9FF">},</span></div><div class="line"><span style="color: #D8DEE9FF"> {</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">iTerm</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">, </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">6,0 2x2</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">}</span></div><div class="line"><span style="color: #D8DEE9FF">})</span></div></code></div></pre> <p>Now I can press <code>hyper+1</code> to have Bear on the left 1/4 of the screen and Arc in the middle half. If I’m writing code, I can press <code>hyper+2</code> to have VSCode in the middle taking up half of the screen, with Arc on the left and a terminal on the right.</p> <p>Applications will only be positioned if they are already launched. Hammerspoon provides a <code>hs.application.launchOrFocus()</code> method, but I chose to implement <a href="https://github.com/mheap/dotfiles/blob/main/dot_hammerspoon/Layouts.lua#L63-L68">focusIfLaunched</a> as sometimes I don’t have all the apps open (e.g. Bear is optional when I’m writing).</p> <h2 id="give-it-a-go" tabindex="-1">Give it a go</h2> <p>Hammerspoon is wonderful, and I recommend giving it a go if you haven’t already tried it. <a href="https://github.com/mheap/dotfiles/tree/main/dot_hammerspoon">My Hammerspoon config</a> is publicly available, and whilst I’m not a power user, it saves me a ton of time every single day.</p> <p>If you’ve a Hammerspoon user and have tips and tricks you want to share, I’m <a href="https://hachyderm.io/@mheap/followers">@mheap@hachyderm.io</a>. I’d love to hear them!</p> <h2 id="bonus-content" tabindex="-1">Bonus content</h2> <p>I’ve recently stumbled across <a href="https://github.com/nikitabobko/AeroSpace">aerospace</a>, which bills itself as “an i3-like tiling window manager for macOS”. I haven’t given it a go yet, but it looks promising based on <a href="https://www.youtube.com/watch?v=UOl7ErqWbrk">the YouTube video</a>.</p> Could/Should/Will/Why 2024-01-08T20:27:59Z https://michaelheap.com/could-should-will-why/ <p>I had an interesting conversation with my manager this week. I’ve been working on a lot of different projects, and was a little spread thin. In one of our calls my manager asked me why I’m working on .</p> <p>We sat and chatted about it, and I tried to answer the question: <strong>why</strong> am I doing these things? After hearing me out, they agreed that everything I was doing was important. Then they repeated the question, with a slightly different emphasis:</p> <p>Why am <strong>I</strong> doing these things?</p> <p>I didn't have a good answer. The things needed doing. No-one else was doing them. I could do them, so I took them on. But why was <strong>I</strong> the one taking them on?</p> <p>A day has a finite number of hours. If I’m spending my time doing X, then Y isn’t getting done. If Y is something that only I can (or will) do, then isn’t that a better use of my time than X?</p> <p>Here’s a concrete example. I had a choice to either:</p> <ol> <li>Write some tutorials for one of our products using the API</li> <li>Build a proof of concept Terraform provider to understand if we want to invest in the next financial year</li> </ol> <p>My decision? To do both. Work on the proof of concept first, and write some tutorials between my meetings once it's done.</p> <p>That’s the wrong answer.</p> <p>It turns out that writing tutorials isn’t my responsibility. More than that, by writing them I’m taking away learning opportunities from others. I’m taking it away from the product manager that wants to learn to write documentation. I’m taking it away from the writer that wants to learn more about API development.</p> <p>Anything I take on should meet one of the following criteria:</p> <ul> <li>No-one else can do the work</li> <li>It’s time sensitive, and no-one else has capacity</li> <li>There’s a learning opportunity for me</li> </ul> <h2 id="building-a-system" tabindex="-1">Building a system</h2> <p>When my manager asked why <strong>I</strong> was working on these projects, I realised that I couldn’t answer. We talked about who <em>could</em> do the work, who <em>should</em> do the work, and who <em>will</em> do the work. This gave me a good framework for evaluating projects before starting them.</p> <p>I’ve added a final column - <em>why</em> - to help keep a record of why the person that <em>will</em> take on the project is responsible.</p> <p>Here’s an example with a couple of projects:</p> <table> <thead> <tr> <th>Project</th> <th>Could</th> <th>Should</th> <th>Will</th> <th>Why</th> </tr> </thead> <tbody> <tr> <td>Public Demos</td> <td>DevRel, Sales Engineering</td> <td>Sales Engineering</td> <td>Sales Engineering</td> <td>Demos are a core part of the SE role. If DevRel build demos, there would be duplication.</td> </tr> <tr> <td>Release Demos</td> <td>Product, DevRel, Product Marketing</td> <td>Product Marketing</td> <td>DevRel</td> <td>DevRel has an engineering background to understand the features + how to get started</td> </tr> <tr> <td>Splunk Tutorial</td> <td>Docs, DevRel</td> <td>Docs</td> <td>DevRel</td> <td>Docs team is at capacity with regular release documentation</td> </tr> <tr> <td>VSCode Extension</td> <td>DevRel, Engineering</td> <td>Engineering</td> <td>N/A</td> <td>Project not prioritised.</td> </tr> <tr> <td>Terraform Provider</td> <td>DevRel, Engineering</td> <td>DevRel</td> <td>DevRel</td> <td>This is an initial proof of concept exploration. If the idea shows potential, it's added to the Engineering roadmap.</td> </tr> </tbody> </table> <p>Let’s work through the tutorials example in more detail:</p> <ul> <li>A community member asked how to send logs from Kong on a VM to Splunk</li> <li>DevRel couldn’t find any existing documentation to send them</li> <li>DevRel the docs team if they could write a new tutorial</li> <li>The docs team share that they’re already overcommitted.</li> </ul> <p>At this point DevRel have three choices:</p> <ul> <li>Abandon the project</li> <li>Submit it in to the docs team backlog</li> <li>Do it themselves</li> </ul> <p>This is your classic prioritisation problem. Whichever option you choose, fill out the <em>why</em> column with the rationale then move on.</p> <h2 id="has-it-helped%3F" tabindex="-1">Has it helped?</h2> <p>Kind of. I still take on more projects than I should, but having an explicit <em>will</em> step forces me to think about why we’re taking on this specific project. It forces me to talk to other people about the project. Could their teams take on the work? Do they <strong>want</strong> to take on the work? Are there learning opportunities?</p> <p>Next time you have an idea for some work that needs doing, run it through <em>could/should/will/why</em> to make sure that you’re the best suited person or team to complete the project.</p> Create an AWS RDS database 2024-01-05T15:52:09Z https://michaelheap.com/aws-rds-db/ <p>I've been using RDS to provide throwaway databases for testing and wanted to work from the CLI to speed things up. The <code>aws rds</code> incantations were hard to find at times, so here they are for posterity.</p> <p>Create a publicly accessible Postgres database:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds create-db-instance --db-instance-identifier demo-db --db-instance-class db.t3.micro --allocated-storage 50 --engine postgres --publicly-accessible --master-username postgres --master-user-password YOUR_PASSWORD</span></div></code></div></pre> <p>Show the database status and connection details:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds describe-db-instances </span><span style="color: #81A1C1">|</span><span style="color: #D8DEE9FF"> jq </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">.DBInstances[] | select(.DBInstanceIdentifier == "demo-db") | .DBInstanceStatus,.Endpoint</span><span style="color: #ECEFF4">'</span></div></code></div></pre> <p>Delete the database:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws rds delete-db-instance --db-instance-identifier demo-db --skip-final-snapshot</span></div></code></div></pre> Create an Azure AKS Cluster 2024-01-05T15:50:50Z https://michaelheap.com/create-aks-cluster/ <p>I needed to test Azure AKS with the Application Gateway Ingress Controller. Thankfully, Azure makes it easy with it's <code>az aks</code> CLI.</p> <p>Create a new Azure resource group:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az group create --name mheap-test --location uksouth</span></div></code></div></pre> <p>Create a new AKS cluster with the Azure Application Gateway Ingress Controller enabled:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks create --name mheap-aks-test --resource-group mheap-test --node-count 1 --network-plugin azure --enable-managed-identity --enable-addons ingress-appgw --appgw-name mheap-appgw --appgw-subnet-cidr </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">10.225.0.0/16</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> --generate-ssh-keys</span></div></code></div></pre> <p>Configure <code>kubectl</code> to connect to this cluster:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks get-credentials --resource-group mheap-test --name mheap-aks-test</span></div></code></div></pre> <p>Once you've finished testing, delete the AKS cluster and resource group:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">az aks delete --name mheap-aks-test --resource-group mheap-test --yes</span></div><div class="line"><span style="color: #D8DEE9FF">az group delete --name mheap-test --yes</span></div></code></div></pre> Does this live in the docs or on the knowledge base? 2024-01-05T15:21:17Z https://michaelheap.com/docs-or-knowledge-base/ <blockquote> <p>You may also be interested in my thoughts on content living on <a href="https://michaelheap.com/blog-or-docs/">a blog vs docs</a></p> </blockquote> <p>Making customers successful with a product requires three key components:</p> <ol> <li><strong>UX</strong>: If the application is intuitive, customers are successful without thinking about it.</li> <li><strong>Documentation</strong>: Practically speaking, most apps need supporting documentation to explain new concepts or provide concrete implementation instructions.</li> <li><strong>Support</strong>: Heroes that work directly with customers every day to make them successful in a 1:1 setting.</li> </ol> <p>Great UX is the ideal solution, but in most companies, customers need supporting enablement content.</p> <p>As the company grows, both the documentation and support teams build out their own libraries of content to make customers successful. Eventually, they’ll hit a critical mass and someone asks the inevitable question:</p> <blockquote> <p>Does this live in the docs or on the knowledge base?</p> </blockquote> <p>Both teams want their content to live in their own system. Support wants to use the knowledge base as it has deep integration with their ticketing system, with workflows for pulling answers from articles which aren’t always able to integrate with external docs. Docs want to use their docs-as-code platform and all the tooling that they’ve built around the written word.</p> <h2 id="where-does-the-content-live%3F" tabindex="-1">Where does the content live?</h2> <p>Whatever you do, don't store the content in both places. The second copy goes out of sync before you even hit publish.</p> <p>Instead, categorise content in two buckets:</p> <ul> <li>This is generally applicable to the majority of customers using this product</li> <li>This is specific to a small number of customer setups</li> </ul> <p>Imagine that you have a customer issue come in and the support team resolves it. Now what? Where does the solution go?</p> <p>If the answer applies to the majority of customers, then I recommend having a small KB article so that it shows up in their day to day workflow, but the KB article has a small description that then links to docs.</p> <p>If the answer is specific to a small number of customers, the answer lives in the KB. The support team shares these answers on demand in response to tickets. They're also available if people search the KB specifically (or on the docs too if you have multi-site indexing).</p> <p>Splitting content by audience allows you to keep your documentation focused on the 80% of customers that can be successful without human interaction. Providing an exhaustive list of failure modes makes it difficult for customers to find what they're looking for when everything is working as expected.</p> <h2 id="examples" tabindex="-1">Examples</h2> <p>The above is quite abstract, so here are some examples from the docs teams that I’ve been a part of.</p> <p>Vonage provides communication APIs, including SMS. Sending an SMS using the API is something that was applicable to everyone, so it had public docs. Restrictions around the sender ID (<code>from</code>) used are different on a per-country basis (<a href="https://api.support.vonage.com/hc/en-us/articles/204017823-Peru-SMS-Features-and-Restrictions">Peru</a> for example). Most of Vonage’s customers were in the USA where there were no restrictions, so this information lived in the knowledge base. When the USA introduced 10DLC it affected the majority of customers, so the restrictions were added to the public docs <em>and</em> the knowledge base.</p> <p>Kong provides connectivity tools, such as API Gateways and Service Meshes. Older versions of Ubuntu don’t have a required library (<code>zlib</code>) installed by default and every customer using an old version of Ubuntu encounters this issue, so we document it on the installation page. Kong Mesh allows you to proxy requests to Redis, but sometimes you get a cryptic <code>&quot;Error: Protocol error, got &quot;H&quot;</code> error. This means that you configured your <code>MeshGatewayRoute</code> as with a HTTP protocol rather than TCP. A small percentage of customers hit this issue, so it lives in the knowledge base.</p> <h2 id="tl%3Bdr" tabindex="-1">TL;DR</h2> <ol> <li>If it’s applicable to &gt; 50% of customers, put it in the public docs</li> <li>If it applies to a small percentage of customers, put it in the knowledge base</li> <li>Make the support team’s life easier by putting pointers to public docs in the knowledge base</li> </ol> Create an Amazon EKS Cluster 2023-12-20T16:44:58Z https://michaelheap.com/create-eks-cluster/ <p>I've been using <a href="https://github.com/hashicorp/learn-terraform-provision-eks-cluster">this</a> Terraform module to deploy test EKS clusters for the longest time, but I just learned that it's even easier when using <a href="https://eksctl.io/"><code>eksctl</code></a>.</p> <p>Create a <code>cluster.yaml</code> file with the following contents. You can change the <code>instanceType</code> and <code>desiredCapacity</code> (the number of nodes) if needed:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #8FBCBB">apiVersion</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">eksctl.io/v1alpha5</span></div><div class="line"><span style="color: #8FBCBB">kind</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">ClusterConfig</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">metadata</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">mheap-testing</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">region</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">eu-west-2</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">nodeGroups</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">ng-1</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">instanceType</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">m5.large</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">desiredCapacity</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">volumeSize</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">80</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ssh</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">allow</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span></div></code></div></pre> <p>Run <code>eksctl create cluster</code>. It takes around 4 minutes:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">eksctl create cluster -f cluster.yaml</span></div></code></div></pre> <p>Update your <code>kubeconfig</code> using the <code>aws</code> CLI:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">aws eks --region eu-west-2 update-kubeconfig --name mheap-testing</span></div></code></div></pre> <p>Now you can run all the <code>kubectl</code> commands that you want.</p> <p>To delete the cluster when you're done:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">eksctl delete cluster -f cluster.yaml</span></div></code></div></pre> How I work: Email 2023-09-09T11:06:19Z https://michaelheap.com/email-workflow/ <p>Depending on who you ask, I’m excellent with email, or terrible with email. I don’t let it run my life, and process my (work) inbox every 2-3 days.</p> <p>My review process has four steps: <em>triage</em>, <em>unsubscribe</em>, <em>respond</em> and <em>prioritise</em>.</p> <ol> <li>Triage: Read all unread emails in my inbox with the search <code>is:unread in:inbox</code>.</li> <li>If there are emails that I don’t want to receive (e.g. cold outreach) I create a filter based on the sender so that it doesn’t land in my inbox</li> <li>Things that take 2 minutes to answer are responded to immediately. Otherwise they’re labelled (not starred - more on this later) for action later</li> <li>Finally, I go through my labelled items and import them in to Sunsama to be scheduled alongside my other work.</li> </ol> <h2 id="the-klinger-email-method" tabindex="-1">The Klinger email method</h2> <p>I’ve been using a <a href="https://klinger.io/posts/how-to-use-gmail-more-efficiently">multiple inbox setup</a> from Andreas Klinger for years. Having a main inbox, then separate inboxes for high/medium priority emails allowed me to keep on top of things that I needed to do.</p> <p><strong>Go and read the <a href="https://klinger.io/posts/how-to-use-gmail-more-efficiently">Klinger email method</a> now</strong></p> <p>However, since I <a href="https://michaelheap.com/sunsama/">started using Sunsama</a> my inbox is no longer the source of truth for what I need to do. Instead, it’s an input in to my Sunsama plan.</p> <h2 id="pain-points" tabindex="-1">Pain Points</h2> <p>The Klinger method is great, but it isn’t perfect. I have two big complaints, and they both have to do with the use of custom stars.</p> <ol> <li>The mobile gmail app only supports star/unstar, not custom stars</li> <li>Sunsama only support starred/unstarred, which meant I lost my high/medium priority differentiation</li> </ol> <p>To work around this issue, I created two labels: <code>priority/high</code> and <code>priority/medium</code>. I use these for my custom inboxes instead of the red and yellow bang stars from the Klinger method. This allows me to set priorities on mobile, and filter to specific priorities within Sunsama.</p> <p>The real game changer for me was when I learned that you can customise label colours. Click on the three dots next to the label name in the sidebar and choose a label colour. I chose red for high priority and yellow for medium priority. The colours help these emails stand out when I’m scrolling through my inbox.</p> <h2 id="sunsama-integration" tabindex="-1">Sunsama Integration</h2> <p>Finally, it’s time to look at my Sunsama workflow for emails. I review emails tagged with <code>priority/high</code> or <code>priority/medium</code> on a Monday, Wednesday and Friday.</p> <p>I start with <code>priority/high</code> and schedule work on those emails until the list is empty. Once there are no more remaining, I start working through <code>priority/medium</code> emails. When the email is imported in to Sunsama, the <code>priority</code> label is removed automatically which makes Sunsama the source of truth.</p> <p>Email is one of my lowest priority task sources. I’ll always pull from Slack and GitHub before working through my email task backlog. However, everything that comes in via email <em>does</em> eventually need doing so pulling them in to Sunsama is important.</p> How I work: Sunsama 2023-09-08T16:41:16Z https://michaelheap.com/sunsama/ <p>I’m a big believer in Parkinson’s Law, which states that work will expand to fill its allotted time span. The work keeps coming, and no matter how well I prioritise my todo list it always seemed to be longer at the end of the day than it was at the beginning.</p> <p>Thankfully in the last couple of years I’ve solved this issue using Sunsama, a tool for tracking and scheduling work across multiple systems.</p> <p>Before I get started, I want to make one thing clear. This isn’t a paid post, and I don’t get anything for recommending Sunsama. I’m just a <em>very</em> happy user.</p> <h2 id="what-is-sunsama%3F" tabindex="-1">What is Sunsama?</h2> <p>Sunsama is a combined inbox for all of your different to-do systems. It sounds strange to have an inbox for your inboxes, but when work can appear in multiple different places it’s hard to keep track of. I know some people that try to make their email inbox their to-do list, but it never worked for me.</p> <p>Depending on which role I’m fulfilling, my work comes from different places. If I’m being an engineer, I’m pulling from Jira (or GitHub, depending on which team I’m working with). As a manager I’m usually responding to questions in Slack or by email. If I’m collaborating with the marketing team the tasks come via Asana, and if it’s something I’ve captured myself whilst away from my desk it’s in my work project in Todoist.</p> <p>Trying to keep track of everything I need to do across all of those systems is impossible. There’s no way to rank the tasks against each other and build a consolidated, prioritised list. That’s where Sunsama comes in. It allows me to pull in tasks from all of those systems and see them in a single screen.</p> <p>More than that, Sunsama lets me estimate how long each will take and schedule the tasks around my meetings for the day. It’s sad to see that some days I only get around 2 hours of time to work on assigned tasks, but it’s better to know that’s the case than to plan 6 hours of tasks and be disheartened when most of them are still on my to-do list at the end of the day.</p> <p>So that’s what Sunsama is for me: a consolidated inbox that allows me to schedule tasks on my calendar to keep me honest when it comes to capping my work day at 8 hours.</p> <h2 id="integrations" tabindex="-1">Integrations</h2> <p>Sunsama’s killer feature is its deep integration with other products. Once upon a time I spent most of my time pulling in Jira and GitHub tasks, but now it tends to be more Slack messages and emails. Here’s my integration tier list:</p> <ul> <li>S Tier: Slack (easily 75%+ of my tasks), Email</li> <li>A Tier: Email, Jira, Todoist</li> <li>B Tier: GitHub</li> <li>C Tier: Asana</li> </ul> <p>Sunsama also integrates with Trello, Notion, Outlook, ClickUp and Linear but I don’t use those tools and can’t talk about how well they work.</p> <p>One of the things I love about Sunsama is that you can interact with the underlying service directly in the UI. Here’s an example of how Sunsama renders a Jira ticket. It feels familiar, and allows me to see the ticket details and update the status without leaving Sunsama.</p> <div class="image-wrapper "><picture> <source class="m-auto" type="image/webp" srcset="https://michaelheap.com/images/sunsama/sunsama-jira.png/d7IveIOEX6-320.webp 320w" sizes="(max-width: 320px) 320px, 100vw" /> <img class="m-auto" alt="Sunsama Jira Example" src="https://michaelheap.com/images/sunsama/sunsama-jira.png/d7IveIOEX6-320.jpeg" sizes="(max-width: 320px) 320px, 100vw" srcset="https://michaelheap.com/images/sunsama/sunsama-jira.png/d7IveIOEX6-320.jpeg 320w" width="320" height="390" /> </picture></div> <h2 id="my-sunsama-workflow" tabindex="-1">My Sunsama Workflow</h2> <h3 id="calendar-setup" tabindex="-1">Calendar Setup</h3> <p>Sunsama works best when you schedule your work on your calendar. I prefer to keep my planned work separate from my meetings, so I created a second calendar to use with Sunsama. This gives me the ability to see everything in the Sunsama app, but only see my committed meetings in Google Calendar.</p> <h3 id="channels" tabindex="-1">Channels</h3> <p>When I first started using Sunsama I didn’t use channels. It felt like I’d spend more time categorising work than actually doing it. However, as I started to try and build blocks of focused time I found that some coarse grained categorisation was helpful.</p> <p>I don’t overdo it with channels, aiming to have as few as possible. Here’s what my current list looks like:</p> <ul> <li><code>admin</code></li> <li><code>aip</code></li> <li><code>apiops</code></li> <li><code>devrel</code></li> <li><code>docs</code></li> <li><code>kic</code></li> <li><code>product</code></li> <li><code>review</code></li> <li><code>watch</code></li> </ul> <p>Most of the channels are teams that I actively work with (<code>aip</code>, <code>apiops</code>, <code>devrel</code>, <code>docs</code>, <code>kic</code>), which leaves a couple of general areas. Anything that isn’t directly attached to a team is either <code>admin</code>, or something I need to <code>review</code> or <code>watch</code>. I recently split out <code>watch</code> as I realised that video is a much bigger time commitment than reviewing a doc.</p> <p>Having each item categorised by channel allows me to batch up my day and focus on specific areas for an extended period of time. Which brings us on to the daily plan.</p> <h3 id="daily-planning" tabindex="-1">Daily Planning</h3> <p>Each morning starts with a review of what needs to get done today. I’m a heavy user of “save for later” in Slack, so the first thing that I do is go through my saved items and send them to Sunsama using the Slack integration. This works <em>really</em> well as Sunsama attaches a link to the conversation to the item so that I can navigate back to see additional context if needed. Any items created at this point go in to the “Today” list, or in to the backlog.</p> <p>Once that’s done, I run through the “Today” list and ensure that all items have a channel set. This helps a little later on when I’m scheduling my day. If it looks like I’ll have some spare capacity during the day, I’ll try to move something from the future to today and get it done earlier (but <em>not</em> from the backlog. That happens once per week, which I’ll explain next).</p> <p>Next, I group the items by channel before pressing the “Plan” button. I try to batch up my work by area to reduce context switching, which means that I get most of my work done in the mornings and have calls in the afternoon. I add all my events to my list of tasks so that I can see how many hours I have scheduled. Today is a busy day, and I have 7h15m of work planned to complete</p> <div class="image-wrapper "><picture> <source class="m-auto" type="image/webp" srcset="https://michaelheap.com/images/sunsama/sunsama-calendar.png/RrNvuJoSqZ-320.webp 320w" sizes="(max-width: 320px) 320px, 100vw" /> <img class="m-auto" alt="Sunsama Calendar Example" src="https://michaelheap.com/images/sunsama/sunsama-calendar.png/RrNvuJoSqZ-320.jpeg" sizes="(max-width: 320px) 320px, 100vw" srcset="https://michaelheap.com/images/sunsama/sunsama-calendar.png/RrNvuJoSqZ-320.jpeg 320w" width="320" height="577" /> </picture></div> <p>At this point my day is planned and I can get started. I hover over the first item in the list, press <code>F</code> to enter focus mode then get started.</p> <h3 id="backlog-review" tabindex="-1">Backlog Review</h3> <p>Tasks that I can’t pick up in the next day or two get sent to the Sunsama backlog. Tasks in the backlog all have a channel set to help with the review process. Initially I tried to review the backlog on a Monday morning, but I found that most of the backlog tasks weren’t getting completed during the week.</p> <p>Today, I process my backlog on a Thursday when I know how well my week is going and how much time I’ll have free on Thursday/Friday. I usually only manage to get one or two tasks out of the backlog, but manage to add around 5 per week. I’m not sure how to resolve this yet, but I follow a first-in, first-out model to try and get everything processed in a reasonable time.</p> <p>When I take tasks from the backlog I try to choose tasks from a specific channel. This allows me to batch process and prevents context switching as I try to wrap things up before the end of the week.</p> <h3 id="automation" tabindex="-1">Automation</h3> <p>Sunsama’s killer feature is the number of integrations there are, but that’s just the start. What really makes it powerful are the automations that you can configure for each platform.</p> <p>Automations change the state of your task on the <em>remote</em> site based on actions within Sunsama. Here are a couple of ways that I use it:</p> <ul> <li>When a Jira task is completed, prompt me to update the Jira ticket status</li> <li>When a Gmail task is completed, unstar and archive the email in my inbox</li> <li>When a Todoist task is completed, mark it as done in Todoist</li> </ul> <p>This allows me to work entirely in Sunsama whilst still keeping external systems up to date.</p> <h3 id="focus-mode" tabindex="-1">Focus mode</h3> <p>Finally, we come to focus mode. I mentioned that I press <code>F</code> and get started with the day above, but let’s take a deeper look at focus mode. There are two ways to enter focus mode - the “Focus” button at the top of the current day, and pressing <code>F</code> whilst focused on a task.</p> <p>Clicking “Focus” at the top will hide all other days and the left hand sidebar in the UI. It leaves you with your list of tasks for the day and whatever integration you have open on the right (for me this is always the calendar). This is handy to see an “at a glance” view of your day but I don’t use it much.</p> <p>The alternative is to press <code>F</code>, which hides all sidebars and tasks except the one you were focused on. It shows the task title and metadata and brings up a small task timer (which I don’t use). I like to use this view to keep me focused on a single task, and it moves on to the next one in the list when I mark it as complete.</p> <h2 id="features-i-don%E2%80%99t-use" tabindex="-1">Features I don’t use</h2> <p>I feel like I’ve got a pretty good Sunsama workflow, but there are features that I don’t use too. Some of the features have been added recently and I just haven’t tried them out yet, whilst others don’t seem to fit my workflow.</p> <ul> <li><strong>Weekly objectives</strong>: Sunsama allows you to set key objectives for the week and associate your tasks with objectives. This keeps you focused on the strategic work. Unfortunately a lot of my work is escalation driven, so weekly objectives don’t work for me</li> <li><strong>Auto-archive</strong>: If a task keeps rolling over from day to day, Sunsama can remove it from your to-do list and move it to an archived section. I aggressively prune my task list manually so I have no use for auto-archive.</li> <li><strong>Time tracking</strong>: In addition to estimating how much time a task will take, Sunsama allows you track how much time you spend on each task. I just forget to do this.</li> <li><strong>Automatic scheduling</strong>: Sunsama can take the tasks for a day and automatically schedule them around your meetings. If you complete a task sooner than expected, it can re-plan your day automatically. I’ve disabled this feature, as I like to be in control of my own calendar. If I finish a task early, I prefer to stand up and walk around for a while</li> <li><strong>Daily shutdown</strong>: The end of day counterpart to the daily planning process. I do a lightweight version of this where I move incomplete items to the next day, but don’t review what was shipped. I do weekly reviews using a separate process outside of Sunsama with my wider team.</li> </ul> <p>At some point I’d like to be more intention with my focus and try setting <em>weekly objectives</em> and try out <em>time tracking</em> to see if I’m spending time in the right areas.</p> <h2 id="sunsama-changed-my-(work)-life" tabindex="-1">Sunsama changed my (work) life</h2> <p>As someone that used to try and get everything on their to-do list done in a single day (and was inevitably disappointed when I didn’t make it all the way through), Sunsama has changed my life.</p> <p>I’m fortunate that all the systems I interact with day to day are supported. In a previous role they migrated from Jira Cloud to Jira Server and Sunsama became much less useful for me.</p> <p>If you’re working across multiple different systems and struggling to keep track of what needs to be done, why not <a href="https://www.sunsama.com/start/signup">give Sunsama a go</a>?</p> <h2 id="useful-links" tabindex="-1">Useful links</h2> <p>The Sunsama team release loom videos of useful workflows. Here are the ones that I've found most useful</p> <ul> <li><a href="https://www.loom.com/share/f52e90a0fbb74123aa02770b571680a7">Pro-tips for dealing with multi-day tasks</a></li> </ul> GitHub Actions notifications in Slack 2023-09-05T09:04:52Z https://michaelheap.com/github-actions-slack-notifications/ <p>I recently learned that GitHub can <a href="https://michaelheap.com/github-scheduled-reminders/">send you reminders on Slack at a regularly scheduled interval</a>. What I <em>didn’t</em> know is that GitHub can also send you notifications on Slack in real-time when events that you’re interested in occur. In my case, that’s when a GitHub Actions build fails.</p> <p>Ever since GitHub Actions launched, people have been reaching for prebuilt actions (usually <a href="https://github.com/marketplace/actions/slack-notify">Slack Notify</a>) to send updates to Slack. It works great, but there are a couple of downsides to this approach:</p> <ol> <li>You need your Slack admin to generate a webhook URL for you</li> <li>Notification logic leaks in to your GitHub Actions builds</li> <li>You can only notify a single channel at a time</li> </ol> <p>In December 2022, GitHub launched support for <a href="https://github.blog/changelog/2022-12-06-github-actions-workflow-notifications-in-slack-and-microsoft-teams/">GitHub Actions workflow notifications in Slack and Microsoft Teams</a>, which allows you to configure Slack as a webhook receiver for GitHub Actions workflow notifications.</p> <p>Notifications are configured on a per-channel basis using the <code>/github subscribe</code> command in Slack. To get started:</p> <ol> <li>Invite <code>@github</code> to the channel you’d like to receive notifications in</li> <li>Run <code>/github subscribe owner/repo</code></li> </ol> <h2 id="real-world-usage" tabindex="-1">Real World Usage</h2> <p>Here’s a real world example that we use to be notified about our scheduled broken links checker, which runs once per week:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">/github subscribe Kong/docs.konghq.com workflows:{name:</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Scheduled Broken Links Checker</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">}</span></div></code></div></pre> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">✅ Subscribed to Kong/docs.konghq.com. This channel will receive notifications </span><span style="color: #81A1C1">for</span></div><div class="line"><span style="color: #D8DEE9FF">issues, pulls, commits, releases, deployments, workflows:{name:</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Scheduled Broken Links Checker</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">}</span></div></code></div></pre> <p>That’s all! As you can see, you’ll get notifications for <code>issues</code>, <code>pulls</code>, <code>commits</code> (only on the default branch), <code>releases</code> and <code>deployments</code> by default. That’s a bit too much for me, and I unsubscribe from all of these notifications using the following:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">/github unsubscribe Kong/docs.konghq.com issues pulls commits releases deployments</span></div></code></div></pre> <p>Now the channel will only receive notifications for the named workflow:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">bash</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">This channel will receive notifications from Kong/docs.konghq.com for:</span></div><div class="line"><span style="color: #D8DEE9FF">workflows:{name:</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Scheduled Broken Links Checker</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">}</span></div></code></div></pre> <p>You can filter the notifications by workflow name, the event on which the workflow is triggered, the person that triggered the workflow (useful to review your CEO’s pull requests quickly! 😉), or the branch on which the workflow is running. For a full set of documentation, see the <a href="https://github.com/integrations/slack#workflow-notification-filters">GitHub workflow notification filters docs</a>.</p> <p>I feed these notifications in to a personal, private channel so that they don’t impact the rest of the team. I couldn’t do that as easily if we were using <code>notify-slack</code> directly in the pipeline.</p> <h2 id="pull-request-notifications" tabindex="-1">Pull Request Notifications</h2> <p>Our main use case for the GitHub notifications in Slack is for workflow success/failure, but there is one other trick we use that I’d like to share.</p> <p>We use labels heavily to categorise pull requests and control CI checks (including <a href="https://github.com/mheap/github-action-required-labels">making labels required</a>). Here are a couple of filters I use to make sure that I don’t miss anything:</p> <ul> <li><code>/github subscribe Kong/docs.konghq.com +label:&quot;review:tech&quot;</code> - Makes sure that I see any docs platform code changes</li> <li><code>/github subscribe Kong/docs.konghq.com +label:&quot;review:sme&quot;</code> - These are usually really interesting docs to read and learn from. If a topic needs SME (subject matter expert) review, there’s something for me to learn.</li> <li><code>/github subscribe Kong/docs.konghq.com +label:&quot;ci:manual-approve:linting&quot;</code> - Alert whenever the <code>linting</code> CI check is skipped. We have multiple <code>manual-approve</code> labels and I subscribe to all of them. You can see how we override workflows manually using labels in <a href="https://github.com/Kong/docs.konghq.com/blob/main/.github/workflows/linting.yml#L7-L25">the linting workflow</a>.</li> </ul> <h2 id="give-it-a-go" tabindex="-1">Give it a go</h2> <p>The GitHub Slack integration is pretty great. Whether you’re looking for GitHub Actions notifications, to be alerted when pull requests have a specific label or just to be <a href="https://michaelheap.com/github-scheduled-reminders/">reminded about PRs that need your review</a>, I recommend trying them out to see what works for you.</p> Accessing secrets from forks safely with GitHub Actions 2023-09-04T14:05:28Z https://michaelheap.com/access-secrets-from-forks/ <p>It feels like I see someone asking “how do I let pull requests from forks access my GitHub Actions secrets?” every other day. GitHub prevents PRs from forks from accessing secrets by default so that they can’t exfiltrate secrets and use them for malicious purposes.</p> <p>There are posts out there that show you how to use <code>pull_request_target</code> with <code>actions/checkout</code> to check out the <code>HEAD</code> ref, but <a href="https://securitylab.github.com/research/github-actions-preventing-pwn-requests/">this is insecure</a> by default as it exposes all of your secrets to anyone that can raise a pull request against a repo (which is <em>everyone</em> on a public repo).</p> <p>So, how do you get the best of both worlds? How can you safely provide secrets to forks without malicious actors stealing them?</p> <p>GitHub Actions provides a <em>triggering actor</em> field that lets you know who ran (or re-ran) a workflow. This allows maintainers to provide access to secrets on forks once they’ve checked the changes manually and are happy that they don’t pose a security risk by re-running a job.</p> <blockquote> <p>If you’re working in a private repo and want to enable secrets for forks, you can <a href="https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/">make them available</a> via repo settings. This post is only relevant for public repos.</p> </blockquote> <p>Here’s what the workflow would look like:</p> <ul> <li>Alice opens a pull request from her fork</li> <li>GitHub Actions runs and fails the build as she doesn’t have permissions on the repo</li> <li>Bob reviews the PR and decides that there’s nothing dangerous</li> <li>Bob re-runs the failed job</li> <li>GitHub Actions runs and the access check passes as Bob has <code>write</code> access to the repo</li> <li><code>github.event.pull_request.head.sha</code> is checked out and points to Alice’s changes</li> <li>The rest of the workflow runs successfully with access to any repository secrets</li> </ul> <p>So in short:</p> <ol> <li>Check permissions</li> <li>Checkout code</li> <li>Run tests with secrets</li> </ol> <h2 id="check-collaborator-permissions" tabindex="-1">Check Collaborator Permissions</h2> <p>GitHub not trusting pull requests from forks is a proxy for “do we trust this person?”. Rather than asking the question “is the PR from a fork”, why don’t we check what permission the actor triggering the job has?</p> <p>There are two ways to check permissions - <em>author association</em> or <em>user permissions</em>. Author association tells us if they have any permissions on the repository (they’ll be marked as a <code>collaborator</code> no matter what permissions they have). User permissions tells us exactly what permissions they have on the repository.</p> <blockquote> <p>If you want to learn more about author associations vs user permissions see <a href="https://michaelheap.com/github-actions-check-permission/">Check permissions in a GitHub Actions workflow</a></p> </blockquote> <p>If you’re not sure which to choose, I recommend checking the actor’s permissions rather than relying on author association.</p> <h3 id="author-association" tabindex="-1">Author Association</h3> <p>The author association is available by default in the pull request event. This means that you can check permissions directly in a workflow without using the GitHub API:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Check access</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">if</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ github.event.pull_request.author_association != 'COLLABORATOR' && github.event.pull_request.author_association != 'OWNER' }}</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">run</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #A3BE8C"> echo "Event not triggered by a collaborator."</span></div><div class="line"><span style="color: #A3BE8C"> exit 1</span></div></code></div></pre> <p>This step would check if the author of the PR has <em>any</em> access to the repo. If they do not, the workflow will exit with error code 1.</p> <h3 id="user-permission" tabindex="-1">User Permission</h3> <p>Alternatively, you can check if the actor has specific permissions using a GitHub Action. I personally use the following action:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Get User Permission</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">checkAccess</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">uses</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">actions-cool/check-user-permission@v2</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">with</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">require</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">write</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">username</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ github.triggering_actor }}</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">env</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">GITHUB_TOKEN</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ secrets.GITHUB_TOKEN }}</span></div><div class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Check User Permission</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">if</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">steps.checkAccess.outputs.require-result == 'false'</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">run</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #A3BE8C"> echo "${{ github.triggering_actor }} does not have permissions on this repo."</span></div><div class="line"><span style="color: #A3BE8C"> echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}"</span></div><div class="line"><span style="color: #A3BE8C"> echo "Job originally triggered by ${{ github.actor }}"</span></div><div class="line"><span style="color: #A3BE8C"> exit 1</span></div></code></div></pre> <p>The action returns outputs that you can then use to determine what to do next. I check the permission in the second step above and print a debugging message before exiting with an error code of 1 to fail the build.</p> <p>You might notice that I use <code>github.triggering_actor</code> rather than <code>github.actor</code>. This is what allows us to run tests from forks if the job is re-run by someone with the correct permission level.</p> <p>When checking for permissions, the available levels are <code>none</code>, <code>read</code>, <code>write</code> and <code>admin</code>. Although <code>triage</code> and <code>maintain</code> are permissions in GitHub itself, they are not reflected in the API.</p> <h2 id="check-out-the-new-code" tabindex="-1">Check out the new code</h2> <p>At this point it’s safe to check out the code as the job has been triggered by someone with <code>write</code> access.</p> <p>I mentioned earlier that using <code>actions/checkout</code> with <code>github.event.pull_request.head.sha</code> is insecure. However, as the PR has been reviewed and the workflow run has been triggered by someone with <code>write</code> access we can be a little more trusting.</p> <p>Here’s how to check out the code from the PR in your workflow by explicitly setting a <code>ref</code>:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Checkout code</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">uses</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">actions/checkout@v3</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">with</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ github.event.pull_request.head.sha }}</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># This is dangerous without the first access check</span></div></code></div></pre> <h2 id="run-the-tests" tabindex="-1">Run the tests</h2> <p>Your workflow will be different to mine at this point. If you’re testing if you’ve configured things correctly, you can set a secret named <code>MY_SECRET</code> to <code>val</code> and use the following step:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Test</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">run</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #A3BE8C"> if [[ "x${{ secrets.MY_SECRET }}" == "xval" ]]; then</span></div><div class="line"><span style="color: #A3BE8C"> echo "Access to secrets"</span></div><div class="line"><span style="color: #A3BE8C"> else</span></div><div class="line"><span style="color: #A3BE8C"> echo "No access to secrets"</span></div><div class="line"><span style="color: #A3BE8C"> exit 1</span></div><div class="line"><span style="color: #A3BE8C"> fi</span></div></code></div></pre> <h2 id="putting-it-all-together" tabindex="-1">Putting it all together</h2> <p>Taking all of the above and combining it in to a single workflow gives us the following:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">yaml</div><div class="code-container"><code><div class="line"><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Run tests from fork</span></div><div class="line"><span style="color: #81A1C1">on</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">pull_request_target</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">types</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #A3BE8C">opened</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">synchronize</span><span style="color: #ECEFF4">]</span></div><div class="line"><span style="color: #8FBCBB">jobs</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">demo</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">runs-on</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">ubuntu-latest</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">steps</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Get User Permission</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">checkAccess</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">uses</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">actions-cool/check-user-permission@v2</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">with</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">require</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">write</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">username</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ github.triggering_actor }}</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">env</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">GITHUB_TOKEN</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ secrets.GITHUB_TOKEN }}</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Check User Permission</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">if</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">steps.checkAccess.outputs.require-result == 'false'</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">run</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #A3BE8C"> echo "${{ github.triggering_actor }} does not have permissions on this repo."</span></div><div class="line"><span style="color: #A3BE8C"> echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}"</span></div><div class="line"><span style="color: #A3BE8C"> echo "Job originally triggered by ${{ github.actor }}"</span></div><div class="line"><span style="color: #A3BE8C"> exit 1</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Checkout code</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">uses</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">actions/checkout@v3</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">with</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">${{ github.event.pull_request.head.sha }}</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># This is dangerous without the first access check</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">Run tests</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">run</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #A3BE8C"> if [[ "x${{ secrets.MY_SECRET }}" == "xval" ]]; then</span></div><div class="line"><span style="color: #A3BE8C"> echo "Access to secrets"</span></div><div class="line"><span style="color: #A3BE8C"> else</span></div><div class="line"><span style="color: #A3BE8C"> echo "No access to secrets"</span></div><div class="line"><span style="color: #A3BE8C"> exit 1</span></div><div class="line"><span style="color: #A3BE8C"> fi</span></div></code></div></pre> <p>So there we have it - a mostly safe way to run workflows with access to secrets when a pull request is raised from a fork.</p> <p>Why mostly safe rather than 100% safe? It relies on humans to read the pull request diff and make a good decision. We’re all human and mistakes happen. At some point a PR will get run that shouldn’t have been, and you’ll need to rotate your secrets.</p> <p>I think it’s worth it. Being able to run your full suite of tests with secrets on PRs from forks is key to delivering robust software. If secrets get leaked by mistake, well, it’s always good to test your rotation procedure periodically 😁.</p>