This week, my most significant contribution yet to the GitLab project was merged into master.

The contribution adds the ability for GitLab Pages websites to be served only over HTTPS connections, with plain HTTP requests 301-redirected to their secure counterpart.

The changes required to make this work were not trivial, and actually required two separate patches to be merged: one in Ruby and a second in Go.

Here’s a random sampling of some things that I learnt during the development process.

GitLab is a fun project to hack on

The GitLab application consists of a Rails monolith plus a number of secondary services, including a dedicated HTTP server for GitLab Pages which is written in Go.

GitLab Pages websites are typically auto-generated by the GitLab CI/CD system. The artifacts of this process are static website assets, such as HTML files, which are served by the Pages server.

Implementing HTTPS-only pages required two separate changes to the overall application.

Firstly, the Rails monolith had to be altered to write additional metadata to a per-project configuration file, indicating whether the project’s Pages website should be forced to HTTPS or not. Supporting this were the user interface changes to allow a user to enable or disable the behaviour, and the usual layers of automated testing.

Secondly, the Go HTTP server had to be updated to parse the newly-added configuration, and adapt its behaviour accordingly (that is, serve a 301-redirect when appropriate).

I love the challenge of implementing a distributed feature across multiple technical domains. Being forced to consider the interaction between the services, error-handling, reliability and so on means that it’s possible to learn much more than the sum of the two individual pull requests.

Additionally, GitLab’s code quality in general feels very good. The Rails app is written in a modern style, with heavy use of service objects and comprehensive automated testing, including a very slick parallelized CI flow.

Finally, the GitLab community appears to be very professional and friendly, and contributing a significant feature was a very positive experience. Thanks guys!

Go is a nice language

Generally I distrust Google as a corporation, and this political bias had led me to be somewhat suspicious about Go as a language. This turns out to have been a fallacy, and it’s been great to dispel it.

Go is a much nicer language than I unfairly expected it to be. In its syntax, basic types and use of pointers it immediately reminded me of C, which made me feel comfortable very quickly.

I also found the Go toolchain to be very productive and enjoyable (although given Google’s investment in the ultra-slick Android toolchain this probably shouldn’t be a surprise). Being able to import modules directly from Git repositories, and auto-format code from the command-line are just two simple examples.

This project helped me to understand why Go has become so popular, and when it might be considered the right tool to choose in the future.

Parameterized RSpec tests are a great thing

When writing RSpec tests, it is common to start with this kind of pattern:

RSpec.describe "something" do
  before do
    send_request(path, "GET")
  end

  context "path is /" do
    let(:path) { "/" }

    it { is_successful }
  end

  context "path is /abc" do
    let(:path) { "/abc" }

    it { is_successful }
  end
end

This feels simple and reasonably readable, until a second contextual variable is introduced. Not only do we quickly see code-repetition being introduced, but the nesting also starts to feel unwieldy:

RSpec.describe "something" do
  before do
    send_request(path, method)
  end

  context "path is /" do
    let(:path) { "/" }

    context "method is GET" do
      let(:method) { "GET" }
      it { is_successful }
    end

    context "method is POST" do
      let(:method) { "POST" }
      it { is_not_successful }
    end
  end

  context "path is /abc" do
    let(:path) { "/abc" }

    context "method is GET" do
      let(:method) { "GET" }
      it { is_successful }
    end

    context "method is POST" do
      let(:method) { "POST" }
      it { is_not_successful }
    end
  end
end

Introducing even more contextual levels exacerbates the problem further.

Enter RSpec::Parameterzied, which allows the same examples to be defined using a simple, flat table instead:

RSpec.describe "something" do
  using RSpec::Parameterized::TableSyntax

  where(:path, :method, :success) do
    "/"    | "GET"  | true
    "/abc" | "GET"  | true
    "/"    | "POST" | false
    "/abc" | "POST" | false
  end

  with_them do
    before do
      send_request(path, "GET")
    end

    it "returns the expected response" do
      expect(response.success).to eq success
    end
  end
end

RSpec will now run each of the rows in the table as a separate example, with inputs corresponding to the column values.

Although a downside of this approach is that debugging the specs can be more challenging, in test-heavy applications with a lot of contextual variants this feels like a useful option.

Conclusions

Implementing HTTPS-only GitLab Pages has been a great learning experience, and a lot of fun! I’d highly recommend anybody who wants to hack on a non-trivial, backend-centric Rails app to check out the GitLab Contributing page and get involved.