Using GitHub actions and GitFlow to automate your release process

This blogpost describes an idea of using GitHub actions and GitFlow to automate away most of the steps that are involved in creating a new release. The end goal is to create a release of your software with only two steps:

  • Creating an issue
  • Merging a PR

Note that both of these can be done from within GitHub's web interface and without commandline interaction.
As a result, this strategy is very accessible and doesn't require deep understanding of the steps necessary to create a release.


This workflow assumes the following:

  • You are using GitFlow as your branching model
  • Your code is hosted on GitHub and you have access to GitHub actions
  • You have a CI system that builds and tests pushed branches/commits


GitFlow is a branching model for Git that makes fairly liberal use of branches. For people unfamiliar with GitFlow, here is a quick primer:

  • Your main branch is called 'develop' or 'dev'. This is where the work happens.
  • All code-changes need to branch of 'develop' and merge back into it after code-review. These are called 'feature' branches.
  • To draft a release, we branch of a 'release/{version}' branch.
  • To finalize a release, the release branch is merged into 'master'. The resulting merge-commit is tagged with a tag.

There are more details to it in regards to hot fixes but for the our case, these don't matter.
What is interesting about GitFlow is that through this particular use of branches, we are very explicit about what we are currently doing and what is supposed to happen next.
This allows us to build automation around events that happen in our CI system.

GitHub actions

GitHub actions are a way of executing custom code as a result of events happening on GitHub.
They use the same event system as GitHub's webhooks, hence there is a whole bunch of them, for almost anything that happens on Github.

We are going to use the events triggered through our use of GitFlow to automate our release process with GitHub actions.

CI configuration

Your CI system should be configured to:

  • build and test your dev and feature/* branches
  • build and test release branches (those starting with release/)1

A fully automated release workflow

In GitFlow, making a new release typically involves the following steps:

  1. We create a new release branch, update the changelog, bump version in manifest files, make a commit and push it
  2. We open a pull request against the 'master' branch (remember that we branched of 'dev')
  3. Our CI system builds the software in release-setting
  4. We merge the pull request if everything still works
  5. We merge the release branch back into dev
  6. We tag the merge commit on the master branch with the new version
  7. We publish the new version to a package registry or deploy it somewhere if it is an application

Here is how we are going to automate this:

To trigger the automated workflow, we are going to open an issue on our repository with a title like 'Release version 0.5.0'. We create a GitHub workflow that listens on the IssueCreated event and:

  1. checks out the current HEAD of 'dev'
  2. extracts the version from the issue title
  3. creates a new release branch: release/0.5.0
  4. updates the changelog2
  5. bumps versions in the manifest files
  6. makes a commit
  7. pushes the commit to origin
  8. opens a pull request and requests reviews

Opening the pull request will trigger our CI system and build and test the software3. If everything works out, we approve and merge the release branch. We create a 2nd GitHub workflow that listens on 'closed' events for pull requests that target the 'master' branch. In GitFlow, this only happens for releases (and hotfixes). This GitHub workflow will:

  1. Checkout the current master branch
  2. Create a new release on GitHub (this also creates a tag)
  3. Build desired release artifacts and upload/deploys them
  4. Finally, the same GitHub workflow will create a pull request for merging the release branch back into dev.

You can see all of this in action here: github-action-gitflow-release-workflow


In GitFlow, hotfixes are (usually urgent) changes to a version of the code that is currently running in production. This is achieved by checking out the tag that is associated with the affected version (f.e. 1.5.0) and branching of a new hotfix branch (hotfix/1.5.1).
On this branch, the hotfix is implemented, tested and eventually merged into master and dev.

In terms of automation, there is little difference between a hotfix and a release branch being merged into master. The example repository only makes a difference here in how the version is extracted from the branch name.


Releasing a new version is something everyone on the team should be able to do. You never know, which people will leave the team permanentely or might just be not available, yet an urgent bugfix needs to be released.

A branching model like GitFlow already helps to standardize, what needs to be done in such a situation. Automating as much as possible empowers your team members even more. In our case, all they need to do is create an issue with a specific title and approve the automatically created pull request. That's it!


Why do we need to use GitFlow for this automation to work?

The impact of GitFlow is fairly subtle, yet important. What we need is a distinct event that triggers the actual release. In GitFlow, that is the merge of a release branch into master. Without the distinction between master and develop, releasing the merge-commit of a release branch into master might accidentially increase the scope of the release if any other pull request gets merged into master after the release branch was branched of.

With GitFlow, release branches are always branched of develop and merged into master. This guarantees that the scope of a release is always the diff between the last release and the HEAD of develop at the time the release branch is branched of.

  1. Depending on your software, you might want to run a 'release build' in this scenario. For example in Rust, cargo has a switch --release. Your CI should be configured to use such functionality when building a release/ branch 

  2. I am assuming here that we are using keep-a-changelog. With keep-a-changelog, the entries are written together with the code changes under an 'Unreleased' section. To create a new release, we only need to rename this section to reflect the new version. 

  3. It is important to note that GitHub actions does not trigger events for actions that have been performed by a GitHub workflow. A GitHub workflow that pushes a branch (like in our case) will not trigger the push event. Unfortunately, that means we cannot use GitHub actions to build your release/* branches and we will have to use a 3rd party CI system like Travis, or CircleCI. 

Generic Newtypes: A way to work around the orphan rule

Rust's orphan rule prevents us from implementing a foreign trait on a foreign type. While this may appear limiting at first, it is actually a good thing and one of the ways how the Rust compiler can prove at compile time that our code works the way we intended.

This blog post is a follow-up on one that I already wrote some time ago:
In this one, we will go more in-depth into the "local wrapper type" idea and rebrand it as "generic newtypes".

An example use case

Imagine we have an application that allows us to manage a database of users via an HTTP API. At the core of our application, we are dealing with a User struct that for now tracks only the name and the user's signup date.

use chrono::prelude::*;

pub struct User {
    name: String,
    signedup_on: DateTime<Utc>

While this is a fairly simple type, we will establish some constraints now that should help demonstrate of how the pattern of generic newtypes is applicable and helpful in other, more complicated scenarios.

The constraints are:

  • Our struct User most not directly implement Serialize.

This could be the case if the crate this struct is defined in should not have any dependencies or only selected ones. Many crates in the ecosystem already expose a serde feature-flag that gives you some serialization implementation. In my opinion, this is sub-optimal because it does not really scale in the long run. What about libraries like diesel? diesel provides similar traits to serde with FromSql and ToSql. Should crates also give you a diesel feature flag in case you want to use those types directly in a database schema? Testing libraries like quickcheck are another example. As the Rust ecosystem grows, this list will likely grow.

  • The serialisation provided by chrono's serde feature flag does not meet our requirements.

This is easily imaginable for any datatype that is not defined in some specification.1 Even if it is, the crate might just not expose a serde feature flag and we are left with writing our own implementation of Serialize anyway.

Existing solutions

1. Create a regular newtype

We can create a newtype for DateTime<Utc> like this:

pub struct SignupDate(pub DateTime<Utc>);

Now that newtype might probably be useful in and of itself, even without the idea I am trying to sketch out here. Independently though, we now have a type that is local to our crate and we can implement Serialize on it: impl Serialize for SignupDate.

The downside of this approach is that we need to create a newtype for every type. Even though there is no runtime cost associated with that thanks for Rust's zero-cost abstractions, we have to write those newtypes which can be tiring, depending on how many there are. In addition, naming those newtypes can become tricky if their only purpose is to allow trait implementations: SerdeSignupDate is kind of a weird name. The bottom line is that while creating newtypes works, it is not ideal.

2. Create a serde module

We can create a module that exposes dedicated serialize and deserialize functions and then use this module to instruct serde, how to serialize/deserialize our type:

use chrono::prelude::*;

pub struct User {
    name: String,
    #[serde(with = "serde_signup_date")]
    signedup_on: DateTime<Utc>

where serde_signup_date is the module exposing the serialize and deserialize functions.

The downside of this approach is that you have to repeat yourself on all the call sites: i.e. every struct that contains our signedup_on field will need to use this attribute. Another constraint is that it only works as long as you don't use any type parameters for the field you are trying to annotate with #[serde(with)]:

pub struct Foo<T> {
    #[serde(with = "...")] // how are the functions in the module supposed to know how to serialize `T`?
    bar: T

Generic newtypes

Instead of repeating ourselves in creating newtypes, we can define a reusable, generic newtype:

pub struct Http<T>(pub T);

Similar to regular newtypes, this allows us to implement Serialize:

impl Serialize for Http<DateTime<Utc>> {
 // ...

Later, we would use it like this:

use chrono::prelude::*;

pub struct User {
    name: String,
    signedup_on: Http<DateTime<Utc>>

What is cool about this?

  1. We only have to define one newtype that can be reused all over the place.
  2. It actually reads fairly nicely: no more awkward names for newtypes.
  3. It works with type parameters: we simply have to use Http<Bar> instead of just Bar:
pub struct Foo<T> {
    bar: T

let instance: Foo<Http<Bar>> = Foo {
    bar: Http(Bar(...))

Going back to our User example, we had one more constraint that we ignored so far, that is: User by itself must not implement Serialize.
Since we have Http<T>, should we just do: impl Serialize for Http<User>?

In short: no. We made bad experiences doing that.
The reason is because User is a record type2 (i.e. it is a type with named fields).
In order to serialize this one, we would need to use serde's features of serialize.serialize_struct() and state all the fieldnames ourselves. This is actually fairly repetitive and exactly the reason why there is #[derive(Serialize, Deserialize)]. How can we leverage this functionality without breaking our constraint?

Our API is the contract we have with our clients. I would consider it to be a good practise to explicitly define this contract in the code. Hence, I recommend to create dedicated types for the wireformat:

#[derive(Serialize, Deserialize)]
pub struct UserResponse {
    name: String,
    signedup_on: Http<DateTime<Utc>

As we can see, things fall neatly into place. We can leverage serde's custom derive for our structural type and at the same time, simply wrap the types that don't define a Serialize implementation with our Http newtype and we are good to go!

Converting between UserResponse and User is fairly trivial as all we need to do is wrap the DateTime<Utc> in our Http<T> newtype.


Instead of repetitively creating specific newtypes, we can create generic newtypes for certain contexts in our applications like API modules. This allows us to work around the orphan rule while not taking too much of an ergonomic hit. In combination with dedicated types for the wire format, we can leverage a lot of serde's functionality.

The key here is to be as modular as possible: Ideally, you want to use the generic newtype for things that serialize to a single value like a JSON string or number. Then you create specific wire types for the messages you are exchanging with your clients and use the generic newtype so that you are able to derive Serialize.


Comments or ideas?
Post them to the /r/rust thread:

  1. I guess using DateTime for this example is kind of bad because there is actually a well-defined serialization for these data types in ISO6801. 

  2. A previous version of this post used the term "structural type". Thanks to /u/thristian99 for suggesting a different term to avoid confusion. 

Rust's custom derives in a hexagonal architecture: Incompatible ideas?

This blog post is a manifestation of a problem that has been floating around in my head for quite a while now. It is about the seemingly incompatible idea of fully embracing Rust's custom derive system in an application that puts a strong focus on a hexagonal architecture.

To discuss this problem, I am going to first write about both concepts individually. Feel free to skip over those sections if you are already familiar with the topics. The blog post finishes off with some ideas on how Rust could be extended to better support these kind of usecases.

Hexagonal architecture

The concept of a hexagonal architecture is known under a variety of different names. Some people refer to it as onion architecture whereas others like to call it "ports and adaptors"1. Independent of the name, the idea is always the same: embracing the principle of Dependency Inversion. Or, the way Uncle Bob calls it: Clean Architecture.

In a nutshell, you want to model the "core" of your problem domain (your "business logic") in a way that it is ignorant about the rest of the system

  • Is your system invoked via an HTTP API or in a CLI? Your core doesn't care.
  • Is the state persisted in an SQL database or just held in memory? Your core doesn't care.

Note: With system, I am always referring to a single runtime component (one application) and the core is just a module that (for most languages) only exists at the source-code level.

What is the point of such a modularisation? The examples already provided quite a strong hint: It is great at separating concerns and has many interesting implications down the road. If the code that models your problem domain is independent from (as in: the core module does not depend on) the rest of the code base:

  • you can compile it very fairly quickly: Fast compile times allow for many iterations and hence faster feature development.
  • you can (unit-)test it in isolation
  • you can reason about the critical behaviour of the system in isolation
  • you can port it to other runtime environments

There is probably many more things that could be mentioned and they also kind of overlap at certain points. The key takeaway for me is simplicity, which is one of the crucial points of why we as developers are coming up with abstractions and architectures.

A hexagonal architecture is a simple architecture. Not only because it separates concerns but also because it allows you to defer some decisions to a later stage in the development of a system2. The project I am currently working on still stores things in-memory and we've been developing for over a year now. It is actually fully functional but just doesn't persist the state between restarts (yet).

Hexagonal architecture in Rust

That project is actually written in Rust, which turned out to be an excellent choice for the problem domain. It is also the reason I am writing this blog post because we've been trying to embrace a hexagonal architecture and we've hit some problems with it.

Overall, Rust has a very well thought-through module system:

  • Symbols are private by default and need to be explicitly exported
  • Transitive dependencies are not exposed: A dependency on module B in module A is not leaked through to C (if you don't use it in a public signature)

This allows for a sophisticated modularisation even within a single crate which is what Rust uses to package up and distribute libraries. What is the trouble then?

Rust has a clear separation of data and behaviour. Data is stored inside structs whereas behaviour is attached to structs by implementing traits on them. If there is an automated way to implement a certain trait (like creating a printable representation for debugging purposes through Debug) Rust allows you to "derive" the implementation and thereby having it generated for you by the compiler. This will, and that is the important part, add source code at compile-time in the same module as the declared struct. Here is an example:

#[derive(Debug)] // <-- Instruction to derive in implementation of the `Debug` trait for the struct `Person`
struct Person {
   name: String

// <-- Implementation of `Debug` is going to be generated here at compile time:
// impl Debug for Person {
//    ...
// }

One of the most popular crates in the Rust ecosystem is serde. It allows you to implement (and derive) implementations of the Serialize and Deserialize traits which can then be used to serialize an instance of a struct into a variety of formats (JSON, YAML, XML, etc) and also deserialize into an instance from any of these. As with all custom derives, those implementations are generated next to the actual struct definition.

You might already have an idea of where I am going with this ...

Let's recap:

In a hexagonal architecture, we want our core module to be independent of the other aspects of a system. If we are building an HTTP API, we don't want the code in our core module to know about that. For example, if any of those types need to be serialized, the code for serializing them should not be within the core module. It should be in an http module or something like that. The stress test for whether this requirement is fulfilled is always: if I would extract the core module in its own project without any of the other stuff being present, would it still compile? As soon as we start to derive Serialize on any of our core types, this is no longer given unless we would add serde as a dependency to that project.

Note: serde by itself is very well designed and is actually separated into the generic serialization library and concrete formats like json or yaml so it might not be so bad to depend on serde but I hope the examples still communicates the point I am trying to make.

What is the alternative?

We can obviously always go and implement the trait ourselves in whichever module we want:

// In our "core" module

struct Person {
   name: String
// In our "http" module
use serde::Serialize;
use core::Person;

impl Serialize for Person {

This has two downsides:

  1. It is tedious and error prone to implement the serialization code yourself, especially if you want the exact same one that would already be provided by serdes custom-derive. Also, serde is very configurable, so it very likely that, even if you have special requirements for the serialization, it is probably gonna support it in a way so that you don't have to handroll your own implementation.
  2. It actually doesn't survive our "stresstest" because of Rust's orphan rules. If we move Person to its own crate, neither Serialize nor Person are local to the crate that hosts the http module and hence, declaring this implementation will not compile.

Possible solutions

Let's try to workaround those to problems. In the end, we want to achieve the following:

  • Having the host module of Person be free of any serialization code and the resulting dependencies
  • Being able to serialize an instance of Person in another module

Solution 1: Create a new type

Creating types is cheap in Rust thanks to zero-cost abstractions. We can therefore define a new type: HttpPerson that lives in the http module. This one will mirror the fields of Person and derive Serialize. The only thing we have to do is convert between HttpPerson and Person. Yeah!

Well, depending on how complex our real-world data structures are, this can be quite a tedious and also error prone task. Also, let's not forget the mental load (why are there two, seemingly identical structs?) that would come with such an approach. If you are writing software for a complex business domain, you shouldn't make the code any harder to understand than it already is. In his book Domain Driven Design, Eric Evans suggests several patterns on how to design software for complex domains. One important technique is to reduce the mental mapping between the business domain and the actual source code as much as possible. Having several data types that represent the same element of the business domain does not help with that, especially because we only introduced it because of a technical limitation of our tool.

Solution 2: Create a local wrapper type

Instead of creating a type that mirrors the implementation of Person, we can create a generic Http<T> struct:

struct Http<T>(T);

This one will be local to our crate and hence we are allowed to implementation Serialize like this:

use serde::Serialize;
use core::Person; // Imagine `core` being a crate instead of just a module

impl Serialize for Http<Person> {

This avoids the need for a type that mirrors the structure of Person but has the downside that we have to manually implement Serialize again.


This is the stage where I am currently out of ideas on how to proceed. Both solutions are sub-optimal and hard to justify just for the sake of the "stresstest". Obviously, if the core of your system already lives in another crate, you will have to roll with one of those anyway but if the modules still live in the same crate, you might just bend the rules a little and roll with #[derive(Serialize, Deserialize)].

The 2nd solution is currently my favourite if I'd have to go for one. Mainly for it's cleanliness of not having to re-define Person but also because I have the feeling, it should be possible (through changes to the language or other clever things one can do with Rust) to make Serialize easier to implement. The limitation we are currently hitting there is that macros are processed very early in the compile phase, hence they only have access to the source code and cannot resolve symbols. Declaring #[derive(Serialize)] will only receive the tokens of the declaration it is sitting on, which is the struct definition of Person in our case. I think it is therefore not possible to write a custom-derive that generates code based on some code somewhere else.

Extending custom-derives with symbol resolution

It would be nice if one could do like:

use serde::Serialize;
use core::Person;

// Imaginary syntax:
derive Serialize on Person;

and get access to the declaration of the Person symbol in the implementation of the custom-derive, no matter where it is actually defined. Orphan rules would still apply obviously. This would allow for some seriously powerful code generation and at the same time, keep concerns nicely separated.

Baking this into the custom-derive feature is probably not such a good idea though since derive is associated with annotating a struct. More generally expressed, I'd like to have a way of doing meta-programming in Rust that has access to symbol resolution kind of like reflection in languages such as Java and C#. This would allow for generating code like Serialize impls in a different module other than where the actual struct is defined.

Lexical trait implementations

Another feature, although completely orthogonal, that would nicely fit into hexagonal architectures are lexical trait impls. Currently, the Rust compiler enforces the so-called "orphan-rule" when it comes to trait implementations. Roughly summarized, it states that either the trait or the type that the trait is implemented on have to be local to current crate. This is to guarantee that no matter which crates are linked together, there is at maximum one implementation of a specific trait on a certain struct. This is because declaring the implementation of a trait on a struct is an element of a crate that is "exported". In other words, any piece of code that depends on this crate can use this implementation. If one could lexically scope trait implementations, the "orphan-rule" could be relaxed under certain circumstances. Imagine you could do the following:

use serde::Serialize; // Foreign trait
use core::Person; // Foreign struct

impl Serialize for Person {


Or with a different syntax:

use serde::Serialize;
use core::Person;

pub(crate) impl Serialize for Person {


In the above scenario, Serialize as-well as Person are types foreign to the current crate. However, the impl blocks are marked/annotated as private to the current crate. Hence, no code outside of the current crate is affected by this implementation because there is an unambiguous way of selecting which functionality should be called: invoking Serialize within the current module will always use the local implementation. In a way, this would a form of specialisation.

Wrapping up

A hexagonal architecture allows for a clean separation of concerns within a codebase. At the current state, embracing such an architecture in a Rust code base to its fullest causes some friction with how certain things like custom-derives in the Rust ecosystem work. I am super excited about seeing Rust evolve and tackle problems like these!


Comments or ideas?
Post them to the /r/rust post:

Refactoring is a developer's business

Recently, I read a thread on Twitter about the need for developers to learn the skills of refactoring. In that thread, Ron Jeffries, one of the founders of the Agile Manifesto, claims that without tools like refactoring, agile projects cannot work because they are inherently about being able to change fast, which is hard if your design doesn't allow this. Refactoring in turn makes sure your design incorporates the gained knowledge as you iterate. Or as Kent Beck puts it: First make the change easy, then make the easy change!

The interesting bit about this thread is actually one of the responses. It mentioned the book "The mythical man month" and how project managers and managers in general never seem to read it although they would often greatly benefit from doing so.

Spoiler alert: While it is definitely an interesting read, developers shouldn't blame project managers for not being able to refactor the codebase or get rid of technical debt.

Refactoring or more precisely the desire to refactor parts of a software can be a point of friction between developers and project managers. The reason being is that it is often considered a waste of time. When being asked for time to refactor some code, a manager might ask: "why should I pay you for something that doesn't make a difference to me?" To some degree, they are right. From the outside, it is often hard to notice that a refactoring happend. And that is good! After all, refactoring is changing the code without changing the functionality. This means we actually don't want the changes to be visible to the outside.

Why do we, as developers, then want to make those changes?

Software always operates on a model of the real world. Because of that, some things are left out and other's may be represented in a simplified manner. As our software grows and new features are added, there is a good chance that the old models no longer fit. For some time, we might be okay with working around that. But as time passes, those workarounds get more and more awkward until we reach the point where more drastic changes are needed.

If you've worked over a longer period of time on a single software, you have very likely witnessed such a situation. While remembering that experience, pause for a moment and ask yourself: who on the project was aware of these awkward things? There is a good chance it was only 1 or 2 developers on the team, maybe a 3rd one who fixed an urgent bug once because the other two were on vacation. However, I'll go ahead and claim that the project manager is very likely not aware of them. You might have told them but that doesn't mean they understand what or why things are awkward. Reason being is that they probably don't know how to code and thus simply have a hard time relating to this problem. They don't see the problem and that is fine. They have different responsibilities on the project and thus other problems to deal with. Problems you probably cannot relate to or don't want to deal with.

As a software developer, you are the one who is concerned with the software design. After all, you (or your colleagues) created it. If it has flaws, it is your problem. A flaw in the design doesn't mean you are a bad developer. It just means that the circumstances changed. What may have seemed like a good idea 3 months ago, can be totally invalid today. However, why concern someone else with that? Especially, why concern someone who is not an expert on this field like your project manager or a customer?

It is your job as a software developer to understand what the business requirements are and translate them into code1. Any abstraction, any trade-off you make as part of doing that is your business. If the requirements change, you change the code. If abstractions no longer fit, change them aswell. It is part of your daily work. You shouldn't ask for dedicated time to do refactoring. This will only make it appear as something optional where in reality it is not. Don't put refactoring on the backlog. Just do it as part of your daily work.

There is another side of the story though. Developers do enjoy a lot of trust. After all, unless you are a developer yourself and watching closely, how can you tell if someone is actually working while sitting at a computer? Not only that but if you are working, are you working on the most important thing right now? This is something that you as the developer have to evaluate.

  • Is the gain of the refactoring worth the cost?
  • How valuable is this other feature compared to the refactoring?
  • Is creating an abstraction at this point worth the cost?
  • Am I sure that I understand the problem domain well enough, to know that the abstraction makes sense?

Only you can evaluate the gain of a refactoring since you are the expert on that matter. A manager will have a hard time evaluating the gain, so all they see is the cost. On the other hand, developers might have a harder time to see the cost and/or evaluate the business value of a feature. The missing bits of information make an informed decision hard.

As often, the key thing again is communication. As a developer, you should thrive to understand the business problem you are trying to solve. This will allow you to get a feeling for the priority and value of the features. You should also try and judge as objectively as possible whether a refactoring is actually necessary. Sometimes, a piece of code might be annoying but at least, it is well encapsulated, meaning it is not in the way. Unless it is actually causing you problems, there is little need to spend time on it. In addition, try to take little steps and refactor things piece-by-piece instead of doing one of those giant PRs again. Big rewrites are often a bad idea.

However, the most important thing to remember is: refactoring is your business, your concern. Don't make it someone else's by asking for permission.

  1. You are free to disagree with this definition.