<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Blog - James Mead</title>
  <id>tag:jamesmead.org,2005:Typo</id>
  <link href="https://feeds.jamesmead.org/floehopper-blog" rel="self" type="application/atom+xml"/>
  <link href="https://jamesmead.org/" rel="alternate" type="text/html"/>
  <updated>2026-05-04T15:53:47+00:00</updated>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:b3bc505e-934c-4a37-a694-e171e76bad03</id>
    <published>2026-04-19T10:13:00+01:00</published>
    <updated>2026-04-19T10:13:00+01:00</updated>
    <title>Organisation-specific git authentication and commit signing</title>
    <link href="https://jamesmead.org/blog/2026-04-19-organisation-specific-git-authentication-and-commit-signing" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;Recently I wanted to use a separate SSH key for work on repositories in a specific GitHub organisation, but continue to use a single GitHub user account. I already had &lt;a href=&quot;&#x2F;blog&#x2F;2018-09-06-organisation-specific-git-config&quot;&gt;some organisation-specific git configuration&lt;&#x2F;a&gt; which sets the email address used in commit notes and I&#x27;ve now managed to extend that to incorporate using an organisation-specific SSH key both for authentication with GitHub and for signing my commits.&lt;&#x2F;p&gt;

&lt;p&gt;One complication was that I wanted to continue to manage my SSH keys in 1Password which I use as my &lt;a href=&quot;https:&#x2F;&#x2F;developer.1password.com&#x2F;docs&#x2F;ssh&#x2F;agent&#x2F;&quot;&gt;SSH agent&lt;&#x2F;a&gt;. I struggled to find a way to make the 1Password SSH agent offer the correct organisation-specific key, but eventually worked out that I could set &lt;a href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-config#Documentation&#x2F;git-config.txt-coresshCommand&quot;&gt;&lt;code&gt;core.sshCommand&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; in my organisation-specific git config to override the default &lt;code&gt;ssh&lt;&#x2F;code&gt; command and specify the relevant public key using the &lt;code&gt;-i&lt;&#x2F;code&gt; (identity file) option. Although this feels like a bit of a hack, the only downside I can see is that I had to export each of the public keys to my &lt;code&gt;~&#x2F;.ssh&lt;&#x2F;code&gt; directory which effectively introduces duplication, but it&#x27;s duplication that I can live with.&lt;&#x2F;p&gt;

&lt;p&gt;While testing this configuration I came across the &lt;a href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-config#Documentation&#x2F;git-config.txt-gpgsshallowedSignersFile&quot;&gt;&lt;code&gt;gpg &quot;ssh&quot;.allowedSignersFile&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; git configuration option which means you can use &lt;code&gt;git log --show-signature&lt;&#x2F;code&gt; to check your commits have been signed correctly.&lt;&#x2F;p&gt;

&lt;h2 id=&quot;password-configuration&quot;&gt;1Password configuration&lt;&#x2F;h2&gt;

&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.1password.com&#x2F;docs&#x2F;ssh&#x2F;manage-keys&#x2F;#generate-an-ssh-key&quot;&gt;Using these instructions&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;Generate an &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;EdDSA#Ed25519&quot;&gt;Ed25519&lt;&#x2F;a&gt; SSH key for organisation 1&lt;&#x2F;li&gt;
  &lt;li&gt;Generate an &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;EdDSA#Ed25519&quot;&gt;Ed25519&lt;&#x2F;a&gt; SSH key for organisation 2&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;p&gt;And then download the public key for each to:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;github-organisation-1.pub&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;code&gt;~&#x2F;.ssh&#x2F;github-organisation-2.pub&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h2 id=&quot;github-configuration&quot;&gt;GitHub configuration&lt;&#x2F;h2&gt;

&lt;h3 id=&quot;emails&quot;&gt;Emails&lt;&#x2F;h3&gt;

&lt;p&gt;In &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;settings&#x2F;emails&quot;&gt;your GitHub email settings&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;Add &amp;amp; verify &lt;code&gt;email.address@organisation-1.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;Add &amp;amp; verify &lt;code&gt;email.address@organisation-2.com&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h3 id=&quot;ssh-keys&quot;&gt;SSH keys&lt;&#x2F;h3&gt;

&lt;p&gt;In &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;settings&#x2F;ssh&#x2F;&quot;&gt;your GitHub SSH key settings&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;Add SSH key for organisation 1 as authentication key&lt;&#x2F;li&gt;
  &lt;li&gt;Add SSH key for organisation 2 as authentication key&lt;&#x2F;li&gt;
  &lt;li&gt;Add SSH key for organisation 1 as signing key&lt;&#x2F;li&gt;
  &lt;li&gt;Add SSH key for organisation 2 as signing key&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h2 id=&quot;ssh-configuration&quot;&gt;SSH configuration&lt;&#x2F;h2&gt;

&lt;p&gt;Configure ssh to use 1Password as its agent. And list allowed signers for each organisation to be used as &lt;code&gt;allowedSignersFile&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code&gt;
    # ~&#x2F;.ssh&#x2F;config
    Host *
      IdentityAgent &amp;lt;path-to-1password-agent.sock&amp;gt;

    # ~&#x2F;.ssh&#x2F;allowed_signers_for_organisation_1
    email.address@organisation-1.com ssh-ed25519 &amp;lt;ssh-public-key-for-organisation-1&amp;gt;

    # ~&#x2F;.ssh&#x2F;allowed_signers_for_organisation_2
    email.address@organisation-2.com ssh-ed25519 &amp;lt;ssh-public-key-for-organisation-2&amp;gt;
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;h2 id=&quot;git-configuration&quot;&gt;Git configuration&lt;&#x2F;h2&gt;

&lt;h3 id=&quot;shared&quot;&gt;Shared&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.1password.com&#x2F;docs&#x2F;ssh&#x2F;git-commit-signing&quot;&gt;Sign commits using SSH keys in 1Password&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;2018-09-06-organisation-specific-git-config&quot;&gt;Include organisation-specific configurations&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;pre&gt;
  &lt;code&gt;
    # ~&#x2F;.gitconfig
    [gpg]
      format = ssh
    
    [gpg &quot;ssh&quot;]
      program = &#x2F;Applications&#x2F;1Password.app&#x2F;Contents&#x2F;MacOS&#x2F;op-ssh-sign
    
    [commit]
      gpgsign = true
    
    [includeIf &quot;gitdir:~&#x2F;Code&#x2F;organisation-1&#x2F;**&quot;]
      path = ~&#x2F;.config&#x2F;git&#x2F;organisation-1.inc
    
    [includeIf &quot;gitdir:~&#x2F;Code&#x2F;organisation-2&#x2F;**&quot;]
      path = ~&#x2F;.config&#x2F;git&#x2F;organisation-2.inc
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;h3 id=&quot;organisation-1&quot;&gt;Organisation 1&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;Use SSH key for organisation 1 to authenticate with GitHub&lt;&#x2F;li&gt;
  &lt;li&gt;Use SSH key for organisation 1 to sign commits&lt;&#x2F;li&gt;
  &lt;li&gt;Specify allowed signers for organisation 1 (used by e.g. &lt;code&gt;git log --show-signature&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;pre&gt;
  &lt;code&gt;
    # ~&#x2F;.config&#x2F;git&#x2F;organisation-1.inc
    
    [user]
      email = email.address@organisation-1.com
      signingkey = ssh-ed25519 &amp;lt;ssh-public-key-for-organisation-1&amp;gt;
    
    [core]
      sshCommand = &quot;ssh -i ~&#x2F;.ssh&#x2F;github-organisation-1.pub&quot;
    
    [gpg &quot;ssh&quot;]
      allowedSignersFile = ~&#x2F;.ssh&#x2F;allowed_signers_for_organisation_1
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;h3 id=&quot;organisation-2&quot;&gt;Organisation 2&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;Use SSH key for organisation 2 to authenticate with GitHub&lt;&#x2F;li&gt;
  &lt;li&gt;Use SSH key for organisation 2 to sign commits&lt;&#x2F;li&gt;
  &lt;li&gt;Specify allowed signers for organisation 2 (used by e.g. &lt;code&gt;git log --show-signature&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;pre&gt;
  &lt;code&gt;
    # ~&#x2F;.config&#x2F;git&#x2F;organisation-2.inc
    [user]
      email = email.address@organisation-2.com
      signingkey = ssh-ed25519 &amp;lt;ssh-public-key-for-organisation-2&amp;gt;
    
    [core]
      sshCommand = &quot;ssh -i ~&#x2F;.ssh&#x2F;github-organisation-2.pub&quot;
    
    [gpg &quot;ssh&quot;]
      allowedSignersFile = ~&#x2F;.ssh&#x2F;allowed_signers_for_organisation_2
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:58190639-2d4e-408c-9bdb-9f0e4baf2531</id>
    <published>2026-04-04T21:30:00+01:00</published>
    <updated>2026-04-04T21:30:00+01:00</updated>
    <title>Adding human.json to my website</title>
    <link href="https://jamesmead.org/blog/2026-04-04-adding-human-json-to-my-website" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;Prompted by reading &lt;a href=&quot;https:&#x2F;&#x2F;tzovar.as&#x2F;maintaining-a-human-web-with-humans-json-aiblacklist&#x2F;&quot;&gt;this blog post by Bastian Tzovaras&lt;&#x2F;a&gt;, I&#x27;ve added &lt;a href=&quot;&#x2F;human.json&quot;&gt;a &lt;code&gt;human.json&lt;&#x2F;code&gt; file&lt;&#x2F;a&gt; to this website to assert my authorship of the content and vouch for the &quot;humanity&quot; of other websites. &lt;a href=&quot;https:&#x2F;&#x2F;codeberg.org&#x2F;robida&#x2F;human.json&#x2F;src&#x2F;commit&#x2F;ef5adcdea90fe935d5ac3cf983006585fbae4134&#x2F;README.md&quot;&gt;The human.json protocol&lt;&#x2F;a&gt; uses URL ownership as identity and trust propagates through a crawlable web of &quot;vouches&quot; between sites. I&#x27;ve also added &lt;a href=&quot;https:&#x2F;&#x2F;slashpages.net&#x2F;&quot;&gt;an IndieWeb slashpage&lt;&#x2F;a&gt; to explain that &lt;a href=&quot;&#x2F;ai&quot;&gt;I don&#x27;t use generative AI to create content on this website&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;If we know each other and you avoid using &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Generative_AI&quot;&gt;generative AI&lt;&#x2F;a&gt; on your website, &lt;a href=&quot;&#x2F;contact&quot;&gt;let me know&lt;&#x2F;a&gt; and I&#x27;ll add a &quot;vouch&quot; for you.&lt;&#x2F;p&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:097d2eef-e198-4274-8d7e-bdfcd4aeb145</id>
    <published>2025-03-30T17:05:00+00:00</published>
    <updated>2025-03-30T17:05:00+00:00</updated>
    <title>Configuring Zed editor to use HTML Beautifier as a formatter in ERB templates</title>
    <link href="https://jamesmead.org/blog/2025-03-30-configuring-zed-editor-to-use-htmlbeautifier-as-formatter-in-erb-template" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;Following on from my previous post about &lt;a href=&quot;&#x2F;blog&#x2F;2025-03-30-configuring-zed-editor-to-use-erb-lint-as-formatter&quot;&gt;configuring Zed editor to use ERB Lint as a formatter&lt;&#x2F;a&gt;, here&#x27;s how to configure &lt;a href=&quot;https:&#x2F;&#x2F;zed.dev&#x2F;&quot;&gt;Zed&lt;&#x2F;a&gt; to use &lt;a href=&quot;https:&#x2F;&#x2F;po-ru.com&#x2F;&quot;&gt;Paul&lt;&#x2F;a&gt;&#x27;s excellent &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;threedaymonk&#x2F;htmlbeautifier&quot;&gt;HTML Beautifier&lt;&#x2F;a&gt; as a formatter in ERB templates.&lt;&#x2F;p&gt;

&lt;p&gt;Configure HTML Beautifier as &lt;a href=&quot;https:&#x2F;&#x2F;zed.dev&#x2F;docs&#x2F;configuring-languages?highlight=formatter#configuring-formatters&quot;&gt;a formatter&lt;&#x2F;a&gt; for ERB files as well as HTML files by adding the following JSON to your Zed settings, either global or project-specific:&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    {
      &quot;languages&quot;: {
        &quot;ERB&quot;: {
          &quot;formatter&quot;: [
            {
              &quot;external&quot;: {
                &quot;command&quot;: &quot;bundle&quot;,
                &quot;arguments&quot;: [&quot;exec&quot;, &quot;htmlbeautifier&quot;, &quot;--keep-blank-lines&quot;, &quot;1&quot;]
              }
            }
          ]
        },
        &quot;HTML&quot;: {
          &quot;formatter&quot;: [
            {
              &quot;external&quot;: {
                &quot;command&quot;: &quot;bundle&quot;,
                &quot;arguments&quot;: [&quot;exec&quot;, &quot;htmlbeautifier&quot;, &quot;--keep-blank-lines&quot;, &quot;1&quot;]
              }
            }
          ]
        }
      }
    }
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;p&gt;Note that I&#x27;ve added the HTML Beautifier formatter configuration &lt;em&gt;after&lt;&#x2F;em&gt; the ERB Lint formatter configuration for ERB files and that seems to work well.&lt;&#x2F;p&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:c6548610-46c8-4f3d-853f-b28d5d389475</id>
    <published>2025-03-30T15:46:00+00:00</published>
    <updated>2025-03-30T15:46:00+00:00</updated>
    <title>Configuring Zed editor to use ERB Lint as a formatter</title>
    <link href="https://jamesmead.org/blog/2025-03-30-configuring-zed-editor-to-use-erb-lint-as-formatter" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;I&#x27;ve recently started using the &lt;a href=&quot;https:&#x2F;&#x2F;zed.dev&#x2F;&quot;&gt;Zed editor&lt;&#x2F;a&gt; instead of VS Code. It took me a
while to work out how to configure &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Shopify&#x2F;erb_lint&quot;&gt;ERB Lint&lt;&#x2F;a&gt; as a formatter, so hopefully this
will save someone else some time.&lt;&#x2F;p&gt;

&lt;p&gt;Write a shell script to run ERB Lint as follows:&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    #!&#x2F;usr&#x2F;bin&#x2F;env sh

    bundle exec erb_lint --autocorrect --stdin $1 2&amp;gt;&#x2F;dev&#x2F;null \
      | sed &#x27;1,&#x2F;^================&#x2F;d&#x27;
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;ul&gt;
  &lt;li&gt;Enable the option to automatically correct linting errors (&lt;code&gt;--autocorrect&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
  &lt;li&gt;Read the file content from standard input and specify the path supplied in
the first command-line argument (&lt;code&gt;--stdin $1&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
  &lt;li&gt;Suppress irrelevant warnings &amp;amp; error messages (&lt;code&gt;2&amp;gt;&#x2F;dev&#x2F;null&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
  &lt;li&gt;Remove the preamble from the output to make it suitable for a Zed editor
formatter (&lt;code&gt;sed &#x27;1,&#x2F;^================&#x2F;d&#x27;&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
  &lt;li&gt;Ensure the script is executable (&lt;code&gt;chmod +x erb-lint-formatter&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;p&gt;Configure ERB Lint as &lt;a href=&quot;https:&#x2F;&#x2F;zed.dev&#x2F;docs&#x2F;configuring-languages?highlight=formatter#configuring-formatters&quot;&gt;a formatter&lt;&#x2F;a&gt; by adding the
following JSON to your Zed settings, either global or project-specific,
replacing &lt;code&gt;$PATH_TO_FORMATTER&lt;&#x2F;code&gt; with the path to the shell script you created
above:&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    {
      &quot;languages&quot;: {
        &quot;ERB&quot;: {
          &quot;formatter&quot;: [
            {
              &quot;external&quot;: {
                &quot;command&quot;: &quot;$PATH_TO_FORMATTER&#x2F;erb-lint-formatter&quot;,
                &quot;arguments&quot;: [&quot;{buffer_path}&quot;]
              }
            }
          ]
        }
      }
    }
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;p&gt;Zed sets the &lt;code&gt;{buffer_path}&lt;&#x2F;code&gt; placeholder to the path of the buffer currently
being edited.&lt;&#x2F;p&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:2bfd318b-4c2a-47ce-b545-0ace0fe5caad</id>
    <published>2022-11-01T14:01:00+00:00</published>
    <updated>2022-11-01T14:01:00+00:00</updated>
    <title>Mocha v2 release</title>
    <link href="https://jamesmead.org/blog/2022-11-01-mocha-v2-release" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;This major version bump of the Ruby mock object library, &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;&quot;&gt;Mocha&lt;&#x2F;a&gt;, includes some fairly significant changes. So I wanted to expand a bit on the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;mocha&#x2F;blob&#x2F;c5cf3249d9706f3470cbfcfd76e97b4bae87a3d0&#x2F;RELEASE.md#200&quot;&gt;release notes&lt;&#x2F;a&gt; and give some more detailed guidance on factors to consider when upgrading.&lt;&#x2F;p&gt;

&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#strict-keyword-argument-matching&quot; id=&quot;markdown-toc-strict-keyword-argument-matching&quot;&gt;Strict keyword argument matching&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;#removal-of-deprecated-functionality&quot; id=&quot;markdown-toc-removal-of-deprecated-functionality&quot;&gt;Removal of deprecated functionality&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;#dropping-of-support-for-older-versions-of-ruby-minitest--test-unit&quot; id=&quot;markdown-toc-dropping-of-support-for-older-versions-of-ruby-minitest--test-unit&quot;&gt;Dropping of support for older versions of Ruby, minitest &amp;amp; test-unit&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;#acknowledgements&quot; id=&quot;markdown-toc-acknowledgements&quot;&gt;Acknowledgements&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;p&gt;&lt;em&gt;TL;DR: If you&#x27;re using a non-ancient version of Ruby, you&#x27;re using a non-ancient test library version, you&#x27;ve already upgraded to Mocha v1.16.0, and you&#x27;ve fixed all the Mocha deprecation warnings, then the worst that should happen when you upgrade is that you&#x27;ll see some new deprecation warnings!&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;

&lt;h2 id=&quot;strict-keyword-argument-matching&quot;&gt;Strict keyword argument matching&lt;&#x2F;h2&gt;

&lt;p&gt;Previously Mocha parameter matching always considered a positional &lt;code&gt;Hash&lt;&#x2F;code&gt; as exactly equivalent to a set of keyword arguments. However, in Ruby v3, positional arguments and keyword arguments have been separated and, in Ruby v2.7, behaviour that would be different in Ruby v3 is flagged by deprecation warnings. See &lt;a href=&quot;https:&#x2F;&#x2F;www.ruby-lang.org&#x2F;en&#x2F;news&#x2F;2019&#x2F;12&#x2F;12&#x2F;separation-of-positional-and-keyword-arguments-in-ruby-3-0&quot;&gt;this article&lt;&#x2F;a&gt; for more details on the separation of positional and keyword arguments in Ruby v3.&lt;&#x2F;p&gt;

&lt;p&gt;To address this a new configuration option (&lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Configuration.html#strict_keyword_argument_matching=-instance_method&quot;&gt;Configuration#strict_keyword_argument_matching=&lt;&#x2F;a&gt;) has been introduced in Mocha v2. This option is available in Ruby v2.7 upwards.&lt;&#x2F;p&gt;

&lt;p&gt;In Mocha v2 the configuration option defaults to &lt;code&gt;false&lt;&#x2F;code&gt;, but in a future version of Mocha it will default to &lt;code&gt;true&lt;&#x2F;code&gt;. When the option is set to &lt;code&gt;true&lt;&#x2F;code&gt;, Mocha parameter matching considers a positional &lt;code&gt;Hash&lt;&#x2F;code&gt; and a set of keyword arguments as &lt;em&gt;different&lt;&#x2F;em&gt; even if their &quot;keys&quot; and &quot;values&quot; are exactly the same, i.e. the parameter matching is stricter and some invocations which previously matched may no longer match.&lt;&#x2F;p&gt;

&lt;p&gt;When the configuration option is set to &lt;code&gt;false&lt;&#x2F;code&gt;, parameter matching that would behave differently if the option were set to &lt;code&gt;true&lt;&#x2F;code&gt; is flagged by Mocha deprecation warnings. Once all these deprecation warnings are addressed, the configuration option can safely be set to &lt;code&gt;true&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;It&#x27;s important to address this issue, because otherwise you may end up with passing tests that give you a false sense of security. See the examples below.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;keyword-argument-syntax&quot;&gt;Keyword argument syntax&lt;&#x2F;h3&gt;

&lt;p&gt;An area of possible confusion is the Ruby syntax that distinguishes between a positional &lt;code&gt;Hash&lt;&#x2F;code&gt; and a set of keyword arguments. In particular the use of &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Fat_comma&quot;&gt;hash rockets&lt;&#x2F;a&gt; (&quot;=&amp;gt;&quot;) does &lt;strong&gt;NOT&lt;&#x2F;strong&gt; imply a positional &lt;code&gt;Hash&lt;&#x2F;code&gt;. Instead what matters is whether the &quot;keys&quot; and &quot;values&quot; are surrounded by braces (&quot;{ … }&quot;).&lt;&#x2F;p&gt;

&lt;p&gt;The following code defines a method that in Ruby v3 expects to be called with a single keyword argument. That method is then called four times, twice with the correct keyword argument and twice with a positional &lt;code&gt;Hash&lt;&#x2F;code&gt; including a key with the correct name. You might be surprised that the 2nd call (i.e. &lt;code&gt;foo(:bar =&amp;gt; 1)&lt;&#x2F;code&gt;) is passing a keyword argument.&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    def foo(bar:); p bar; end

    # Method called with correct keyword argument
    foo(bar: 1) # =&amp;gt; 1
    foo(:bar =&amp;gt; 1) # =&amp;gt; 1

    # Method called with positional Hash
    foo({ bar: 1 }) # =&amp;gt; ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: bar)
    foo({ :bar =&amp;gt; 1 }) # =&amp;gt; ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: bar)
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;h3 id=&quot;example-with-relaxed-matching&quot;&gt;Example with relaxed matching&lt;&#x2F;h3&gt;

&lt;p&gt;The parameters in the expectation include a set of keyword arguments, but the parameters in the invocation include a positional &lt;code&gt;Hash&lt;&#x2F;code&gt;. With strict matching disabled, these parameters match the expectation and the test passes. However, when &lt;code&gt;Example#foo&lt;&#x2F;code&gt; is invoked in production code in Ruby v3 an &lt;code&gt;ArgumentError&lt;&#x2F;code&gt; is raised, i.e. the passing test does not highlight that &lt;code&gt;Example#foo&lt;&#x2F;code&gt; must be called with a set of keyword arguments.&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    class Example
      def foo(a, bar:); end
    end

    class ExampleTest &amp;lt; MiniTest::Test
      def test_foo
        example = Example.new

        # The parameters in the expectation include a set of keyword arguments
        example.expects(:foo).with(&#x27;a&#x27;, bar: &#x27;b&#x27;)

        # The parameters in the invocation include a positional Hash
        # These parameters match the expectation and the test passes
        example.foo(&#x27;a&#x27;, { bar: &#x27;b&#x27; })
      end
    end

    example = Example.new
    example.foo(&#x27;a&#x27;, { bar: &#x27;b&#x27; }) # =&amp;gt; ArgumentError in Ruby v3
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;p&gt;Note, however, that a deprecation warning is displayed:&lt;&#x2F;p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Mocha deprecation warning at example_test.rb:NN:in `test_foo&#x27;: Expectation defined at example_test.rb:MM:in `test_foo&#x27; expected keyword arguments (:bar =&amp;gt; &quot;b&quot;), but received positional hash ({:bar =&amp;gt; &quot;b&quot;}). These will stop matching when strict keyword argument matching is enabled. See the documentation for Mocha::Configuration#strict_keyword_argument_matching=.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;

&lt;h3 id=&quot;example-with-strict-matching&quot;&gt;Example with strict matching&lt;&#x2F;h3&gt;

&lt;p&gt;With strict matching enabled, the parameters no longer match the expectation and the test fails. This test failure highlights that &lt;code&gt;Example#foo&lt;&#x2F;code&gt; must be called with a set of keyword arguments.&lt;&#x2F;p&gt;

&lt;pre&gt;
  &lt;code class=&quot;prettyprint&quot;&gt;
    Mocha.configure do |c|
      c.strict_keyword_argument_matching = true
    end

    class Example
      def foo(a, bar:); end
    end

    class ExampleTest &amp;lt; MiniTest::Test
      def test_foo
        example = Example.new

        # The parameters in the expectation include a set of keyword arguments
        example.expects(:foo).with(&#x27;a&#x27;, bar: &#x27;b&#x27;)

        # The parameters in the invocation include a positional Hash
        # These parameters no longer match the expectation and the test fails
        example.foo(&#x27;a&#x27;, { bar: &#x27;b&#x27; })
      end
    end

    # When Example#foo is invoked in production code:
    example = Example.new
    example.foo(&#x27;a&#x27;, { bar: &#x27;b&#x27; }) # =&amp;gt; ArgumentError in Ruby v3
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;h2 id=&quot;removal-of-deprecated-functionality&quot;&gt;Removal of deprecated functionality&lt;&#x2F;h2&gt;

&lt;p&gt;A bunch of deprecated functionality has been removed in Mocha v2. As long as you&#x27;ve previously upgraded to Mocha v1.16.0 and fixed all the deprecation warnings you shouldn&#x27;t have any trouble.🤞&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;It&#x27;s no longer possible to pass &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;API.html#mock-instance_method&quot;&gt;&lt;code&gt;API#mock&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;API.html#stub-instance_method&quot;&gt;&lt;code&gt;API#stub&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;API.html#stub_everything-instance_method&quot;&gt;&lt;code&gt;API#stub_everything&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; a single symbol argument to create a mock object responding to a method named according to that symbol argument. Such an argument is used to name the mock object itself; any stubbed methods and return values should be setup by passing a &lt;code&gt;Hash&lt;&#x2F;code&gt; into these methods or by calling &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Mock.html#expects-instance_method&quot;&gt;&lt;code&gt;Mock#expects&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Mock.html#stubs-instance_method&quot;&gt;&lt;code&gt;Mock#stubs&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
  &lt;li&gt;If &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Expectation.html#yields-instance_method&quot;&gt;&lt;code&gt;Expectation#yields&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Expectation.html#multiple_yields-instance_method&quot;&gt;&lt;code&gt;Expectation#multiple_yields&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; have been used to specify that a stubbed method should yield then the stubbed method must be invoked with a block otherwise a &lt;code&gt;LocalJumpError&lt;&#x2F;code&gt; will be raised.&lt;&#x2F;li&gt;
  &lt;li&gt;The &lt;code&gt;Configuration#reinstate_undocumented_behaviour_from_v1_9=&lt;&#x2F;code&gt; method has been removed. If you have addressed the deprecation warnings for &lt;code&gt;API#mock&lt;&#x2F;code&gt;, &lt;code&gt;API#stub&lt;&#x2F;code&gt;, &lt;code&gt;API#stub_everything&lt;&#x2F;code&gt;, &lt;code&gt;Expectation#yields&lt;&#x2F;code&gt; and &lt;code&gt;Expectation#multiple_yields&lt;&#x2F;code&gt; as explained above then this configuration option is redundant.&lt;&#x2F;li&gt;
  &lt;li&gt;The &lt;code&gt;Configuration.allow&lt;&#x2F;code&gt;, &lt;code&gt;Configuration.warn&lt;&#x2F;code&gt; and &lt;code&gt;Configuration.prevent&lt;&#x2F;code&gt; methods have been removed. Use &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha.html#configure-class_method&quot;&gt;&lt;code&gt;Mocha.configure&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; and&#x2F;or &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;Configuration.html#override-class_method&quot;&gt;&lt;code&gt;Mocha::Configuration.override&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; instead.&lt;&#x2F;li&gt;
  &lt;li&gt;The &lt;code&gt;mocha&#x2F;setup.rb&lt;&#x2F;code&gt; mechanism has been removed. Use one of the &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;index.html#installation&quot;&gt;supported installation mechanisms&lt;&#x2F;a&gt; instead.&lt;&#x2F;li&gt;
  &lt;li&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;rubyonrails.org&#x2F;&quot;&gt;Ruby on Rails&lt;&#x2F;a&gt; plugin mechanism has been removed. Use one of the &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;index.html#installation&quot;&gt;supported installation mechanisms&lt;&#x2F;a&gt; instead.&lt;&#x2F;li&gt;
  &lt;li&gt;A &lt;a href=&quot;https:&#x2F;&#x2F;mocha.jamesmead.org&#x2F;Mocha&#x2F;StubbingError.html&quot;&gt;&lt;code&gt;StubbingError&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; is now raised when stubbed methods are invoked in a test other than the one in which they were defined. This is to avoid unintended interactions between tests and hence unexpected test failures. A test should clean up any state that it introduces.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h2 id=&quot;dropping-of-support-for-older-versions-of-ruby-minitest--test-unit&quot;&gt;Dropping of support for older versions of Ruby, minitest &amp;amp; test-unit&lt;&#x2F;h2&gt;

&lt;p&gt;Mocha v2 drops support for older versions of &lt;a href=&quot;https:&#x2F;&#x2F;www.ruby-lang.org&#x2F;&quot;&gt;Ruby&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;test-unit&#x2F;&quot;&gt;test-unit&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;minitest&#x2F;&quot;&gt;minitest&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;More specifically Mocha v2 only supports:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;Ruby v2.0 and upwards. In particular Ruby v1.9 is no longer supported. Note that support for Ruby v1.9.3 ended on &lt;a href=&quot;https:&#x2F;&#x2F;www.ruby-lang.org&#x2F;en&#x2F;news&#x2F;2014&#x2F;01&#x2F;10&#x2F;ruby-1-9-3-will-end-on-2015&#x2F;&quot;&gt;23 Feb 2015&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
  &lt;li&gt;Gem versions of test-unit from v2.5.1 (released on 05 Jul 2012) upwards. Versions of test-unit from the Ruby v1.8 standard library are no longer supported.&lt;&#x2F;li&gt;
  &lt;li&gt;Versions of minitest from v3.3.0 (released on 27 Jul 2012) upwards.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h2 id=&quot;acknowledgements&quot;&gt;Acknowledgements&lt;&#x2F;h2&gt;

&lt;p&gt;Many thanks to the following:&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;wasabigeek.com&#x2F;&quot;&gt;Nick Koh&lt;&#x2F;a&gt; for all his hard work on the strict keyword argument matching functionality.&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;po-ru.com&#x2F;&quot;&gt;Paul Battley&lt;&#x2F;a&gt; and &lt;a href=&quot;http:&#x2F;&#x2F;hlame.com&#x2F;&quot;&gt;Murray Steele&lt;&#x2F;a&gt; for testing preview releases of Mocha v2 on sizeable codebases.&lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.chao-xian.co.uk&#x2F;&quot;&gt;Kelvin Gan&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;ollie.treend.uk&#x2F;&quot;&gt;Ollie Treend&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;dilwoarhussain.com&#x2F;&quot;&gt;Dilwoar Hussain&lt;&#x2F;a&gt;, and the rest of the &lt;a href=&quot;https:&#x2F;&#x2F;gds.blog.gov.uk&#x2F;&quot;&gt;GDS&lt;&#x2F;a&gt; developer team for helping me test preview releases of Mocha v2 on the sizeable &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;alphagov&#x2F;whitehall&quot;&gt;alphagov&#x2F;whitehall&lt;&#x2F;a&gt; codebase.&lt;&#x2F;li&gt;
  &lt;li&gt;My &lt;a href=&quot;https:&#x2F;&#x2F;gofreerange.com&#x2F;&quot;&gt;Go Free Range&lt;&#x2F;a&gt; colleagues, &lt;a href=&quot;https:&#x2F;&#x2F;blog.chrislowis.co.uk&#x2F;&quot;&gt;Chris Lowis&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;gofreerange.com&#x2F;people#chris-roos&quot;&gt;Chris Roos&lt;&#x2F;a&gt;, for funding a lot of my time working on Mocha, for reviewing code &amp;amp; documentation and for just generally being very supportive.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:64e8a85a-d2c1-11ec-892c-529fc877b60a</id>
    <published>2022-05-18T17:27:00+00:00</published>
    <updated>2022-05-18T17:27:00+00:00</updated>
    <title>How to backup Google Drive to S3 using the AWS CDK</title>
    <link href="https://jamesmead.org/blog/2022-05-18-how-to-backup-google-drive-to-s3-using-the-aws-cdk" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;Way back in &lt;a href=&quot;https:&#x2F;&#x2F;www.urbandictionary.com&#x2F;define.php?term=The+Before+Time&quot;&gt;the Before Time&lt;&#x2F;a&gt;, I did some work to automate a couple of our recurring manual &lt;a href=&quot;https:&#x2F;&#x2F;harmonia.io&#x2F;&quot;&gt;Harmonia&lt;&#x2F;a&gt; tasks. One of these was the task to back up our shared &lt;a href=&quot;https:&#x2F;&#x2F;www.google.com&#x2F;drive&#x2F;&quot;&gt;Google Drive&lt;&#x2F;a&gt; to &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;s3&#x2F;&quot;&gt;Amazon S3&lt;&#x2F;a&gt;. Prior to this we&#x27;d been running the &lt;a href=&quot;https:&#x2F;&#x2F;rclone.org&#x2F;commands&#x2F;rclone_sync&#x2F;&quot;&gt;&lt;code&gt;rclone sync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; command manually on one of our local machines. One significant downside of this was that we each needed to have a local copy of all the files (~17GB), so I was keen to come up with an automated solution running in the cloud.&lt;&#x2F;p&gt;

&lt;p&gt;Unlike with our &lt;a href=&quot;&#x2F;blog&#x2F;2020-03-30-automatic-backup-of-trello-boards-to-s3-using-aws-cdk&quot;&gt;Trello backup&lt;&#x2F;a&gt;, it wasn&#x27;t obvious to me how we could split the work up into tasks short enough to run as &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;lambda&#x2F;&quot;&gt;AWS Lambda&lt;&#x2F;a&gt; functions. Also, although it was interesting from an educational point-of-view, I felt as if the orchestration&#x2F;coordination complexities introduced by splitting up the Trello backup tasks had been overly cumbersome. So I decided to explore the idea of spinning up some compute to execute a script in one go much more like how a &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Cron&quot;&gt;cron job&lt;&#x2F;a&gt; would run on a traditional server.&lt;&#x2F;p&gt;

&lt;p&gt;I was (and still am) enjoying using the &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;cdk&#x2F;&quot;&gt;AWS CDK&lt;&#x2F;a&gt; and so after a bit of research, I decided to use the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;v1&#x2F;docs&#x2F;@aws-cdk_aws-ecs-patterns.ScheduledFargateTask.html&quot;&gt;&lt;code&gt;ScheduledFargateTask&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; construct which is one of the higher-level patterns made available in the CDK. This construct meant that it was relatively straightfoward to spin up a container on &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;ecs&#x2F;&quot;&gt;Amazon Elastic Container Service (ECS)&lt;&#x2F;a&gt; at regular intervals and execute a shell script on that container.&lt;&#x2F;p&gt;

&lt;h2 id=&quot;scheduled-fargate-task&quot;&gt;Scheduled Fargate Task&lt;&#x2F;h2&gt;

&lt;p&gt;The task needed &lt;strong&gt;access to&lt;&#x2F;strong&gt; the internet, but there was no need for it to &lt;strong&gt;be accessible from&lt;&#x2F;strong&gt; the internet. I could&#x27;ve run it on a private subnet, but this would&#x27;ve meant I&#x27;d need either a NAT Gateway (expensive) or to run a NAT Instance on &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;ec2&#x2F;&quot;&gt;Amazon EC2&lt;&#x2F;a&gt; (maintenance&#x2F;complexity overhead). Since the tasks only run for a few minutes every week I was willing to sacrifice the extra security provided by a private subnet in favour of a simpler&#x2F;cheaper system where the tasks run on a public subnet.&lt;&#x2F;p&gt;

&lt;p&gt;However, at that point &lt;code&gt;ScheduledFargateTask&lt;&#x2F;code&gt; only ran if its VPC had a private subnet - if there was no private subnet available, an error was reported. So I decided to take the opportunity to contribute to the AWS CDK project and opened &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;aws&#x2F;aws-cdk&#x2F;pull&#x2F;6624&quot;&gt;a pull request to allow ECS tasks to run on a public subnet&lt;&#x2F;a&gt; which was released in &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;aws&#x2F;aws-cdk&#x2F;releases&#x2F;tag&#x2F;v1.29.0&quot;&gt;v1.29.0&lt;&#x2F;a&gt;. I really enjoy contributing to open-source projects like this - it&#x27;s a really good way to get a deeper understanding of how it all works.&lt;&#x2F;p&gt;

&lt;p&gt;Having incorporated that change, I used &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;v1&#x2F;docs&#x2F;@aws-cdk_aws-ecs.ContainerImage.html#static-fromwbrassetdirectory-props&quot;&gt;&lt;code&gt;ecs.ContainerImage.fromAsset&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to define the container image using &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;google-drive-backup&#x2F;blob&#x2F;19a065b9bfebe8a7a4cbdc9f3739d628261d9f2c&#x2F;local-image&#x2F;Dockerfile&quot;&gt;a local &lt;code&gt;Dockerfile&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. This installs &lt;code&gt;rclone&lt;&#x2F;code&gt; on an Ubuntu base image and copies &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;google-drive-backup&#x2F;blob&#x2F;ffc52080da5de7b780ba6b50352d0147ffad793e&#x2F;local-image&#x2F;home&#x2F;backup.sh&quot;&gt;a backup script&lt;&#x2F;a&gt; and associated &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;google-drive-backup&#x2F;blob&#x2F;ffc52080da5de7b780ba6b50352d0147ffad793e&#x2F;local-image&#x2F;home&#x2F;rclone.conf&quot;&gt;rclone configuration&lt;&#x2F;a&gt; files into the home directory. This means you need the &lt;a href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;engine&#x2F;reference&#x2F;commandline&#x2F;cli&#x2F;&quot;&gt;Docker CLI&lt;&#x2F;a&gt; available locally when you run &lt;code&gt;cdk deploy&lt;&#x2F;code&gt; so it can build the container image and push it up to &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;ecr&#x2F;&quot;&gt;Amazon Elastic Container Registry&lt;&#x2F;a&gt; ready for use by ECS.&lt;&#x2F;p&gt;

&lt;p&gt;It turned out that using the &lt;code&gt;rclone sync&lt;&#x2F;code&gt; command on a Google Drive folder containing so much data needs quite a bit of CPU and memory, but it was easy to increase this from the default of ¼vCPU &amp;amp; ½GB to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;google-drive-backup&#x2F;blob&#x2F;ffc52080da5de7b780ba6b50352d0147ffad793e&#x2F;lib&#x2F;google-drive-backup-stack.ts#L30-L31&quot;&gt;4vCPU &amp;amp; 16GB&lt;&#x2F;a&gt; so that the command ran very quickly. Even though this is pretty beefy, given that it only runs for a few minutes once a week, the cost is negligible.&lt;&#x2F;p&gt;

&lt;p&gt;The task is scheduled using the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;latest&#x2F;docs&#x2F;@aws-cdk_aws-applicationautoscaling.CronOptions.html&quot;&gt;&lt;code&gt;CronOptions&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; interface. Configuration is supplied to the container via environment variables using &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;dotenv&quot;&gt;dotenv&lt;&#x2F;a&gt;. Credentials for Google Drive are supplied via &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;secrets-manager&#x2F;&quot;&gt;Secrets Manager&lt;&#x2F;a&gt;. Those for the S3 bucket are made available via the IAM role assigned to the ECS Task and used by &lt;code&gt;rclone&lt;&#x2F;code&gt; with the &lt;a href=&quot;https:&#x2F;&#x2F;rclone.org&#x2F;s3&#x2F;#authentication&quot;&gt;&lt;code&gt;env_auth&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; option set to &lt;code&gt;true&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;The task is monitored with the excellent &lt;a href=&quot;https:&#x2F;&#x2F;healthchecks.io&#x2F;&quot;&gt;Healthchecks&lt;&#x2F;a&gt; service which we were already using for the Trello backup. This is effectively a &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Dead_man%27s_switch&quot;&gt;dead man&#x27;s switch&lt;&#x2F;a&gt; which alerts us if the script doesn&#x27;t complete successfully at a given frequency and within a defined grace period.&lt;&#x2F;p&gt;

&lt;h2 id=&quot;reflections&quot;&gt;Reflections&lt;&#x2F;h2&gt;

&lt;p&gt;Two years on, I&#x27;m really happy how this turned out. Once I&#x27;d got the backup running successfully, we&#x27;ve only had one failure which was due to a recent change to the Google Drive API requiring a newer version of &lt;code&gt;rclone&lt;&#x2F;code&gt;. This meant I had to dive back into the code again to fix it, but I found it pretty easy to find my way around again partly because there&#x27;s not actually very much code!&lt;&#x2F;p&gt;

&lt;p&gt;The source code for the whole CDK project is &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;freerange&#x2F;google-drive-backup&quot;&gt;available on GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:8d8937e0-79a1-4e44-a1df-e37b7d43ff36</id>
    <published>2021-01-23T21:32:00+00:00</published>
    <updated>2021-01-29T09:15:00+00:00</updated>
    <title>Youtube video of my 3D maze game for the ZX Spectrum</title>
    <link href="https://jamesmead.org/blog/2021-01-23-youtube-video-of-my-3d-maze-game-for-the-zx-spectrum" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;&lt;img style=&quot;display: block; margin-left: auto; margin-right: auto; width: 33.3%; float:right; padding: 10px&quot; src=&quot;&#x2F;images&#x2F;graphic-adventures-for-the-spectrum-48k.jpg&quot; alt=&quot;Book cover for &#x27;Graphic Adventures for the Spectrum 48K&#x27;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;

&lt;p&gt;I recently stumbled across &lt;a href=&quot;#the-youtube-video&quot;&gt;a quirky Youtube video&lt;&#x2F;a&gt; which piqued my interest. In the video &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;JAMOGRAD&quot;&gt;James O&#x27;Grady&lt;&#x2F;a&gt; demonstrated a 3D maze game. He&#x27;d typed in the code for the game from a familiar-sounding book called &lt;a href=&quot;https:&#x2F;&#x2F;www.amazon.co.uk&#x2F;dp&#x2F;0744700132&quot;&gt;Graphic Adventures for the Spectrum 48K&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;The nominal author of this book, Richard Hurley, was one of my teachers and he included programs written by me and a number of my friends. The 3D maze game was one I wrote in about 1984 when I was 16. In the video James goes on to critique the game, to explore some ways to improve it, and to read some reviews of the book from magazines of the time.&lt;&#x2F;p&gt;

&lt;p&gt;In my early teens I played a lot of games on the ZX81 and then the Spectrum, but as I got older I became bored of playing the games and more interested in writing them. I learnt a lot about programming games from typing in code from magazines and books.&lt;&#x2F;p&gt;

&lt;p&gt;The first games I developed were written entirely in &lt;a href=&quot;https:&#x2F;&#x2F;worldofspectrum.org&#x2F;ZXBasicManual&#x2F;&quot;&gt;Sinclair BASIC&lt;&#x2F;a&gt;, e.g. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;sub-hunt&quot;&gt;Sub Hunt&lt;&#x2F;a&gt; which was published in &lt;a href=&quot;https:&#x2F;&#x2F;spectrumcomputing.co.uk&#x2F;index.php?cat=96&amp;amp;id=2000461&quot;&gt;an earlier book&lt;&#x2F;a&gt;, but I quickly realised I would need to use machine code to get the performance I wanted. Initially I wrote small bits of machine code to speed up critical bits of the games. However, the 3D Maze game in the video was the first game I wrote pretty much entirely in Z80 machine code using the excellent &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Zeus_Assembler&quot;&gt;Zeus assembler&lt;&#x2F;a&gt; and with my trusty copy of &lt;a href=&quot;https:&#x2F;&#x2F;archive.org&#x2F;details&#x2F;CompleteSpectrumROMDisassemblyThe&quot;&gt;The Complete Spectrum ROM Disassembly&lt;&#x2F;a&gt;. It was closely based on the &quot;3D Monster Maze&quot; game by J.K. Greye Software.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;the-zx81-original&quot;&gt;The ZX81 original&lt;&#x2F;h3&gt;

&lt;div style=&quot;text-align: center; padding-bottom: 12px&quot;&gt;
  &lt;iframe width=&quot;80%&quot; height=&quot;315&quot; src=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;nKvd0zPfBE4&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;

&lt;blockquote&gt;
  &lt;p&gt;
    Most importantly, from the point of view of video game history, the ZX81 was the computer which hosted the world&#x27;s first ever 3D game on a home computer - JK Greye&#x27;s 3D Monster Maze. A simple labyrinth is generated, and the player has to find their way out, all the while being stalked by a Tyrannosaurus Rex. The whole experience was rendered in what is now referred to as &#x27;first person&#x27; view - ie, you see what you would see out of the eyes of the character in the maze, as pictured in the ZX81&#x27;s rather blocky but still effective graphics. A quick play of this game on an emulator is recommended to all fans of Doom, Quake, Unreal, Half Life and all the other FPSs which are now so popular, as it really is the literal grandaddy of them all. It is difficult now to describe the impact this game had on a public who had quite literally never seen anything like it.
    &amp;ndash;
    &lt;cite&gt;
      &lt;a href=&quot;https:&#x2F;&#x2F;h2g2.com&#x2F;edited_entry&#x2F;A821648&quot;&gt;The Hitchhiker&#x27;s Guide to the Galaxy (Earth Edition): The Wonderful Computers of Clive Sinclair&lt;&#x2F;a&gt;
    &lt;&#x2F;cite&gt;
  &lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;

&lt;h3 id=&quot;my-version&quot;&gt;My version&lt;&#x2F;h3&gt;

&lt;p&gt;One slight disappointment was that unlike in &quot;3D Monster Maze&quot; there was no &quot;monster&quot; in my version of the game or at least not in the version James was playing. I know that I did eventually add a Tyrannosaurus Rex to the game, but I vaguely remember having to rush for a publication deadline, so the monster might&#x27;ve have missed the cut! If I recall correctly, a friend with better artistic skills than me drew a T Rex in a series of &quot;frames&quot; walking towards the observer. I then traced the drawings onto graph paper and converted them into &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ZX_Spectrum_character_set&quot;&gt;user-defined graphic characters&lt;&#x2F;a&gt;. I do half wonder whether these might be the mystery bytes which James refers to at one point in his video. Otherwise I believe the program uses calls to the ROM, e.g. &lt;a href=&quot;https:&#x2F;&#x2F;speccy.xyz&#x2F;rom&#x2F;asm&#x2F;24b7&quot;&gt;this line-drawing subroutine&lt;&#x2F;a&gt;, to draw the walls of the maze.&lt;&#x2F;p&gt;

&lt;div style=&quot;text-align: center; padding-bottom: 12px&quot;&gt;
  &lt;iframe width=&quot;80%&quot; height=&quot;315&quot; src=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;Q656CqMIXLY&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;&#x2F;div&gt;

&lt;h3 id=&quot;the-youtube-video&quot;&gt;The Youtube video&lt;&#x2F;h3&gt;

&lt;p&gt;James must&#x27;ve been incredibly patient to type in all the raw numbers for the machine code with only very rudimentary checksums. And, given that the game is written entirely in machine code and the assembler source code is lost in the mists of time, I was impressed that James managed to successfully modify the game in a couple of different ways using a load of judicious &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;PEEK_and_POKE&quot;&gt;&lt;code&gt;PEEK&lt;&#x2F;code&gt;s and &lt;code&gt;POKE&lt;&#x2F;code&gt;s&lt;&#x2F;a&gt; and apparently without the use of a disassembler. In particular he&#x27;s written a nice maze editor program which runs on the Spectrum and allows you to design your own maze. I was quite amused to learn that the maze had to be square - I can&#x27;t imagine it would&#x27;ve been much harder for me to have allowed rectangular ones!&lt;&#x2F;p&gt;

&lt;p&gt;James is very fair in his criticisms of the game - his main observation is that it&#x27;s not very interesting to play, but it is very fast compared to other similar games. I also enjoyed reading the reviews of the book he&#x27;d found in a couple of magazines of the time. I had a lovely exchange with him in &lt;a href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=Q656CqMIXLY&amp;amp;lc=UgzsXaL19aLWF7T3qCp4AaABAg&quot;&gt;the Youtube comments&lt;&#x2F;a&gt; and he &lt;a href=&quot;https:&#x2F;&#x2F;twitter.com&#x2F;JAMOGRAD&#x2F;status&#x2F;1351920870621589506&quot;&gt;changed the title&lt;&#x2F;a&gt; of the Youtube video to include my name which was a nice gesture. Anyway, this was a brilliant trip down memory lane for me and reminded me of my programming roots!&lt;&#x2F;p&gt;

&lt;h3 id=&quot;playing-the-game&quot;&gt;Playing the game&lt;&#x2F;h3&gt;

&lt;p&gt;If you feel as if you want the full &quot;type it in&quot; experience, the Portuguese (!) version of the book is available for &lt;a href=&quot;https:&#x2F;&#x2F;archive.org&#x2F;download&#x2F;World_of_Spectrum_June_2017_Mirror&#x2F;World%20of%20Spectrum%20June%202017%20Mirror.zip&#x2F;World%20of%20Spectrum%20June%202017%20Mirror&#x2F;sinclair&#x2F;books&#x2F;g&#x2F;GraphicAdventuresForTheSpectrum48K(AventurasGraficasParaOSpectrum48K)(TemposLivres).pdf&quot;&gt;download&lt;&#x2F;a&gt; from &lt;a href=&quot;https:&#x2F;&#x2F;spectrumcomputing.co.uk&#x2F;index.php?cat=96&amp;amp;id=2000168&quot;&gt;Spectrum Computing&lt;&#x2F;a&gt; and you can find the game in &quot;Labirinto&quot; (chapter 4, page 105). Otherwise, &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;3d-maze&quot;&gt;this GitHub repo&lt;&#x2F;a&gt; includes a set of &lt;a href=&quot;https:&#x2F;&#x2F;worldofspectrum.org&#x2F;faq&#x2F;reference&#x2F;formats.htm#TAP&quot;&gt;TAP format&lt;&#x2F;a&gt; files which might work in a Spectrum emulator, although I haven&#x27;t yet had a chance to try them myself.&lt;&#x2F;p&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:05fc7138-2985-481a-ab79-72a5835fecd7</id>
    <published>2021-01-17T16:38:00+00:00</published>
    <updated>2021-01-17T16:38:00+00:00</updated>
    <title>Using Lambda@Edge with CloudFront to configure redirect rules for domains</title>
    <link href="https://jamesmead.org/blog/2021-01-17-using-lambda-edge-with-cloudfront-to-configure-redirect-rules-for-domains" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;I&#x27;m a big fan of &lt;a href=&quot;https:&#x2F;&#x2F;www.w3.org&#x2F;Provider&#x2F;Style&#x2F;URI&quot;&gt;cool URLs&lt;&#x2F;a&gt; and not &lt;a href=&quot;https:&#x2F;&#x2F;gofreerange.com&#x2F;broken-rubyforge-urls&quot;&gt;breaking the internet&lt;&#x2F;a&gt; and so over the years I&#x27;ve accumulated a few domains for which I&#x27;ve implemented a lot of redirect rules. In recent years I&#x27;ve implemented these rules using &lt;a href=&quot;https:&#x2F;&#x2F;httpd.apache.org&#x2F;docs&#x2F;current&#x2F;mod&#x2F;mod_rewrite.html&quot;&gt;mod_rewrite&lt;&#x2F;a&gt; and run them on &lt;a href=&quot;https:&#x2F;&#x2F;httpd.apache.org&#x2F;&quot;&gt;Apache&lt;&#x2F;a&gt; and on my &lt;a href=&quot;https:&#x2F;&#x2F;www.linode.com&#x2F;&quot;&gt;Linode&lt;&#x2F;a&gt; VPS, e.g. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;jamesmead.org&#x2F;blob&#x2F;b3db4135b3b90b96e50e99edf0551a52e0dc240f&#x2F;config&#x2F;sites-available&#x2F;blog.floehopper.org.conf&quot;&gt;the redirect rules for &lt;code&gt;blog.floehopper.org&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;p&gt;However, in September 2019 I started &lt;a href=&quot;https:&#x2F;&#x2F;jamesmead.org&#x2F;blog&#x2F;2019-09-07-using-github-actions-to-publish-a-static-site-to-github-pages&quot;&gt;publishing this website on GitHub Pages&lt;&#x2F;a&gt; and over the intervening period I&#x27;ve removed pretty much everything else I was running on that VPS. And so I&#x27;d started thinking it would be nice to shutdown the VPS and stop paying for it! The legacy redirects mentioned above were the only things preventing me from doing this.&lt;&#x2F;p&gt;

&lt;p&gt;In the last 18 months I&#x27;ve used the &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;cdk&#x2F;&quot;&gt;AWS CDK&lt;&#x2F;a&gt; quite a bit both at work and for personal projects. Unfortunately I&#x27;ve only got round to publishing &lt;a href=&quot;2020-03-30-automatic-backup-of-trello-boards-to-s3-using-aws-cdk&quot;&gt;one article about it&lt;&#x2F;a&gt; (something I hope to remedy in the not too distant future). Anyway, I&#x27;d read a bit about &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;lambda&#x2F;edge&#x2F;&quot;&gt;Lambda@Edge&lt;&#x2F;a&gt; and noticed that it was indeed supported by &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;cloudformation&#x2F;&quot;&gt;CloudFormation&lt;&#x2F;a&gt; and the AWS CDK, so I thought I&#x27;d give it a whirl.&lt;&#x2F;p&gt;

&lt;p&gt;My basic idea was to create a &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;latest&#x2F;docs&#x2F;@aws-cdk_aws-cloudfront.Distribution.html&quot;&gt;CloudFront distribution&lt;&#x2F;a&gt;, associate a custom domain with it, configure &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;latest&#x2F;docs&#x2F;aws-cloudfront-readme.html#lambdaedge&quot;&gt;an Edge Lambda function&lt;&#x2F;a&gt; to handle all requests to that distribution and implement the appropriate redirects in JavaScript, then point the relevant DNS records at the CloudFront distribution.
This turned out to be pretty straightforward, although I did have to use the &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;certificate-manager&#x2F;&quot;&gt;AWS Certificate Manager&lt;&#x2F;a&gt; to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;edge-redirector&#x2F;blob&#x2F;da128e7dc42fe89b09d496a2b5e68f4aaa931f78&#x2F;create-ssl-certificate.sh&quot;&gt;create an SSL certificate&lt;&#x2F;a&gt; for the domain to get everything to work.&lt;&#x2F;p&gt;

&lt;p&gt;You can find the full project source code &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;edge-redirector&quot;&gt;on GitHub&lt;&#x2F;a&gt;. I&#x27;m afraid I haven&#x27;t yet got round to updating the README from the default version generated by the AWS CDK. If you prefer, here&#x27;s a slightly cut down version of a couple of key project files illustrating how I did this for the &lt;code&gt;blog.floehopper.org&lt;&#x2F;code&gt; domain:&lt;&#x2F;p&gt;

&lt;pre class=&quot;prettyprint lang-js&quot;&gt;
  &lt;code&gt;
    # file: lib&#x2F;edge-redirector-stack.ts

    import * as cdk from &#x27;@aws-cdk&#x2F;core&#x27;;
    import * as lambda from &#x27;@aws-cdk&#x2F;aws-lambda&#x27;;
    import * as cloudfront from &#x27;@aws-cdk&#x2F;aws-cloudfront&#x27;;
    import * as origins from &#x27;@aws-cdk&#x2F;aws-cloudfront-origins&#x27;;
    import * as acm from &#x27;@aws-cdk&#x2F;aws-certificatemanager&#x27;;

    export class EdgeRedirectorStack extends cdk.Stack {
      constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        this.createDistribution(&#x27;blog.floehopper.org&#x27;, &#x27;blogFloehopperOrg&#x27;, &#x27;arn:aws:acm:us-east-1:687105911108:certificate&#x2F;aa11ee5a-54db-4a04-8307-f77330f86cb5&#x27;);
      }

      createDistribution(domain: string, handler: string, certificateArn: string) {
        const certificate = acm.Certificate.fromCertificateArn(this, `${handler}Certificate`, certificateArn);
        new cloudfront.Distribution(this, `${handler}Distribution`, {
          defaultBehavior: {
            origin: new origins.HttpOrigin(&#x27;example.com&#x27;),
            edgeLambdas: [
              {
                eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
                functionVersion: this.redirectVersion(domain, handler)
              }
            ]
          },
          domainNames: [domain],
          certificate: certificate,
          enableLogging: true
        });
      }

      redirectVersion(domain: string, handler: string) : lambda.IVersion {
        const redirectFunction = new cloudfront.experimental.EdgeFunction(this, `${handler}Redirect`, {
          runtime: lambda.Runtime.NODEJS_12_X,
          handler: `${handler}.handler`,
          code: lambda.Code.fromAsset(&#x27;.&#x2F;lambdaFunctions&#x2F;redirect&#x27;)
        });

        return redirectFunction.currentVersion;
      }
    }
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;

&lt;pre class=&quot;prettyprint lang-js&quot;&gt;
  &lt;code&gt;
    # file: lambdaFunctions&#x2F;redirect&#x2F;blogFloehopperOrg.js

    &#x27;use strict&#x27;;

    exports.handler = function(event, context, callback) {
      const request = event.Records[0].cf.request;

      const mapping = [
        &#x2F;&#x2F; Legacy Typo-style articles
        [&#x27;^&#x2F;articles&#x2F;([0-9]{4})&#x2F;([0-9]{2})&#x2F;([0-9]{2})&#x2F;(.+)$&#x27;, (m) =&amp;gt; `http:&#x2F;&#x2F;jamesmead.org&#x2F;blog&#x2F;${m[1]}-${m[2]}-${m[3]}-${m[4]}`],

        &#x2F;&#x2F; Redirect blog.floehopper.org -&amp;gt; jamesmead.org
        [&#x27;^&#x2F;(.*)$&#x27;, (m) =&amp;gt; `http:&#x2F;&#x2F;jamesmead.org&#x2F;${m[1]}`],
        [&#x27;^$&#x27;, (m) =&amp;gt; `http:&#x2F;&#x2F;jamesmead.org`],
    ];

      let redirectUrl;
      for (const [pattern, url] of mapping) {
        const match = request.uri.match(new RegExp(pattern));
        if (match) {
          if (typeof(url) == &#x27;function&#x27;) {
            redirectUrl = url(match);
          } else {
            redirectUrl = url;
          };
          break;
        };
      };

      let response;

      if (redirectUrl) {
        response = {
          status: &#x27;301&#x27;,
          statusDescription: &#x27;Moved Permanently&#x27;,
          headers: {
            location: [{
              key: &#x27;Location&#x27;,
              value: redirectUrl
            }],
          }
        };
      } else {
        response = {
          status: &#x27;404&#x27;,
          statusDescription: &#x27;Not Found&#x27;
        };
      };

      callback(null, response);
    };
  &lt;&#x2F;code&gt;
&lt;&#x2F;pre&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:4276f4a5-2406-41c9-baba-2bf154e0712b</id>
    <published>2020-11-29T12:21:00+00:00</published>
    <updated>2020-11-29T12:21:00+00:00</updated>
    <title>Multiple Rails development environments using nix-shell</title>
    <link href="https://jamesmead.org/blog/2020-11-29-multiple-rails-development-environments-using-nix-shell" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;I&#x27;ve continued to make slow but steady progress with my experiment to setup Rails development environments using nix-shell on a Vagrant VM running Ubuntu. I&#x27;ve now got to the stage where I have four Rails apps using combinations of Ruby versions, Rails versions, PostgreSQL versions, and MySQL versions which I&#x27;m pretty happy about!&lt;&#x2F;p&gt;

&lt;ul&gt;
  &lt;li&gt;Ruby v2.5, Rails v5.2.4.4, PostgreSQL v10&lt;&#x2F;li&gt;
  &lt;li&gt;Ruby v2.5, Rails v5.2.4.4, MySQL v5.7&lt;&#x2F;li&gt;
  &lt;li&gt;Ruby v2.6, Rails v6.0.3.4, PostgreSQL v11&lt;&#x2F;li&gt;
  &lt;li&gt;Ruby v2.6, Rails v6.0.3.4, MySQL v8.0&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;p&gt;&lt;img style=&quot;display: block; margin-left: auto; margin-right: auto; width: 100%;&quot; src=&quot;&#x2F;images&#x2F;four-rails-apps.png&quot; alt=&quot;Four Rails apps&quot; &#x2F;&gt;&lt;&#x2F;p&gt;

&lt;p&gt;I&#x27;ve continued to use bash scripts as Vagrant provisioners to do this in a reproducible way, although &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;rails-on-nix&#x2F;tree&#x2F;3c40a3fe195a08dfdec54a50a2b042eae1305b64&quot;&gt;the code&lt;&#x2F;a&gt; is currently a bit messier than I would like.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;creating-the-rails-apps&quot;&gt;Creating the Rails apps&lt;&#x2F;h3&gt;

&lt;p&gt;I&#x27;ve improved the way that &lt;code&gt;rails new&lt;&#x2F;code&gt; is run so that it works correctly for different versions of Ruby. The new approach closely based on &lt;a href=&quot;https:&#x2F;&#x2F;discourse.nixos.org&#x2F;t&#x2F;using-bundlerenv-with-non-default-version-of-ruby-v2-5&#x2F;8470&#x2F;4&quot;&gt;this answer&lt;&#x2F;a&gt; to a question I asked on the NixOS forums.&lt;&#x2F;p&gt;

&lt;p&gt;Some of the complications around having a suitable environment to run &lt;code&gt;rails new&lt;&#x2F;code&gt; for a particular version of Ruby and of Rails has reminded me that I don&#x27;t have a particularly good solution for this in my current non-Nix MacOS setup.&lt;&#x2F;p&gt;

&lt;p&gt;In fact I tend to do something analagous to what I&#x27;ve done with Nix, i.e. I use &lt;code&gt;rbenv&lt;&#x2F;code&gt; to switch to the relevant version of Ruby, create a &lt;code&gt;Gemfile&lt;&#x2F;code&gt; containing just a reference to the version of the Rails gem that I want, run &lt;code&gt;bundle install&lt;&#x2F;code&gt; and then &lt;code&gt;rails new&lt;&#x2F;code&gt;. I&#x27;d be interested to hear if anyone has a better&#x2F;simpler way of doing this.&lt;&#x2F;p&gt;

&lt;p&gt;At this point, it&#x27;s probably instructive to show you the relevant files for one of the four Rails apps. A &lt;code&gt;bundler.nix&lt;&#x2F;code&gt; is used only to run &lt;code&gt;bundle lock&lt;&#x2F;code&gt; to generate &lt;code&gt;Gemfile.lock&lt;&#x2F;code&gt;. Previously I had been using &lt;code&gt;bundix&lt;&#x2F;code&gt; itself to generate &lt;code&gt;Gemfile.lock&lt;&#x2F;code&gt;, but I couldn&#x27;t work out how to do this for different versions of Ruby.&lt;&#x2F;p&gt;

&lt;pre&gt;&lt;code&gt;# Gemfile
source &#x27;https:&#x2F;&#x2F;rubygems.org&#x27;
gem &#x27;rails&#x27;, &#x27;= 6.0.3.4&#x27;

# bundler.nix
with (import &amp;lt;nixpkgs&amp;gt; {});
let
  myBundler = bundler.override { ruby = ruby_2_6; };
in
mkShell {
  name = &quot;bundler-shell&quot;;
  buildInputs = [ myBundler ];
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;p&gt;A &lt;code&gt;shell.nix&lt;&#x2F;code&gt; is used to run &lt;code&gt;bundix&lt;&#x2F;code&gt; to generate a &lt;code&gt;gemset.nix&lt;&#x2F;code&gt; and to run &lt;code&gt;rails new&lt;&#x2F;code&gt; using this gemset.&lt;&#x2F;p&gt;

&lt;pre&gt;&lt;code&gt;# shell.nix
with (import &amp;lt;nixpkgs&amp;gt; {});
let
  env = bundlerEnv {
    name = &quot;ruby2.6-rails6.0.3.4-mysql8.0&quot;;
    ruby = ruby_2_6;
    gemdir = .&#x2F;.;
  };
in mkShell { buildInputs = [ env env.wrappedRuby ]; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;p&gt;Inside the rails app directory there&#x27;s another &lt;code&gt;bundler.nix&lt;&#x2F;code&gt; (exactly the same as the one above) which is again only used to run &lt;code&gt;bundle lock&lt;&#x2F;code&gt; and another &lt;code&gt;shell.nix&lt;&#x2F;code&gt; which is used both to run &lt;code&gt;bundix&lt;&#x2F;code&gt; and to provide the actual development environment including all the relevant dependencies:&lt;&#x2F;p&gt;

&lt;pre&gt;&lt;code&gt;# shell.nix
with (import &amp;lt;nixpkgs&amp;gt; {});
let
  env = bundlerEnv {
    name = &quot;ruby2.6-rails6.0.3.4-mysql8.0&quot;;
    ruby = ruby_2_6;
    gemdir = .&#x2F;.;
  };
in mkShell {
  buildInputs = [ env env.wrappedRuby nodejs yarn mysql80 ];
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;p&gt;One other thing I had to deal with to handle Rails v5 was to run &lt;code&gt;rails yarn:install&lt;&#x2F;code&gt; instead of &lt;code&gt;rails webpacker:install&lt;&#x2F;code&gt; for Rails v6 when initally setting up the app.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;setting-up-databases&quot;&gt;Setting up databases&lt;&#x2F;h3&gt;

&lt;p&gt;I&#x27;ve added a &lt;a href=&quot;https:&#x2F;&#x2F;nixos.org&#x2F;manual&#x2F;nix&#x2F;stable&#x2F;command-ref&#x2F;nix-shell.html#description&quot;&gt;shellHook&lt;&#x2F;a&gt; to the development environment &lt;code&gt;shell.nix&lt;&#x2F;code&gt; to configure and run an instance of a database server for each Rails app. I&#x27;m now less sure that configuring and running a database on entering the nix-shell is very sensible. I suspect it might make more sense to have a separate script to do this.&lt;&#x2F;p&gt;

&lt;p&gt;This time I&#x27;ve made a couple of changes to improve the level of isolation between the apps. Firstly I&#x27;ve configured each database to store their data in a Rails app sub-directory rather than in a global location. And secondly I&#x27;ve configured each database to only accept connections via a &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Unix_domain_socket&quot;&gt;unix domain socket&lt;&#x2F;a&gt; also stored in a Rails app sub-directory.&lt;&#x2F;p&gt;

&lt;p&gt;I managed to achieve the former by moving the Rails apps under the Vagrant user&#x27;s home directory. This avoided the problem I had previously with hard links in a VirtualBox shared directory. Although this means the Rails app source code is not available from the guest OS, that seems like just a temporary inconvenience since I&#x27;m only using the Vagrant VM to simulate a fresh machine. Eventually my aim is to run Nix natively and not use Vagrant at all.&lt;&#x2F;p&gt;

&lt;p&gt;I&#x27;m particularly pleased with the unix domain socket solution, because it means there&#x27;s no need to identify an unused port for each Rails app to connect over TCP&#x2F;IP. Here&#x27;s the shellHook code for PostgreSQL and MySQL databases:&lt;&#x2F;p&gt;

&lt;h4 id=&quot;postgresql&quot;&gt;PostgreSQL&lt;&#x2F;h4&gt;

&lt;pre&gt;&lt;code&gt;export PGHOST=&#x2F;home&#x2F;vagrant&#x2F;ruby2.6-rails6.0.3.4-postgres11&#x2F;tmp&#x2F;postgres
export PGDATA=$PGHOST&#x2F;data
export PGDATABASE=postgres
export PGLOG=$PGHOST&#x2F;postgres.log

mkdir -p $PGHOST

if [ ! -d $PGDATA ]; then
  initdb --auth=trust --no-locale --encoding=UTF8
fi

if ! pg_ctl status
then
  pg_ctl start -l $PGLOG -o &quot;--unix_socket_directories=&#x27;$PGHOST&#x27; --listen_addresses=&#x27;&#x27;&#x27;&quot;
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;h4 id=&quot;mysql&quot;&gt;MySQL&lt;&#x2F;h4&gt;

&lt;pre&gt;&lt;code&gt;MYSQL_HOME=&#x2F;home&#x2F;vagrant&#x2F;ruby2.6-rails6.0.3.4-mysql8.0&#x2F;tmp&#x2F;mysql
MYSQL_DATA=$MYSQL_HOME&#x2F;data
export MYSQL_UNIX_PORT=$MYSQL_HOME&#x2F;mysql.sock

mkdir -p $MYSQL_HOME

if [ ! -d $MYSQL_DATA ]; then
  mysqld --initialize-insecure --datadir=$MYSQL_DATA
fi

if ! mysqladmin status --user=root
then
  mysqld_safe --datadir=$MYSQL_DATA --skip-networking &amp;amp;
  while ! mysqladmin status --user=root; do
    sleep 1
  done
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;p&gt;I&#x27;m definitely no expert on setting up databases, so if you can suggest any improvements, I&#x27;d love to hear from you!&lt;&#x2F;p&gt;

&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h3&gt;

&lt;p&gt;I&#x27;m pretty happy with where I&#x27;ve got to. It&#x27;s starting to feel as if I have a solid basis for using Nix to create decent isolated development environments using various versions of Ruby, Rails, PostgreSQL &amp;amp; MySQL on the same machine.&lt;&#x2F;p&gt;

&lt;p&gt;One nice side-benefit is the way dependencies on OS package are made more explicit. And I don&#x27;t think it would take much more work to have reproducible configurations to share with other developers and&#x2F;or for use in continuous integration and&#x2F;or deployments.&lt;&#x2F;p&gt;

&lt;p&gt;As usual the source code is available in &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;rails-on-nix&quot;&gt;a GitHub repository&lt;&#x2F;a&gt; and there are instructions on how to run it yourself in the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;rails-on-nix&#x2F;blob&#x2F;main&#x2F;README.md&quot;&gt;README&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next steps&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Come up with a better way to manage each database instance, i.e. not in a shellHook - either using a separate script (or possibly using &lt;a href=&quot;https:&#x2F;&#x2F;systemd.io&#x2F;&quot;&gt;systemd&lt;&#x2F;a&gt;?).&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Use specific patch versions of Ruby or minor versions of Ruby not available in the current set of nix packages. I&#x27;m pretty confident this is possible by &lt;a href=&quot;https:&#x2F;&#x2F;nixos.wiki&#x2F;wiki&#x2F;FAQ&#x2F;Pinning_Nixpkgs&quot;&gt;pinning the version of nix packages&lt;&#x2F;a&gt; or it might be worth investigating &lt;a href=&quot;https:&#x2F;&#x2F;nixos.wiki&#x2F;wiki&#x2F;Flakes&quot;&gt;nix flakes&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Use specific versions of Bundler. I haven&#x27;t really looked into this at all yet, because I&#x27;m not sure it&#x27;s a deal-breaker.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Investigate how hard it is to upgrade a gem in one of these Rails apps, i.e. regenerating the &lt;code&gt;Gemfile.lock&lt;&#x2F;code&gt; and &lt;code&gt;gemset.nix&lt;&#x2F;code&gt; files.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Investigate using &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;direnv&#x2F;direnv&#x2F;wiki&#x2F;Nix&quot;&gt;direnv in conjunction with nix&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;target&#x2F;lorri&#x2F;&quot;&gt;lorri&lt;&#x2F;a&gt; to seamlessly move between different Rails app directories without having to explicitly enter&#x2F;exit the relevant nix-shell.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Investigate using Nix to somehow make Node packages available to the environment in a similar way to Bundix instead of using Yarn directly, i.e. also automatically installing any OS package dependencies.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Investigate using &lt;a href=&quot;https:&#x2F;&#x2F;nixos.wiki&#x2F;wiki&#x2F;Home_Manager&quot;&gt;Nix home-manager&lt;&#x2F;a&gt; or custom scripting to make it easy to be able to run &lt;code&gt;rails new&lt;&#x2F;code&gt; for a specified version of Ruby and Rails.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;

&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;This is the third article in a series:
    &lt;ol&gt;
      &lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;2020-09-10-a-simple-rails-development-environment-using-nix-shell&quot;&gt;A simple Rails development environment using nix-shell&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
      &lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;2020-10-12-generating-and-running-a-rails-app-with-postgresql-using-nix-on-ubuntu&quot;&gt;Generating and running a Rails app with PostgreSQL using Nix on Ubuntu&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
    &lt;&#x2F;ol&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.sulami.xyz&#x2F;posts&#x2F;nix-for-developers&#x2F;&quot;&gt;Lightning Introduction to Nix for Developers&lt;&#x2F;a&gt; by Robin Schroer.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;ghedam.at&#x2F;15978&#x2F;an-introduction-to-nix-shell&quot;&gt;An introduction to nix-shell&lt;&#x2F;a&gt; by Mattia Gheda.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;better-programming&#x2F;easily-reproducible-development-environments-with-nix-and-direnv-e8753f456110&quot;&gt;Easy Reproducible Development Environments with Nix and direnv&lt;&#x2F;a&gt; by Tom Feron.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;lazamar.co.uk&#x2F;nix-versions&#x2F;&quot;&gt;Nix package versions&lt;&#x2F;a&gt; by Marcelo Lazaroni. Find all versions of a package that were available in a channel and the revision you can download it from.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.mankier.com&#x2F;package&#x2F;nix&quot;&gt;Man pages for Nix command line tools&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;


    </content>
  </entry>
  <entry>
    <author>
      <name>James Mead</name>
    </author>
    <id>urn:uuid:e3390a6e-2423-4fe8-8089-7b12c3c8c3e9</id>
    <published>2020-10-13T08:29:00+00:00</published>
    <updated>2020-10-13T08:29:00+00:00</updated>
    <title>Automatically sending Webmentions from a static website</title>
    <link href="https://jamesmead.org/blog/2020-10-13-sending-webmentions-from-a-static-website" rel="alternate" type="text/html"/>
    <content type="html">
      &lt;p&gt;A few months back I wrote about &lt;a href=&quot;https:&#x2F;&#x2F;jamesmead.org&#x2F;blog&#x2F;2020-06-27-indieweb-ifying-my-personal-website&quot;&gt;indieweb-ifying this website&lt;&#x2F;a&gt;. I attempted to follow the excellent &lt;a href=&quot;https:&#x2F;&#x2F;indiewebify.me&#x2F;&quot;&gt;indiewebify.me guide&lt;&#x2F;a&gt;, but I skipped step 2 of Level 2, i.e. &lt;a href=&quot;https:&#x2F;&#x2F;indiewebify.me&#x2F;#send-webmentions&quot;&gt;adding the ability to send Webmentions to other IndieWeb sites&lt;&#x2F;a&gt;. My &lt;a href=&quot;https:&#x2F;&#x2F;jamesmead.org&#x2F;blog&#x2F;2020-06-27-indieweb-ifying-my-personal-website#publishing-on-the-indieweb&quot;&gt;excuse&lt;&#x2F;a&gt; at the time was:&lt;&#x2F;p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I decided to skip this step for now given that it&#x27;s relatively easy to &lt;a href=&quot;https:&#x2F;&#x2F;indieweb.org&#x2F;webmention-implementation-guide#One-liner_webmentions&quot;&gt;send a Webmention manually using &lt;code&gt;curl&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; and it&#x27;s not as if I currently blog that frequently!&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;

&lt;p&gt;Anyway a couple of recent discoveries led me to fix this omission…&lt;&#x2F;p&gt;

&lt;h3 id=&quot;webmentionapp&quot;&gt;webmention.app&lt;&#x2F;h3&gt;

&lt;p&gt;This lovely little &lt;a href=&quot;https:&#x2F;&#x2F;webmention.app&#x2F;&quot;&gt;service&lt;&#x2F;a&gt; built by &lt;a href=&quot;https:&#x2F;&#x2F;remysharp.com&#x2F;&quot;&gt;Remy Sharp&lt;&#x2F;a&gt;, not to be confused with &lt;a href=&quot;https:&#x2F;&#x2F;webmention.io&#x2F;&quot;&gt;webmention.io&lt;&#x2F;a&gt; which is used for &lt;em&gt;receiving&lt;&#x2F;em&gt; incoming &lt;a href=&quot;https:&#x2F;&#x2F;indieweb.org&#x2F;Webmention&quot;&gt;Webmentions&lt;&#x2F;a&gt;, makes it easy to &lt;em&gt;send&lt;&#x2F;em&gt; outgoing webmentions for all the links on a given page:&lt;&#x2F;p&gt;

&lt;blockquote&gt;
  &lt;p&gt;This is a platform agnostic service that will check a given URL for links to other sites, discover if they support webmentions, then send a webmention to the target.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;

&lt;p&gt;Fortunately I still have an &lt;a href=&quot;https:&#x2F;&#x2F;feeds.jamesmead.org&#x2F;floehopper-blog&quot;&gt;RSS feed&lt;&#x2F;a&gt; for my blog and in this case the documentation &lt;a href=&quot;https:&#x2F;&#x2F;webmention.app&#x2F;docs#using-ifttt-to-trigger-checks&quot;&gt;suggests using IFTTT&lt;&#x2F;a&gt; to automate doing this each time you publish an article.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;actionsflow&quot;&gt;Actionsflow&lt;&#x2F;h3&gt;

&lt;p&gt;Somewhat serendipitously I recently came across &lt;a href=&quot;https:&#x2F;&#x2F;actionsflow.github.io&#x2F;docs&#x2F;&quot;&gt;Actionsflow&lt;&#x2F;a&gt; which is a free Zapier&#x2F;IFTTT alternative for developers to automate workflows based on GitHub Actions.&lt;&#x2F;p&gt;

&lt;p&gt;I have to admit that I was initially quite confused by the Actionsflow documentation and I tried to add my Webmention-sending workflow to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;jamesmead.org&quot;&gt;the repo for this website&lt;&#x2F;a&gt;. However, once I realised the idea was to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;actionsflow&#x2F;actionsflow-workflow-default&#x2F;generate&quot;&gt;create a new repo&lt;&#x2F;a&gt; based on a template, things became a little clearer.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;workflow-to-send-webmentions&quot;&gt;Workflow to send Webmentions&lt;&#x2F;h3&gt;

&lt;p&gt;I created &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;send-webmentions&quot;&gt;this repo&lt;&#x2F;a&gt; and added &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;send-webmentions&#x2F;blob&#x2F;main&#x2F;workflows&#x2F;send-webmentions.yml&quot;&gt;this workflow&lt;&#x2F;a&gt; to poll my RSS feed and send an HTTP POST request to the webmention.app API for every new item. I was pleasantly surprised by how simple this was:&lt;&#x2F;p&gt;

&lt;pre&gt;&lt;code&gt;name: Send webmentions for new blog posts
on:
  rss:
    url: https:&#x2F;&#x2F;feeds.jamesmead.org&#x2F;floehopper-blog
    config:
      logLevel: debug
      limit: 1
jobs:
  send_webmentions:
    name: Send webmentions
    runs-on: ubuntu-latest
    steps:
      - name: &#x27;Send webmentions for RSS item link&#x27;
        uses: actionsflow&#x2F;axios@v1
        with:
          url: https:&#x2F;&#x2F;webmention.app&#x2F;check&#x2F;
          method: &#x27;POST&#x27;
          params: &#x27;{ &quot;url&quot;: &quot;${{on.rss.outputs.link}}&quot;, &quot;token&quot;: &quot;${{ secrets.WM_TOKEN }}&quot; }&#x27;
          is_debug: true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;

&lt;p&gt;It took me a while to realise that the underlying Actionsflow GitHub Action was running every 5 minutes and &lt;em&gt;polling&lt;&#x2F;em&gt; my RSS feed. It seems to use the GitHub Action cache to &quot;remember&quot; which items it has seen before. Since I don&#x27;t publish blog posts very often, polling every 5 minutes seemed a bit excessive and so I decided to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;floehopper&#x2F;send-webmentions&#x2F;commit&#x2F;eb5a9cb573b1c532c92143b7fb2aed260c5fa552&quot;&gt;reduce the frequency to hourly&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

&lt;h3 id=&quot;observations&quot;&gt;Observations&lt;&#x2F;h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;I&#x27;m not sure I like the design of Actionsflow which means creating a new repo, but perhaps this would make more sense to me if I had more than one workflow. I suppose this repo is roughly equivalent to a single IFTTT account.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;Over the course of the last year I&#x27;ve automated some backup jobs for &lt;a href=&quot;https:&#x2F;&#x2F;gofreerange.com&quot;&gt;Go Free Range&lt;&#x2F;a&gt; using the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cdk&#x2F;api&#x2F;latest&#x2F;typescript&#x2F;api&#x2F;aws-ecs-patterns&#x2F;scheduledfargatetask.html#aws_ecs_patterns_ScheduledFargateTask&quot;&gt;&lt;code&gt;ScheduledFargateTask&lt;&#x2F;code&gt; class&lt;&#x2F;a&gt; in the &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;cdk&#x2F;&quot;&gt;AWS CDK&lt;&#x2F;a&gt; to fire up a container and run a script on a cron schedule. This has worked really well, but it&#x27;s quite tempting to port these over to Actionsflow so we don&#x27;t have to maintain anything other than the &lt;code&gt;Dockerfile&lt;&#x2F;code&gt; and associated shell scripts.&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;webmention.app is really nicely implemented with good documentation; it&#x27;s a classic example of an elegant solution to a tightly scoped problem. Since I&#x27;ll be making use of the API on a regular basis, I decided to &lt;a href=&quot;https:&#x2F;&#x2F;paypal.me&#x2F;rem&quot;&gt;buy Remy a drink&lt;&#x2F;a&gt; to say thank you!&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
  &lt;li&gt;
    &lt;p&gt;I&#x27;d also like to find a way to say thank you to &lt;a href=&quot;https:&#x2F;&#x2F;aaronparecki.com&#x2F;&quot;&gt;Aaron Parecki&lt;&#x2F;a&gt; who built webmention.io and &lt;a href=&quot;https:&#x2F;&#x2F;snarfed.org&#x2F;&quot;&gt;Ryan Barrett&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;kylewm.com&#x2F;&quot;&gt;Kyle Mahan&lt;&#x2F;a&gt;, et al who built &lt;a href=&quot;https:&#x2F;&#x2F;brid.gy&#x2F;&quot;&gt;brid.gy&lt;&#x2F;a&gt;. However, I can&#x27;t see a way to do either and, indeed, the latter &lt;a href=&quot;https:&#x2F;&#x2F;brid.gy&#x2F;about#cost&quot;&gt;explicitly say&lt;&#x2F;a&gt; &quot;We don&#x27;t need donations, promise.&quot;&lt;&#x2F;p&gt;
  &lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;


    </content>
  </entry>
</feed>
