From zero to hero on backporting pull requests

The Quarkus project moves fast and when we prepare bugfix releases, we usually have several dozens of pull requests to backport. The number of pull requests to backport is enormous and doing it via the GitHub UI is inconvenient and takes a lot of time (click PR, copy/paste commit hashes to cherry-pick, remove label, assign milestone, assign milestone to all fixed issues, next) plus some limitation of the UI (it’s not possible to sort by merge date to avoid conflicts, for example). So we decided to automate this work, and built an application. Of course, with Quarkus!

Before we dig into the solution, let me give you a quick explanation of our release process.

Release process

The Quarkus Team adopts a Major.Minor.Patch.Classifier (eg. 1.7.0.CR1, 1.7.0.Final) version pattern. Depending on the version bumped, different processes are adopted:

Major and Minor bumps

Our master branch is always ready for the next major or minor release. This process is usually very smooth and involves no backporting.

Patch or Classifier bumps

Whenever a new patched version (or a second CR) is about to be released in Quarkus, our Release team starts backporting commits from pull requests that were merged in the master branch. How do they know which pull request to grab? We have a triage/backport? label that our team add to pull requests for features that would be worth having it in the upcoming patch release.

How do we automate that?

The application basically queries a GitHub repository’s merged pull requests and closed issues (using the GraphQL API exposed by GitHub) containing a certain label and changes their milestones to one selected in the UI (and removing the certain label afterwards). Applying the changes introduced by these pull-requests (aka. cherry-picking) is simplified by providing a button next to each pull-request to copy the necessary git cherry-pick commands to the clipboard - we prefer this step to be done manually by the Release engineer.


Choosing a milestone


Choosing the pull requests to be backported


Our experience with Quarkus

Here you can find a summary of the extensions used in the backports application:

  • ArC (CDI): Manages the services lifecycle

  • MicroProfile Config: To externalize properties

  • RESTEasy: Exposes an endpoint the UI can consume. Also serves the UI using Qute’s Type-safe templates (see Qute).

  • Qute: Used on all templates (UI and GraphQL query payload)

  • Rest Client: To invoke the GitHub’s GraphQL endpoint

  • Cache: For caching open Milestones from the GitHub’s GraphQL endpoint

  • Hibernate Validator: For validating input

  • Jackson (JSON library): To parse the result from the GitHub GraphQL endpoint

Live Coding

The live coding is a killer feature in Quarkus and provided a quick feedback while classes and methods were changed during development. As a rule of thumb, always use ./mvnw quarkus:dev while developing a Quarkus application.

MicroProfile Config

Configuring which GitHub repository to use (to test or even for non-Quarkus repositories) and the GitHub authentication token (plus a different backport label if necessary) should be easy to configure without changing any source code. Quarkus uses Microprofile Config, so we externalized these properties. Quarkus also supports .env files, which we used while testing. This made local testing easier by not requiring us to change the directly or setting any environment or system property before running the application.


Qute is a template engine. We used it to generate the UI and to generate GraphQL queries where simply using GraphQL variables are not enough - for example, getting issue data from a list of issue numbers. We used Type-safe templates to generate the UI and the GraphQL queries.

Rest Client

GraphQL, in a nutshell, means POSTing some JSON data to an HTTP endpoint and parsing the result as a JSON document. Simply that. The Microprofile REST Client is a good option to perform this task, so we came up with this:


import io.vertx.core.json.JsonObject;

@RegisterRestClient(baseUri = "")
public interface GraphQLClient {

    JsonObject graphql(@HeaderParam(HttpHeaders.AUTHORIZATION) String authentication, JsonObject query);

In the GitHubService we can now consume the GraphQLClient object:

    GraphQLClient graphQLClient;

You can see how the client is invoked here

Next steps

Given the nature of dynamic queries, we decided to not use the SmallRye GraphQL extension, but that can be changed when the extension supports that feature.


This application took ~1 week to be developed (learning GraphQL included). That was made possible due to the following Quarkus team members:

  • 48 Guillaume Smet: For the beautiful frontend work

  • 48 George Gastaldi: For having fun developing the backend and the GraphQL integration

  • 48 David Lloyd: For the crazy regular expressions needed to extract issue numbers in commit messages.