michaelheap.com Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order) 2023-09-09T11:06:19Z https://michaelheap.com Michael Heap m@michaelheap.com 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> 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> Setting `created_at` with GitHub Actions when a PR is merged 2023-08-21T07:37:42Z https://michaelheap.com/update-created-date-github-actions/ <p>The blog that you’re reading is a static site that’s generated on every push to <code>main</code>. That means that whatever’s in the repo is the source of truth, including the <code>created_at</code> date when a PR is merged.</p> <p>I do a lot of my writing in batches, but don’t want to publish everything at the same time. Rather than keeping finished posts in Ulysses (my writing app of choice) and staging a new post every time I want to publish, I raise a pull request immediately and use GitHub Actions to set the <code>created_at</code> date when publishing a post.</p> <p>Here’s the rough workflow:</p> <ul> <li>Check if the <code>submit-post</code> label has been added</li> <li>Fetch the list of changed files in the PR</li> <li>For each <code>added</code> file, fetch the file contents</li> <li>Replace the <code>created_at</code> date (it’s called <code>date</code> in the frontmatter) in each file (using <code>.replace()</code> but I <em>should</em> probably use <code>gray-matter</code>)</li> <li>Use <code>github.repos.createOrUpdateFileContents</code> to update the contents of each file</li> <li>Use <code>github.pulls.merge</code> with <code>merge_method: &quot;squash&quot;</code> to merge the PR as a single commit (<code>createOrUpdateFileContents</code> adds multiple commits)</li> <li>Delete the branch</li> </ul> <p>I could write a new GitHub Action to do this, but as it’s not reusable and it’s only a small number of lines I’ve chosen to use <code>actions/github-script</code> which lets me write JavaScript and use the GitHub API easily.</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">js</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">on</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> pull_request</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> types</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">labeled</span></div><div class="line"><span style="color: #D8DEE9FF">jobs</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">merge</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">label</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">runs</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">on</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ubuntu</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">latest</span></div><div class="line"><span style="color: #D8DEE9FF"> steps</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> uses</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">actions</span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9">github</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">script</span><span style="color: #D8DEE9FF">@</span><span style="color: #D8DEE9">v4</span></div><div class="line"><span style="color: #D8DEE9FF"> id</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">set</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">result</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">with</span><span style="color: #D8DEE9FF">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">token</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">$</span></div><div class="line"><span style="color: #D8DEE9FF"> script</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">|</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">labelName</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">payload</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">label</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">name</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> (</span><span style="color: #D8DEE9">labelName</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">submit-post</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">console</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">log</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">`</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">labelName</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C"> label was added. No action needed</span><span style="color: #ECEFF4">`</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">data</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">files</span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">pulls</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">listFiles</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">pull_number</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">issue</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">number</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">owner</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">repo</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</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: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Load all new files + their contents</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">newPosts</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Promise</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">all</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">files</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">filter</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">f</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">status</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">added</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&&</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">filename</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">endsWith</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">.md</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: #D8DEE9FF">)</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">map</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">blob</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">request</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">`</span><span style="color: #A3BE8C">GET </span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">contents_url</span><span style="color: #81A1C1">}</span><span style="color: #ECEFF4">`</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">content</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Buffer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">blob</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">base64</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toString</span><span style="color: #D8DEE9FF">()</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #88C0D0">sha</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">sha</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">filename</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">filename</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">content</span><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF">))</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Replace the date + updated_at with the current date</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">now</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> (</span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Date</span><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toISOString</span><span style="color: #D8DEE9FF">()</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Promise</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">all</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">newPosts</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">map</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">replace</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">/</span><span style="color: #EBCB8B">date: "</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">^</span><span style="color: #EBCB8B">"</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">+</span><span style="color: #EBCB8B">"</span><span style="color: #ECEFF4">/</span><span style="color: #81A1C1">mi</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #A3BE8C">date: "</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">now</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">`</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">replace</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">/</span><span style="color: #EBCB8B">updated_at: "</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">^</span><span style="color: #EBCB8B">"</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">+</span><span style="color: #EBCB8B">"</span><span style="color: #ECEFF4">/</span><span style="color: #81A1C1">mi</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #A3BE8C">updated_at: "</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">now</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">`</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repos</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">createOrUpdateFileContents</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">owner</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">repo</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">path</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">filename</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">message</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Update date</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">branch</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">payload</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">pull_request</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">head</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ref</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sha</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">sha</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">content</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Buffer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">f</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">content</span><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toString</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">base64</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">committer</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Michael Heap</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">email</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">michael@example.com</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF"> </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: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF">))</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Wait 10 seconds for changes to propogate</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Promise</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">resolve</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">setTimeout</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">resolve</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">10000</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Squash and merge the PR</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">await</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">pulls</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">merge</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">pull_number</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">issue</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">number</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">owner</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">repo</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">merge_method</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">squash</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: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Delete the branch</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">github</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">git</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">deleteRef</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">repo</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">owner</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">repo</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">ref</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #A3BE8C">heads/</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">payload</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">pull_request</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">head</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ref</span><span style="color: #81A1C1">}</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: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">result</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">encoding</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">string</span></div></code></div></pre> <p>This action allows me to stage posts when I finish writing them rather than when I plan to publish. Once my intended publishing date arrives, I add the <code>submit-post</code> label to the PR (usually via my phone) and the post is published as intended.</p> kubectl autocomplete with ZSH and aliases 2023-08-19T12:12:20Z https://michaelheap.com/kubectl-alias-autocomplete/ <p>This took me way too long to figure out.</p> <p>How to make the <code>k</code> alias for <code>kubectl</code> autocomplete in <code>zsh</code>:</p> <ol> <li>Ensure that <code>setopt COMPLETE_ALIASES</code> is not set (I’m not sure why) TODO: CHECK THIS</li> <li>Define your alias <code>alias k=kubectl</code></li> <li>Load the <code>kubectl</code> completions with<code>source &lt;(kubectl completion zsh) </code></li> <li>Run <code>compdef k='kubectl'</code> to associate the autocomplete for <code>kubectl</code> with <code>k</code></li> </ol> <p>Here’s a complete <code>.zshrc</code> that works for me:</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">autoload -Uz compinit</span></div><div class="line"><span style="color: #D8DEE9FF">compinit</span></div><div class="line"></div><div class="line"><span style="color: #88C0D0">alias</span><span style="color: #D8DEE9FF"> k=kubectl</span></div><div class="line"><span style="color: #88C0D0">source</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&lt;(</span><span style="color: #A3BE8C">kubectl completion zsh</span><span style="color: #ECEFF4">)</span></div><div class="line"><span style="color: #D8DEE9FF">compdef k=</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">kubectl</span><span style="color: #ECEFF4">'</span></div></code></div></pre> <p>PS: Make sure that the aliases are set <em>before</em> calling <code>compdef</code>. That’s an hour of my time I’m never getting back.</p> Spider-shaped DevRel 2023-08-19T12:03:12Z https://michaelheap.com/spider-shaped-devrel/ <p>The “T-shaped people” theory states that people generally have a wide understanding of an area, or a deep one. It’s very rare that you find someone that is both wide <em>and</em> deep at the same time.</p> <div class="grid grid-cols-4 gap-x-2"> <div class="col-span-1"> <strong>Deep:</strong> <div class="image-wrapper "><picture> <source class="m-auto" type="image/webp" srcset="https://michaelheap.com/images/spider-shaped-devrel/deep-t.png/O5PtZbcgxw-311.webp 311w" sizes="(max-width: 311px) 311px, 100vw" /> <img class="m-auto" alt="First team discord illustration" src="https://michaelheap.com/images/spider-shaped-devrel/deep-t.png/O5PtZbcgxw-311.jpeg" sizes="(max-width: 311px) 311px, 100vw" srcset="https://michaelheap.com/images/spider-shaped-devrel/deep-t.png/O5PtZbcgxw-311.jpeg 311w" width="311" height="540" /> </picture></div> </div> <div class="col-span-3"> <strong>Wide:</strong> <div class="image-wrapper "><picture> <source class="m-auto" type="image/webp" srcset="https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-320.webp 320w, https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-640.webp 640w, https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="m-auto" alt="First team discord illustration" src="https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-320.jpeg 320w, https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-640.jpeg 640w, https://michaelheap.com/images/spider-shaped-devrel/wide-t.png/djA6QLSrVN-960.jpeg 960w" width="960" height="201" /> </picture></div> </div> </div> <p>When it comes to Developer Relations, I’ve found that most companies expect us to be spider-shaped. Slightly less wide than a lot of professions, and slightly deeper than most others:</p> <div class="w-2/3 m-auto"> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-320.webp 320w, https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-640.webp 640w, https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="First team discord illustration" src="https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-320.jpeg 320w, https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-640.jpeg 640w, https://michaelheap.com/images/spider-shaped-devrel/spider-t.png/uvejIOHFtr-960.jpeg 960w" width="960" height="306" /> </picture></div> </div> <h2 id="examples" tabindex="-1">Examples</h2> <p>Working in DevRel, you need to fulfil multiple roles. You might build a demo, provide UX feedback, think about product integrations, manage a community, give presentations, build marketing messages, write blog posts and talk to customers. You’re not expected to be the world’s best engineer, or writer, or marketeer. You’re expected to know the most impactful 20% of each of those roles.</p> <div class="w-2/3 m-auto"> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-320.webp 320w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-640.webp 640w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-960.webp 960w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-1200.webp 1200w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, (max-width: 1200px) 1200px, 100vw" /> <img class="" alt="First team discord illustration" src="https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-1200.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, (max-width: 1200px) 1200px, 100vw" srcset="https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-320.jpeg 320w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-640.jpeg 640w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-960.jpeg 960w, https://michaelheap.com/images/spider-shaped-devrel/spider-t-label.png/-khogBlBUQ-1200.jpeg 1200w" width="1200" height="383" /> </picture></div> </div> <h3 id="engineering" tabindex="-1">Engineering</h3> <p>As an engineer, it’s likely that you need to know your language in depth. You need to know the ecosystem, how to write unit tests, the best way to deploy your code. You need to know which package to depend on, and which to avoid. You need to ship robust code.</p> <p>As a developer advocate, you have to ship a demo that works. It usually doesn’t even have to work in 6 months time. You’re showing a capability <em>today</em>.</p> <h3 id="marketing" tabindex="-1">Marketing</h3> <p>Working in marketing, you need to understand MQLs/SQLs, attribution, drip campaigns, marketing automation tools, copywriting, SEO and paid advertising.</p> <p>As a developer advocate, you just need to know what messaging resonates with your target audience. There’s usually someone to help you get it out there.</p> <h3 id="product" tabindex="-1">Product</h3> <p>Being a product manager is a lot more involved than “tell engineering what to build”. You have to watch market trends, deal with customer escalations, build requirements documents and figure out pricing and packaging for your product.</p> <p>As a developer advocate, you have to provide product feedback and suggest integration opportunities with other parts of the ecosystem.</p> <h2 id="be-a-spider" tabindex="-1">Be a spider</h2> <p>You know more than most people about a wide range of topics. You know less than most people about their specific topic. Being a great developer advocate is about maximising the impact you can have with that 20% knowledge.</p> <p>Be a spider. Get involved in as many cross functional initiatives as you can and help get the ball rolling. You can connect the dots in a way that very few other people can. Then once things are going smoothly, get out and leave it to the professionals. There are always more projects that need a hand to get started.</p> Show all issues for an epic in Confluence 2023-07-16T19:27:51Z https://michaelheap.com/jira-epic-confluence/ <p>I’ve been working on go-to-market kits for large features recently, and needed to show all of the issues included in a specific epic on a Confluence page. It took me longer than I’d like to work out, so here’s how to do it:</p> <ul> <li>Insert a Jira Issue / Filter</li> <li>Use the filter <code>&quot;Epic Link&quot;=EPIC-123 ORDER BY createdDate ASC </code>, customising the epic name</li> </ul> <p>That’s all there is to it. You can now see the progress towards your epic alongside all of your other information in Confluence.</p> Search for inactive users on Jira 2023-07-15T21:09:25Z https://michaelheap.com/inactive-jira-users/ <p>It’s unfortunate, but people leave teams all of the time. I inherited a team that had some turnover, and needed to get the backlog in to shape.</p> <p>The first thing I did was to remove all deactivated users from tickets. It turns out that Jira allows you search for all members of a specific team. Combining this with <code>assignee not in</code> gives us the following:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">sql</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">project </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">MYPROJ</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">AND</span><span style="color: #D8DEE9FF"> assignee </span><span style="color: #81A1C1">not</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> (membersOf(jira</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">software</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">users)) </span><span style="color: #81A1C1">AND</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">status</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">Open</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">ORDER BY</span><span style="color: #D8DEE9FF"> created </span><span style="color: #81A1C1">DESC</span></div></code></div></pre> <p>I also found it useful to find tickets raised by those no longer with the company to be triaged and closed if outdated:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">sql</div><div class="code-container"><code><div class="line"><span style="color: #D8DEE9FF">project </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">MYPROJ</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">AND</span><span style="color: #D8DEE9FF"> reporter </span><span style="color: #81A1C1">not</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> (membersOf(jira</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">software</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">users)) </span><span style="color: #81A1C1">AND</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">status</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">Open</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">ORDER BY</span><span style="color: #D8DEE9FF"> created </span><span style="color: #81A1C1">DESC</span></div></code></div></pre> <p>Removing outdated tickets that I couldn’t follow up on helped trim the backlog massively, and unassigning tickets from those that have left the team means that the backlog matches reality.</p> Providing both default and named exports in JavaScript 2023-07-15T21:00:29Z https://michaelheap.com/default-named-exports/ <p>I always wondered how some NPM packages provided a default export in addition to named exports. Something like this:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">js</div><div class="code-container"><code><div class="line"><span style="color: #616E88">// Provide the most common use case</span></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validator</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">require</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">my-validator</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #616E88">// Then you can use validator() or one of the components such as validateEmail()</span></div><div class="line"></div><div class="line"><span style="color: #616E88">// Validate everything in one</span></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">isValid</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">validator</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">payload</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #616E88">// Or validate specific things</span></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">isValidEmail</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validator</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">validateEmail</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">payload</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">email</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div></code></div></pre> <p>Then one day I was using <a href="https://michaelheap.com/default-named-exports/#">nock</a> to write some tests and noticed that they do the same thing:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">js</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">nock</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">require</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">nock</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9">nock</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">disableNetConnect</span><span style="color: #D8DEE9FF">()</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">scope</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">nock</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">http://www.example.com</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: #88C0D0">get</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">/resource</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: #88C0D0">reply</span><span style="color: #D8DEE9FF">(</span><span style="color: #B48EAD">200</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">path matched</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div></code></div></pre> <p>So, <a href="https://github.com/nock/nock/blob/main/index.js#L21-L52">I went digging in the code</a> and found that it can be accomplished by setting <code>module.exports</code> to be a function, then using <code>Object.assign</code> to attach new method to the function prototype:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">js</div><div class="code-container"><code><div class="line"><span style="color: #616E88">// my-validator/main.js</span></div><div class="line"></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validateEmail</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">require</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">./validators/email</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validatePhoneNumber</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">require</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">./validators/phone</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validateAddress</span><span style="color: #ECEFF4">,</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">require</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">./validators/address</span><span style="color: #ECEFF4">'</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">module</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">exports</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">function</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">payload</span><span style="color: #ECEFF4">){</span></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88">// Do validation</span></div><div class="line"><span style="color: #ECEFF4">}</span></div><div class="line"></div><div class="line"><span style="color: #8FBCBB">Object</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">assign</span><span style="color: #D8DEE9FF">(</span><span style="color: #8FBCBB">module</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">exports</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validateEmail</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validatePhoneNumber</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">validateAddress</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div></code></div></pre> <p>Now that I’ve seen it in action it makes total sense. Using named exports allows you to export useful functions from your package without people needing to require specific files (which you may choose to move in the future). <code>Object.assign</code> expands your public API, providing additional value to consumers but also additional maintenance burden to you.</p> <p>Now you know how to expose multiple functions from a single package as part of your public API. Now, you just need to decide what you want to expose as your public API 😀.</p>