<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Version-Control | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/version-control/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:52 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/version-control/index.xml" rel="self" type="application/rss+xml"/><item><title>I Built a CLI to Check My GitHub Pending Stuff: ghpending</title><link>https://akitaonrails.github.io/en/2026/05/23/i-built-a-cli-to-check-my-github-pending-stuff-ghpending/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/23/i-built-a-cli-to-check-my-github-pending-stuff-ghpending/</guid><pubDate>Sat, 23 May 2026 22:00:00 -0300</pubDate><description>&lt;p&gt;After my &lt;a href="https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/"&gt;AI marathon&lt;/a&gt;, I&amp;rsquo;m sitting on dozens of public GitHub repos to look after. A bunch of them have contributors filing issues and sending pull requests. The problem is, opening a browser, walking into each repo, checking whether anything new is waiting, becomes a chore I end up dodging. And when I dodge it, I go weeks without responding to collaborators.&lt;/p&gt;
&lt;p&gt;I know there are plenty of tools that solve this. But I wanted something dead simple and fast to run in the terminal so I can see pending work across all my projects at once. So I built it: &lt;strong&gt;&lt;a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener"&gt;ghpending&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>After my <a href="/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/">AI marathon</a>, I&rsquo;m sitting on dozens of public GitHub repos to look after. A bunch of them have contributors filing issues and sending pull requests. The problem is, opening a browser, walking into each repo, checking whether anything new is waiting, becomes a chore I end up dodging. And when I dodge it, I go weeks without responding to collaborators.</p>
<p>I know there are plenty of tools that solve this. But I wanted something dead simple and fast to run in the terminal so I can see pending work across all my projects at once. So I built it: <strong><a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a></strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/23/ghpending/ghpending-screenshot.png" alt="ghpending output in the terminal: compact table listing each tracked repo with counts of open issues and pull requests, latest activity author, and how long ago."  loading="lazy" /></p>
<p>Runs on Linux and macOS. I tested on Linux (Arch + Omarchy) but haven&rsquo;t run it on macOS yet. If you try it there and hit a bug, file an issue at <a href="https://github.com/akitaonrails/ghpending/issues"target="_blank" rel="noopener">github.com/akitaonrails/ghpending/issues</a>.</p>
<h2>Install<span class="hx:absolute hx:-mt-20" id="install"></span>
    <a href="#install" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Three paths, pick whichever fits your setup.</p>
<p>Via Homebrew (macOS or Linux):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Via Cargo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">cargo install ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Via mise:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mise use -g github:akitaonrails/ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Upgrading<span class="hx:absolute hx:-mt-20" id="upgrading"></span>
    <a href="#upgrading" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># Homebrew</span>
</span></span><span class="line"><span class="cl">brew upgrade ghpending
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cargo</span>
</span></span><span class="line"><span class="cl">cargo install ghpending --force
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise upgrade ghpending
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From source</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ghpending <span class="o">&amp;&amp;</span> git pull <span class="o">&amp;&amp;</span> cargo install --path .</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Usage<span class="hx:absolute hx:-mt-20" id="usage"></span>
    <a href="#usage" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Four commands. The normal flow is to run <code>ghpending add</code> once to set up which repos you want to track, then just <code>ghpending</code> whenever you want to see the digest.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ghpending add    <span class="c1"># pick repos from a GitHub user or org to track</span>
</span></span><span class="line"><span class="cl">ghpending        <span class="c1"># print the digest of open issues and PRs</span>
</span></span><span class="line"><span class="cl">ghpending list   <span class="c1"># show tracked repos</span>
</span></span><span class="line"><span class="cl">ghpending rm     <span class="c1"># remove repos from tracking via interactive menu</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>add</code> asks for a user or organization, lists their public repos, and lets you check off the ones you want to follow. The username gets saved, so re-running <code>add</code> skips that prompt.</p>
<p>The main command (<code>ghpending</code> with no args) fetches every tracked repo concurrently and prints the table with open issues and PRs.</p>
<h2>GitHub token (optional)<span class="hx:absolute hx:-mt-20" id="github-token-optional"></span>
    <a href="#github-token-optional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It works without auth, but GitHub caps anonymous traffic at 60 requests per hour. To raise that ceiling to 5,000 req/h, export a token:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">GITHUB_TOKEN</span><span class="o">=</span><span class="k">$(</span>gh auth token<span class="k">)</span> ghpending</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s read silently at startup, no config needed.</p>
<h2>Where the config lives<span class="hx:absolute hx:-mt-20" id="where-the-config-lives"></span>
    <a href="#where-the-config-lives" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Linux: <code>~/.config/ghpending/config.toml</code></li>
<li>macOS: <code>~/Library/Application Support/ghpending/config.toml</code></li>
</ul>
<p>You can edit it directly if you want to reorder repos or change the <code>user</code>. Shape:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="s2">&#34;akitaonrails&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">repos</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;akitaonrails/ai-memory&#34;</span><span class="p">,</span> <span class="s2">&#34;akitaonrails/ai-jail&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Why this CLI exists<span class="hx:absolute hx:-mt-20" id="why-this-cli-exists"></span>
    <a href="#why-this-cli-exists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The honest answer: tools I build for myself, I use. Tools written by other people always come with some friction (web login, a different architectural opinion, a feature I wanted missing or one I didn&rsquo;t want included). Since the goal was to reduce the friction of checking 30 repos a week, 200 lines of Rust on a Saturday afternoon beat learning a new stack.</p>
<p>Now I&rsquo;m hoping I can respond to contributors faster. If you find it useful, it&rsquo;s MIT licensed, PRs are welcome.</p>
]]></content:encoded><category>ghpending</category><category>automation</category><category>developer-tools</category><category>version-control</category></item><item><title>3 Months of GitLab. The Hybrid-SaaS Era</title><link>https://akitaonrails.github.io/en/2016/11/16/3-months-of-gitlab-the-hybrid-saas-era/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/11/16/3-months-of-gitlab-the-hybrid-saas-era/</guid><pubDate>Wed, 16 Nov 2016 16:44:00 -0200</pubDate><description>&lt;p&gt;If you didn&amp;rsquo;t already, maybe it&amp;rsquo;s a good idea to read my previous post on &lt;a href="https://about.gitlab.com/2016/08/04/moving-to-gitlab-yes-its-worth-it/"target="_blank" rel="noopener"&gt;Moving to GitLab! Yes, it&amp;rsquo;s worth it!&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My impressions stem from very specific circumstances, so this is not to be read as a general recommendation for every situation. For example, I didn&amp;rsquo;t review GitLab&amp;rsquo;s hosted options, which may be more compatible with circumstances different than mine.&lt;/p&gt;
&lt;p&gt;In my case, the move to GitLab was part of an internal company strategy to have more control over our own data. In that strategy we also moved from using Slack to Mattermost, and from using Pivotal Tracker to my own open source alternative called &lt;a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener"&gt;Central&lt;/a&gt;, among other things.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you didn&rsquo;t already, maybe it&rsquo;s a good idea to read my previous post on <a href="https://about.gitlab.com/2016/08/04/moving-to-gitlab-yes-its-worth-it/"target="_blank" rel="noopener">Moving to GitLab! Yes, it&rsquo;s worth it!</a>.</p>
<p>My impressions stem from very specific circumstances, so this is not to be read as a general recommendation for every situation. For example, I didn&rsquo;t review GitLab&rsquo;s hosted options, which may be more compatible with circumstances different than mine.</p>
<p>In my case, the move to GitLab was part of an internal company strategy to have more control over our own data. In that strategy we also moved from using Slack to Mattermost, and from using Pivotal Tracker to my own open source alternative called <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">Central</a>, among other things.</p>
<p>Before the migration, I had dozens of active projects over GitHub and lots of archived ones at Bitbucket. I&rsquo;m the kind that hates to lose data, so I keep redundant backups and try to never erase anything.</p>
<p>I moved almost everything over to my own GitLab server and this is almost <strong>200 repositories</strong>, spread in 4 groups, with almost 80 active users. This accounts for more than 13,500 notes and over 5,200 Merge Requests already, and over 2,500 builds in the CI, and the internal Sidekiq controlling everything has over 51,000 jobs processed.</p>
<p>In the last 3 months since the migration, I am paying a Digital Ocean bill of around $140 every month for the exact infrastructure I described in the previous article, with a separated box for the GitLab Core, the CI Runner, the CI Docker Registry Mirror, the CI Cache server and disposable machines for parallel builds, controlled by the Runner.</p>
<p>Although the reduction in cost is welcome, the whole endeavor was not done because of it. I could easily pay double or triple just for the convenience, but the underlying strategy is more important in this case.</p>
<p>My company has over 60 developers, working from our own offices in 6 different cities in Brazil (no freelancers and no home-office), communicating daily over Mattermost, Code Reviewing through GitLab&rsquo;s Merge Requests, having immediate automated tests feedback from GitLab CI, and project management through our own Central application.</p>
<p>The only piece that is currently missing is a good static code analysis tool. We heavily rely on Code Climate, which I strongly believe is one of the best out there from our own experience.</p>
<p>I was about to start an endeavor to build a simplified OSS version to integrate into GitLab CI&rsquo;s, but <a href="http://blog.codeclimate.com/blog/2016/10/06/series-a-and-community-edition/"target="_blank" rel="noopener">Bryan Helmkamp recently announced</a> that they would release their own &ldquo;Community Edition&rdquo; version of Code Climate in the next few months.</p>
<p>This is exactly the missing piece in my stack, and it will make my strategy move faster for next year.</p>
<h3>Hybrid-SaaS Era<span class="hx:absolute hx:-mt-20" id="hybrid-saas-era"></span>
    <a href="#hybrid-saas-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the past 5 to 10 years there was a fast move towards 3rd party &ldquo;micro-services&rdquo; structure. And this is really great as it allowed many small companies or even independent developers to tap into technology that made them not only move faster, but with increasingly more quality in their delivery.</p>
<p>It&rsquo;s now super easy to have top-notch upload and storage service through tools like Cloudinary.</p>
<p>Also very easy to have top-notch relational database, with replication and scalability, such as Heroku Postgres or AWS RDS.</p>
<p>Project management as a service with Pivotal Tracker, Trello.</p>
<p>Communication as a service with Slack, Hangout, etc.</p>
<p>Knowledge Management as a service with GitHub, Bitbucket, etc.</p>
<p>So now many tech companies are a mashup of several different 3rd party services.</p>
<p>My small beef is that our entire knowledge, experience, portfolio is spread across a dozen or more opaque services, completely out of our control and depending on each company&rsquo;s (or their investors) whim.</p>
<p>Don&rsquo;t get me wrong, I am not against using those services, on the contrary. I use several of them and I will continue to use many of them for the foreseeable future. Some endeavors would not even be possible without the efficiency of this sample of a technology-based free market.</p>
<p>But every now and then you reach a tipping point where it becomes important to have more control over your own data, your own identity. Not only for pure ownership but also to be able to use this data with better intent.</p>
<p>And I believe that at that point we should have an option that doesn&rsquo;t cost a couple of limbs to replicate.</p>
<p>Before an option such as GitLab, we were either limited to choose between walled-gardens such as GitHub or Bitbucket or you could invest a ton of resources trying to use small open sourced components to try to build your own. With GitLab we now have the option to make a smooth transition to its hosted option, so we don&rsquo;t have to deal with infrastructure maintenance or we can choose to have full control. And the users will not suffer in the process.</p>
<p>Which is why I believe that the best SaaS to endure the next decade will start to go into <strong>Hybrid-mode</strong>: having a commercial, usually &ldquo;cheaper&rdquo;, hosted option, and a DIY (do-it-yourself) OSS version.</p>
<p>GitLab is like that. Mattermost is like that. Now Code Climate is getting like that.</p>
<p>In that option, companies like GitLab have a win-win situation. Many more people and companies can contribute towards making a mature and robust platform that everybody enjoys using, and at the same time, each participant can extend it to their own particular plans. GitLab can have a sustainable business model serving the long tail of companies and developers that want a reasonably affordable hosted service while the top 20% can pursue more specific endeavors using the same technology.</p>
<p>That way we remove the discussion of confidentiality, the way companies treat our data, and we move forward into more productive actions such as building a tool that benefits both the external public and each other&rsquo;s internal plans.</p>
<p>Walled gardens are here to stay. Releasing integration — while opaque — public APIs is not enough. It&rsquo;s very exciting to see those new options arising to fill this gap. The OSS environment along with a sustainable business model makes sense. And I hope to continue to see more and more competitors to closed services following the Hybrid model in the near future.</p>
<p>Why go through the trouble to install, maintain and tweak open sourced alternatives to well-established hosted (albeit opaque) services? I believe it&rsquo;s more than a discussion over convenience, maintenance and cutting costs. We should start small, less risky, but as we grow we should be able to take control back. But we usually can&rsquo;t get out of walled gardens without a significant - and sometimes impossible - investment in reinventing the wheel. We always had the small open sourced components that those services are built on, but to go from those small components to a full-featured system, it&rsquo;s an unrealistic path.</p>
<p>With this possible trend of post-SaaS, or Hybrid-SaaS as I call it, we may have just gained the missing link to go from just convenience to full control without the inherent costs and risks chasm.</p>
]]></content:encoded><category>version-control</category><category>open-source</category><category>business</category></item><item><title>Moving to GitLab! Yes, it's worth it!</title><link>https://akitaonrails.github.io/en/2016/08/03/moving-to-gitlab-yes-it-s-worth-it/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/03/moving-to-gitlab-yes-it-s-worth-it/</guid><pubDate>Wed, 03 Aug 2016 14:49:00 -0300</pubDate><description>&lt;p&gt;I &lt;a href="https://akitaonrails.github.io/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/"&gt;started evangelizing Git in 2007&lt;/a&gt;. It was a very tough sell to make at the time.&lt;/p&gt;
&lt;p&gt;Outside of the kernel development almost no one wanted to learn it and we had very worthy competitors, from Subversion, to Mercurial, to Bazaar, to Darcs, to Perforce, and so on. But those of us who dug deeper knew that Git had the edge and it was a matter of time.&lt;/p&gt;
&lt;p&gt;Then GitHub showed up in 2008 and the rest is history. For many years it was just &amp;ldquo;cool&amp;rdquo; to be in GitHub. The Ruby community drove GitHub up into the sky. Finally it became the status quo and the one real monopoly in information repositories - not just software source code, but everything.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I <a href="/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/">started evangelizing Git in 2007</a>. It was a very tough sell to make at the time.</p>
<p>Outside of the kernel development almost no one wanted to learn it and we had very worthy competitors, from Subversion, to Mercurial, to Bazaar, to Darcs, to Perforce, and so on. But those of us who dug deeper knew that Git had the edge and it was a matter of time.</p>
<p>Then GitHub showed up in 2008 and the rest is history. For many years it was just &ldquo;cool&rdquo; to be in GitHub. The Ruby community drove GitHub up into the sky. Finally it became the status quo and the one real monopoly in information repositories - not just software source code, but everything.</p>
<p>I always knew that we should have a &ldquo;local&rdquo; option, which is why <a href="https://gitorious.org/gitorious/oboxodo-gitorious?p=gitorious:oboxodo-gitorious.git;a=search;h=9f6bdf5887c65a440bc3fdc43a14652f42ddf103;s=Fabio&#43;Akita;st=committer"target="_blank" rel="noopener">I tried to contribute to Gitorious</a> way back in 2009. Other options arose, but eventually GitLab appeared around 2011 and picked up steam in the last couple of years.</p>
<p>GitHub itself raised <a href="https://www.crunchbase.com/organization/github#/entity"target="_blank" rel="noopener">USD 350 million in funding</a> and one of its required goals is to nail the Enterprise Edition for big corporations that don&rsquo;t want their data outside their closed gardens. Although GitHub hosts every single open source project out there, they are themselves closed-source.</p>
<p><a href="http://gitlab.com"target="_blank" rel="noopener">GitLab Inc.</a> started differently with an open source-first approach with their Community Edition (CE) and having both a GitHub-like hosted option as well as a supported Enterprise Edition for risk-averse corporations. They already raised <a href="https://www.crunchbase.com/organization/gitlab-com#/entity"target="_blank" rel="noopener">USD 5.62 million in funding</a>, and they are the most promising alternative to GitHub so far.</p>
<p>Of course, there are other platforms such as Atlassian&rsquo;s Bitbucket. But I believe Atlassian&rsquo;s strategy is slower and they have a larger suite of enterprise products to sell first, such as Confluence and Jira. I don&rsquo;t think they ever posed much of a competition against GitHub.</p>
<p>GitLab really started accelerating in 2015 as this <a href="https://github.com/gitlabhq/gitlabhq/graphs/contributors?from=2015-03-14&amp;to=2016-08-02&amp;type=c"target="_blank" rel="noopener">commit graph</a> shows:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/545/big_Contributors_to_gitlabhq_gitlabhq.png" alt="contributions"  loading="lazy" /></p>
<p>It&rsquo;s been steadily growing since 2011, but they seem to have crossed the first tipping point around late 2014, from early adopters to the early majority. This became more important as GitHub <a href="https://github.com/blog/2164-introducing-unlimited-private-repositories"target="_blank" rel="noopener">announced their pricing changes</a> in May.</p>
<p>They said they haven&rsquo;t committed to a deadline to enforce the change, so organizations can opt out of the new format for the time being. They are changing from &ldquo;limited repositories and unlimited users&rdquo; to &ldquo;unlimited repositories and limited users&rdquo;.</p>
<h2>The Cost-Benefit Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-benefit-conundrum"></span>
    <a href="#the-cost-benefit-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For example, if you have up to 8 developers on the USD 50/month plan (20 private repositories), the change won&rsquo;t affect you, as you will pay USD 25/month for 5 users and USD 9 for additional users (total of USD 52/month).</p>
<p>Now, if you have a big team of 100 developers currently in the Diamond Plan of USD 450/month (300 private repositories), you would have to pay USD 25/month + 95 times USD 9, which totals a staggering USD 880/month! <strong>Double the amount!</strong></p>
<p>This is an extra <strong>USD 10,560 per year</strong>!</p>
<p>And what does GitLab afford you instead?</p>
<p>You can have way more users and more repositories in a <strong>USD 40/month</strong> virtual box (4GB of RAM, 60GB SSD, 4TB transfer).</p>
<p>And it doesn&rsquo;t stop there. GitLab also has very functional <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab Multi Runner</a> which you can install in a separate box (actually, at least 3 boxes - more on that below).</p>
<p>You can easily connect this runner to the build system over GitLab so every new git push triggers the runner to run the automated test suite in a Docker image of your choosing. So it&rsquo;s a fully functional, full featured Continuous Integration system nicely integrated in your GitLab project interface:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/546/big_Pipelines___cm42-archived___PremiosOnline___GitLab.png" alt="Pipeline"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/547/big_test___144____Builds___cm42-archived___PremiosOnline___GitLab.png" alt="CI Runner"  loading="lazy" /></p>
<p>Remind you of anything? Yep, it&rsquo;s a fully functional alternative to Travis-CI, Semaphore, CircleCI or any other CI you&rsquo;re using with a very easy to install procedure. Let&rsquo;s say you&rsquo;re paying Travis-CI USD 489/month to have 10 concurrent jobs.</p>
<p>You can install GitLab Runner in 3 boxes of USD 10/month (1GB RAM, 1 Cores, 30GB SSD) and have way more concurrent jobs (20? 50? Auto-Scale!?) that run <strong>faster</strong> (in a simple test, one build that took 15 minutes over Travis took less than 8 minutes at Digital Ocean).</p>
<p>So let&rsquo;s do the math for a year&rsquo;s worth of service. First considering no GitHub plan change:</p>
<blockquote>
  <p>USD 5,400 (GitHub) + USD 5,868 (Travis) = USD 11,268 a year.</p>

</blockquote>
<p>Now, the GitLab + GitLab Runner + Digital Ocean for the same features and unlimited users, unlimited repositories, unlimited concurrent builds:</p>
<blockquote>
  <p>USD 480 (GitLab) + USD 840 (Runner box) = USD 1,320 a year.</p>

</blockquote>
<p>This is already almost 8.5x cheaper with almost no change in quality.</p>
<p>For the worst case scenario, compare it when GitHub decides to enforce the new plans:</p>
<blockquote>
  <p>USD 10,560 (GitHub new plans) + USD 5,868 (Travis) = USD 16,428</p>

</blockquote>
<p>Now the GitLab option is 11x cheaper! You&rsquo;re saving almost USD 15,000 a year! This is not something you can ignore in your cost sheet.</p>
<p>As I said, the calculations above are only significant in a scenario with 100 developers. You must do your own math taking into account your team size and number of active projects (you can always archive unused projects).</p>
<p>Even if you don&rsquo;t have 100 developers. Let&rsquo;s consider the scenario for <strong>30 developers</strong> in the new GitHub per user plans and a smaller Travis configuration for 5 concurrent jobs:</p>
<blockquote>
  <p>USD 3,000 (GitHub new plan) + USD 3,000 (Travis) = USD 6,000</p>

</blockquote>
<p>It&rsquo;s 4.5x cheaper in the Digital Ocean + GitLab suite option.</p>
<p>Heck, let&rsquo;s consider the <strong>Current</strong> GitHub plan (the Platinum one, for up to 125 repositories):</p>
<blockquote>
  <p>USD 2,400 (GitHub current plan) + USD 3,000 (Travis) = USD 5,400</p>

</blockquote>
<p>Still at least <strong>4x more expensive</strong> than a GitLab-based solution!</p>
<p>And how long will it take for a single developer to figure out the set up and migrate everything from GitHub over to the new GitLab installation? I will say that you can reserve 1 week of work for the average programmer to do it following the official documentation and my tips and tricks below.</p>
<h2>Installing GitLab CE<span class="hx:absolute hx:-mt-20" id="installing-gitlab-ce"></span>
    <a href="#installing-gitlab-ce" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I will not bore you with what you can readily find over the Web. I highly recommend you start with the easiest solution first: <a href="https://www.digitalocean.com/features/one-click-apps/gitlab/"target="_blank" rel="noopener">Digital Ocean&rsquo;s One-Click Automatic Install</a>. Install it in at least a 4GB RAM machine (you will want to keep it if you like it).</p>
<p>Of course, there is a number of different installation options, from AWS AMI images to Ubuntu packages you can install manually. Study the <a href="https://about.gitlab.com/installation/"target="_blank" rel="noopener">documentation</a>.</p>
<p>It will cost you USD 40 for a month of trial. If you want to save as much as tens of thousands of dollars, this is a bargain.</p>
<p>GitLab has many customization options. You can lock down your private GitLab to allow only users with an official e-mail from your domain, for example. You can configure <a href="http://docs.gitlab.com/ee/integration/omniauth.html"target="_blank" rel="noopener">OAuth2 providers</a> so your users can quickly sign in using their GitHub, Facebook, Google or other accounts.</p>
<h4>A Few Gotchas<span class="hx:absolute hx:-mt-20" id="a-few-gotchas"></span>
    <a href="#a-few-gotchas" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>I&rsquo;ve stumbled upon a few caveats in the configuration. Which is why I recommend that you plan ahead - study this entire article ahead of time! - do a quick install that you can blow away, so you can &ldquo;feel&rdquo; the environment before trying to migrate all your repos over to your brand new GitLab. As a reference, this is a part of my <code>/etc/gitlab/gitlab.rb</code>:</p>
<p>&mdash; ruby</p>
<h1>register a domain for your server and place it here:</h1><p>external_url &ldquo;<a href="http://my-gitlab-server.com/%22"target="_blank" rel="noopener">http://my-gitlab-server.com/"</a></p>
<h1>you will want to enable <a href="https://git-lfs.github.com"target="_blank" rel="noopener">LFS</a></h1><p>gitlab_rails[&rsquo;lfs_enabled&rsquo;] = true</p>
<h1>register your emails</h1><p>gitlab_rails[&lsquo;gitlab_email_from&rsquo;] = &ldquo;<a href="mailto:no-reply@my-gitlab-server.com">no-reply@my-gitlab-server.com</a>&rdquo;
gitlab_rails[&lsquo;gitlab_support_email&rsquo;] = &ldquo;<a href="mailto:contact@my-gitlab-server.com">contact@my-gitlab-server.com</a>&rdquo;</p>
<h1>add your email configuration (template for gmail)</h1><p>gitlab_rails[&lsquo;smtp_enable&rsquo;] = true
gitlab_rails[&lsquo;smtp_address&rsquo;] = &ldquo;smtp.gmail.com&rdquo;
gitlab_rails[&lsquo;smtp_port&rsquo;] = 587
gitlab_rails[&lsquo;smtp_user_name&rsquo;] = &ldquo;&ndash; some no-reply email &mdash;&rdquo;
gitlab_rails[&lsquo;smtp_password&rsquo;] = &ldquo;&ndash; the password &mdash;&rdquo;
gitlab_rails[&lsquo;smtp_domain&rsquo;] = &ldquo;my-gitlab-server.com&rdquo;
gitlab_rails[&lsquo;smtp_authentication&rsquo;] = &ldquo;login&rdquo;
gitlab_rails[&lsquo;smtp_enable_starttls_auto&rsquo;] = true
gitlab_rails[&lsquo;smtp_openssl_verify_mode&rsquo;] = &lsquo;peer&rsquo;</p>
<h1>this is where you enable oauth2 integration</h1><p>gitlab_rails[&lsquo;omniauth_enabled&rsquo;] = true</p>
<h1>CAUTION!</h1><h1>This allows users to login without having a user account first. Define the allowed providers</h1><h1>using an array, e.g. [&ldquo;saml&rdquo;, &ldquo;twitter&rdquo;], or as true/false to allow all providers or none.</h1><h1>User accounts will be created automatically when authentication was successful.</h1><p>gitlab_rails[&lsquo;omniauth_allow_single_sign_on&rsquo;] = [&lsquo;github&rsquo;, &lsquo;google_oauth2&rsquo;, &lsquo;bitbucket&rsquo;]
gitlab_rails[&lsquo;omniauth_block_auto_created_users&rsquo;] = true</p>
<p>gitlab_rails[&lsquo;omniauth_providers&rsquo;] = [
{
&ldquo;name&rdquo; =&gt; &ldquo;github&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; github app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; github secret &ndash;&rdquo;,
&ldquo;url&rdquo; =&gt; &ldquo;https://github.com/&rdquo;,
&ldquo;args&rdquo; =&gt; { &ldquo;scope&rdquo; =&gt; &ldquo;user:email&rdquo; }
},
{
&ldquo;name&rdquo; =&gt; &ldquo;google_oauth2&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; google app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; google secret &ndash;&rdquo;,
&ldquo;args&rdquo; =&gt; { &ldquo;access_type&rdquo; =&gt; &ldquo;offline&rdquo;, &ldquo;approval_prompt&rdquo; =&gt; &lsquo;&rsquo;, hd =&gt; &lsquo;codeminer42.com&rsquo; }
},
{
&ldquo;name&rdquo; =&gt; &ldquo;bitbucket&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; bitbucket app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; bitbucket secret id &ndash;&rdquo;,
&ldquo;url&rdquo; =&gt; &ldquo;https://bitbucket.org/&rdquo;
}
]</p>
<h1>if you&rsquo;re importing repos from GitHub, Sidekiq workers can grow as high as 2.5GB of RAM and the default <a href="http://docs.gitlab.com/ee/operations/sidekiq_memory_killer.html"target="_blank" rel="noopener">Sidekiq Killer</a> config will cap it down to 1GB, so you want to either disable it by adding &lsquo;0&rsquo; or adding a higher limit</h1><h2>gitlab_rails[&rsquo;env&rsquo;] = { &lsquo;SIDEKIQ_MEMORY_KILLER_MAX_RSS&rsquo; =&gt; &lsquo;3000000&rsquo; }<span class="hx:absolute hx:-mt-20" id="gitlab_railsenv---sidekiq_memory_killer_max_rss--3000000-"></span>
    <a href="#gitlab_railsenv---sidekiq_memory_killer_max_rss--3000000-" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There are <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb#L57"target="_blank" rel="noopener">dozens of default variables</a> you can <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/environment-variables.md"target="_blank" rel="noopener">override</a>, just be careful on your testings.</p>
<p>Every time you change a configuration, you can just run the following commands:</p>
<hr>
<h2>sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart<span class="hx:absolute hx:-mt-20" id="sudo-gitlab-ctl-restart"></span>
    <a href="#sudo-gitlab-ctl-restart" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You can open a Rails console to inspect production objects like this:</p>
<hr>
<h2>gitlab-rails console<span class="hx:absolute hx:-mt-20" id="gitlab-rails-console"></span>
    <a href="#gitlab-rails-console" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I had a lot of trouble importing big repos from GitHub, but after a few days debugging the problem with GitLab Core Team developers <a href="https://gitlab.com/u/dbalexandre"target="_blank" rel="noopener">Douglas Alexandre</a>, <a href="https://gitlab.com/u/brodock"target="_blank" rel="noopener">Gabriel Mazetto</a>, a few Merge Requests and some local patching, I was finally able to import relatively big projects (more than 5,000 commits, more than 1,000 issues, more than 1,200 pull requests with several comments worth of discussion threads). A project of this size can take a couple of hours to complete, mainly because <strong>it&rsquo;s damn slow to use GitHub&rsquo;s public APIs</strong> (they are slow and they have rate limits and abuse detection, so you can&rsquo;t fetch everything as fast as your bandwidth would allow).</p>
<p>(By the way, don&rsquo;t miss that GitLab will be over at <a href="http://www.rubyconf.com.br/pt-BR/speakers#Gabriel%20Gon%c3%a7alves%20Nunes%20Mazetto"target="_blank" rel="noopener">Rubyconf Brazil 2016</a>, on Sep 23-24)</p>
<p>Migrating all my GitHub projects took a couple of days, but they all went through smoothly and my team didn&rsquo;t have any trouble, just adjusting their git remote URLs and they&rsquo;re done.</p>
<p>The import procedure from GitHub is quite complete, it brings not only the git repo per se, but also all the metadata, from labels to comments and pull request history - which is the one that usually takes more time.</p>
<p>But I&rsquo;d recommend waiting for at least version 8.11 (it&rsquo;s currently 8.10.3) before trying to import large GitHub projects.</p>
<p>If you&rsquo;re on Bitbucket, unfortunately there are fewer features in the importer. It will mostly just bring the source code. So be aware of that if you extensively depend on their pull request system and you want to preserve this history. More features will come and you can even help them out, they are very resourceful and willing to make GitLab better.</p>
<h3>Side-track: Customizations for every Digital Ocean box<span class="hx:absolute hx:-mt-20" id="side-track-customizations-for-every-digital-ocean-box"></span>
    <a href="#side-track-customizations-for-every-digital-ocean-box" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Assume that you should run what&rsquo;s in this section for all new machines you create over Digital Ocean.</p>
<p>First of all, they come without a swap file. No matter how much RAM you have, the Linux OS is meant to work better by combining a swap file. You can <a href="https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04"target="_blank" rel="noopener">read more about it</a> later, for now just run the following as root:</p>
<hr>
<p>fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile</p>
<h2>sysctl vm.swappiness=10
sysctl vm.vfs_cache_pressure=50<span class="hx:absolute hx:-mt-20" id="sysctl-vmvfs_cache_pressure50"></span>
    <a href="#sysctl-vmvfs_cache_pressure50" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Edit the <code>/etc/fstab</code> file and add this line:</p>
<hr>
<h2>/swapfile   none    swap    sw    0   0<span class="hx:absolute hx:-mt-20" id="swapfile---none----swap----sw----0---0"></span>
    <a href="#swapfile---none----swap----sw----0---0" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finally, edit the <code>/etc/sysctl.conf</code> file and add these lines:</p>
<hr>
<h2>vm.swappiness=10
vm.vfs_cache_pressure = 50<span class="hx:absolute hx:-mt-20" id="vmvfs_cache_pressure--50"></span>
    <a href="#vmvfs_cache_pressure--50" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Don&rsquo;t forget to set the <a href="http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue"target="_blank" rel="noopener">default locale</a> of your machine. Start by editing the <code>/etc/environment</code> file and adding:</p>
<hr>
<h2>LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8<span class="hx:absolute hx:-mt-20" id="langen_usutf-8"></span>
    <a href="#langen_usutf-8" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Then run:</p>
<hr>
<h2>sudo locale-gen en_US en_US.UTF-8
sudo dpkg-reconfigure locales<span class="hx:absolute hx:-mt-20" id="sudo-dpkg-reconfigure-locales"></span>
    <a href="#sudo-dpkg-reconfigure-locales" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finally, you should have Ubuntu automatically install stable security patches for you. You don&rsquo;t want to forget machines online without the most current security fixes, so just run this:</p>
<hr>
<h2>sudo dpkg-reconfigure &ndash;priority=low unattended-upgrades<span class="hx:absolute hx:-mt-20" id="sudo-dpkg-reconfigure-prioritylow-unattended-upgrades"></span>
    <a href="#sudo-dpkg-reconfigure-prioritylow-unattended-upgrades" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Choose &ldquo;yes&rdquo; and you&rsquo;re done. And of course, for every fresh install, it&rsquo;s always good to run the good old:</p>
<hr>
<h2>sudo apt-get update &amp;&amp; sudo apt-get upgrade<span class="hx:absolute hx:-mt-20" id="sudo-apt-get-update--sudo-apt-get-upgrade"></span>
    <a href="#sudo-apt-get-update--sudo-apt-get-upgrade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the very basics, I believe it&rsquo;s easier to have an image with all this ready, but if you use the standard Digital Ocean images, these settings should do the trick for now.</p>
<h2>Installing the CI Runner<span class="hx:absolute hx:-mt-20" id="installing-the-ci-runner"></span>
    <a href="#installing-the-ci-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Once you finish your GitLab installation, it&rsquo;s <a href="https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/"target="_blank" rel="noopener">super easy</a> to deploy the GitLab Runner. You can use the same machine but I recommend you install it in a separate machine.</p>
<p>If you don&rsquo;t know what a runner is, just imagine it like this: It&rsquo;s basically a server connected to the GitLab install. When it&rsquo;s available and online, whenever someone pushes a new commit, merge request, to a repository that has a <code>gitlab-ci-yml</code> file present, GitLab will push a command to the runner.</p>
<p>Depending on how you configured the runner, it will receive this command and spawn a new Docker container. Inside the container it will execute whatever you have defined in the <code>gitlab-ci.yml</code> file in the project. Usually it&rsquo;s fetching cached files (dependencies, for example), and run your test suite.</p>
<p>In the most basic setup, you will only have one Runner and any subsequent builds from other users will wait in line until they finish. If you&rsquo;ve used external CI services such as Travis-CI or CircleCI, you know that they charge for some number of concurrent builds. And it&rsquo;s <strong>very expensive</strong>.</p>
<p>The less concurrent builds available, the more your users will have to wait for feedback on their changes, and less productive you will become. People may even start to avoid adding new tests, or completely ignore the tests, which will really hurt the quality of your project over time. If there is one thing you <strong>must not</strong> do is not having good automated test suites.</p>
<p>Gabriel Mazetto pointed me to a very important GitLab CI Runner feature: <a href="https://about.gitlab.com/2016/03/29/gitlab-runner-1-1-released/"target="_blank" rel="noopener"><strong>auto-scaling</strong></a>. This is what they use in their hosted offering over at GitLab.com.</p>
<p>You can <strong>easily</strong> set up a runner that can use &ldquo;docker-machine&rdquo; and your IaaS provider APIs to spin up machines on the fly to run as many concurrent builds as you want, and it will be super cheap!</p>
<p>For example, on Digital Ocean you can be charged USD 0.06 (6 cents) per hour of usage of a 4GB machine. Over at AWS EC2 you can be charged USD 0.041 per hour for an m3.medium machine.</p>
<p>There is extensive documentation but I will try to summarize what you have to do. For more details I highly recommend you to study their <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/autoscaling.md#prepare-the-docker-registry-and-cache-server"target="_blank" rel="noopener">official documentation</a>.</p>
<p>Start by creating 3 new machines at Digital Ocean, all in the same Region with private networking enabled! I will list a fake private IP address just for the sake of advancing in the configuration examples:</p>
<ul>
<li>a 1GB machine called &ldquo;docker-registry-mirror&rdquo;, (ex 10.0.0.1)</li>
<li>a 1GB machine called &ldquo;ci-cache&rdquo;, (ex 10.0.0.2)</li>
<li>a 1GB machine called &ldquo;ci-runner&rdquo;, (ex 10.0.0.3)</li>
</ul>
<p>Yeah, they can be small as very little will run on them. You can be conservative and choose the 2GB RAM options just to be on the safe side (and pricing will still be super cheap).</p>
<p>Don&rsquo;t forget to execute the basic configuration I mentioned above to enable a swapfile, auto security update and locale regeneration.</p>
<p>SSH in to &ldquo;docker-registry-mirror&rdquo; and just run:</p>
<hr>
<h2>docker run -d -p 6000:5000 <br>
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io <br>
&ndash;restart always <br>
&ndash;name registry registry:2<span class="hx:absolute hx:-mt-20" id="name-registry-registry2"></span>
    <a href="#name-registry-registry2" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you will have a local Docker images registry proxy and cache at <code>10.0.0.1:6000</code> (take note of the real private IP).</p>
<p>SSH in to &ldquo;ci-cache&rdquo; and run:</p>
<hr>
<p>mkdir -p /export/runner</p>
<h2>docker run -it &ndash;restart always -p 9005:9000 <br>
-v /.minio:/root/.minio -v /export:/export <br>
&ndash;name minio <br>
minio/minio:latest /export<span class="hx:absolute hx:-mt-20" id="miniominiolatest-export"></span>
    <a href="#miniominiolatest-export" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you will have an AWS S3 clone called <a href="https://github.com/minio/minio"target="_blank" rel="noopener">Minio</a> running. I didn&rsquo;t know this project even existed, but it is a nifty little service written in Go to clone the AWS S3 behavior and APIs. So now you can have your very own S3 inside your infrastructure!</p>
<p>After Docker spins up, it will print out the Access Key and Secret keys, make notes. And this service will be running at <code>10.0.0.2:9005</code>.</p>
<p>You can even open a browser and see their web interface at <code>http://10.0.0.2:9005</code> and use the access and secret keys to login. Make sure you have a bucket named &ldquo;runner&rdquo;. The files will be stored at the <code>/export/runner</code> directory.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/549/big_Minio_Browser.png" alt="Minio Dashboard"  loading="lazy" /></p>
<p>Make sure the <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"target="_blank" rel="noopener">bucket name is valid</a> (it must be a valid DNS naming, for example, DO NOT use underlines).</p>
<p>Open this URL from your freshly installed GitLab-CE: <code>http://yourgitlab.com/admin/runners</code> and take note of the Registration Token. Let&rsquo;s say it&rsquo;s <code>1aaaa_Z1AbB2CdefGhij</code></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/550/big_Admin_Area___GitLab.png" alt="Admin Area for Runner Registration Token"  loading="lazy" /></p>
<p>Finally, SSH in to &ldquo;ci-runner&rdquo; and run:</p>
<hr>
<p>curl -L <a href="https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-%60uname"target="_blank" rel="noopener">https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-`uname</a> -s<code>-</code>uname -m` &gt; /usr/local/bin/docker-machine</p>
<p>chmod +x /usr/local/bin/docker-machine</p>
<p>curl -L <a href="https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh"target="_blank" rel="noopener">https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh</a> | sudo bash</p>
<p>sudo apt-get install gitlab-ci-multi-runner</p>
<h2>rm -Rf ~/.docker # just to make sure<span class="hx:absolute hx:-mt-20" id="rm--rf-docker--just-to-make-sure"></span>
    <a href="#rm--rf-docker--just-to-make-sure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you can register this new runner with your GitLab install, you will need the Registration Token mentioned above.</p>
<hr>
<h2>sudo gitlab-ci-multi-runner register<span class="hx:absolute hx:-mt-20" id="sudo-gitlab-ci-multi-runner-register"></span>
    <a href="#sudo-gitlab-ci-multi-runner-register" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You will be asked a few questions, and this is what you can answer:</p>
<hr>
<h2>Please enter the gitlab-ci coordinator URL (e.g. <a href="https://gitlab.com/ci"target="_blank" rel="noopener">https://gitlab.com/ci</a> )
<a href="https://yourgitlab.com/ci"target="_blank" rel="noopener">https://yourgitlab.com/ci</a>
Please enter the gitlab-ci token for this runner
1aaaa_Z1AbB2CdefGhij # as in the example above
Please enter the gitlab-ci description for this runner
my-autoscale-runner
INFO[0034] fcf5c619 Registering runner&hellip; succeeded
Please enter the executor: shell, docker, docker-ssh, docker+machine, docker-ssh+machine, ssh?
docker+machine
Please enter the Docker image (eg. ruby:2.1):
codeminer42/ci-ruby:2.3
INFO[0037] Runner registered successfully. Feel free to start it, but if it&rsquo;s
running already the config should be automatically reloaded!<span class="hx:absolute hx:-mt-20" id="running-already-the-config-should-be-automatically-reloaded"></span>
    <a href="#running-already-the-config-should-be-automatically-reloaded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s make a copy of the original configuration, just to be safe:</p>
<hr>
<h2>cp /etc/gitlab-runner/config.toml /etc/gitlab-runner/config.bak<span class="hx:absolute hx:-mt-20" id="cp-etcgitlab-runnerconfigtoml-etcgitlab-runnerconfigbak"></span>
    <a href="#cp-etcgitlab-runnerconfigtoml-etcgitlab-runnerconfigbak" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Copy the first few lines of this file (you want the token), it will look like this:</p>
<hr>
<p>concurrent = 1
check_interval = 0</p>
<h2>[[runners]]
name = &ldquo;my-autoscale-runner&rdquo;
url = &ldquo;<a href="http://yourgitlab.com/ci%22"target="_blank" rel="noopener">http://yourgitlab.com/ci"</a>
token = &ldquo;&mdash; generated runner token &mdash;&rdquo;
executor = &ldquo;docker+machine&rdquo;<span class="hx:absolute hx:-mt-20" id="executor--dockermachine"></span>
    <a href="#executor--dockermachine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The important part here is the &ldquo;token&rdquo;. You will want to take note of it. And now you also will want to create a <a href="https://cloud.digitalocean.com/settings/api/tokens"target="_blank" rel="noopener">new API Token over at Digital Ocean</a>. Just Generate a New Token and take note.</p>
<p>You can now replace the entire <code>config.toml</code> file for this:</p>
<hr>
<p>concurrent = 20
check_interval = 0</p>
<h2>[[runners]]
name = &ldquo;my-autoscale-runner&rdquo;
url = &ldquo;<a href="http://yourgitlab.com/ci%22"target="_blank" rel="noopener">http://yourgitlab.com/ci"</a>
token = &ldquo;&mdash; generated runner token &mdash;&rdquo;
executor = &ldquo;docker+machine&rdquo;
limit = 15
[runners.docker]
tls_verify = false
image = &ldquo;codeminer42/ci-ruby:2.3&rdquo;
privileged = false
[runners.machine]
IdleCount = 2                   # There must be 2 machines in Idle state
IdleTime = 1800                 # Each machine can be in Idle state up to 30 minutes (after this it will be removed)
MaxBuilds = 100                 # Each machine can handle up to 100 builds in a row (after this it will be removed)
MachineName = &ldquo;ci-auto-scale-%s&rdquo;   # Each machine will have a unique name (&rsquo;%s&rsquo; is required)
MachineDriver = &ldquo;digitalocean&rdquo;  # Docker Machine is using the &lsquo;digitalocean&rsquo; driver
MachineOptions = [
&ldquo;digitalocean-image=coreos-beta&rdquo;,
&ldquo;digitalocean-ssh-user=core&rdquo;,
&ldquo;digitalocean-access-token=&ndash; your new Digital Ocean API Token &ndash;&rdquo;,
&ldquo;digitalocean-region=nyc1&rdquo;,
&ldquo;digitalocean-size=4gb&rdquo;,
&ldquo;digitalocean-private-networking&rdquo;,
&ldquo;engine-registry-mirror=http://10.0.0.1:6000&rdquo;
]
[runners.cache]
Type = &ldquo;s3&rdquo;   # The Runner is using a distributed cache with Amazon S3 service
ServerAddress = &ldquo;10.0.0.2:9005&rdquo;  # minio
AccessKey = &ldquo;&ndash; your minio access key &ndash;&rdquo;
SecretKey = &ldquo;&ndash; your minio secret key&rdquo;
BucketName = &ldquo;runner&rdquo;
Insecure = true # Use Insecure only when using with Minio, without the TLS certificate enabled<span class="hx:absolute hx:-mt-20" id="insecure--true--use-insecure-only-when-using-with-minio-without-the-tls-certificate-enabled"></span>
    <a href="#insecure--true--use-insecure-only-when-using-with-minio-without-the-tls-certificate-enabled" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And you can restart the runner to pick up the new configuration like this:</p>
<hr>
<h2>gitlab-ci-multi-runner restart<span class="hx:absolute hx:-mt-20" id="gitlab-ci-multi-runner-restart"></span>
    <a href="#gitlab-ci-multi-runner-restart" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As I said before, you will want to read the extensive <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/autoscale.md"target="_blank" rel="noopener">official documentation</a> (and every link within).</p>
<p>If you did everything right, changing the correct private IPs for the docker registry and cache, the correct tokens, and so forth, you can log in to your Digital Ocean dashboard and you will see something like this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/548/big_DigitalOcean_-_Droplets.png" alt="Digital Ocean CI Setup"  loading="lazy" /></p>
<p>And from the <code>ci-runner</code> machine, you can list them like this:</p>
<hr>
<h1>docker-machine ls</h1><h2>NAME                                                ACTIVE   DRIVER         STATE     URL                         SWARM   DOCKER    ERRORS
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://191.168.0.1:2376            v1.10.3
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://192.169.0.2:2376           v1.10.3<span class="hx:absolute hx:-mt-20" id="runner-xxxx-ci-auto-scale-xxxx-xxxx------------digitalocean---running---tcp192169022376-----------v1103"></span>
    <a href="#runner-xxxx-ci-auto-scale-xxxx-xxxx------------digitalocean---running---tcp192169022376-----------v1103" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>They should not list any errors, meaning that they are up and running, waiting for new builds to start.</p>
<p>There will be 2 new machines listed in your Digital Ocean dashboard, named &ldquo;runner-xxxxx-ci-auto-scale-xxxxx&rdquo;. This is what <code>IdleCount = 2</code> does. If they stay idle for more than 30 minutes (<code>IdleTime = 1800 </code>) they will be shut down so you don&rsquo;t get charged.</p>
<p>You can have several &ldquo;runner&rdquo; definitions, each with a <code>limit</code> of builds/machines that can be spawned in Digital Ocean. You can have other runner definitions for other providers, for example. But in this example we are limited to at most 15 machines, so 15 concurrent builds.</p>
<p>The <code>concurrent</code> limit is a global setting. So if I had 3 runner definitions, each with a <code>limit</code> of 15, they would still be globally limited to 20 as defined in the <code>concurrent</code> global variable.</p>
<p>You can use different providers for specific needs, for example, to run OS X builds or Raspberry Pi builds or other exotic kinds of builds. In the example I am keeping it simple and just setting many builds in the same provider (Digital Ocean).</p>
<p>And don&rsquo;t worry about the monthly fee for each machine. When used in this manner, you will be paying per hour.</p>
<p>Also, make sure you spun up all your machines (docker-registry, minio cache, CI runner) all with <strong>private networking enabled</strong> (so they talk through the internal VLAN instead of having to go all the way through the public internet) and that they are all in the same region data center (NYC1 is New York 1 - New York has 3 sub-regions, for example). Don&rsquo;t start machines in different regions.</p>
<p>Because we have Docker proxy/cache and Minio/S3 cache, your builds will take longer the first time (let&rsquo;s say, 5 minutes), and then subsequent builds will fetch everything from the cache (taking, let&rsquo;s say, 1:30 minute). It&rsquo;s fast and it&rsquo;s convenient.</p>
<h3>Setting up each Project for the Runner<span class="hx:absolute hx:-mt-20" id="setting-up-each-project-for-the-runner"></span>
    <a href="#setting-up-each-project-for-the-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Runner is one of the newest pieces of the GitLab ecosystem so you might have some trouble at first to figure out a decent configuration. But once you have the whole infrastructure figured out as described in the previous section, now it&rsquo;s as easy as adding a <code>.gitlab-ci.yml</code> file to your root directory. Something like this:</p>
<p>&mdash; yaml</p>
<h1>This file is a template, and might need editing before it works on your project.</h1><p>image: codeminer42/ci-ruby:2.3</p>
<h1>Pick zero or more services to be used on all builds.</h1><h1>Only needed when using a docker container to run your tests in.</h1><h1>Check out: <a href="http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service"target="_blank" rel="noopener">http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service</a></h1><p>services:</p>
<ul>
<li>postgres:latest</li>
<li>redis:latest</li>
</ul>
<p>cache:
key: your-project-name
untracked: true
paths:
- .ci_cache/</p>
<p>variables:
RAILS_ENV: &rsquo;test&rsquo;
DATABASE_URL: postgresql://postgres:@postgres
CODECLIMATE_REPO_TOKEN: &ndash; your codeclimate project token &ndash;</p>
<p>before_script:</p>
<ul>
<li>bundle install &ndash;without development production -j $(nproc) &ndash;path .ci_cache</li>
<li>cp .env.sample .env</li>
<li>cp config/database.yml.example config/database.yml</li>
<li>bundle exec rake db:create db:migrate</li>
</ul>
<h2>test:
script:
- xvfb-run bundle exec rspec<span class="hx:absolute hx:-mt-20" id="--xvfb-run-bundle-exec-rspec"></span>
    <a href="#--xvfb-run-bundle-exec-rspec" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My team at <a href="http://www.codeminer42.com"target="_blank" rel="noopener">Codeminer 42</a> prepared a <a href="https://hub.docker.com/r/codeminer42/ci-ruby/"target="_blank" rel="noopener">simple Docker image</a> with useful stuff pre-installed (such as the newest phantomjs, xvfb, etc), so it&rsquo;s now super easy to enable automated builds within GitLab by just adding this file to the repositories. (Thanks to Carlos Lopes, Danilo Resende and Paulo Diovanni - who will be talking <a href="http://www.rubyconf.com.br/pt-BR/speakers#Paulo%20Diovani%20Gon%c3%a7alves"target="_blank" rel="noopener">about Docker at Rubyconf Brasil 2016</a>, by the way)</p>
<p>GitLab-CI even supports building a pending Merge Request, and you can enforce the request so it can only be merged if builds pass, just like in GitHub + Travis. And as Code Climate is agnostic to Repository host or CI runner, you can easily integrate it as well.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/551/big_Settings___Codeminer42___CM-Fulcrum___GitLab.png" alt="Project Force Successful Build to Merge"  loading="lazy" /></p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The math is hard to argue against: the GitLab + GitLab-CI + Digital Ocean combo is a big win. GitLab&rsquo;s interface is very familiar so users from GitHub or Bitbucket will feel quite at home in no time.</p>
<p>We can use all the <a href="https://about.gitlab.com/2014/09/29/gitlab-flow/"target="_blank" rel="noopener">Git flows</a> we&rsquo;re used to.</p>
<p>GitLab-CE is still a work in progress though, the team is increasing their pace but there are currently more than <a href="https://gitlab.com/gitlab-org/gitlab-ce/issues"target="_blank" rel="noopener">4,200 open issues</a>. But as this is all Ruby on Rails and Ruby tooling, you can easily jump in and contribute. No contribution is too small. Just reporting how to reproduce a bug is help enough to assist the developers to figure out how to improve faster.</p>
<p>But don&rsquo;t shy away because of the open issues, it&rsquo;s fully functional as of right now and I have not found any bugs that could be considered show stoppers.</p>
<p>They have many things right. First of all, it&rsquo;s a &ldquo;simple&rdquo; Ruby on Rails project. It&rsquo;s a no-frills front-end with plain jQuery. The choice of HAML for the views is questionable but it doesn&rsquo;t hurt. They use good old Sidekiq+Redis for asynchronous jobs. No black magic here. A pure monolith that&rsquo;s not difficult to understand and to contribute.</p>
<p>The APIs are all written using Grape. They have the <a href="https://gitlab.com/gitlab-org/gitlab-ce"target="_blank" rel="noopener">GitLab CE</a> project separated from other components, such as the <a href="https://gitlab.com/gitlab-org/gitlab-shell"target="_blank" rel="noopener">GitLab-Shell</a> and <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab-CI-Multi-Runner</a>.</p>
<p>They also forked <a href="https://gitlab.com/gitlab-org/omnibus-gitlab"target="_blank" rel="noopener">Omnibus</a> in order to be able to package the CE Rails project as a &ldquo;.deb&rdquo;. Everything is orchestrated with Docker. And when a new version is available, you only need to <code>apt-get update &amp;&amp; apt-get upgrade</code> and it will do all the work of backing up and migrating Postgresql, updating the code, bundling in new dependencies, restarting the services and so forth. It&rsquo;s super convenient and you should take a look at this project if you have complicated Rails deployments into your own infrastructure (out of Heroku, for example).</p>
<p>I am almost done moving hundreds of repositories from both BitBucket and GitHub to GitLab right now and the developers from my company are already using it on a daily basis without any problems. We are almost at the point where we can disengage from BitBucket, GitHub and external CIs.</p>
<p>You will be surprised how easily your company can do it too and save a couple thousand dollars in the process, while having fun doing it!</p>
]]></content:encoded><category>version-control</category><category>devops</category></item></channel></rss>