<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>michaelheap.com</title>
  <subtitle>Thoughts on leadership, code and how to fix odd edge cases in tools (not necessarily in that order)</subtitle>
  <link href="https://michaelheap.com/rss" rel="self"/>
  <link href="https://michaelheap.com"/>
  <updated>2025-03-15T20:19:27Z</updated>
  <id>https://michaelheap.com</id>
  <author>
    <name>Michael Heap</name>
    <email>[email protected]</email>
  </author>
  
  <entry>
    <title>Pin your GitHub Actions</title>
    <link href="https://michaelheap.com/pin-your-github-actions/"/>
    <updated>2025-03-15T20:19:27Z</updated>
    <id>https://michaelheap.com/pin-your-github-actions/</id>
    <content type="html">&lt;p&gt;Way back in 2019, Julien Renaux published &lt;a href=&quot;https://julienrenaux.fr/2019/12/20/github-actions-security-risk/&quot;&gt;Use GitHub Actions at your own risk&lt;/a&gt;. While the title is a little sensational, it correctly pointed out that any maintainer can update a branch or tag to point at new code without you knowing. This means that if any action is compromised, you&#39;ll start leaking secrets without knowing it.&lt;/p&gt;
&lt;p&gt;Today, &lt;code&gt;tj-actions/changed-files&lt;/code&gt;, a widely used GitHub Action &lt;a href=&quot;https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised&quot;&gt;was compromised&lt;/a&gt; and started leaking secrets.&lt;/p&gt;
&lt;p&gt;Hopefully this was the wakeup call the industry needed to start paying attention to supply chain security.&lt;/p&gt;
&lt;h2 id=&quot;solving-the-problem&quot; tabindex=&quot;-1&quot;&gt;Solving the problem&lt;/h2&gt;
&lt;p&gt;Security is always a trade-off. You can solve the supply chain problem by specifying a full length commit SHA, but fetching that SHA for every action is a painful process. Then, whenever you want to upgrade your action you have to do it all again.&lt;/p&gt;
&lt;p&gt;Thankfully, there are tools and automations available to solve this problem. You can be secure &lt;em&gt;and&lt;/em&gt; feel minimal pain thanks to these projects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mheap/pin-github-action&quot;&gt;pin-github-action&lt;/a&gt; - This is one of my projects. It takes a directory of workflows and uses the GitHub API to convert tag and branch references to a full length commit SHA.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions&quot;&gt;github-actions-ensure-sha-pinned-actions&lt;/a&gt; - A community action that will fail the build if it detects any unpinned actions being used in the current repository.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot&quot;&gt;Dependabot&lt;/a&gt; / &lt;a href=&quot;https://docs.renovatebot.com/modules/manager/github-actions/&quot;&gt;Renovate&lt;/a&gt; - Dependency management systems that submits PRs to upgrade GitHub Actions. They both natively understand the &lt;code&gt;# &amp;lt;ref&amp;gt;&lt;/code&gt; comments in workflow files.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;pin-to-a-sha&quot; tabindex=&quot;-1&quot;&gt;Pin to a SHA&lt;/h3&gt;
&lt;p&gt;The first thing to do is update all of your existing workflows to use the long commit SHA using &lt;code&gt;pin-github-action&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can run it with &lt;code&gt;npx&lt;/code&gt; or &lt;code&gt;docker&lt;/code&gt;. If you&#39;re not sure which to use, use &lt;code&gt;docker&lt;/code&gt; through the following alias:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;alias&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; pin-github-action=&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;docker run --rm -v &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;pwd&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;:/workflows -e GITHUB_TOKEN mheap/pin-github-action&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;If you&#39;re working with a large number of workflows, or any private actions you&#39;ll need to set the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; environment variable to prevent rate limiting or provide valid access credentials to a repository:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; GITHUB_TOKEN=&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;ghp_YOUR_TOKEN_HERE&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;Finally, change directory to your repository and run &lt;code&gt;pin-github-action&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; my-repo&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;pin-github-action .github/workflows&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;If you run &lt;code&gt;git diff .github/workflows&lt;/code&gt; you&#39;ll see that all of your actions have been updated to the long commit SHA:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;diff&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    steps:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #BF616A&quot;&gt;     - uses: actions/checkout@v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #BF616A&quot;&gt;     - uses: MyOrg/some-action@v1&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;     - uses: MyOrg/some-action@25ed13d0628a1601b4b44048e63cc4328ed03633 # v1&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;I recommend pinning all actions to a SHA, but this may not be feasible for some companies that use internal actions. If you want to trust internal actions, you can pass the &lt;code&gt;--allow&lt;/code&gt; flag to &lt;code&gt;pin-github-action&lt;/code&gt; to add a specific prefix to an allowlist:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;pin-github-action --allow &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;MyOrg/*&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; .github/workflows&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;This will ignore any actions with the prefix &lt;code&gt;MyOrg&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;diff&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    steps:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #BF616A&quot;&gt;     - uses: actions/checkout@v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      - uses: MyOrg/some-action@v1&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;h3 id=&quot;prevent-regressions&quot; tabindex=&quot;-1&quot;&gt;Prevent regressions&lt;/h3&gt;
&lt;p&gt;Pinning all actions to a specific SHA solves the problem today, but it doesn&#39;t guarantee that a new action won&#39;t be added in the future without using a SHA. To prevent that happening, &lt;a href=&quot;https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions&quot;&gt;github-actions-ensure-sha-pinned-actions&lt;/a&gt; can be used to fail the build when any unpinned actions are detected.&lt;/p&gt;
&lt;p&gt;The README for the action contains examples, but if you&#39;re using &lt;code&gt;pin-github-action&lt;/code&gt; you can automatically add a new workflow using the &lt;code&gt;--enforce&lt;/code&gt; flag. The &lt;code&gt;--enforce&lt;/code&gt; flag writes a workflow containing &lt;code&gt;github-actions-ensure-sha-pinned-actions&lt;/code&gt; to the path provided, including adding any actions passed in &lt;code&gt;--allow&lt;/code&gt; to the &lt;code&gt;allowlist&lt;/code&gt; input for the action.&lt;/p&gt;
&lt;p&gt;The following command will create a workflow at &lt;code&gt;.github/workflows/security.yaml&lt;/code&gt; that ensures all actions are using the long commit SHA, &lt;em&gt;unless&lt;/em&gt; they are actions from &lt;code&gt;MyOrg&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;pin-github-action --allow &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;MyOrg/*&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; --enforce .github/workflows/security.yaml .github/workflows &lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;The created workflow looks like this:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;yaml&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;push&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Security&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;ensure-pinned-actions&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;runs-on&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;steps&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Checkout code&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Ensure SHA pinned actions&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;zgosalvez/github-actions-ensure-sha-pinned-actions@25ed13d0628a1601b4b44048e63cc4328ed03633&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# v3&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;with&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;          &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;allowlist&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;|&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;            MyOrg/&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;h3 id=&quot;automating-updates&quot; tabindex=&quot;-1&quot;&gt;Automating updates&lt;/h3&gt;
&lt;p&gt;Finally, we need to keep our dependencies up to date. You have two options here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pin-github-action&lt;/code&gt; again&lt;/li&gt;
&lt;li&gt;Use Dependabot or Renovate&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;I recommend using Dependabot or Renovate.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;pin-github-action&lt;/code&gt; on a repository with pinned SHAs and will extract the target version from the comment and update the SHA like so:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;diff&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    steps:&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #BF616A&quot;&gt;     - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;This is great for updating actions in bulk, but it doesn&#39;t improve your security posture that much. You&#39;re still blindly upgrading to the latest version.&lt;/p&gt;
&lt;p&gt;I recommend using Dependabot or Renovate to update your actions. Both tools create Pull Requests with updated SHAs and provide a diff for review. Each pull request contains a link to the &lt;code&gt;compare&lt;/code&gt; view on GitHub that you can use to audit the changes since your last update and ensure that the action has not been compromised.&lt;/p&gt;
&lt;h3 id=&quot;stay-safe%2C-pin-your-dependencies&quot; tabindex=&quot;-1&quot;&gt;Stay safe, pin your dependencies&lt;/h3&gt;
&lt;p&gt;The compromise of &lt;code&gt;tj-actions/changed-files&lt;/code&gt; serves as a crucial reminder of the security risks in GitHub Actions. While pinning actions to commit SHAs adds an extra step, it significantly reduces the risk of supply chain attacks.&lt;/p&gt;
&lt;p&gt;By leveraging tools like &lt;code&gt;pin-github-action&lt;/code&gt;, enforcing SHA pinning with &lt;code&gt;github-actions-ensure-sha-pinned-actions&lt;/code&gt;, and automating updates with Dependabot or Renovate, teams can secure their workflows without unnecessary overhead.&lt;/p&gt;
&lt;p&gt;Supply chain security is an ongoing effort, but with these practices in place, you can proactively protect your repositories and secrets from potential threats.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Output a JSON file with Jekyll</title>
    <link href="https://michaelheap.com/jekyll-output-json/"/>
    <updated>2025-03-14T21:14:14Z</updated>
    <id>https://michaelheap.com/jekyll-output-json/</id>
    <content type="html">&lt;p&gt;I needed to expose some of the data we had in &lt;code&gt;app/_data&lt;/code&gt; in our Jekyll deployment for consumption in another project. Instead of copy/pasting the data, I chose to expose it as an API.&lt;/p&gt;
&lt;p&gt;To do the same, create a file named &lt;code&gt;app/_plugins/your_name_here_hook.rb&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;ruby&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; MyApi&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;self.process&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    api_prefix &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;api&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    the_data &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;path_to&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;your&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;FileUtils&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;mkdir_p&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;#{&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;dest&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;#{&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;api_prefix&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;File&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;#{&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;dest&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;#{&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;api_prefix&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;/your-data.json&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; the_data&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;to_json&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;end&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;end&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;Jekyll&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;Hooks&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;register &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;post_write &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;do&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;_&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;|&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;MyApi&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;site&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;Then you can access &lt;code&gt;/api/your-data.json&lt;/code&gt; on your Jekyll deployment to see the data.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Running Scrumdog with Docker</title>
    <link href="https://michaelheap.com/scrumdog-docker/"/>
    <updated>2025-02-25T19:25:33Z</updated>
    <id>https://michaelheap.com/scrumdog-docker/</id>
    <content type="html">&lt;p&gt;I was intrigued by the idea of &lt;a href=&quot;https://github.com/whoek/scrumdog&quot;&gt;Scrumdog&lt;/a&gt;, which allows you to export Jira tickets to a sqlite database.&lt;/p&gt;
&lt;p&gt;Unfortunately the website is now offline, and the provided binaries don&#39;t work on non-intel macs. Thankfully, Scrumdog is open source so I can just build it myself... except that it&#39;s written in OCaml and I don&#39;t have any of the toolchain installed and don&#39;t really want to install it just for one project.&lt;/p&gt;
&lt;p&gt;So, I reached for Docker.&lt;/p&gt;
&lt;p&gt;It seems as though the &lt;code&gt;opam&lt;/code&gt; dependency file isn&#39;t configured correctly for scrumdog, so here&#39;s a &lt;code&gt;Dockerfile&lt;/code&gt; that explicitly installs the correct libraries:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# Build a base image with updated dependencies&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;FROM ocaml/opam:alpine AS init-opam&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;RUN &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; -x &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Update and upgrade default packages&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    sudo apk update &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; sudo apk upgrade &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    sudo apk add gmp-dev sqlite-dev&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# Install opam dependencies explicitly then build scrumdog&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;FROM init-opam AS ocaml-app-base&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;COPY &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;.&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;RUN &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; -x &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Install related packages&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    opam install dune yojson sqlite3 cohttp-lwt-unix tls-lwt &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;opam env&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Build applications&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;RUN &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;opam env&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; dune build&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# Install dependencies + copy the built executable&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;FROM alpine AS ocaml-app&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;COPY --from=ocaml-app-base /home/opam/_build/default/bin/scrumdog.exe /home/bin/main.exe&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;RUN &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; -x &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Update and upgrade default packages&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    apk update &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; apk upgrade &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    apk add gmp-dev sqlite-dev &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Create a user to execute application&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    adduser -D app &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;Change owner to app&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;&amp;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    chown app:app /home/bin/main.exe&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;WORKDIR /home/app&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;USER app&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;ENTRYPOINT &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;/home/bin/main.exe&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;Finally, build the image and run it.&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;docker build -t scrumdog-local &lt;/span&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;.&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;docker run --rm -v &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;PWD&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;:/home/app scrumdog-local -j&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;This generates a &lt;code&gt;sample.jql&lt;/code&gt; file in the current directory that you can edit and use as a configuration file:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;docker run --rm -v &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;PWD&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;:/home/app scrumdog-local sample.jql&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
</content>
  </entry>
  
  <entry>
    <title>Quality &gt; Speed &gt; Scope</title>
    <link href="https://michaelheap.com/quality-speed-scope/"/>
    <updated>2025-02-23T13:56:47Z</updated>
    <id>https://michaelheap.com/quality-speed-scope/</id>
    <content type="html">&lt;p&gt;In the world of product development, prioritization is everything. Should we optimize for quality, ship as fast as possible, or build as many features as we can?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Never compromise on quality.&lt;/strong&gt;&lt;br /&gt;
A product that doesn’t work well, isn’t reliable, or doesn’t meet user expectations will ultimately fail. It doesn&#39;t matter how quickly you ship it or how many features it has. Users won’t stick around for a bad experience. A great product that solves a real problem with elegance and consistency will always win in the long run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Never compromise on speed.&lt;/strong&gt;&lt;br /&gt;
Perfect is the enemy of good. If you’re not shipping, all the work you&#39;ve done so far is delivering &lt;em&gt;zero&lt;/em&gt; value. It’s easy to get caught up in refining every detail, but a product that never sees the light of day doesn’t help anyone. Shipping fast ensures that you’re constantly learning, iterating, and improving based on real-world feedback.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compromise on scope.&lt;/strong&gt;&lt;br /&gt;
Instead of trying to build everything at once, focus on solving a narrow use case extremely well. Identify the core problem your users face, build the simplest and most effective solution for it, and get it in their hands as soon as possible. A great MVP isn’t about having the most features; it’s about having the right ones.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then, &lt;strong&gt;iterate relentlessly&lt;/strong&gt;. Ship, learn, refine, and ship again. Small, incremental progress compounds over time.&lt;/p&gt;
&lt;p&gt;If you follow this approach consistently, you’ll be amazed at what kind of product you can build in just six months. The best products don’t emerge from grand, ambitious plans. They evolve through &lt;em&gt;disciplined execution&lt;/em&gt; and &lt;em&gt;continuous improvement&lt;/em&gt;.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Using AWS credential_process and 1Password</title>
    <link href="https://michaelheap.com/aws-credential-helper-1password/"/>
    <updated>2025-02-18T17:15:37Z</updated>
    <id>https://michaelheap.com/aws-credential-helper-1password/</id>
    <content type="html">&lt;p&gt;A while back I read an excellent post from Paul Galow on &lt;a href=&quot;https://paulgalow.com/securing-aws-credentials-macos-lastpass&quot;&gt;securing AWS credentials with LastPass&lt;/a&gt;. I wanted exactly this, but with 1Password instead. Here&#39;s how to do it.&lt;/p&gt;
&lt;h2 id=&quot;configure-1password&quot; tabindex=&quot;-1&quot;&gt;Configure 1Password&lt;/h2&gt;
&lt;p&gt;If you don’t already have &lt;code&gt;op&lt;/code&gt; installed, you’ll need to install the &lt;a href=&quot;https://developer.1password.com/docs/cli/&quot;&gt;op CLI&lt;/a&gt; from the 1Password website.&lt;/p&gt;
&lt;p&gt;Once that’s done, create a new vault for storing secrets that are accessible from the CLI:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;op vault create CLI&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;Then create a new item in that vault, making sure to replace &lt;code&gt;XXX&lt;/code&gt; with your actual access key and secret:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; op item create --category Password --vault CLI --title AWSCredentials ACCESS_KEY=XXX SECRET_KEY=XXX&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;The above command is prefixed with a space so that it is not written to your shell history if &lt;code&gt;HIST_IGNORE_SPACE&lt;/code&gt; is enabled&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;create-the-credential_process&quot; tabindex=&quot;-1&quot;&gt;Create the credential_process&lt;/h2&gt;
&lt;p&gt;The AWS CLI has the ability to read credentials from a process rather than a static configuration file. It expects that the credential process will return a JSON document containing &lt;code&gt;AccessKeyid&lt;/code&gt; and &lt;code&gt;SecretAccessKey&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Copy and paste the following in to a terminal to create the credentials script:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; AWS_HELPER=&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;/bin/aws-1password&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;mkdir -p &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;/bin&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;readonly opVault=&quot;CLI&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;readonly opEntry=&quot;AWSCredentials&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;readonly accessKeyId=$(op read &quot;op://$opVault/$opEntry/ACCESS_KEY&quot;)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;readonly secretAccessKey=$(op read &quot;op://$opVault/$opEntry/SECRET_KEY&quot;)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;# Create JSON object that AWS CLI expects&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;jq -n \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;    --arg accessKeyId &quot;$accessKeyId&quot; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;    --arg secretAccessKey &quot;$secretAccessKey&quot; \&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;    &quot;.Version = 1&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;    | .AccessKeyId = \$accessKeyId&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;    | .SecretAccessKey = \$secretAccessKey&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&#39;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;AWS_HELPER&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;chmod +x &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;AWS_HELPER&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;h2 id=&quot;configure-aws&quot; tabindex=&quot;-1&quot;&gt;Configure AWS&lt;/h2&gt;
&lt;p&gt;The final thing to do is to create an AWS config file that uses that script to fetch authentication credentials:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;mkdir -p &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;/.aws&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #88C0D0&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;[default]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;credential_process = &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;AWS_HELPER&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;/.aws/config&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;To check if it worked, run &lt;code&gt;aws configure list&lt;/code&gt;. You should be prompted for your 1Password credentials to unlock the vault.&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;$ aws configure list&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      Name                    Value             Type    Location&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      ----                    -----             ----    --------&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;   profile                &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;not set&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;             None    None&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;access_key      &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;****************&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;XYZ   custom-process&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;secret_key      &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;****************&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;XYZ   custom-process&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    region                &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;not set&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;             None    None&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
</content>
  </entry>
  
  <entry>
    <title>Slack channels are free</title>
    <link href="https://michaelheap.com/slack-channels-are-free/"/>
    <updated>2025-02-12T20:51:45Z</updated>
    <id>https://michaelheap.com/slack-channels-are-free/</id>
    <content type="html">&lt;p&gt;“Should we create a new Slack channel for this?”&lt;/p&gt;
&lt;p&gt;I’ve not seen a topic as divisive as this since the days of tabs vs spaces (tl;dr: &lt;a href=&quot;https://adamtuttle.codes/blog/2021/tabs-vs-spaces-its-an-accessibility-issue/&quot;&gt;use tabs&lt;/a&gt;). Whenever there’s a suggestion to create a new channel, there are fervent discussions about why it’s a good/bad idea, with strong feelings on both sides.&lt;/p&gt;
&lt;p&gt;I’m firmly in camp “as many channels as needed, sometimes more channels than employees”. Once you hit a certain size, having a few firehose channels for everything means that 80%+ of the messages in a channel are irrelevant to the majority of the audience.&lt;/p&gt;
&lt;p&gt;Instead, spin up a new channel for each area of focus. This allows you to keep your working groups small (12 rather than 1200 people) and lets people focus on the channels that are important day to day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extra credit:&lt;/strong&gt; If you really want to help people focus, ensure that you use threads to prevent unread notifications. If you see a topic that you want to follow, click the “get notified about new replies” option in the context menu for the top level message.&lt;/p&gt;
&lt;h2 id=&quot;the-power-of-focused-channels&quot; tabindex=&quot;-1&quot;&gt;The Power of Focused Channels&lt;/h2&gt;
&lt;p&gt;Using specific channels for each topic has immediate benefits. Scoping conversations down to a topic rather than it being a free for all provides consumers with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Notification control&lt;/li&gt;
&lt;li&gt;Easier searching&lt;/li&gt;
&lt;li&gt;Single purpose channels (e.g. discussion, alerts etc)&lt;/li&gt;
&lt;li&gt;The correct audience&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Even Slack recommends using more, topic specific channels:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you set up multiple topic- and project-specific channels, groups can focus their discussions among smaller numbers of people, helping them to align and move faster. And having lots of specific channels means that each person can participate in fewer channels, because only a handful of them will be necessary for their daily work.&lt;br /&gt;
 &lt;br /&gt;
via &lt;a href=&quot;https://slack.com/intl/en-gb/resources/using-slack/how-to-organize-your-slack-channels&quot;&gt;https://slack.com/intl/en-gb/resources/using-slack/how-to-organize-your-slack-channels&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;fewer-unnecessary-notifications&quot; tabindex=&quot;-1&quot;&gt;Fewer Unnecessary Notifications&lt;/h3&gt;
&lt;p&gt;Being able to manage notifications is my favourite reason to use more Slack channels. Not all messages are created equally, but when they land in the same channel I have to manually read and filter the messages in my head.&lt;/p&gt;
&lt;p&gt;For concrete example, imagine a company that has a single &lt;code&gt;#general&lt;/code&gt; channel. It’s a place where company updates are shared, but also a place where people chat about the weekend. As a consumer I have to follow the channel in case I miss anything but the signal to noise ratio is low.&lt;/p&gt;
&lt;p&gt;Now imagine that this channel is split in to &lt;code&gt;#announcements&lt;/code&gt; and &lt;code&gt;#watercooler&lt;/code&gt;. I can safely mute or leave the water cooler channel while paying close attention to &lt;code&gt;#announcements&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;find-what-you-need%E2%80%94fast&quot; tabindex=&quot;-1&quot;&gt;Find What You Need—Fast&lt;/h3&gt;
&lt;p&gt;Once you have separate channels, searching becomes much easier. All you need is a keyword or two and the channel that you recall seeing the conversation in.&lt;/p&gt;
&lt;p&gt;When working on some new documentation, I searched in the &lt;code&gt;#support&lt;/code&gt; channel for “flubjam”, the name of the product I was working on (no, thats not the real product name). This surfaced all the issues customers were having with the product that I can now weave in to the updated documentation.&lt;/p&gt;
&lt;h3 id=&quot;dedicated-channels%2C-better-workflows&quot; tabindex=&quot;-1&quot;&gt;Dedicated Channels, Better Workflows&lt;/h3&gt;
&lt;p&gt;If we take the notification control benefit and supercharge it, we get to “single use channels”. Slack can do so much more than be a chat room for your team.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;large&lt;/em&gt; portion of my Slack usage is monitoring events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new post on Stack Overflow with a specific tag goes to &lt;code&gt;#stack-overflow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;New GitHub issues and releases for the flubjam project are piped in to &lt;code&gt;#notify-flubjam&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;All GitHub activity for the flubjam project is piped in to &lt;code&gt;#firehose-flubjam&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Posts elsewhere on Slack that get a floppy disk icon are cross posted to &lt;code&gt;#zmeta-saved&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Creating single use channels gives people the information that they need, when they need it.&lt;/p&gt;
&lt;h3 id=&quot;reach-the-right-people%2C-every-time&quot; tabindex=&quot;-1&quot;&gt;Reach the Right People, Every Time&lt;/h3&gt;
&lt;p&gt;Finally, using single purpose channels means that your audience is probably the one you’re looking for. Instead of blasting your question to 1200 people in a general channel, you can ask a targeted group your specific question.&lt;/p&gt;
&lt;p&gt;I find this much easier than trying to figure out who’s involved in a project to set up a five way DM (which will inevitably miss out a key person who then feels slighted that I forgot them).&lt;/p&gt;
&lt;h2 id=&quot;finding-the-right-channels&quot; tabindex=&quot;-1&quot;&gt;Finding the Right Channels&lt;/h2&gt;
&lt;p&gt;So if focused channels are so great, why don’t we all do it? There’s an easy answer:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Finding new channels is hard&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There are a couple of ways to solve this problem. The most effective ones I’ve seen are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have a channel that announces new channels (very meta!)&lt;/li&gt;
&lt;li&gt;Set a reminder to check for new channels weekly using the Channels-&amp;gt;Browse Channels option. Usually there haven’t been that many created in the last 7 days.&lt;/li&gt;
&lt;li&gt;You can rely on people to invite you. Usually they won’t invite you directly. Instead, they’ll mention you by your Slack username, realise that you’re not there and then click “invite”. This means you only join channels when there is something for you to actively participate in&lt;/li&gt;
&lt;li&gt;Finally, update your onboarding documentation with links to relevant channels (you do have onboarding documentation, right? 😁)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-should-you-post%3F&quot; tabindex=&quot;-1&quot;&gt;Where Should You Post?&lt;/h2&gt;
&lt;p&gt;Ok, so you have lots of tightly scoped channels. You’ve joined the channels relevant to your day to day work, and now you have a question about the “flubjam” project.&lt;/p&gt;
&lt;p&gt;Do you post in &lt;code&gt;#team-product&lt;/code&gt;, &lt;code&gt;#flubjam-is-cool&lt;/code&gt; or &lt;code&gt;#general&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Channel naming schemes can help with this. One of the patterns I’ve seen at a few different places now is to have an “&lt;code&gt;#ask-team-name&lt;/code&gt;” channel that is public. Have a question for finance? &lt;code&gt;#ask-finance&lt;/code&gt;! Curious about the product roadmap? &lt;code&gt;#ask-product&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;If you’re small enough that you have 1-3 products, keeping a single &lt;code&gt;#ask-product&lt;/code&gt; channel makes sense.&lt;/p&gt;
&lt;p&gt;As you expand your product offering, you may want to expand to “&lt;code&gt;#ask-product-name&lt;/code&gt;” style channels, such as “&lt;code&gt;#ask-dev-portal&lt;/code&gt;”. This allows the &lt;a href=&quot;https://medium.com/design-bootcamp/the-power-of-the-product-triad-0e76801a384d&quot;&gt;product triad&lt;/a&gt; to answer the questions as a team rather than the questions going to a specific department all the time.&lt;/p&gt;
&lt;p&gt;Finally, each product is made up of various initiatives and deliverables. You likely have a “&lt;code&gt;#private-team-a&lt;/code&gt;” channel that you use to discuss ongoing projects, but I encourage you to bring that conversation in to “&lt;code&gt;#project-name&lt;/code&gt;” channels. Not only does it give you a single place to read for any given project, it invites collaboration with other teams who may have suggestions based on their experience. These channels are short lived and should be archived as soon as the project is completed.&lt;/p&gt;
&lt;p&gt;To recap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#ask-team-name&lt;/code&gt; to get started&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#ask-product-name&lt;/code&gt; as the team grows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#project-name&lt;/code&gt; to stop conversation sprawl and build a culture of collaboration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With concrete examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#ask-product&lt;/code&gt; for general product questions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#ask-flubjam&lt;/code&gt; for all flubjam related questions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#project-flubjam-capacitors&lt;/code&gt; is a short term channel to talk about implementing capacitors in flubjam&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;create-that-slack-channel&quot; tabindex=&quot;-1&quot;&gt;Create That Slack Channel&lt;/h2&gt;
&lt;p&gt;Focused channels improve notification control, make searching easier, ensure the right audience sees your messages, and unlock powerful workflows beyond just chat. While discoverability can be a challenge, a few simple strategies—like clear naming conventions—can make it easy for people to find the right spaces.&lt;/p&gt;
&lt;p&gt;At the end of the day, good Slack hygiene isn’t about having fewer channels—it’s about having the &lt;strong&gt;right&lt;/strong&gt; channels.&lt;/p&gt;
&lt;h2 id=&quot;update%3A-super-bonus-content&quot; tabindex=&quot;-1&quot;&gt;Update: Super Bonus Content&lt;/h2&gt;
&lt;p&gt;I shared a preview of this post with some colleagues and &lt;a href=&quot;https://www.linkedin.com/in/jasonhnaustin&quot;&gt;Jason&lt;/a&gt; had some great feedback on how to cope with channel sprawl:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Housecleaning built-in:&lt;/strong&gt; If you have a topic that is not meant to be long-lived, a convention that flags the channel (usually &lt;code&gt;#temp-&lt;/code&gt;) signifies that when the topic is resolved, the channel dies. This helps address the channel sprawl problem, and intentionally redirects folks back to long-lived comms channels, vs tactical problem solving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join many, star few:&lt;/strong&gt; the likelihood that you keep up with the conversation in every channel you join, in a &amp;quot;Slack channels are free&amp;quot; environment, is pretty much zero. Missing critical notifications on people you are working with regularly is a big risk to productivity, and job security in some cases. Star key channels, and mark your closest collaborators and reporting chain as &amp;quot;VIP&amp;quot;, so they are on a short-list to focus your attention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ruthless prioritization on &amp;quot;Leave Channel&amp;quot;:&lt;/strong&gt; if you&#39;re not contributing or engaging in a channel, leave it immediately. If you&#39;re mentioned, you&#39;ll be notified, and you can rejoin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As someone that left &lt;em&gt;175&lt;/em&gt; Slack channels in his January cleanup, that last point on ruthless priorization is key. I&#39;m already back in some of them, but I know it&#39;s because I&#39;m actively engaged rather than through inertia.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Excellent Pull Request Reviews</title>
    <link href="https://michaelheap.com/pull-request-reviews/"/>
    <updated>2025-02-05T03:55:49Z</updated>
    <id>https://michaelheap.com/pull-request-reviews/</id>
    <content type="html">&lt;p&gt;Pull request reviews are a critical part of building high-quality products, but too often, they become a rubber-stamping exercise—skim, “LGTM,” approve. This kind of review can lead to broken code, unclear documentation, and missed opportunities for improvement.&lt;/p&gt;
&lt;p&gt;A great PR review isn’t just about glancing at the code; it’s about &lt;strong&gt;running the changes&lt;/strong&gt;, &lt;strong&gt;thinking critically about what’s missing&lt;/strong&gt;, and &lt;strong&gt;providing actionable feedback&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;run-the-thing&quot; tabindex=&quot;-1&quot;&gt;Run the thing&lt;/h2&gt;
&lt;p&gt;If I could only have one rule for reviewing pull requests, it would be “did you run it?”. Far too often I see people skimming over the content, giving it a “LGTM” and pressing approve.&lt;/p&gt;
&lt;p&gt;This happens &lt;em&gt;everywhere&lt;/em&gt;. My most recent example is a project that is migrating documentation from one platform to another. All of the content already exists, so it’s just about reformatting and publishing it elsewhere, right?&lt;/p&gt;
&lt;p&gt;No. No, no, &lt;em&gt;no&lt;/em&gt;. You see, a lot of the documentation doesn’t work. Maybe it didn’t work when it was originally published (“LGTM!”). Maybe it did, and it’s rotted over time and no longer works.&lt;/p&gt;
&lt;p&gt;So now we have a new definition of done for the migration work. Before you submit a pull request for review, you need to work through the content from top to bottom and ensure it works. Then your reviewer has to do the same.&lt;/p&gt;
&lt;p&gt;Having two people test the same thing ensures that the documentation is clear, isn’t missing implicit knowledge and most of all, that it works.&lt;/p&gt;
&lt;p&gt;If you’re building software rather than writing docs, good news, the same process works! Having two people &lt;em&gt;use&lt;/em&gt; the software makes sure that it meets the requirements, uncovers edge cases and generally helps build better products.&lt;/p&gt;
&lt;h2 id=&quot;what%E2%80%99s-missing%3F&quot; tabindex=&quot;-1&quot;&gt;What’s missing?&lt;/h2&gt;
&lt;p&gt;Looking at a pull request and saying “LGTM” is only half of the review. You also need to think about what you’re &lt;strong&gt;not&lt;/strong&gt; looking at. What should be there, but isn’t?&lt;/p&gt;
&lt;p&gt;Are there any missing tests? Missing docs? How about example configuration or usage examples for the feature that you just added?&lt;/p&gt;
&lt;p&gt;Spotting what isn’t there is one of the hardest things to learn when reviewing PRs, but it’s also one of the most impactful. Being able to say “this looks great, but I think we should add a test for when X” helps build better products.&lt;/p&gt;
&lt;h2 id=&quot;is-this-blocking%3F&quot; tabindex=&quot;-1&quot;&gt;Is this blocking?&lt;/h2&gt;
&lt;p&gt;As you add more comments to a pull request (and you &lt;em&gt;will&lt;/em&gt; add more comments if you’re actually running what’s there and thinking about what’s missing), it can be hard to understand which pieces of feedback are opinions, and which require changes.&lt;/p&gt;
&lt;p&gt;To make it obvious for the original author, I recommend prefixing feedback if it doesn’t need to be actioned immediately.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;No prefix&lt;/em&gt;: This is important feedback for the current PR&lt;/li&gt;
&lt;li&gt;&lt;em&gt;future&lt;/em&gt;: Could we refactor this into something that’s reusable?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;nit&lt;/em&gt;: I find it easier to parse when using early returns rather than else blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By following this pattern of feedback you can ensure that you provide all the context you have in the PR without worrying about preventing the original author from shipping.&lt;/p&gt;
&lt;p&gt;I’ve been on the receiving end of this type of feedback, and it’s been great to learn what &lt;em&gt;could&lt;/em&gt; be done given more time. I don’t always go back and fix the &lt;em&gt;nit&lt;/em&gt; comments, but it always makes me think about what I can do differently next time.&lt;/p&gt;
&lt;h2 id=&quot;do-the-work&quot; tabindex=&quot;-1&quot;&gt;Do the work&lt;/h2&gt;
&lt;p&gt;Last, but by no means least, do the work yourself. I don’t mean “pull down the branch and make changes” (though in at least one of my teams that &lt;em&gt;is&lt;/em&gt; the default operating model and it works well).&lt;/p&gt;
&lt;p&gt;I mean use GitHub’s suggestions macro when reviewing a pull request. If you have a suggestion how to improve something, instead of trying to explain it in text you can edit the lines yourself using the following format:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;bash&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;```&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;suggestion&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;This line will be suggested instead of the one that you selected&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;```&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;Not only is making the actual change easier to parse for the original author, they can quickly apply any changes they agree with by clicking “add to batch” in the web UI.&lt;/p&gt;
&lt;h2 id=&quot;go-forth-and-review&quot; tabindex=&quot;-1&quot;&gt;Go forth and review&lt;/h2&gt;
&lt;p&gt;By making sure the code works, thinking critically about what’s missing, and clearly distinguishing between blocking and non-blocking comments, reviewers can significantly improve the quality of the product. And when possible, making direct suggestions instead of just pointing out issues can streamline the process for everyone.&lt;/p&gt;
&lt;p&gt;A great review isn’t just about catching issues. It&#39;s about fostering collaboration, improving maintainability, and ultimately, building better products. So next time you review a PR, take the time to do it right. Your team (and your future self) will thank you.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Run go test -tags in VSCode</title>
    <link href="https://michaelheap.com/go-test-tags/"/>
    <updated>2025-01-10T13:01:15Z</updated>
    <id>https://michaelheap.com/go-test-tags/</id>
    <content type="html">&lt;p&gt;I&#39;ve been writing more Go recently, and the &lt;a href=&quot;https://code.visualstudio.com/docs/languages/go#_test&quot;&gt;Go extension&lt;/a&gt; for VSCode provides an awesome &lt;em&gt;Run Test&lt;/em&gt; button inline.&lt;/p&gt;
&lt;p&gt;In many of our projects, we have a combination of unit and integration tests. To keep &lt;code&gt;go test&lt;/code&gt; quick, the integration tests have an &lt;code&gt;integration&lt;/code&gt; tag enabled. This means that the &lt;em&gt;Run Test&lt;/em&gt; button doesn&#39;t work for integration tests.&lt;/p&gt;
&lt;p&gt;To make them work, add the following to &lt;code&gt;.vscode/settings.json&lt;/code&gt; in your project:&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;json&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;{&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;go.testFlags&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;-v&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;],&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #8FBCBB&quot;&gt;go.testTags&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;integration&lt;/span&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #ECEFF4&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
</content>
  </entry>
  
  <entry>
    <title>Flatten nested foreach loops with Terraform</title>
    <link href="https://michaelheap.com/terraform-flatten-nested-loops/"/>
    <updated>2024-09-12T09:26:22Z</updated>
    <id>https://michaelheap.com/terraform-flatten-nested-loops/</id>
    <content type="html">&lt;p&gt;Given a list of identifiers and an associated list of values, here&#39;s how to flatten them all down to a single list of unique values.&lt;/p&gt;
&lt;pre class=&quot;shiki nord&quot; style=&quot;background-color: #2e3440ff; color: #d8dee9ff&quot;&gt;&lt;div class=&quot;language-id&quot;&gt;hcl&lt;/div&gt;&lt;div class=&quot;code-container&quot;&gt;&lt;code&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# Define your input data. We&#39;ll convert this in to a list of ${name}-${version} values&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;variable&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt; &quot;ai_providers&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; list(object({&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    name     = string&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    versions = list(string)&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  }))&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;default&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      name     = &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;OpenAI&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      versions = [&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;gpt-3.5-turbo&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;gpt-4o&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    },&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;     &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;Anthropic&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;versions&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;Opus&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;Sonnet&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;Haiku&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    },&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;     &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;Mistral&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;versions&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;7B&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;8x7B&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;8x22B&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    },&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;     &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;llama3&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;versions&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;8B&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;70B&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    },&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  ]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;## Flatmap using nested loops and flatten()&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;locals&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;ai_providers_flattened&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; flatten([&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    for provider in var&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;ai_providers : [&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      for version in provider&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;versions : {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        version  = version&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        provider = provider&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;        key      = &lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;${provider&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;.name&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;${version}&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;      }&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;    ]&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  ])&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #616E88&quot;&gt;# Iterate over the flattened list to create resources&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;resource&lt;/span&gt;&lt;span style=&quot;color: #A3BE8C&quot;&gt; &quot;demo_foo&quot; &quot;my_resource&quot;&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; {&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;for_each&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; { for api in local.ai_providers_flattened : api.key =&amp;gt; api }&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;     &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; each&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;provider&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #D8DEE9&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #81A1C1&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt; each&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color: #B48EAD&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;version&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;line&quot;&gt;&lt;span style=&quot;color: #D8DEE9FF&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;for_each = { for api in local.ai_providers_flattened : api.key =&amp;gt; api }&lt;/code&gt; line confused me for a while. This line is used to identify the resource later as &lt;code&gt;my_resource&lt;/code&gt; is now a map of objects rather than a single object. Let&#39;s walk through it step by step&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;demo_foo.my_resource&lt;/code&gt; is a map of objects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;demo_foo.my_resource[api.key]&lt;/code&gt; points to a single resource&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.key&lt;/code&gt; is defined as &lt;code&gt;&amp;quot;${provider.name}-${version}&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Details for the &lt;code&gt;demo_foo&lt;/code&gt; resource for OpenAI gpt-4o is available at &lt;code&gt;demo_foo.my_resource[&amp;quot;OpenAI-gpt-4o&amp;quot;]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title>Bind a Hyper/Meh key with Keychron Launcher</title>
    <link href="https://michaelheap.com/keychron-launcher-hyper-meh/"/>
    <updated>2024-09-05T07:55:33Z</updated>
    <id>https://michaelheap.com/keychron-launcher-hyper-meh/</id>
    <content type="html">&lt;p&gt;I&#39;ve been using a &lt;a href=&quot;https://www.keychron.com/products/keychron-q1-he-qmk-wireless-custom-keyboard&quot;&gt;Keychron Q1 HE&lt;/a&gt; this week instead of my trusty Ergodox EZ as I&#39;ve been travelling.&lt;/p&gt;
&lt;p&gt;One of the biggest issues I had is that none of my keyboard shortcuts were working as I didn&#39;t have a &lt;code&gt;hyper&lt;/code&gt; key bound. I could have used something like Karabiner to rebind keys, but I knew I could do it at the keyboard level.&lt;/p&gt;
&lt;p&gt;It took me far too long to figure out, so I&#39;m writing it down for future me.&lt;/p&gt;
&lt;h2 id=&quot;configuring-hyper%2Fmeh-keys&quot; tabindex=&quot;-1&quot;&gt;Configuring Hyper/Meh keys&lt;/h2&gt;
&lt;p&gt;First of all, VIA App doesn&#39;t work for the Q1 HE. You have to use the Keychron configuration UI at &lt;a href=&quot;https://www.launcher.keychron.com/#/keymap&quot;&gt;https://www.launcher.keychron.com/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I learned about the &lt;code&gt;Any&lt;/code&gt; key definition from &lt;a href=&quot;https://www.reddit.com/r/Keychron/comments/17yv7nk/comment/ks710n9/&quot;&gt;this useful Reddit comment&lt;/a&gt;. On the keymap screen, select &lt;code&gt;Custom&lt;/code&gt; then &lt;code&gt;Any&lt;/code&gt;. It will show a text input where you should put the keys to send.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Be aware - the key that you&#39;re binding will change as you type on the keyboard. After entering the keys, click on the key that you want to bind before pressing Enter.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To create a &lt;code&gt;Hyper&lt;/code&gt; (Left Control + Shift + Alt + GUI) key, enter &lt;code&gt;HYPR(KC_NO)&lt;/code&gt;. To create a &lt;code&gt;Meh&lt;/code&gt; (Left Control + Shift + Alt) key, enter &lt;code&gt;MEH(KC_NO)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;GUI&lt;/code&gt; key is &lt;code&gt;cmd&lt;/code&gt; on MacOS, and the Windows key on Windows.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;KC_NO&lt;/code&gt; parameter tells QMK not to send any additional keycodes with the modifiers. This allows you to press other keys on the keyboard.&lt;/p&gt;
&lt;p&gt;Personally, I like to define a &lt;code&gt;LCAG(KC_NO)&lt;/code&gt; (Left Control + Alt + GUI) key. This means I can have a single key as my keyboard trigger, and double the number of available shortcuts by additionally pressing &lt;code&gt;Shift&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;learn-more-about-qmk&quot; tabindex=&quot;-1&quot;&gt;Learn more about QMK&lt;/h2&gt;
&lt;p&gt;The full list of modifier keys is available in the &lt;a href=&quot;https://docs.qmk.fm/feature_advanced_keycodes#modifier-keys&quot;&gt;QMK docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&#39;ve used prebuilt modifiers such as &lt;code&gt;HYPR&lt;/code&gt; and &lt;code&gt;LCAG&lt;/code&gt;, but you can also nest them to build composite modifiers. e.g. Send &lt;code&gt;ctrl+alt&lt;/code&gt; with &lt;code&gt;LCTL(LALT(KC_NO))&lt;/code&gt;. You can change &lt;code&gt;KC_NO&lt;/code&gt; for a key to send a full key combination, e.g. &lt;code&gt;ctrl+alt+delete&lt;/code&gt; with &lt;code&gt;LCTL(LALT(KC_DEL))&lt;/code&gt;&lt;/p&gt;
</content>
  </entry>
</feed>