michaelheap.com Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order) 2023-05-23T12:06:32Z https://michaelheap.com Michael Heap m@michaelheap.com Take a screenshot of a video using ffmpeg 2023-05-23T12:06:32Z https://michaelheap.com/screenshot-video-ffmpeg/ <p>I needed to extract a frame at the same time for multiple videos (the title slide for a TV show) to help make sure that the filenames reflected the episode the file contained. <code>ffmpeg</code> made it super-easy.</p> <p>To take a single screenshot:</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">ffmpeg -ss 00:00:51 -i video.mp4 -frames:v 1 -q:v 2 output.jpg</span></div></code></div></pre> <p>To take a screenshot of all videos in a folder and name the output file based on the input file:</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: #81A1C1">for</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">i</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">`</span><span style="color: #A3BE8C">ls</span><span style="color: #ECEFF4">`</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">do</span><span style="color: #D8DEE9FF"> ffmpeg -ss 00:00:51 -i </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">i</span><span style="color: #D8DEE9FF"> -frames:v 1 -q:v 2 /tmp/screenshots/</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">i</span><span style="color: #D8DEE9FF">.jpg</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">done</span></div></code></div></pre> GitHub Scheduled Reminders 2023-05-22T08:32:31Z https://michaelheap.com/github-scheduled-reminders/ <p>I’ve always struggled to manage my GitHub notifications. Having them pushed to me in real-time is overwhelming, but having a separate inbox to check means that I never remember to look at them.</p> <p>GitHub notifications can be separated in to two categories for me, periodic and real-time. Periodic are things that need doing, but not right now (a PR is ready to review, for example). Real-time are items where there is a real human interacting with you (someone’s commented or requested changes on your PR).</p> <p>It turns out that GitHub launched <a href="https://github.blog/2020-04-21-stay-on-top-of-your-code-reviews-with-scheduled-reminders/">scheduled reminders</a> back in 2020 which is a great fit for this use case. Scheduled reminders allow you to receive reminders at specific times throughout the day in addition to real-time alerts for specific events. You can configure them for yourself, or for a specific GitHub team.</p> <h2 id="personal-reminders" tabindex="-1">Personal reminders</h2> <p>Personal reminders are linked to your personal GitHub account. That means you configure them in your personal GitHub settings and receive them as a DM from the GitHub bot. Head to your <a href="https://github.com/settings/reminders">GitHub settings</a> to configure them. You’ll be presented with a list of organisations. Once you choose an org it will ask you to authorize a Slack workspace and configure the days/times that you'd like to be reminded.</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-640.webp 640w, https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder configuration page" src="https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-640.jpeg 640w, https://michaelheap.com/images/github-scheduled-reminders/new-scheduled-reminder.png/ETDoQRZy0q-960.jpeg 960w" width="960" height="555" /> </picture></div> <p>I've found that 8:30am and 2:30pm on weekdays is a good schedule for me. It allows me to review PRs before getting back to focused work when I start my day / after lunch. I also enable review requests waiting on my team in addition to those that are assigned specifically to me. This takes care of the period reminders that there are still PR reviews outstanding.</p> <p>The next thing to configure is real-time alerts. I try to focus on things that need immediate attention, which means I disable notifications when I'm assigned a review (I'll see it in the periodic reminders). Here are the alerts that I <em>do</em> enable:</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-640.webp 640w, https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - real-time events" src="https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-640.jpeg 640w, https://michaelheap.com/images/github-scheduled-reminders/real-time-events.png/vIbYHTe7fk-960.jpeg 960w" width="960" height="399" /> </picture></div> <p>I'd also like to enable &quot;Your PR has failed CI checks&quot; but it requires a list of failed check names. I'd love to see the ability to get notified on all check failures so that I can keep my open PRs moving.</p> <p>Once you press &quot;Create reminder&quot; you'll see your selected reminder times and the connected Slack workspace on your reminders overview:</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-640.webp 640w, https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - fully configured reminder" src="https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-640.jpeg 640w, https://michaelheap.com/images/github-scheduled-reminders/configured-personal.png/ZKSpZbQ0Zc-960.jpeg 960w" width="960" height="80" /> </picture></div> <h2 id="team-reminders" tabindex="-1">Team reminders</h2> <p>You can also configure team reminders in addition to personal reminders. The settings are a little bit different as team reminders don't allow for real-time alerts. They <em>do</em> however have more filters available to control which PRs are shown in the reminder.</p> <p>To configure team reminders, head to <a href="https://github.com/orgs/YOUR_ORGKong/teams">https://github.com/orgs/YOUR_ORGKong/teams</a> and select the team you'd like to receive reminders for. Click <code>Settings</code> in the top right, then <code>Scheduled reminders</code> on the left. You can configure multiple reminders, each going to a different channel. This is useful if you want to direct specific repos to specific channels.</p> <p>The settings I always enable here are &quot;Ignore drafts&quot; and &quot;Require review requests&quot;. This makes sure that only PRs that can be reviewed are in the list of reminders. If you use &quot;require review requests&quot;, make sure that you use the <code>CODEOWNERS</code> file to trigger review requests for your team.</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-640.webp 640w, https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - team reminders" src="https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-640.jpeg 640w, https://michaelheap.com/images/github-scheduled-reminders/team-reminders.png/GkAVmBVghX-960.jpeg 960w" width="960" height="1650" /> </picture></div> <p>There are some other useful configuration options in here too:</p> <ul> <li><strong>Ignore approved pull requests</strong> - Only remind the team about PRs that have not yet been approved. I set this to &quot;Ignore with 2 or more&quot; as the PR requires approval from two teams</li> <li><strong>Minimum staleness</strong> - This controls how long a PR has to be inactive before you're reminded about it. We set this to 0 hours, but it could be useful if you want to see stale PRs.</li> <li><strong>Ignored labels</strong> - This is useful if your PR is awaiting changes from the author. You can add a <code>waiting-for-author</code> label (or use the <a href="https://github.com/marketplace/actions/issue-management">Issue Management GitHub Action</a>) and exclude any PRs that have that label</li> <li><strong>Required labels</strong> - The opposite of the entry above. Some of our PRs have a review scope (e.g. copyedit, tech or SME - that's subject matter expert) to indicate what we should be looking for. We use this option to push <code>review:tech</code> items to a separate channel as the audience for these PRs is much smaller.</li> </ul> <h2 id="what-does-it-look-like%3F" tabindex="-1">What does it look like?</h2> <p>Once you've configured your reminders, you'll start to receive notifications in Slack. These notifications will be broken down by repository, and contain additional metadata such as how old the PR is and how long it's been inactive. Here are a couple of examples.</p> <h3 id="example%3A-personal-reminders" tabindex="-1">Example: Personal Reminders</h3> <p>Personal reminders will be sent to you via DM:</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-personal-reminder.png/ExXV8LXHYf-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/example-personal-reminder.png/ExXV8LXHYf-640.webp 640w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - personal example" src="https://michaelheap.com/images/github-scheduled-reminders/example-personal-reminder.png/ExXV8LXHYf-640.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-personal-reminder.png/ExXV8LXHYf-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/example-personal-reminder.png/ExXV8LXHYf-640.jpeg 640w" width="640" height="296" /> </picture></div> <h3 id="example%3A-team-reminders" tabindex="-1">Example: Team Reminders</h3> <p>Team reminders will be sent to a specific channel:</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--640.webp 640w, https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--960.webp 960w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - personal example" src="https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--960.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 960px) 960px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--640.jpeg 640w, https://michaelheap.com/images/github-scheduled-reminders/example-team-reminder.png/aeCfxSWh6--960.jpeg 960w" width="960" height="137" /> </picture></div> <p>If your team have linked their Slack and GitHub accounts, the notification will show their Slack handle rather than their GitHub handle:</p> <div class="image-wrapper "><picture> <source class="" type="image/webp" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-team-linked.png/DUNHTQRQVv-320.webp 320w, https://michaelheap.com/images/github-scheduled-reminders/example-team-linked.png/DUNHTQRQVv-640.webp 640w" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" /> <img class="" alt="GitHub Scheduled Reminder - personal example" src="https://michaelheap.com/images/github-scheduled-reminders/example-team-linked.png/DUNHTQRQVv-640.jpeg" sizes="(max-width: 320px) 320px, (max-width: 640px) 640px, 100vw" srcset="https://michaelheap.com/images/github-scheduled-reminders/example-team-linked.png/DUNHTQRQVv-320.jpeg 320w, https://michaelheap.com/images/github-scheduled-reminders/example-team-linked.png/DUNHTQRQVv-640.jpeg 640w" width="640" height="310" /> </picture></div> <h2 id="further-reading" tabindex="-1">Further reading</h2> <ul> <li><a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/managing-your-scheduled-reminders">Managing your scheduled reminders</a></li> <li><a href="https://docs.github.com/en/organizations/organizing-members-into-teams/managing-scheduled-reminders-for-your-team">Managing scheduled reminders for your team</a></li> <li><a href="https://github.blog/changelog/2022-10-13-scheduled-reminders-updates-in-github-app-for-microsoft-teams/">Scheduled reminders in Microsoft Teams</a></li> </ul> Where should my content live? Docs or blog? 2023-05-13T13:09:59Z https://michaelheap.com/blog-or-docs/ <p>It’s 4pm on Thursday and you’ve just wrapped up an awesome piece of content that you’ve been thinking about for over a month. You finally had time to sit down and build a demo and write down how you did it so that others can follow along too.</p> <p>Then you hear the age old question:</p> <blockquote> <p>Are you planning to publish this on our blog or in the documentation?</p> </blockquote> <p>The question hits you like a ton of bricks. Not this again. You just wanted to write. To teach. Who cares where it’s published? Just publish it so that others can learn!</p> <p>The thing is, where you publish your content <em>is</em> important. Let's take a look at when you'd choose to publish on your blog, and when to contribute to the documentation.</p> <h2 id="choose-the-blog" tabindex="-1">Choose the Blog</h2> <p>There are a couple of instances where content should definitely go on your blog rather than in your documentation:</p> <ul> <li>It’s an announcement rather than something educational</li> <li>It’s a fun extra that’s not really core to your business (e.g. “managing your infrastructure with Terraform”, where you just happen to deploy your product)</li> <li>Niche but interesting technical content (e.g. how we diagnosed network performance issues on $cloud)</li> <li>It’s something that you want to publish now, but not maintain forever. Publishing on a blog means that it has a <code>published_at</code> date. People don’t expect a blog post from 2017 to be accurate, and are a lot more forgiving. Some examples of when this is useful: <ul> <li>Collaborations with other companies</li> <li>Using X with your product (where X is the flavour of the month, such as AI)</li> <li>Using X with Y, where newer versions make the instructions invalid</li> </ul> </li> <li>Anything that contains an opinion. This is typically your thought-leadership type content</li> </ul> <p>If your content doesn’t fit in to any of the above buckets, you may choose to publish it on your blog anyway. This is typically the case when:</p> <ul> <li>Your blog has much better SEO than your docs and you want people to find the content</li> <li>The author’s personal style does not fit the rest of your documentation, but the content benefits from having a unique style</li> <li>You’re trying to tell a story rather than educate someone. Blog posts can skip a lot of the prerequisites for a project and assume the reader knows enough to get started.</li> </ul> <h2 id="choose-the-docs" tabindex="-1">Choose the docs</h2> <p>When we publish documentation, we commit to it being accurate forever. We need to regularly revisit pages to ensure that the content is accurate for the current version (and potentially older versions too). It’s no longer a snapshot of a point in time. It’s a living thing that grows with your product.</p> <p>It’s an easy decision to put things in the docs for me if any of the following are true:</p> <ul> <li>You need to maintain the content indefinitely</li> <li>It’s focused purely on the <em>how</em> rather than the <em>why</em></li> <li>It’s a step-by-step tutorial on how to achieve something with your product</li> <li>It’s a reference document, such as an API specification or configuration reference</li> <li>There are multiple code samples showing how to achieve a single task</li> </ul> <p>You might also choose to put the content in the docs if:</p> <ul> <li>The content is maintained by multiple people, follows standards and has a consistent tone of voice</li> <li>It’s something that you need to send to customers. Being in the docs add’s an “official” feeling to the content</li> </ul> <h2 id="help-me-decide" tabindex="-1">Help me decide</h2> <p>If you’re still not sure where to put your content, put in on your blog. It’s the lowest friction way to get started, and having the content somewhere is generally better than not having it at all.</p> <p>Writing precise, organised, complete documentation takes time and effort. Writing a blog post takes time and effort too, but less so. People are more forgiving when it comes to transient content.</p> <p>Once published, watch your analytics. If you see a blog post receiving consistent traffic, you should officially adopt the content and work it in to your documentation (at which point you’ll have the fun “what about our SEO” conversation where everyone’s too scared to move the content).</p> <h2 id="in-summary" tabindex="-1">In summary</h2> <ul> <li>Use the blog for time-limited content. Use the docs for evergreen</li> <li>Explain <em>why</em> on the blog, and <em>how</em> in the docs</li> <li>Authors have a unique style on the blog. Content is uniform in the docs</li> <li>If you’re sharing ideas, use the blog. If you’re educating, use the docs.</li> <li>Finally, if there’s a risk the content won’t get published at all, just use the blog. It’s easier to move from the blog to the docs than it is to remove something from the docs later.</li> </ul> Carving the Turkey 2023-05-08T12:00:25Z https://michaelheap.com/carving-the-turkey/ <p>One of the things I learned early on in my DevRel career is that you’ve got to be creative when it comes to content production. Starting from scratch for every blog post, video or conference talk means that you can only produce 1-2 pieces of deep content per month.</p> <p>Fortunately, I’ve worked with some great advocates that taught me how to “carve the turkey”. Just like there are lots of different cuts from a turkey, there are ways to get more out of a single piece of content.</p> <h2 id="an-abstract-workflow" tabindex="-1">An Abstract Workflow</h2> <p>I start each project with the intention to produce at least three pieces of content - internal documentation, a public blog post and a demo video about the feature. Many projects will lead to more pieces of content than this, but three is the minimum I aim for.</p> <p>For larger projects, you might end up with live streams and a conference talk in addition to your documentation, blog post and videos. This all depends on the team you have and how much emphasis you’re putting on content</p> <p>Here’s the process I go through when working through a project:</p> <ul> <li><strong>(Offline)</strong> Read the basics + build up a list of docs / cheat sheet to use later</li> <li><strong>(Live Stream, optional)</strong> Try out the things you’ve learned. Make mistakes, it’s ok! We’re all learning together at this point. These aren’t expected to be polished.</li> <li><strong>(Documentation)</strong> Write user facing documentation that combines the generic understanding of your topic with things that are specific to your business</li> <li><strong>(Blog Post)</strong> Write unique content about the topic. It could be something you struggled to understand, or an innovative application of the technology. It may or may not mention your product.</li> <li><strong>(Videos)</strong> Record short demo videos to show off the capabilities of your product. Try to explain both <em>why</em> and the <em>how</em> these things work. These can be used for multiple purposes (with some editing), including: <ul> <li>Sales enablement</li> <li>Social media promotion</li> <li>Product capability videos on YouTube</li> <li>Documentation</li> </ul> </li> <li><strong>(Conference Talk, optional)</strong> Take what you’ve learned and combine them in to a 20-40 minute conference talk. Here are a few suggested frameworks: <ul> <li>Introduction to X</li> <li>A deep dive in to X</li> <li>X for Y Audience</li> <li>Extending X</li> <li>Real world X use cases</li> </ul> </li> </ul> <p>This follows a pattern that’s common across most of my projects:</p> <ul> <li>Learn</li> <li>Try</li> <li>Document</li> <li>Apply</li> <li>Teach</li> </ul> <p>It’s hard to imagine what the above might look like for your project if you’ve never done it before, so let’s take a look at a project that I’m currently working on.</p> <h2 id="a-real-world-example" tabindex="-1">A Real World Example</h2> <blockquote> <p>Context: I’m the product manager for Kong Ingress Controller for Kubernetes. As part of my role I’m learning about new Kubernetes APIs</p> </blockquote> <p>I’m currently learning about the Gateway API in Kubernetes, which is an area that I’m not too familiar with. There’s a lot of reading involved, and a lot of testing things out to see how it actually works.</p> <p>Here’s a rough list of things I need to do to be successful:</p> <ul> <li>Read the Gateway API specification <ul> <li>Understand the component parts</li> <li>What are the differences between the Gateway API and Ingress API?</li> </ul> </li> <li>Try out the most common parts of the Gateway API <ul> <li>I think this is HTTPRoute and TCPRoute from prior conversations</li> </ul> </li> <li>Document how Gateway API works with Kong <ul> <li>How to add a service and a route</li> <li>How to add an authentication plugin</li> </ul> </li> <li>Provide Gateway API examples for the top 5 Kong use cases <ul> <li>Prebuilt demos for Sales Engineering org</li> </ul> </li> <li>Talking points for the sales team targeting Kubernetes accounts</li> </ul> <p>Looking at the above list, I can start to think about what content I can produce for each stage:</p> <table> <thead> <tr> <th></th> <th>Personal</th> <th>Marketing</th> <th>Product</th> <th>Sales</th> </tr> </thead> <tbody> <tr> <td><strong>Read the Gateway API specification</strong></td> <td>Cheat sheet of components. <br /><br />Difference between Ingress and Gateway API</td> <td>Blog post: Ingress vs Gateway API.</td> <td></td> <td></td> </tr> <tr> <td><strong>Try out the most common parts of the Gateway API</strong></td> <td></td> <td>Live stream: Exploring the Gateway API. <br /><br />Blog post: An introduction to HTTPRoute, TCPRoute and UDPRoute.</td> <td></td> <td>Demo video: Switch from X to Kong in &lt; 5 minutes</td> </tr> <tr> <td><strong>Document how Gateway API works with Kong</strong></td> <td></td> <td></td> <td>Documentation: Getting started with Kong and the Gateway API.</td> <td></td> </tr> <tr> <td><strong>Provide Gateway API Examples</strong></td> <td></td> <td>Demo video: Gateway API and Y (x5, top 5 use cases) - used for social</td> <td>Product feedback: Experience of augmenting the Gateway API with vendor. <br /><br />Documentation: Gateway API and Y (x5, top 5 use cases) extensions</td> <td>Demo video: Gateway API and Y (x5, top 5 use cases) - used for demonstrating capabilities</td> </tr> <tr> <td><strong>Talking points for sales targeting Kubernetes</strong></td> <td></td> <td>Blog post: Removing vendor lock-in with the Gateway API. <br /><br />Blog post: OSS at Kong: Building the Kubernetes ecosystem</td> <td>Documentation: Migrating from X to Y using the Gateway API.</td> <td>Demo video: Gateway API and Y (x5, top 5 use cases)</td> </tr> </tbody> </table> <p>That’s a <em>lot</em> of content for something that could have resulted in zero pieces of public facing material. It would have been easy to spend a week reading everything and making private notes. Instead, we’ve produced 13 pieces of content which help your team mates, your customers, and you!</p> <p>After all, the best way to learn is to teach.</p> Obsidian - Daily Note + Random Review 2023-04-18T07:55:58Z https://michaelheap.com/obsidian-random-notes/ <p>I’ve been dutifully collecting notes in Obsidian for a while now, but the idea of going through and cleaning them all up was daunting. There are hundreds of notes that are just copy/pasted from other sources, screenshots of interesting things or rough notes on a topic.</p> <p>Then I was reading about how you can use <a href="https://github.com/liamcain/obsidian-periodic-notes">Periodic Notes</a> to generate daily notes in Obsidian when I stumbled across <a href="https://michaelheap.com/obsidian-random-notes/heymichellemac.com/obsidian-daily-note-2022">this post</a> from Michelle Mac. It showed how you can generate a list of random notes based on a tag.</p> <p>I use tags in the page front matter rather than inline hashtags, so I needed to rework the logic a little. I also don’t keep all my notes in the same “Evergreen” folder, so I needed different logic for the filename.</p> <p>Here’s what I ended up with. It generates a list of three posts when it runs. If you want to use it, change the values in <code>allowedTags</code> to source whichever pages you need:</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">&lt;%*</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">randomNotes</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</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">allowedTags</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">stub</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">unedited</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">app</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">metadataCache</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getCachedFiles</span><span style="color: #D8DEE9FF">()</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">forEach</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">filename</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">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">data</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">app</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">metadataCache</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getCache</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">filename</span><span style="color: #D8DEE9FF">)</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: #81A1C1">!</span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">frontmatter</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></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: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">tags</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> (</span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">frontmatter</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">tags</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">||</span><span style="color: #D8DEE9FF"> [])</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">filter</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">t</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">allowedTags</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">indexOf</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">t</span><span style="color: #D8DEE9FF">) </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #B48EAD">1</span><span style="color: #D8DEE9FF">)</span></div><div class="line"></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> (</span><span style="color: #D8DEE9">tags</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length </span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</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">p</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">filename</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">split</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">"</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: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">f</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">p</span><span style="color: #D8DEE9FF">[</span><span style="color: #D8DEE9">p</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length</span><span style="color: #81A1C1">-</span><span style="color: #B48EAD">1</span><span style="color: #D8DEE9FF">]</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">split</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><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">]</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">randomNotes</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">f</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></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: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">i</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">do</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">randomIndex</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">Math</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">floor</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">Math</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">random</span><span style="color: #D8DEE9FF">() </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">randomNotes</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">tR</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">- [[</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">randomNotes</span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9">randomIndex</span><span style="color: #ECEFF4">]</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><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">i</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">while</span><span style="color: #D8DEE9FF"> (</span><span style="color: #D8DEE9">i</span><span style="color: #81A1C1">&lt;</span><span style="color: #B48EAD">3</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #81A1C1">%&gt;</span></div></code></div></pre> Check permissions in a GitHub Actions workflow 2023-04-03T15:14:46Z https://michaelheap.com/github-actions-check-permission/ <p>When working with GitHub Actions, you may want to check what relationship the person performing an action has to a repo before running a workflow. Public documentation on collaborators is scarce, so here’s what I’ve been able to work out so far.</p> <ul> <li>There are two ways to check relationships: <code>pull_request.author_association</code> and the <code>/repos/:org/:repo/collaborators/:user/permission</code> endpoint.</li> <li><code>pull_request.author_association</code> returns an item from <a href="https://docs.github.com/en/graphql/reference/enums#commentauthorassociation">CommentAuthorAssociation</a>, which is one of the following: <ol> <li><code>COLLABORATOR</code>: Author has been invited to collaborate on the repository.</li> <li><code>CONTRIBUTOR</code>: Author has previously committed to the repository.</li> <li><code>FIRST_TIMER</code>: Author has not previously committed to GitHub.</li> <li><code>FIRST_TIME_CONTRIBUTOR</code>: Author has not previously committed to the repository.</li> <li><code>MANNEQUIN</code>: Author is a placeholder for an unclaimed user.</li> <li><code>MEMBER</code>: Author is a member of the organization that owns the repository.</li> <li><code>NONE</code>: Author has no association with the repository.</li> <li><code>OWNER</code>: Author is the owner of the repository.</li> </ol> </li> <li>The <code>/repos/:org/:repo/collaborators/:user/permission</code> endpoint returns the permissions that the provided user has on a repository. This is one of: <code>read</code>, <code>write</code>, <code>admin</code></li> </ul> <h2 id="author-association" tabindex="-1">Author Association</h2> <p>The author association field is available in the default <code>pull_request</code> payload, which makes it useful if you don’t want to make an additional API call to figure out if an actor should be allowed to perform an action or not.</p> <p>When interacting with an organization's repo, the <code>pull_request.author_association</code> value is different depending on if the actor is an external collaborator, an org member, or neither.</p> <p>If the actor is neither an org member nor an external collaborator their <code>author_association</code> will be one of the following:</p> <ul> <li><code>FIRST_TIMER</code>: The actor has never contributed to any repository on GitHub before</li> <li><code>FIRST_TIME_CONTRIBUTOR</code>: The actor has never contributed to this repository before</li> <li><code>CONTRIBUTOR</code>: The actor has previously contributed to the repository</li> </ul> <p>If the actor has been added as an external collaborator, their <code>author_association</code> will be <code>COLLABORATOR</code>, no matter which set of permissions they’ve been given. If they have <code>read</code> permissions, they’re a <code>COLLABORATOR</code>. If they have <code>admin</code> permissions, they’re still a <code>COLLABORATOR</code>.</p> <p>Finally, if the actor is a member of the organization that owns the repository their <code>author_association</code> will be <code>MEMBER</code>. This is true whether they are an organization member <em>or</em> owner. In addition, their <code>author_association</code> will always be <code>MEMBER</code> regardless of the permissions they have on the repository. If they have <code>read</code> permissions, they’re a <code>MEMBER</code>. If they have <code>admin</code> permissions, they’re still a <code>MEMBER</code>.</p> <p>The only other state that you might be interested in is <code>OWNER</code>. This state is not possible with organization owned repositories. It is only returned when a repository is owned by a specific user.</p> <p>That leaves us with 2 unused states:</p> <ul> <li><code>MANNEQUIN</code> - <a href="https://docs.github.com/en/graphql/reference/objects#mannequin">Mannequins</a> are created when source code and metadata are <a href="https://docs.github.com/en/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/reclaiming-mannequins-for-github-enterprise-importer#about-mannequins">migrated</a> from one source control platform to GitHub. If the user was not properly mapped to a GitHub account at the time of migration a placeholder mannequin is created. If you’re interested in seeing a <code>mannequin</code>, check out <a href="https://github.com/llvm/llvm-project/issues/2930">this issue</a>.</li> <li><code>NONE</code> - I’m not sure how to trigger this as any actor is implicitly a <code>FIRST_TIMER</code>, <code>FIRST_TIME_CONTRIBUTOR</code> or <code>CONTRIBUTOR</code></li> </ul> <p>Finally, let’s talk about deleted users. Any deleted user actions are attributed to <code>ghost</code>, which is a <a href="https://github.com/ghost">GitHub owned account</a> that “takes the place of user accounts that have been deleted 👻”</p> <h2 id="user-permissions" tabindex="-1">User Permissions</h2> <p>If you’re looking for more granularity than the data provided in <code>author_association</code> you can use the <code>/repos/:org/:repo/collaborators/:user/permission</code> endpoint to fetch which permission the actor has for this repo.</p> <blockquote> <p><strong>Note:</strong> You’ll need <code>push</code> permission on the repository to be able to use the endpoint above.</p> </blockquote> <p>The possible values returned by this repository are as follows:</p> <ul> <li><code>none</code> - <code>:repo</code> is a private repository and the user has no permissions</li> <li><code>read</code> - The user has <code>read</code> or <code>triage</code> permissions, or <code>:repo</code> is a public repository and the user has no permissions</li> <li><code>write</code> - The user has <code>write</code> or <code>maintain</code> permissions</li> <li><code>admin</code> - The user has <code>admin</code> permission</li> </ul> <p>The permission level provided is the same no matter how the permission is granted. The actor may be added as an external collaborator or to an organization team. As long as they have permission to the repository somehow, it will be returned via the endpoint above.</p> <p>If you’d like to check a user’s permission in a workflow before performing a step, I recommend the <a href="https://github.com/marketplace/actions/has-permission">Has Permission</a> action.</p> <p>Here’s an example from their README (with a small change to use <code>github.token</code> rather than a secret):</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">Action Sample Workflow</span></div><div class="line"></div><div class="line"><span style="color: #616E88"># Run workflow when a new pull request is opened</span></div><div class="line"><span style="color: #81A1C1">on</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #A3BE8C">pull_request</span><span style="color: #ECEFF4">]</span></div><div class="line"></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">check_user_permission</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">name</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">A job to check user's permission level</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: #ECEFF4"> </span><span style="color: #616E88"># Check for write permission</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">id</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">check</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">scherermichael-oss/action-has-permission@master</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">required-permission</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">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">$</span></div><div class="line"><span style="color: #ECEFF4"> </span><span style="color: #616E88"># Use the output from the `check` step</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 only if user has sufficient permissions</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.check.outputs.has-permission</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: #A3BE8C">echo "Congratulations! Your permissions to access the repository are sufficient."</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 only if user has NOT sufficient permissions</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: #ECEFF4">"</span><span style="color: #A3BE8C">! steps.check.outputs.has-permission</span><span style="color: #ECEFF4">"</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: #A3BE8C">echo "Sorry! Your permissions are insufficient."</span></div></code></div></pre> <h2 id="when-to-use-each-option" tabindex="-1">When to use each option</h2> <p>So, which should we use? It depends what you want to check.</p> <p><code>pull_request.author_association</code> should be used when you want to check the relationship between the person performing an action and the repository itself. It’s useful when you want to auto-approve workflows for people that have contributed in the past (<code>CONTRIBUTOR</code>) or for everyone in your organization (<code>MEMBER</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: #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">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 != 'CONTRIBUTOR' && github.event.pull_request.author_association != 'MEMBER' }}</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>If you need to check for specific permissions, the <code>/repos/:org/:repo/collaborators/:user/permission</code> endpoint is the option to choose. This is useful when you work for an organization where only specific teams have <code>write</code> access to a repository and should be allowed to perform specific tasks.</p> <p>If you’re not sure which to use, I’d recommend using the permission endpoint (via <code>scherermichael-oss/action-has-permission</code>) as it allows for more granular access controls than <code>pull_request.author_association</code>.</p> The TAPE Model for Content 2023-03-20T11:00:17Z https://michaelheap.com/tape-method-content/ <p>Content is a mainstay of many Developer Relations teams, but it can sometimes be tough to show the impact that you’re having. Content on your company blog can be instrumented and attributed well, but what about things like YouTube videos or recorded conference talks?</p> <p>You can build out complex tracking systems with UTM parameters and short URLs, but there’s an easier way to understand how your content is being used. It all starts with your internal teams.</p> <p>DevRel is typically a cost centre. There isn’t any revenue attributed directly to the team. The best way I’ve found to justify your existence (and yes, unfortunately this is a thing in every business) is to attach yourself to the revenue generating teams.</p> <p>Serving your internal audience has two outcomes:</p> <ol> <li>You support teams that drive new business. The sales team can link to your videos to show prospective customers the capabilities of your platform and how easy it is to use</li> <li>You save time for existing teams, which means you’re saving $$$. Every proof of concept cycle that is 10% faster thanks to your demo videos is money that the business isn’t spending on Sales Engineers. Every support ticket that is replied to with a link to your content is time that an agent doesn’t have to spend writing a response.</li> </ol> <p>In both of these cases you’re drawing a direct link between the content that you’re producing and the impact it has on the revenue or efficiency of the business.</p> <p>This is where the TAPE model comes in.</p> <p>TAPE stands for <strong>T</strong>rigger, <strong>A</strong>ction, <strong>P</strong>eople, <strong>E</strong>xamples.</p> <ul> <li><strong>Trigger</strong>: <em>Why</em> are we producing this content? What problems have we observed that we can help solve?</li> <li><strong>Action</strong>: <em>What</em> are we producing? Is this best presented in the documentation, as a blog post, as a video etc?</li> <li><strong>People</strong>: <em>Who</em> is asking for this? Not our end customers, but our internal stakeholders. Can we make other teams successful?</li> <li><strong>Examples</strong>: <em>Where</em> is this being used? Is your content being used to solve support tickets? Is it being used to prove we have certain capabilities during an RFP?</li> </ul> <p>By matching the content you produce to pain points that internal teams are having you <em>know</em> that the content is going to resonate with at least one audience. This is your <strong>trigger</strong>, your reason for producing the content in the first place.</p> <p>Understanding who’s going to consume your content helps you decide which <strong>action</strong> to take. A VP of infrastructure is more likely to scan a documentation page than watch a video. A developer at a Global System Integrator might be more at home with a 60 minute deep dive video on to how to write plugins for your system.</p> <p>Aim to make friends by supporting the right <strong>people</strong>. If you’re working with the sales team, target enterprise sales reps rather than lower value digital customers. If you’re working with Sales Engineers, work with regional leads who can help evangelise the work you do more widely. Saying “I helped $VP with $TOP10 customer” is much more impactful than saying “I solved 10 loosely related support tickets”</p> <p>Then finally, keep track of how your content is being used. Make a note every time someone links to one of your pieces of content in Slack. This could be a product manager asking how a feature built before they joined works. It could be someone from marketing asking how a new feature behaves so that they can position it correctly. Building up a list of <strong>examples</strong> allows you to show that not only did you identify an opportunity to support people internally, the content is being used repeatedly to help enable the rest of the business.</p> <h2 id="examples" tabindex="-1">Examples</h2> <p>Alright, so what does that actually look like? Here are five examples to get you started.</p> <p><strong>Trigger:</strong> Product decision to deprecate a feature<br /> <strong>Action:</strong> FAQ, migration recommendations<br /> <strong>People:</strong> CPO, Support, Docs Team, Customer Success<br /> <strong>Examples</strong>: CPO shares with internal teams. Support use with inbound tickets. Docs team write public facing docs. Customer Success proactively reach out to their customers to explain how to migrate.</p> <p><strong>Trigger:</strong> New feature released<br /> <strong>Action:</strong> 3 minute demo video showing how it works<br /> <strong>People:</strong> Product Manager, Product Marketing, Social Media Manager, Solutions Engineers, Sales<br /> <strong>Examples</strong>: Product Manager / Product Marketing used in sales enablement. Social media used for a blog post. Solutions engineers used as a basis for their own demos. Sales used to show prospects the platform capabilities.</p> <p><strong>Trigger:</strong> Marketing campaign by competitor<br /> <strong>Action:</strong> Rebuttal video showing off product capabilities<br /> <strong>People:</strong> Product Marketing, Sales<br /> <strong>Examples</strong>: Product Marketing builds battle cards using the information. Sales use the content to influence champions within a business.</p> <p><strong>Trigger:</strong> Difficult to run your OSS project on MacOS<br /> <strong>Action:</strong> Work on README and improving error messages in the app<br /> <strong>People:</strong> Engineering, Community, Support<br /> <strong>Examples</strong>: Increased contribution velocity for engineering. A community develops around your project. Reduced support ticket volume for common use cases</p> <p><strong>Trigger:</strong> An event is looking for a speaker<br /> <strong>Action:</strong> Write and present a talk<br /> <strong>People:</strong> Marketing, Sales<br /> <strong>Examples</strong>: Marketing run the event with good attendance to drive MQLs. Sales use the content to show off platform capabilities with potential customers.</p> <h2 id="conclusion" tabindex="-1">Conclusion</h2> <p>Using internal triggers as your source of content ideas allows you to map your contributions to the goals of other teams. Many of these teams contribute directly to revenue, and if you make them successful it’s easy to show the impact you’re having on the business.</p> <p>It might be tempting at this point to try to add some attribution directly to your team. You might negotiate that 2% of every sale where your material is used is attributed to your team.</p> <p>This is a trap.</p> <p>If you can claim 2% of every deal (and this is a high percentage), you need to do 100 deals worth $500,000 every single year to get $1 million in attribution. If you’re impacting 100 deals per year, chances are that you’ve got a large-ish team who are definitely costing more than $1 million per year.</p> <p>Instead, focus on the relationships. <em>People trust people</em>. A VP saying “that DevRel team sure are awesome. They’ve cut our sales cycle by 2 weeks” is worth much more than 2% of a sale.</p> Filter Google Analytics report 2022-11-29T13:58:08Z https://michaelheap.com/filter-google-analytics-report/ <p>I'm trying out <a href="https://getradar.co/">Radar</a> to keep track of analytics at a glance. It's Google Analytics collection works great, but the way that we've set up Google Analytics the data for multiple sites ends up in the same property.</p> <p>I wanted to filter the report to only traffic to a specific subdomain. It took me too long to work out that I needed a <code>dimensionFilterClauses</code> entry with the <code>ga:pagePath</code> (via <a href="https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema">the schema</a>).</p> <p>Here's how to filter your reports in case you (or I) need to do it in the future:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">json</div><div class="code-container"><code><div class="line"><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">reportRequests</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">viewId</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">YOUR_VIEW_ID</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">dateRanges</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">startDate</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">today</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">endDate</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">today</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></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">dimensions</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ga:date</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></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">metrics</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">expression</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ga:pageviews</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></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">dimensionFilterClauses</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">filters</span><span style="color: #ECEFF4">"</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: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">dimensionName</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">ga:pagePath</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">operator</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">PARTIAL</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #8FBCBB">expressions</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">docs.konghq.com</span><span style="color: #ECEFF4">"</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></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></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></div><div class="line"><span style="color: #ECEFF4">}</span></div></code></div></pre> <p>Other things that may be useful:</p> <p><strong>Endpoint</strong>: <a href="https://analyticsreporting.googleapis.com/v4/reports:batchGet">https://analyticsreporting.googleapis.com/v4/reports:batchGet</a><br /> <strong>Method</strong>: <code>POST</code><br /> <strong>Auth</strong>: <code>Bearer [token]</code></p> <p>Then transform the results using the following JS snippet:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">javascript</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pageCount</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Number</span><span style="color: #D8DEE9FF">(</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">reports</span><span style="color: #D8DEE9FF">[</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">]</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">totals</span><span style="color: #D8DEE9FF">[</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">]</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">values</span><span style="color: #D8DEE9FF">[</span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">]</span></div><div class="line"><span style="color: #D8DEE9FF">)</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toLocaleString</span><span style="color: #D8DEE9FF">()</span><span style="color: #81A1C1">;</span></div></code></div></pre> Upgrade from Winston 2 to 3 using colors 2022-10-23T11:39:59Z https://michaelheap.com/winston-3-upgrade-colors/ <p>I updated a project from Winston 2 to Winston 3 this morning and needed to make a few changes to how it was configured.</p> <p>I previously created a logger instance, then switched to using <code>syslog</code> levels with a custom colour scheme. Here's how I did it in Winston 2:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">javascript</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</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">winston</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: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">logger</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: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">Logger</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">transports</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> [</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">transports</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">Console</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">level</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">warning</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">colorize</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</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: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> ]</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><div class="line"></div><div class="line"><span style="color: #D8DEE9">syslogColors</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">debug</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">rainbow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">info</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cyan</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">notice</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">white</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">warning</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">yellow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold red</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">crit</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">inverse yellow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">alert</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold inverse red</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">emerg</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold inverse magenta</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addColors</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">syslogColors</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"><span style="color: #D8DEE9">logger</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setLevels</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">config</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">syslog</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">levels</span><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div></code></div></pre> <p>In Winston 3, the <code>new winston.Logger</code> call has been replaced with <code>winston.createLogger</code>, <code>levels</code> must be provided in the constructor, and formatting is handled by individual formatters using the <a href="https://github.com/winstonjs/logform">logform</a> library.</p> <p>Here's how to achieve the same thing as above in Winston 3:</p> <pre class="shiki nord" style="background-color: #2e3440ff; color: #d8dee9ff"><div class="language-id">javascript</div><div class="code-container"><code><div class="line"><span style="color: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</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">winston</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: #81A1C1">const</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">syslogColors</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">debug</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">rainbow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">info</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">cyan</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">notice</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">white</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">warning</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">yellow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold red</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">crit</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">inverse yellow</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">alert</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold inverse red</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">emerg</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">bold inverse magenta</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">format</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">format</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">combine</span><span style="color: #D8DEE9FF">(</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">format</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">colorize</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">all</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">colors</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">syslogColors</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: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">format</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">simple</span><span style="color: #D8DEE9FF">()</span></div><div class="line"><span style="color: #D8DEE9FF">)</span><span style="color: #81A1C1">;</span></div><div class="line"></div><div class="line"><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">logger</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">createLogger</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">transports</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> [</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">transports</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">Console</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">{</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">level</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">warning</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">format</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: #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: #88C0D0">levels</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">winston</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">config</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">syslog</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">levels</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> Running `gickup` on a QNAP NAS 2022-10-15T18:00:01Z https://michaelheap.com/gickup-on-qnap/ <p>I learned about <a href="https://github.com/cooperspencer/gickup">Gickup</a> whilst reading <a href="https://github.com/geerlingguy/my-backup-plan">Jeff Geerling’s backup plan</a> and thought it would be a useful thing to run for my own repos.</p> <p>I initially ran it on my local machine periodically, then realised that I could run it on my QNAP NAS every night. Here’s how I did it:</p> <ul> <li>Create a shared folder named <code>gickup</code> to contain the backups. I’ve got a volume named <code>Backups</code> that contains this, but any volume will do</li> <li>Create a directory to store your backups by running <code>mkdir -p /share/gickup/repos</code></li> <li>Edit <code>/etc/config/crontab</code> and add the following line to the end: <code>0 3 * * * docker run --rm -v /share/gickup:/hostvol buddyspencer/gickup:0.9 /gickup/app /hostvol/config.yml &gt; /share/gickup/last-run.log 2&gt;&amp;1</code>. This will run <code>gickup</code> every day at 3am</li> <li>Reload cron to pick up that change by running <code>crontab /etc/config/crontab &amp;&amp; /etc/init.d/crond.sh restart</code></li> </ul> <p>You may have noticed that I reference <code>/hostvol/config.yml</code>. I mount the <code>/share/gickup</code> folder a <code>/hostvol</code>, and <code>config.yml</code> is my <code>gickup</code> configuration file. Here’s what it looks like:</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: #616E88"># config.yml in /share/gickup</span></div><div class="line"><span style="color: #8FBCBB">source</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">github</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">token</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">CHANGE_ME_TO_YOUR_GITHUB_PAT</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">excludeorgs</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88"># this excludes repos from the organizations "foo" and "bar"</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">foo</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">bar</span></div><div class="line"><span style="color: #8FBCBB">destination</span><span style="color: #ECEFF4">:</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">local</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">path</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">/hostvol/repos</span></div><div class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">structured</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></div></code></div></pre> <p>Create your own <code>config.yml</code>, making sure you change the <code>token</code> value. If you’ve configured all of the above, all the repositories you have access to will be backed up at 3am each day.</p>