<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://bridgetownrb.com/" version="2.1.1">Bridgetown</generator><link href="https://davidrunger.com/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://davidrunger.com/blog/" rel="alternate" type="text/html" /><updated>2026-03-11T23:15:35-05:00</updated><id>https://davidrunger.com/blog/feed.xml</id><title type="html">David Runger : Blog</title><subtitle>I&apos;m a full stack web developer focusing on Ruby on Rails, and this is my tech blog.</subtitle><entry><title type="html">Easy PostgreSQL major version upgrades for a Docker Compose hobby app</title><link href="https://davidrunger.com/blog/easy-postgresql-major-version-upgrades-for-a-docker-compose-hobby-app" rel="alternate" type="text/html" title="Easy PostgreSQL major version upgrades for a Docker Compose hobby app" /><published>2026-01-22T00:00:00-06:00</published><updated>2026-01-22T00:00:00-06:00</updated><id>repo://posts.collection/_posts/2026-01-22-easy-postgresql-major-version-upgrades-for-a-docker-compose-hobby-app.md</id><content type="html" xml:base="https://davidrunger.com/blog/easy-postgresql-major-version-upgrades-for-a-docker-compose-hobby-app">&lt;p&gt;Upgrading PostgreSQL major versions in Docker doesn’t have to be scary. For small hobby apps, a simple dump-and-restore process can be the safest and most understandable method.&lt;/p&gt;

&lt;p&gt;Below is a step-by-step runbook that I use when upgrading PostgreSQL to a new major version for my hobby app, which is managed using Docker Compose. The runbook is a sequence of commands that I paste into an SSH session one at a time, checking results after every step.&lt;/p&gt;

&lt;p&gt;This approach has worked well for my small app, and this post walks through the workflow and explains why each step exists.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;caveats&quot;&gt;Caveats&lt;/h2&gt;

&lt;p&gt;This upgrade process works well for my &lt;strong&gt;small hobby app&lt;/strong&gt;, but it does come with tradeoffs.&lt;/p&gt;

&lt;p&gt;It requires some downtime (hopefully just a few minutes), involves manually running commands directly on the server (somewhat violating strict “infrastructure as code” practices), and assumes a relatively small database (mine is currently ~122 MB). For much larger databases or stricter uptime requirements, this approach is likely too slow and too manual.&lt;/p&gt;

&lt;p&gt;But if you’re in a similar position — running a personal app and looking for a straightforward, low-risk way to do Postgres major upgrades — this process might be useful to you.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;assumptions&quot;&gt;Assumptions&lt;/h2&gt;

&lt;p&gt;This workflow assumes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Postgres runs via Docker Compose&lt;/li&gt;
  &lt;li&gt;Your database data lives in a named Docker volume&lt;/li&gt;
  &lt;li&gt;You’re OK with downtime during dump + restore&lt;/li&gt;
  &lt;li&gt;You can SSH into the server and run commands interactively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll need to adapt service names, database names, and volume names to your specific setup.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;high-level-strategy&quot;&gt;High-level strategy&lt;/h2&gt;

&lt;p&gt;This upgrade process will:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Verify the current database state&lt;/li&gt;
  &lt;li&gt;Take a logical backup&lt;/li&gt;
  &lt;li&gt;Start a brand-new Postgres major version with a fresh data directory (new volume)&lt;/li&gt;
  &lt;li&gt;Restore the backup to the new Postgres database&lt;/li&gt;
  &lt;li&gt;Verify correctness&lt;/li&gt;
  &lt;li&gt;Delete the old data only after everything looks good&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The old database remains untouched until the very end, which makes rollback trivial.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;check-application-compatibility-first&quot;&gt;Check application compatibility first&lt;/h2&gt;

&lt;p&gt;Before starting the upgrade process, verify that your application is compatible with the new Postgres major version. Review the Postgres release notes for breaking changes or deprecated features that might affect your app and run your test suite against the new version.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-by-step-upgrade-runbook-postgres-17-to-18-example&quot;&gt;Step-by-step upgrade runbook (Postgres 17 to 18 example)&lt;/h2&gt;

&lt;p&gt;Each step below is intended to be copy/pasted &lt;strong&gt;individually&lt;/strong&gt; on your server machine, with you verifying output before moving on.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;0-make-sure-your-deployment-config-is-ready&quot;&gt;0. Make sure your deployment config is ready&lt;/h3&gt;

&lt;p&gt;Before touching production, make sure that a PR to update your Postgres image and volume name (with the exact same changes seen in the &lt;code class=&quot;highlighter-rouge&quot;&gt;sed&lt;/code&gt; commands below) is ready to merge. In the steps below, we will apply changes temporarily to upgrade, then revert to keep the repo clean, then merge a PR to make them permanent.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Make sure that PR to update docker-compose.yml to Postgres 18 is ready to merge.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;1-check-current-state&quot;&gt;1. Check current state&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git status
git show
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user app_production &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;SELECT VERSION();&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user app_production &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;SELECT COUNT(*) FROM users; SELECT * FROM orders ORDER BY created_at DESC LIMIT 1;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This gives you a concrete baseline for later verification.&lt;/p&gt;

&lt;h3 id=&quot;2-create-an-off-server-backup-optional-but-recommended&quot;&gt;2. Create an off-server backup (optional, but recommended)&lt;/h3&gt;

&lt;p&gt;If you have an existing backup pipeline (e.g. S3 snapshots), now is the time to run it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bin/backup-to-s3.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This gives you an escape hatch, even if the server melts down mid-upgrade.&lt;/p&gt;

&lt;h3 id=&quot;3-update-docker-config-to-the-new-postgres-version-and-volume&quot;&gt;3. Update Docker config to the new Postgres version and volume&lt;/h3&gt;

&lt;p&gt;Edit &lt;code class=&quot;highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; so that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Postgres image moves from postgres:17.x to postgres:18.x&lt;/li&gt;
  &lt;li&gt;The data volume moves from postgres-data-v17 to postgres-data-v18&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changing the volume name ensures the new Postgres version starts with a completely empty directory, avoiding errors like “The data directory was initialized by PostgreSQL version 17, which is not compatible with this version 18.0”.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/postgres:17.6-alpine/postgres:18.0-alpine/g&apos;&lt;/span&gt; docker-compose.yml
&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/postgres-data-v17:/postgres-data-v18:/g&apos;&lt;/span&gt; docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then review:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git status
git diff
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nothing should be changed except the Postgres image tag and volume name in &lt;code class=&quot;highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;4-pre-pull-the-new-postgres-image&quot;&gt;4. Pre-pull the new Postgres image&lt;/h3&gt;

&lt;p&gt;This reduces downtime later (since we won’t waste time downloading the new Postgres Docker image while our app is down):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose pull postgres
docker images postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;5-stop-services-that-talk-to-the-database-or-serve-the-web-app&quot;&gt;5. Stop services that talk to the database or serve the web app&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose stop web worker nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, nothing should be writing to Postgres.&lt;/p&gt;

&lt;h3 id=&quot;6-create-a-logical-backup-from-the-old-database&quot;&gt;6. Create a logical backup from the old database&lt;/h3&gt;

&lt;p&gt;We use &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_dumpall&lt;/code&gt; to capture everything, including global objects (such as roles and permissions). This creates a portable SQL script that works across Postgres major versions.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Permission note:&lt;/strong&gt; &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_dumpall&lt;/code&gt; requires superuser privileges for complete dumps. To check if your app user is a superuser, run this command (substituting your Postgres app user name for &lt;code class=&quot;highlighter-rouge&quot;&gt;app_user&lt;/code&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;SELECT rolsuper FROM pg_roles WHERE rolname = current_user;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If this returns &lt;code class=&quot;highlighter-rouge&quot;&gt;true&lt;/code&gt;/&lt;code class=&quot;highlighter-rouge&quot;&gt;t&lt;/code&gt;, then you can proceed.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres pg_dumpall &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; backup.sql
&lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-5&lt;/span&gt; backup.sql | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PostgreSQL&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;✓ Backup format looks correct&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-lh&lt;/span&gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Confirm the file size looks roughly correct before continuing.&lt;/p&gt;

&lt;h3 id=&quot;7-shut-down-the-old-postgres-container&quot;&gt;7. Shut down the old Postgres container&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose down postgres
docker ps
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should no longer see the Postgres container running.&lt;/p&gt;

&lt;h3 id=&quot;8-start-postgres-on-the-new-major-version&quot;&gt;8. Start Postgres on the new major version&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose up &lt;span class=&quot;nt&quot;&gt;--detach&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then verify:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;SELECT VERSION();&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should now see PostgreSQL 18.x.&lt;/p&gt;

&lt;h3 id=&quot;9-restore-the-backup&quot;&gt;9. Restore the backup&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-TTY&lt;/span&gt; postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user &amp;lt; backup.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This puts the data into the new Postgres database.&lt;/p&gt;

&lt;h3 id=&quot;10-verify-the-data&quot;&gt;10. Verify the data&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user app_production &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;SELECT COUNT(*) FROM users; SELECT * FROM orders ORDER BY created_at DESC LIMIT 1;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should see the same results as before the upgrade.&lt;/p&gt;

&lt;h3 id=&quot;11-restart-application-services&quot;&gt;11. Restart application services&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose up &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; web worker nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now verify that your web app loads and that database writes succeed (e.g. fill out a form to create a test record).&lt;/p&gt;

&lt;h3 id=&quot;12-clean-up&quot;&gt;12. Clean up&lt;/h3&gt;

&lt;p&gt;Once you’re fully confident:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;rm &lt;/span&gt;backup.sql
git checkout docker-compose.yml
docker volume &lt;span class=&quot;nb&quot;&gt;rm &lt;/span&gt;app_postgres-data-v17
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;13-merge-pr-that-commits-the-above-changes&quot;&gt;13. Merge PR that commits the above changes&lt;/h3&gt;

&lt;p&gt;Merge and deploy the PR that you prepared in Step 0, committing to version control the same changes that you temporarily made above.&lt;/p&gt;

&lt;h3 id=&quot;14-post-deploy-sanity-check&quot;&gt;14. Post-deploy sanity check&lt;/h3&gt;

&lt;p&gt;After the PR has deployed to your server, make sure that the Postgres version is still the new major version and confirm that the data is still what you expect.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user app_production &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;SELECT VERSION();&apos;&lt;/span&gt;
docker compose &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;postgres psql &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; app_user app_production &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;SELECT COUNT(*) FROM users; SELECT * FROM orders ORDER BY created_at DESC LIMIT 1;&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, the upgrade is complete.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;rolling-back&quot;&gt;Rolling back&lt;/h2&gt;

&lt;p&gt;Rollback is possible at any point before executing &lt;code class=&quot;highlighter-rouge&quot;&gt;docker volume rm app_postgres-data-v17&lt;/code&gt; in Step 12.&lt;/p&gt;

&lt;p&gt;If anything fails before deleting the old volume:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker compose down
git checkout docker-compose.yml
docker compose up &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; postgres
docker compose up &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; web worker nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You’re instantly back to the original database.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;when-this-approach-is-a-bad-fit&quot;&gt;When this approach is a bad fit&lt;/h2&gt;

&lt;p&gt;You probably want to use &lt;code class=&quot;highlighter-rouge&quot;&gt;pg_upgrade&lt;/code&gt; or do a replication-based cutover if your database is very large and/or absolutely minimizing downtime is important.&lt;/p&gt;

&lt;p&gt;But, for a small hobby app, this approach is pretty safe, easy, and simple for an app whose Postgres database is managed with Docker Compose, which can make Postgres major version upgrades relatively complicated.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;

&lt;p&gt;If you want Postgres major upgrades that are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Low-risk&lt;/li&gt;
  &lt;li&gt;Easy to roll back&lt;/li&gt;
  &lt;li&gt;Easy to reason about&lt;/li&gt;
  &lt;li&gt;Easy to repeat every few years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;… then this pattern has worked well for me. It’s not the fastest possible approach, but it’s pretty safe and pretty easy.&lt;/p&gt;

&lt;p&gt;I hope that this might be helpful to you!&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;appendix-a-complete-real-example-script&quot;&gt;Appendix: a complete, real example script&lt;/h2&gt;

&lt;p&gt;I use this exact process to upgrade Postgres major versions in production for the &lt;a href=&quot;https://davidrunger.com/&quot;&gt;davidrunger.com&lt;/a&gt; database. You can see the &lt;a href=&quot;https://github.com/davidrunger/david_runger/blob/main/docs/postgres-17-to-18-upgrade.sh&quot;&gt;current version of the playbook here&lt;/a&gt; (and also my &lt;a href=&quot;https://github.com/davidrunger/david_runger/blob/main/docker-compose.yml&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The playbook includes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My service names&lt;/li&gt;
  &lt;li&gt;My deployment flow&lt;/li&gt;
  &lt;li&gt;My verification queries&lt;/li&gt;
  &lt;li&gt;My volume naming scheme&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seeing that concrete example for my specific app might help you to understand how you might need to adjust the commands above to fit your own app.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://davidrunger.com/blog/images/davidrunger.jpg" /><media:content medium="image" url="https://davidrunger.com/blog/images/davidrunger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Automatically compiling Crystal for development tooling</title><link href="https://davidrunger.com/blog/automatically-compiling-crystal-for-development-tooling" rel="alternate" type="text/html" title="Automatically compiling Crystal for development tooling" /><published>2024-08-08T00:00:00-05:00</published><updated>2024-08-08T00:00:00-05:00</updated><id>repo://posts.collection/_posts/2024-08-08-automatically-compiling-crystal-for-development-tooling.md</id><content type="html" xml:base="https://davidrunger.com/blog/automatically-compiling-crystal-for-development-tooling">&lt;h2 id=&quot;the-importance-of-good-tooling&quot;&gt;The importance of good tooling&lt;/h2&gt;

&lt;p&gt;Having good tooling can be an important part of the software development process. Good tooling can help a developer to operate more quickly/efficiently, and also to get into a flow state. If one’s tooling can take care of some of the more mundane aspects of software development, then more of that developer’s time, energy, and uninterrupted focus can be spent on the higher-value aspects of software delivery.&lt;/p&gt;

&lt;h2 id=&quot;custom-tooling&quot;&gt;Custom tooling&lt;/h2&gt;

&lt;p&gt;Lots of great tooling is available off the shelf, and it’s usually best to leverage the work done by others, when it’s possible to do so, rather than reinventing the wheel.&lt;/p&gt;

&lt;p&gt;However, there are some tools one will want that are unique to one’s idiosyncratic workflow, or, for whatever other reason, off-the-shelf tooling might not be available to meet a given need. In these cases, happily, as software developers, we can write our own tooling! I often do.&lt;/p&gt;

&lt;p&gt;A lot of the tools that I create for myself are programs that I execute from a terminal, or which are invoked indirectly by some &lt;em&gt;other&lt;/em&gt; command that I execute in a terminal.&lt;/p&gt;

&lt;p&gt;For example, I often want to pull updates from a GitHub repository down to my local machine, and then rebase my branch onto that updated version of the &lt;code class=&quot;highlighter-rouge&quot;&gt;main&lt;/code&gt; branch. I do this by executing in my terminal a command that I’ve written called &lt;code class=&quot;highlighter-rouge&quot;&gt;gform&lt;/code&gt; (which stands for “git fetch origin and rebase with main”).&lt;/p&gt;

&lt;p&gt;In addition to updating my branch with the latest version of the &lt;code class=&quot;highlighter-rouge&quot;&gt;main&lt;/code&gt; branch, this &lt;code class=&quot;highlighter-rouge&quot;&gt;gform&lt;/code&gt; command also executes another program that I’ve written, called &lt;code class=&quot;highlighter-rouge&quot;&gt;install-packages-in-background&lt;/code&gt;, which looks at the project’s dependency lock files (the &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;pnpm-lock.yaml&lt;/code&gt;, etc) and checks whether the relevant package installation command (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;pnpm install&lt;/code&gt;) has ever been executed on my machine for the current version of the dependency lock file. If not, then my &lt;code class=&quot;highlighter-rouge&quot;&gt;install-packages-in-background&lt;/code&gt; script will execute the relevant package installation command (in the background).&lt;/p&gt;

&lt;h2 id=&quot;which-language-to-use&quot;&gt;Which language to use?&lt;/h2&gt;

&lt;p&gt;Which computer language should &lt;code class=&quot;highlighter-rouge&quot;&gt;install-packages-in-background&lt;/code&gt; be written in? Since it’s just a command that I am running on my local machine, I could write it in pretty much any language that I can install on my machine.&lt;/p&gt;

&lt;h2 id=&quot;consideration-startup-time&quot;&gt;Consideration: startup time&lt;/h2&gt;

&lt;p&gt;As a primarily Ruby and JavaScript developer, I always have those languages installed on my development machine, so those are options. However, one downside of these languages is that they have a somewhat noticeable startup time. Running about the most minimal Ruby program imaginable takes over 130 milliseconds on my machine:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ time ruby -e &apos;puts(&quot;Hi!&quot;)&apos;
Hi!
ruby -e &apos;puts(&quot;Hi!&quot;)&apos;  0.09s user 0.04s system 99% cpu 0.132 total
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Node is even a little bit slower, taking about 200 ms to start up:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ time node -e &apos;console.log(&quot;Hi!&quot;)&apos;
Hi!
node -e &apos;console.log(&quot;Hi!&quot;)&apos;  0.18s user 0.07s system 122% cpu 0.205 total
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few hundred milliseconds might not seem like a lot, but it adds up, and the time spent waiting for a relatively slow Ruby or Node program to execute risks interfering with one’s development flow state.&lt;/p&gt;

&lt;h2 id=&quot;bash-the-fast-solution&quot;&gt;Bash: the fast solution?&lt;/h2&gt;

&lt;p&gt;Another (and typically much faster-executing) option is to use a shell scripting language, such as bash. And, indeed, I do write the vast majority of my little development tooling scripts in bash. Bash starts up much more quickly than Ruby or Node:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ time bash -c &apos;echo &quot;Hi!&quot;&apos;
Hi!
bash -c &apos;echo &quot;Hi!&quot;&apos;  0.00s user 0.00s system 89% cpu 0.006 total
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Just six milliseconds!&lt;/p&gt;

&lt;h2 id=&quot;bash-not-fun-for-complicated-logic&quot;&gt;Bash: not fun for complicated logic&lt;/h2&gt;

&lt;p&gt;However, while bash can do a lot, I don’t find it very pleasant to work with when it comes to performing any sort of semi-complicated logic.&lt;/p&gt;

&lt;h2 id=&quot;bash-no-package-system-to-lean-on&quot;&gt;Bash: no package system to lean on&lt;/h2&gt;

&lt;p&gt;Additionally, the bash scripting language doesn’t really have a package management framework, so we don’t really have the ability to easily leverage the work of others. Our bash scripts must be pretty self contained and do everything themselves. Bash scripts can call out to other programs, but there aren’t generally libraries that we can load into our script to provide useful functionality.&lt;/p&gt;

&lt;h2 id=&quot;what-about-lua&quot;&gt;What about Lua?&lt;/h2&gt;

&lt;p&gt;Another language that I experimented with recently is Lua. It has a fast startup time that is comparable to bash’s (just a few milliseconds) and a package management system (LuaRocks). I was hopeful that I’d find the language/syntax more pleasant and natural to work with than bash.&lt;/p&gt;

&lt;p&gt;However, after trying Lua, I found it’s standard library to be quite limited. As a result, I had to manually implement some basic functionality (such as merging two dictionaries) that I had hoped/expected would be built into the language.&lt;/p&gt;

&lt;h2 id=&quot;could-we-use-a-compiled-language&quot;&gt;Could we use a compiled language?&lt;/h2&gt;

&lt;p&gt;All of the languages that I have named so far are &lt;em&gt;interpreted languages&lt;/em&gt; (as opposed to compiled ones). I never thought that a compiled programming language would work well for writing the sort of developer tooling programs that I’ve been discussing, for the simple reason that I’d have to remember and bother with recompiling the program into a binary whenever I make a change to the program, and I’d also need to make that compiled binary available as an executable program on my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt; (so that they can be invoked easily from the command line or from other programs).&lt;/p&gt;

&lt;p&gt;In contrast, when writing a program in an interpreted language, all that I have to do is to put the program’s source code in a location on my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt;. Then, whenever I edit the program source code, that new version of the program is immediately “live” on my system, without any additional steps on my part. The relative hassle of a compiled language didn’t seem worthwhile to me.&lt;/p&gt;

&lt;h2 id=&quot;the-big-idea-automatically-compiled-programs&quot;&gt;The big idea: automatically compiled programs&lt;/h2&gt;

&lt;p&gt;However, that all changed, when ChatGPT and I collaborated on a way to automatically compile and make available on my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt; the executable binary output of compiled development tooling programs. This opened up a whole new world of compiled programming language options that I could now consider for writing development tooling programs, without the hassle that had made me shy away from compiled languages for this purpose up until now.&lt;/p&gt;

&lt;h2 id=&quot;my-go-to-compiled-language-crystal&quot;&gt;My go-to compiled language: Crystal&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://david-runger-public-uploads.s3.amazonaws.com/crystal.png&quot; alt=&quot;Crystal logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The compiled language that I decided to try out first as a language in which to write development tooling is &lt;strong&gt;&lt;a href=&quot;https://crystal-lang.org/&quot;&gt;Crystal&lt;/a&gt;&lt;/strong&gt;. Crystal is a compiled language with a syntax that is very similar to Ruby (with the main difference being that Crystal sometimes requires type annotations). Crystal programs are extremely fast, often comparable to (or even faster than) a raw C implementation. Crystal programs also use much less memory than an equivalent Ruby program would.&lt;/p&gt;

&lt;p&gt;Crystal also has extremely helpful and well formatted error messages, and &lt;a href=&quot;https://crystal-lang.org/api/master/&quot;&gt;the documentation&lt;/a&gt; is useful and easy to read.&lt;/p&gt;

&lt;p&gt;Overall, I find it a pleasure to work with, and I feel lucky that there exists a language with the ease and beauty of Ruby and yet also with the speed of C and light memory usage.&lt;/p&gt;

&lt;h2 id=&quot;these-concepts-apply-to-any-compiled-language&quot;&gt;These concepts apply to any compiled language&lt;/h2&gt;

&lt;p&gt;The focus of this blog post is not about Crystal, though. What I want to focus on is the framework that ChatGPT and I came up with to automatically compile my Crystal development tooling programs and make them available on my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt;. Indeed, there is very little that is Crystal-specific in what I’m about to share, and I think that this framework/process could easily be adapted to almost any other compiled language, such as Rust or Go.&lt;/p&gt;

&lt;h2 id=&quot;example-my-unique-union-program&quot;&gt;Example: my &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt; program&lt;/h2&gt;

&lt;p&gt;To illustrate concretely, let’s look at one particular program that I have written in Crystal, called &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt;. This program takes two arguments, and prints the set of words in those arguments, deduplicated and sorted. Example:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ unique-union &quot;wave hello bye&quot; &quot;hello ocean wave&quot;
bye
hello
ocean
wave
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(This might seem kind of pointless, but it’s useful within one of my other tools.)&lt;/p&gt;

&lt;p&gt;That functionality is implemented in Crystal as such:&lt;/p&gt;

&lt;div class=&quot;language-cr highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env crystal&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# file location: ~/code/dotfiles/crystal-programs/unique-union.cr&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;unique_union&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set2&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;set1_words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;set2_words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set1_words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set2_words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ARGV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;unique_union&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ARGV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ARGV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Usage: unique-union &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;first list of words&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;second list of words&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;setting-up-automatic-crystal-compilation&quot;&gt;Setting up automatic Crystal compilation&lt;/h2&gt;

&lt;p&gt;What we want is some way to automatically convert that Crystal source code into an executable binary.&lt;/p&gt;

&lt;h3 id=&quot;part-1-symlink-crystal-programs&quot;&gt;Part 1: &lt;code class=&quot;highlighter-rouge&quot;&gt;symlink-crystal-programs&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Part of the trick comes from adding this line to my &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.zshrc&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Set up (in the background) symlinks for programs written in Crystal&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt; symlink-crystal-programs &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;3 &amp;amp; &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt; 3&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Most of that syntax is just to make the command execute without printing any output, and to do so in the background (so that it doesn’t slow down my shell startup time). The key point is that, whenever I open a new terminal tab, &lt;code class=&quot;highlighter-rouge&quot;&gt;symlink-crystal-programs&lt;/code&gt; will execute, which is the following bash program.&lt;/p&gt;

&lt;p&gt;It iterates over all of the Crystal source code files in my &lt;code class=&quot;highlighter-rouge&quot;&gt;crystal-programs&lt;/code&gt; source directory, and, for each program, creates a symlink that is available in my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt; and which points to another bash program (&lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt;) that will automatically compile the Crystal source code into a binary, as needed.&lt;/p&gt;

&lt;p&gt;The end result will look like this:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ tree ~/bin/crystal-symlinks
/home/david/bin/crystal-symlinks
├── install-packages-in-background -&amp;gt; /home/david/code/dotfiles/bin/run-crystal-program
├── open-pr-in-browser -&amp;gt; /home/david/code/dotfiles/bin/run-crystal-program
├── runger-config -&amp;gt; /home/david/code/dotfiles/bin/run-crystal-program
└── unique-union -&amp;gt; /home/david/code/dotfiles/bin/run-crystal-program
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s the script that does it:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# file name (available on PATH): symlink-crystal-programs&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;crystal_programs_source_code_directory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/code/dotfiles/crystal-programs&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;crystal_executable_symlinks_directory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/bin/crystal-symlinks&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Delete the symlinks directory, to ensure that there aren&apos;t any dangling programs left there.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_executable_symlinks_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Recreate the symlinks directory.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_executable_symlinks_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;crystal_program_source_file &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_programs_source_code_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.cr &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;symlink_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_program_source_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; .cr&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;ln&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-sf&lt;/span&gt; ~/code/dotfiles/bin/run-crystal-program &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_executable_symlinks_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$symlink_name&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The symlinks drop the &lt;code class=&quot;highlighter-rouge&quot;&gt;.cr&lt;/code&gt; extension. That way, although I write the source code in a file called &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union.cr&lt;/code&gt;, I will be able to invoke the compiled program from my command line (or other programs) via simply &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;part-2-run-crystal-program&quot;&gt;Part 2: &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The final piece of the puzzle is the &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; bash script referenced as the target of the symlinks above. That script does two main things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;compiles (or recompiles) the relevant Crystal source code into an executable binary, if no binary has yet been compiled, or if the source code or the &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; script itself has been modified more recently than the binary was compiled&lt;/li&gt;
  &lt;li&gt;executes the compiled binary, forwarding along any arguments&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# file location: ~/code/dotfiles/bin/run-crystal-program&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;crystal_compiled_binaries_directory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/bin/crystal-binaries&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;script_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;source_file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/code/dotfiles/crystal-programs/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$script_name&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.cr&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;binary_file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_compiled_binaries_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$script_name&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check if the binary doesn&apos;t exist, the source file has changed, or this&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# compilation script has changed.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$source_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-nt&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;realpath&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-nt&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Create the compiled binaries directory.&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$crystal_compiled_binaries_directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Add shards directory for dotfiles to the CRYSTAL_PATH.&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# More details: https://github.com/davidrunger/dotfiles/commit/d73a9df .&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CRYSTAL_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/.shards/dotfiles:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CRYSTAL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Compile the binary.&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Compiling &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$source_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ...&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; crystal build &lt;span class=&quot;nt&quot;&gt;--warnings&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;none &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$source_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;There was an error compiling &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$source_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; .&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
  &lt;span class=&quot;k&quot;&gt;fi
fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Execute the compiled binary, passing along any provided arguments.&lt;/span&gt;
&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary_file&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;review-and-overview&quot;&gt;Review and overview&lt;/h2&gt;

&lt;p&gt;This all might seem a little bit complicated, and it is. There are three different directories involved, each with a different purpose:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/code/dotfiles/crystal-programs/&lt;/code&gt;: This is where the Crystal source code lives, e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;~/code/dotfiles/crystal-programs/unique-union.cr&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/bin/crystal-symlinks/&lt;/code&gt;: This directory (which is on my &lt;code class=&quot;highlighter-rouge&quot;&gt;PATH&lt;/code&gt;) contains symlinks, one for each Crystal program. These symlinks all point to the &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; script.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;~/bin/crystal-binaries/&lt;/code&gt;: This is where the actual compiled binaries created from the Crystal source code are stored. These compiled binaries will be invoked by &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, when I invoke &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt; from my command line, that refers to the &lt;code class=&quot;highlighter-rouge&quot;&gt;~/bin/crystal-symlinks/unique-union&lt;/code&gt; file (since &lt;code class=&quot;highlighter-rouge&quot;&gt;~/bin/crystal-symlinks&lt;/code&gt; is on my path). That file is actually just a symlink to the shell script &lt;code class=&quot;highlighter-rouge&quot;&gt;~/code/dotfiles/bin/run-crystal-program&lt;/code&gt;, so that is what actually executes when invoking &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt;. &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; ensures that there is an up-to-date compiled binary located at &lt;code class=&quot;highlighter-rouge&quot;&gt;~/bin/crystal-binaries/unique-union&lt;/code&gt;, and then executes that binary, passing along any arguments.&lt;/p&gt;

&lt;h2 id=&quot;downsides&quot;&gt;Downsides&lt;/h2&gt;

&lt;p&gt;Overall, after ironing out a kink or two, this system seems to work pretty smoothly, but there are some downsides.&lt;/p&gt;

&lt;p&gt;One downside is that, after I change any Crystal source file, then the next time that I invoke that command, there is a significant delay (a few seconds), as &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; compiles an up-to-date version of the executable binary. However, thereafter, &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; simply invokes the already-compiled binary, and so the compiled program executes quickly.&lt;/p&gt;

&lt;p&gt;Another downside is that, even when an up-to-date binary has already been compiled, this system does waste a little bit of time executing the &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; bash script, which probably adds somewhere on the order of 8 milliseconds or so to the overall execution time. It would be faster if, instead of going through the &lt;code class=&quot;highlighter-rouge&quot;&gt;run-crystal-program&lt;/code&gt; bash script, calling &lt;code class=&quot;highlighter-rouge&quot;&gt;unique-union&lt;/code&gt; would directly invoke the compiled Crystal binary. However, then I’d lose the benefit of automatic compilation and the assurance that I’m always running a binary that has been compiled using the latest version of the Crystal source code.&lt;/p&gt;

&lt;h2 id=&quot;summary-im-happy-&quot;&gt;Summary: I’m happy 🙂&lt;/h2&gt;

&lt;p&gt;For me, the benefits of this framework outweigh these downsides, and I really enjoy being able to write some of my development tooling programs using Crystal, and then executing the resulting, automatically compiled, quick, and low-memory compiled binaries.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://david-runger-public-uploads.s3.amazonaws.com/crystal-logo-stacked.png" /><media:content medium="image" url="https://david-runger-public-uploads.s3.amazonaws.com/crystal-logo-stacked.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Flaky specs due to ActionCable leakage</title><link href="https://davidrunger.com/blog/flaky-parallel-specs-caused-by-actioncable" rel="alternate" type="text/html" title="Flaky specs due to ActionCable leakage" /><published>2024-07-25T00:00:00-05:00</published><updated>2024-07-25T00:00:00-05:00</updated><id>repo://posts.collection/_posts/2024-07-25-flaky-parallel-specs-caused-by-actioncable.md</id><content type="html" xml:base="https://davidrunger.com/blog/flaky-parallel-specs-caused-by-actioncable">&lt;h2 id=&quot;a-spec-setup-at-risk-of-flaking&quot;&gt;A spec setup at risk of flaking&lt;/h2&gt;

&lt;p&gt;If the following are all true of your application, then your tests might flake (i.e. fail randomly).&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;You run tests in parallel (locally and/or in CI)&lt;/li&gt;
  &lt;li&gt;Your application uses ActionCable with the Redis adapter&lt;/li&gt;
  &lt;li&gt;Your parallel tests use the same Redis instance (even if different Redis database numbers)&lt;/li&gt;
  &lt;li&gt;You don’t have a &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt; for the &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; environment in your &lt;code class=&quot;highlighter-rouge&quot;&gt;config/cable.yml&lt;/code&gt;, or the &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt; doesn’t have any dynamic interpolation&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;why-this-setup-causes-flakiness&quot;&gt;Why this setup causes flakiness&lt;/h2&gt;

&lt;p&gt;This test setup causes flakiness because Redis’s publish/subscribe messaging system is global to a Redis instance. This creates a risk of unintentional interaction between your specs. If &lt;strong&gt;Spec A&lt;/strong&gt; triggers an ActionCable update to be broadcast, then that ActionCable update might be received in some other, simultaneously executing &lt;strong&gt;Spec B&lt;/strong&gt;. If &lt;strong&gt;Spec B&lt;/strong&gt; is a system spec or feature spec, for example, then this ActionCable update might cause a change in the data in the browser, and this unanticipated change to the browser state might cause the spec to fail.&lt;/p&gt;

&lt;p&gt;In a case like this, uncovering the cause of the flakiness can be really tricky, because — no matter how hard you look through the code of &lt;strong&gt;Spec B&lt;/strong&gt; — there is nothing there that gives a hint about why the browser UI sometimes seems to update in a seemingly random way that causes the spec to fail. This is because the thing that’s causing the unexpected state change actually originates in the code of an entirely different spec. What’s more, it’s probably pretty random which specs happen to execute at the same time in any given test run. This makes it challenging to reproduce the flakiness/failures with any reliability, adding to the difficulty of investigating the cause of the flakiness.&lt;/p&gt;

&lt;h2 id=&quot;the-solution-use-a-channel_prefix-that-is-unique-to-each-test-process&quot;&gt;The solution: use a &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt; that is unique to each test process&lt;/h2&gt;

&lt;p&gt;Fortunately, ActionCable includes a configuration option — &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt; — that can prevent this problem, by effectively keeping the ActionCable broadcasts issued by one Rails/RSpec process from being received by tests that are executing in another Rails/RSpec process.&lt;/p&gt;

&lt;p&gt;In my case, the solution looked like &lt;a href=&quot;https://github.com/davidrunger/david_runger/pull/4586&quot;&gt;changing&lt;/a&gt; the &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; section of my &lt;code class=&quot;highlighter-rouge&quot;&gt;config/cable.yml&lt;/code&gt; from this:&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel_prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;david_runger_test&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redis://localhost:6379&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;to this:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel_prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;david_runger_test&amp;lt;%= ENV[&apos;DB_SUFFIX&apos;] %&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redis://localhost:6379&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note the addition of the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;%= ENV[&apos;DB_SUFFIX&apos;] %&amp;gt;&lt;/code&gt; ERB interpolation tag at the end of the &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt;. In my CI test setup, each of the RSpec processes (some of which might execute simultaneously) is provided with a different &lt;code class=&quot;highlighter-rouge&quot;&gt;DB_SUFFIX&lt;/code&gt; environment variable (such as &lt;code class=&quot;highlighter-rouge&quot;&gt;_unit&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;_api&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;_feature&lt;/code&gt;, etc.). I’m leveraging that environment variable to ensure that the &lt;code class=&quot;highlighter-rouge&quot;&gt;channel_prefix&lt;/code&gt; is distinct for each test process, meaning that their ActionCable broadcasts will remain isolated from each other, even when executing in parallel.&lt;/p&gt;

&lt;p&gt;Here’s some relevant Rails documentation about this feature: &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html#redis-adapter&quot;&gt;Redis Adapter&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;note-using-distinct-redis-database-numbers-will-not-work&quot;&gt;Note: Using distinct Redis database numbers will &lt;em&gt;not&lt;/em&gt; work&lt;/h2&gt;

&lt;p&gt;I initially tried to fix this problem in a different (but conceptually similar) way, by using a different Redis database number for each RSpec process. However, that didn’t work, because Redis’s publish/subscribe functionality (the basis for the ActionCable functionality) is global to all of the databases of a given Redis instance.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://davidrunger.com/blog/images/davidrunger.jpg" /><media:content medium="image" url="https://davidrunger.com/blog/images/davidrunger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Enforce Git hooks in a Rails initializer</title><link href="https://davidrunger.com/blog/enforce-git-hooks-in-a-rails-initializer" rel="alternate" type="text/html" title="Enforce Git hooks in a Rails initializer" /><published>2024-07-02T00:00:00-05:00</published><updated>2024-07-02T00:00:00-05:00</updated><id>repo://posts.collection/_posts/2024-07-02-enforce-git-hooks-in-a-rails-initializer.md</id><content type="html" xml:base="https://davidrunger.com/blog/enforce-git-hooks-in-a-rails-initializer">&lt;h2 id=&quot;git-hooks&quot;&gt;Git hooks&lt;/h2&gt;

&lt;p&gt;Git has a cool feature called &lt;a href=&quot;https://githooks.com/&quot;&gt;Git hooks&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Git Hooks are scripts that Git can execute automatically when certain events occur, such as before or after a commit, push, or merge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, I have a &lt;a href=&quot;https://github.com/davidrunger/david_runger/blob/295ee53c793fc9975ca8fe2d8b3b5523ecd6cafc/bin/githooks/pre-push&quot;&gt;pre-push Git hook&lt;/a&gt; script that I want to be run before I push any code up to GitHub for &lt;a href=&quot;https://davidrunger.com/&quot;&gt;my website&lt;/a&gt;’s repository. This script performs various checks, such as using &lt;a href=&quot;https://github.com/gitleaks/gitleaks&quot;&gt;Gitleaks&lt;/a&gt; to scan the diff for any secrets (like an API access token) and to abort the push if any such secrets are found. The &lt;code class=&quot;highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; hook also runs various linters.&lt;/p&gt;

&lt;p&gt;As long as everyone who works on the repository configures their local Git setup to use the repository’s Git hooks, then Git will run that &lt;code class=&quot;highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; check and verify that it passes before Git will actually push any code to GitHub.&lt;/p&gt;

&lt;h2 id=&quot;but-how-to-enforce-that-git-hooks-are-used&quot;&gt;But, how to enforce that Git hooks are used?&lt;/h2&gt;

&lt;p&gt;The problem is that, although we can put Git hook scripts in a repo, we cannot enforce that developers actually use them. The nature of git hooks is that one must “opt in” to use them in the configuration of each local copy of a repository, and it’s easy to fail to do so. For example, I recently set up a Linux boot on my MacBook. I had previously set up the git hooks for the version of my repository in my Mac operating system, but I didn’t think to also set up the git hooks after I then cloned the repository to my fresh Linux OS.&lt;/p&gt;

&lt;p&gt;Frequently, a repository’s README.md or setup documentation might say something like, “We encourage you to set up this repository’s Git hooks, by executing [a certain command],” but developers might ignore or accidentally overlook that instruction, or at some later point they might lose their git configuration and not think to reconfigure the git hooks.&lt;/p&gt;

&lt;h2 id=&quot;idea-enforce-git-hooks-configuration-in-a-rails-initializer&quot;&gt;Idea: enforce Git hooks configuration in a Rails initializer&lt;/h2&gt;

&lt;p&gt;One solution to this conundrum recently occurred to me: put a check in a Rails initializer to verify that the project’s git hooks have indeed been configured. If not, then refuse to boot the app.&lt;/p&gt;

&lt;p&gt;I should note that, unfortunately, this is not a &lt;em&gt;100%&lt;/em&gt; foolproof way to ensure that developers configure their git setup to use a project’s git hooks. A developer could theoretically modify the project’s source code without ever successfully booting up the app. But, realistically speaking, I think that’s quite unlikely, at least for a developer who is going to do any substantial amount of work on an application. They’re going to need to boot up the app sooner or later.&lt;/p&gt;

&lt;p&gt;The solution presented below is nominally Rails-specific (in that it leverages the fact that Rails will automatically load during the boot process any &lt;code class=&quot;highlighter-rouge&quot;&gt;.rb&lt;/code&gt; file in the &lt;code class=&quot;highlighter-rouge&quot;&gt;config/initializers/&lt;/code&gt; directory), but it should also be very possible to translate this general approach to other web frameworks (or other types of applications), by hooking into the local development boot process in a similar way.&lt;/p&gt;

&lt;h2 id=&quot;the-initializer&quot;&gt;The initializer&lt;/h2&gt;

&lt;p&gt;Here’s what my relatively simple initializer looks like (with some minor tweaks):&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# File: config/initializers/githooks_check.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;local?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;SKIP_GITHOOKS_CHECK&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blank?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;CI&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blank?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;sb&quot;&gt;`git config core.hooksPath`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bin/githooks&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vg&quot;&gt;$stderr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    You have not configured the git hooks for this repo! To do so, run:
        git config core.hooksPath bin/githooks
    Or, if you must, you can put SKIP_GITHOOKS_CHECK=1 in your .env file.
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;  ERROR&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(You can see the actual source code &lt;a href=&quot;https://github.com/davidrunger/david_runger/blob/295ee53c793fc9975ca8fe2d8b3b5523ecd6cafc/config/initializers/githooks_check.rb&quot;&gt;here&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;So, if one tries to boot up the Rails app (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;rails server&lt;/code&gt;) without having locally set up the repository’s Git hooks, then an error message like this will be printed to stderr, and the app will abort the boot process:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ bin/rails server
=&amp;gt; Booting Puma
=&amp;gt; Rails 7.1.3.4 application starting in development
=&amp;gt; Run `bin/rails server --help` for more startup options
You have not configured the git hooks for this repo! To do so, run:
    git config core.hooksPath bin/githooks
Or, if you must, you can put SKIP_GITHOOKS_CHECK=1 in your .env file.
Exiting
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;breaking-it-down&quot;&gt;Breaking it down&lt;/h2&gt;

&lt;p&gt;Here’s an explanation of each part of the initializer:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.env.local?&lt;/code&gt; checks that we are booting in either the &lt;code class=&quot;highlighter-rouge&quot;&gt;development&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; environment (i.e. not &lt;code class=&quot;highlighter-rouge&quot;&gt;production&lt;/code&gt;, where Git hooks won’t be configured).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ENV[&apos;SKIP_GITHOOKS_CHECK&apos;].blank?&lt;/code&gt; gives developers a way to skip this check, if that is necessary for some reason, by setting this environment variable.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;ENV[&apos;CI&apos;].blank?&lt;/code&gt; checks that the app is not booting in our Continuous Integration (CI) environment (GitHub Actions), because, like &lt;code class=&quot;highlighter-rouge&quot;&gt;production&lt;/code&gt;, our Git hooks aren’t configured there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those conditions are all true, then we perform the key part of the check:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;`git config core.hooksPath`.strip != &apos;bin/githooks&apos;&lt;/code&gt;: This checks if the local git repository has &lt;em&gt;not&lt;/em&gt; been configured to use the project’s &lt;code class=&quot;highlighter-rouge&quot;&gt;bin/githooks/&lt;/code&gt; directory as the Git hooks path, and, if it hasn’t, then we enter the body of the &lt;code class=&quot;highlighter-rouge&quot;&gt;if&lt;/code&gt; condition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Within the &lt;code class=&quot;highlighter-rouge&quot;&gt;if&lt;/code&gt; block, we print a warning stating that the developer doesn’t have the repository’s Git hooks configured, and providing a command to configure the git hooks. Finally, we abort the boot process, via &lt;code class=&quot;highlighter-rouge&quot;&gt;exit(1)&lt;/code&gt;. The developer won’t be able to boot the app successfully until they make some sort of change (such as, ideally, configuring the git hooks on their machine, using the provided command).&lt;/p&gt;

&lt;h2 id=&quot;configuring-the-git-hooks&quot;&gt;Configuring the Git hooks&lt;/h2&gt;

&lt;p&gt;Remedying that situation is as simple as executing the provided command at the top level of the project directory:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ git config core.hooksPath bin/githooks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That tells Git to look in the repository’s &lt;a href=&quot;https://github.com/davidrunger/david_runger/tree/main/bin/githooks&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;bin/githooks/&lt;/code&gt;&lt;/a&gt; directory for relevant scripts before and after performing key actions. For example, Git will look for (and run) a script called &lt;code class=&quot;highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; before pushing, or a &lt;code class=&quot;highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; script before committing.&lt;/p&gt;

&lt;p&gt;Having configured the Git hooks, the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails server&lt;/code&gt; will now boot successfully!:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ bin/rails server
=&amp;gt; Booting Puma
=&amp;gt; Rails 7.1.3.4 application starting in development
   [ ... ]
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And, when the developer goes to push their code changes up to GitHub, then all of the repository’s intended pre-push checks will execute, guarding against secrets being accidentally committed to the source code, and helping to keep code quality high by running several of the project’s linting tools.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://davidrunger.com/blog/images/davidrunger.jpg" /><media:content medium="image" url="https://davidrunger.com/blog/images/davidrunger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using VS Code as a Rails app:update merge tool</title><link href="https://davidrunger.com/blog/using-vs-code-as-a-rails-app-update-merge-tool" rel="alternate" type="text/html" title="Using VS Code as a Rails app:update merge tool" /><published>2024-06-26T00:00:00-05:00</published><updated>2024-06-26T00:00:00-05:00</updated><id>repo://posts.collection/_posts/2024-06-26-using-vs-code-as-a-rails-app:update-merge-tool.md</id><content type="html" xml:base="https://davidrunger.com/blog/using-vs-code-as-a-rails-app-update-merge-tool">&lt;h2 id=&quot;background-the-rails-appupdate-command&quot;&gt;Background: the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; command&lt;/h2&gt;

&lt;p&gt;As part of bumping the major or minor version of a Rails app, one should execute the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; command (&lt;a href=&quot;https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#the-update-task&quot;&gt;docs&lt;/a&gt;). This command is intended to help maintainers of existing Rails applications to update their application configuration files as appropriate to accommodate changes that have been made to Rails’s configuration files and changes of the configuration defaults in the newer version of Rails.&lt;/p&gt;

&lt;p&gt;This command will compare, one by one in an interactive console session, the content of the configuration files (such as &lt;code class=&quot;highlighter-rouge&quot;&gt;config/application.rb&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;config/environments/production.rb&lt;/code&gt;, etc.) that &lt;em&gt;would&lt;/em&gt; be generated for a brand new Rails app versus the current actual content of those same files in the existing application that is being upgraded.&lt;/p&gt;

&lt;p&gt;The developer performing this upgrade will want, in many cases, to keep some parts of their existing application configuration, including some departures from the Rails default configuration that was provided by the initial &lt;code class=&quot;highlighter-rouge&quot;&gt;rails new&lt;/code&gt; command that generated the app. In other words, most application configurations will contain some adjustments versus the out-of-the-box Rails defaults, and many of the reasons for those adjustments will still be relevant and appropriate, even after upgrading to the newer version of Rails.&lt;/p&gt;

&lt;p&gt;However, for some aspects of a Rails application’s configuration, a developer might want to make some additions, changes, or deletions, to reflect newly available configuration settings that have been added in the new version of Rails, a change that has been made to the default value of a certain configuration, or some other change in the framework.&lt;/p&gt;

&lt;h2 id=&quot;using-a-merge-tool-will-make-this-task-easier&quot;&gt;Using a merge tool will make this task easier&lt;/h2&gt;

&lt;p&gt;Comparing and understanding the differences between the new default configuration files that Rails would generate for a brand new application versus the current state of the configuration files for an existing application (generated by an earlier version of Rails) is not an easy task, nor is resolving those differences to create a final version of the configuration files that will be used by the app going forward, synthesizing both the old and the new, but using a merge tool as part of this process can be a big help.&lt;/p&gt;

&lt;h2 id=&quot;running-rails-appupdate&quot;&gt;Running &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;When you execute the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; command, then for each configuration file where there is a difference between what Rails would generate for a new application versus the app’s existing configuration, the tool will note that there is a &lt;strong&gt;conflict&lt;/strong&gt;, and it will wait for input about what to do:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;❯ bin/rails app:update
    conflict  config/boot.rb
Overwrite /home/david/code/david_runger/config/boot.rb? (enter &quot;h&quot; for help) [Ynaqdhm]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Entering &lt;code class=&quot;highlighter-rouge&quot;&gt;h&lt;/code&gt; for help tells us what the options (&lt;code class=&quot;highlighter-rouge&quot;&gt;[Ynaqdhm]&lt;/code&gt;) stand for:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Y - yes, overwrite
n - no, do not overwrite
a - all, overwrite this and all others
q - quit, abort
h - help, show this help
d - diff, show the differences between the old and the new
m - merge, run merge tool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Either totally overwriting the existing configuration file with the default contents for a brand new Rails app (&lt;code class=&quot;highlighter-rouge&quot;&gt;Y&lt;/code&gt;) or simply ignoring the new configuration changes and just keeping the existing config (&lt;code class=&quot;highlighter-rouge&quot;&gt;n&lt;/code&gt;) would be easy options, but doing that risks, respectively, losing some existing configuration that we want to preserve or missing out on relevant new changes in the Rails framework.&lt;/p&gt;

&lt;p&gt;Although it’s more work than those simple options, in my opinion, the best option by far is the last one: &lt;code class=&quot;highlighter-rouge&quot;&gt;m - merge, run merge tool&lt;/code&gt;. However, if we choose this, we’ll get an error message (and then will be re-prompted to choose again):&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Overwrite /home/david/code/david_runger/config/boot.rb? (enter &quot;h&quot; for help) [Ynaqdhm] m
Please specify merge tool to `THOR_MERGE` env.
Overwrite /home/david/code/david_runger/config/boot.rb? (enter &quot;h&quot; for help) [Ynaqdhm]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So, let’s exit the &lt;code class=&quot;highlighter-rouge&quot;&gt;app:update&lt;/code&gt; interactive session by hitting Ctrl-C, and, as the error message suggests, we’ll provide a &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt; environment variable, which will tell the &lt;code class=&quot;highlighter-rouge&quot;&gt;app:update&lt;/code&gt; command which program to use as a merge tool to reconcile the diff between our app’s current configuration file and the version of the configuration file that would be generated for a fresh app by the new version of Rails to which we are upgrading.&lt;/p&gt;

&lt;p&gt;Since I already use VS Code as my primary code editor (including using its merge editor to resolve git conflicts), I already have it installed and running, and I’m familiar with how it works. So, I like to use VS Code as the merge tool for &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt;. However, it’s not immediately obvious how to do this. The next section explains how, by using a relatively simple bash script as a sort of “adapter”.&lt;/p&gt;

&lt;h2 id=&quot;how-to-use-vs-code-as-an-appupdate-merge-tool&quot;&gt;How to use VS Code as an &lt;code class=&quot;highlighter-rouge&quot;&gt;app:update&lt;/code&gt; merge tool&lt;/h2&gt;

&lt;h3 id=&quot;background-code---merge&quot;&gt;Background: &lt;code class=&quot;highlighter-rouge&quot;&gt;code --merge&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;code --help&lt;/code&gt; output includes this:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;❯ &lt;code class=&quot;highlighter-rouge&quot;&gt;code --help&lt;/code&gt;&lt;/p&gt;

  &lt;p&gt;[…]&lt;/p&gt;

  &lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;-m --merge &amp;lt;path1&amp;gt; &amp;lt;path2&amp;gt; &amp;lt;base&amp;gt; &amp;lt;result&amp;gt;&lt;/code&gt; Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;background-rails-appupdate-and-thor_merge&quot;&gt;Background: &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;For each configuration file that needs to be updated, the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; command will provide two file paths to the &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt; tool:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The first argument will be the path of a temporary file that will contain the version of the configuration file as Rails would generate that file for a brand new application.&lt;/li&gt;
  &lt;li&gt;The second argument will be the path of that configuration file in our existing app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h3&gt;

&lt;p&gt;So, the &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt; program that we use will receive from the &lt;code class=&quot;highlighter-rouge&quot;&gt;app:update&lt;/code&gt; tool only two files, but, as seen in the &lt;code class=&quot;highlighter-rouge&quot;&gt;code --help&lt;/code&gt; output above, VS Code’s &lt;code class=&quot;highlighter-rouge&quot;&gt;--merge&lt;/code&gt; functionality needs to be provided with four files, which it calls &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;path1&amp;gt;&lt;/code&gt;,  &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;path2&amp;gt;&lt;/code&gt;,  &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;base&amp;gt;&lt;/code&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;result&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trick is that we will create copies of one of the files provided by &lt;code class=&quot;highlighter-rouge&quot;&gt;app:update&lt;/code&gt;, and we’ll use these copies as the additional file arguments required by &lt;code class=&quot;highlighter-rouge&quot;&gt;code --merge&lt;/code&gt;. To accomplish this, we can write a bash script, which we’ll provide as the &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt; tool. You can put this bash script anywhere. You might or might not want to commit the script to your repository. I’ll create the file at &lt;code class=&quot;highlighter-rouge&quot;&gt;bin/app_update_thor_merge_tool&lt;/code&gt; in my Rails app, and we also need to make it executable:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;bin/app_update_thor_merge_tool
&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x bin/app_update_thor_merge_tool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, paste this into the &lt;code class=&quot;highlighter-rouge&quot;&gt;bin/app_update_thor_merge_tool&lt;/code&gt; file (or wherever else you are putting the script):&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;RAILS_DEFAULT_CONFIG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;APP_CURRENT_CONFIG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /tmp/rails-app-update
&lt;span class=&quot;nv&quot;&gt;PATH2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mktemp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/tmp/rails-app-update/current_XXXX.rb&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;BASE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mktemp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/tmp/rails-app-update/base_XXXX.rb&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$APP_CURRENT_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PATH2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$APP_CURRENT_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BASE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

code &lt;span class=&quot;nt&quot;&gt;--merge&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$RAILS_DEFAULT_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PATH2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BASE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$APP_CURRENT_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--wait&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$RAILS_DEFAULT_CONFIG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, run &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; again, this time providing the above script as the &lt;code class=&quot;highlighter-rouge&quot;&gt;THOR_MERGE&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It’s important to provide the THOR_MERGE tool as an absolute path.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;THOR_MERGE=$(pwd)/bin/app_update_thor_merge_tool bin/rails app:update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, if we press &lt;code class=&quot;highlighter-rouge&quot;&gt;m&lt;/code&gt; (for &lt;code class=&quot;highlighter-rouge&quot;&gt;merge, run merge tool&lt;/code&gt;) when prompted with a config file that needs attention, VS Code will open its three-way merge view:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://david-runger-public-uploads.s3.amazonaws.com/app-upate-vs-code-three-way-merge.png&quot; alt=&quot;app:update VS Code three-way merge&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;left pane&lt;/strong&gt; shows the content of the config file as Rails would generate it for a brand new application.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;right pane&lt;/strong&gt; shows the content of the config file as it currently exists in our repository.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;center pane&lt;/strong&gt; is what will become the final result, which it is your job to craft, using the contents of the left and right panes of the merge view as a starting point and guide.&lt;/p&gt;

&lt;p&gt;Read through any relevant Rails documentation about the changes and do whatever else you need to do to figure out how to resolve the discrepancies between your existing configuration and the configuration that Rails would generate for a new app. Then, once you have the middle pane into the desired final state, save the file, and close the tab in VS Code. In your terminal, the &lt;code class=&quot;highlighter-rouge&quot;&gt;rails app:update&lt;/code&gt; command will then move on to the next file, for which you can again hit &lt;code class=&quot;highlighter-rouge&quot;&gt;m&lt;/code&gt; to open the file in the VS Code three-way merge view. Repeat until you have worked through all of the configuration files that need attention.&lt;/p&gt;

&lt;p&gt;Congratulations! You have now completed an important step in upgrading your Rails application.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://david-runger-public-uploads.s3.amazonaws.com/app-upate-vs-code-three-way-merge.png" /><media:content medium="image" url="https://david-runger-public-uploads.s3.amazonaws.com/app-upate-vs-code-three-way-merge.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Quick Tip: Use gem.wtf to go to the GitHub repo of a gem</title><link href="https://davidrunger.com/blog/quick-tip-use-gem.wtf-to-go-to-the-github-repo-of-a-gem" rel="alternate" type="text/html" title="Quick Tip: Use gem.wtf to go to the GitHub repo of a gem" /><published>2023-02-13T00:00:00-06:00</published><updated>2023-02-13T00:00:00-06:00</updated><id>repo://posts.collection/_posts/2023-02-13-quick-tip:-use-gem.wtf-to-go-to-the-github-repo-of-a-gem.md</id><content type="html" xml:base="https://davidrunger.com/blog/quick-tip-use-gem.wtf-to-go-to-the-github-repo-of-a-gem">&lt;p&gt;&lt;a href=&quot;https://gem.wtf/&quot;&gt;gem.wtf&lt;/a&gt; is a handy little website/tool that I use probably at least once a week.
Its purpose is quite simple: enter &lt;code class=&quot;highlighter-rouge&quot;&gt;gem.wtf/some_ruby_gem&lt;/code&gt; into your browser’s URL bar, and it will
redirect you to that gem’s source code repository (i.e. usually their GitHub repo).&lt;/p&gt;

&lt;p&gt;Here are some examples:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://gem.wtf/rails&quot;&gt;gem.wtf/rails&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gem.wtf/sidekiq&quot;&gt;gem.wtf/sidekiq&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gem.wtf/simple_cov-formatter-terminal&quot;&gt;gem.wtf/simple_cov-formatter-terminal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a really quick and convenient way to get from the name of a gem to its GitHub repo, which
can be handy…&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;to quickly see how many GitHub stars a gem has that you’re thinking about using&lt;/li&gt;
  &lt;li&gt;to view a gem’s README documentation&lt;/li&gt;
  &lt;li&gt;to search/browse through the GitHub Issues/PRs/Discussions, if you’re running into a bug/problem&lt;/li&gt;
  &lt;li&gt;to clone the git repository, so that you can dig through the code history and get some additional
context about how and why the library works the way it does&lt;/li&gt;
  &lt;li&gt;and more!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, you can also find the gem’s GitHub repo through Google or
&lt;a href=&quot;https://rubygems.org/&quot;&gt;RubyGems&lt;/a&gt;, but this saves you a click and takes you straight there.&lt;/p&gt;

&lt;p&gt;Thanks to the creator of gem.wtf, &lt;a href=&quot;https://github.com/zeke&quot;&gt;Zeke Sikelianos&lt;/a&gt;. You can check out
gem.wtf’s source code &lt;a href=&quot;https://github.com/zeke/gem.wtf&quot;&gt;here&lt;/a&gt; (it’s actually a simple Node &lt;code class=&quot;highlighter-rouge&quot;&gt;express&lt;/code&gt;
app).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus tip:&lt;/strong&gt; There’s a similar tool to get to the GitHub repo for a JavaScript npm package:
&lt;a href=&quot;https://ghub.io/&quot;&gt;ghub.io&lt;/a&gt;. For example, &lt;a href=&quot;https://ghub.io/lodash&quot;&gt;ghub.io/lodash&lt;/a&gt; takes you to the
GitHub repo for the &lt;code class=&quot;highlighter-rouge&quot;&gt;lodash&lt;/code&gt; npm package.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://davidrunger.com/blog/images/davidrunger.jpg" /><media:content medium="image" url="https://davidrunger.com/blog/images/davidrunger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mocking external requests in Rails feature tests</title><link href="https://davidrunger.com/blog/mocking-external-requests-in-rails-feature-tests" rel="alternate" type="text/html" title="Mocking external requests in Rails feature tests" /><published>2023-01-30T00:00:00-06:00</published><updated>2023-01-30T00:00:00-06:00</updated><id>repo://posts.collection/_posts/2023-01-30-mocking-external-requests-in-rails-feature-tests.md</id><content type="html" xml:base="https://davidrunger.com/blog/mocking-external-requests-in-rails-feature-tests">&lt;p&gt;Without any introduction, allow me to share some code that I want to talk about, which exists within
a Rails feature test:&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;around&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;intercept&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;https://accounts.google.com/o/oauth2/auth?&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;This is Google OAuth.&apos;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The purpose of that code is to mock Google’s response to a request made by a Chrome browser that’s
being driven through an OAuth authorization flow by Capybara in an automated Rails test suite. I
want to share it here because, when I was searching how to mock a browser request made within a
feature test, my Googling failed to bring up many relevant or helpful code examples.&lt;/p&gt;

&lt;p&gt;If that’s all that you’re looking for, then maybe copy-paste that code and stop reading here. 🙂
&lt;strong&gt;One additional note, though&lt;/strong&gt;: for this to work, you’ll need to add the &lt;code class=&quot;highlighter-rouge&quot;&gt;selenium-devtools&lt;/code&gt; gem to
the &lt;code class=&quot;highlighter-rouge&quot;&gt;test&lt;/code&gt; group of your Gemfile, if it’s not there already.&lt;/p&gt;

&lt;p&gt;If you’re interested in some additional background context, though, read on.&lt;/p&gt;

&lt;h2 id=&quot;why-though-some-background&quot;&gt;Why, though? Some background.&lt;/h2&gt;

&lt;p&gt;The code above comes from a spec seen
&lt;a href=&quot;https://github.com/davidrunger/david_runger/blob/25ac799/spec/features/user_google_login_spec.rb#L55-L62&quot;&gt;here&lt;/a&gt;.
That spec verifies the OAuth flow for logging in to &lt;a href=&quot;https://davidrunger.com/&quot;&gt;my
website&lt;/a&gt; using Google OAuth. That test was added in
&lt;a href=&quot;https://github.com/davidrunger/david_runger/commit/6aab7bf&quot;&gt;this commit&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;content-security-policy-csp&quot;&gt;Content Security Policy (CSP)&lt;/h3&gt;

&lt;p&gt;I had previously introduced a bug when modifying my app’s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP&quot;&gt;Content Security Policy (CSP)&lt;/a&gt;. Setting up an effective CSP can be a little annoying, and there’s a risk of introducing bugs when doing it (as happened to me), but the upside is that a well-crafted CSP can provide a significant amount of protection against various potential security vulnerabilities in a web app.&lt;/p&gt;

&lt;h3 id=&quot;oauth&quot;&gt;OAuth&lt;/h3&gt;

&lt;p&gt;When initiating an OAuth authorization flow (e.g. by clicking a “Sign in with Google” button), a
user’s browser will make a request to my app’s server, and my app’s server will respond with a
redirect to a Google URL. However, in some browsers (as discussed
&lt;a href=&quot;https://github.com/w3c/webappsec-csp/issues/8&quot;&gt;here&lt;/a&gt;), if the application has a &lt;code class=&quot;highlighter-rouge&quot;&gt;form-action&lt;/code&gt; CSP
directive that doesn’t specifically authorize a redirect to the Google domain in question, the
browser will refuse to follow the redirect, thus halting/breaking the OAuth flow and preventing the
user from logging in.&lt;/p&gt;

&lt;p&gt;I had inadvertently broken my app in that way when setting up a CSP for my application. After
realizing that this bug was happening and figuring out how to fix it, I also wanted to write a test
to reproduce the bug and ensure that particular bug would never be reintroduced.&lt;/p&gt;

&lt;h2 id=&quot;tests-should-be-able-to-run-and-pass-without-an-internet-connection&quot;&gt;Tests should be able to run (and pass) without an Internet connection&lt;/h2&gt;

&lt;p&gt;A relatively straightforward way to write such a regression test for the aforementioned bug would be
to write a feature test that visits my app’s login page (hosted by a test server), clicks the “Sign
in with Google” button, and then checks that the browser follows the redirect to a Google login
page, and that the page has some content like “Google will share your name, email address, language
preference, and profile picture with davidrunger.com”.&lt;/p&gt;

&lt;p&gt;However, that approach relies on the test browser making a real, live external request over the
Internet to a Google web server in order for the test to pass. This would violate one of my guiding
principles of testing: tests should be able to run (and pass) without an Internet connection. There
are a few reasons for this, the biggest of which are probably test speed and test reliability.&lt;/p&gt;

&lt;h2 id=&quot;webmock-for-the-test-browser&quot;&gt;WebMock for the test browser&lt;/h2&gt;

&lt;p&gt;When writing unit tests in Ruby, external HTTP requests that are made by the Ruby application can be
mocked using the most excellent &lt;a href=&quot;https://github.com/bblimke/webmock/tree/v3.18.1&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;webmock&lt;/code&gt;&lt;/a&gt; gem.
WebMock can mock requests that are made from the Ruby app in question to an external server.
However, it doesn’t allow us to mock a request from a Chrome browser that’s being driven in a
feature test to an external server (in this example, a Google server for OAuth).&lt;/p&gt;

&lt;p&gt;What we basically need is the equivalent of WebMock for a feature test. And that’s just what the
code at the top of this article does.&lt;/p&gt;

&lt;p&gt;Below is that code within the larger context of the regression test in question. I’ll make some
additional comments about this test beneath the code.&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;RSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Logging in as a User via Google auth&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;when OmniAuth test mode is disabled&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;around&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;original_omni_auth_test_mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;OmniAuth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;test_mode&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;OmniAuth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;test_mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;

      &lt;span class=&quot;no&quot;&gt;OmniAuth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;test_mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;original_omni_auth_test_mode&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;when Google responds with &quot;This is Google OAuth.&quot;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:mocked_google_response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;This is Google OAuth.&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;around&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;intercept&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_with?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;s1&quot;&gt;&apos;https://accounts.google.com/o/oauth2/auth?&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mocked_google_response&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# try to do some cleanup (though I&apos;m not sure how useful this is)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;devtools&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;callbacks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clear&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;devtools&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;disable&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;renders Google&apos;s response&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_css&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;button.google-login&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;click_button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;google-login&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# The point of all of this: verify that the browser&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# indeed followed the redirect to Google.&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mocked_google_response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;First, within an RSpec &lt;code class=&quot;highlighter-rouge&quot;&gt;around&lt;/code&gt; block, we disable &lt;code class=&quot;highlighter-rouge&quot;&gt;OmniAuth&lt;/code&gt; test mode in this test. This is
necessary because, when the &lt;code class=&quot;highlighter-rouge&quot;&gt;OmniAuth&lt;/code&gt; test mode is enabled, &lt;code class=&quot;highlighter-rouge&quot;&gt;OmniAuth&lt;/code&gt; will not actually redirect
the browser to an external OAuth server. So, in order to test that the browser does follow a
redirect to an external Google OAuth server, we need to temporarily ensure that the &lt;code class=&quot;highlighter-rouge&quot;&gt;OmniAuth&lt;/code&gt; test
mode is disabled. We then restore &lt;code class=&quot;highlighter-rouge&quot;&gt;OmniAuth.config.test_mode&lt;/code&gt; to its original value (presumably
&lt;code class=&quot;highlighter-rouge&quot;&gt;true&lt;/code&gt;) after the test has completed.&lt;/p&gt;

&lt;p&gt;Next is the code that mocks the request to accounts.google.com, providing a specified response code
(200) and content (“This is Google OAuth.”). I wrote some code at the end of that &lt;code class=&quot;highlighter-rouge&quot;&gt;around&lt;/code&gt; block to
try to do some cleanup to restore the test browser etc to its original state (though I’m not sure if
it really helps anything).&lt;/p&gt;

&lt;p&gt;Finally, is the test example itself, which visits the login page, clicks on the Google login button,
and verifies, by checking for the &lt;code class=&quot;highlighter-rouge&quot;&gt;mocked_google_response&lt;/code&gt; content, that the browser was indeed
willing (even given my application’s Content Security Policy) to follow a redirect to the external
Google URL.&lt;/p&gt;</content><author><name>David Runger</name></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://davidrunger.com/blog/images/davidrunger.jpg" /><media:content medium="image" url="https://davidrunger.com/blog/images/davidrunger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>