In my 2020 vision, I mentioned that this year I wanted to contribute to an open source project and I am happy to announce that my first ever pull request was merged. This post mainly concerns my experience of the contribution process and my thoughts throughout rather than an in depth technical report.

When I embarked on this journey I had a few criteria that I was looking for in my contribution. Specifically, I wanted to contribute something meaningful to both me and others which meant that I wanted:

With those parameters I went through the Github issues of projects I liked and checked the “good first issue” labels until I stumbled upon this one in the repository for Singularity (a containerisation tool for high performance computing on Linux). The issue stood out to me, because I had also stumbled into the same headache as the author and it seemed to me to be a very achievable thing to fix.

At first, I was tempted to comment on the issue that I could look into it, but a teacher of mine once told me, “talk is cheap”, and so I decided to instead produce a solution first.

Doing My Homework

Although I had a vague idea of what the process was for contributing to open source projects, I also know that each project might have a subtly different workflow for contributions. As a result, I made sure to read the contribution guidelines for the project, and forked the repo.

The first thing to do was to ensure that I was able to actually compile the project from source, which meant understanding the build system and dependencies. Luckily, working with lots of build systems (and even writing my own makefiles) meant that it went smoothly and soon I had a workflow figured out.

Finding the Problem

At this point, I had reached the limit of my comfort zone. The project is written in Go which I had very little experience with having only completed the Tour of Go previously. Furthermore, I was not at all familiar with the codebase and so my first issue was finding where the problem could actually be solved.

Thankfully, I am a bit lazy and instead of reading through each and every file to find something I ended up using a recursive grep search to find the specific occurrence of the sanity check prompt.

$ grep -r "Build target already exists"

cmd/internal/cli/build.go: question := "Build target already exists. Do you want to overwrite? [N/y] "

So I dove into the build.go file and found the line where the prompt was being served, but if I was really clever I would have added the -n option to grep which would spit out the line number as well as the file it was in.

$ grep -rn "Build target already exists"

cmd/internal/cli/build.go:268: question := "Build target already exists. Do you want to overwrite? [N/y] "

Initial Solve

My first thought about solving this was to check if the build target file had the .def extension that denotes it as a build file. However, there were two big issues with this approach: it depended on the name of the file rather than the content, it required introducing the os package to the file which I thought was unnecessary.

Looking through the file for tools I could use I came across the following method parser.IsValidDefinition(path string) which would judge a build target file on its content rather than its name. Furthermore, it incorporated the internally built tools to check definition files. This lead me to the following diff:

 1diff --git a/cmd/internal/cli/build.go b/cmd/internal/cli/build.go
 2index fe02643162..558a7a2bfc 100644
 3--- a/cmd/internal/cli/build.go
 4+++ b/cmd/internal/cli/build.go
 5@@ -265,7 +265,17 @@ func checkBuildTarget(path string) error {
 6 			return fmt.Errorf("only sandbox update is supported: %s is not a directory", path)
 7 		}
 8 		if !buildArgs.update && !forceOverwrite {
 9-			question := "Build target already exists. Do you want to overwrite? [N/y] "
10+
11+			var question string
12+
13+			isDefFile, _ := parser.IsValidDefinition(path)
14+			if isDefFile {
15+				question = "Build target is a valid definition file that will be overwritten. Do you still want to overwrite? [N/y]"
16+			} else {
17+
18+				question = "Build target already exists. Do you want to overwrite? [N/y] "
19+			}
20+
21 			input, err := interactive.AskYNQuestion("n", question)
22 			if err != nil {
23 				return fmt.Errorf("while reading the input: %s", err)

What followed was a bit strange for me. I pushed the new commit/branch to my fork with the commit message “Add build command sanity check to close #3502” with “#3502” being the reference to the issue in the main repo. I did not expect this to immediately reflect on the issue in the main upstream repository. So when I amended my commit because I forgot to ensure all the test cases, and builds passed, it also reflected multiple commits on the main issue conversation. Although unexpected, this is very useful for connecting commits to issues automagically.

Excited and thrilled, I opened a pull request and waited for a review.

Feedback Loop

The pull review process was also a novel experience for me. Initially I was worried that my changes would be outright rejected, and my PR tanked. Instead, there was a back and forth discussion between myself and David Trudgian — a maintainer for the project.

The conversation about how to address the problem resulted — at least in my view — in a solution that was greater than something I could have achieved by myself. I would incorporate suggested changes and neaten up my code which ended up in a total of three commits for the pull request. The review process felt very welcoming and collaborative.

Victory

With all suggestions incorporated, continuous integration and build tests passed, my PR finally passed review and was merged into the master branch.

While I believe that the contribution I made was definitely beneficial to future users of the software, I definitely walked away from this experience with a lot more insight. I experienced the power of the review process, and that contributing to open source is not only achievable, but exciting and enjoyable.

I definitely plan on making more contributions to Singularity in the future, although I suspect not all of them will warrant a full blog post.