<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://breckenedge.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://breckenedge.github.io/" rel="alternate" type="text/html" /><updated>2026-03-11T19:16:41+00:00</updated><id>https://breckenedge.github.io/feed.xml</id><title type="html">Aaron Breckenridge’s Blog</title><subtitle></subtitle><author><name>Aaron Breckenridge</name></author><entry><title type="html">My Rails workflow: March 2026 update</title><link href="https://breckenedge.github.io/dev/2026/03/11/rails-workflow-update.html" rel="alternate" type="text/html" title="My Rails workflow: March 2026 update" /><published>2026-03-11T19:00:00+00:00</published><updated>2026-03-11T19:00:00+00:00</updated><id>https://breckenedge.github.io/dev/2026/03/11/rails-workflow-update</id><content type="html" xml:base="https://breckenedge.github.io/dev/2026/03/11/rails-workflow-update.html"><![CDATA[<p>Six weeks ago I wrote about <a href="/dev/2026/02/03/rails-workflow-claude-code-devcontainers-mcp.html">my Rails workflow</a> — devcontainers, Claude Code on the host, MCP servers, a Go binary for team onboarding. Most of that still holds. But enough has changed that it’s worth a follow-up.</p>

<h2 id="devcontainers-update">Devcontainers update</h2>

<p>I’m still using devcontainers and encouraging their adoption across the team. One thing I’ve added: checking in development decryption keys so the container can come up with zero interaction. Rails encrypted credentials need a key to decrypt, and if that key isn’t available, you’re prompted before anything works. Really I want anyone to be able to just click that “Launch Workspace” button and have a ready-to-use IDE and development environment.</p>

<p>Checking in keys sounds like a bad idea, and in general it is. The reason it’s fine here is that the development credentials file doesn’t contain anything real — no production secrets, no API keys that matter. If someone gets the dev key, they get lorem ipsum. The habit of never checking in keys is a good one, but it exists to protect real secrets. Blindly applying it to dev environment setup just creates friction for automation with no actual security benefit.</p>

<h2 id="worktree-evolution">Worktree evolution</h2>

<p>In the last post I said I wasn’t using git worktrees because of port conflicts — the Shakapacker dev server port gets baked into the build and hardcoded into the repo, so two containers can’t share it. That was accurate at the time.</p>

<p>What’s changed is that I now have a custom skill that handles port provisioning automatically. When it creates a new worktree, it picks an available port, updates the relevant config, and brings up the container on that port. The problem is mostly solved. I’m using worktrees. Except that worktrees still don’t work out-of-the box with devcontainers since the repo is a symlink, and Docker won’t import symlinks by default. The march towards perfect environment setup continues.</p>

<p>It’s worth calling out that I’m not using Claude Code’s built-in worktree support, though. It leaves orphaned worktrees around, and it gets the mechanics wrong often enough that I stopped trusting it for this. The custom skill is more reliable because it’s specific to this project’s port setup. I do see the built-in Claude Code worktree feature being much more useful when I have fully migrated my development environment to the cloud cause I just wont care how many worktrees it spins up when the environment is ephemeral. Not quite there yet with the team though.</p>

<h2 id="background-agents-the-claude-github-action">Background agents: the @claude GitHub action</h2>

<p>I’ve set up the <code class="language-plaintext highlighter-rouge">@claude</code> GitHub action on a few repos. When I open an issue, Claude Code picks it up automatically, runs headless, and opens a PR. I’m not watching.</p>

<p>The clearest example is <a href="https://github.com/breckenedge/claude-code-radar">claude-code-radar</a>, a Technology Radar I maintain for the Claude Code ecosystem — tools, plugins, patterns, and practices organized by adoption maturity (Adopt, Trial, Assess, Hold), modeled on the ThoughtWorks radar. Every week, I open an issue asking Claude to research recent releases and update the radar. It reads the changelog, checks community sources, and opens a PR with a sourced, well-reasoned update. I review, merge, done.</p>

<p>What I appreciate about this pattern is that it matches the task to the agent’s actual strengths. Tracking a fast-moving ecosystem means reading a lot of release notes and making incremental updates — tedious for a human, easy for Claude. The output is consistently good enough to merge with light review.</p>

<h2 id="git-town-and-stacked-prs">Git Town and stacked PRs</h2>

<p>We’ve adopted <a href="https://www.git-town.com/">Git Town</a> for stacking PRs. The motivation is keeping individual PRs reviewable — a 1500-line PR gets shallow reviews because nobody wants to read it. Splitting work into a stack of focused PRs means reviewers actually engage with the code.</p>

<p>The side effect I didn’t expect: I’ve learned things about git. Claude uses tools like <code class="language-plaintext highlighter-rouge">rerere</code> naturally — it just sets it up, uses it when rebasing across multiple branches, moves on. I’d never touched rerere. Watching it work made me understand what I’d been missing. I’m a decent git user, but Claude is better, and working alongside it has closed some gaps.</p>

<h2 id="team-plans-and-the-shift-away-from-mcp-servers">Team plans and the shift away from MCP servers</h2>

<p>In February I wrote about <code class="language-plaintext highlighter-rouge">bin/mcp-setup</code>, a Go script that configures MCP servers across different agents for everyone on the team. We’ve moved to a Claude Code team plan, and that’s changed the picture.</p>

<p>On the team plan, managers can approve and centrally install plugins. Atlassian and Figma — which I was previously wiring up manually as MCP servers — are now just there. The person who owns the tool access grants it, and it shows up in everyone’s environment. That’s the right model: tool access is an organizational concern, not a developer concern.</p>

<p><code class="language-plaintext highlighter-rouge">bin/mcp-setup</code> is still around for the MCP servers that don’t have a team-managed equivalent, but it covers less ground now.</p>

<h2 id="subagents">Subagents</h2>

<p>Using Claude Code and Git Town together has a side effect: a lot more PRs. More PRs means more CI runs, and CI queues that were fine before started backing up.</p>

<p>To address it, I used a two-phase approach. First, a main agent researched the CI suite and identified where improvements were possible. Then I told Claude to use the worktree skill to create five worktrees and launch five <a href="https://code.claude.com/docs/en/sub-agents.md">subagents</a> to implement the changes in parallel, one per worktree, each opening its own PR.</p>

<p>The constraint is that this only works when the changes are genuinely independent. If two agents are touching the same files, you’ve just created merge conflicts at scale. The main agent’s research phase is what makes the parallel phase viable — it tells you which improvements don’t step on each other.</p>

<h2 id="insights">/insights</h2>

<p>One more thing worth mentioning: <a href="https://code.claude.com/docs/en/interactive-mode.md"><code class="language-plaintext highlighter-rouge">/insights</code></a>. Run it in a Claude Code session and it analyzes your history and generates a report — interaction patterns, friction points, what’s working, and ready-to-paste suggestions for your <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>. It’s a useful way to surface things you’re doing repeatedly that could be codified as instructions or skills. I’ve run it a few times and each time caught something worth adding to project config.</p>

<h2 id="where-it-stands">Where it stands</h2>

<p>The trajectory since February has been toward less babysitting. Background agents handling recurring research, worktrees running in parallel, plugins provisioned by admins rather than configured by hand. The interactive workflow is still there for design and complex problems, but a growing share of the work is just happening in the background.</p>

<p>Claude Code recently shipped <a href="https://code.claude.com/docs/en/scheduled-tasks"><code class="language-plaintext highlighter-rouge">/loop</code></a>, a native scheduled task command. <code class="language-plaintext highlighter-rouge">/loop 1d /my-skill</code> and it runs on a cron, no external infrastructure needed. That’s essentially what <a href="https://en.wikipedia.org/wiki/OpenClaw">OpenClaw</a> was doing — always-on background work on a timer — but built into the tool I’m already using.</p>

<p>I haven’t tried the <a href="https://github.com/snarktank/ralph">Ralph Loop</a> yet — running an agent repeatedly against a PRD until it’s done, with git as the memory between fresh sessions. Still on the list.</p>]]></content><author><name>Aaron Breckenridge</name></author><category term="dev" /><summary type="html"><![CDATA[Six weeks ago I wrote about my Rails workflow — devcontainers, Claude Code on the host, MCP servers, a Go binary for team onboarding. Most of that still holds. But enough has changed that it’s worth a follow-up.]]></summary></entry><entry><title type="html">My Rails workflow: devcontainers, Claude Code, and MCP</title><link href="https://breckenedge.github.io/dev/2026/02/03/rails-workflow-claude-code-devcontainers-mcp.html" rel="alternate" type="text/html" title="My Rails workflow: devcontainers, Claude Code, and MCP" /><published>2026-02-03T19:00:00+00:00</published><updated>2026-02-03T19:00:00+00:00</updated><id>https://breckenedge.github.io/dev/2026/02/03/rails-workflow-claude-code-devcontainers-mcp</id><content type="html" xml:base="https://breckenedge.github.io/dev/2026/02/03/rails-workflow-claude-code-devcontainers-mcp.html"><![CDATA[<p>I’ve settled into a workflow over the last couple of months that I’m pretty happy with. Rails inside a devcontainer, Claude Code on the host talking into it, and a handful of MCP servers handling context that I used to go hunting for manually.</p>

<h2 id="devcontainers">Devcontainers</h2>

<p>This is the piece that moved the needle the most. New project, legacy project, doesn’t matter — clone, open in VS Code, and the devcontainer comes up with Ruby, Node, Postgres, Redis, whatever the project needs. Zero setup on the host. I used to sometimes spend hours getting a legacy Rails app running on a new machine. Now it’s seconds or, at most, minutes. If you’ve never used them, they’re worth the hour it takes to learn.</p>

<h2 id="im-not-using-git-worktrees">I’m not using git worktrees</h2>

<p>I tried. The problem is specific to Rails with Webpacker/Shakapacker (a thoroughly antiquated setup these days). The Shakapacker dev server runs on its own port, and that port gets baked into the client-side HTML at build time so the browser knows where to fetch assets from. Rails has to know about it at startup.</p>

<p>With worktrees, you’d want each branch in its own devcontainer, each on its own set of ports — you can’t have two containers fighting over 3000. But the port configuration lives in the repo, so it’s the same across every worktree unless you manually change it per-container before you start. And if you have to configure a container before you can use it, you’ve lost most of the reason to have worktrees in the first place.</p>

<p>Chicken and egg. I stick with one working tree per project and switch branches the normal way.</p>

<h2 id="claude-code-on-the-host">Claude Code on the host</h2>

<p>Claude Code runs on my host machine, not inside the devcontainer. It needs access to my MCP servers and the broader file system, not just what’s inside one container.</p>

<p>The trick is a <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> in the project root that tells it how to reach in and use the shell:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Commands need to run inside the devcontainer. Prefix commands with <span class="sb">`docker exec`</span>:

docker exec -it my-app bundle exec rails test
docker exec -it my-app bundle exec rails console
</code></pre></div></div>

<p>Claude reads the workspace files directly through the volume mount — no copying back and forth. Any command that needs the container environment goes through <code class="language-plaintext highlighter-rouge">docker exec</code>. It works well enough that I rarely have to think about the boundary.</p>

<p>The thing I’d like to change eventually is running Claude inside the devcontainer instead. If it’s already in the container, I can pass <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> and stop worrying about the <code class="language-plaintext highlighter-rouge">docker exec</code> indirection entirely. I haven’t gotten that working yet — MCP setup is the sticking point — but it’s on the list.</p>

<h2 id="mcp-servers">MCP servers</h2>

<p>At work I’ve got a handful running that feed Claude context it would otherwise have to go dig for:</p>

<ul>
  <li><a href="https://github.com/modelcontextprotocol/servers#github"><strong>GitHub</strong></a> — PRs, issues, code search. Claude also knows how to use the <code class="language-plaintext highlighter-rouge">gh</code> CLI directly, so I go back and forth.</li>
  <li><a href="https://github.com/atlassian/atlassian-mcp-server"><strong>Jira and Confluence</strong></a> — tickets, epics, internal docs</li>
  <li><a href="https://github.com/getsentry/sentry-mcp-stdio"><strong>Sentry</strong></a> — error tracking. I can point Claude at a specific error and it can look at the stack trace and the relevant code at the same time.</li>
  <li><a href="https://github.com/CircleCI-Public/mcp-server-circleci"><strong>CircleCI</strong></a> — build and pipeline status</li>
  <li><a href="https://developers.figma.com/docs/figma-mcp-server/"><strong>Figma</strong></a> — design specs. This one’s a bit odd: it requires the Figma desktop app to be running.</li>
</ul>

<p>The MCP servers are the part that changes the day-to-day the most. Before, I’d bounce between six tabs gathering context before I could even ask a useful question. Now I just ask.</p>

<h2 id="setting-up-mcp-for-the-whole-team">Setting up MCP for the whole team</h2>

<p>Not everyone on the team uses Claude Code. Some use Cursor, some use Codex, and each one stores its MCP configuration in a different place and in a different format. One of them uses TOML. Manually maintaining separate config files for each agent wasn’t going to work.</p>

<p>So there’s a <code class="language-plaintext highlighter-rouge">bin/mcp-setup</code> script in the main repo. You clone, you run it, it presents a menu of supported agents, and it writes the right config to the right place for whichever one you picked. The list of MCP servers is defined inside the script and mirrors what I wrote above.</p>

<p>The script is a compiled Go binary. I don’t know Go — Claude wrote it. The reason for Go specifically is portability: a single static binary with zero runtime dependencies. No Ruby, no Python, no Node. Anyone on the team can pull the repo and run it immediately regardless of what’s installed on their machine. Autodetecting which agents are installed would be nicer than a menu, but that would mean the script needs to probe a bunch of directories and config files on the user’s machine, and that felt like too much to ask for. I can see go being a big winner as devs write less and less code directly.</p>

<h2 id="skills">Skills</h2>

<p>Skills are another piece worth mentioning. They’re slash commands you can add to Claude Code — reusable prompts that expand when you invoke them. I’ve got one for <code class="language-plaintext highlighter-rouge">/gmail</code> that can access my inbox using the Gmail Python API, both for reading and writing mail.</p>

<p>The use case is inbox management. I get 60–100 emails a day, and having Claude summarize what’s unread or find a specific thread is genuinely useful.</p>

<p>Claude wrote the skill. I described what I wanted, it found the Gmail API docs on its own, and generated a Python script that handles OAuth, calls the API, and returns email content in a format it can work with. I don’t write Python often, so having Claude handle the credentials.json setup, the token refresh logic, and the API query syntax saved me a lot of fumbling through documentation. The whole thing lives in <code class="language-plaintext highlighter-rouge">~/.claude/skills/gmail/</code>.</p>

<p>Skills are simpler than MCP servers — just a prompt template and optionally a script. If you find yourself repeatedly giving Claude the same setup instructions, a skill is probably the right abstraction.</p>

<h2 id="in-practice">In practice</h2>

<p>Open a project, devcontainer comes up (or it’s already running), open Claude Code in a terminal on the host. Give it a Jira task number. It reads the <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>, knows how to run things in the container, pulls what it needs from Jira or Sentry or GitHub, and gets to work.</p>

<p>There’s an extra layer of indirection for every command thanks to the host/container split. But compared to where I was a couple of months ago — manually provisioning environments, context-switching between tools, spending half my time on plumbing — it’s a significant step forward.</p>]]></content><author><name>Aaron Breckenridge</name></author><category term="dev" /><summary type="html"><![CDATA[I’ve settled into a workflow over the last couple of months that I’m pretty happy with. Rails inside a devcontainer, Claude Code on the host talking into it, and a handful of MCP servers handling context that I used to go hunting for manually.]]></summary></entry><entry><title type="html">Interview question repository for prospective employers</title><link href="https://breckenedge.github.io/2025/05/23/engineering_interview_questions.html" rel="alternate" type="text/html" title="Interview question repository for prospective employers" /><published>2025-05-23T19:10:21+00:00</published><updated>2025-05-23T19:10:21+00:00</updated><id>https://breckenedge.github.io/2025/05/23/engineering_interview_questions</id><content type="html" xml:base="https://breckenedge.github.io/2025/05/23/engineering_interview_questions.html"><![CDATA[<p>My repository of questions I can use during interviews to gauge the viability of a collaboration.</p>

<h1 id="product">Product</h1>

<ul>
  <li>Is there a business analytics or data analytics team?</li>
  <li>How many customers does the product have?</li>
  <li>How large is the product’s total-addressable-market?</li>
  <li>What is the overall product architecture?</li>
  <li>How old is the product?</li>
  <li>How do stakeholders monitor the viability and health of the product?</li>
</ul>

<h1 id="lifecycle">Lifecycle</h1>

<ul>
  <li>How often does the team release their code or deploy to production?</li>
  <li>How are releases approved?</li>
  <li>How long does it take to make architectural changes? How many people are involved in the process?</li>
  <li>How does the application run in production? Binary? Docker? Natively? Kubernetes?</li>
  <li>Does the team use a particular cloud provider? What is the monthly cloud spend?</li>
  <li>How is infrastructure provisioned?</li>
  <li>Does the team use any CI/CD tools such as Jenkins, GitHub Actions or CircleCI?</li>
  <li>How are projects estimated and planned?</li>
  <li>How are individual tasks assigned?</li>
  <li>Do merges require approval? Who approves them?</li>
  <li>How long does it typically take for a PR to be reviewed and merged?</li>
</ul>

<h1 id="culture">Culture</h1>

<ul>
  <li>How geographically distributed are team members?</li>
  <li>How are process changes discussed and made?</li>
  <li>How frequent are stand-ups and how long do they last?</li>
  <li>Do team members pair on code?</li>
  <li>How are new dependencies evaluated and discussed?</li>
  <li>Does the team have a plan for career growth?</li>
  <li>How large is the team? What is the experience level of the most-junior developer? Most-senior?</li>
  <li>Does the team have a budget for learning or attending conferences?</li>
  <li>How much time is afforded for learning new technologies, techniques and remaining current with changes in the field?</li>
  <li>How often is development hardware renewed?</li>
  <li>What is the interaction like with Design and Product team members? Marketing and Sales?</li>
  <li>How often do developers interact with internal and external customers and stakeholders?</li>
  <li>Does the team have any taboo topics or practices?</li>
  <li>Does the business require tracking of time spent on particular tasks?</li>
  <li>Is this a new position or backfill for turnover?</li>
</ul>

<h1 id="quality">Quality</h1>

<ul>
  <li>How are code changes reviewed and approved?</li>
  <li>How are architectural proposals made and discussed?</li>
  <li>How fast are deprecated features disabled and removed?</li>
  <li>Does the team have a support escalation process?</li>
  <li>Does the team conduct on-call rotations?</li>
  <li>What was the last outage like? How was recovery?</li>
  <li>Does the team conduct A/B testing?</li>
  <li>How does the team handle slow and flaky tests?</li>
</ul>]]></content><author><name>Aaron Breckenridge</name></author><summary type="html"><![CDATA[My repository of questions I can use during interviews to gauge the viability of a collaboration.]]></summary></entry><entry><title type="html">Certbot SSL Renewal Script</title><link href="https://breckenedge.github.io/2021/08/13/2021-08-12-certbot-ssl-renewal-script.html" rel="alternate" type="text/html" title="Certbot SSL Renewal Script" /><published>2021-08-13T02:18:00+00:00</published><updated>2021-08-13T02:18:00+00:00</updated><id>https://breckenedge.github.io/2021/08/13/2021-08-12-certbot-ssl-renewal-script</id><content type="html" xml:base="https://breckenedge.github.io/2021/08/13/2021-08-12-certbot-ssl-renewal-script.html"><![CDATA[<p>I have a <a href="https://todo.breckenridge.dev">subdomain</a> for a while that’s secured for free via <a href="https://certbot.eff.org">certbot</a>, but I was still manually refreshing the certificate every 45 days because that subdomain also runs an Nginx proxy. I hadn’t automated this renewal because the Nginx proxy can’t be up during certificate renewal; renewal depends on being able to run on port 80 and 443. (Certbot does this in order to confirm domain ownership.)</p>

<p>I’d been mulling over a complicated setup using Docker and containers until I gave up and just wrote a shell script (ash in this case since the server is running Alpine):</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/ash</span>

<span class="nb">set</span> <span class="nt">-e</span>
service nginx stop
certbot renew <span class="nt">--standalone</span>
service nginx start
</code></pre></div></div>

<p>which I trigger once a month from <code class="language-plaintext highlighter-rouge">cron</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># min	hour	day	month	weekday	command
0	2	1	*	*	/root/renew.sh
</code></pre></div></div>

<p>Note that <code class="language-plaintext highlighter-rouge">certbot renew --standalone</code> requires running certbot manually at least once for every domain that the server hosts.</p>

<p>Is this portable and will it work for multiple servers? No. Do I care? Not at this time. Is this perfect? Nothing ever is.</p>

<p>Just one of those things that I can’t believe it took me this long to automate.</p>]]></content><author><name>Aaron Breckenridge</name></author><summary type="html"><![CDATA[I have a subdomain for a while that’s secured for free via certbot, but I was still manually refreshing the certificate every 45 days because that subdomain also runs an Nginx proxy. I hadn’t automated this renewal because the Nginx proxy can’t be up during certificate renewal; renewal depends on being able to run on port 80 and 443. (Certbot does this in order to confirm domain ownership.)]]></summary></entry><entry><title type="html">My Github Actions Docker-and-VPS-Based CI/CD Pipeline</title><link href="https://breckenedge.github.io/ops/2021/01/29/docker_github_cicd_ssh_pipeline.html" rel="alternate" type="text/html" title="My Github Actions Docker-and-VPS-Based CI/CD Pipeline" /><published>2021-01-29T22:06:21+00:00</published><updated>2021-01-29T22:06:21+00:00</updated><id>https://breckenedge.github.io/ops/2021/01/29/docker_github_cicd_ssh_pipeline</id><content type="html" xml:base="https://breckenedge.github.io/ops/2021/01/29/docker_github_cicd_ssh_pipeline.html"><![CDATA[<p>Last updated February 3, 2026.</p>

<p>Oddly, I still enjoy running my own Linux server. A few years ago, I became interested in deploying a container-based application using an automated Github-Actions-based CI/CD workflow to a $5/mo Linode VPS server. These workflows run the test suite against PRs and build and deploy docker images via SSH.</p>

<p>I recently split up this workflow into separate files and stopped using the Docker-based <code class="language-plaintext highlighter-rouge">docker-compose.test.yml</code>/<code class="language-plaintext highlighter-rouge">sut</code> workflow. I’ve replaced it with the native Github Ruby pipeline, which is substantially faster than building a Docker image just to run tests.</p>

<p>Here’s my test workflow pipeline that runs the Ruby-based test suite on every new PR.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in .github/workflows/test.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Tests</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="c1"># Run tests.</span>
  <span class="c1"># See also https://docs.docker.com/docker-hub/builds/automated-testing/</span>
  <span class="na">test</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Ruby</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install ruby dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">bundle install</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install javascript dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">yarn install</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run tests</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">bundle exec rake</span>
</code></pre></div></div>

<p>Once PRs are merged to <code class="language-plaintext highlighter-rouge">main</code>, a follow-up <code class="language-plaintext highlighter-rouge">Deploy</code> workflow builds a Docker image and deploys it to my Linode server via SSH:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in .github/workflows/deploy.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="c1"># Push image to GitHub Packages.</span>
  <span class="c1"># See also https://docs.docker.com/docker-hub/builds/</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name == 'push'</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log into registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">ghcr.io</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build image</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">docker build . --file Dockerfile --tag ghcr.io/my-profile/my-repo/my-image:latest</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Push image</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">docker push ghcr.io/my-profile/my-repo/my-image:latest</span>

  <span class="na">deploy</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name == 'push'</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">webfactory/ssh-agent@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">ssh-private-key</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">ssh $@$ "sh -s" &lt; deploy.sh</span>
</code></pre></div></div>

<p>The above Deploy workflow references a custom <code class="language-plaintext highlighter-rouge">deploy.sh</code> script. Here’s a generic example of what that script does:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="c"># This script runs on the docker server to deploy the application. It can be kicked off locally via:</span>
<span class="c">#</span>
<span class="c"># ```</span>
<span class="c"># ssh my-server &lt; deploy.sh</span>
<span class="c"># ```</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="nb">echo</span> <span class="s1">'Removing dangling images'</span>
<span class="nb">yes</span> | docker image prune

<span class="nb">echo</span> <span class="s1">'Pulling latest'</span>
docker pull ghcr.io/my-profile/my-repo/my-image:latest

<span class="nb">echo</span> <span class="s1">'Stopping the container'</span>
docker container stop my-app <span class="o">||</span> <span class="nb">true

</span><span class="k">until</span> <span class="o">[</span> <span class="s2">"</span><span class="sb">`</span>docker ps <span class="nt">--filter</span> <span class="s1">'name=my-app'</span> <span class="nt">--format</span> <span class="s1">''</span><span class="sb">`</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]</span><span class="p">;</span> <span class="k">do
	</span><span class="nb">sleep </span>0.1<span class="p">;</span>
<span class="k">done</span><span class="p">;</span>

<span class="nb">echo</span> <span class="s1">'Starting the container'</span>
docker run <span class="nt">--rm</span> <span class="nt">--name</span> my-app <span class="nt">-d</span> <span class="nt">-p</span> 3000:3000 ghcr.io/my-profile/my-repo/my-image:latest startup.sh
</code></pre></div></div>]]></content><author><name>Aaron Breckenridge</name></author><category term="ops" /><summary type="html"><![CDATA[Last updated February 3, 2026.]]></summary></entry><entry><title type="html">Advice for transitioning into a software career</title><link href="https://breckenedge.github.io/career/2020/09/08/getting_started.html" rel="alternate" type="text/html" title="Advice for transitioning into a software career" /><published>2020-09-08T00:00:00+00:00</published><updated>2020-09-08T00:00:00+00:00</updated><id>https://breckenedge.github.io/career/2020/09/08/getting_started</id><content type="html" xml:base="https://breckenedge.github.io/career/2020/09/08/getting_started.html"><![CDATA[<p>I’ve been asked a few times over the years how to break into software development as a career. About nine years ago, I switched careers from Technical Writer to Software Developer.</p>

<p><strong>Note:</strong> This post was written in 2020 and is significantly out of date. The advice may no longer reflect current practices or resources.</p>

<p>Here are a few things that I recommend doing to become a Software Developer:</p>

<ol>
  <li>
    <p>Attend meetups to network. Nine years later, I still correspond with people from my first programming meetup (the Dallas Ruby Brigade).</p>
  </li>
  <li>
    <p>Pick one high-level language and UI or web framework and go really deep on both. For a first language, I really liked learning Ruby because the Ruby community tends to pick one style and stick to it, which made it easier to learn. It also has an amazing book: “Practical Object Oriented Design in Ruby.” The Ruby on Rails framework is really easy to learn the basics and find jobs. If you already know a dynamic programming language, learn a statically-typed programming language like Java or C#.</p>
  </li>
  <li>
    <p>Learn some C programming. C is the ancestor of many modern languages. I read “C Programming Absolute Beginner’s Guide” a few years ago and liked it.</p>
  </li>
  <li>
    <p>Practice coding fundamentals. Entry-level programming interviews will probably expect some demonstration of using an algorithm to solve some well-trod problem. The book “Cracking the Coding Interview” is invaluable and it has a timeline for beginning a coding career. <a href="https://codewars.com">Codewars</a> is great for self-paced practice in a variety of languages. <a href="https://adventofcode.com">Advent of Code</a> is fun and very challenging. There’s also <a href="https://projecteuler.net/">Project Euler</a>.</p>
  </li>
  <li>
    <p>Read about trends in programming languages, architectures and frameworks. <a href="https://news.ycombinator.com">Hacker News</a> is a good source for daily trends, but I also like to read blogs and tech radars. My favorite blogs include <a href="https://martinfowler.com">Martin Fowler</a> and <a href="https://blog.codinghorror.com">Coding Horror</a>.</p>
  </li>
  <li>
    <p>Launch a personal project. Don’t think you need to know everything to start your career. Start interviewing as soon as you have a personal project that you can show off. A common first project is a todo list app.</p>
  </li>
</ol>]]></content><author><name>Aaron Breckenridge</name></author><category term="career" /><summary type="html"><![CDATA[I’ve been asked a few times over the years how to break into software development as a career. About nine years ago, I switched careers from Technical Writer to Software Developer.]]></summary></entry></feed>