diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/core/RegisteredSystems.java b/src/main/java/de/hftstuttgart/unifiedticketing/core/RegisteredSystems.java
index 62882a1832bbe1ea4659d0368b8321cd4f39d437..4332df7df84c6696644d33b5543e49e6a4aafb17 100644
--- a/src/main/java/de/hftstuttgart/unifiedticketing/core/RegisteredSystems.java
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/core/RegisteredSystems.java
@@ -1,5 +1,6 @@
 package de.hftstuttgart.unifiedticketing.core;
 
+import de.hftstuttgart.unifiedticketing.systems.github.GithubTicketSystemBuilder;
 import de.hftstuttgart.unifiedticketing.systems.gitlab.GitlabTicketSystemBuilder;
 
 import java.util.logging.Level;
@@ -26,6 +27,14 @@ public class RegisteredSystems
         return instance;
     }
 
+    /**
+     * Starts the builder mechanism for a GitHub connection
+     */
+    public GithubTicketSystemBuilder github()
+    {
+        return new GithubTicketSystemBuilder();
+    }
+
     /**
      * Starts the builder mechanism for a GitLab connection
      */
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/core/TicketSystem.java b/src/main/java/de/hftstuttgart/unifiedticketing/core/TicketSystem.java
index d3e3658de7ee943cd177dacd9da08d372da5c5c1..b44b57dc5dbcc30da1849f537b8bd419cc6bd997 100644
--- a/src/main/java/de/hftstuttgart/unifiedticketing/core/TicketSystem.java
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/core/TicketSystem.java
@@ -1,6 +1,7 @@
 package de.hftstuttgart.unifiedticketing.core;
 
 import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+import de.hftstuttgart.unifiedticketing.systems.github.GithubTicketSystem;
 import de.hftstuttgart.unifiedticketing.systems.gitlab.GitlabTicketSystem;
 
 import java.util.HashMap;
@@ -57,6 +58,9 @@ public abstract class TicketSystem<T extends Ticket, TS extends TicketSystem, TB
 
         switch (matcher.group(2))
         {
+            case "github":
+                return GithubTicketSystem.fromUri(matcher.group(3));
+
             case "gitlab":
                 return GitlabTicketSystem.fromUri(matcher.group(3));
 
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilter.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..673fe5f1c27ee87bd70008674d79d231d33873a2
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilter.java
@@ -0,0 +1,259 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hftstuttgart.unifiedticketing.core.Filter;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.core.TicketSystem;
+import de.hftstuttgart.unifiedticketing.exceptions.*;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class GithubFilter extends Filter<GithubTicket, GithubFilter>
+{
+    private final static Logger logger = Logging.getLogger(GithubFilter.class.getName());
+
+    protected final GithubTicketSystem parent;
+
+    protected GithubFilter(GithubTicketSystem parent)
+    {
+        this.parent = parent;
+    }
+
+    protected OkHttpClient getHttpClient() { return new OkHttpClient(); }
+
+    @Override
+    public List<GithubTicket> get()
+    {
+        ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        Request.Builder requestBuilder = new Request.Builder()
+            .url(parent.baseUrl)
+            .addHeader("accept", parent.acceptHeader)
+            .get();
+
+        if (parent.username != null && parent.apiKey != null)
+        {
+            requestBuilder.addHeader("Authorization", Credentials.basic(parent.username, parent.apiKey));
+            logger.log(Level.FINEST, "added token authentication header");
+        }
+
+        HttpUrl.Builder urlBuilder = requestBuilder.build().url().newBuilder();
+
+        for (Map.Entry<String, Object> mapEntry : setFilters.entrySet())
+        {
+            String f = mapEntry.getKey();
+            Object v = mapEntry.getValue();
+            try
+            {
+                if (f.equals(FilterNames.ASSIGNEEID.name()))
+                {
+                    logger.log(Level.WARNING, "assignee-id check only possible after request |" +
+                        " Filter: " + FilterNames.ASSIGNEEID.name());
+                } else if (f.equals(FilterNames.ASSIGNEENAME.name()))
+                {
+                    Set<String> names = (Set<String>) v;
+                    if (names.size() > 0)
+                    {
+                        urlBuilder.addQueryParameter("assignee", names.stream()
+                            .findFirst()
+                            .get());
+
+                        if (names.size() > 1)
+                        {
+                            logger.log(Level.WARNING, "assignee-name filter natively only for one name supported");
+                        }
+                    }
+                } else if (f.equals(FilterNames.DESCRIPTION_CONTAIN.name()))
+                {
+                    logger.log(Level.FINE, "Description contain check only possible after request |" +
+                        " Filter: " + FilterNames.DESCRIPTION_CONTAIN.name());
+                } else if (f.equals(FilterNames.DESCRIPTION_MATCH.name()))
+                {
+                    logger.log(Level.FINE, "Regex matching only possible after request |" +
+                        " Filter: " + FilterNames.DESCRIPTION_MATCH.name());
+                } else if (f.equals(FilterNames.IDS.name()))
+                {
+                    logger.log(Level.FINE, "Ticket id matching only possible after request |" +
+                        " Filter: " + FilterNames.IDS.name());
+                } else if (f.equals(FilterNames.LABELS.name()))
+                {
+                    urlBuilder.addQueryParameter("labels", ((Set<String>) v).stream()
+                        .reduce((l1, l2) -> l1 + "," + l2)
+                        .orElse(""));
+                } else if (f.equals(FilterNames.PAGE.name()))
+                {
+                    urlBuilder.addQueryParameter("page", String.valueOf(v));
+                } else if (f.equals(FilterNames.PAGINATION.name()))
+                {
+                    urlBuilder.addQueryParameter("per_page", String.valueOf(v));
+                } else if (f.equals(FilterNames.OPEN.name()))
+                {
+                    urlBuilder.addQueryParameter("state", ((boolean) v) ? "open" : "closed");
+                } else if (f.equals(FilterNames.TITLE_CONTAINS.name()))
+                {
+                    logger.log(Level.FINE, "title contain check only possible after request |" +
+                        " Filter: " + FilterNames.TITLE_CONTAINS.name());
+                } else if (f.equals(FilterNames.TITLE_MATCH.name()))
+                {
+                    logger.log(Level.FINE, "Regex matching only possible after request |" +
+                        " Filter: " + FilterNames.TITLE_MATCH.name());
+                } else
+                {
+                    logger.log(Level.WARNING, String.format("unrecognized filter key: %s", f));
+                }
+            }
+            catch (ClassCastException e)
+            {
+                logger.log(Level.SEVERE, "Filter with key "
+                    + f
+                    + " unexpectedly had type "
+                    + v.getClass().getName());
+                if (!this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR))
+                {
+                    throw new AssertionException(e);
+                }
+            }
+        }
+
+        requestBuilder.url(urlBuilder.build());
+
+        OkHttpClient client = getHttpClient();
+        Response response;
+
+        try
+        {
+            Request request = requestBuilder.build();
+            logger.log(Level.FINEST, String.format(
+                "created request:\n%s",
+                (this.parent.apiKey != null)
+                    ? request.toString().replace(this.parent.apiKey, "SECRET")
+                    : request.toString()
+            ));
+
+            response = client.newCall(request).execute();
+        }
+        catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format("get request FAILED with: %s", e.getMessage()));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpReqeustException(e);
+        }
+
+        if (response.code() >= 400)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "request failed with response code %d",
+                response.code()
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException(
+                String.format("ticket query failed, error response code: %d", response.code()),
+                response.code());
+        }
+
+        logger.log(Level.FINEST, "response received\n");
+
+        ResponseBody responseBody;
+        responseBody = response.body();
+
+        if (responseBody == null)
+        {
+            logger.log(Level.SEVERE, "query didn't deliver a body in response");
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException("ticket query failed, no response body", response.code());
+        }
+
+        List<GithubTicketResponse> tr;
+        try
+        {
+            tr = mapper.readValue(responseBody.bytes(), new TypeReference<List<GithubTicketResponse>>(){});
+
+            logger.log(Level.FINER, "parsed response body to ticketResponse list instance");
+            logger.log(Level.FINEST, String.format("found %d items pre post-filter", tr.size()));
+        } catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format("parsing query response FAILED with: %s", e.getMessage()));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new DeserializationException(e);
+        }
+
+        logger.log(Level.FINER, "starting query post filter");
+        Stream<GithubTicketResponse> ticketStream = tr.stream();
+
+        for (Map.Entry<String, Object> entry : setFilters.entrySet())
+        {
+            String f = entry.getKey();
+            Object v = entry.getValue();
+            try
+            {
+                if (f.equals(FilterNames.ASSIGNEEID.name()))
+                {
+                    ticketStream = ticketStream.filter(t ->  t.assignees.stream()
+                            .map(a -> String.valueOf(a.id))
+                            .collect(Collectors.toSet())
+                            .equals((Set<String>) v));
+                }
+
+                else if (f.equals(FilterNames.ASSIGNEENAME.name()) && ((Set) v).size() > 1)
+                {
+                    ticketStream = ticketStream.filter(t ->  t.assignees.stream()
+                        .map(a -> a.login)
+                        .collect(Collectors.toSet())
+                        .equals((Set<String>) v));
+                }
+
+                else if (f.equals(FilterNames.DESCRIPTION_CONTAIN))
+                {
+                    ticketStream = ticketStream.filter(t -> t.body
+                        .toLowerCase()
+                        .contains(((String) v).toLowerCase()));
+                }
+
+                else if (f.equals(FilterNames.DESCRIPTION_MATCH.name()))
+                {
+                    ticketStream = ticketStream.filter(t -> t.body.matches((String) v));
+                }
+
+                else if (f.equals(FilterNames.TITLE_CONTAINS.name()))
+                {
+                    ticketStream = ticketStream.filter(t -> t.title
+                        .toLowerCase()
+                        .contains(((String) v).toLowerCase()));
+                }
+
+                else if (f.equals(FilterNames.TITLE_MATCH.name()))
+                {
+                    ticketStream = ticketStream.filter(t -> t.title.matches((String) v));
+                }
+            } catch (ClassCastException e)
+            {
+                logger.log(Level.SEVERE, "Filter with key "
+                    + f
+                    + " unexpectedly had type "
+                    + v.getClass().getName());
+            }
+        }
+
+        logger.log(Level.FINER, "post-filter finished");
+
+        List<GithubTicket> ret = ticketStream.map(t -> GithubTicket.fromTicketResponse(parent, t))
+            .collect(Collectors.toList());
+        logger.log(Level.FINEST, String.format("remaining items: %d", ret.size()));
+
+        return ret;
+    }
+
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicket.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicket.java
new file mode 100644
index 0000000000000000000000000000000000000000..01c12b812275187e78fa25a4f10d89d666f9ea8c
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicket.java
@@ -0,0 +1,286 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.core.Ticket;
+import de.hftstuttgart.unifiedticketing.core.TicketSystem;
+import de.hftstuttgart.unifiedticketing.exceptions.DeserializationException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpReqeustException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpResponseException;
+import de.hftstuttgart.unifiedticketing.exceptions.SerializationException;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+public class GithubTicket extends Ticket<GithubTicketSystem, GithubTicket>
+{
+    private final static Logger logger = Logging.getLogger(GithubTicket.class.getName());
+
+    protected static GithubTicket fromTicketResponse(GithubTicketSystem parent, GithubTicketResponse response)
+    {
+        GithubTicket ret = new GithubTicket(parent);
+        ret.description = response.body;
+        ret.id = String.valueOf(response.number);
+        ret.open = response.state == null || response.state.equalsIgnoreCase("open");
+        ret.title = response.title;
+
+        if (response.assignees != null)
+        {
+            ret.assignees = response.assignees.stream()
+                .map(a -> new GithubTicketAssignee(a.id, a.login))
+                .collect(Collectors.toSet());
+        }
+
+        if (response.labels != null)
+        {
+            ret.labels = response.labels.stream()
+                .map(l -> l.name)
+                .collect(Collectors.toSet());
+        }
+
+        return ret;
+    }
+
+    protected GithubTicket(GithubTicketSystem parent)
+    {
+        super(parent);
+    }
+
+    @Override
+    public GithubTicket addAssignee(String identifier)
+    {
+        this.assignees.add(new GithubTicketAssignee(0, identifier));
+        this.updatedFields.add(FieldNames.ASSIGNEES.name());
+        return this;
+    }
+
+    @Override
+    public GithubTicket removeAssignee(String identifier)
+    {
+        this.assignees.removeIf(a -> Objects.equals(a.username, identifier));
+        this.updatedFields.add(FieldNames.ASSIGNEES.name());
+        return null;
+    }
+
+    protected OkHttpClient getHttpClient() { return new OkHttpClient(); }
+
+    @Override
+    public GithubTicket save()
+    {
+        if (updatedFields.size() == 0)
+        {
+            logger.info("No changed fields, no save required");
+            return this;
+        }
+
+        OkHttpClient client = this.getHttpClient();
+        ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        logger.log(Level.FINEST, String.format(
+            "[Ticket %s] preparing body for update request",
+            this.id
+        ));
+
+        ObjectNode body = mapper.createObjectNode();
+        for (String name : this.updatedFields)
+        {
+            if (FieldNames.ASSIGNEES.name().equals(name))
+            {
+                ArrayNode arrayNode = body.putArray("assignees");
+                this.assignees.forEach(a -> arrayNode.add(a.username));
+            }
+
+            else if (FieldNames.DESCRIPTION.name().equals(name))
+            {
+                body.put("body", this.description);
+            }
+
+            else if (FieldNames.LABELS.name().equals(name))
+            {
+                ArrayNode arrayNode = body.putArray("labels");
+                this.labels.forEach(arrayNode::add);
+            }
+
+            else if (FieldNames.OPEN.name().equals(name))
+            {
+                body.put("state", (this.open) ? "open" : "closed");
+            }
+
+            else if (FieldNames.TITLE.name().equals(name))
+            {
+                body.put("title", this.title);
+            }
+
+            else
+            {
+                logger.log(Level.WARNING, String.format(
+                    "[Ticket %s] unknown field %s will be ignored from update",
+                    this.id,
+                    name
+                ));
+                continue;
+            }
+
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] %s added to update",
+                this.id,
+                name
+            ));
+        }
+
+        logger.log(Level.FINEST, String.format(
+            "[Ticket %s] request body for update prepared",
+            this.id
+        ));
+
+        Request.Builder builder = new Request.Builder()
+            .url(String.format("%s/%s", this.parent.baseUrl, this.id))
+            .addHeader("accept", parent.acceptHeader);
+        try
+        {
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] serializing update request body",
+                this.id
+            ));
+
+            String bodyJson = mapper.writeValueAsString(body);
+
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] serialized JSON:\n%s",
+                this.id,
+                bodyJson
+            ));
+            builder
+                .patch(RequestBody.create(bodyJson, MediaType.get("application/json")));
+        } catch (JsonProcessingException e)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "[Ticket %s] serializing update request body FAILED!",
+                this.id
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new SerializationException(e);
+        }
+
+        if (parent.username != null && parent.apiKey != null)
+        {
+            builder.addHeader("Authorization", Credentials.basic(parent.username, parent.apiKey));
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] added token authentication header",
+                this.id
+            ));
+        }
+
+        Response response;
+        try
+        {
+            Request request = builder.build();
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] created request:\n%s",
+                this.id,
+                (this.parent.apiKey != null)
+                    ? request.toString().replace(this.parent.apiKey, "SECRET")
+                    : request.toString()
+            ));
+
+            response = client.newCall(request).execute();
+        }
+        catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "[Ticket %s] update request FAILED",
+                this.id
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpReqeustException(e);
+        }
+
+        if (response.code() >= 400)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "[Ticket %s] update request failed with response code %d",
+                this.id,
+                response.code()
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException(
+                String.format("ticket save failed, error response code: %d", response.code()),
+                response.code());
+        }
+
+        logger.log(Level.FINEST, String.format(
+            "[Ticket %s] response received",
+            this.id
+        ));
+
+        ResponseBody responseBody;
+        responseBody = response.body();
+
+        if (responseBody == null)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "[Ticket %s] update request didn't deliver a body in response",
+                this.id
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException("ticket save failed, no response body", response.code());
+        }
+
+        GithubTicketResponse ticketResponse;
+        try
+        {
+            ticketResponse = mapper.readValue(responseBody.bytes(), GithubTicketResponse.class);
+
+            logger.log(Level.FINEST, String.format(
+                "[Ticket %s] parsed response body to ticketResponse instance",
+                this.id
+            ));
+        } catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format("parsing update response FAILED with: %s", e.getMessage()));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new DeserializationException(e);
+        }
+
+        this.updatedFields.clear();
+
+        logger.log(Level.FINEST, String.format(
+            "[Ticket %s] update mark state reset",
+            this.id
+        ));
+
+        return GithubTicket.fromTicketResponse(this.parent, ticketResponse);
+    }
+
+    /**
+     * compares a given Object to this one, including all data fields.
+     * The normal {@link #equals(Object)} method does only compare Ticket Type and id,
+     * but not the data fields like this one.
+     * @param o Object to compare to this
+     * @return true if the given object is equal in terms of type, id and content to this
+     */
+    public boolean deepEquals(Object o)
+    {
+        if (!equals(o)) return false;
+        GithubTicket t = (GithubTicket) o;
+
+        return Objects.equals(title, t.title)
+            && Objects.equals(description, t.description)
+            && Objects.equals(labels, t.labels)
+            && Objects.equals(assignees, t.assignees);
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketAssignee.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketAssignee.java
new file mode 100644
index 0000000000000000000000000000000000000000..08e3257c1ddf536077962124aae87b38ea05b91f
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketAssignee.java
@@ -0,0 +1,11 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import de.hftstuttgart.unifiedticketing.core.TicketAssignee;
+
+public class GithubTicketAssignee extends TicketAssignee
+{
+    protected GithubTicketAssignee(int id, String username)
+    {
+        super(null, String.valueOf(id), username, null);
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilder.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c4b00aa6343fce51615183e0f2bed0679cfe265
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilder.java
@@ -0,0 +1,160 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.core.TicketBuilder;
+import de.hftstuttgart.unifiedticketing.core.TicketSystem;
+import de.hftstuttgart.unifiedticketing.exceptions.*;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class GithubTicketBuilder extends TicketBuilder<GithubTicketBuilder, GithubTicket, GithubTicketSystem>
+{
+    private final static Logger logger = Logging.getLogger(GithubTicketBuilder.class.getName());
+
+    protected GithubTicketBuilder(GithubTicketSystem parent)
+    {
+        super(parent);
+    }
+
+    protected OkHttpClient getHttpClient() { return new OkHttpClient(); }
+
+    @Override
+    public GithubTicket create()
+    {
+        logger.log(Level.FINEST, "starting Ticket creation from builder data");
+
+        ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        ObjectNode body = mapper.createObjectNode();
+
+        // title is mandatory field
+        if (this.title == null)
+        {
+            String msg = "mandatory field title not set before building ticket";
+            logger.log(Level.SEVERE, msg);
+
+            if (parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new AssertionException(msg);
+        }
+
+        if (this.description != null)
+        {
+            body.put("body", this.description);
+            logger.log(Level.FINEST, "description set");
+        }
+
+        body.put("title", this.title);
+        logger.log(Level.FINEST, "title set");
+
+        if (this.assignees != null)
+        {
+            ArrayNode arrayNode = body.putArray("assignees");
+            assignees.forEach(arrayNode::add);
+            logger.log(Level.FINEST, "assignees set");
+        }
+
+        if (this.labels != null)
+        {
+            ArrayNode arrayNode = body.putArray("labels");
+            this.labels.forEach(arrayNode::add);
+            logger.log(Level.FINEST, "labels set");
+        }
+
+        String jsonBody;
+
+        try
+        {
+            jsonBody = mapper.writeValueAsString(body);
+        } catch (JsonProcessingException e)
+        {
+            String msg = "json serialization FAILED";
+            logger.log(Level.SEVERE, msg);
+
+            if (parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new SerializationException(e);
+        }
+
+        OkHttpClient client = getHttpClient();
+
+        Request.Builder builder = new Request.Builder()
+            .url(parent.baseUrl)
+            .addHeader("accept", parent.acceptHeader)
+            .post(RequestBody.create(jsonBody, MediaType.get("application/json")));
+
+        if (this.parent.username != null && this.parent.apiKey != null)
+        {
+            builder.addHeader("Authorization", Credentials.basic(this.parent.username, this.parent.apiKey));
+            logger.log(Level.FINEST, "added basic auth header with api token");
+        }
+
+        Response response;
+        try
+        {
+            Request request = builder.build();
+            logger.log(Level.FINEST, String.format(
+                "created request:\n%s",
+                (this.parent.apiKey != null)
+                    ? request.toString().replace(this.parent.apiKey, "SECRET")
+                    : request.toString()
+            ));
+
+            response = client.newCall(request).execute();
+        } catch (IOException e)
+        {
+            logger.log(Level.SEVERE, "create request FAILED");
+
+            if (parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpReqeustException(e);
+        }
+
+        if (response.code() >= 400)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "create request failed with response code %d",
+                response.code()
+            ));
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException(
+                String.format("ticket creation failed, error response code %d", response.code()),
+                response.code());
+        }
+
+        logger.log(Level.FINEST, "response received\n");
+
+        ResponseBody responseBody;
+        responseBody = response.body();
+
+        if (responseBody == null)
+        {
+            logger.log(Level.SEVERE, "create request didn't deliver a body in response");
+
+            if (this.parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException("ticket creation failed, no response body", response.code());
+        }
+
+        GithubTicketResponse ticketResponse;
+        try
+        {
+            ticketResponse = mapper.readValue(responseBody.bytes(), GithubTicketResponse.class);
+            logger.log(Level.FINEST, "parsed response to ticketResponse instance");
+        } catch (IOException e)
+        {
+            logger.severe(String.format("parsing create response FAILED with: %s", e.getMessage()));
+
+            if (parent.getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new DeserializationException(e);
+        }
+
+        return GithubTicket.fromTicketResponse(parent, ticketResponse);
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponse.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..478036f212dc2d1d762ae17c6f354c229f8fe3cc
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponse.java
@@ -0,0 +1,24 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import java.util.Set;
+
+public class GithubTicketResponse
+{
+    public Set<Assignee> assignees;
+    public String body;
+    public int number;
+    public Set<Label> labels;
+    public String state;
+    public String title;
+
+    protected static class Label
+    {
+        public String name;
+    }
+
+    protected static class Assignee
+    {
+        public int id;
+        public String login;
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystem.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac791830060327a71ec65698fbd187b528e19347
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystem.java
@@ -0,0 +1,229 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hftstuttgart.unifiedticketing.core.*;
+import de.hftstuttgart.unifiedticketing.exceptions.*;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GithubTicketSystem extends TicketSystem<GithubTicket, GithubTicketSystem, GithubTicketBuilder, GithubFilter>
+{
+    private final static Logger logger = Logging.getLogger(GithubTicketSystem.class.getName());
+
+    protected final String acceptHeader;
+    protected final String apiKey;
+    protected final String username;
+
+    /**
+     * creates a new instance of this class from an uri.<br>
+     * <br>
+     * <b>Attention:</b> This method should not be called from the enduser!<br>
+     * Instead call the same method from {@link TicketSystem}.<br>
+     * To call this method directly, the uri has to be shortend by the {@code "unifiedticketing:github:"} part.
+     * @param uri gitlab specific part of full uri
+     * @return new github ticket system instance
+     */
+    public static GithubTicketSystem fromUri(String uri)
+    {
+        Matcher matcher = Pattern.compile("^((http|https):\\/\\/)?(.*?(:[0-9]+.*?)?)::(.*?):(.*?)(:(.*?):(.*?))?$").matcher(uri);
+
+        if (!matcher.matches())
+        {
+            String msg = "uri didn't match regex";
+            logger.log(Level.SEVERE, msg);
+            throw new AssertionException(msg);
+        }
+
+        GithubTicketSystemBuilder builder = new GithubTicketSystemBuilder();
+
+        if (matcher.group(2) != null && matcher.group(2).length() > 0)
+        {
+            if (matcher.group(2).equalsIgnoreCase("http")) builder.withHttp();
+            else builder.withHttps();
+        }
+
+        if (matcher.group(3) == null || matcher.group(3).length() == 0)
+        {
+            String msg = "no base url identified in uri";
+            throw new AssertionException(msg);
+        }
+        builder.withBaseUrl(matcher.group(3));
+
+        if (matcher.group(5) == null || matcher.group(5).length() == 0)
+        {
+            String msg = "no owner identified in uri";
+            throw new AssertionException(msg);
+        }
+        builder.withOwner(matcher.group(5));
+
+        if (matcher.group(6) == null || matcher.group(6).length() == 0)
+        {
+            String msg = "no repo identified in uri";
+            throw new AssertionException(msg);
+        }
+        builder.withRepo(matcher.group(6));
+
+        if (matcher.group(8) != null && matcher.group(9) != null)
+        {
+            builder.withAuthentication(matcher.group(8), matcher.group(9));
+        }
+        else logger.log(Level.INFO, "no authentication given, creating anonymous instance");
+
+        return builder.build();
+    }
+
+    protected GithubTicketSystem(String acceptHeader, String baseUrl)
+    {
+        this(acceptHeader, baseUrl, null, null);
+    }
+
+    protected GithubTicketSystem(String acceptHeader, String baseUrl, String username, String apiKey)
+    {
+        super();
+
+        this.acceptHeader = acceptHeader;
+        this.apiKey = apiKey;
+        this.baseUrl = baseUrl;
+        this.username = username;
+    }
+
+    /**
+     * starts builder process for new github ticket
+     */
+    @Override
+    public GithubTicketBuilder createTicket()
+    {
+        return new GithubTicketBuilder(this);
+    }
+
+    @Override
+    public GithubFilter find()
+    {
+        return new GithubFilter(this);
+    }
+
+    protected OkHttpClient getHttpClient() { return new OkHttpClient(); }
+
+    @Override
+    public GithubTicket getTicketById(String id)
+    {
+        ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        Request.Builder requestBuilder = new Request.Builder()
+            .url(String.format("%s/%s", baseUrl, id))
+            .addHeader("accept", acceptHeader)
+            .get();
+
+        if (username != null && apiKey != null)
+        {
+            requestBuilder.addHeader("Authorization", Credentials.basic(username, apiKey));
+            logger.log(Level.FINEST, "added token authentication header");
+        }
+
+        HttpUrl.Builder urlBuilder = requestBuilder.build().url().newBuilder();
+
+        requestBuilder.url(urlBuilder.build());
+
+        OkHttpClient client = getHttpClient();
+        Response response;
+
+        try
+        {
+            Request request = requestBuilder.build();
+            logger.log(Level.FINEST, String.format(
+                "created request:\n%s",
+                (apiKey != null)
+                    ? request.toString().replace(apiKey, "SECRET")
+                    : request.toString()
+            ));
+
+            response = client.newCall(request).execute();
+        }
+        catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format("get request FAILED with: %s", e.getMessage()));
+
+            if (getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpReqeustException(e);
+        }
+
+        if (response.code() >= 400)
+        {
+            logger.log(Level.SEVERE, String.format(
+                "request failed with response code %d",
+                response.code()
+            ));
+
+            if (getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException(
+                String.format("ticket query failed, error response code: %d", response.code()),
+                response.code());
+        }
+
+        logger.log(Level.FINEST, "response received\n");
+
+        ResponseBody responseBody;
+        responseBody = response.body();
+
+        if (responseBody == null)
+        {
+            logger.log(Level.SEVERE, "query didn't deliver a body in response");
+
+            if (getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new HttpResponseException("ticket query failed, no response body", response.code());
+        }
+
+        GithubTicketResponse tr;
+        try
+        {
+            tr = mapper.readValue(responseBody.bytes(), GithubTicketResponse.class);
+
+            logger.log(Level.FINER, "parsed response body to ticketResponse instance");
+        } catch (IOException e)
+        {
+            logger.log(Level.SEVERE, String.format("parsing query response FAILED with: %s", e.getMessage()));
+
+            if (getConfigTrue(TicketSystem.ConfigurationOptions.RETURN_NULL_ON_ERROR)) return null;
+            else throw new DeserializationException(e);
+        }
+
+        return GithubTicket.fromTicketResponse(this, tr);
+    }
+
+    @Override
+    public boolean hasAssigneeSupport()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean hasDefaultPagination()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean hasLabelSupport()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean hasPaginationSupport()
+    {
+        return true;
+    }
+
+    @Override
+    public boolean hasReturnNullOnErrorSupport()
+    {
+        return true;
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilder.java b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ec93eed92fb4d19a545cd342c903832a5236510
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilder.java
@@ -0,0 +1,94 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.core.TicketSystemBuilder;
+import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class GithubTicketSystemBuilder extends TicketSystemBuilder<GithubTicketSystemBuilder, GithubTicketSystem>
+{
+    private static final Logger logger = Logging.getLogger(GithubTicketSystemBuilder.class.getName());
+
+    protected String acceptHeader;
+    protected String apiKey;
+    protected boolean https;
+    protected String owner;
+    protected String repo;
+    protected String username;
+
+    /**
+     * starts builder process for a {@link GithubTicketSystem} instance
+     */
+    public GithubTicketSystemBuilder()
+    {
+        super();
+
+        acceptHeader = "application/vnd.github.v3+json";
+        https = true;
+
+        logger.log(Level.FINEST, "Builder for Github System instance started");
+    }
+
+    public GithubTicketSystemBuilder withAuthentication(String username, String apiKey)
+    {
+        this.username = username;
+        this.apiKey = apiKey;
+        logger.log(Level.FINEST, "set authentication values");
+        return this;
+    }
+
+    public GithubTicketSystemBuilder withHttp()
+    {
+        this.https = false;
+        logger.log(Level.FINEST, "set http");
+        return this;
+    }
+
+    public GithubTicketSystemBuilder withHttps()
+    {
+        this.https = true;
+        logger.log(Level.FINEST, "set https");
+        return this;
+    }
+
+    public GithubTicketSystemBuilder withOwner(String owner)
+    {
+        this.owner = owner;
+        logger.log(Level.FINEST, String.format("set owner to %s", this.owner));
+        return this;
+    }
+
+    public GithubTicketSystemBuilder withRepo(String repo)
+    {
+        this.repo = repo;
+        logger.log(Level.FINEST, String.format("set repo to %s", this.repo));
+        return this;
+    }
+
+    @Override
+    public GithubTicketSystem build()
+    {
+        if (baseUrl == null
+            || owner == null
+            || repo == null)
+        {
+            String msg = "one of mandatory fields 'baseUrl', 'owner' or 'repo' missing!";
+            throw new AssertionException(msg);
+        }
+
+        return new GithubTicketSystem(
+            acceptHeader,
+            String.format(
+                "%s://%s/repos/%s/%s/issues",
+                (https) ? "https" : "http",
+                baseUrl,
+                owner,
+                repo
+            ),
+            username,
+            apiKey
+        );
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilterTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b73fa636d7daab6351206eafde6b5bc2178e7fda
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubFilterTest.java
@@ -0,0 +1,225 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.exceptions.DeserializationException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpResponseException;
+import okhttp3.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class GithubFilterTest
+{
+    public GithubFilter instance;
+    public Call call;
+    public ArgumentCaptor<Request> requestCaptor;
+    public Response.Builder responseBuilder;
+
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @BeforeEach
+    public void initBeforeEach()
+    {
+        GithubTicketSystem parent =
+            spy(new GithubTicketSystem("someHeader", "https://api.github.com"));
+        instance = spy(new GithubFilter(parent));
+
+        OkHttpClient client = mock(OkHttpClient.class);
+        call = mock(Call.class);
+        requestCaptor = ArgumentCaptor.forClass(Request.class);
+        responseBuilder = new Response.Builder()
+            .request(new Request.Builder().url("http://test.some.tld/").build())
+            .protocol(Protocol.HTTP_1_1)
+            .message("some message");
+
+        doReturn(client).when(instance).getHttpClient();
+        doReturn(call).when(client).newCall(requestCaptor.capture());
+    }
+
+    @Test
+    public void testGetServerError() throws IOException
+    {
+        doReturn(responseBuilder.code(500).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.get());
+    }
+
+    @Test
+    public void testGetClientError() throws IOException
+    {
+        doReturn(responseBuilder.code(400).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.get());
+    }
+
+    @Test
+    public void testGetNullBody() throws IOException
+    {
+        doReturn(responseBuilder.code(200).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.get());
+    }
+
+    @Test
+    public void testGetNoJsonBody() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("somestrangething", MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        assertThrows(DeserializationException.class, () -> instance.get());
+    }
+
+    @Test
+    public void testGetRequestParamsPart1() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("[]".getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        String[] values = new String[]
+            {
+                "bug",
+                "label1",
+                "2",
+                "50",
+            };
+
+        instance
+            .withLabel(values[0])
+            .withLabel(values[1])
+            .setPage(Integer.parseInt(values[2]))
+            .setPageSize(Integer.parseInt(values[3]))
+            .isOpen()
+            .get();
+
+        HttpUrl url = requestCaptor.getValue().url();
+        assertAll(
+            () -> assertEquals(String.format("%s,%s", values[0], values[1]), url.queryParameter("labels")),
+            () -> assertEquals(values[2], url.queryParameter("page")),
+            () -> assertEquals(values[3], url.queryParameter("per_page")),
+            () -> assertEquals("open", url.queryParameter("state"))
+        );
+    }
+
+    @Test
+    public void testGetRequestParamsPart2() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("[]".getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        instance
+            .isClosed()
+            .get();
+
+        HttpUrl url = requestCaptor.getValue().url();
+        assertAll(
+            () -> assertEquals("closed", url.queryParameter("state"))
+        );
+    }
+
+    @Test
+    public void testGetLocalFilter() throws IOException
+    {
+        GithubTicketResponse res = new GithubTicketResponse();
+        res.number = 5;
+        res.title = "some title";
+        res.body = "descriptive text";
+        res.assignees = new HashSet<>();
+        res.state = "open";
+        res.labels = Arrays.stream(new String[]{"unifiedticketing", "bug"})
+            .map(l -> {
+                GithubTicketResponse.Label label = new GithubTicketResponse.Label();
+                label.name = l;
+                return label;
+            })
+            .collect(Collectors.toSet());
+
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode arrayNode = mapper.createArrayNode();
+        arrayNode.add(mapper.valueToTree(res));
+
+        res.number = 8;
+        res.title = "some special title";
+        arrayNode.add(mapper.valueToTree(res));
+
+        res.number = 94;
+        res.body = "description with @username marked";
+        arrayNode.add(mapper.valueToTree(res));
+
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create(arrayNode.toString().getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        List<GithubTicket> result = instance
+            .withTitleMatch("^.*special.*$")
+            .withDescriptionMatch("^.*@username.*$")
+            .get();
+
+        assertAll(
+            () -> assertEquals(1, result.size()),
+            () -> Assertions.assertEquals("94", result.get(0).getId())
+        );
+    }
+
+    @Test
+    public void testGetDeserialization() throws IOException
+    {
+        GithubTicketResponse res = new GithubTicketResponse();
+        res.number = 99;
+        res.title = "some title";
+        res.body = "descriptive text";
+        res.assignees = new HashSet<>();
+        res.state = "open";
+        res.labels = Arrays.stream(new String[]{"unifiedticketing", "feature-request"})
+            .map(l -> {
+                GithubTicketResponse.Label label = new GithubTicketResponse.Label();
+                label.name = l;
+                return label;
+            })
+            .collect(Collectors.toSet());
+
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode arrayNode = mapper.createArrayNode();
+        arrayNode.add(mapper.valueToTree(res));
+
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create(arrayNode.toString().getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        List<GithubTicket> expected = new LinkedList<>(
+            Collections.singleton(GithubTicket.fromTicketResponse(instance.parent, res)));
+        List<GithubTicket> actual = instance.get();
+
+        assertEquals(expected, actual);
+        assertTrue(expected.get(0).deepEquals(actual.get(0)));
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilderTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..265b2399cd091e962a2a1defabd5c34508a356c9
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketBuilderTest.java
@@ -0,0 +1,158 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+import de.hftstuttgart.unifiedticketing.exceptions.DeserializationException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpResponseException;
+import okhttp3.*;
+import okio.Buffer;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class GithubTicketBuilderTest
+{
+    public GithubTicketBuilder instance;
+    public Call call;
+    public ArgumentCaptor<Request> requestCaptor;
+    public Response.Builder responseBuilder;
+
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @BeforeEach
+    public void initBeforeEach()
+    {
+        GithubTicketSystem parent =
+            spy(new GithubTicketSystem("something", "https://example.org"));
+        instance = spy(new GithubTicketBuilder(parent));
+
+        OkHttpClient client = mock(OkHttpClient.class);
+        call = mock(Call.class);
+        requestCaptor = ArgumentCaptor.forClass(Request.class);
+        responseBuilder = new Response.Builder()
+            .request(new Request.Builder().url("http://test.some.tld/").build())
+            .protocol(Protocol.HTTP_1_1)
+            .message("some message");
+
+        doReturn(client).when(instance).getHttpClient();
+        doReturn(call).when(client).newCall(requestCaptor.capture());
+    }
+
+    @Test
+    public void testSaveNoCreationWithoutMinimumRequirements()
+    {
+        assertThrows(AssertionException.class, () -> instance.create());
+        verify(instance, never()).getHttpClient();
+    }
+
+    @Test
+    public void testSaveServerError() throws IOException
+    {
+        doReturn(responseBuilder.code(500).build()).when(call).execute();
+        instance.title("we have a new title");
+        assertThrows(HttpResponseException.class, () -> instance.create());
+    }
+
+    @Test
+    public void testSaveClientError() throws IOException
+    {
+        doReturn(responseBuilder.code(400).build()).when(call).execute();
+        instance.title("we have another new title");
+        assertThrows(HttpResponseException.class, () -> instance.create());
+    }
+
+    @Test
+    public void testSaveNullBody() throws IOException
+    {
+        doReturn(responseBuilder.code(200).build()).when(call).execute();
+        instance.title("we have no body this time");
+        assertThrows(HttpResponseException.class, () -> instance.create());
+    }
+
+    @Test
+    public void testSaveNoJsonBody() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("somestrangething", MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        instance.title("this body is no json");
+        assertThrows(DeserializationException.class, () -> instance.create());
+    }
+
+    @Test
+    public void testSaveSuccessfulUpdate() throws IOException
+    {
+        GithubTicketResponse ticketResponse = new GithubTicketResponse();
+        ticketResponse.title = "title of ticket";
+        ticketResponse.body = "description";
+        ticketResponse.number = 5;
+        ticketResponse.labels = Arrays.stream(new String[]{"unifiedticketing", "feature-request"})
+            .map(l -> {
+                GithubTicketResponse.Label label = new GithubTicketResponse.Label();
+                label.name = l;
+                return label;
+            })
+            .collect(Collectors.toSet());
+        GithubTicketResponse.Assignee assignee = new GithubTicketResponse.Assignee();
+        assignee.id = 234;
+        assignee.login = "username";
+        ticketResponse.assignees = new HashSet<>(Collections.singleton(assignee));
+
+        GithubTicket expected = GithubTicket.fromTicketResponse(instance.parent, ticketResponse);
+        String responseJson = new ObjectMapper().writeValueAsString(ticketResponse);
+
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create(responseJson.getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        GithubTicket actual = instance.title("some title").create();
+        assertEquals(expected, actual);
+        assertTrue(expected.deepEquals(actual));
+    }
+
+    @Test
+    public void testSaveRequestJson() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("{}".getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        instance.title("title of ticket")
+            .description("description")
+            .labels(new HashSet<>(Arrays.asList("bug", "unifiedticketing")))
+            .assignees("somebody")
+            .create();
+
+        Buffer buffer = new Buffer();
+        ObjectMapper mapper = new ObjectMapper();
+        requestCaptor.getValue().body().writeTo(buffer);
+        String expectedJson = "{\"assignees\":[\"somebody\"],\"body\":\"description\"," +
+            "\"title\":\"title of ticket\",\"labels\":[\"bug\",\"unifiedticketing\"]}";
+        assertEquals(mapper.readTree(expectedJson), mapper.readTree(buffer.readUtf8()));
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponseTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce8dac94b8bd8c6d0458ba7f92fd8154164ed39c
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketResponseTest.java
@@ -0,0 +1,48 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class GithubTicketResponseTest
+{
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @Test
+    public void testDeserialization() throws IOException
+    {
+        ObjectMapper mapper = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        String[] values = new String[]
+            // title                descr       iid     label   label           userid  username
+            {"title of ticket", "description", "5", "bug", "unifiedticketing", "234", "username"};
+        String responseJson = String.format(
+            "{\"title\":\"%s\",\"body\":\"%s\",\"number\":%s," +
+                "\"labels\":[{\"name\":\"%s\"},{\"name\":\"%s\"}],\"assignees\":[{\"id\":%s,\"login\":\"%s\"}]}",
+            (Object[]) values);
+
+        GithubTicketResponse response = mapper.readValue(responseJson.getBytes(), GithubTicketResponse.class);
+        assertAll(
+            () -> assertEquals(values[0], response.title),
+            () -> assertEquals(values[1], response.body),
+            () -> assertEquals(values[2], String.valueOf(response.number)),
+            () -> assertTrue(response.labels.stream().map(l -> l.name).collect(Collectors.toSet()).contains(values[3])),
+            () -> assertTrue(response.labels.stream().map(l -> l.name).collect(Collectors.toSet()).contains(values[4])),
+            () -> assertEquals(1, response.assignees.size()),
+            () -> assertEquals(values[5], String.valueOf(response.assignees.stream().findFirst().get().id)),
+            () -> assertEquals(values[6], response.assignees.stream().findFirst().get().login)
+        );
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilderTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef5199a075226cbf147544cd9ef6e2759127c6c7
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemBuilderTest.java
@@ -0,0 +1,120 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.logging.Level;
+import java.util.regex.Pattern;
+
+public class GithubTicketSystemBuilderTest
+{
+    public GithubTicketSystemBuilder instance;
+
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @BeforeEach
+    public void initBeforeEach()
+    {
+        instance = Mockito.spy(new GithubTicketSystemBuilder());
+    }
+
+    @Test
+    public void testInit()
+    {
+        assertAll(
+            () -> assertEquals("application/vnd.github.v3+json", instance.acceptHeader),
+            () -> assertNull(instance.apiKey),
+            () -> assertNull(instance.owner),
+            () -> assertNull(instance.repo),
+            () -> assertTrue(instance.https),
+            () -> assertNull(instance.username)
+        );
+    }
+
+    @Test
+    public void testWithApiKey()
+    {
+        String username = "someUser";
+        String apiKey = "asoashdboahasjhdfbeuazhvfef4q34v3h4v435v2";
+        instance.withAuthentication(username, apiKey);
+        assertAll(
+            () -> assertEquals(username, instance.username),
+            () -> assertEquals(apiKey, instance.apiKey)
+        );
+    }
+
+    @Test
+    public void testWithHttp()
+    {
+        instance.https = true;
+        instance.withHttp();
+
+        assertFalse(instance.https);
+    }
+
+    @Test
+    public void testWithHttps()
+    {
+        instance.https = false;
+        instance.withHttps();
+        assertTrue(instance.https);
+    }
+
+    @Test
+    public void testWithProjectIdByInt()
+    {
+        String expected = "255254123";
+        instance.withRepo(expected);
+
+        assertEquals(expected, instance.repo);
+    }
+
+    @Test
+    public void testBuild()
+    {
+        instance.withBaseUrl("blabla.tld");
+        assertThrows(AssertionException.class, () -> instance.build());
+
+        String apiKey = "aaesfsef32qqfq3f";
+        String baseUrl = "api.github.com";
+        String owner = "someOwner";
+        String repo = "42";
+        String username = "someUsername";
+        instance = new GithubTicketSystemBuilder();
+        instance.withOwner(owner);
+        instance.withRepo(repo);
+        assertThrows(AssertionException.class, () -> instance.build());
+
+        instance = new GithubTicketSystemBuilder();
+        String regex = "^%s://%s/repos/%s/%s/issues$";
+        Pattern pattern = Pattern.compile(
+            String.format(regex, "https", baseUrl, owner, repo));
+
+        instance.withBaseUrl(baseUrl)
+            .withOwner(owner)
+            .withRepo(repo);
+        GithubTicketSystem system = instance.build();
+        assertTrue(pattern.matcher(system.baseUrl).matches());
+
+        pattern = Pattern.compile(String.format(regex, "http", baseUrl, owner, repo));
+        instance
+            .withHttp()
+            .withAuthentication(username, apiKey);
+        system = instance.build();
+        assertTrue(pattern.matcher(system.baseUrl).matches());
+        assertEquals(username, system.username);
+        assertEquals(apiKey, system.apiKey);
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8ee03a995566abc5c3ae49c49703ecf697145fd
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketSystemTest.java
@@ -0,0 +1,198 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.core.TicketSystem;
+import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+import de.hftstuttgart.unifiedticketing.exceptions.DeserializationException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpResponseException;
+import okhttp3.*;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class GithubTicketSystemTest
+{
+    public GithubTicketSystem instance;
+    public Call call;
+    public ArgumentCaptor<Request> requestCaptor;
+    public Response.Builder responseBuilder;
+
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @BeforeEach
+    public void initBeforeEach()
+    {
+        instance = spy(new GithubTicketSystem("someHeader", "https://api.github.com"));
+
+        OkHttpClient client = mock(OkHttpClient.class);
+        call = mock(Call.class);
+        requestCaptor = ArgumentCaptor.forClass(Request.class);
+        responseBuilder = new Response.Builder()
+            .request(new Request.Builder().url("http://test.some.tld/").build())
+            .protocol(Protocol.HTTP_1_1)
+            .message("some message");
+
+        doReturn(client).when(instance).getHttpClient();
+        doReturn(call).when(client).newCall(requestCaptor.capture());
+    }
+
+    @Test
+    public void testFromUri()
+    {
+        String base = "api.github.com";
+        String owner = "myOwner";
+        String repo = "myRepo";
+        String username = "someUser";
+        String apikey = "afdaf3aqraf3afafmyxcbvmyxvbas3wrawra";
+        GithubTicketSystem actual = (GithubTicketSystem) TicketSystem.fromUri(buildTestUri(true, base, owner, repo, username, apikey));
+
+        assertEquals(buildFinalBaseurl(true, base, owner, repo), actual.baseUrl);
+        assertEquals(username, actual.username);
+        assertEquals(apikey, actual.apiKey);
+        assertEquals(GithubTicketSystem.class, actual.getClass());
+
+        base = "api.github.somedomain.tld:8080";
+        owner = "otherOwner";
+        repo = "otherRepo";
+        username = "me";
+        apikey = "a34adfae3ra3rasd";
+        actual = (GithubTicketSystem) TicketSystem.fromUri(buildTestUri(false, base, owner, repo, username, apikey));
+
+        assertEquals(buildFinalBaseurl(false, base, owner, repo), actual.baseUrl);
+        assertEquals(username, actual.username);
+        assertEquals(apikey, actual.apiKey);
+        assertEquals(GithubTicketSystem.class, actual.getClass());
+
+        actual = (GithubTicketSystem) TicketSystem.fromUri(buildTestUri(false, base, owner, repo, null, null));
+        assertEquals(buildFinalBaseurl(false, base, owner, repo), actual.baseUrl);
+        assertNull(actual.username);
+        assertNull(actual.apiKey);
+        assertEquals(GithubTicketSystem.class, actual.getClass());
+
+        assertThrows(AssertionException.class, () -> TicketSystem.fromUri("unifiedticketing:github:blablabal"));
+    }
+
+    @Test
+    public void testCreateTicket()
+    {
+        assertEquals(GithubTicketBuilder.class, instance.createTicket().getClass());
+        assertSame(instance, instance.createTicket().parent);
+    }
+
+    @Test
+    public void testFind()
+    {
+        assertEquals(GithubFilter.class, instance.find().getClass());
+        assertSame(instance, instance.find().parent);
+    }
+
+    @Test
+    public void testGetServerError() throws IOException
+    {
+        doReturn(responseBuilder.code(500).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.getTicketById("bla"));
+    }
+
+    @Test
+    public void testGetClientError() throws IOException
+    {
+        doReturn(responseBuilder.code(400).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.getTicketById("bla"));
+    }
+
+    @Test
+    public void testGetNullBody() throws IOException
+    {
+        doReturn(responseBuilder.code(200).build()).when(call).execute();
+        assertThrows(HttpResponseException.class, () -> instance.getTicketById("bla"));
+    }
+
+    @Test
+    public void testGetNoJsonBody() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("somestrangething", MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        assertThrows(DeserializationException.class, () -> instance.getTicketById("bla"));
+    }
+
+    @Test
+    public void testGetTicketById() throws IOException
+    {
+        GithubTicketResponse res = new GithubTicketResponse();
+        res.number = 99;
+        res.title = "some title";
+        res.body = "descriptive text";
+        res.assignees = new HashSet<>();
+        res.state = "open";
+        res.labels = Arrays.stream(new String[]{"unifiedticketing", "feature-request"})
+            .map(l -> {
+                GithubTicketResponse.Label label = new GithubTicketResponse.Label();
+                label.name = l;
+                return label;
+            })
+            .collect(Collectors.toSet());
+
+        ObjectMapper mapper = new ObjectMapper();
+
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create(mapper.writeValueAsString(res), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        GithubTicket expected = GithubTicket.fromTicketResponse(instance, res);
+        GithubTicket actual = instance.getTicketById(res.number);
+
+        assertEquals(expected, actual);
+        assertTrue(expected.deepEquals(actual));
+    }
+
+    @Test
+    public void testSupport()
+    {
+        assertAll(
+            () -> assertTrue(instance.hasAssigneeSupport()),
+            () -> assertTrue(instance.hasDefaultPagination()),
+            () -> assertTrue(instance.hasLabelSupport()),
+            () -> assertTrue(instance.hasPaginationSupport()),
+            () -> assertTrue(instance.hasReturnNullOnErrorSupport())
+        );
+    }
+
+    private static String buildTestUri(boolean https, String base, String owner, String repo, String username, String apikey)
+    {
+        return String.format(
+            "unifiedticketing:github:%s://%s::%s:%s%s",
+            (https) ? "https": "http",
+            base,
+            owner,
+            repo,
+            (username == null || apikey == null) ? "" : ":" + username + ":" + apikey);
+    }
+
+    private static String buildFinalBaseurl(boolean https, String base, String owner, String repo)
+    {
+        return String.format("%s://%s/repos/%s/%s/issues", (https) ? "https" : "http", base, owner, repo);
+    }
+}
diff --git a/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketTest.java b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..716a892c178d80319d82d39b8a57d9adea374afc
--- /dev/null
+++ b/src/test/java/de/hftstuttgart/unifiedticketing/systems/github/GithubTicketTest.java
@@ -0,0 +1,195 @@
+package de.hftstuttgart.unifiedticketing.systems.github;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hftstuttgart.unifiedticketing.core.Logging;
+import de.hftstuttgart.unifiedticketing.exceptions.AssertionException;
+import de.hftstuttgart.unifiedticketing.exceptions.DeserializationException;
+import de.hftstuttgart.unifiedticketing.exceptions.HttpResponseException;
+import okhttp3.*;
+import okio.Buffer;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class GithubTicketTest
+{
+    public GithubTicket instance;
+    public Call call;
+    public ArgumentCaptor<Request> requestCaptor;
+    public Response.Builder responseBuilder;
+
+    @BeforeAll
+    public static void initBeforeAll()
+    {
+        Logging.setLevel(Level.ALL);
+    }
+
+    @BeforeEach
+    public void initBeforeEach()
+    {
+        instance =
+            spy(new GithubTicket(
+                spy(new GithubTicketSystem("someHeader", "https://example.org"))));
+
+        OkHttpClient client = mock(OkHttpClient.class);
+        call = mock(Call.class);
+        requestCaptor = ArgumentCaptor.forClass(Request.class);
+        responseBuilder = new Response.Builder()
+            .request(new Request.Builder().url("http://test.some.tld/").build())
+            .protocol(Protocol.HTTP_1_1)
+            .message("some message");
+
+        doReturn(client).when(instance).getHttpClient();
+        doReturn(call).when(client).newCall(requestCaptor.capture());
+    }
+
+    @Test
+    public void testFromTicketResponse()
+    {
+        GithubTicketResponse res = new GithubTicketResponse();
+        GithubTicket ticket = GithubTicket.fromTicketResponse(instance.getParent(), res);
+
+        assertAll(
+            () -> assertSame(instance.getParent(), ticket.getParent()),
+            () -> assertNull(ticket.getTitle()),
+            () -> assertNull(ticket.getDescription()),
+            () -> assertEquals("0", ticket.getId()),
+            () -> assertNotNull(ticket.getLabels()),
+            () -> assertTrue(ticket.isOpen()),
+            () -> assertNull(ticket.getTitle()),
+            () -> assertNotNull(ticket.getAssignees())
+        );
+    }
+
+    @Test
+    public void testAddAssigneeByString()
+    {
+        String username = "someUsername";
+        instance.addAssignee(username);
+        assertEquals(1, instance.getAssignees().size());
+        assertEquals(username, instance.getAssignees().stream().findFirst().get().username);
+    }
+
+    @Test
+    public void testRemoveAssigneeByString()
+    {
+        // first add assignee and check it was added properly
+        testAddAssigneeByString();
+
+        String username = instance.getAssignees().stream().findFirst().get().username;
+        instance.removeAssignee(username);
+        assertEquals(0, instance.getAssignees().size());
+    }
+
+    @Test
+    public void testSaveNoHttpClientWithoutChanges()
+    {
+        GithubTicket actual = instance.save();
+        verify(instance, never()).getHttpClient();
+        assertSame(instance, actual);
+    }
+
+    @Test
+    public void testSaveServerError() throws IOException
+    {
+        doReturn(responseBuilder.code(500).build()).when(call).execute();
+        instance.setTitle("we have a new title");
+        assertThrows(HttpResponseException.class, () -> instance.save());
+    }
+
+    @Test
+    public void testSaveClientError() throws IOException
+    {
+        doReturn(responseBuilder.code(400).build()).when(call).execute();
+        instance.setTitle("we have another new title");
+        assertThrows(HttpResponseException.class, () -> instance.save());
+    }
+
+    @Test
+    public void testSaveNullBody() throws IOException
+    {
+        doReturn(responseBuilder.code(200).build()).when(call).execute();
+        instance.setTitle("we have no body this time");
+        assertThrows(HttpResponseException.class, () -> instance.save());
+    }
+
+    @Test
+    public void testSaveNoJsonBody() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("somestrangething", MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        instance.setTitle("this body is no json");
+        assertThrows(DeserializationException.class, () -> instance.save());
+    }
+
+    @Test
+    public void testSaveSuccessfulUpdate() throws IOException
+    {
+        GithubTicketResponse ticketResponse = new GithubTicketResponse();
+        ticketResponse.title = "title of ticket";
+        ticketResponse.body = "description";
+        ticketResponse.number = 5;
+        ticketResponse.labels = Arrays.stream(new String[]{"bug", "unifiedticketing"})
+            .map(l -> {
+                GithubTicketResponse.Label label = new GithubTicketResponse.Label();
+                label.name = l;
+                return label;
+            })
+            .collect(Collectors.toSet());
+        GithubTicketResponse.Assignee assignee = new GithubTicketResponse.Assignee();
+        assignee.id = 234;
+        assignee.login = "username";
+        ticketResponse.assignees = new HashSet<>(Collections.singleton(assignee));
+
+        GithubTicket expected = GithubTicket.fromTicketResponse(instance.getParent(), ticketResponse);
+        String responseJson = new ObjectMapper().writeValueAsString(ticketResponse);
+
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create(responseJson.getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+        GithubTicket actual = instance.close().save();
+        assertEquals(expected, actual);
+        assertTrue(expected.deepEquals(actual));
+    }
+
+    @Test
+    public void testSaveRequestJson() throws IOException
+    {
+        doReturn(
+            responseBuilder
+                .code(200)
+                .body(ResponseBody.create("{}".getBytes(), MediaType.get("application/json")))
+                .build())
+            .when(call).execute();
+
+        instance.setTitle("title of ticket")
+            .setDescription("description")
+            .setLabels(new HashSet<>(Arrays.asList("bug", "unifiedticketing")))
+            .addAssignee("someUsername")
+            .save();
+
+        Buffer buffer = new Buffer();
+        ObjectMapper mapper = new ObjectMapper();
+        requestCaptor.getValue().body().writeTo(buffer);
+        String expectedJson = "{\"assignees\":[\"someUsername\"],\"body\":\"description\"," +
+            "\"title\":\"title of ticket\",\"labels\":[\"bug\",\"unifiedticketing\"]}";
+        assertEquals(mapper.readTree(expectedJson), mapper.readTree(buffer.readUtf8()));
+    }
+}