Picture of Patrick

My Ideas

Stuff I found interesting

Curbing Cumbersome CHANGELOG Conflicts with Swift Argument Parser | Part 1

How I made the first tool that I use every single day.

Patrick Gatewood

6-Minute Read

CHANGELOG merge conflict

For the past year or so I’ve been working on an SDK. We maintain a fairly comprehensive CHANGELOG to keep our integrators up to date on our latest features and bugfixes. Until recently, this CHANGELOG was the source of dozens of hours of small, annoying merge conflict resolutions that slowed down our velocity. Here’s how I crafted a tool that I use every single day.

Identifying a Solvable Problem

Our merge conflict crisis started out simple: when I joined the team, I noticed that each PR always ended in a quick and dirty Update changelog commit or a rebase that removed the conflict. For quite awhile, I wrote this off as just part of being on this team.

Does this sound familiar? I’d encourage you to stop here for a moment and think about if you or your team have any similar issues. Let me know in a comment, and maybe you can use this article to help craft a similar tool to help your team focus on what’s important. You’ll be surprised at how much your team appreciates your gift.

What About Existing Tools?

One of the reasons I open-sourced the changelog generator is because I know this problem is not unique to my team. GitLab blogged about the very same problem and actually gave me most of the inspiration for my tool. Heck, the only reason I didn’t just use their tool was that it solved a very specific problem in the GitLab team’s workflow, and my team worked a bit differently. I wanted to make a slightly more generic tool that could be utilized by teams of varying workflows.

Enter Swift Argument Parser

I’ve written my share of bash scripts and other command-line tools, but they were always clunky and never tested. This time around I wanted to do right by the open-source community and make sure my tool was solid before casting it out into the world. The last thing in the world I also wanted was to soak up more of my team’s development cycles by introducing a buggy tool.

Swift Argumenet Parser is an open-source Swift package created by Apple to make parsing command-line arguments straightforward, type-safe, and to be honest, pretty fun.

Design

One of the things that caught my eye about Swift Argument Parser was how it uses your command line utility’s types to design the command line interface. This may not sound like much without some example code, but this makes designing elegant command line utilities a breeze, and the user interacts with the command line tool in a standard way that they expect.

I took major cues from git’s succinct command line interface. My intention was that the tool would both feel familiar and be easily understood. Since my project already followed the Keep a Changelog format, I already had my commands in mind (taken straight from the Keep a Changelog document):

  • Added for new features.
  • Changed for changes in existing functionality.
  • Deprecated for soon-to-be removed features.
  • Removed for now removed features.
  • Fixed for any bug fixes.
  • Security in case of vulnerabilities.

And I had our existing Changelog format that I wanted the tool to eventually output (GitHub-flavored Markdown):

Changelog Header. Contains some project-specific information.

## [1.0.0] - <release-date>

### Added
- Add something cool
- And something boring

### Changed
- Start following [Semantic Versioning](https://semver.org) properly 

### Fixed
- Fix all of `guy who used to work here`'s bugs

Now all I had to do was design the changelog tool’s commands and then actually implement the thing.

Commands

Boiled down into its simplest form, the changelog tool needed two subcommands: a log command to create a new entry, and a publish command to collect all the changelog entries and prepend them to the existing CHANGELOG.md.

Before digging into the subcommands, I created the changelog command. The building blocks of Commands created through the Argument Parser are ParsableCommand types. These types are simply declared, and the Argument Parser handles all the boilerplate code necessary to hook them up to the command line.

import ArgumentParser

public struct Changelog: ParsableCommand {
    public static let configuration = CommandConfiguration(
        abstract: "Curbing Cumbersome Changelog Conflicts.",
        discussion: "Creates changelog entries and stores them as single files to avoid merge conflictss in version control. When it's time to release, `changelog publish` collects these files and appends them to your changelog file.",
        version: "0.3.0",
        subcommands: [])
        
    public init() { }
}

As you can see, the Changelog command declaration is surprisingly simple. It also doesn’t do anything yet.

Log Subcommand

How does the tool actually prevent merge conflicts? The strategy I settled on was saving each changelog entry as an entry with a unique filename. This way, each changelog entry is purely additive, and there’s no interaction between a new entry and an existing one.

The meat of the Log command definition is fairly straightforward.

struct Log: ParsableCommand {
    @Argument(help: ArgumentHelp(
                "The type of changelog entry to create. ",
                discussion: "Valid entry types are \(EntryType.allCasesSentenceString).\n"),
                transform: EntryType.init)
    var entryType: EntryType // One of the types of changelog entries from the Keep a Changelog list above
    
    // Properties wrapped with the @Argument property wrapper are automatically parsed and type-checked for you
    @Argument(help: ArgumentHelp("A list of quoted strings separated by spaces to be recorded as a bulleted changelog entry.")
    var text: [String] = []
    
    // Errors thrown from the run() function cause your program to exit with a helpful message.
    public func run() throws {
        guard fileManager.fileExists(atPath: options.unreleasedChangelogsDirectory.path) else {
            throw ChangelogError.changelogDirectoryNotFound(expectedPath: options.unreleasedChangelogsDirectory.relativeString)
        }
        
        if text.isEmpty {
            try openEditor(editor, for: entryType)
        } else {
            try createEntry(with: text)
        }
    }
    
    private func createEntry(with text: [String]) throws {
        // save the entry to disk
    }
    
    private func openEditor(_ editor: String, for entryType: EntryType) throws {
        // opens vim or another text editor for interactive editing and save to disk
    }
}

I’ve omitted a few bells and whistles for simplicity’s sake. If you want, you can view the full Log command implementation.

Adding Log.self to the Changelog command’s subcommands array allows the tool to start logging changes. I also made the Log command the Changelog command’s defaultSubcommand by using another the CommandConfiguration initializer.

Now, the tool is actually functional. The user can run changelog fix "A ton of cool stuff" and a new changelog entry will be saved to disk.

Next up is to document how I created the publish command. This command also was where I really started focusing on the testability of the tool, which required a bit of a refactor to get around some Swift Package implementation details. If there’s anything in particular you’d like me to touch on in part 2, leave me a comment to let me know. Or, if you’d like to peek ahead and see the completed implementation, check out the changelog-generator repository!

Say Something

Comments

Recent Posts

Categories